Commit Graph

15 Commits

Author SHA1 Message Date
filip131311 eea6c27cf0 style: fix prettier formatting on main (#604)
## Summary

`npx prettier --check .` fails on current `main` with one violation:
`packages/argent-tools-client/test/artifacts.test.ts` (a single `expect`
line wrapped where it now fits the 100-char print width). This PR is the
`prettier --write` output for that file — whitespace-only, no behavior
change.

## Test plan

- [x] `npx prettier --check .` passes on the branch (was failing on
`main`)
- [x] Diff is whitespace-only (1 file, 1 reflowed statement)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-31 13:13:12 +02:00
filip131311 fc09612f30 feat(config): recordings.directory — configurable save location for screen recordings (#601)
## Summary

Adds a `recordings.directory` configuration entry (built on the #534
config system) that lets users choose where finished screen recordings
(mp4) are saved, instead of the fixed `<project>/.argent/recordings/`.

- **Schema entry** (`configuration-core`): scopes `project` + `global`,
`prioritize-local` merge (project wins when both are set). Picked up
automatically by `argent config set/get/unset/list`.
- **Value semantics**: absolute path, `~`-prefixed (expands to home), or
relative — relative anchors at the project root (home dir when not in a
project).
- **Client-side wiring** (`argent-tools-client` `durableSaveTarget`):
the wire `saveDir` hint stays `.argent/recordings` (wire contract
unchanged → old/new client-server interop). Past the existing client
allowlist, the recordings hint is remapped to the configured directory.

## Security model unchanged

The allowlist exists because `saveDir` arrives from a possibly-hostile
remote tool-server. That gate still runs first and is unchanged: a
server can only *select* the sanctioned recordings category, never name
a destination. The configured directory comes from the client's own
trusted config files. Exclusive never-overwrite writes (`name (2).mp4`),
the durable download size caps, and the symlink real-path guard all
remain; for a configured dir the realpath check degenerates to "the dir
resolves to itself" (a user-chosen symlink is legitimate, matching how
the default base may be symlinked). A test asserts hostile wire values
(`.git`, `..`, absolute) stay rejected even with config set.

## `argent link` interaction

Materialization always runs on the **client** (co-located → copy from
`hostPath`; remote → download over `/artifacts/:id`), and the config is
read there at materialize time — so with a remote tool-server the mp4
lands on the client host per the *client's* config, on both paths.

## Testing

- New unit tests: 10 in `argent-tools-client` (absolute/`~`/relative
resolution, local-vs-global precedence, both materialization paths,
hostile wire values with config set) + 4 in `configuration-core` (scope
writes, trim, precedence, validation).
- Suites green: tools-client 188, configuration-core 87, tool-server
screen-recording 62, CLI config 16; full workspace vitest 4727 pass (3
pre-existing `scripts/*.test.mjs` node:test files don't parse under root
vitest — unrelated).
- **Live E2E** on an iPhone 15 Pro simulator via the branch tool-server:
real recording captured, then materialized through the branch client
with configs written by the real `argent config` CLI in a sandboxed
HOME/project. 7 scenarios green: default location, global-only (`~`
path), project-only (relative), both-set → project wins (co-located
**and** remote download with Bearer auth), unset fallbacks, `argent
config list` rendering. Remote-downloaded mp4 ffprobe-verified
(byte-exact size, 4.17 s duration); name-collision suffixing observed
live.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-31 12:36:31 +02:00
Ignacy Łątka 98ae93145b feat(screen-recording): save recordings to the project's .argent/recordings (#551)
Stacked on #549 (touch visualizer).

## What

Screen recordings now land durably under **`.argent/recordings/`** on
the client, instead of the disposable temp-artifact cache — and for a
remote `argent link` tool-server they land on the **client** host, not
the server.

## Why

Two rough edges in the recording flow:
1. The finished video was only ever written to the OS temp-artifact
cache, so it was easy to lose and not obviously part of the project.
2. Over `argent link`, the video was downloaded into that same temp
cache; there was no project-scoped home for it.

## How

- New optional `saveDir` hint on the artifact wire contract
(`@argent/registry` + `@argent/tools-client`).
- `screen-recording-stop` tags its video with `saveDir:
".argent/recordings"` and drops the internal `argent-` temp prefix from
the filename.
- The client materializer honours `saveDir`: **copies** the file when
the tool-server is co-located, **downloads** it over `/artifacts/:id`
when it is remote. Either way the bytes end up in the durable directory.
- `saveDir` resolves against the client's **project root** (nearest
ancestor with `.git`/`package.json`/`.argent`), falling back to the
global **`~/.argent`** when run outside a project — matching how the
rest of argent scopes per-project config. Adds `findProjectRoot` to
`configuration-core` (a null-returning variant of `resolveProjectRoot`)
to drive that choice.
- Hardened: absolute paths and `..` escapes are rejected, directory
bundles excluded — a tool-server can't steer a write outside the base
directory.

## Verification

- Unit: 28 cases in `argent-tools-client/test/artifacts.test.ts`
(co-located copy, remote download, project-root anchoring incl. from a
subdirectory, global fallback, path-traversal rejection, integrity).
configuration-core suite green (55).
- E2E against a booted iOS 18.5 sim + a worktree tool-server, driving
the real client materializer path:
- in a project, co-located server →
`<projectRoot>/.argent/recordings/…mp4` (valid h264)
- in a project, forced remote download (`GET /artifacts/:id`) → same
durable dir, temp cache left empty
  - outside a project (HOME redirected) → `~/.argent/recordings/…mp4`
- Root `tsc --build`, prettier, and eslint all clean.

MoQ / `sim-remote` recording support lands in a separate PR stacked on
this one.

---------

Co-authored-by: Ignacy Łątka <claude-hubert.gancarczyk@swmansion.com>
2026-07-23 22:43:01 +02:00
filip131311 07427917e2 fix(launcher,lens): drop the npm postinstall — its jobs move to runtime (#510)
## Why

Shipping *any* `postinstall` makes pnpm 10+ wall the install behind its
build gate: `ERR_PNPM_IGNORED_BUILDS`, `pnpm install` exits 1, and `pnpm
exec argent` is unusable until the user runs `pnpm approve-builds` —
their first experience of argent is a bricked CLI. The same script is
also silently skipped by `--ignore-scripts`, Yarn PnP, and locked-down
CI, so anything correctness-relevant in it was already unreliable (the
codebase says as much in several places, e.g. `runTrustingDisk`).

Live-verified during the 0.15.0 QA: with **all builds declined** in pnpm
11, argent installs, inits, and serves MCP fine — the postinstall was
never load-bearing for install correctness. Removing it means pnpm users
never see the approve-builds wall for argent itself.

## What the postinstall did, and where it went

| Job | New home |
|---|---|
| SIGTERM a tool-server stranded by an upgrade that replaced its bundle
dir (pnpm/yarn version-pinned layouts) | `sweepDeadStateFiles` in the
launcher's slow path now retires **live** servers whose recorded bundle
is gone from disk, with the same pid-identity guard as
`killToolServerForInstallDir` (never signal a recycled pid; keep
unidentifiable live records reachable for `server stop`). Same-bundle
in-place bumps were already self-healed by the disk-authoritative
version gate. |
| `chmod +x` node-pty's macOS `spawn-helper` prebuilds (shipped without
the exec bit; first `pty.spawn` otherwise fails `posix_spawnp`) |
`ensureSpawnHelperExecutable()` in `lens-pty.ts`, run when the module
loads, best-effort with the AppleScript fallback intact. |
| Print the `argent init` hint | Dropped — init flows drive the install,
and the hint never printed on pnpm/PnP anyway. |

`ARGENT_SKIP_POSTINSTALL` in `update.ts` is kept deliberately:
downgrades and pinned installs fetch older versions whose postinstall
still exists and honors it.

## Testing

- New `launcher-sweep.test.ts` (6 tests) exercises the sweep against
**real processes**: live-server-with-deleted-bundle is terminated and
its record unlinked; live server with intact bundle untouched;
recycled-pid record kept, not signalled; dead-pid record unlinked;
legacy slot untouched; a SIGTERM-trapping orphan doesn't hold the sweep
(and the spawn lock above it) hostage for its kill grace window — the
record is unlinked and the SIGKILL escalation runs in the background.
- New `ensureSpawnHelperExecutable` tests (real fs, darwin-gated) verify
the +x restore and silent failure; a regression test pins that
`loadNodePty` tolerates req seams without `resolve`.
- Full suites green in all touched packages: tools-client 154, installer
535, cli 248, mcp 76, argent 60.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-15 20:26:39 +02:00
Jakub Piasecki 551c781fb0 feat(flow): support running end-to-end flows without agent (#429)
# Flow E2E

This PR turns flows from replayable tool-call recordings into a
declarative, portable e2e test system that runs headlessly via `argent
flow run <name>` — no LLM in the loop, non-zero exit on failure,
suitable for CI.

Agents can still replay flows via `flow-execute` and get the full output
from each step after it finishes.

## Flow format

Flows are YAML files in `.argent/flows/`, now built from **directives**
interpreted by the runner rather than raw tool calls:

- `launch` — terminate + relaunch the app under test (per-platform app
id map supported); its presence marks a flow as **e2e** (controls its
own start state) vs. a **fragment** (runs against current state,
declares an `executionPrerequisite`, composable via `run:` with
cycle/depth guards).
- `tap`, `type` (focus handshake + optional submit), `scroll-to`
(momentum-free, increments by half the container, `within:` for nested
scrollers), `await`/`assert` (`visible`/`exists`/`hidden`/`text`
conditions), `wait`, `snapshot`, `echo`, and raw `tool:` as escape hatch
— still fully supported, so previously recorded flows keep working
unchanged.
- **Selectors** are `{ text, identifier, role }`; a bare string is a
loose selector resolving identifier-first, then visible text. Any
directive failure hard-stops the flow; later steps report `skip`.

## Execution engine

- Flows store no device id — the runner binds a device at replay
(explicit `--device`/`--platform` or the single booted one).
- Selector resolution runs against **platform-unified UI trees**: iOS
native view hierarchy (full testID coverage, gated on native-devtools
connecting), Android full accessibility hierarchy (full resource-id
coverage), Chromium CDP DOM, and the Vega automation toolkit. A shared
flatten/text-hoisting pass lets `text` assertions read what an
identified container visibly shows. Vega is remote-driven, so touch
directives fail with guidance to use `tv-remote`/`keyboard`.
- Steps wait for the tree to **settle** (two consecutive identical
reads) before acting; `launch` additionally gates on platform
tree-source readiness instead of silently degrading.

## Visual snapshots

`snapshot` diffs a full-resolution screenshot against baselines in
`.argent/flows/__baselines__/<flow>/`, keyed by platform + resolution,
with configurable `maxMismatch` and `--update-baselines`. The status bar
is pinned for the whole run (iOS `simctl status_bar`, Android demo mode)
so clocks/battery don't drive diffs. Baseline/current/diff images are
returned as artifacts on failure (and the written baseline on first
capture/update); a clean pass carries none, keeping reports and agent
context lean.

## Recording

The record-live workflow (`flow-start-recording` → `flow-add-step` →
`flow-finish-recording`) now emits directives automatically: coordinate
taps are rewritten to portable selector `tap:` steps (selector captured
from the pre-tap tree), a leading `restart-app` becomes `launch:`,
`flow-execute` of a sibling fragment becomes `run:`, and device ids are
stripped. The skill instructs the agent to polish the saved YAML
afterwards (fold tap+keyboard into `type:`, swipes-to-reach into
`scroll-to:`, awaits into sugar form).

## Also included

- `argent flow run` / `argent flow list` CLI with per-step report
rendering, artifact materialization, and `--json` output.
- New structured failure codes for flow validation, device resolution,
composition, and devtools readiness.
- Extensive unit test coverage (tree adapters, selector matching,
scrolling, composition, type focus, snapshots, status bar).
2026-07-13 17:15:01 +02:00
Gabe Debes 10a78d0527 feat(file-inputs): add tar-upload kind for streaming remote file/directory uploads (#443)
## What

A new `tar-upload` file-input kind so a tool that reads a local file or
directory (e.g. `reinstall-app`'s app bundle) works when the tool-server
runs on a different machine than the caller.

## Why

`reinstall-app` assumes the bundle is on the tool-server's filesystem.
That breaks with a cloud-hosted simulator — the bundle is on your
machine, not the server's. The existing `file` kind inlines base64 and
is capped at 32 MB, too small for a real `.app`/`.apk`; `tar-upload`
streams instead.

## How

When you're not linked to a remote server, an existing local path is
used in place.

When you are (e.g. `argent link`), the client tars the path, streams it
to `POST /upload`, and the tool call carries `uploadId` plus a SHA-256
`contentHash`. The server always extracts from that upload — even if the
same path already exists on disk — verifies the hash, and vets tar
members before extracting.

The client only uploads when the path is on its own machine (a line on
stderr so you know it's working). Same command local or remote:

```
argent run reinstall-app --udid <udid> --bundleId <id> --appPath ./MyApp.app
```

## Shared extraction

Per review feedback, the tar pack/unpack logic is unified in a small
`@argent/archive` package used by **both** directions — this upload path
and the pre-existing artifact download (`streamDirectoryAsTarGz` /
`downloadAndExtractArchive`). The shared extractor rejects
path-traversal entries, symlinks whose target escapes the extract dir,
and non-file/dir members (hardlinks, devices) — while still allowing a
bundle's internal symlinks. This also hardens the artifact-download
path, which previously extracted without those checks.

## Tests

Client + server unit and HTTP round-trip tests: dir and file upload,
skip-when-not-local, co-located no-op, hash required, path-traversal /
escaping-symlink / hardlink rejection, sidecar handling, single-member
resolution, `/upload` auth + size caps, and artifact download
extraction. All suites pass.

---------

Co-authored-by: Filip131311 <contact@filipkaminski.com>
2026-07-10 12:29:48 +02:00
filip131311 7bd017a796 feat(installer): committable repo-local (--local) install mode (#428)
## Summary

Adds an opt-in **committable / repo-local** install mode so a team can
version Argent _with the repo_. `argent init --local` installs
`@swmansion/argent` as a project **devDependency** and writes MCP
configs that launch the project-local copy — so after `git clone && npm
install`, every teammate has the same Argent version and MCP wiring with
**no global install and no per-developer `argent init`**.

**Global mode stays the default** and is unchanged; everything here is
additive and backward-compatible.

Motivation: the workflow some users already do by hand (#207).
Supersedes the earlier, since-closed #232 — this version is Windows-safe
(`node`, not the `.bin` shim), handles Yarn PnP, and closes the
cross-install tool-server hazards rather than leaving them.

## What changed

**`init`**
- `--local` / `--global` flags + an interactive "How should argent be
installed?" prompt. Mutually exclusive; non-interactive (`--yes`)
defaults to **global**.
- Local mode installs the devDependency with the package manager
detected from the project's **lockfile** (run with `cwd = projectRoot`).
- MCP entry points at the local copy: `node
node_modules/@swmansion/argent/dist/cli.js mcp` (the bin path is derived
from the installed `package.json` `bin` and existence-checked — forward
slashes, committable, Windows-safe). Yarn PnP → `yarn argent mcp`; an
`npx --no-install argent mcp` fallback is used only when the bin can't
be resolved.
- Records the mode in a committed `<root>/.argent/install.json`.
- Drops global-only adapters (Windsurf/Hermes) in local mode; prints
team-setup / commit guidance.

**`update`** — detects the project's mode; in local mode reads the
installed version from `node_modules` (not the global binary / npx
cache) and bumps the **devDependency + lockfile**, then re-emits the
local MCP command.

**`uninstall`** — scoped to the project's mode: a local-mode uninstall
removes the **local devDependency only and never the shared global
install**, and prunes `.argent/install.json`. Global-mode behavior is
unchanged.

**Cross-install hardening (the gate that makes per-project pinning
safe)**
- `launcher`: a tool-server is reused only when its `bundlePath` matches
the caller's — a repo-local pin never silently runs against (or kills) a
different version's server.
- `postinstall`: only stops a tool-server spawned from *this package's
own* bundle, so a devDependency install can't tear down an unrelated
editor session.

**Telemetry** — adds an `install_mode` dimension to
init/update/uninstall events.

## Testing

- New unit tests: install-mode helpers + record (`utils`), mode-aware
`getMcpEntry`/`resolveLocalCommandMode` incl. the opencode array form
(`mcp-configs`), local-only uninstall guard (`uninstall`), and
bundlePath-gated tool-server reuse (`launcher`).
- All existing suites green (installer 346, tools-client 115, telemetry
191, registry 83, argent 13, tool-server 1621). Build + lint clean.
- **Live end-to-end** in an isolated sandbox: `init --local` → real MCP
stdio handshake serves **69 tools from `node_modules`**; `update
--local` reads the local version; `uninstall --local` removes the local
install and **leaves the global install intact**; `init --global`
unchanged; flag conflict rejected.

## Notes / known limitations

- Recommended on mainstream platforms (macOS, Linux x64, Windows x64)
with prebuilt native binaries; Linux arm64 / Windows arm need a C/C++
toolchain to build `tree-sitter` on `npm install` (documented in the
README).
- Concurrent sessions of two *different* Argent versions on one machine
each spawn their own tool-server; an orphaned one idle-times-out (no
session is ever broken).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-08 12:24:19 +02:00
filip131311 ba769f6941 fix(tools-client): unref spawned tool-server's stdout pipe so argent run exits (#473)
## Problem

If the tool-server is **not** already running the first time `argent run
<tool>` is called, the CLI prints its result but **never exits** — it
hangs indefinitely, even when the tool call succeeded.

## Root cause

`spawnToolsServer` (in `packages/argent-tools-client/src/launcher.ts`)
launches the tool-server `detached` with `stdio: ["ignore", "pipe",
logFd]` and calls `child.unref()`. But `child.unref()` only detaches the
**process** handle — the piped `child.stdout` is a *separate*
`net.Socket` handle. After reading the "listening" banner the code calls
`child.stdout.resume()` (so the child doesn't hit EPIPE) but leaves that
socket **ref'd**, which keeps the short-lived caller's event loop alive
forever.

The server-reuse path never opens that pipe, so only the first
(spawning) invocation hangs — exactly matching the report.

## Fix

Unref the stdout socket right after the ready banner is read:

```ts
child.stdout?.resume();
(child.stdout as { unref?: () => void } | null)?.unref?.();
```

Safe for both callers:
- **`argent run`** (short-lived): the loop drains the pipe while
working, then exits. The tool-server already installs an EPIPE handler
on stdout (`packages/tool-server/src/index.ts`), so it tolerates the
parent closing the read end.
- **MCP launcher** (long-lived): its loop stays alive by other means, so
the pipe still drains normally; the unref is a no-op for it.

## Testing

- New regression test `test/launcher-exit.test.ts` — **fails without the
fix**, passes with it.
- End-to-end control: a real subprocess calling the launcher **hangs**
with the unref removed and **exits cleanly** with it in place.
- Full local CI parity: `tsc --build`, `typecheck:tests` (all
workspaces), `typecheck:scripts`, full test suite (12 packages, ~2000
tests) — all green. ESLint + Prettier clean.
2026-07-06 17:13:45 +02:00
Hubert Gancarczyk 6bc5376746 fix(tool-server): stop list-devices from hanging - wedged-VVD stall and CLI socket leak (#419)
## Summary

`list-devices` (an `alwaysLoad` tool called at session start and
frequently after)
could appear to **hang**. Investigation found **two independent
causes**, both fixed here:

1. **Server-side (Vega):** a wedged Vega VVD turned `list-devices` into
a ~40s stall
   and leaked `vega` CLI subprocesses.
2. **Client-side (CLI):** every `argent run …` lingered a constant **~6s
after it had
already printed its result**, because a health-check `fetch` never
drained its
response body and kept an undici keep-alive socket ref'd until the
server closed it.

Net effect for `argent run list-devices`: **6.15s → 0.91s**, and the
worst-case
wedged-VVD stall drops from ~40s to a bounded short call.

---

## Problem 1 — wedged Vega VVD stalls `list-devices` (~40s) and leaks
processes

On a host with the Vega SDK installed and a **running-but-unresponsive
VVD**,
`list-devices`' Vega discovery path made **two serial 20s CLI calls**
that both hung:

- `vega device list` → SIGKILL at 20s
- then (recovery) `vega device info` → SIGKILL at another 20s → **~40s
total**

Three compounding defects:

- `execFileAsync` resolves on stdio **close**, not child **exit**. The
`vega` launcher
forks a `python3 → node → vda` worker tree; an orphaned worker holds the
stdout pipe
open, so the call stayed pending for the full timeout even after the
launcher was
  killed.
- The `device info` recovery fired even when `device list` had **hung**,
doubling the
  cost and spawning a second hanging subprocess.
- On timeout we killed only the **direct child**, so the `python3 → node
→ vda` worker
tree was **orphaned, not reaped** — leaking a process tree per call (a
wedged host had
  accumulated ~269 of them).

### Fix

- **`runVega` (`vega-cli.ts`):** switched from `execFileAsync` to
`spawn` with
**`detached: true`**, making the launcher its own **process-group
leader**, and on
timeout SIGKILL the **whole process group** (`process.kill(-pid)`) —
which reaps the
entire `python3 → node → vda` tree, **not just the launcher**. We also
destroy our
stdio stream ends so a pipe-holding worker can't keep the call pending.
(`execFile`
silently drops `detached`, which is why the switch to `spawn` was
required.)
- **Success path doesn't depend on stdio EOF either:** we prefer `close`
(fully
drained), but if a grandchild holds the stdout pipe open after a **clean
exit**, we
fall back to `exit` + a short (~1s) drain grace and resolve with the
captured
output — so a finished call can't be stalled into the timeout and then
rejected,
discarding valid data. The output cap is measured in real UTF-8 bytes.
For that
guarantee to hold at the boundary, the `exit` handler **disarms the main
timeout
timer**: once the child has terminated the wall-clock deadline is moot,
so a clean
exit landing within the drain grace of the deadline resolves with its
output instead
of being mis-rejected as a `timeout` (which would also wrongly suppress
the
    `listVegaDevices` recovery for a still-running VVD).
- **Belt-and-suspenders for a worker that escapes the group:** an
earlier
investigation reported the worker tree `setsid()`-ing into its own
session (which
would defeat a group-only SIGKILL). To be safe regardless of which is
true, on
timeout we **snapshot the launcher's descendants from the process table
*before*
killing** (a setsid'd worker keeps its `ppid` pointing at the launcher
until the
launcher dies, so a `ppid` tree-walk still reaches it) and SIGKILL those
pids
individually after the group kill. The snapshot is **precise** — only
this
launcher's own descendants, by pid, so a concurrent `vega` call's
workers are never
touched — and bounded, and the group kill runs regardless of whether it
succeeds.
- A real-subprocess test confirms **0 orphans** after a timeout,
including a worker
deliberately spawned into its **own** process group (so the group kill
alone cannot
    reach it — the test fails without the descendant sweep).
- **`listVegaDevices` (`vega-devices.ts`):** skip the `device info`
recovery only when
  `device list` **timed out** (a wedged agent — keyed off the failure's
`error_kind: "timeout"`), so a hung list no longer pays for a second
timeout wait. A
*fast* (non-timeout) list failure still recovers a genuinely-running VVD
rather than
mis-reporting it as stopped. Tightened discovery timeouts (20s → 6s/4s —
a generous
multiple of the ~1s healthy time so a cold-start `vega` launcher isn't
spuriously
timed out, with their sum (10s) kept under the 15s branch deadline with
margin).
- **`list-devices.ts`:** added a 15s `withDeadline` backstop around the
Android and Vega
branches so no single branch can stall the whole fan-out (returns
partial results +
logs a note). Sized to sit ~5s above the Vega branch's ~10s worst case,
so the
backstop never truncates a slow-but-completing branch (which would drop
a real
  device); an invariant test guards that ordering.
- **`readAvdName` (`adb.ts`):** run the two `getprop` probes
concurrently so a wedged
  emulator costs 5s, not 10s.

---

## Problem 2 — `argent run` hangs ~6s after printing output (CLI socket
leak)

Independent of Vega. The tool-server returns `list-devices` in <1s, but
the CLI process
took a **constant ~6s** to exit. Any path that calls `process.exit()`
was instant; any
path that returned naturally (`argent run …`, `argent run … --help`,
`argent tools`)
lingered ~6s.

**Root cause:** `isToolsServerHealthy` (`launcher.ts`) did
`const res = await fetch('/tools'); return res.ok;` and **never drained
the body**.
`/tools` is a large (~112KB) payload, so undici keeps the keep-alive
socket **ref'd**
until the tool-server's idle `keepAliveTimeout` (~5s) closes it.
`ensureToolsServer`
runs this health check on every invocation, and `run()`'s success path
has no
`process.exit(0)`, so the ref'd socket holds the event loop open.
(Confirmed
telemetry-independent — `DO_NOT_TRACK` made no difference.) The same
undrained-body
pattern existed in `link.ts` (`argent link`).

### Fix

Drain the response body before returning in both health-check-style
fetches:

```ts
await res.body?.cancel().catch(() => {});
return res.ok;
```

(`fetchTools`/`callTool` were already fine — they `await res.json()`.)

---

## Testing

**Empirical, end-to-end** (on a machine reproducing the wedged VVD):

| Command | Before | After |
| --- | --- | --- |
| `argent run list-devices` | 6.15s | **0.91s** |
| `argent run list-devices --help` | 6.14s | **0.16s** |
| `argent tools` | 6.15s | **0.16s** |
| handle probe after run | 2 sockets ref'd for ~6s | **none — immediate
exit** |

- Raw `POST /tools/list-devices` was ~0.7s throughout — confirming the
~5.4s was pure
  CLI teardown, not tool work.
- **Process-group reap proven empirically:** the live `vega → bash →
python3 → node`
tree all shares the detached launcher's PGID; a group-SIGKILL left **0
survivors**.
A real-subprocess test (unmocked `runVega` vs. a fake hanging `vega` on
`PATH`) confirms
  it settles at its deadline **and** leaves **0 orphaned processes**.

**Automated:**

- `vega-cli-timeout.test.ts` (real-subprocess test) — `runVega` rejects
on its deadline,
**reaps the entire worker tree (0 orphans)** — the fake's hung worker is
spawned into its
**own** process group, so the test fails without the descendant sweep —
and returns
normally when the CLI responds. Also covers the boundary case: a **clean
exit whose
drain grace outlasts the deadline** resolves with its captured output
rather than being
rejected as a `timeout` (guards the `exit`-handler timer disarm above).
- `vega-adb-connect-recovery.test.ts` — a *timed-out* `device list` does
not trigger a
second `device info` call (no stacking); a *fast, non-timeout* failure
**still** recovers
  a running VVD.
- `list-devices-deadline.test.ts` — `withDeadline` returns the branch
value on the happy
path and the fallback (with a logged note) when a branch exceeds the
deadline.
- `list-devices.test.ts` — added a `spawn` mock (vega moved off
`execFile`).
- `launcher-helpers.test.ts` — `isToolsServerHealthy` cancels (drains)
its response body.
- Suites green: **tool-server (1610), argent-tools-client (109),
argent-cli (84)**.
- eslint (`--max-warnings 0`), prettier, and `tsc` clean.

---
2026-07-01 11:02:33 +02:00
filip131311 5f9dd48e2a fix(tool-server): prevent duplicate tool-servers after an nvm node switch (#385)
## Problem

Switching Node versions with **nvm** can leave **two (or more)
`tool-server` processes alive at once** while
`~/.argent/tool-server.json` tracks only one. The extra servers are
orphaned (untracked, on leaked ports) until their 30‑minute idle
timeout.

This was reproduced live: a machine had **three** `node
…/dist/tool-server.cjs start` processes running, all spawned within the
same minute, with state tracking only the last one.

## Root cause

`@argent/tools-client` `ensureToolsServer()` ran a non-atomic
`read state → check alive+healthy → clearState → spawn → writeState`
sequence with:

1. **No cross-process serialization.** An nvm switch relaunches `argent
mcp` under the new Node while the *previous* MCP's 30s health monitor
reconnects — several launchers run `ensureToolsServer()` at nearly the
same time, all observe "no healthy server", and each spawns its own
**detached** server on a fresh free port. Last‑writer‑wins on the state
file orphans the rest.
2. **No kill-before-respawn.** When the tracked server was alive but
unhealthy, the old code just `clearState()`d and spawned a new one,
leaving the old detached process running untracked.

## Fix (`packages/argent-tools-client/src/launcher.ts`)

- **O_EXCL spawn lock** (`~/.argent/tool-server.lock`) serialises the
spawn decision across processes: per-acquisition nonce, stale‑steal
(dead holder / aged lock), bounded wait, and a hot‑spin‑proof reclaim
loop. Inside the lock, `ensureToolsServer` **double‑checks** for a
peer‑spawned healthy server before spawning, so a burst of concurrent
launches collapses to a single server.
- **Kill-before-respawn** terminates a wedged server we own before
spawning its replacement, with three guards so it can never signal the
wrong process:
- `managed === "autospawn"` — a user's `argent server start` server is
now tagged `managed: "cli"` and is **never** killed by the auto‑spawn
path;
- a **structural** `node <bundlePath> start` command-line identity match
(not a bare substring);
- a re-check of that identity immediately before each signal (covers PID
reuse across the SIGTERM grace window).
- **`spawnToolsServer` reaps its detached child on every reject path**,
so a readiness timeout can no longer leak a child that binds its port
moments later as an untracked second server.

The lock-free fast path (reuse a healthy tracked server) is unchanged,
so steady‑state behaviour and latency are unaffected; the lock is only
taken when a spawn is actually needed.

## Tests

New
`packages/argent-tools-client/test/launcher-duplicate-spawn.test.ts`:

- a 5× concurrent launch burst collapses to **one** server;
- an alive‑but‑unhealthy `autospawn` server is terminated (not orphaned)
on respawn;
- a recycled pid whose command line doesn't match is **never**
signalled;
- a `managed: "cli"` server is left alive instead of being killed;
- a readiness timeout reaps the spawned child (no orphan).

Each case was verified to **fail against the pre-fix code** and pass
with the fix. Existing `launcher-spawn` / `launcher-state` suites are
unchanged and green.

> Note: the `@swmansion/argent` `dispatcher › forwards SIGTERM to the
child binary` unit test is a pre-existing flake (alternates pass/fail
independent of this change, which doesn't touch that package).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Radoniusz Cloudiusz <radoniusz@radoniusz.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 17:20:37 +02:00
filip131311 c01f41cd5f feat(remote): input-side file boundary — flows, screenshot-diff, profiler reports, workspace tools (#319)
## Why

PR #278 fixed the **output** half of the remote file boundary: files a
tool *produces* travel to the client as `ArtifactHandle`s. But seven
tools still break when the tool-server runs on a different machine than
the agent, in one (or both) of two ways:

- **Output-side leftovers** — `native-profiler-analyze` and
`screenshot-diff` still returned raw host paths (and embedded them in
report/summary prose), so the agent can't open them remotely.
- **Input-side gap** — nothing moved a file the *agent* owns onto the
tool-server. Any tool taking a client-local path as a parameter
(`screenshot-diff` baselines, the three flow tools' `project_root`,
`react-profiler-component-source`, `gather-workspace-data`) silently
read/wrote the wrong machine's disk. Flows were fully broken in remote
mode: the YAML landed on the server's disk under the agent's path.

## What

### Output side (apply the existing pattern)
- **native-profiler-analyze** — `reportFile` is now an `ArtifactHandle`
(mirrors `react-profiler-analyze`'s `fileArtifact()`); the inline report
now says *"use the Read tool on the `reportFile` path in this result"*
instead of embedding the server path.
- **screenshot-diff** — `diffPath` / `contextDiffPath` are now
`ArtifactHandle`s; the MCP adapter renders the context-diff image from
materialized bytes (legacy string-path results from older servers still
render co-located). The summary references the result fields instead of
embedding host paths.

### Input side (new, symmetric mechanism)
A declarative **file-input boundary**, mirroring the artifact gate in
the opposite direction:

- A tool declares `fileInputs: [{ target, path: "${param}…", kind }]` on
its `ToolDefinition`; the declaration ships to the client via `GET
/tools`.
- Before sending a call, the client (`@argent/tools-client`, shared by
CLI and MCP) interpolates each template from the args and replaces the
target arg with a wrapper: `{ __argentFileInput, path, size, mtimeMs,
content? }`. File bytes are base64-inlined **only when routed to an
external tool-server** (`argent link` / `ARGENT_TOOLS_URL`) — unlinked
local calls never pay for encoding.
- The server resolves wrappers **before zod validation**, so tools
always execute against a plain local path:
- path matches on the server's own filesystem (existence for dirs,
size+mtime for files) → used **in place**, zero copies — the exact dual
of the artifact materializer's co-located gate;
- otherwise `kind: "file"` content is materialized into a temp file
(with size-integrity check, 32 MB cap);
- `kind: "directory"` (trees that can't ride in a call) → 422 with
actionable remote-mode guidance;
- `kind: "probe"` → never fails; the tool learns host-presence via
`ctx.fileInputs` and adapts.
- The reverse channel: a tool whose output belongs in the **agent's
project** returns a `__argentClientFile` directive (path + content); the
client writes it and rewrites the directive to the path. Writes are
constrained to `**/.argent/flows/<safe-name>.yaml` so a tool-server
can't direct writes anywhere else on the client machine.

### Tool migrations
| Tool | Boundary use |
|---|---|
| `flow-start-recording` | `probe` on `project_root`: present → host
persistence exactly as before; absent (remote) → recording kept in
server memory, YAML returned as a client-write directive |
| `flow-add-step` / `flow-add-echo` / `flow-finish-recording` | follow
the session's persist mode; every mutation returns `savedTo` (path
locally, directive remotely) so the agent-side file is always current |
| `flow-execute` / `flow-read-prerequisite` | `file` input derived from
`${project_root}/.argent/flows/${name}.yaml` into an internal
`flow_file` param — read in place locally, uploaded when remote |
| `screenshot-diff` | `baselinePath`/`currentPath` as `file` inputs;
`outputDir` now **optional** (temp default) and probe-ignored when
absent on the host |
| `react-profiler-component-source`, `gather-workspace-data` |
`directory` gate — unchanged locally; clear error remotely instead of a
silent empty AST index / all-nulls snapshot |

### Version skew
Both halves degrade to today's behavior: an old client sends plain
strings (the resolver passes them through, tools take their legacy
paths), and an old server doesn't advertise `fileInputs` (the new client
never wraps). Wrappers on undeclared params fail the tool's own schema
validation, so nothing can be smuggled through free-form args.

## Tests

**Unit** (all green: registry 68, tool-server 992, tools-client 107,
argent-mcp 66, argent-cli 117):
- `tool-server/test/file-inputs.test.ts` — in-place gate, stat-mismatch
→ upload, truncated-upload rejection, directory gate, probe semantics,
undeclared-target smuggling.
- `argent-tools-client/test/file-inputs.test.ts` — wrapping (stat-only
vs inlined content), multi-param template derivation, explicit-override
respect, directive writes incl. path-validation refusals (relative,
traversal, outside `.argent/flows`, bad charset/extension).
- `tool-server/test/flows/flow-remote-recording.test.ts` — remote
recording stays in memory + returns directives, nothing written on the
host, replay/read from a boundary-resolved `flow_file`.
- Updated existing screenshot-diff / native-profiler tests to the
artifact-handle contract.

**Local / co-located (unlink scenario)** — isolated tool-server + CLI on
an iPhone 17 Pro simulator: full flow
record→finish→read-prerequisite→execute loop (YAML written via the same
path as before, in-place gate hit, no byte copies); screenshot-diff with
and without `outputDir`; wire-level checks of the unlinked client shape
(wrapper without content → in-place read) and the legacy plain-string
shape; missing-file → 422 with a precise message.

**Remote (Lima Linux VM as the agent machine, tool-server on the host
Mac, `--host` bind + bearer auth)** — via both the CLI and the real MCP
stdio server:
- flow recording from the VM: YAML lands **in the VM's project**,
`savedTo` rewritten to the VM path, *no* agent-path directories created
on the host; replay from the VM uploads the YAML and drives the host
simulator.
- `screenshot` materializes into the VM temp cache; `screenshot-diff`
round-trips both directions (VM baseline uploaded, live capture on host,
diff artifacts downloaded back; MCP returns `image,text` content with
the context diff inlined).
- `gather-workspace-data` / `react-profiler-component-source` with
VM-only paths → the new actionable 422.

## Notes
- `native-profiler-analyze` was not exercised live (xctrace export is
unusable on this machine's iOS 26.4 simulators — known, unrelated); its
change is the same one-line `fileArtifact` pattern
`react-profiler-analyze` already ships, covered by unit tests.
- `react-profiler-analyze`'s own `project_root` (AST resolve stage) is
left as-is deliberately: it degrades gracefully (report still renders)
and gating it would break currently-working remote analysis. Tracked as
follow-up alongside nested invocations (`run-sequence`/`flow-execute`
steps bypass the HTTP boundary by design).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:14:04 +02:00
filip131311 20cb452fd4 feat(remote): artifact transport for files produced by a remote tool-server (#278)
## Why

Files produced on the **tool-server host** (screenshots, profiler
exports) were surfaced to the agent as host paths / `127.0.0.1` media
URLs, which the MCP client (`argent-mcp`) then fetched a *second* time
to get the bytes. When the tool-server runs **remotely**, that localhost
URL resolves to the client's own machine — nothing there — so
screenshots fail with `(Screenshot unavailable: no valid PNG …)`.
Profiler exports have the same shape: paths that only exist on the
remote host.

## What

A reusable **artifact boundary layer** so any file-producing tool works
regardless of where the tool-server runs:

**Foundation**
- **tool-server** (`src/artifacts.ts`, `http.ts`): in-process artifact
registry + `GET /artifacts/:id` streaming route.
`registerArtifact(hostPath)` returns a wire handle (`{ __argentArtifact,
id, filename, mimeType, size }`) instead of a raw path.
- **argent-mcp** (`src/artifacts.ts`, `content.ts`, `mcp-server.ts`): a
client materializer deep-walks tool results for handles, downloads each
over the remote-aware `TOOLS_URL`, writes it into a **structured
cache**, and rewrites the result to the **local** path (+ inline image
rendering).

**Cache layout** (override root with `ARGENT_ARTIFACTS_DIR`):
```
~/.argent/artifacts/<project>/<session>/<device>/<filename>
```
- `project` = `basename(cwd)` + short hash of the full path (readable +
collision-safe)
- `session` = minted once per argent-mcp process (groups a run, easy GC)
- `device` = udid/serial when device-scoped, omitted otherwise

**Consumers wired**
- **screenshot** → returns an image artifact; rendered inline from
downloaded bytes with **no second `127.0.0.1` fetch**. Fixes
`auto-screenshot` and `flow-execute` screenshots via the same path.
- **native-profiler-stop** → `exportedFiles` (XML) become downloadable
artifacts materialized client-side.

## Deferred (follow-ups)
- **`.trace` bundle** — it's an Instruments bundle (a directory);
streaming needs archiving (no archiver dep yet). `native-profiler-stop`
now returns a `traceFileNote` explaining it stays on the profiling host.
- **Project source on remote** — `react-profiler-component-source` /
`react-profiler-analyze` read the user's source tree on the tool-server
host. The profiler analysis chains already work remotely
(server-internal); tracked separately.

## Compatibility
Legacy `{ url, path }` screenshot rendering is retained as a fallback
when no artifact context is present (older tool-servers / unit tests).

## Tests
- tool-server: artifact registry (infer/override), `GET /artifacts/:id`
(200/404/410), updated `stop-recovery` assertions.
- argent-mcp: materializer
(download/rewrite/nested/failure/device-scoping), artifact-aware
`toMcpContent`.
- argent-mcp typecheck + full suite green; tool-server artifact/profiler
tests green. (Note: a full local tool-server typecheck trips a
pre-existing zod-v4-vs-v3 mismatch in `run-sequence` that's unrelated to
this change and resolves on this branch's own lockfile.)

## Notes
Routes around the `argent-private` native simulator-server binary (which
emits the `127.0.0.1` media URL) rather than changing it — no submodule
change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: pFornagiel <pawel.fornagiel2@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Paweł Fornagiel <85541429+pFornagiel@users.noreply.github.com>
2026-06-10 12:04:30 +02:00
filip131311 8efd7c6346 feat: add argent server start for long-lived tool-server (#182)
## Human summary: 

This adds `argent server start` command to enable us to spawn tools
server in arbitrary location. we want this to enable using remote
tools-server via our mcp not only local ones.

## Summary

- Adds `argent server start` to spawn a tool-server that does **not**
auto-shutdown on idle, with configurable bind port and host — the
missing piece for running tool-server on a remote/dedicated host.
- Foreground by default so process supervisors (systemd, Docker,
supervisord) own the lifecycle; `--detach` keeps the existing
daemon-spawn behavior. Refuses to clobber a healthy running server
unless `--force`.
- Phase 2 (consumer-side `--tools-url` flag pointing the MCP / CLI at a
remote tool-server) is now a one-flag change away — `tools-client.ts`
and `mcp-server.ts` already honor `ARGENT_TOOLS_URL`. Out of scope for
this PR.

## What changed

- **`tool-server`** — read `HOST` env var (default `127.0.0.1` so the
existing auto-spawn path is unaffected), bind to it, attach an `error`
listener on `app.listen` so `EADDRINUSE` / `EACCES` exit cleanly instead
of routing through `uncaughtException`.
- **`argent-tools-client`** — broaden the readiness regex to match any
host; extend `buildToolsServerEnv` with optional `{ host,
idleTimeoutMinutes }`; record `host` in `~/.argent/tool-server.json`
(backward-compat: missing field defaults to `127.0.0.1`); export
`spawnToolsServer`, `findFreePort`, `isToolsServerHealthy`,
`isToolsServerProcessAlive`, the state read/write/clear helpers, and
`formatToolsServerUrl` so the CLI can reuse them.
- **`argent-cli`** — new `server start [flags]`: `--port`/`-p` (default
3001, `0` = pick free), `--host` (default `127.0.0.1`), `--idle-timeout`
(default `0` = never), `--detach`/`-d`, `--force`, `--help`. Warns when
host is non-loopback. Foreground mode forwards `SIGINT`/`SIGTERM` to the
child and writes the state file so local `argent run` / `argent mcp` can
attach.
- **`argent` dispatcher** — pass `BUNDLED_RUNTIME_PATHS` to `server()`
so the new subcommand can find the bundled tool-server.

## Test plan

- [x] `argent server --help` lists the new `start` subcommand
- [x] `argent server start --help` shows full flag reference
- [x] `argent server start --detach --port 0` → status alive/healthy,
`/tools` reachable, state file populated with `host` field
- [x] Second `argent server start --detach` refuses (`exit 1`) with
helpful message
- [x] `argent server start --detach --force` kills the old PID and
starts a new one
- [x] Foreground `argent server start --port 0` runs with inherited
stdio; child PID is the actual tool-server (not the wrapper); `argent
server stop` from a sibling shell tears down both
- [x] Existing auto-spawn path (`argent tools`) still works — verifies
the regex broadening + default-host change is backward-compatible
- [x] `tool-server` (444 tests) + `argent-tools-client` (1) + `argent`
dispatcher (1) suites pass
- [ ] Manual smoke on `--host 0.0.0.0` — could not exercise locally
(sandbox declined the public-interface bind); the warning + bind path is
identical to loopback besides the listen address

---------

Co-authored-by: pFornagiel <pawel.fornagiel2@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Paweł Fornagiel <85541429+pFornagiel@users.noreply.github.com>
2026-06-03 17:44:15 +02:00
Ignacy Łątka 74c2065ca0 fix: multiple minor bugs (#194)
## Human Summary

This PR aims to fix bugs that are benign for now, could become serious
in the future:

- multiple CSRF via CORS misconfiguration on `tool-server`
- multiple SSRF that were possible due to the CSRF
- path traversal via flow creator
- multiple shell injections
- lack of auth on `MCP/CLI <-> tool-server` path (bug details discussed
in person). After the other fixes this is somewhat redundant, but
doesn't hurt and protects against theoretical access attempts from
within other processes running on the user's device.

## AI Summary

<details>
Bundles six independent fixes that together close a CSRF cluster on the
local tool-server. The vulnerabilities chain because the HTTP API has no
auth and serves wildcard CORS, so any web origin (Firefox
unconditionally; Chrome via PNA-disabled or loopback-served) can drive
the tool-server from a cross-origin tab. Each individual fix lives on
its own branch (linked below) and stands alone — this PR exists so the
cluster can be reviewed and shipped as one piece.

### Fixes

1. **`fix(http): require auth token + drop wildcard CORS`** —
per-process 32-byte hex Bearer token, generated by the launcher,
persisted in `~/.argent/tool-server.json` with mode `0600`, required on
every route except `/preview`. Constant-time compare. Wildcard CORS
dropped. Threaded through MCP server + CLI + tools-client.
Backwards-compat: when `ARGENT_AUTH_TOKEN` is unset (e.g. `npm run
dev`), the server logs a one-shot warning and skips the gate.
2. **`fix(http): reject non-loopback Host headers`** — middleware
(before the auth gate) returns 403 for any request whose Host is not
`127.0.0.1`, `localhost`, or `::1`. Closes DNS-rebinding bypass.
3. **`fix(ios-profiler-start): prevent shell injection via device_id`**
— `detectRunningApp(udid)` no longer interpolates `params.device_id`
into `execSync` template strings. Both `xcrun simctl spawn ${udid} …`
and the piped `xcrun simctl listapps ${udid} | plutil …` are rewritten
to `execFileSync` array form, with the pipe to `plutil` happening in JS
(input option) rather than the shell.
4. **`fix(flows): validate name to close path-traversal in
getFlowPath`** — `getFlowPath()` requires `^[A-Za-z0-9_\\-]+$` and
verifies `path.relative(getFlowsDir(), filePath)` does not start with
`..`. Centralizing the check in `getFlowPath` covers every caller
(start-recording, add-step, insert-echo, finish-recording, run,
read-prerequisite) without per-tool churn.
5. **`fix(source-resolver): reject paths outside projectRoot +
non-source extensions`** — `readSourceFragment()` previously accepted
any `file` field on the SourceLocation it was given (the field traces
back to a React fiber's `_debugSource.fileName`, which is
attacker-controllable). Now: containment check via
`path.relative(projectRoot, abs)` plus an extension allowlist
(`.js/.jsx/.ts/.tsx/.mjs/.cjs/.json`).
6. **`fix(source-maps): reject non-loopback URLs (close SSRF)`** —
`doRegister()` validates `sourceMapURL` before fetching: only
`http://`/`https://` URLs whose hostname is loopback. Closes a
blind-SSRF probe of the host network (cloud metadata at
`169.254.169.254`, intranet pages, etc.).

### Individual branches

- `fix/auth-token-drop-cors`
- `fix/host-header-validation`
- `fix/profiler-command-injection`
- `fix/flow-name-validation`
- `fix/source-resolver-traversal`
- `fix/source-map-ssrf`

If you want to land them as separate PRs, the integration branch can be
dropped after each lands.

## Test plan

Unit tests (already in CI):

- [x] `npm run build` passes (workspace tsc clean)
- [x] `npm run test -w @argent/tool-server` — **510 passed (was 473 on
`main`)**, +37 new tests across:
- `test/http-auth.test.ts` — 10 tests (auth, CORS removal, dev-mode
escape hatch)
- `test/http-host-validation.test.ts` — 7 tests (loopback allow,
DNS-rebinding reject)
- `test/flows/flow-utils.test.ts` — +5 tests (path-traversal name
rejection)
- `test/metro/source-resolver.test.ts` — +5 tests (containment +
extension allowlist)
- `test/metro/source-maps-ssrf.test.ts` — 10 tests (loopback-only allow)

Live end-to-end (run locally against this branch's bundled artifacts):

- [x] Launcher spawns tool-server; `~/.argent/tool-server.json` contains
a 64-char hex token and is mode `0600`
- [x] `GET /tools` without `Authorization` → **401**
- [x] `GET /tools` with wrong token → **401**
- [x] `GET /tools` with correct token → **200** (61 tools)
- [x] Request with `Host: attacker.example` → **403**
- [x] No `Access-Control-Allow-Origin` header on responses
- [x] Cross-origin command-injection probe via `device_id=\"x;touch
/tmp/marker;#\"` does NOT create the marker file (Vuln 4 closed)
- [x] `flow-start-recording` with `name=\"../../../tmp/escape\"`
rejected with `Invalid flow name`
- [x] `flow-start-recording` with a valid name still works
- [x] `tools-client.fetchTools()` and
`tools-client.callTool('list-simulators')` succeed via the auth-token
threading (the path the MCP server uses internally)
- [x] MCP-stdio entry point (`startMcpServer`) end-to-end: `initialize`
→ `tools/list` (61 tools) → `tools/call('list-simulators')` returns
content. Confirms the launcher → token → fetch → tool-server flow is
intact for the production `argent mcp` path.

## Notes for reviewers

- `/preview` is intentionally exempt from the auth gate (it was not
gated before; tightening it deserves a separate PR).
- The `ARGENT_AUTH_TOKEN`-unset escape hatch keeps `npm run dev`
workable; production `argent mcp` always passes a token via env, so the
warning never fires there.
- The auth bundle changes the public return shape of `ensureToolsServer`
from `Promise<string>` to `Promise<{ url, token }>`. The two in-tree
callers (`argent-mcp/mcp-server.ts`,
`argent-tools-client/tools-client.ts`) were updated; external consumers
using `ARGENT_TOOLS_URL` should also set `ARGENT_AUTH_TOKEN` to match
the server.

</details>
2026-06-03 15:57:42 +02:00
filip131311 6355620c4d refactor(mcp): split mcp package into argent, argent-cli, argent-installer, argent-mcp, argent-tools-client (#162)
## Summary

- Splits the monolithic `packages/mcp` package into five focused
packages:
- `argent` — the user-facing CLI entry (`argent` binary,
init/uninstall/update flows, bundled-paths, scripts).
- `argent-cli` — CLI runner internals (flag-parser, run, server, tools).
- `argent-installer` — `init`/`update`/`uninstall` flows, MCP config
adapters, skills installer.
- `argent-mcp` — MCP server itself (mcp-server, tool-mapping,
auto-screenshot, content adapters).
  - `argent-tools-client` — HTTP client + launcher for the tool-server.
- Adds Gemini agent definitions, VSCode MCP config, and per-package
`tsconfig.json` / `vitest.config.ts`.
- Updates root `tsconfig.json`, `scripts/dev.cjs`, CI workflows, and
`package.json` to wire the new packages together.

## Test plan

- [ ] `npm install` resolves the new workspaces cleanly
- [ ] `npm run build` succeeds across all packages
- [ ] `npm test` passes (renamed test files keep their assertions)
- [ ] `npx argent init` still produces the expected MCP config in a
sample project
- [ ] `npx argent update` / `argent uninstall` exercise the installer
package
- [ ] CI workflows (`publish.yml`, `unit-tests.yml`) run on this branch
2026-04-28 08:36:24 +02:00