The XML parsers swallow ENOENT and return [], so when ios-profiler-stop
recorded a path but the file was deleted/unreadable, analyze rendered an
"All clear" report. Pre-flight each non-null path with fs.access so the
tool surfaces a real Export warning naming the missing file.
## Summary
`npx @swmansion/argent update` falsely reported *"Already on the latest
version"* after the user had removed the package via `npx
@swmansion/argent uninstall`.
`getInstalledVersion()` reads `package.json` from `PACKAGE_ROOT`, which
under `npx` is the npx-cached copy of argent — always the registry's
latest. The version compare then matches and short-circuits, hiding the
fact that there is no global install at all.
- Share `isGloballyInstalled()` between `init` and `update` via
`utils.ts`; `update.ts` now routes through a fresh-install prompt
(`Install @swmansion/argent@<latest> globally?`) when no permanent
argent binary is on PATH.
- Fix a Windows-only bug in the temp-runner check while the helper was
being moved: `where argent` returns multiple lines, and the previous
logic flagged the entire output as transient if *any* line contained an
npx marker. Now each line from `where` / `which -a` is inspected
independently, so a real global install is not masked by a concurrent
npx invocation.
- Add `isTempRunnerPath()` plus 5 unit tests covering npx, pnpm dlx,
bunx, Windows dlx, and real global paths.
## Test plan
- [x] `npm run build` (argent-installer) — passes
- [x] `npm test` (argent-installer) — 231 passing (5 new)
- [x] `npx prettier --write` on touched files — no diffs
- [ ] Manual: `npm install -g @swmansion/argent` → `npx
@swmansion/argent uninstall` → `npx @swmansion/argent update` should now
offer to install the latest version instead of reporting "already on the
latest"
- [ ] Manual: `argent update` while globally installed and on latest
still reports "already on the latest version"
- [ ] Manual: `argent update` while globally installed and on an older
version still prompts to update
## Summary
- Bumps all 9 published packages from `0.6.0-next.2` → `0.6.0` (stable)
in preparation for the `0.6.0` release.
- Affected: `@swmansion/argent`, `@argent/cli`, `@argent/installer`,
`@argent/mcp`, `@argent/tools-client`, `@argent/native-devtools-ios`,
`@argent/registry`, `@argent/skills`, `@argent/tool-server`.
`packages/ui` (`0.0.1`) is on a separate version track and left as-is.
- Lockfile updated via `npm install`; `npm run format` was a no-op.
## Test plan
- [ ] CI green
- [ ] Run the stable publish workflow against this commit and confirm
publish succeeds
## Summary
- Bumps all 9 published packages from `0.6.0-next.1` → `0.6.0-next.2` in
preparation for the next prerelease publish.
- Affected: `@swmansion/argent`, `@argent/cli`, `@argent/installer`,
`@argent/mcp`, `@argent/tools-client`, `@argent/native-devtools-ios`,
`@argent/registry`, `@argent/skills`, `@argent/tool-server`.
`packages/ui` (`0.0.1`) is on a separate version track and left as-is.
- Lockfile updated via `npm install`; `npm run format` was a no-op.
## Test plan
- [ ] CI green
- [ ] Run `Publish prerelease to npm (next)` workflow against this
commit and confirm publish succeeds
This PR fixes regression introduced in #162:
```
◇ Add Argent tools to editor auto-approve lists? - recommended
│ Yes
node:internal/modules/cjs/loader:1459
throw err;
^
Error: Cannot find module '/Users/ignacylatka/.npm/_npx/9bf42f550c32750c/node_modules/@swmansion/argent/tool-server.cjs'
at Module._resolveFilename (node:internal/modules/cjs/loader:1456:15)
at defaultResolveImpl (node:internal/modules/cjs/loader:1066:19)
at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1071:22)
at Module._load (node:internal/modules/cjs/loader:1242:25)
at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)
at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5)
at node:internal/main/run_main_module:33:47 {
code: 'MODULE_NOT_FOUND',
requireStack: []
}
Node.js v24.14.1
```
### Root cause
`packages/argent-installer/src/mcp-configs.ts:22` resolves the bundled
tool-server with:
```ts
path.join(import.meta.dirname, "..", "tool-server.cjs")
```
That was correct in the pre-#162 layout where the file lived at
`packages/mcp/dist/cli/mcp-configs.js` and `..` stepped from `dist/cli/`
to `dist/`.
After the refactor the file is inlined into
`packages/argent/dist/installer.mjs` — a direct **sibling** of
`dist/tool-server.cjs` — so `..` now climbs one level too far and
resolves to `<pkg>/tool-server.cjs`.
The codex adapter's `addAllowlist`/`removeAllowlist` calls
`getAvailableToolIds()` → `execFileSync("node", [TOOL_SERVER_BUNDLE,
"-t"])`, which is where the broken path is dereferenced.
### Fix
Drop the `..`. Existing `packages/argent/src/bundled-paths.ts:7` already
uses the sibling form, so this aligns the two.
## Summary
- Bumps all 9 published packages from `0.6.0-next.0` → `0.6.0-next.1` in
preparation for the next prerelease publish (the `0.6.0-next.0` train
was blocked by the build-config and update-checker fixes in #172 and
#173).
- Affected: `@swmansion/argent`, `@argent/cli`, `@argent/installer`,
`@argent/mcp`, `@argent/tools-client`, `@argent/native-devtools-ios`,
`@argent/registry`, `@argent/skills`, `@argent/tool-server`.
`packages/ui` (`0.0.1`) is on a separate version track and left as-is,
matching #171.
- Lockfile updated via `npm install` — only the 9 workspace `version`
entries changed.
## Test plan
- [ ] CI green
- [ ] Run `Publish prerelease to npm (next)` workflow against this
commit (after #172 and #173 land) and confirm publish succeeds
## Summary
- The homegrown `parseSemver` in
`packages/tool-server/src/utils/update-checker.ts` only matched plain
`MAJOR.MINOR.PATCH` and returned `null` for prerelease strings. After
[#171](https://github.com/software-mansion/argent/pull/171) bumped the
local version to `0.6.0-next.0`, `isNewerVersion` started returning
`false` for **any** value of `latest`, because the *current* side failed
to parse. Two consequences:
- Users on the `next` dist-tag are never notified of new releases
(silent feature degradation, not a crash).
- The `detects an available update on startup` unit test broke because
its asserted state now reads `{ updateAvailable: false, latestVersion:
"99.0.0" }` instead of `{ true, "99.0.0" }` — see [run
25161184594](https://github.com/software-mansion/argent/actions/runs/25161184594/job/73755770427).
- Replaces the regex parser with the `semver` package (already pinned at
`^7.7.4` and present in the lockfile via `@argent/installer`). Adds it
as a dep of `@argent/tool-server` along with `@types/semver`.
- Keeps the existing UX rule: don't notify when upstream `latest` is
itself a prerelease — npm's `latest` dist-tag should be stable, and we
shouldn't push betas to stable users. `semver.gt` correctly treats a
stable version as greater than its own prereleases, so a user on
`0.6.0-next.0` will be notified when `0.6.0` ships.
## Behavior matrix
| current | npm `latest` | before (broken) | after |
|---|---|---|---|
| `0.6.0-next.0` | `99.0.0` | not flagged | **flagged** |
| `0.6.0-next.0` | `0.6.0` | not flagged | **flagged** |
| `0.6.0` | `99.0.0` | flagged | flagged |
| `0.6.0` | `1.0.0-beta.1` | not flagged | not flagged |
| anything | invalid string | not flagged | not flagged |
## Test plan
- [x] `npm test -w @argent/tool-server -- test/update-checker.test.ts` —
all 10 tests pass (was 9/10).
- [x] `npm test -w @argent/tool-server` — full suite 444/444 passes.
- [x] `npx tsc --build` clean from the repo root.
- [ ] CI green on this branch.
## Summary
- Bump all published packages to `0.6.0-next.0` in preparation for the
next prerelease train.
- Affected: `@swmansion/argent`, `@argent/cli`, `@argent/installer`,
`@argent/mcp`, `@argent/tools-client`, `@argent/native-devtools-ios`,
`@argent/registry`, `@argent/skills`, `@argent/tool-server`.
`@argent/native-devtools-ios` was at `0.5.1`; aligned with the rest.
`packages/ui` (`0.0.1`) is on a separate version track and left as-is.
## Test plan
- [ ] CI green
- [ ] `npm next` prerelease publish workflow picks up the new version
OpenCode supports:
- project-scope: `opencode.jsonc` and `opencode.json`;
- global-scope: `config.json` (legacy/migrated), `opencode.json`, and
`opencode.jsonc`
This PR adds support for these alternative filenames.
This PR also adds `jsonc` support.
<img width="502" height="377" alt="image"
src="https://github.com/user-attachments/assets/a0f1d437-93ce-4677-bc6f-b8d0c8427b57"
/>
Closes#165.
`argent init` was wiping users' `~/.config/zed/settings.json` - Zed's
settings format is JSONC, so `readJson` used `JSON.parse` and silently
fell back to `{}` on any parse failure, after which the Zed adapter
would overwrite the file with an argent-only stub.
Co-authored-by: Filip131311 <contact@filipkaminski.com>
## What this PR does
This refactor:
- replaces the DFS fiber-walking hook inside the app's JS runtime with
the **React DevTools backend** as the source of profiling data
- adds a session-ownership system so two tool-server instances can't
silently clobber each other
- gives agent a new status tool for recovery after interruptions
- improves what the profiling analysis report shows the agent
I realise this a big pull request, but changes began to affect each
other when it comes to registered scripts on the react-native side.
This, all-in-all, should however cover our needs when it comes to the
react profiler and if it proves sufficient - we will only have to
iterate on small fixes afterwards.
If I were you, I would skip over the implementation of scripts on the
native-side and focus on what the tool-server does when it comes to
profiling right now.
---
## Profiling workflow
The overall flow has not changed from the agent's perspective - agent
calls `react-profiler-start`, do something in the app, call
`react-profiler-stop`.
What changed is how the profiling data is collected.
**Before:** `react-profiler-start` injected a custom `onCommitFiberRoot`
hook that walked the live fiber tree at every React commit in DFS-like
way and appended records to a global `__ARGENT_DEVTOOLS_COMMITS__` array
in the app's JS heap. On stop, the array was fetched in chunks over CDP.
**Now:** `react-profiler-start` calls `ri.startProfiling(true)` on the
React DevTools backend's `rendererInterface`. React's own profiler
accumulates commit data internally. On stop, a single
`STOP_AND_READ_SCRIPT` call stops the backend and reads
`ri.getProfilingData()` in one round-trip. The DevTools backend data is
richer, more accurate, and doesn't require maintaining a bespoke
fiber-walking implementation.
---
## Session ownership
The profiler runs inside the app's JS heap. There's only one "slot": it
used to be that when two tool-server instances (or the same instance
after a process restart) both try to start profiling, the second call
silently overwrites the first and all data from the first session is
gone.
Right now, there is an approach which blocks silent overtake of the
profiling session.
When `react-profiler-start` runs, it:
1. Reads the current state from the runtime using `READ_STATE_SCRIPT` (a
pure, side-effect-free read).
2. If a session is already running, it reads the
`__ARGENT_PROFILER_OWNER__` object written into `globalThis`, which
contains `sessionId`, `startedAtEpochMs`, and `lastHeartbeatEpochMs`.
3. Classifies the existing session as **fresh** or **stale** using
`classifyStaleness()` (stale = no heartbeat for > 5 minutes).
If the session is fresh and you don't pass `{ force: true }`, start
refuses and returns an `already_running` payload instead of overwriting.
4. If the session is stale or `force: true`, it calls
`STOP_FOR_TAKEOVER_SCRIPT` to cleanly stop the prior session, then
starts a new one.
### Heartbeats
Each `sessionId` UUID is written to `__ARGENT_PROFILER_OWNER__` in the
runtime at start. While profiling, any side query tool
(`react-profiler-renders`, `react-profiler-fiber-tree`) that this same
tool-server process calls will bump `lastHeartbeatEpochMs` via
`HEARTBEAT_SCRIPT`. This keeps the session from being classified as
stale as long as the owning process is still making calls.
### What the agent sees when something goes wrong
Previously, if you called `react-profiler-start` into an already-running
session you'd get a silent clobber or a confusing Hermes error. Now:
- **Running session detected, still fresh:** `{ already_running: true,
owner: { sessionId, startedAtEpochMs, lastHeartbeatEpochMs },
age_seconds, how_to_reclaim }` — the agent can read the owner timestamp
and decide whether to tell the user to wait or force-reclaim.
- **Running session detected, stale:** automatic takeover with a log
note.
- **No owner record (e.g. another DevTools client started the
session):** also automatic takeover.
### New `react-profiler-status` tool
A read-only tool that reports the current state without starting or
stopping anything. Returns `session_status` as one of `"active"` /
`"taken_over"` / `"stopped"` / `"no_react_runtime"`, plus the full owner
record. Use this after a debugger disconnect, an unexpected error, or
when the agent restarts mid-session to decide whether to continue,
reclaim, or give up.
The agent has instructions to use the tool whenever it is interrupted
during the profiling by the user or otherwise. Will have to look into
how much this is actually used.
---
## What changed in the data we collect
The old hook read fiber fields directly (`fiber.type.displayName`,
`fiber.memoizedProps`, `fiber.selfBaseDuration`, etc.) at commit time.
This meant:
- Names for **transient components** (modals, tooltips, navigation
screens) that unmounted before stop were already lost - the fiber was
gone.
- Change-detection logic (which props changed, which hooks fired) was
reimplemented manually against the fiber internals.
The new approach reads `ri.getProfilingData()` which gives us the
DevTools backend's own commit log. Each commit entry has
`fiberActualDurations`, `fiberSelfDurations`, `changeDescriptions`,
`timestamp`, and `duration` - the same data React DevTools Profiler
shows in its browser panel.
### Fiber name cache for transient components
To address the lost-name problem, `FIBER_ROOT_TRACKER_SCRIPT` now hooks
`onCommitFiberRoot` and, immediately after the DevTools backend finishes
processing each commit (synchronously, while the fiber is still in
memory), populates `globalThis.__argent_fiberNames__` with `fiberID →
displayName` entries. Because fiber IDs are monotonically increasing and
never reused, this cache never goes stale. `STOP_AND_READ_SCRIPT` uses
this cache as a fallback for any fiber whose name
`getDisplayNameForElementID` can no longer resolve.
### Unattributed fiber tracking
When a transient fiber renders during the session but still can't be
named at stop time (not in the name cache, not in the DevTools instance
map), its work is now **surfaced rather than silently dropped**. The
stop tool computes `unattributedByCommit`: a per-commit tuple of
`[commitIndex, droppedFiberCount, droppedSelfMs]`. This flows through
to:
- The session metadata file on disk (`unattributedByCommit` array).
- The pipeline (`buildHotCommitSummaries` receives and indexes it).
- The `HotCommitSummary` type gains `unattributedMs` and
`unattributedFiberCount`.
- The generated report shows a warning line per commit with unattributed
work, e.g. `⚠️ 12ms unattributed — 3 fibers unmounted before stop`.
- `react-profiler-stop` returns top-level `unattributed_ms`,
`unattributed_fiber_count`, `unattributed_commit_count` in its response
so the agent immediately knows if there's a gap.
### Actual vs. self duration now both surfaced
The old report showed only `selfDuration` (exclusive render time). Both
are now captured in `HotCommitComponentEntry` and rendered in the report
as:
```
- `ComponentName` — 4.2ms self, 18.7ms w/children
```
A legend at the top of the report explains when to sum self and when not
to sum inclusive time. The summary table also shows both columns.
### Component name unwrapping
Display names from the DevTools backend can be wrapped:
`Forget(Memo(MyButton))`, `ForwardRef(Icon)`, etc. The pipeline's render
stage now strips these wrappers using `annotateComponentName()` and
appends a human-readable tag, so the report shows `\`MyButton\`
[React.memo + React Compiler]` instead of the raw internal string. The
raw DevTools name is still used in all tool-call suggestions (since
every pipeline stage keys on the original string).
---
## What scripts do what
All injected JS scripts now live in a single file —
`packages/tool-server/src/utils/react-profiler/scripts.ts` — grouped by
responsibility:
| Script | When injected | What it does |
|---|---|---|
| `REACT_NATIVE_PROFILER_SETUP_SCRIPT` | Every `react-profiler-start`
(idempotent) | Wraps `ri.startProfiling` / `ri.stopProfiling` to track
`isProfiling` state and `startedAtEpochMs` without clock skew; installs
the `__argent_profilerHeartbeat` helper |
| `FIBER_ROOT_TRACKER_SCRIPT` | On CDP connect (idempotent) | Intercepts
`onCommitFiberRoot` to populate the `__argent_fiberNames__` commit-time
name cache |
| `READ_STATE_SCRIPT` | Every `react-profiler-start`,
`react-profiler-status` | Pure read: returns `{ hookExists,
rendererInterfaceFound, isRunning, owner, nowEpochMs }` |
| `buildStartScript(ownerJson)` | `react-profiler-start` on the happy
path | Calls `ri.startProfiling`, writes owner record, returns
post-start verification flags |
| `STOP_FOR_TAKEOVER_SCRIPT` | `react-profiler-start` on the takeover
path | Stops the prior session cleanly before starting a new one |
| `HEARTBEAT_SCRIPT` | `react-profiler-renders`,
`react-profiler-fiber-tree` | Bumps `lastHeartbeatEpochMs` while the
owning process is still active |
| `STOP_AND_READ_SCRIPT` | `react-profiler-stop` | Stops the backend
profiler and reads `getProfilingData()` + name cache in one round-trip |
| `RESOLVE_FIBER_META_SCRIPT` | `react-profiler-stop` (if commits > 0) |
Walks the live fiber tree to collect `hookTypes`, `parentName`, and
`isCompilerOptimized` metadata per component |
## Summary
Sets up the foundation for adding Android support without changing any
user-visible iOS behavior. iOS works exactly as before; Android handlers
are stubs that throw a clear \"not yet implemented\" message until
they're filled in.
### New primitives
- `Platform`, `DeviceKind`, `DeviceInfo`, `ToolCapability`,
`ToolDependency` (in `@argent/registry`)
- `classifyDevice(udid)` + `resolveDevice(udid)` (UUID shape → iOS, else
Android)
- `assertSupported(toolId, capability, device)` — throws
`UnsupportedOperationError`
- `ensureDep(s)` — host-binary preflight with install hints
- `dispatchByPlatform({ toolId, capability, ios, android })` — one-line
cross-platform `execute()`
### HTTP gating
`/tools/:name` now runs:
- Host-binary preflight (`requires`) → 424 with install hint
- Capability gate (`capability` + `udid`) → 400 with reason
### Per-tool directory layout
Every cross-platform tool now lives in its own directory:
\`\`\`
tools/gesture-tap/
index.ts ← ToolDefinition, capability, dispatchByPlatform
platforms/
ios.ts ← iOS impl (existing logic)
android.ts ← stub: throws \"not yet implemented\"
\`\`\`
Refactored 17 tools this way: gesture-{tap,swipe,custom,pinch,rotate},
screenshot, button, paste, keyboard, launch-app, restart-app,
reinstall-app, open-url, rotate, run-sequence, describe (with shared
contract + iOS adapters under platforms/).
### Tool call flow (iOS, today)
1. Agent calls \`gesture-tap { udid: \"<uuid>\", x, y }\`
2. HTTP zod-parses, then host-binary preflight, then capability gate
(passes — capability has \`apple\`)
3. Tool's \`execute()\` (built by \`dispatchByPlatform\`):
- \`resolveDevice(udid)\` → \`{ platform: \"ios\", kind: \"simulator\",
id }\`
- \`assertSupported\` → passes
- branches to \`tapIos(services, params, device)\`
4. iOS impl uses \`SimulatorServerApi\` exactly as before.
### Tool call flow (Android, future)
Same flow; the Android branch in \`dispatchByPlatform\` runs. Filling in
Android = three steps:
1. Implement the function in \`tools/<tool>/platforms/android.ts\`
2. Add \`android: { emulator: true, device: true, unknown: true }\` to
the tool's capability
3. Make \`services()\` platform-aware where the iOS service URN doesn't
apply (e.g. \`launch-app\` declares \`nativeDevtools\` for iOS only)
## Test plan
- [x] All 395 existing tests pass; 9 new tests for `device-info` +
`capability`
- [x] TypeScript clean across `@argent/registry` and
`@argent/tool-server`
- [ ] Manual smoke-test on a real iOS simulator (gesture-tap,
screenshot, describe, launch-app)
## Deferred to follow-ups
- **Per-tool capability declarations on iOS-only tools**
(`native-devtools/*`, `profiler/ios-profiler/*`, `network/*`,
`simulator/*`). They still work; the HTTP capability gate is a no-op
when no declaration exists. Adding them is mechanical and doesn't affect
iOS behavior.
- **`list-simulators` / `boot-simulator` rename to `list-devices` /
`boot-device`** — that's a behavior change, paired with the Android
impls.
- **Platform-aware `services()`** for tools whose iOS service has no
Android equivalent (e.g. `nativeDevtools` in `launch-app` /
`restart-app`). Needed when filling in Android impls.
## Summary
These tests were taking up too long.
| File | Before | After |
|------|-------:|------:|
| `fetch-timeout.test.ts` hanging-POST test | 6281ms | ~600ms |
| `workspace-reader.test.ts` (full file) | ~4500ms | ~140ms |
| `gather-workspace-data.test.ts > valid snapshot` | ~590ms | ~3ms |
**Package totals:** `packages/mcp` 6.44s → ~830ms,
`packages/tool-server` 5.29s → ~790ms.
### What was slow and why
- `fetchWithReconnect` is now exported from `mcp-server.ts` so the test
uses the real function. The config accepts an `expBackoffBase` and a
`fetchTimeoutMs` so the hanging-POST test shrinks the backoff base to
`5ms` and the per-attempt timeout to `100ms` (~575ms total)
- every `readWorkspaceSnapshot` call spawned 8 real subprocesses
(`node`, `npm`, `yarn`, `pnpm`, `bun`, `pod`, `eas`, `expo`) via
`runVersionCommand`. Added `vi.mock("node:child_process", ...)` that
stubs `execFile`.
This PR adds a thin cli layer to argent by introducing 3 new commands:
1) `server` allows for checking the tools-server status
2) `tools` allows for tools discovery
3) `run` allows for running tools straight from the shell
This is a baseline functionality needed for testing hypothesis that CLI
with skills is more efficient then mcp and allow should allow easier
integration with potential cloud infrastructure
* feat(mcp): support progressive tool loading via _meta hints
Add optional alwaysLoad and searchHint fields to ToolDefinition,
forward them through GET /tools, and emit them as
_meta["anthropic/alwaysLoad"] / _meta["anthropic/searchHint"] in the
MCP ListTools response. Claude Code's ToolSearch defers MCP tools by
default; tools marked alwaysLoad stay in context on every turn, and
searchHint feeds the BM25 ranker so deferred tools surface for
relevant queries.
Mark the core discovery + interaction tools as alwaysLoad so Claude
never has to round-trip through ToolSearch to tap the screen:
describe, debugger-component-tree, screenshot, gesture-tap,
gesture-swipe, launch-app, run-sequence. Everything else defers.
* feat(mcp): mark restart-app as alwaysLoad
Tools that depend on controlling app startup (relaunch for clean
state, refreshing native-devtools injection) need restart-app to be
immediately callable rather than routed through a search round-trip.
* feat: add Get Started tip to end of argent init flow
New users finished init and asked "what now?" — surface the
"ask your assistant What can Argent do?" tip from the README
as a final note before the outro so the next step is obvious.
* style: make Get Started tip more prominent in init flow
Bold colored title, bold cyan call-to-action indented on its own
line, and a dimmed tagline below — so the "what now?" answer
visually pops instead of reading as another summary line.
* style: fold outro into Get Started note and make it more prominent
* style: render Get Started as outro so the end of init flow is clear
* style: restore boxed banner around Get Started tip, keep outro terminator
* human fix
* fix: update skills during argent update and use semver-aware comparison
- Replace `installed !== latest` with `isNewerVersion` so a locally
installed prerelease (e.g. 0.5.4-beta on top of published 0.5.3) no
longer triggers a downgrade prompt during `argent update`.
- Replace the "Check if skills have updates?" prompt (which only ran
`npx skills check`) with an actual update that runs `npx skills update
-y`. Under --yes/-y the step runs unattended so CI and scripted
upgrades actually pick up new skill versions.
* Update skills unconditionally, scope to argent-owned skills, cover both scopes
- Drop the skills prompt entirely — the rest of the update flow already
refreshes MCP configs, allowlists, rules, and agents without asking, so
the skills step is now consistent. Stale skills silently break argent
behavior after a version bump, so the safe default is to always resync.
- Scope the update to argent-owned skills by reading bundled skill names
from SKILLS_DIR at runtime (new `listBundledSkills` helper). Third-party
skills the user installed separately are left untouched.
- Run against both -p and -g scopes instead of relying on `-y`'s
auto-detect, which only covers one scope and would miss whichever the
user didn't pick during init.
* Use `skills add` for argent skill resync, only in scopes that track them
The skills CLI's `update` command silently skips every entry with
sourceType="local" (which is how argent ships its bundled skills), so
`npx skills update` never actually re-synced them — the previous commit
was effectively a no-op. Switch to `npx skills add <SKILLS_DIR> --skill
'*' -y`, which hashes the source on disk and reinstalls whatever
changed after `npm i -g @swmansion/argent@new`.
To avoid creating a `skills-lock.json` in an unrelated working
directory, only resync a scope that already tracks at least one
argent-owned skill. Scope detection reads the skills CLI's lock files
directly: `./skills-lock.json` for project scope and
`$XDG_STATE_HOME/skills/.skill-lock.json` or `~/.agents/.skill-lock.json`
for global scope.
* Prune argent skills that are no longer shipped
`skills add` only scans the current SKILLS_DIR, so a skill that argent
ships in v0.5.3 but removes in v0.5.4 stayed in every lock file and in
every agent's skills/ directory — the agent would keep loading it.
After resync, diff the scope's lock against the bundled set and run
`npx skills remove` for each argent-prefixed entry that's no longer
bundled. Ownership detection is prefix-based (every skill argent has
ever shipped starts with `argent-`), which also lets us find orphans
that listBundledSkills() no longer knows about.
Replaces the previous hasSkillsInLock helper with
listArgentSkillsInLock, which returns the actual names so they can be
used for both scope detection and orphan computation.
* Extract argent-skills refresh into a shared module
Pulls the two-scope resync+prune block out of update.ts into
packages/mcp/src/cli/skills.ts, exposed as `refreshArgentSkills` (pure
data) and `formatSkillRefreshSummary` (display). update.ts shrinks
from ~80 lines of inline logic to a two-line call.
Also wires the helper into init.ts's upgrade prompt: when the user
accepts an in-init version bump, we now prune argent skills that the
new version dropped — previously orphans leaked forever because
init.ts never ran a remove, only a single-scope `skills add` in
Step 2.
The install action is deliberately not extracted here — update's and
init's install paths differ on stdio mode, failure handling,
killToolServer prerequisite, and the ARGENT_SKIP_POSTINSTALL env, and
unifying them would force behavior alignment that doesn't obviously
belong together.
* format
* Clean project lock after skills remove
The skills CLI's `skills remove` deletes the canonical skill directory
and per-agent symlinks for both scopes, but only removes the entry
from the GLOBAL lock (`~/.agents/.skill-lock.json`) — the project
`skills-lock.json` is left with a stale entry pointing at a skill
whose files no longer exist. The end-to-end test in
`scripts/test-skills-prune.sh` caught this: canonical dir and symlink
gone, lock still contained the orphan.
Without cleanup, every subsequent `argent update` would keep seeing
the orphan in the lock, try to prune it again, and report "pruned 1"
for something that's already been pruned. Fix it by stripping the
pruned names from the lock file after a successful `skills remove`.
Also commits `scripts/test-skills-prune.sh`, the e2e script that
uncovered this.
* Revert project-lock cleanup workaround
The previous commit worked around a bug in the vercel-labs/skills CLI
where project-scope `skills remove` cleans the canonical skill
directory and per-agent symlinks but leaves the `skills-lock.json`
entry behind. In practice this only matters cosmetically — the agent
never loads the orphan because its files are gone — so maintaining a
workaround in our code base for a bug that belongs upstream is more
churn than it's worth.
Reverts the `removeSkillsFromLockFile` helper and its call site in
`refreshArgentSkills`, plus the two tests that asserted on it.
Updates the e2e script to expect the stale lock entry for project
scope and only fail on missing canonical dir / symlink cleanup.
Global scope still gets the strict check — the skills CLI does clean
the global lock correctly there.
Issue to be filed upstream so they can fix it in the right place.
* Drop scripts/test-skills-prune.sh
The e2e script served its purpose uncovering the skills-CLI
project-lock quirk (now filed upstream as vercel-labs/skills#977) and
isn't needed as a committed artifact.
* Add --from tarball flag to argent update
Mirrors the `--from <path>` flag that already exists in `argent
init`: installs from a local tarball or filesystem path instead of
the registry, then runs the same refresh+prune as a normal update.
Intended for dry-running a locally-packed argent end-to-end before
publishing to npm.
Undocumented by design — it's a dev tool, not a user-facing feature.
Bypasses the registry probe entirely so testing doesn't depend on
what npm currently has as latest.
Extracts `extractFlag` from init.ts into utils.ts so both commands
share a single implementation.
* Revert "Add --from tarball flag to argent update"
This reverts commit 53ea4e5334.
* Use resolveProjectRoot in init and update
Both commands were pinning their project root to the literal cwd,
which silently misbehaved when run from a subdirectory of the
project. The skills refresh was worst-affected: `refreshArgentSkills`
reads `skills-lock.json` at the given root, so running
`argent update` from deep inside the project would check a file that
doesn't exist, get zero tracked skills, and silently skip the
project-scope resync and prune. The MCP/allowlist/rules refresh
similarly ended up writing phantom configs at the subdirectory
instead of updating the real ones at the project root.
`uninstall.ts` already did the right thing via `resolveProjectRoot",
which walks up to the nearest managed marker (`.mcp.json`, `.claude`,
`.cursor`, ..., `skills-lock.json`, or `.git`). Apply the same to
`init.ts` and `update.ts` so the whole CLI resolves the root
consistently. Custom-path override in init (`scope === "custom"") is
unaffected — it still wins over the resolved root.
Previously the CLI checked `latest !== version`, which triggered an
update prompt (and recommended a downgrade) whenever a locally installed
prerelease was newer than the published release. Switch to semver-aware
comparison so update prompts only appear for genuine upgrades.
* show offline banner during argent init
Detect offline state via a DNS lookup of the npm registry at the start
of `argent init` and render a red "Offline mode" note listing the steps
that require network access (global install/update, `npx skills`). Skip
the update-check spinner entirely when offline and default the skills
prompt to the manual option so the user is not dropped into a failing
npx run. Also pipe stderr on the `npm view` version lookup so its 404
output no longer leaks to the terminal.
* soften offline skills wording to account for npx cache
* hide offline skills warning when npx skills is already cached
* extract offline banner into its own helper
* drop redundant manual-option hint from offline bullet
* harden isOnline: unref timer, clear on resolve, guard URL parse
* test(mcp/cli): mock network probes; harden offline skills install
Replace the `isOnline` test that hit real DNS with deterministic mocks
for `node:dns.lookup` and `node:child_process.execSync`, and add
coverage for `isSkillsCliAvailable` and `getLatestVersion` (trim,
stderr pipe regression guard, error propagation).
Fixes found during verification:
- Offline skills install now passes `--no-install` to npx so it uses
the cache that `isSkillsCliAvailable()` already confirmed, instead of
hitting the registry for a fresh copy and failing offline.
- Strip `--no-install` from the manual-recovery hint so users can
retry with network access.
- `isSkillsCliAvailable` and `getLatestVersion` now pass execSync
timeouts (2s and 10s) so a wedged `npx` or captive portal cannot
hang `argent init`.
* refactor: replace top-level offline banner with contextual skills warning
Instead of showing a large offline banner at the start of init that
lists everything that will be skipped, only check online status at the
skills step — the one place where offline truly matters. The update
check already handles registry failures gracefully via try/catch.
When offline and skills CLI is not cached, a single-line warning appears
right before the skills prompt, and the default shifts to Manual.
* revert: remove unrelated getLatestVersion stdio/timeout change
The stdio piping and timeout addition to getLatestVersion belongs in a
separate PR — it fixes stderr leakage from npm view, not offline handling.
* refactor: build skills args then prepend --no-install when offline
Build the base skillsArgs list once, then derive npxArgs by prepending
--no-install when using the offline cache. The error recovery hint uses
the base list directly — no stripping needed.
* cleanup: remove initialValue override and fix stale test comment
Drop the initialValue on the skills prompt — it changed the default
selection which is unrelated to offline handling. Fix the test file
comment that still referenced getLatestVersion after its tests were
removed.
* chore: unify probe timeout to 8s and fix prettier
* add timeout to getLatestVersion and lower probe timeout to 3s
getLatestVersion (npm view) hangs indefinitely offline without a timeout,
blocking users from reaching the skills step. Use the shared
PROBE_TIMEOUT_MS constant, lowered to 3s for snappier offline detection.
* skip skills prompt when offline, add timeout to getLatestVersion
When offline without cached skills CLI, go straight to manual instead
of showing unselectable options. Add PROBE_TIMEOUT_MS to getLatestVersion
so the update check doesn't hang indefinitely offline.
* feat: add MCP config adapter for Hermes
Add hermesAdapter to the MCP config registry so `argent init` can write
the mcp_servers.argent entry into ~/.hermes/config.yaml.
- Uses the yaml Document API (parseDocument/setIn/toString) to preserve
user comments and formatting on round-trip.
- remove() cleans up by deleting the argent key from mcp_servers, or the
entire mcp_servers block when argent is the only entry.
- readYamlDocument throws on malformed YAML instead of silently
clobbering; writeYamlDocument uses lineWidth: 0 to avoid re-wrapping
long user strings.
- Adds symmetric local→global fallback in the init write loop so
global-only adapters (Hermes, Windsurf) are not silently skipped when
the user picks scope=local.
* refactor: drop unused readYaml POJO flavor, unify yaml util names
readYaml (POJO round-trip) was added to utils.ts but never called — the
hermes adapter uses the Document-returning helpers exclusively. Drop it
and rename readYamlDocument/writeYamlDocument to readYaml/writeYaml so
the naming matches the readJson/readToml pairs; with only one flavor
left, the Document suffix no longer disambiguates anything.
* feat(init): flag adapters that only have one config scope
For adapters like Hermes and Windsurf that only expose a global config
path (or conversely, only a project path), the multiselect menu now
shows an italic cyan note next to the adapter name so users understand
up front which scope the entry will land in, instead of discovering it
after selection.
* fix: address crash recovery review issues from #135
- crashShutdown now exits with code 1 (was 0 via shared shutdown())
- reconnect() failure no longer aborts the retry loop
- health monitor timeout 3s → 10s (avoids false positives on busy server)
- ListToolsRequestSchema lets errors propagate (empty list could be cached)
- event emitter catches async listener rejections
- forceExit timer keeps event loop alive (removed .unref())
- stderr error handler no longer writes to stdout
- SIGINT/SIGTERM handlers fall back to process.exit(0) if shutdown is null
- fetchTools() checks res.ok
* fix: clarify stderr error handler comment
* fix: revert stderr handler — stdout fallback is safe
The launcher uses readline + regex matching on stdout, so extra
non-matching lines are harmlessly discarded. Restore the original
stdout fallback for stderr errors.
* fix: add fetch timeout, MCP error handlers, and idle timer on GET /tools
- fetchWithReconnect: wrap each attempt in an AbortController with a
configurable timeout (ARGENT_FETCH_TIMEOUT_MS, default 30s) so a
hanging tool-server doesn't wedge the MCP parent indefinitely
- MCP parent (mcp-server.ts): install process.stdout/stderr error
handlers and uncaughtException/unhandledRejection listeners so an
EPIPE from a closed Codex transport doesn't crash the process
- GET /tools: touch the idle timer so list_tools polling keeps the
tool-server alive between tool calls
- Add tests for fetch timeout and idle timer on GET /tools
* chore: update showcase GIF asset
* Revert "chore: update showcase GIF asset"
This reverts commit 2b9379fb05.
* fix: add safety timeout to shutdown and validate FETCH_TIMEOUT_MS
- shutdown() now has a 5s force-exit safety timeout matching
crashShutdown, preventing hangs if server.close() blocks on a
lingering connection
- ARGENT_FETCH_TIMEOUT_MS is validated: NaN/0/negative values fall
back to 30s instead of causing immediate aborts
* fix: remove dead null guard on SIGINT/SIGTERM handlers
shutdown is always assigned (line 57) before the handlers are
registered (line 98-99) in the same synchronous block. The arrow
wrapper is still needed to avoid passing the signal string as exitCode.
* cleanup unneeded bang op
* update comment
* update comment
* prettier
* fix: rewrite fetch-timeout test to not depend on pre-built dist
The test spawned `node dist/cli.js mcp` which requires running
`npm run build:mcp` first. If the dist is stale (e.g. from main),
the old code without the AbortController timeout runs and the test
hangs. Rewrite as an in-process test using a replica of the
fetchWithReconnect pattern against a hanging HTTP server.
* test: add minimal EPIPE → Transport Closed repro
Proves the mechanism: a Node child (mimicking argent mcp) without a
stdout error handler dies on EPIPE when the parent (mimicking Codex)
closes its read end. With the handler, the child survives and the
transport stays usable.
* revert: remove speculative EPIPE stdout/stderr handlers from MCP parent
The EPIPE defense assumed Codex would close its read end of the stdio
pipe while the MCP server is still alive. We can't evidence this from
the logs — the proven root cause is the fetch-timeout hang (repro #3).
Remove the handlers and the EPIPE repro test.
* revert emitter promise error handling
* use universal process timeout value
* remove dubious fixes
* remove unused const
* rev event emitter to main
* revert mcp server to main
* move const to file scope
---------
Co-authored-by: pFornagiel <pawel.fornagiel2@gmail.com>
* fix: add tool server crash recovery to MCP server
- MCP CLI: log uncaught exceptions/rejections but don't exit when
running as MCP server (long-lived stdio process must survive)
- Tool server: attempt graceful registry disposal before exiting on
fatal errors so child processes (simulator servers) are not orphaned
- fetchWithReconnect: retry up to 5 times with exponential backoff
(500ms→8s) giving a restarted tool server time to boot
- ListToolsRequestSchema: add try/catch, return empty list on failure
instead of propagating raw errors
- Health monitor: periodic 30s ping detects tool server crashes between
requests and preemptively triggers reconnect
- Defense-in-depth: catch errors in EventEmitter listeners, handle
rejected promises in simulator watcher and update checker, log
stdout/stderr errors instead of rethrowing
* fix: address PR review feedback
- Halve retry backoff (4 retries, 250ms base → ~3.75s total wait)
- Log initSimulator failures instead of silently swallowing them
`xcrun simctl getenv` silently truncates return values at 127 bytes.
The code was reading with `simctl getenv` but writing with
`simctl spawn ... launchctl setenv`, causing each ensureEnv() cycle to
read a truncated value, fail to recognize the truncated argent path
fragments, preserve them, and append a fresh bootstrap — corrupting
the variable and clobbering third-party dylibs like SimCam.
Fix:
- Read via `simctl spawn <udid> launchctl getenv` (same store, no limit)
- Restore fs.existsSync guard to strip truncated/stale entries while
keeping third-party dylibs that exist on disk (e.g. SimCam)
- Bump mcp version to 0.4.4
* fix: detect project root via .argent dir instead of git
Flow tools previously shelled out to 'git rev-parse --show-toplevel' to
locate the project root, which failed outside git repos and pulled flows
from the wrong place in nested worktrees. Walk up from cwd looking for
an existing .argent/ directory; fall back to cwd so the first
flow-start-recording still creates it in place.
* fix: skip $HOME when walking up for .argent/
The MCP launcher stores its own state in ~/.argent/ (logs, tool-server
pid). Without this guard, running any flow tool from anywhere under
$HOME — but outside a real project with its own .argent/ — would stop
at the home directory and treat it as the project root, mixing personal
MCP state with flow files.
* revert: restore getGitRoot in flow-utils
The walk-up .argent approach is fundamentally broken because the
tool-server is a singleton daemon — its process.cwd() is not the
calling project's cwd. Reverting both 882ed4c and 83e6ae6 so the
next commit can replace the resolution with an AsyncLocalStorage
context populated from an X-Argent-Project-Root header sent by the
argent mcp proxy.
This reverts commit 882ed4c.
This reverts commit 83e6ae6.
* feat: resolve project root via AsyncLocalStorage from mcp proxy header
The tool-server is a singleton daemon shared by every MCP client,
so its process.cwd() is never the calling project's cwd. The git-
root and walk-up heuristics both ran inside the daemon and silently
returned the wrong directory.
Shift the source of truth: the argent mcp proxy is per-client, so
it IS in the client's workspace. Stamp process.cwd() on every POST
to the tool-server as X-Argent-Project-Root, run invokeTool inside
an AsyncLocalStorage scope, and expose requireProjectRoot() to any
tool that needs it. Missing/malformed headers surface a clear error
from the getter instead of silently defaulting to the daemon's cwd.
- packages/mcp/src/mcp-server.ts: callTool sends the header
- packages/tool-server/src/request-context.ts: ALS module
- packages/tool-server/src/http.ts: extract header, wrap invokeTool
- packages/tool-server/src/tools/flows/flow-utils.ts: requireProjectRoot
- packages/tool-server/test/http-project-root.test.ts: integration tests
covering present/missing/empty/malformed header, unicode roundtrip,
non-flow tools bypassing the getter, and concurrent-request isolation
* refactor: profiler tools resolve project root via requireProjectRoot
Drop the project_root input parameter from react-profiler-analyze
and react-profiler-component-source. Both now pull the root from
the AsyncLocalStorage context stamped by the argent mcp proxy,
matching flow-utils.
Second caller for requireProjectRoot so the mechanism is exercised
beyond flows. If the ALS approach turns out flaky, these two tools
break in the same way as the flow tools and fail loudly with the
same clear error message.
Update the profiler skill doc to drop the project_root arg since
agents no longer pass it.
* test: stress-test AsyncLocalStorage projectRoot mechanism
Four comprehensive test files generated by a parallel swarm audit of
the new request-context.ts / requireProjectRoot() approach, covering
every realistic way it could break.
- test/als-propagation.test.ts — 15 tests documenting what Node's
AsyncLocalStorage actually does under every async boundary the
tool-server uses. Split into SAFE baselines (Promise chains,
microtasks, timers, streams, synchronous EventEmitter emits,
concurrent frames) and LATENT TRAPS (listeners fired outside
an active frame, worker_threads, child_process). Serves as the
authoritative reference for "can I read projectRoot here?".
- test/als-nested-invocation.test.ts — 10 tests proving propagation
through registry.invokeTool nesting: two-level, deep (A→B→C→D→E),
mixed sync/async, parallel fan-out, cross-request isolation,
flow-execute style loops, error mid-chain, long chains, explicit
runWithContext re-entry overrides.
- test/als-http-edge-cases.test.ts — HTTP boundary stress tests:
malformed percent-encoding, null-byte injection, header case
variants, duplicate headers, unicode round-trip, abort mid-flight,
relative/Windows/URL-shaped paths.
- test/als-state-contamination.test.ts — adversarial audit that
surfaced two PRE-EXISTING P0 bugs the ALS fix does NOT solve:
* activeFlowName module-level singleton in flow-utils.ts
leaks between concurrent projects on a shared daemon
* profilerPathsCache in react-profiler-session.ts is keyed
only by Metro port, not projectRoot — two projects sharing
port 8081 see each other's cached profiler sessions
Both are documented via it.fails() so vitest passes today; the
day a fix lands the it.fails flips to an alert and the test
becomes a regression guard.
449/449 tool-server tests pass.
* revert: drop AsyncLocalStorage project-root approach
Reverts the ALS-based project root resolution because the approach
isn't working well in practice. Restores the previous getGitRoot
implementation in flow-utils, the project_root input parameter on
the profiler tools, and the skill doc that describes it.
Reverts:
- 3932eb5 test: stress-test AsyncLocalStorage projectRoot mechanism
- 5398cbc refactor: profiler tools resolve project root via requireProjectRoot
- b7ae895 feat: resolve project root via AsyncLocalStorage from mcp proxy header
A follow-up commit adds clearer error handling around the git call
so the agent gets a useful message when the tool-server's cwd isn't
inside a git repo.
* fix: clearer error when getGitRoot fails outside a git repo
Wrap the `git rev-parse --show-toplevel` call in getGitRoot so that
the failure surfaces a useful message to the agent instead of the
raw child_process stderr. The tool-server daemon's cwd is not the
calling project's cwd, so if the daemon happens to be launched
outside any git repository the git command fails with a terse
"fatal: not a git repository" that tells the agent nothing about
what to do next.
The new error reports the daemon's cwd, explains that flows are
anchored to a git root, and points the user at `git init`.
* refactor: require project_root as a flow tool parameter
The tool-server is a singleton daemon — its process.cwd() is never
the calling project's cwd, so flow-utils cannot infer the project
root on its own. Instead of guessing via git or walking the
filesystem, ask the calling agent directly: every flow tool now
takes project_root as a required parameter and the agent supplies
the absolute path it is working in.
This matches the existing pattern used by react-profiler-analyze,
react-profiler-component-source, and gather-workspace-data, all of
which already accept an agent-provided project/workspace path.
Changes:
- flow-utils.ts: drop getGitRoot/execFile; getFlowsDir and
getFlowPath become pure functions that take projectRoot; throw
on non-absolute paths at the boundary.
- flow-start-recording, flow-add-step, flow-add-echo,
flow-finish-recording, flow-read-prerequisite, flow-execute:
add project_root to each zod schema and plumb it through.
- argent-create-flow SKILL.md: document project_root on every
example tool call; note that it must be passed every time.
- flow-tools.test.ts: remove the flow-utils vi.mock (no longer
needed) and pass project_root: tmpDir on every call; add a
regression test rejecting relative project_root.
* prettier
* cleanup skill
* refactor: store project_root on flow-start-recording, not on every tool
The tool-server cannot infer the calling project's working directory
on its own. The agent now provides it once — as project_root on
flow-start-recording, which stores it alongside activeFlowName in
module state. All other flow tools (flow-add-step, flow-add-echo,
flow-finish-recording, flow-execute, flow-read-prerequisite) read
the stored value and require no project_root parameter.
This matches the existing "active flow" pattern: just as flow name
is tracked automatically after flow-start-recording, so is the
project root.
* feat: require project_root on flow-execute and flow-read-prerequisite
These tools are independent entry points that may be called without a
prior recording session (e.g. after daemon restart), so they cannot
rely on module state set by flow-start-recording. Adding project_root
as an explicit parameter mirrors the pattern already used by
flow-start-recording and react-profiler-analyze.
* fix: make requireActiveProjectRoot error message tool-agnostic
The previous message pointed only at flow-start-recording, but the
check is now reached from flow-execute and flow-read-prerequisite as
well.
* refactor: store flows in .argent/flows/ instead of .argent/
Keeps the .argent/ directory clean for other argent-related artifacts
and groups flow files under a dedicated subdirectory.
* style: prettier --write
process.exit() is synchronous and does not flush Node's WriteStream
buffer when stdout is a pipe. For large outputs like the ~61 KB
tool manifest from `-t`, the tail was being truncated, causing
`argent init` to fail with "Unterminated string in JSON" when
parsing the child's output in getAvailableToolIds.
* Reapply "feat: enhance React profiler session handling with device ID support"
This reverts commit 2bf25cb950.
* Reapply "refactor: remove device-related fields from React profiler session"
This reverts commit badf127411.
* Reapply "refactor: unify device ID handling across debugger and profiler tools, make device_id required, not optional"
This reverts commit c6f1fd7578.
* format: run format
* refactor: enforce device ID requirement in JsRuntimeDebugger and ReactProfilerSession blueprints
* feat: add appName and deviceName to React profiler session and related tools
* fix: update React profiler session output to reflect accurate field names for runtime and project details
* skills: enhance skills for Metro debugger and React profiler tools to clarify device_id usage and connection diagnostics
* format: run format
* test: update NetworkInspector integration tests to include mock-device in service resolutions