Commit Graph

366 Commits

Author SHA1 Message Date
Filip131311 13cbc7e4c8 chore: apply prettier formatting 2026-05-20 11:51:17 +02:00
Filip131311 20fb8ba0ee fix(describe): fall back to systemElements when primaryApp walk is empty
The ax-service daemon already collects two AX trees: `elements` (foreground
app via `[AXElement primaryApp]`) and `systemElements` (Springboard via
`[AXElement systemApplication]`, gated on `isSystemAppShowingAnAlert`). The
TS blueprint silently dropped the second one, so any dialog presented
outside the foreground app's process tree was invisible to `describe`.

Wire `systemElements` through the typed response and let the iOS adapter
use it as a fallback when the `primaryApp` walk produced zero usable
children. In-process dialogs (UIAlertController, Settings sheets) already
land in `elements`, so the fallback never fires for them and we avoid
duplicating the same buttons. Status-bar leakage from the fallback path is
accepted as cheap and rare.
2026-05-19 14:16:13 +02:00
Ignacy Łątka 95d2ffa0e4 fix(skills): remove iOS bias from skill/tool docs (#212)
We've had multiple instances of iOS-biased references in our skills.
This PR makes these references platform-agnostic.
 
<details>

## Summary

After Android emulator support landed (#148), several skill docs and
tool descriptions still treated iOS as the only/default platform. A QA
reported that asking the agent to "test … using argent" makes it spin up
an iOS simulator even when an Android emulator is already running.

These are **minimal de-biasing edits** — mostly one-word `simulator` →
`device` swaps. Android is **not** promoted above iOS; the incidental
iOS bias is just removed where it was unnecessary. Genuinely iOS-only
tools (`paste`, `native-*`, `native-profiler-*`) are left untouched
since the capability gate already rejects them on Android.

## Changes (14 files, +43/-29)

- **`argent.md` rule**: one-line `PLATFORM DETECTION` note (call
`list-devices` first, don't default to iOS); `simulator actions` →
`device actions`; mention `adb` next to `xcrun` in the escape-hatch
bullet. Routing order unchanged (iOS still listed first).
- **Skill docs**: `native iOS app navigation` → `app navigation`;
`simulator` → `device` where the action is platform-neutral; added the
missing `argent-android-emulator-setup` row to the RN-workflow Related
Skills table; `argent-ios-simulator-setup` description scoped to iOS.
- **`flow-run` / `flow-start-recording`**: `simulator interactions` →
`device interactions`.
- **`network-logs` / `network-request`**: `device_id` description `iOS
Simulator UDID` → `Device UDID`.
- **`list-devices`**: merged result now sorts by readiness so a booted
Android emulator is not buried beneath idle iOS simulators (the only
logic change).

## Test plan

- [x] `npx tsc --build` clean
- [x] `@argent/tool-server` 478 tests pass; `@argent/mcp` 51;
`@argent/installer` 231; `@argent/registry` 57
- [x] `prettier --check` clean
- [ ] Sanity-check that an agent with only an Android emulator booted no
longer reaches for an iOS simulator

</details>
2026-05-19 11:57:42 +02:00
filip131311 94fa867818 feat(describe): render tree as text to cut token cost (#197)
## Summary
- Move the JSON tree out of the describe tool's response and add an
in-between layer (`format-tree.ts`) that converts it to a sectioned text
rendering. On a typical iOS screen the new `description` field is ~6×
cheaper on the wire than the previous nested tree once braces, keys, and
indentation are counted.
- Public response shape becomes `{ description, source, should_restart?
}`. All existing pruning (Android v2 trimmer in `uiautomator-parser.ts`,
iOS frame clamp in `ios-ax-adapter.ts`) is untouched — the new layer
only re-presents what the platform adapters already produced.
- Header now spells out the coordinate space and the tap-point formula
so an agent can act on a frame without further docs.

## How it renders
Two modes picked automatically from the tree shape:
- **flat** (iOS ax-service): bucket children by `frame.y` into Top bar /
Body / Composer / Bottom; split the bottom zone into keyboard rows by
rounded y and classify each row (letters / shift / return /
globe-dictate).
- **nested** (Android uiautomator, iOS native-devtools fallback):
iterative DFS with depth indentation, preserving `clickable` /
`scrollable` / `scrollHidden` / `disabled` / `password` flags inline.

Example (Messages with keyboard up):
```
Source: ax-service
Mode: flat
Coordinates are normalized [0,1] fractions of the screen (x, y, width, height), not pixels — pass them straight to gesture-tap / gesture-swipe / gesture-pinch, which expect this same space. To tap an element, use its centre: tap_x = frame.x + frame.width / 2, tap_y = frame.y + frame.height / 2.

ROOT  AXGroup (0.000, 0.000, 1.000, 1.000)

— Top bar —
  AXButton     "Messages"  (0.040, 0.071, 0.109, 0.050)
  AXButton     "+1 (888) 555-1212"  (0.267, 0.134, 0.466, 0.037)

— Body / content —
  AXStaticText "iMessage  Encrypted"  (0.040, 0.156, 0.920, 0.030)
  ...

— Bottom row 1: letters —
  AXImage      "Q"  (0.012, 0.675, 0.098, 0.062)
  ...
```

## Files
- `packages/tool-server/src/tools/describe/contract.ts` — split into
`DescribeTreeData` (internal) and `DescribeResult` (public)
- `packages/tool-server/src/tools/describe/index.ts` —
`withDescription(data)` wrapper applied to both platform handlers
- `packages/tool-server/src/tools/describe/format-tree.ts` — the
renderer (new)
-
`packages/tool-server/src/tools/describe/platforms/{ios,android}/index.ts`
— return `DescribeTreeData`
- `packages/tool-server/test/describe-tool.test.ts` — assertions
migrated to `result.description` / `result.source` /
`result.should_restart`
- `packages/tool-server/test/describe-format-tree.test.ts` — renderer
unit tests (new): flat zoning, nested indentation + flags,
embedded-newline escaping, empty tree

## Test plan
- [x] `npm test` in `packages/tool-server` — 49 files / 482 tests
passing locally
- [x] `tsc --build` at workspace root — clean
- [x] Live MCP sanity check: `argent run describe --udid <booted>
--json` returns the new shape; tested against Messages (full keyboard
surface), Safari (chrome-only, expected), and a minimal in-process
WKWebView host. Existing pruning behaviour unchanged in every case.
2026-05-19 11:54:13 +02:00
Paweł Fornagiel a0a43522c8 docs(skills): device selection guidance for list-devices and RN runs (#203)
## Summary

This change improves how Argent skills tell agents to pick and use
devices before booting, running Metro, or driving UI. It reduces
wrong-platform assumptions and encourages explicit targets derived from
`list-devices`.

## Fixes #196

## What changed

- **`packages/skills/rules/argent.md`**: Added a
`<device_selection_rule>` block that instructs agents to call
`list-devices` first, then follow a clear decision order: explicit user
intent → prefer already-running devices → ask when both iOS and Android
are ready with no intent → boot or ask for cross-platform vs
single-platform projects using environment inspector signals.
- **`argent-react-native-app-workflow/SKILL.md`**: RN run commands now
require an explicit simulator name or Android `deviceId` from
`list-devices`; minor checklist wording tweak for booting devices.
- **`argent-test-ui-flow/SKILL.md`**: Clarifies resolving the test
device via `list-devices` and `<device_selection_rule>` before
per-platform setup.

## Motivation

Agents sometimes assumed a platform or picked an arbitrary device.
Aligning skills with `list-devices`-first behavior makes device choice
observable and consistent with multi-platform workflows.
2026-05-19 10:28:32 +02:00
filip131311 b75cb3bb42 chore: add Dependabot config (npm workspaces + GitHub Actions) (#213)
## Summary
- Adds `.github/dependabot.yml` to enable weekly version + security
updates.
- Covers the **npm workspaces monorepo** (`/` + `/packages/*`) and
**GitHub Actions** used in `.github/workflows`.
- Groups updates to keep PR noise low:
- `npm-minor-patch` — one PR/week bundling all minor/patch bumps across
the workspace.
- `npm-major` — separate grouped PR for majors (typically needs human
review).
  - `npm-security` — grouped security updates across all directories.
  - `github-actions` — one PR/week for all Actions bumps.
- Uses `directories:` (plural) with the `/packages/*` glob so every
workspace package is monitored; majors stay separate from minor/patch so
reviewers can triage breaking changes without blocking routine bumps.
- `gitsubmodule` ecosystem was intentionally **not** enabled —
`packages/argent-private` is an internal submodule maintained by the
same org, so SHA-bump PRs would be noise rather than signal.

## Notes
- Schedule: weekly, Monday 06:00 UTC.
- Commit prefixes follow the existing `chore(...)` convention used in
this repo (`chore(deps)`, `chore(deps-dev)`, `chore(ci)`).
- `open-pull-requests-limit` set to 10 (npm) / 5 (actions) to bound the
queue.

## Test plan
- [ ] Merge to `main`; confirm Dependabot picks up the file (visible
under repo *Insights → Dependency graph → Dependabot*).
- [ ] First scheduled run on the following Monday produces grouped PRs
(or trigger immediately via *Check for updates* in the Dependabot tab).
- [ ] Verify a sample PR's diff touches the expected workspace
`package.json` + root `package-lock.json`.
2026-05-18 17:22:22 +02:00
filip131311 cc9d5d035b Add Dependabot configuration file
Configure Dependabot for version updates with weekly schedule.
2026-05-18 17:15:38 +02:00
Ignacy Łątka 1bfe516fbd fix(deps): resolve dependabot alerts (#211)
Manual dependabot fixes.

## Details

<details>

**Lockfile-only.** `package.json` is unchanged; all bumps are within
parents' already-declared semver ranges (API-compatible by semver
contract). Refreshing the stale lock to the in-range patched versions:

- `fast-uri` 3.1.0 → **3.1.2** — *shipped*
- `path-to-regexp` 0.1.12 → **0.1.13** — *shipped* (upstream fix = two
`backtrack = ''` resets)
- `hono` 4.12.3 → 4.12.19, `@hono/node-server` 1.19.9 → 1.19.14,
`express-rate-limit` 8.2.1 → 8.5.2, `router`'s `path-to-regexp` 8.3.0 →
8.4.2 — *not shipped* (SDK HTTP/SSE transport; argent uses stdio only →
tree-shaken out of bundles). Same low-risk stale-lock class, bumped in
the same pass.
- `ip-address` 10.0.1 → 10.2.0 — benign in-range cascade from
`express-rate-limit@8.5.2`'s own manifest.

`npm audit` HIGH for shipped/runtime deps: **7 → 0**.

## Scope

In scope (the QA finding): the two shipped HIGH advisories — fixed and
proven present in freshly-built bundles.

Out of scope (deliberately): the remaining ~7 `npm audit` entries are
**exclusively the dev-only vitest toolchain** (`vite`, `vite-node`,
`esbuild`, `postcss`, `picomatch`, `@vitest/mocker`, `vitest`). Not
shipped, not runtime. Their fix requires a `vitest` **semver-major** — a
separate, breaking change.

## Verification

- `tsc --build` clean; **998 tests pass** across all 7 workspaces;
`prettier --check .` clean.
- Bundles rebuilt from the patched tree; MCP stdio + tool-server E2E
confirmed working.
- 4 independent reviews (correctness / scope / edge-cases / ripple): all
PASS. Edge-case review ran an **offline differential** of every
tool-server express route under 0.1.12 vs 0.1.13 (byte-identical
`RegExp` source + keys) and a **live E2E** on the running patched
tool-server — route matching is unchanged. `fast-uri` 3.1.2 only rejects
more malformed URIs; `ajv` uses it solely for schema `$id`/`$ref`, never
runtime data.
- `npm ci` is byte-stable against this lockfile (does not regenerate
it); `npm pack --dry-run -w @swmansion/argent` passes.

## Residual `npm audit`

7 vulns remain (2 high, 5 moderate) — all dev-only vitest toolchain,
tracked separately (needs `vitest` major).

</details>
2026-05-18 16:54:33 +02:00
pFornagiel 12f6ff1115 chore: bump to 0.7.1 v0.7.1 2026-05-15 16:55:57 +02:00
Paweł Fornagiel 8a3b5a1ff7 fix(react-profiler): self-bootstrap DevTools backend when no client is attached (#205)
## Summary

`react-profiler-start` failed in any RN bridgeless dev build with no
external React DevTools client (Fusebox React tab / `npx
react-devtools`) connected — returning the misleading error *"No React
renderer interface attached yet. Wait for the app to render its first
commit and retry."* even when the app had rendered. This PR makes the
tool self-bootstrap the DevTools backend so profiling works out of the
box, and replaces the one-size-fits-all error with mode-specific
actionable messages.

Companion to #199 (schema fix). With both landed, `react-profiler-start`
is end-to-end usable from MCP clients against a vanilla RN 0.76 dev app
with zero extra setup.

> [!IMPORTANT]
> The bootstrap path mutates global runtime state (Bridge + Agent + hook
subscriptions via `connectWithCustomMessagingProtocol`). Unit tests
cover every reason branch, but the live behavior — especially
coexistence with a later-attached `npx react-devtools` client and the
absence of subscription leaks across repeated start/stop cycles — must
be **smoke-tested against a working RN application** before this merges.

## Root cause

argent's profiler delegates React commit capture to the in-app DevTools
backend via `rendererInterface.startProfiling(...)`. The two maps on
`__REACT_DEVTOOLS_GLOBAL_HOOK__` look similar but are populated by
different actors:

| Map | Populated by | When |
|---|---|---|
| `hook.renderers` | React itself via `hook.inject(renderer)` | First
render |
| `hook.rendererInterfaces` | DevTools backend's `attach(hook, id,
renderer, global)` (in `react-devtools-shared/src/backend/renderer.js`)
| When `initBackend(hook, agent, global)` runs |

In an RN 0.76 bridgeless dev build, `setUpReactDevTools.js` runs at
bundle load and calls `react-devtools-core.connectToDevTools({ host:
'localhost', port: 8097, websocket })`. That opens a WebSocket;
`initBackend` only runs on `ws.onopen`. With no DevTools client
listening on 8097, the connection never opens, `initBackend` never runs,
`rendererInterfaces` stays empty — but `hook.renderers` is populated
because React doesn't care about the backend.

So `rendererInterfaces.size === 0` while `renderers.size === 2` is the
steady-state of a dev build with no DevTools client. argent's pre-flight
check saw the empty map and threw with a misleading message ("wait for
first commit") that conflated three distinct failure modes:
1. App hasn't rendered yet (legitimate wait).
2. App rendered but no DevTools backend attached (this PR — the common
case).
3. Production build / no `react-devtools-core` in bundle (different
recovery).

Direct CDP verification of the hook from the bug state:
```js
{ rIds: [1, 2], riIds: [], reactDevtoolsAgent: undefined }
```

## Fix

When the pre-flight detects `rendererInterfaceFound: false` with the
hook present, `react-profiler-start` now runs a self-bootstrap script
(`BOOTSTRAP_DEVTOOLS_BACKEND_SCRIPT`) that:

1. **Looks up `react-devtools-core` in the Metro module registry** by
scanning `__r.getModules()` entries for a `verboseName` matching
`react-devtools-core/dist/backend`. Handles both `Map<id, meta>` (modern
Metro) and array-of-meta (older Metro) shapes — mirrors the pattern in
`debugger-component-tree`.

2. **Calls `connectWithCustomMessagingProtocol`** (rdt-core ≥5.1) with
no-op `onSubscribe` / `onUnsubscribe` / `onMessage` handlers. The
function internally constructs a `Bridge` + `Agent` and runs
`initBackend(hook, agent, window)`, which iterates `hook.renderers` and
calls `attach()` on each — exactly the call path that populates
`rendererInterfaces`. The fake wall discards every outbound message,
which is fine because argent calls into the rendererInterface directly
and never reads from the bridge.

3. **Returns one of seven distinct reasons**, each mapping (via
`bootstrapFailureMessage` in
`utils/react-profiler/devtools-bootstrap.ts`) to its own actionable
error message:
   - `no-hook` → "production build" guidance
   - `no-renderers` → "wait for first commit"
   - `no-metro-modules` → bundle isn't Metro-style
- `no-rdt-module` → react-devtools-core stripped from bundle
(production)
- `unsupported-rdt-version` → rdt-core <5.1 (RN <0.74) → fall back to
`npx react-devtools`
- `metro-scan-error` / `bootstrap-threw` / `bootstrap-no-effect` →
unexpected, with verbatim error text

After a successful bootstrap, `REACT_NATIVE_PROFILER_SETUP_SCRIPT` is
**re-run** to install argent's `__argent_startWrapped__` wrappers on the
freshly-attached interfaces. Without this re-run, `buildStartScript`'s
post-start `__argent_isProfiling__` check would falsely report
`startProfiling-threw` (the wrapper sets the flag; an unwrapped
interface leaves it false even on a successful start).

### Idempotency / leak protection

`initBackend` adds subscriptions to `hook.sub('renderer', ...)` on every
call, so naive re-invocation would leak listeners. The script
short-circuits with `reason: 'already-attached'` when
`rendererInterfaces.size > 0`, so repeated `react-profiler-start` calls
within a session never re-bootstrap. Verified empirically: listener
counts stay flat across multiple start/stop cycles.

### Coexistence with external DevTools

If `npx react-devtools` is later started while argent has already
bootstrapped, RN's bundled reconnect path calls `connectToDevTools`
again, which goes through `attachRenderer` → sees `rendererInterface !=
null` (argent's already there) → re-emits `renderer-attached` without
re-attaching. Both backends coexist; argent talks to the renderer
interface directly, so the active "agent" is irrelevant to profiling.

## Files changed

- `packages/tool-server/src/utils/react-profiler/scripts.ts` — adds
`BOOTSTRAP_DEVTOOLS_BACKEND_SCRIPT` (the injected IIFE that does the
Metro scan + connect call).
- `packages/tool-server/src/utils/react-profiler/devtools-bootstrap.ts`
*(new)* — `BootstrapResult` type, `BootstrapReason` union, and the
`bootstrapFailureMessage()` helper that maps each reason to its
agent-facing error. Mirrors the `session-ownership.ts` convention (types
+ pure helpers as a sibling of `scripts.ts`, unit-testable without CDP /
vitest mocking).
-
`packages/tool-server/src/tools/profiler/react/react-profiler-start.ts`
— calls bootstrap on empty `rendererInterfaces`, re-runs setup, throws
via the imported `bootstrapFailureMessage`. Also replaces two
pre-existing error strings ("hook not present", `how_to_reclaim`) with
the same agent-friendly phrasing used elsewhere in this PR.
-
`packages/tool-server/test/react-profiler/bootstrap-devtools-backend.test.ts`
*(new)* — 12 unit tests covering every reason branch (Map + array Metro
registries, require() throws, getModules throws, connect throws, connect
is a no-op, idempotent already-attached path).

## Manual verification (iPhone Air, RN 0.76.7 bridgeless, no external
DevTools)

1. Reload bundle → confirm `rIds: [1,2], riIds: []` (bug state).
2. Pre-fix: `react-profiler-start` returns *"No React renderer interface
attached yet…"*
3. Post-fix: `react-profiler-start` returns `{ started_at,
hermes_version: "for RN 0.76.7", detected_architecture: "bridgeless" }`.
Hook now shows `riIds: [1,2]`, `__argent_startWrapped__: true` on each,
`reactDevtoolsAgent` set, `__argent_isProfiling__: true`.
4. Forced a React state-hook dispatch during the session and stopped:
`total_react_commits: 2`, `unattributed_ms: 8.41` — real commit data
captured through the bootstrapped backend.
5. Second `react-profiler-start` in same session returns success via
`already-attached` early-exit (verified directly against the script).
Hook listener counts (`renderer`, `renderer-attached`, `react-devtools`)
stable across multiple cycles — no subscription leak.
6. Simulated `no-renderers` by clearing `h.renderers` → got *"React has
not rendered yet, so there is nothing to profile. Ask the user to wait
until the app shows React content…"*
2026-05-15 16:52:56 +02:00
Copilot 74481155af fix: remove hardcoded model: haiku from argent-environment-inspector skill (#206)
Removes the `model: haiku` frontmatter field from
`packages/skills/agents/argent-environment-inspector.md`.

The hardcoded model caused non-Anthropic coding harnesses (e.g. Gemini)
to fail when loading the skill, since `haiku` is an Anthropic-specific
model identifier that those runtimes don't recognize. Dropping the field
lets each harness use its own default model.

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: kacperkapusciak <39658211+kacperkapusciak@users.noreply.github.com>
2026-05-15 16:44:38 +02:00
Ignacy Łątka bea43f555a feat: per-step delayMs on flow steps (#198)
- Tool steps in a flow YAML may carry an optional `delayMs` that sleeps
that long *before* the step runs; `flow-add-step` exposes the same
field.
- New `longRunning?: boolean` field on `ToolDefinition`; when set, the
MCP fetch wrapper bypasses its 30s per-request timeout (`fetchTimeoutMs:
null`) so long invocations aren't aborted. `run-sequence` and
`flow-execute` opt in.
2026-05-13 18:32:42 +02:00
Ignacy Łątka 8ce8f8e573 fix: 100% CPU usage on broken stdio (#191)
One of our users reported occasional 100% CPU usage stale MCP server
process.

I wasn't able to reproduce this bug directly in Codex. We only have
non-codex repros that are able to consistantly reproduce a bug that
*looks* like the one reported to us.

The bug in question is: Our error handler causes errors which throws it
into an infinite loop.
 
<img width="713" height="251" alt="image"
src="https://github.com/user-attachments/assets/4b9fad41-55d0-49ab-b19c-25b7f5c12d12"
/>

### More details:

When `argent mcp` runs as an orphaned process whose stderr pipe is
broken (e.g. parent died), every `stderr.write` from the
uncaughtException handler emits an async 'error' event. Without an
'error' listener that becomes another uncaughtException, runs the
handler again, and so on forever.

Move the handlers into `fatal-handlers.ts` and add three guards:
- 'error' listeners on stdout/stderr exit cleanly when stdio is broken,
before the failure becomes another uncaughtException
- try/catch around `stderr.write` so a sync write failure exits instead
of escaping
- try/catch around the formatter so a throwing `.stack` getter or
`toString` (the production trace pointed at defaultPrepareStackTrace)
can't take down the handler

Idempotent: a second `installFatalHandlers` call is a no-op.
2026-05-12 15:40:43 +02:00
Ignacy Łątka 837d088744 test: cover #148 (android) (#186)
This PR adds tests for #148.

## Summary

Moves the Android emulator branch's test coverage out of #148 so that PR
can be reviewed without the test churn. This branch adds the test files
back on top of the (now test-stripped) base branch.

Tests inseparable from source changes (`describe-tool`,
`workspace-reader`, `workspace-reader-integration`) and the
`boot-simulator.test.ts` deletion stayed on #148 - base PR's CI passes
on its own.

## Test plan

- [x] `npx tsc --build` — clean
- [x] `npx prettier --check .` — clean
- [x] `npm test --workspaces --if-present` — 585/585 tool-server tests
pass; all other workspaces green

---------

Co-authored-by: Paweł Fornagiel <pawel.fornagiel2@gmail.com>
2026-05-12 14:21:09 +02:00
filip131311 85e7aa9612 fix(registry): emit JSON Schema type for ZodDefault fields (#199)
## Summary

`zodTypeToJsonSchema` in `packages/registry/src/zod-to-json-schema.ts`
did not handle `z.ZodDefault`. Any tool input field declared as
`z.X().default(...)` fell through to `{}`, dropping the type.
Downstream, the MCP harness sent values for those fields as strings, and
the tool-server's strict Zod validators rejected them with `Expected
number, received string` / `Expected boolean, received string`.

Also fixes a related issue in the same file: `isOptional` returned
`false` for `ZodDefault`, so defaulted fields were pushed into
`required` even though the caller doesn't have to provide them.

Tools that misbehave in practice today because of this and start working
after this fix:
- `stop-metro` — `port`
- `react-profiler-start` — `port`, `force`, `sample_interval_us`
- `debugger-component-tree` — `onScreenOnly`, `includeSkipped`

(Reproduced against the published `argent 0.6.1` MCP server while
profiling an Expo app — the `argent run <tool> --args '<json>'` CLI path
is unaffected because it doesn't go through the published JSON Schema.)

## Changes

- `packages/registry/src/zod-to-json-schema.ts`: handle `ZodDefault`
(recurse into `_def.innerType`, attach `default: defaultValue()`); treat
`ZodDefault` as optional.
- `packages/registry/tests/zod-to-json-schema.test.ts`: new file. Covers
`z.number()`, `z.boolean()`, `z.string().optional()`,
`z.number().default(...)`, `z.boolean().default(...)`,
`z.coerce.number().int().positive().default(...)`, and a mixed-object
happy path.

## Test plan

- [x] `npx vitest run` in `packages/registry` — 64 passed, 0 failed (4
files; the new test file contributes 7 cases).
- [ ] Reviewer sanity check: load the rebuilt MCP server and confirm a
tool with a `.default(...)` numeric/boolean field now publishes the
correct \`type\` in its input schema.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:00:50 +02:00
pFornagiel 60ae7cb767 chore: bump versions to 0.7.0 v0.7.0 2026-05-11 16:44:03 +02:00
pFornagiel ad502a46ed chore: bump argent-private submodule 2026-05-11 16:36:10 +02:00
Ignacy Łątka d017ba340c feat: Android emulator support (#148)
### For reviewers - what to focus on:

The diff is ~6k LoC; this breakdown should help triage where to spend
attention:

| Bucket | Lines | Effort |
| --- | --- | --- |
| **Net-new Android logic**: `boot-device.ts` (660) + `adb.ts` (399) +
`uiautomator-parser.ts` (292) | 1351 | **Read carefully** |
| Blueprint `DeviceInfo` refactor: `simulator-server`, `ax-service`,
`native-devtools` | 365 | Read |
| Tool `index.ts` / `types.ts` sprawl across ~20 tools (capability
gates, `services()`, descriptions) | ~965 | Skim — watch for cross-file
consistency |
| `platforms/android.ts` impls (stub → real implementation) | ~426 |
Skim |
| Skill/docs prose, profiler rename ripple | ~1000 | Skim |
| **`platforms/ios.ts` deletions** — 12 files, 0 additions; logic moved
into unified handlers | 684 | **Skip — mechanical** |

## What works, what doesn't:

### Just works thanks to either sim server or metro:
- `gesture-*`
- `button`, `keyboard`
- `screenshot`
- `run-sequence`, `flow-*`
- `debugger-*`
- `react-profiler-*`

### Adjusted to work on android too:

| tool | android | ios |
| --- | --- | --- |
| `describe` | `uiautomator dump` → `DescribeNode` | `AXServiceApi` |
| `launch-app` | `cmd package resolve-activity` + `am start -W` |
`simctl launch` |
| `restart-app` | `cmd package resolve-activity` + `am force-stop` + `am
start -W` | `simctl terminate` + `simctl launch` |
| `reinstall-app` | `adb install -r` (+`-g`/`-d`) | `simctl uninstall` +
`simctl install` |
| `open-url` | `am start -a VIEW -d <url>` | `simctl openurl` |
| `list-devices` | `adb devices` + `emulator -list-avds` | `xcrun simctl
list --json` |
| `boot-device` | snapshot probe → `-force-snapshot-load` hot boot with
screencap-alive guard, falls back to cold boot on any failure | `simctl
boot` + `bootstatus -b` |

### Will not work on android for now:
- `native-devtools-*` (7 total)
- `native-profiler-*` (3 total — was `ios-profiler-*`)
- `profiler-stack-query`, `profiler-combined-report` (depend on
`native-profiler-*`)
- `paste` — unregistered on both platforms for now.

---------

Co-authored-by: Paweł Fornagiel <pawel.fornagiel2@gmail.com>
2026-05-11 11:39:10 +02:00
filip131311 626c3274e2 chore: bump package versions to 0.6.1 (#193)
## Summary
- Bumps all 9 published packages from `0.6.0` → `0.6.1`.
- 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
v0.6.1
2026-05-08 12:48:23 +02:00
Ignacy Łątka 9cd453b857 fix(skills): prefix bare skill references with 'argent-' (#183)
All skills are currently exposed as `argent-***`. In the markdown
though, we refer to them as just `***`. This may confuse the agent when
it tries reading `skills/***` instead of `skills/argent-***`.

This PR changes all skill references in docs to the correct `argent-***`
form.

## Why?
There's no downside to this fix. Keeping it as-is potentially introduces
some confusion especially if when the user is running a weaker model or
if we encounter namespace conflicts.
2026-05-06 16:48:46 +02:00
Paweł Fornagiel 529fffb322 fix installer update version detection under npx (#188)
## Summary
- fix `argent update` version detection when invoked via `npx
@swmansion/argent`, so it compares against the actually installed global
binary instead of the transient npx-cached package
- add `getGloballyInstalledVersion()` that resolves the real global
binary path (`which -a` / `where`), follows symlinks, and reads the
owning `package.json`
- add regression tests covering staged global-install layouts and
npx-style transient paths to prevent false "already on latest" results
2026-05-06 16:47:56 +02:00
Ignacy Łątka 2bdc8ca323 chore: bump package-lock.json (#187)
This PR fixes stale `package-lock.json` on main.
It's been adding noise to my other PRs so i'm fixing it in this one.

### Why?

#158 forgot to regenerate `package-lock.json`
2026-05-05 12:26:01 +02:00
Paweł Fornagiel 55d1567d48 feat: improve iOS profiler startup, shutdown, and exit recovery (#181)
## Summary

Refactor of the iOS profiler (`xctrace`-driven) session to make start,
stop, and unexpected-exit handling proper, without the iOS hanging
infinitely.

### Startup (`ios-profiler-start`)
- **Locale ready signal**: subscribe-before-spawn to a Darwin
notification (`com.argent.ios-profiler.started.<pid>.<ts>`) via
`notifyutil`, passed through `xctrace --notify-tracing-started`. Falls
back to the existing stdout substring match (`Starting recording` /
`Ctrl-C to stop`) if `notifyutil` fails to spawn or register within 2 s.
- **Startup timeout**: new 10 s cap on reaching the ready state — on
timeout, `SIGKILL` the child so it cannot leak.
- **Bounded `simctl` calls**: `detectRunningApp` now applies a 10 s
timeout to both `xcrun simctl spawn ... launchctl list` and `xcrun
simctl listapps`, with explicit error messages telling the user to
verify the simulator is booted.
- **Cold-start retry**: if xctrace's process resolver fails with
`"Cannot find process matching name:"` (launchd has registered the
bundle but xctrace's resolver hasn't caught up), the tool retries once
after a 1.2 s backoff — up to 2 attempts total. On exhausted retries
throws a classified error naming the cold-launch race and pointing at
`app_process` as the override. Non-cold-start failures propagate
immediately without retry.
- **Exit listener installed**: every `xctrace record` child gets a
long-lived `'exit'` handler (`handleXctraceExit`) that marks the session
as `recordingExitedUnexpectedly` and stores `lastExitInfo`.

### Shutdown (`ios-profiler-stop`)
- **SIGINT → SIGTERM → SIGKILL ladder** via
`utils/ios-profiler/lifecycle.ts::shutdownChild` (30 s grace, 5 s term,
5 s kill).
- **Returns a `warning`** when the ladder had to escalate past SIGINT,
so callers know the trace bundle may be incomplete.

### Exit recovery
- **New `recordingTimedOut` / `recordingExitedUnexpectedly` flags** on
the session API. If `ios-profiler-stop` is called after either fired,
the tool now exports whatever `.trace` bundle is on disk and returns it
with a descriptive `warning` instead of throwing "no active session".
- Covers the in-process 10-min recording cap, attached-app crashes, and
simulator daemon hiccups.

### Dispose semantics
- Registry teardown (process shutdown) skips SIGINT and goes straight to
`SIGKILL` with a 1 s reap window. The explicit `ios-profiler-stop`
contract is the only path that takes the slow finalise grace.

### Other
- `utils/ios-profiler/run-with-timeout.ts`: `execSyncWithTimeout`
wrapper with a 60 s default — guards every shelled-out `xctrace export`
/ `xctrace version`.
- New reference doc `utils/ios-profiler/IOS_PROFILER_REFERENCE.md`
covering the native foundations, capture flow, and pipeline.

## Tests
New unit tests under `packages/tool-server/test/ios-instruments/`

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-05 09:26:57 +02:00
filip131311 dd702fc720 fix: detect missing global install in argent update (#180)
## 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
2026-05-04 16:23:02 +02:00
filip131311 bb3f484d04 chore: bump package versions to 0.6.0 (#178)
## 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
v0.6.0
2026-04-30 16:02:33 +02:00
filip131311 f004c08a19 chore: bump package versions to 0.6.0-next.2 (#177)
## 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
v0.6.0-next.2
2026-04-30 15:51:32 +02:00
Ignacy Łątka 127a32a101 fix: installer throwing error (#175)
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.
2026-04-30 15:45:28 +02:00
Kacper Kapuściak 4d826ca01c chore: update header image in README.md (#176)
Due to popular vote here's an Argent branded banner in the readme 
<img width="993" height="597" alt="image"
src="https://github.com/user-attachments/assets/30b62430-3888-4e50-a3c7-eb84fef6cad8"
/>
2026-04-30 15:45:01 +02:00
filip131311 1f1f55f72e chore: bump package versions to 0.6.0-next.1 (#174)
## 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
v0.6.0-next.1
2026-04-30 13:08:22 +02:00
filip131311 22a4811cb1 fix(tool-server): use semver for update-checker comparison (#173)
## 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.
2026-04-30 13:05:24 +02:00
filip131311 0dca6bbfef fix: build workspace deps before packing/publishing argent (#172)
## Summary
- The `publish` and `publish-next` workflows ran `npm run build -w
@swmansion/argent` directly, but after the recent CLI/MCP/main split,
`@swmansion/argent` imports types from `@argent/cli`, `@argent/mcp`,
`@argent/installer`, `@argent/tools-client`. Those packages expose types
from their own `dist/index.d.ts`, which doesn't exist until they're
built — so argent's `tsc` failed with `TS2307: Cannot find module
'@argent/...'` (e.g. [run
25161244218](https://github.com/software-mansion/argent/actions/runs/25161244218)).
- Added `npx tsc --build` before the per-workspace argent build in both
publish workflows so the sibling packages produce their `dist/` first.
This mirrors what `unit-tests.yml` already does, which is why the
regression didn't surface on PRs.
- Applied the same fix to the root `pack` script (`npm run pack`) since
it had the same latent bug for local packing.

## Test plan
- [x] Reproduced the failure locally by wiping every `dist/` and
`tsconfig.tsbuildinfo`, then running `npm run build -w
@swmansion/argent` — same four `TS2307` errors as CI.
- [x] Verified `npx tsc --build && npm run build -w @swmansion/argent`
succeeds from a clean state.
- [ ] Re-run the `Publish prerelease to npm (next)` workflow against
this branch (or after merge) to confirm green.
2026-04-30 12:57:28 +02:00
filip131311 be31093f6c chore: bump package versions to 0.6.0-next.0 (#171)
## 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
v0.6.0-next.0
2026-04-30 12:45:57 +02:00
Ignacy Łątka 289fda7ea5 fix: opencode detect alternative filenames (#170)
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"
/>
2026-04-30 12:39:53 +02:00
Ignacy Łątka aac6151cc9 fix: preserve old Zed config (#168)
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>
2026-04-30 12:00:07 +02:00
Kacper Kapuściak 72e0d4d6fd chore: update banner image in README (#169)
Updated the README banner to be Software Mansion branded 
<img width="955" height="594" alt="image"
src="https://github.com/user-attachments/assets/eb57abd2-2870-483a-9719-447ebbdea30b"
/>
2026-04-30 08:36:36 +02:00
Paweł Fornagiel 62d0fe0c73 refactor: overhaul React profiler - DevTools backend, session ownership, richer data (#156)
## 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 |
2026-04-29 19:20:50 +02:00
filip131311 c1fda3834b fix: remove private vscode config from the code base (#167) 2026-04-28 16:44:16 +02:00
filip131311 dbf4de1ab0 refactor(tool-server): per-tool dirs + cross-platform dispatch (iOS only, Android-ready) (#164)
## 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.
2026-04-28 15:32:39 +02:00
Ignacy Łątka e226fe5404 test: speed up long unit tests (#153)
## 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`.
2026-04-28 15:15:03 +02:00
Ignacy Łątka de2d000300 feat(tool-server): prefix stdout/stderr lines with ISO timestamp (#155)
Adds timestamp to every `tool-server.log` line.

`mcp-calls.log` already has timestamps, so no changes.

The tz is UTC+0 because that's what we already use for the
`mcp-calls.log`

Works:

<img width="1074" height="599" alt="image"
src="https://github.com/user-attachments/assets/db7094f3-8957-4f37-8643-9b2e7c449a1e"
/>

---------

Co-authored-by: pFornagiel <pawel.fornagiel2@gmail.com>
2026-04-28 14:28:22 +02:00
Ignacy Łątka 26da07d371 feat: add MCP config adapter for opencode (#152)
This PR adds MCP config adapter for OpenCode

At one point OpenCode worked presumably because it read `.claude` or
some other config (i've had like 13 of them on my machine). That doesn't
seem to be the case anymore.

before|after
---|---
<img width="508" alt="image"
src="https://github.com/user-attachments/assets/615d07f6-aa92-49a4-b159-92d6b292d2c7"
/>|<img width="510" alt="image"
src="https://github.com/user-attachments/assets/4dabcd1a-d920-4979-9e9e-31e45b12d1c2"
/>
<img width="400" alt="image"
src="https://github.com/user-attachments/assets/8853ddf4-0001-4874-81b7-d2a88d3b6956"
/>|<img width="400" alt="image"
src="https://github.com/user-attachments/assets/048928bf-7c59-4ac0-8483-04cc3b3edcda"
/>

---------

Co-authored-by: Filip131311 <contact@filipkaminski.com>
2026-04-28 10:17:53 +02:00
filip131311 e42308106c ci: add workflow to publish prereleases under npm next tag (#163)
## Summary
- Adds `.github/workflows/publish-next.yml`, a manual
`workflow_dispatch` workflow that publishes `@swmansion/argent` to npm
under the `next` dist-tag so users can opt into prereleases via `npm
install @swmansion/argent@next` without affecting `latest`.
- Mirrors the existing `publish.yml` (same checkout / native binaries
download / build / smoke checks); the only functional differences are
`npm publish --tag next` and a pre-publish guard that fails fast unless
`packages/argent/package.json` has a SemVer prerelease version (contains
`-`, e.g. `0.6.0-next.0`).

## Why
`npm publish` defaults to the `latest` dist-tag even for prerelease
SemVer versions. Without an explicit `--tag next`, a published
`0.6.0-next.0` would be served to anyone running `npm install
@swmansion/argent`. This workflow makes prerelease publishing safe and
explicit.

## Usage
1. On a release branch, bump `packages/argent/package.json` to e.g.
`0.6.0-next.0` and tag `v0.6.0-next.0` (the existing `release.yml` will
create a GitHub Release).
2. Run **Actions → Publish prerelease to npm (next) → Run workflow**
with `ref: v0.6.0-next.0`.
3. Verify with `npm dist-tag ls @swmansion/argent` — `latest` is
unchanged, `next` points at the new prerelease.

## Test plan
- [ ] Dry-run the workflow against a prerelease tag and confirm `npm
pack --dry-run` succeeds and the publish step runs `--tag next`.
- [ ] After a real prerelease publish, confirm `npm view
@swmansion/argent dist-tags` shows `next: <version>` and `latest` is
untouched.
- [ ] Sanity check the guard: trigger against a non-prerelease ref (e.g.
`v0.5.3`) and confirm the workflow fails at the "Verify package version
is a prerelease" step before running `npm publish`.
2026-04-28 08:41:09 +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
filip131311 99cd3870ba feat: Add CLI capability to argent (#161)
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
2026-04-27 14:05:26 +02:00
filip131311 037c474956 Feat: Baseline web UI for the simulator (#158)
This is a very base line UI: 

- integrates into the existing simulator server on tools server 
- does not expose any endpoints via mcp
2026-04-24 15:27:53 +02:00
Ignacy Łątka acc5798185 feat(mcp): support progressive tool loading via _meta hints (#120)
* 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.
2026-04-23 17:04:24 +02:00
filip131311 6c97269306 feat: add Get Started tip to end of argent init flow (#154)
* 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
v0.5.3
2026-04-21 16:24:16 +02:00
filip131311 927620cb82 fix: actually update skills on argent update and use semver-aware comparison (#151)
* 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.
2026-04-21 15:42:11 +02:00
filip131311 9fee899e5f chore: bump package versions to 0.5.3 for mcp, registry, skills, and tool-server (#149) 2026-04-20 17:51:14 +02:00
filip131311 aaa1a21c7c fix: only prompt for update when remote version is strictly newer (#150)
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.
2026-04-20 17:26:50 +02:00