Commit Graph

50 Commits

Author SHA1 Message Date
filip131311 2278115028 fix(flow): run a device-less flow without a device, and say why its counts are zero (#649)
Fixes #644.

## Before

```
$ argent flow run echo-only          # .argent/flows/echo-only.yaml: two `echo` steps
[Tool:flow-execute] No booted device found. Pass a device id or platform explicitly.
Available devices: 18BE573F-… (ios, Shutdown), <70 more>
```

and when a device *was* booted:

```
PASS on 18BE573F-… — 0 passed, 0 failed, 0 errored, 0 skipped
```

## After

```
$ argent flow run echo-only          # nothing booted
Flow "echo-only"
  › first step
  › second step

PASS — 0 passed, 0 failed, 0 errored, 0 skipped (no test steps)
```

## Symptom 1 — the device

Whether a device is needed is decided from the flow's own steps, before
resolution.

Classification is **per step kind and defaults to needing one**, so a
kind added later inherits
today's behaviour instead of silently running against no device — and
the `never` binding makes
leaving a new kind unclassified a compile error, backed by a test keyed
on the step union so it
fails to build rather than to run.

Two classifications worth calling out:

- **`when` always needs a device**, whatever its body contains, because
the guard reads one itself
(its platform, or its view tree). A `when: {platform: ios}` block
containing only narration still
  resolves.
- **`run` needs one without the fragment being read here.** The named
flow is resolved at run time;
resolving it a second time would duplicate that lookup and could
disagree with it if the file
changed in between. A deliberate over-approximation: composing a
narration-only fragment still
  resolves a device. Stated in the code and in the tool description.

For `tool:` steps the tool's own schema decides, via a superset of the
keys the runner injects —
`device` is included because a nested `flow-execute` step drives a
device without being handed the
run's own. A tool that declares no input at all
(`stop-all-simulator-servers`) is device-free, and
reading its absent schema must not throw — it previously would have.

`ExecState.device` is nullable, which is the safety mechanism rather
than just typing: the compiler
enumerates every site that acts on a device, each of which now says so
via one `deviceEnv()`
narrowing. `flow-actions.ts`, `flow-visual.ts` and `ActionEnv` are
untouched. The executor also
re-checks each step against the **same predicate**, so if the scan and
the executor ever disagree
the step reports it instead of failing obscurely deeper in.

A run that resolved no device reports `device: ""`, and neither renderer
claims it ran somewhere.

## Symptom 2 — the counters

Reproducing this showed the issue's framing is slightly off: `--json`
doesn't only disagree with the
text, it disagrees with **itself** — counters all zero while its own
`steps[]` marks both steps
`"pass"`. So the CLI text was a faithful rendering of the server's
counters.

**The counters are right and stay as they are.** Narration is
deliberately excluded, and it should
be: after a hard stop every remaining step *including narration* is
reported skipped, so counting it
would inflate `skipped` on every real failing run — a worse and far more
common lie than the vacuous
one being reported here. It would also contradict the JUnit model in
#578, where echo is
`<system-out>` and not a `<testcase>`.

What was missing was saying *why* zero. Both the CLI and the MCP
renderer now append
`(no test steps)`, and only on a pass — a cancelled run can be a FAIL
with all four counters zero,
where "no test steps" would read as though the failure had no cause.

No wire format changes: `steps[].kind === "echo"` already distinguishes
counted from uncounted steps,
so the JSON is self-describing without widening the status enum.

## Conflicts, for whoever lands second

This is a hot area — seven open PRs touch `flow-run.ts`.

- **#578** rewrites the CLI summary line I also change (one hunk, one
7-line function, semantically
independent — resolution is keep-both), and its
`attachFailureDiagnostics` dereferences
`env.device.platform`. **It will need a null-device guard**, or every
failure in a device-less flow
silently degrades to `read-failed` diagnostics. Happy to rebase onto it
rather than the reverse.
- **#585** rewrites `resolveRunDevice` at the same seam and edits the
same description sentence.
- **#538** rewrites `flow-device.ts`. Worth noting it makes
`resolveFlowDevice` reject a physical
iPhone for flows — skipping resolution means a narration-only flow now
passes on a
physical-iPhone-only host, which I think is desirable but is a second
behaviour change on this seam.

All new tests are in new files, since every existing flow test file in
the blast radius is rewritten
by an open PR.

## Behaviour changes a reviewer should weigh

- A device-free flow no longer prints `PASS on <id>` **even when a
device is booted**. That is the
point: otherwise an identical flow reports differently in CI (no
simulator) than on a laptop with
  one open. An explicit `--device` is still honoured and still named.
- `--platform` is ignored for a device-free flow, where it previously
errored with nothing booted.
- A 0.18.1 CLI against a newer server would print `Flow "x" on ` —
cosmetic version skew.

## Verification

Live against the built CLI: device-less flow with nothing booted (the
reported bug) and with a device
booted (identical output, no ambient attribution); `--json` showing
`device: ""`; explicit `--device`
still naming it; a mixed flow and a composed flow both still demanding a
device; a mixed flow with a
device booted still reporting `PASS on <id> — 1 passed`.

24 new tests. tool-server 3103 passed, CLI 284 passed, MCP 78 passed —
no existing test modified.

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

---

> **#697 is stacked on this branch** — it conflicts with this one in
`flow-run.ts`'s import block, so it was rebased on top. Merge this
first.
2026-08-03 16:14:30 +02:00
filip131311 23b9ae5c87 chore: bump to 0.18.1 (#603)
Patch bump. First release whose npm tarball carries `mcpName` (added in
#598), which unblocks the MCP registry publish (`mcp-publisher publish`
rejects `0.18.0` because its manifest predates the field).

`server.json` is bumped alongside `packages/*` for the first time —
`scripts/check-workspace-versions.mjs` now enforces it (`All workspace
packages and server.json are at 0.18.1` locally).

| Commit | Change |
| --- | --- |
| `eea6c27c` | `style`: fix prettier formatting on main (#604) |
| `7e4ece69` | `chore`: server.json for the official MCP registry (#598)
|
| `fc09612f` | `feat(config)`: `recordings.directory` — configurable
save location for screen recordings (#601) |
| `cccda92d` | `feat(config)`: `ios.additionalDeviceSets` — additive
array of extra CoreSimulator device sets (#542) |

Version-only diff, rebased on main as of #604.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-31 15:49:05 +02:00
Ignacy Łątka 646f726bc4 chore: bump to 0.18.0 (#596)
bump

debated myself whether this should be a QoL patch or a minor release and
i think the latter makes more sense

| Commit | Change |
| --- | --- |
| `7189333a` | `feat(tool-server)`: human-readable log message (#582) |
| `5ac69349` | `feat(flow)`: CSS combinator scopes for flow selectors
(#569) |
| `e23f0e28` | `feat(flow)`: snapshot `cropOn` for single-element visual
diffs (#516) |
| `6a53e66a` | `feat(flow)`: rotate directive (#546) |
| `68ceb314` | `ci`: cut over npm publishing to the unified
trusted-publishing workflow (#571) |
| `2546788a` | `ci`: unified publish-npm workflow (#565) |
2026-07-30 23:47:28 +02:00
Ignacy Łątka a0460eeefe chore: bump workspace packages to 0.17.0 (#554)
## What

Lockstep-bump all 16 `packages/*` workspace packages (and the matching
`package-lock.json` entries) from `0.16.1` → **0.17.0**.

Diff is exactly 32 version-field swaps across 17 files — no formatting
or
dependency churn.

## Verification

- **Lockfile CI gate reproduced locally on the exact CI toolchain**
(Node
24.14.1 / npm 11.11.0, matching `lockfile.yml`'s `node-version: "24"`):
`npm install --package-lock-only --ignore-scripts` on top of the commit,
then `git diff --exit-code -- package-lock.json` → **clean**. npm's own
  regeneration reproduced the hand-edit byte-for-byte, so none of the
  `"peer": true` churn that #513/#539 had to keep out reappears here.
- `node scripts/check-workspace-versions.mjs` → `All workspace packages
are
  at 0.17.0.`
- Remaining `repo-hygiene.yml` checks pass: no conflict markers, no
focused
  tests.
- `prettier --check` on all 17 changed files → `All matched files use
  Prettier code style!`
- **Runtime version actually reports 0.17.0**: `getInstalledVersion()`
(`argent-installer/src/utils.ts:384`) and
`getGloballyInstalledVersion()`
(`topology.ts:94`) both read `version` straight out of the resolved
package
root's `package.json` at runtime, so the bump is the whole mechanism —
no
generated constant or second version file to keep in sync. Exercised
that
  resolution against the bumped tree: `PACKAGE_ROOT` → `packages/argent`
  (`@swmansion/argent`) → `0.17.0`.
- Verified all 16 lockfile hits belong to workspace packages (parsed the
  lockfile's `packages{}` and matched every `0.16.1` entry to a
`packages/*` path) — no unrelated dependency happened to sit at 0.16.1.
- No test asserts a release version string; the only `0.1x.x` matches
under
  `*.test.ts` are historical references inside comments.

## Note

`packages/argent-private` needs no pin change here. Its release version
is
carried purely as a git tag — the repo has no version field on the
release
line (its only `package.json` is a `private: true` research CLI on an
independent `0.1.0` line) — and `v0.17.0` is now tagged at `2d6c292`,
which
is **already** the commit this superproject pins. `v0.16.0` and
`v0.16.1`
sit on that same commit, so the submodule entry is correct as-is and
stays
out of this diff.
2026-07-23 23:33:49 +02:00
filip131311 1f5b155229 chore: bump workspace packages to 0.16.1 (#539)
## What

Lockstep-bump all 16 `packages/*` workspace packages (and the matching
`package-lock.json` entries) from `0.16.0` → **0.16.1**.

## Verification

- `node scripts/check-workspace-versions.mjs` → `All workspace packages
are at 0.16.1.`
- Lockfile diff is exactly 16 version-field swaps — no unrelated churn
(the `"peer": true` regeneration noise from a newer npm was again kept
out, same as #513). The one remaining `0.16.0` in the lockfile is the
unrelated `@humanfs/types` dev dependency.
- `packages/argent-private` submodule left pinned at `v0.16.0` — no
`v0.16.1` tag exists there yet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-22 11:03:13 +02:00
Jakub Piasecki a0d1bbb1e7 feat(flow): indent nested steps in reports by block depth (#524)
Flow reports rendered every step flat, hiding which steps ran inside a
`when:` block or a `run:` fragment. The tool-server now stamps
`StepReport.depth` (+1 per block directive, omitted at top level) and
the CLI/MCP renderers indent step labels and under-step lines by it:

```
  ✓  1 launch
  ✓  2 when visible "What's new" — condition met
  ✓  3   tap "Skip"
  ✓  4   run dismiss [dismiss]
  ✓  5     tap "OK" [dismiss]
```

Invariants:

- **Wire-compatible both ways** — a flow with no block directives
reports byte-identical to the pre-depth shape; a pre-depth server
renders flat as before.
- **Skipped ≡ entered** — skipped/errored/aborted blocks report the same
depths as entered ones, keeping reports comparable run-to-run.
- **Untrusted wire data** — renderers clamp depth (display cap, not a
producer bound): hostile values render flat or at the cap, never throw
or allocate huge strings.

Also fixes a pre-existing bug: under-step warning/artifact lines now
stay aligned past 99 steps.

Tests pin depth on every producer path (entered/skipped/errored/aborted
blocks, `run:` success/failure markers, `run:`-in-`when:`) and renderer
alignment incl. the clamp boundary.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 07:28:48 +02:00
filip131311 b50d51a2de chore: bump workspace packages to 0.16.0 (#513)
## What

Lockstep-bump all 16 `packages/*` workspace packages (and the matching
`package-lock.json` entries) from `0.15.0` → **0.16.0**.

## Why

The `packages/argent-private` submodule is already pinned at
**v0.16.0**, while the workspace packages sat at 0.15.0. This aligns the
whole repo on the single 0.16.0 version.

## Verification

- `node scripts/check-workspace-versions.mjs` → `All workspace packages
are at 0.16.0.`
- `git submodule status` → clean (no `+`), pinned at `v0.16.0`.
- Lockfile diff is exactly 17 version-field swaps (no unrelated churn —
a full `npm install --package-lock-only` regeneration was rejected
because it introduced unrelated `"peer": true` churn from a newer npm).
2026-07-15 20:53:36 +02:00
filip131311 07427917e2 fix(launcher,lens): drop the npm postinstall — its jobs move to runtime (#510)
## Why

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

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

## What the postinstall did, and where it went

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

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

## Testing

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

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-15 20:26:39 +02:00
filip131311 70660378bb feat(tool-server): type secrets via {{secret:NAME}} placeholders — plaintext never enters agent context (#506)
## Why

Feedback: unlike Stagehand's `act("type %pw% into password", {
variables: { pw } })`, argent had no way to type a credential without
the LLM seeing the plaintext — which disqualifies it as a CI agent. In
the MCP model the agent composes the `keyboard` call itself, so a
plaintext password lands in **six** places: the agent transcript,
`~/.argent/mcp-calls.log` (spyLog records full args + result), the
flag-gated tool-server event log, the `typed` result echo, recorded flow
YAMLs (committed to the repo), and the post-type auto-screenshot
(plaintext back into context as pixels for non-secure fields).

## What

`keyboard` and `paste` now accept **`{{secret:NAME}}`** placeholders in
`text`, resolved server-side from **`ARGENT_SECRET_NAME`** environment
variables:

```yaml
# GitHub Actions
env:
  ARGENT_SECRET_APP_PASSWORD: ${{ secrets.APP_PASSWORD }}
```

```json
{ "udid": "<UDID>", "text": "{{secret:APP_PASSWORD}}", "key": "enter" }
```

**Resolution happens inside the tool's `execute`** — after every
existing logging boundary — so spyLog, the event log, the transcript,
and recorded flows all see only the placeholder, with near-zero extra
redaction work:

- `run-sequence` keyboard steps and flow `type` steps go through
`invokeSubTool` into the same `execute` → covered for free. A recorded
login flow stores the placeholder verbatim, so the committed YAML is
secret-free — and agent-less `argent flow run` (#429) resolves it in CI
from the job's env, no LLM in the loop at all.
- The mandatory `ARGENT_SECRET_` prefix is an allowlist by construction:
a prompt-injected agent can never exfiltrate `AWS_SECRET_ACCESS_KEY` or
`GITHUB_TOKEN` through this mechanism — only vars the user deliberately
exposed under the prefix resolve.
- `typed` echoes the placeholder, never the value.
- Backend errors are scrubbed at the resolution site (e.g. a failed
Android `input text` echoes the device-side command line containing the
secret) — message and stack, in place, preserving the error class and
with it the HTTP-status/telemetry mapping.
- Unknown placeholder → 400 listing the **names** of available secrets,
never values (`SECRET_PLACEHOLDER_UNKNOWN`).
- **Auto-screenshot skip:** the MCP layer deep-scans args for
`{{secret:` and replaces the after-action screenshot with a text note —
otherwise a non-secure-entry field hands the plaintext straight back to
the model as an image. Covers nested `run-sequence` steps too.
- Remote-routed setups resolve on the machine that types (the
tool-server's env) — documented in the skill.
- Inert unless a placeholder is used; no feature flag.

## Docs

- `keyboard` / `paste` tool descriptions
- `argent-device-interact` skill: "Typing secrets" section (incl.
*don't* `describe`/`screenshot` the field you just filled — submit
first)
- `argent-create-flow` skill: never record a real credential;
placeholder flows stay secret-free in the repo

## Known residual risks (shared with Stagehand-style designs)

- A prompt-injected agent could direct `{{secret:X}}` into an
attacker-visible field and screenshot it — inherent to any "agent types
secrets" design; the prefix allowlist and no-read-back keep the blast
radius to secrets deliberately exposed.
- `describe` can read back a non-secure field's value after typing; iOS
secure fields briefly show the last typed character; Android typing puts
the resolved string on the device-side `input` command line. Acceptable
for emulator CI; noted in the skill.
- `keys` in the result still reflects the real keystroke count (secret
length) — kept for debuggability.

## Tests

- `keyboard-secrets.test.ts`: resolution (embedded/repeated
placeholders, malformed shapes ignored), unknown-name error lists names
but never values, redaction preserves error class, full `execute`
round-trips on the chromium branch asserting the plaintext appears
nowhere in results or thrown errors, paste via mocked `sendCommand`.
- `auto-screenshot.test.ts`: `containsSecretPlaceholder` on
flat/nested/unserializable args (fails safe).
- Full suites green: tool-server 2550, argent-mcp 76, registry 87;
builds + `typecheck:tests` + eslint + prettier clean.

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

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

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

## Flow format

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

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

## Execution engine

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

## Visual snapshots

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

## Recording

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

## Also included

- `argent flow run` / `argent flow list` CLI with per-step report
rendering, artifact materialization, and `--json` output.
- New structured failure codes for flow validation, device resolution,
composition, and devtools readiness.
- Extensive unit test coverage (tree adapters, selector matching,
scrolling, composition, type focus, snapshots, status bar).
2026-07-13 17:15:01 +02:00
filip131311 2a01e407b3 chore: release 0.15.0 (submodule + workspace bump) (#484)
## What

- Bump the `packages/argent-private` submodule pointer from
`v0.11.0-8-g2953e89` to **v0.15.0** (`41484e4`). The superproject pin
had drifted ~4 releases behind the checked-out submodule.
- Lockstep-bump all `packages/*` workspace packages from `0.14.0` →
**0.15.0** (and the matching `package-lock.json` entries) to line up
with the submodule.

## Why

The submodule and the workspace were out of sync: the working tree had
`argent-private` checked out at v0.15.0 while the superproject still
pinned the v0.11 era, and the JS packages sat at 0.14.0. This adopts
v0.15.0 as the single, consistent version across the repo.

## Verification

- `node scripts/check-workspace-versions.mjs` → `All workspace packages
are at 0.15.0.`
- `git submodule status` → clean (no `+`), pinned at `v0.15.0`.
- Lockfile diff is exactly 15 version-field swaps (no unrelated churn).
2026-07-08 17:31:45 +02:00
filip131311 bc04c3b64f perf(argent-mcp): wait for the screen to settle before the auto-screenshot (#464)
## Problem

After an interaction, the MCP layer appends a screenshot so the agent
can see the screen. It first slept a **fixed per-tool delay** — `3000ms`
for `launch-app`/`restart-app`, `2000ms` for `open-url`, `1500ms` for
gestures
([`auto-screenshot.ts`](https://github.com/software-mansion/argent/blob/main/packages/argent-mcp/src/auto-screenshot.ts#L33-L49))
— so the capture wouldn't catch a blank/splash/transition frame:

```ts
const delayMs = getAutoScreenshotDelayMs(params.name);
if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
```

That blind sleep is paid **in full on every call** even when the screen
settled far sooner, and the `ARGENT_AUTO_SCREENSHOT_DELAY_MS` override
is a `Math.max` floor — it can only *raise* the delay, never lower it.
On an agent's relaunch→verify→tap loop the fixed `3000ms` dominates
time-to-interaction.

## Change

Replace the blind sleep with a **bounded readiness wait that runs
tool-server-side**, reusing the polling machinery `await-ui-element`
already has instead of adding a second describe-polling loop.

- **`pollDescribeTree`** (new `utils/poll-describe-tree.ts`) —
`await-ui-element`'s fetch/poll loop, extracted: it re-reads the
`describe` tree on an interval until a caller-supplied predicate holds
or the timeout elapses, racing each fetch against the deadline + abort
via `settleWithin` (moved to `utils/timing.ts`) and clamping the poll
sleep to the deadline. `await-ui-element` now uses this shared loop
(**−77 lines**); its 791-line suite is unchanged and green, so behavior
is preserved.
- **`await-screen-idle`** (new tool) — reuses the loop with a
*stability* predicate: poll until the tree has content whose fingerprint
(role/label/value + frame rounded to 1%) holds identical for
`minStableMs`, bounded by `timeoutMs`. Returns `{ settled, waitedMs,
polls }`.
- **MCP path** — calls `await-screen-idle` **once** (server-side, no
per-poll round trips) with the per-tool delay as the cap. Falls back to
the previous fixed sleep if the tool is unavailable (older / remote
tool-server).

### Why not just `await-ui-element`?

It waits for a **caller-named** element (`{text|identifier|role}` +
`exists|visible|hidden|text`) — its schema requires a selector. After
`launch-app`/`restart-app` there's no element to name, and it has no
"screen settled" condition. So the *predicate* is genuinely different;
only the *loop* is shared, which is exactly what this PR extracts.

## How expensive is the polling?

Measured on a booted **iPhone 17 Pro (iOS 26.4)** relaunching Settings:

| metric | value |
| --- | --- |
| **time-to-ready** | **3000ms → ~1810ms** (~40% faster), `settled`
every run |
| polls per relaunch | 4–5 |
| `describe` cost / poll | ~62ms settled screen, ~290ms mid-launch |
| loop overhead (excl. probe) | negligible |

The cost is entirely the `describe` reads, which run *while the app is
launching anyway* and let the capture fire ~1.2s early instead of
sleeping through it. On slower apps the wait simply extends up to the
same cap — it adapts instead of over-padding.

## Behavior / bounds

- **Never slower than before:** a probe is never started past the
deadline; a screen that never settles falls back to the full cap,
exactly like the old sleep.
- **Static splash screens:** if an app holds a static splash for
`minStableMs` (250ms), the wait can settle on it — the same case the
fixed delay was tuned for; bounded by the cap, so no worse than a
shorter fixed delay.
- **TV / Vega / unsupported:** `await-screen-idle` isn't capable there,
so the MCP path's `catch` falls back to the fixed sleep.

## Testing

- Full **tool-server** suite: **1847 passing** (incl.
`await-ui-element`'s 791-line suite green after the refactor) + 4 new
`await-screen-idle` tests.
- Full **`@argent/mcp`** suite green. `tsc --build`, `eslint`,
`prettier` all clean.
- Net **−68 lines**.
2026-07-03 17:20:01 +02:00
filip131311 c5b9857fa5 chore: bump all workspace packages to 0.14.0 (#454)
Bumps every package under `packages/*` from 0.13.0 → 0.14.0 in lockstep.

- All 14 workspace packages bumped to `0.14.0`.
- `package-lock.json` regenerated to match (also picks up npm's `"peer":
true` normalization).
- `npm run check:versions` passes: *All workspace packages are at
0.14.0.*

Internal deps use `file:../` references, so no dependency version
specifiers needed changing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-01 20:36:58 +02:00
Juliusz Wajgelt 20f5a1282c feat: initial sim-remote support (#291)
## Summary

Adds **initial support for remote iOS simulators** ("sim-remote") —
simulators that run in the cloud (radon-cloud orchestrator) and are
driven from the dev's machine over a QUIC/MoQ tunnel, alongside the
existing local iOS simulators. A remote sim is exposed as a new device
platform, `ios-remote`, and the existing
interaction/inspection/profiling tools work against it through the same
tool surface.

To get there without scattering `isRemote` branches across every tool,
the iOS code paths are refactored behind a small set of
host/transport/backend abstractions so each tool stays
transport-agnostic.

## How it works

- **Discovery** — `list-devices` enumerates remote sims via the
`sim-remote` CLI in addition to local `simctl`. They classify as
`platform: "ios-remote"`. Enumeration is best-effort: if `sim-remote`
isn't installed or the user isn't logged in, it silently returns none.
- **Transport** — remote interaction goes over a MoQ relay
(WebTransport/QUIC). `moq-client.ts` subscribes to the server-published
`simulator` broadcast (catalog/video/screenshot) and publishes a
`control` track carrying protobuf-encoded input commands (touch / key /
button / rotate / wheel / screenshot). WebTransport is provided in Node
by `@fails-components/webtransport`, polyfilled lazily on first use.
- **Native devtools over the tunnel** — unix sockets don't bridge the
QUIC tunnel, so on remote sims `ax-service` and `native-devtools` are
forced onto TCP with OS-assigned ephemeral ports, and a reverse proxy is
wired before the in-sim daemon dials back.

## Key components

New (`packages/tool-server/src/`):
- `utils/sim-remote.ts` — thin wrapper around the `sim-remote` CLI
(simctl verbs, `moq-info`, proxy), propagating CLI stderr verbatim.
- `utils/moq-client.ts` — pure-JS MoQ client for the remote
simulator-server relay.
- `utils/datachannel-proto.ts` + `proto/datachannel.proto` — vendored
protobuf schema + encoders for the control channel.
- `utils/ios-host.ts` — `IosHost` abstraction (`localIosHost` /
`remoteIosHost`, picked by `pickIosHost(device)`) capturing the
local-vs-remote differences (transport, env setup, proxying,
`requiresTcp`).
- `utils/simctl-backend.ts` — strategy for simctl verbs so one iOS
handler serves both `xcrun simctl` (local) and `sim-remote simctl`
(remote).
- `utils/ax-prefs.ts` — AX-pref helpers extracted from `ax-service`.
-
`tools/{launch-app,open-url,reinstall-app,restart-app}/platforms/ios-remote.ts`
+ shared handlers.

Refactors:
- **Strategy pattern** to limit `isRemote`/platform branching inside
tool and blueprint implementations.
- `simulator-client.ts` gains a transport abstraction
(`SimulatorServerTransport`) so call sites stay transport-agnostic;
local sims back it with WebSocket+HTTP, remote sims with the MoQ client.
- `ax-service` / `native-devtools` blueprints updated for ephemeral TCP
ports and the `ios-remote` platform.
- Dependency checks: tools that pick a platform path internally now gate
on the right CLI (`sim-remote` vs `xcrun`).

## New dependencies

`@moq/lite`, `@moq/net`, `@fails-components/webtransport`,
`@fails-components/webtransport-transport-http3-quiche`, `protobufjs`.

## Testing

- Existing unit suite passes (CI green); the native-devtools
status/describe tests now stub the platform dep-check so they exercise
their logic, not the host toolchain.
- E2E device-smoke jobs (local iOS sim, Android, Chromium, remote VVD)
pass.

---

<sub>Branch rebased onto latest `main` (linear history). `sim-remote` is
opt-in — only required when driving a remote iOS simulator.</sub>
2026-07-01 15:57:47 +02:00
dependabot[bot] 02695e6704 chore(deps)(deps): bump the npm-major group across 1 directory with 3 updates (#413)
Bumps the npm-major group with 3 updates in the / directory:
[@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node),
[dotenv](https://github.com/motdotla/dotenv) and
[ini](https://github.com/npm/ini).

Updates `@types/node` from 25.9.3 to 26.0.1
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />

Updates `dotenv` from 16.6.1 to 17.4.2
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md">dotenv's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/motdotla/dotenv/compare/v17.4.1...v17.4.2">17.4.2</a>
(2026-04-12)</h2>
<h3>Changed</h3>
<ul>
<li>Improved skill files - tightened up details (<a
href="https://redirect.github.com/motdotla/dotenv/pull/1009">#1009</a>)</li>
</ul>
<h2><a
href="https://github.com/motdotla/dotenv/compare/v17.4.0...v17.4.1">17.4.1</a>
(2026-04-05)</h2>
<h3>Changed</h3>
<ul>
<li>Change text <code>injecting</code> to <code>injected</code> (<a
href="https://redirect.github.com/motdotla/dotenv/pull/1005">#1005</a>)</li>
</ul>
<h2><a
href="https://github.com/motdotla/dotenv/compare/v17.3.1...v17.4.0">17.4.0</a>
(2026-04-01)</h2>
<h3>Added</h3>
<ul>
<li>Add <code>skills/</code> folder with focused agent skills:
<code>skills/dotenv/SKILL.md</code> (core usage) and
<code>skills/dotenvx/SKILL.md</code> (encryption, multiple environments,
variable expansion) for AI coding agent discovery via the skills.sh
ecosystem (<code>npx skills add motdotla/dotenv</code>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Tighten up logs: <code>◇ injecting env (14) from .env</code> (<a
href="https://redirect.github.com/motdotla/dotenv/pull/1003">#1003</a>)</li>
</ul>
<h2><a
href="https://github.com/motdotla/dotenv/compare/v17.3.0...v17.3.1">17.3.1</a>
(2026-02-12)</h2>
<h3>Changed</h3>
<ul>
<li>Fix as2 example command in README and update spanish README</li>
</ul>
<h2><a
href="https://github.com/motdotla/dotenv/compare/v17.2.4...v17.3.0">17.3.0</a>
(2026-02-12)</h2>
<h3>Added</h3>
<ul>
<li>Add a new README section on dotenv’s approach to the agentic
future.</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Rewrite README to get humans started more quickly with less noise
while simultaneously making more accessible for llms and agents to go
deeper into details.</li>
</ul>
<h2><a
href="https://github.com/motdotla/dotenv/compare/v17.2.3...v17.2.4">17.2.4</a>
(2026-02-05)</h2>
<h3>Changed</h3>
<ul>
<li>Make <code>DotenvPopulateInput</code> accept
<code>NodeJS.ProcessEnv</code> type (<a
href="https://redirect.github.com/motdotla/dotenv/pull/915">#915</a>)</li>
</ul>
<ul>
<li>Give back to dotenv by checking out my newest project <a
href="https://github.com/vestauth/vestauth">vestauth</a>. It is auth for
agents. Thank you for using my software.</li>
</ul>
<h2><a
href="https://github.com/motdotla/dotenv/compare/v17.2.2...v17.2.3">17.2.3</a>
(2025-09-29)</h2>
<h3>Changed</h3>
<ul>
<li>Fixed typescript error definition (<a
href="https://redirect.github.com/motdotla/dotenv/pull/912">#912</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/motdotla/dotenv/commit/f116f70310abab44fbfddbaeb833698b5bf84a9b"><code>f116f70</code></a>
17.4.2</li>
<li><a
href="https://github.com/motdotla/dotenv/commit/3a8161274fdd745239b86e604f4a7e972a1d3902"><code>3a81612</code></a>
fix visual order of faq</li>
<li><a
href="https://github.com/motdotla/dotenv/commit/13f55a89e136b2024e68d277b836dd5260fc16cf"><code>13f55a8</code></a>
Merge branch 'skill'</li>
<li><a
href="https://github.com/motdotla/dotenv/commit/4bbbf73f0906bd69975c48bf310a84b686e5b1b4"><code>4bbbf73</code></a>
reorganize faq</li>
<li><a
href="https://github.com/motdotla/dotenv/commit/c3da64bb2ba1d0e02f8b9b2b7ccb7e6f7a51d56c"><code>c3da64b</code></a>
Merge pull request <a
href="https://redirect.github.com/motdotla/dotenv/issues/1009">#1009</a>
from motdotla/skill</li>
<li><a
href="https://github.com/motdotla/dotenv/commit/6f743b173fbd6c26f7eab7040d251f9a6c8b977d"><code>6f743b1</code></a>
update source</li>
<li><a
href="https://github.com/motdotla/dotenv/commit/fc2c6247e858a32d4024cb06a5b0c79aa35851f5"><code>fc2c624</code></a>
update skill</li>
<li><a
href="https://github.com/motdotla/dotenv/commit/972315ba74bb2bbba4483d112e853fd26006ef8a"><code>972315b</code></a>
Tighten up skill</li>
<li><a
href="https://github.com/motdotla/dotenv/commit/2795fce3d1ed07b4c570f1e06ab1c0d533c86997"><code>2795fce</code></a>
reorganize faq</li>
<li><a
href="https://github.com/motdotla/dotenv/commit/d5495d4ae8e4e41ef9a682c9e00c81552794274e"><code>d5495d4</code></a>
adjust skill</li>
<li>Additional commits viewable in <a
href="https://github.com/motdotla/dotenv/compare/v16.6.1...v17.4.2">compare
view</a></li>
</ul>
</details>
<br />

Updates `ini` from 4.1.3 to 7.0.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/npm/ini/releases">ini's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2><a
href="https://github.com/npm/ini/compare/v6.0.0...v7.0.0">7.0.0</a>
(2026-05-08)</h2>
<h3>⚠️ BREAKING CHANGES</h3>
<ul>
<li><code>ini</code> now supports node <code>^22.22.2 || ^24.15.0 ||
&gt;=26.0.0</code></li>
<li>template-oss-apply</li>
</ul>
<h3>Features</h3>
<ul>
<li><a
href="https://github.com/npm/ini/commit/55b6841261aad580ce979c1ada97885a0617b965"><code>55b6841</code></a>
<a href="https://redirect.github.com/npm/ini/pull/301">#301</a> bump to
new node engine range (<a
href="https://github.com/owlstronaut"><code>@​owlstronaut</code></a>)</li>
<li><a
href="https://github.com/npm/ini/commit/2b11ba8283afa33211844be6fa29794970ccf7b7"><code>2b11ba8</code></a>
<a href="https://redirect.github.com/npm/ini/pull/301">#301</a>
template-oss-apply (<a
href="https://github.com/owlstronaut"><code>@​owlstronaut</code></a>)</li>
</ul>
<h3>Chores</h3>
<ul>
<li><a
href="https://github.com/npm/ini/commit/f6ed5bee3d7b1c175b687c703213217b23df9199"><code>f6ed5be</code></a>
<a href="https://redirect.github.com/npm/ini/pull/301">#301</a>
template-oss-apply (<a
href="https://github.com/owlstronaut"><code>@​owlstronaut</code></a>)</li>
<li><a
href="https://github.com/npm/ini/commit/a2c835ec4690c5e67651a403d0f952aaff018f42"><code>a2c835e</code></a>
<a href="https://redirect.github.com/npm/ini/pull/293">#293</a> bump
<code>@​npmcli/eslint-config</code> from 5.1.0 to 6.0.0 (<a
href="https://redirect.github.com/npm/ini/issues/293">#293</a>) (<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot])</li>
<li><a
href="https://github.com/npm/ini/commit/3661dce193f03675ec6ddc0938e2483d0ab41f38"><code>3661dce</code></a>
<a href="https://redirect.github.com/npm/ini/pull/296">#296</a> bump
<code>@​npmcli/template-oss</code> from 4.28.0 to 4.28.1 (<a
href="https://redirect.github.com/npm/ini/issues/296">#296</a>) (<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot],
<a
href="https://github.com/npm-cli-bot"><code>@​npm-cli-bot</code></a>)</li>
</ul>
<h2>v6.0.0</h2>
<h2><a
href="https://github.com/npm/ini/compare/v5.0.0...v6.0.0">6.0.0</a>
(2025-10-22)</h2>
<h3>⚠️ BREAKING CHANGES</h3>
<ul>
<li><code>ini</code> now supports node <code>^20.17.0 ||
&gt;=22.9.0</code></li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li><a
href="https://github.com/npm/ini/commit/6d395a4aaf7d7a5e32e250809cb7bd04c6a27b66"><code>6d395a4</code></a>
<a href="https://redirect.github.com/npm/ini/pull/291">#291</a> align to
npm 11 node engine range (<a
href="https://redirect.github.com/npm/ini/issues/291">#291</a>) (<a
href="https://github.com/owlstronaut"><code>@​owlstronaut</code></a>)</li>
</ul>
<h3>Chores</h3>
<ul>
<li><a
href="https://github.com/npm/ini/commit/23a36a9ca194d97486ed367f63d13a2cde1d1ce5"><code>23a36a9</code></a>
<a href="https://redirect.github.com/npm/ini/pull/290">#290</a> bump
<code>@​npmcli/template-oss</code> from 4.26.0 to 4.27.1 (<a
href="https://redirect.github.com/npm/ini/issues/290">#290</a>) (<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot],
<a
href="https://github.com/npm-cli-bot"><code>@​npm-cli-bot</code></a>)</li>
</ul>
<h2>v5.0.0</h2>
<h2><a
href="https://github.com/npm/ini/compare/v4.1.3...v5.0.0">5.0.0</a>
(2024-09-03)</h2>
<h3>⚠️ BREAKING CHANGES</h3>
<ul>
<li><code>ini</code> now supports node <code>^18.17.0 ||
&gt;=20.5.0</code></li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li><a
href="https://github.com/npm/ini/commit/3eca645de8586b8349acd79211b01a9199d8fd32"><code>3eca645</code></a>
<a href="https://redirect.github.com/npm/ini/pull/279">#279</a> align to
npm 10 node engine range (<a
href="https://github.com/hashtagchris"><code>@​hashtagchris</code></a>)</li>
</ul>
<h3>Chores</h3>
<ul>
<li><a
href="https://github.com/npm/ini/commit/c89e2090d7989bb91255ec46eaaf5f8cb1c34b34"><code>c89e209</code></a>
<a href="https://redirect.github.com/npm/ini/pull/279">#279</a> run
template-oss-apply (<a
href="https://github.com/hashtagchris"><code>@​hashtagchris</code></a>)</li>
<li><a
href="https://github.com/npm/ini/commit/fc447500efe6523746fd9db9d3123485eed14b76"><code>fc44750</code></a>
<a href="https://redirect.github.com/npm/ini/pull/276">#276</a> bump
<code>@​npmcli/eslint-config</code> from 4.0.5 to 5.0.0 (<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot])</li>
<li><a
href="https://github.com/npm/ini/commit/21c20bb1729005ab46e0f248cea1a881c6ab5b49"><code>21c20bb</code></a>
<a href="https://redirect.github.com/npm/ini/pull/277">#277</a>
postinstall for dependabot template-oss PR (<a
href="https://github.com/hashtagchris"><code>@​hashtagchris</code></a>)</li>
<li><a
href="https://github.com/npm/ini/commit/44b3b503fdd923fb230b6a4d859014b9364ee466"><code>44b3b50</code></a>
<a href="https://redirect.github.com/npm/ini/pull/277">#277</a> bump
<code>@​npmcli/template-oss</code> from 4.23.1 to 4.23.3 (<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot])</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/npm/ini/blob/main/CHANGELOG.md">ini's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/npm/ini/compare/v6.0.0...v7.0.0">7.0.0</a>
(2026-05-08)</h2>
<h3>⚠️ BREAKING CHANGES</h3>
<ul>
<li><code>ini</code> now supports node <code>^22.22.2 || ^24.15.0 ||
&gt;=26.0.0</code></li>
<li>template-oss-apply</li>
</ul>
<h3>Features</h3>
<ul>
<li><a
href="https://github.com/npm/ini/commit/55b6841261aad580ce979c1ada97885a0617b965"><code>55b6841</code></a>
<a href="https://redirect.github.com/npm/ini/pull/301">#301</a> bump to
new node engine range (<a
href="https://github.com/owlstronaut"><code>@​owlstronaut</code></a>)</li>
<li><a
href="https://github.com/npm/ini/commit/2b11ba8283afa33211844be6fa29794970ccf7b7"><code>2b11ba8</code></a>
<a href="https://redirect.github.com/npm/ini/pull/301">#301</a>
template-oss-apply (<a
href="https://github.com/owlstronaut"><code>@​owlstronaut</code></a>)</li>
</ul>
<h3>Chores</h3>
<ul>
<li><a
href="https://github.com/npm/ini/commit/f6ed5bee3d7b1c175b687c703213217b23df9199"><code>f6ed5be</code></a>
<a href="https://redirect.github.com/npm/ini/pull/301">#301</a>
template-oss-apply (<a
href="https://github.com/owlstronaut"><code>@​owlstronaut</code></a>)</li>
<li><a
href="https://github.com/npm/ini/commit/a2c835ec4690c5e67651a403d0f952aaff018f42"><code>a2c835e</code></a>
<a href="https://redirect.github.com/npm/ini/pull/293">#293</a> bump
<code>@​npmcli/eslint-config</code> from 5.1.0 to 6.0.0 (<a
href="https://redirect.github.com/npm/ini/issues/293">#293</a>) (<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot])</li>
<li><a
href="https://github.com/npm/ini/commit/3661dce193f03675ec6ddc0938e2483d0ab41f38"><code>3661dce</code></a>
<a href="https://redirect.github.com/npm/ini/pull/296">#296</a> bump
<code>@​npmcli/template-oss</code> from 4.28.0 to 4.28.1 (<a
href="https://redirect.github.com/npm/ini/issues/296">#296</a>) (<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot],
<a
href="https://github.com/npm-cli-bot"><code>@​npm-cli-bot</code></a>)</li>
</ul>
<h2><a
href="https://github.com/npm/ini/compare/v5.0.0...v6.0.0">6.0.0</a>
(2025-10-22)</h2>
<h3>⚠️ BREAKING CHANGES</h3>
<ul>
<li><code>ini</code> now supports node <code>^20.17.0 ||
&gt;=22.9.0</code></li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li><a
href="https://github.com/npm/ini/commit/6d395a4aaf7d7a5e32e250809cb7bd04c6a27b66"><code>6d395a4</code></a>
<a href="https://redirect.github.com/npm/ini/pull/291">#291</a> align to
npm 11 node engine range (<a
href="https://redirect.github.com/npm/ini/issues/291">#291</a>) (<a
href="https://github.com/owlstronaut"><code>@​owlstronaut</code></a>)</li>
</ul>
<h3>Chores</h3>
<ul>
<li><a
href="https://github.com/npm/ini/commit/23a36a9ca194d97486ed367f63d13a2cde1d1ce5"><code>23a36a9</code></a>
<a href="https://redirect.github.com/npm/ini/pull/290">#290</a> bump
<code>@​npmcli/template-oss</code> from 4.26.0 to 4.27.1 (<a
href="https://redirect.github.com/npm/ini/issues/290">#290</a>) (<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot],
<a
href="https://github.com/npm-cli-bot"><code>@​npm-cli-bot</code></a>)</li>
</ul>
<h2><a
href="https://github.com/npm/ini/compare/v4.1.3...v5.0.0">5.0.0</a>
(2024-09-03)</h2>
<h3>⚠️ BREAKING CHANGES</h3>
<ul>
<li><code>ini</code> now supports node <code>^18.17.0 ||
&gt;=20.5.0</code></li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li><a
href="https://github.com/npm/ini/commit/3eca645de8586b8349acd79211b01a9199d8fd32"><code>3eca645</code></a>
<a href="https://redirect.github.com/npm/ini/pull/279">#279</a> align to
npm 10 node engine range (<a
href="https://github.com/hashtagchris"><code>@​hashtagchris</code></a>)</li>
</ul>
<h3>Chores</h3>
<ul>
<li><a
href="https://github.com/npm/ini/commit/c89e2090d7989bb91255ec46eaaf5f8cb1c34b34"><code>c89e209</code></a>
<a href="https://redirect.github.com/npm/ini/pull/279">#279</a> run
template-oss-apply (<a
href="https://github.com/hashtagchris"><code>@​hashtagchris</code></a>)</li>
<li><a
href="https://github.com/npm/ini/commit/fc447500efe6523746fd9db9d3123485eed14b76"><code>fc44750</code></a>
<a href="https://redirect.github.com/npm/ini/pull/276">#276</a> bump
<code>@​npmcli/eslint-config</code> from 4.0.5 to 5.0.0 (<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot])</li>
<li><a
href="https://github.com/npm/ini/commit/21c20bb1729005ab46e0f248cea1a881c6ab5b49"><code>21c20bb</code></a>
<a href="https://redirect.github.com/npm/ini/pull/277">#277</a>
postinstall for dependabot template-oss PR (<a
href="https://github.com/hashtagchris"><code>@​hashtagchris</code></a>)</li>
<li><a
href="https://github.com/npm/ini/commit/44b3b503fdd923fb230b6a4d859014b9364ee466"><code>44b3b50</code></a>
<a href="https://redirect.github.com/npm/ini/pull/277">#277</a> bump
<code>@​npmcli/template-oss</code> from 4.23.1 to 4.23.3 (<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot])</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/npm/ini/commit/847941ced4fb8465f0ccb383fd8b15c7e5aa09fc"><code>847941c</code></a>
chore: release 7.0.0 (<a
href="https://redirect.github.com/npm/ini/issues/302">#302</a>)</li>
<li><a
href="https://github.com/npm/ini/commit/f6ed5bee3d7b1c175b687c703213217b23df9199"><code>f6ed5be</code></a>
chore: template-oss-apply</li>
<li><a
href="https://github.com/npm/ini/commit/55b6841261aad580ce979c1ada97885a0617b965"><code>55b6841</code></a>
feat!: bump to new node engine range</li>
<li><a
href="https://github.com/npm/ini/commit/2b11ba8283afa33211844be6fa29794970ccf7b7"><code>2b11ba8</code></a>
feat!: template-oss-apply</li>
<li><a
href="https://github.com/npm/ini/commit/e8d16cbbbb91d503fe14163024bd2a01a0446d32"><code>e8d16cb</code></a>
deps &amp; engine update</li>
<li><a
href="https://github.com/npm/ini/commit/3661dce193f03675ec6ddc0938e2483d0ab41f38"><code>3661dce</code></a>
chore: bump <code>@​npmcli/template-oss</code> from 4.28.0 to 4.28.1 (<a
href="https://redirect.github.com/npm/ini/issues/296">#296</a>)</li>
<li><a
href="https://github.com/npm/ini/commit/5d67f4b487a00aa3e2119776192030356e0161ba"><code>5d67f4b</code></a>
chore: bump <code>@​npmcli/template-oss</code> from 4.27.1 to 4.28.0 (<a
href="https://redirect.github.com/npm/ini/issues/294">#294</a>)</li>
<li><a
href="https://github.com/npm/ini/commit/a2c835ec4690c5e67651a403d0f952aaff018f42"><code>a2c835e</code></a>
chore: bump <code>@​npmcli/eslint-config</code> from 5.1.0 to 6.0.0 (<a
href="https://redirect.github.com/npm/ini/issues/293">#293</a>)</li>
<li><a
href="https://github.com/npm/ini/commit/180a8d5c72d7f13ed70c53619132f3a8ee5ac6ed"><code>180a8d5</code></a>
chore: release 6.0.0 (<a
href="https://redirect.github.com/npm/ini/issues/292">#292</a>)</li>
<li><a
href="https://github.com/npm/ini/commit/6d395a4aaf7d7a5e32e250809cb7bd04c6a27b66"><code>6d395a4</code></a>
fix!: align to npm 11 node engine range (<a
href="https://redirect.github.com/npm/ini/issues/291">#291</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/npm/ini/compare/v4.1.3...v7.0.0">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for ini since your current version.</p>
</details>
<br />

Updates `@types/node` from 25.9.3 to 26.0.1
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />

Updates `@types/node` from 25.9.3 to 26.0.1
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />

Updates `@types/node` from 25.9.3 to 26.0.1
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />

Updates `@types/node` from 25.9.3 to 26.0.1
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />

Updates `@types/node` from 25.9.3 to 26.0.1
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />

Updates `@types/node` from 25.9.3 to 26.0.1
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />

Updates `@types/node` from 25.9.3 to 26.0.1
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-26 17:40:19 +02:00
Ignacy Łątka 270c9a158a chore: bump to 0.13.0 (#412)
as per title
2026-06-24 20:27:08 +02:00
filip131311 31bb59fb60 chore: bump all workspace packages to 0.12.1 (#381)
Bumps every workspace package under `packages/*` from `0.12.0` →
`0.12.1`, keeping the monorepo's lockstep versioning intact.

## Changes
- `version` bumped to `0.12.1` in all 15 workspace `package.json` files
- `package-lock.json` workspace entries synced to match

## Verification
- `npm run check:versions` → `All workspace packages are at 0.12.1.`

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-19 21:49:02 +02:00
Hubert Gancarczyk 8483fe23f9 feat: add telemetry (#274)
## Summary

Adds anonymous, opt-out telemetry for Argent installer, update/uninstall
flows, CLI telemetry controls, tool-server lifecycle, and tool
invocation health signals.
## What Changed
- Added a new private `@argent/telemetry` package
- Introduced typed telemetry events with runtime property
allowlisting/sanitization.
- Added lazy anonymous identity creation in `~/.argent/telemetry-id`.
- Added consent handling via: `argent telemetry enable`, `argent
telemetry disable`, `argent telemetry status`
- Added local debug logging with `ARGENT_TELEMETRY_DEBUG=1`.
- Tracks installer funnel events for init, global install/update
decisions, editor selection, allowlist decisions, skill installation,
rules/agents copy, update, and uninstall.
- Tracks tool-server start/stop and tool invocation/complete/fail
events.
- Adds per-tool invocation IDs so request metadata can be correlated
without relying on ordering.
- Records only coarse platform metadata (`ios` / `android`) for device
tools
- Ensures short-lived commands drain telemetry with bounded shutdown
before exit.
- Wires telemetry package into bundling and injects full Argent CLI
version metadata at build time.
- Added central `FAILURE_CODES` and `FailureSignal` helpers in
`@argent/registry`.
- Attached structured, sanitized failure metadata across registry
errors, tool-server tools/services, HTTP validation/dependency gates,
installer flows, and `argent run`.
- Added telemetry allowlisting for safe failure fields only: static
error codes/stages, coarse area/kind/command, bounded exit codes,
allowlisted signals/spawn codes, and coarse network failure classes.
- Split CLI-local `argent run` failures into `cli:run_fail` so they are
not double-counted as tool failures.
- Added installer telemetry for failed package actions, update/uninstall
failures, rules/agents copy results, and skill refresh results.
- Made telemetry consent writes atomic and kept telemetry status
side-effect free via `peekAnonId()`.
- Adds a coarse, anonymous signal to tool telemetry: which AI coding
tool is driving the MCP server (Claude Code, Codex, Cursor, Gemini, VS
Code, Windsurf, Zed, opencode, Copilot, or other).

## Tests Added / Updated

- Telemetry unit coverage for consent precedence, identity creation,
base props, sanitization, PostHog config, debug logging, shutdown,
opt-out, and forget behavior.
- Registry/tool-server tests for invocation IDs, platform metadata
boundaries, startup failure flushing, and tool lifecycle telemetry.
- Installer tests for final telemetry drain, update action
classification, and uninstall identity cleanup ordering.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 18:22:34 +02:00
Ignacy Łątka 02e500884f chore(deps): npm-minor-patch group excluding tree-sitter (fixes #348) (#355)
## What

Applies the safe updates from dependabot #348 (`npm-minor-patch` group)
and **excludes the one bump that breaks the build**, `tree-sitter`.

| Package | From | To |
| --- | --- | --- |
| prettier | 3.8.3 | 3.8.4 |
| @clack/prompts | 1.1.0 / 1.4.0 | 1.5.1 |
| @types/node | 25.9.0 | 25.9.3 |
| esbuild | 0.28.0 | 0.28.1 |
| semver | 7.7.4 / 7.8.0 | 7.8.4 |
| vitest | 4.1.6 | 4.1.9 |
| tree-sitter | 0.21.1 | **held at 0.21.1** |

(`ws` was part of #348's group but is already at 8.21.0 on `main`, so
it's untouched here.)

## Why tree-sitter is excluded

`tree-sitter@0.25.0` ships **no Node 24 prebuilds**, and its
`binding.gyp` forces `-std=c++17` while Node 22+ v8 (cppgc) headers
require C++20, so the native build fails `npm ci`/`npm install` on
current Node. That's exactly why #348's `ESLint` job (Node 24) fails at
install:

```
.../node_modules/tree-sitter ... node-gyp-build
.../include/node/v8config.h:13:2: error: #error "C++20 or later required."
```

(#348's `Unit tests` job is pinned to Node 20, where it still compiles —
which is why that PR looks half-green.) There is no tree-sitter release
between 0.22.4 and 0.25.0, so there's no newer working version.
`^0.21.1` ships N-API prebuilds (no compile) and installs cleanly on
Node 20 / 22 / 24.

This PR also adds a Dependabot `ignore` for tree-sitter minor/major so
the group stops re-proposing the broken bump. Patch updates stay
allowed; drop the ignore once upstream restores prebuilt binaries.

## Verification (local)

- Lockfile regenerated on **Node 20 / npm 10.8.2** (`npm install
--package-lock-only --ignore-scripts`), idempotent under a re-run —
won't trip the strict lockfile check. tree-sitter resolves to 0.21.1
(prebuilt, no native compile).
- Full CI-equivalent on **Node 20**: `tsc --build`, `eslint
--max-warnings 0`, `prettier --check`,
`typecheck:tests`/`typecheck:scripts`, full unit suite incl. **1250
tool-server tests** — all green.
- `npm ci` from the committed lockfile succeeds.
- **Live device E2E**: ran the built tool-server with the bumped deps,
captured a real screenshot of a booted iPhone 16 Pro Max
(`simulator-client` uses `ws`), then downloaded the PNG artifact over
HTTP — `200`, 331 KB PNG.

Supersedes #348 (Dependabot auto-closes it once the safe bumps land and
tree-sitter is ignored).
2026-06-18 11:50:43 +02:00
filip131311 c832109573 chore: bump versions to 0.12.0 (#358)
Bumps the package suite from `0.11.0` to **`0.12.0`**.

## Changed
- All release packages `0.11.0 → 0.12.0`: `@swmansion/argent`,
`@argent/cli`, `@argent/installer`, `@argent/mcp`,
`@argent/tools-client`, `@argent/native-devtools-android`,
`@argent/native-devtools-ios`, `@argent/registry`, `@argent/skills`,
`@argent/tool-server`, `@argent/update-core`,
`@argent/configuration-core`.
- `@argent/preview-window` `0.9.0 → 0.12.0` — aligned to the suite (was
lagging a release behind; `private`/unpublished).
- Regenerated the matching `package-lock.json` workspace version
entries.

## Not changed
- `@argent/ui` stays at `0.0.1` (separate pre-release track).
- The private workspace root has no version.
- Internal deps use `file:../` specifiers, so no dependency ranges
needed updating.

No source/runtime changes — `argent --version` reads from `package.json`
at runtime.
2026-06-17 15:42:11 +02:00
filip131311 f288a461b0 refactor: rename the Electron device platform to "chromium" (CDP) (#332)
## Summary

The Electron support added in #247 is **generic Chromium/CDP under the
hood**. The discovery layer probes the Chromium-default CDP port
(`9222`) and any `ARGENT_*_PORTS` entries; the connection picks the
first non-devtools page target; describe is a plain in-page DOM walker;
input/screenshot/screencast/navigation/JS-debugger are all straight CDP.
Nothing in that path is Electron-specific — the only Electron-specific
piece is the *launcher* (`boot-electron.ts`, which resolves and spawns
an Electron binary).

So any Chromium-family runtime exposing a CDP endpoint — an Electron
app, **or** Chrome/Brave/Edge/WebView2 launched with
`--remote-debugging-port` — is discoverable and drivable through the
same tool surface. This PR renames the platform discriminator (and
everything keyed off it) from `electron` to `chromium` so the naming
matches reality.

## What changed

**Public/contract surface (clean hard rename, no back-compat aliases):**
- Platform discriminator `"electron"` → `"chromium"`; device id
`electron-cdp-<port>` → `chromium-cdp-<port>`.
- `ToolCapability.electron` → `.chromium`; the capability-gate rejection
now reads `Tool 'X' is not supported on chromium app`.
- Env `ARGENT_ELECTRON_PORTS`(`_FILE`) →
`ARGENT_CHROMIUM_PORTS`(`_FILE`); ports cache
`~/.argent/electron-cdp-ports.json` → `chromium-cdp-ports.json`; HTTP
surface `/electron-server/` → `/chromium-server/`.

**Files / dirs / symbols:**
- `src/electron-server/` → `src/chromium-server/`; blueprints
`electron-cdp` / `electron-js-runtime-debugger` → `chromium-*`;
`describe`/`open-url`/`launch-app` `platforms/electron.ts` →
`chromium.ts`; `keyboard/electron-keys` → `chromium-keys`;
`utils/electron-discovery` → `chromium-discovery`.
-
`ElectronServer`/`ElectronCdpApi`/`ElectronJsRuntimeDebugger`/`describeElectron`/`discoverElectronDevices`/`ELECTRON_ID_PREFIX`/…
→ `Chromium*` / `CHROMIUM_*`. Tests renamed to match.

**Stays "electron" (the Electron-specific launcher):**
- `boot-device` keeps the `electronAppPath` / `electronPort` /
`electronArgs` inputs, and `boot-electron.ts` still resolves & spawns an
Electron binary. It just yields a `chromium` device now (an Electron app
*is* a Chromium/CDP runtime).

**Skills:** `argent-device-interact`, `argent-metro-debugger`,
`rules/argent.md`, and the RN-workflow reference were reframed to
describe **Chromium (CDP)** availability — Electron apps *plus* any
Chromium browser over CDP (auto-discovered on `9222` /
`ARGENT_CHROMIUM_PORTS`) — instead of Electron only.

## Test plan
- [x] `tsc --build` clean
- [x] `typecheck:tests` clean
- [x] **1116 vitest cases pass (126 files)**
- [x] prettier clean on all changed/renamed files
- [x] Full-repo sweep: every remaining "electron" mention is intentional
(the launcher path + skill prose explaining Electron as one kind of
Chromium/CDP app); `electronic` in LICENSE/COC untouched.
2026-06-15 11:03:23 +02:00
Ignacy Łątka 9b027096d7 feat: Electron support (#247)
## Summary

Adds Electron (Chromium + CDP) as a third device platform alongside iOS
and Android. Argent can boot an Electron app, list it next to
simulators/emulators, drive it with the same tool surface where it makes
sense — taps, swipes, keyboard, screenshots, describe, navigation — and
debug it via the four ported `debugger-*` tools.

Mirrors the sim-server architecture in TypeScript: a per-device server
with HTTP API, MJPEG screencast, refcounted CDP screencast, FPS tracker,
clipboard, and a WebSocket command bus.

## What works on Electron

| group | tools | status |
|---|---|---|
| Lifecycle | `boot-device` (with `electronAppPath`), `launch-app`,
`open-url` | ✅ works |
| Vision | `screenshot`, `describe` (DOM walker w/ shadow-DOM +
same-origin iframes) | ✅ works |
| Input | `gesture-tap`, `gesture-swipe`, `keyboard` (text + named keys)
| ✅ works |
| Sequencing | `run-sequence` (per-step capability check) | ✅ works |
| **JS debugger** | `debugger-connect`, `debugger-status`,
`debugger-evaluate`, `debugger-log-registry` | ✅ **direct CDP** |
| Discovery | `list-devices` (Electron entries shown alongside
sims/emulators) | ✅ works |
| Cleanup | `stop-simulator-server`, `stop-all-simulator-servers`,
`stop-metro` | ✅ works |
| Misc | `update-argent`, `dismiss-update`, `gather-workspace-data`, all
`flow-*` recording tools | ✅ works |

### Debugger port — how it routes

Dispatch lives in `tools/debugger/debugger-service-ref.ts`: an Electron
device id (`electron-cdp-<port>`) routes to a new
`ElectronJsRuntimeDebugger` blueprint that wraps the page CDP session
already opened by `boot-device`. Everything else stays on the
Metro-driven `JsRuntimeDebugger` blueprint. The new blueprint exposes
the same `JsRuntimeDebuggerApi` shape so tool code is unchanged.
`Runtime.consoleAPICalled` events feed into the existing `LogFileWriter`
+ cluster pipeline — `debugger-log-registry` returns the same shape on
Electron as it does on Metro.

## What is cleanly rejected with `Tool 'X' is not supported on electron
app`

HTTP capability gate declines these up-front (HTTP 400):

| group | tools |
|---|---|
| Mobile gestures | `gesture-pinch`, `gesture-rotate`, `gesture-custom`
|
| Mobile controls | `button`, `rotate` |
| App lifecycle | `restart-app`, `reinstall-app` |
| iOS-native | `paste`, all `native-*`, `native-profiler-*` |
| RN-only debugger | `debugger-component-tree`, `debugger-reload-metro`,
`debugger-inspect-element` |
| RN-only network | `view-network-logs`, `view-network-request-details`
|
| RN-only profiler | all `react-profiler-*` (incl. `component-source`),
all `profiler-*` query / combined tools |

## Verification history

Three commits resolved findings from two parallel verification swarms:

- `507b295` — swarm v1 fixes: early-disconnect race in new blueprint,
NaN timestamp crash, disconnect listener leak, missing test coverage for
13 of 15 lockouts, missing disconnect→terminated propagation test, stale
skill + tool descriptions.
- `dd0f185` — pre-existing crash: `spawn()` in boot-electron lacked an
`'error'` event handler; ENOENT escalated to `uncaughtException`. + 4
regression test cases.
- `95161ab` — swarm v2 fixes: orphan promise rejection when pid-less
spawn fires deferred `'error'`, misleading rationale comment, stale
cross-skill references calling this "Metro debugging only".

Branch was also cherry-picked onto v0.8.0 (`e1327b4`) to pick up #245.

## Follow-ups left for separate PRs

- **CPU profiling on Electron** — Chromium's
`Profiler.start`/`Profiler.stop` work fine; only the V8 sample format
differs from Hermes'. A normalizer in the query layer would unlock
`react-profiler-cpu-summary` + `profiler-cpu-query` for arbitrary
Electron renderers (without the React-DevTools commit half).
- **Source-mapped element inspection on Electron** —
`debugger-inspect-element` could be rebuilt on top of
`DOM.getNodeForLocation` + CSS source maps, but it'd be a from-scratch
implementation rather than a port.

## New sim-server-equivalent HTTP surface

Mounted at `/electron-server/:deviceId/*` — not advertised to MCP,
consumed by preview UIs and integration tests:

- `GET /viewport`, `GET /stream.mjpeg` (multipart-JPEG screencast)
- `POST /api/screenshot`, `/api/clipboard/text`, `/api/fps`,
`/api/navigate`, `/api/reload`, `/api/history/back`,
`/api/history/forward`
- `WS /ws` (input commands + event bus)

Sharp is loaded as an optional dependency — screenshots still work
without it (no rotate/resize, just raw PNG).

## Test plan

- [x] 786 vitest cases pass; new coverage for the dispatch helper,
blueprint factory edge cases (early disconnect, NaN timestamp, dispose
symmetry), 15-tool table-driven lockout assertion, and 5 boot-electron
spawn-error scenarios.
- [x] Build + prettier clean.
- [x] E2E swept the original 27 surfaces (11 MCP tools, 8 HTTP
endpoints, MJPEG, WebSocket, 6 capability rejections, session cleanup).
- [x] E2E swept the debugger surface against a live Electron app —
connect / status / evaluate (`1+1`, JSON-stringified globals,
`document.title`) / log-registry (clusters + log file).
- [x] E2E swept the 17 lockouts — every one returns HTTP 400 with `Tool
'X' is not supported on electron app`.
- [x] Two rounds of parallel verification swarm; every real finding
addressed.
- [ ] Reviewer drives an Electron app end-to-end via MCP and confirms
describe / tap / swipe / keyboard / debugger-evaluate land correctly.

---------

Co-authored-by: Filip131311 <contact@filipkaminski.com>
2026-06-12 17:54:45 +02:00
filip131311 bbc8d789c6 fix(mcp): report the real installed version in the MCP handshake (#329)
## Summary

`serverInfo.version` in the MCP `initialize` response was hardcoded to
`"0.5.3"` in `mcp-server.ts` and had drifted several releases behind the
actual install (currently 0.11.0). Spotted while driving the MCP server
over stdio:

```
server: {"name":"argent","version":"0.5.3"}   # argent --version says 0.11.0
```

## Fix

New `getInstalledVersion()` in
`packages/argent-mcp/src/installed-version.ts` resolves the version at
runtime from the package.json one directory above the executing file —
the same pattern `packages/argent/src/cli.ts` already uses for the
`argent --version` banner:

- published bundle: `dist/mcp-server.mjs` → two-up is
**@swmansion/argent**'s shipped package.json
- dev workspace: compiled `@argent/mcp` output → its own package.json

Both are version-bumped in lockstep. Falls back to `"unknown"` instead
of another literal that can drift.

## Test plan

- [x] Unit tests: helper matches the workspace package.json version and
never returns the stale literal
- [x] `npm run build`, `typecheck:tests`, prettier clean
- [x] 66 argent-mcp tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-12 17:14:46 +02:00
filip131311 c01f41cd5f feat(remote): input-side file boundary — flows, screenshot-diff, profiler reports, workspace tools (#319)
## Why

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

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

## What

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

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

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

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

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

## Tests

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

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

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

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

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:14:04 +02:00
filip131311 fa86905fbd chore: bump versions to 0.11.0 (#322)
Bumps the package suite from `0.10.0` to **`0.11.0`**.

## Changed
- All release packages `0.10.0 → 0.11.0`: `@swmansion/argent`,
`@argent/cli`, `@argent/installer`, `@argent/mcp`,
`@argent/tools-client`, `@argent/native-devtools-android`,
`@argent/native-devtools-ios`, `@argent/registry`, `@argent/skills`,
`@argent/tool-server`, `@argent/update-core`.
- `@argent/configuration-core` `0.9.0 → 0.11.0` — was lagging a release
behind (extracted in #295); aligned to the suite. (It's
`private`/unpublished, so this is internal-only.)
- Regenerated `package-lock.json` to reflect the new workspace versions.

## Not changed
- `@argent/ui` stays at `0.0.1` (separate pre-release track).
- The private workspace root has no version.
- Internal deps use `file:../` specifiers, so no dependency ranges
needed updating.

No source/runtime changes — `argent --version` reads from `package.json`
at runtime.
2026-06-10 17:25:27 +02:00
Paweł Fornagiel 92cc69a75c feat: extract configuration-core and migrate auto-screenshot to feature flag (#295)
## Summary

- Introduces `@argent/configuration-core` as the shared source of truth
for feature-flag storage, the registry, and `isFlagEnabled`, so MCP and
CLI can import the same primitives without coupling runtime code to
CLI-only modules.
- Replaces `ARGENT_AUTO_SCREENSHOT` with the off-by-default
`disable-auto-screenshot` flag (auto-screenshot stays on unless enabled
via `argent enable disable-auto-screenshot`); renames
`ARGENT_AUTO_SHUTDOWN` to `ARGENT_TOOL_SERVER_SHUTDOWN_ON_MCP_EXIT`.
- Keeps `@argent/cli` as the command layer (`enable` / `disable` /
`flags`) with backward-compat re-exports, colored flag state output
(picocolors), and `--help` listings of available flags.

---------

Co-authored-by: Filip131311 <contact@filipkaminski.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 15:01:25 +02:00
filip131311 20cb452fd4 feat(remote): artifact transport for files produced by a remote tool-server (#278)
## Why

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

## What

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: pFornagiel <pawel.fornagiel2@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Paweł Fornagiel <85541429+pFornagiel@users.noreply.github.com>
2026-06-10 12:04:30 +02:00
filip131311 d87fbcc973 chore: bump version to 0.10.0 (#301)
Bumps the version from `0.9.0` to `0.10.0` across all versioned packages
in the workspace.

## Packages bumped (0.9.0 → 0.10.0)
- `@swmansion/argent`
- `@argent/cli`
- `@argent/installer`
- `@argent/mcp`
- `@argent/tools-client`
- `@argent/native-devtools-android`
- `@argent/native-devtools-ios`
- `@argent/registry`
- `@argent/skills`
- `@argent/tool-server`
- `@argent/update-core`

`package-lock.json` updated to match.

## Left unchanged
- `@argent/ui` — on a separate version track (`0.0.1`)
- `packages/argent-private` — has no `version` field

Internal dependencies use `file:` references, so no dependency
specifiers required updating.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-03 17:48:42 +02:00
filip131311 8efd7c6346 feat: add argent server start for long-lived tool-server (#182)
## Human summary: 

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

## Summary

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

## What changed

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

## Test plan

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

---------

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

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

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

## AI Summary

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

### Fixes

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

### Individual branches

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

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

## Test plan

Unit tests (already in CI):

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

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

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

## Notes for reviewers

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

</details>
2026-06-03 15:57:42 +02:00
filip131311 d670751db3 chore: bump all package versions to 0.9.0 (#281)
## Summary
- Bump all 11 workspace packages from `0.8.1` → `0.9.0` (`package.json`
+ `package-lock.json`)
- Includes `@argent/update-core`, which was added since the last bump
(#267)
- `@argent/ui` left at `0.0.1` (independent versioning)

## Notes
- Ran `npm install` to update `package-lock.json` — resolved cleanly
- Ran `npm run format` (prettier) — no files needed reformatting
- No `lint` script / eslint config exists in this repo, so there was
nothing to lint

## Test plan
- [ ] Verify `npm install` resolves cleanly
- [ ] Verify `argent --version` reports `0.9.0`
2026-06-01 17:23:13 +02:00
Hubert Gancarczyk de0dbbda66 feat(tools): implement screenshot-diff tool (#229)
## Summary

Adds a new `screenshot-diff` tool for comparing mobile screenshots
during UI QA and visual regression checks. The tool can compare two
saved PNGs, or compare one saved PNG against one live full-resolution
device capture, then returns a compact text summary and generated diff
artifacts.

## Context

Mobile UI validation often needs more than a single screenshot: small
layout, color, typography,
copy, clipping, and regression changes are easy to miss manually. This
feature gives agents a
deterministic before/after comparison path while keeping
`screenshot-diff` as supporting evidence.

## What Changed

- Registered a new `screenshot-diff` tool in the tool server.
- Supports saved PNG comparisons and one-sided live capture via
`captureBaseline` or
`captureCurrent`.
- Adds PNG diffing with fixed internal defaults, changed-region
detection, status-bar-band
ignoring, full-size diff output, and downscaled context diff output.
- Adds optional OCR/text analysis for moved, appeared, disappeared,
changed, and restyled text.
- Updates MCP content rendering to attach the downscaled context diff
image plus the text summary.

1. Capture a stable full-resolution baseline.
2. Perform an interaction or apply a UI change.
3. Compare the current screen against the baseline.
4. Review a summarized diff plus generated artifacts.
2026-06-01 15:04:25 +02:00
filip131311 05fb58552d chore: bump all package versions to 0.8.1 (#267)
## Summary
- Bump all 10 workspace packages to version `0.8.1` (`package.json` +
`package-lock.json`)
- Includes `native-devtools-android` (`0.7.1` → `0.8.1`), all others
from `0.8.0` → `0.8.1`
- `ui` package left at `0.0.1` (independent versioning)

## Test plan
- [ ] Verify `npm install` resolves cleanly
- [ ] Verify `argent --version` reports `0.8.1`
2026-05-27 16:37:17 +02:00
Ignacy Łątka 90c6081204 fix: prevent returning empty screenshots (#261)
## What broke
`toMcpContent` base64-encoded whatever bytes the simulator-server URL
fetch returned and labelled them `image/png` with no checks. A 404 on
the `/media/...` endpoint (file missing) returns an empty body — that
became `data: ""` shipped as a PNG, which the model API rejects with
"Image could not be processed". Claude Code then loops on the error and
burns the session limit (cf. anthropics/claude-code#60334). Reproduces
directly:

```
$ curl -sI http://127.0.0.1:<port>/media/missing.png
HTTP/1.1 404 Not Found
content-length: 0
```

## What changed
One helper added: `fetchPngBytes(url)` returns the bytes only if:
- `fetch` resolves (catches ECONNREFUSED / DNS / abort)
- `res.ok` is true
- the buffer starts with the 8-byte PNG signature

If any check fails, `toMcpContent` emits a single text placeholder
instead of an image block. Diff: **+103 / −27** across `content.ts` +
tests.

## E2E verification
Tested against the real MCP stdio JSON-RPC interface (not just unit
tests):

**Happy path** — real Android emulator, real tool-server:
```
[1] initialize: argent v0.5.3
[3a] screenshot returned 2 block(s)
     [0] image: mime=image/png dataLen=66968 validPngSig=true
     [1] text: Saved: /var/folders/.../media/430131000-...png
HAPPY PATH overall: PASS
```

**Failure path** — mock tool-server returning a URL pointing at a closed
port:
```
mock on :56529
returned 1 block(s)
  [0] text: (Screenshot unavailable: no valid PNG at http://127.0.0.1:1/stale.png. Take a new screenshot.)
hasImage=false, hasPlaceholder=true
FAILURE PATH: PASS
```

## Test plan
- [x] `npx vitest run` — 1048/1048 pass (5 new regression tests in
`content.test.ts` cover 404, non-PNG body, fetch-throw, and the no-url
fallback)
- [x] `npm run build` — tsc clean
- [x] `npx prettier --check` — clean
- [x] Bundled MCP server rebuilt and driven via real JSON-RPC stdio
against (a) a real Android screenshot and (b) a mock tool-server
reproducing the bug

---------

Co-authored-by: Filip131311 <contact@filipkaminski.com>
2026-05-27 16:25:08 +02:00
Ignacy Łątka 11df817616 test: drop real-time timeouts from the slowest unit tests (#236)
The slowest unit tests were waiting on real wall-clock timers, not doing
work. This makes them deterministic and fast with **no production
behavior change and no production test seams**.

## Speedup

**Suite-level**

| Scope | Before | After | Faster |
|---|---|---|---|
| tool-server — wall clock | 2.72s | ~1.3s | ~2× |
| tool-server — aggregate test time | 9.70s | 4.03s | 2.4× |
| argent-mcp — aggregate test time | 0.90s | 0.28s | 3.2× |

**Per file**

| File | Before | After | Faster |
|---|---|---|---|
| simulator-server-blueprint.test.ts | 2.55s | 0.05s | ~50× |
| boot-device-hardening.test.ts | 2.17s | 0.18s | 12× |
| boot-device-spawn-error.test.ts | 1.11s | 0.005s | ~200× |
| boot-device-hotboot.test.ts | 1.01s | 0.06s | 16× |
| fetch-timeout.test.ts | 0.81s | 0.18s | 4.5× |
| flow-tools.test.ts (delayMs test) | 0.31s | 0.04s | ~7× |

**Worst individual tests, before → after**

| Test | Before | After |
|---|---|---|
| rejects the boot promise…when spawn emits error | 1.06s | ~1ms |
| reports the emulator crash error… | 1.00s | ~2ms |
| falls back to cold boot when hot-boot child exits early | 1.00s | ~1ms
|
| reports a signal-terminated emulator (SIGSEGV)… | 1.00s | ~1ms |
| 5× simulator-server-blueprint tests | ~0.50s ea | ~0.001–0.02s ea |
| throws after retries when POST never responds | 0.60s | 0.14s |
| sleeps the step's delayMs | 0.31s | 0.04s |

No test exceeds ~0.7s anymore (the slowest,
`workspace-reader-integration`, is genuine subprocess I/O — `node
--version` etc. — left as-is on purpose; it is not a timeout).

## What changed

- **`boot-device.ts` (only production change):** the three hardcoded
poll cadences are extracted into a single immutable
`BOOT_POLL_INTERVALS_MS` constant (values unchanged: 1500/1000/500ms)
and referenced at the four `setTimeout` sites. Pure readability
extraction — **no mutable state, no exported test hooks, no runtime
behavior change.**
- **Boot-device tests:** the timing-sensitive tests now use vitest fake
timers (`vi.useFakeTimers()` + `vi.advanceTimersByTimeAsync`).
`boot-device-hardening` / `-spawn-error` add the same partial
`vi.mock("../src/utils/adb")` already used by
`boot-device-hotboot.test.ts`, so pre-flight does no real filesystem I/O
and the timers drive deterministically.
- **`simulator-server-blueprint.test.ts`:** `signalReady` now emits
`stream_ready` then `api_ready` (matching the real streaming server's
ordering) so the blueprint resolves immediately instead of waiting out
the 500ms `STREAM_GRACE_MS` fallback. Adds a dedicated fake-timer test
that explicitly covers the non-streaming grace path (coverage gained,
not lost).
- **`fetch-timeout.test.ts` / `flow-tools.test.ts`:** smaller timing
knobs; same behaviors asserted.

## No regressions

- 1011/1011 tests pass (tool-server 656, argent-mcp 53, argent 6,
registry 64, tools-client 1, installer 231); `tsc --build` and Prettier
clean. The converted boot-device, simulator-server-blueprint, and
flow-tools tests are deterministic under fake timers (verified across
stress runs).
- No production behavior change; `simulator-server.ts` is byte-identical
to `main`; net +1 test.
- Heads-up: `argent-mcp/test/fetch-timeout.test.ts` has a
**pre-existing** macOS flake (~10–20% rate; `connect EADDRNOTAVAIL
127.0.0.1:<port> - Local (0.0.0.0:0)` — kernel source-port exhaustion
under rapid-fire loopback fetches). Stress-testing showed it on `main`'s
pristine file too (2/10 failures vs 1/10 on this branch), so this PR
does not introduce or worsen it. Fixing it (e.g., EADDRNOTAVAIL retry,
or moving the test off real loopback fetch) is worth a follow-up.
2026-05-27 16:24:07 +02:00
filip131311 09a18a65ef bump to v0.8.0 (#245) 2026-05-21 17:46:28 +02:00
Ignacy Łątka 2180dd8e6a ci: typecheck test files in CI (#202)
We've had lots of type errors in our tests, scripts and other non-core
code despite all CI being green. This PR fixes that.

This PR also removes some redundant features like `@ts-check` inline
directives which have been replaced by proper ts config files.

<details>

## AI Summary

Audit of every CI workflow against every package's `package.json` and
`tsconfig` surfaced several gaps where CI did partial coverage. This PR
closes them and fixes every pre-existing type error the new gates
surfaced.

## Gaps closed

| # | Gap | Fix |
|---|---|---|
| 1 | `tsc --build` only covers `src/**`; test files (`test/**`,
`tests/**`) never typecheck. Vitest transforms tests with esbuild —
strips types instead of checking them. | New `tsconfig.test.json` per
test-bearing package (`composite: false`, `noEmit`, `rootDir: "."`,
includes `src/**` + `test/**` (or `tests/**`) + `vitest.config.ts`). New
`typecheck:tests` npm script per package. New CI step `npm run
typecheck:tests --workspaces --if-present`. |
| 2 | Build/publish scripts (`scripts/*.{cjs,mjs}`,
`packages/argent/scripts/*.cjs`, `packages/skills/scripts/install.js`) —
JS, never typechecked. `sync-readme.cjs` runs in `prepack`;
`postinstall.cjs` runs on every install; `bundle-tools.cjs` is
build-critical. A typo only surfaces at runtime. | Root
`tsconfig.scripts.json` with `allowJs` + `checkJs` + `noImplicitReturns`
covers all script paths. Root `typecheck:scripts` npm script. New CI
step. Per-file `// @ts-check` comments removed — tsconfig drives
inclusion. |
| 3 | `argent-cli` had no `typecheck:tests` script. When tests get added
there they'd silently bypass the gate. | Added `tsconfig.test.json` +
`typecheck:tests` script. `--if-present` would have skipped it anyway,
so this is future-proofing. |
| 4 | Local `prettier --check .` walked into the `argent-private`
submodule and reported violations there. CI didn't see this because
`actions/checkout@v4` doesn't init submodules — so local `prettier
--check` did not match CI behavior. | New `.prettierignore` excludes the
submodule plus `dist/`, `node_modules/`, lock files, `tsbuildinfo`.
No-op in CI; aligns local with remote. |

## Test type errors fixed

These were already present. The `npm test` passed because esbuild
stripped the offending types:

- **argent-installer**: widen `scope` literal type for
dead-code-elimination guard; non-null `addAllowlist!`/`removeAllowlist!`
(optional methods on adapter).
- **argent-mcp**: add `.js` extensions for NodeNext module resolution.
- **registry**: broaden `StaticBlueprintResult` blueprint api from `{
id; deps? }` to `Record<string, unknown>` so existing tests can pass
arbitrary api shapes.
- **tool-server**:
- non-null `zodSchema!` on tool defs that always carry one
(`launchAppTool`, `restartAppTool`, etc.)
- cast sentinel `"ignored"` strings to `DeviceInfo` in factory-rejection
tests
- match the 4-arg `dispatchByPlatform<IosServices, AndroidServices,
Params, Result>` signature (tests still passed the old 3-arg shape)
- `assertFlowRunResult` type guard for the `FlowRunResult |
FlowPrerequisiteNotice` union before `.steps` access
- swap broken `typeof import("supertest").default` for static `import
supertest` (supertest is `export = supertest`)
- fill in the 4 fields missing from a `NativeProfilerSessionApi` mock
(`xctraceProcess`, `recordingTimedOut`, `recordingExitedUnexpectedly`,
`lastExitInfo`)
- cast a vitest `Mock` to a plain function at one direct call site where
the `Procedure | Constructable` union loses callability
- fix `update-checker.test.ts` reading `../../package.json` (off-by-one
— went past tool-server; was relying on a Vite resolver quirk)
- replace one cross-package `../../../registry/src/index` import with
`@argent/registry` so registry's private `services` field isn't compared
between src and dist declarations

All fixes are type-level. Full `npm test --workspaces --if-present`
still reports **991 passed across 87 test files**.

</details>
2026-05-20 17:03:34 +02:00
Ignacy Łątka fbe7b087e7 chore(deps): batch-apply Dependabot updates (#214–#227) (#228)
This PR consolidates every open Dependabot PR that requires **no
source-code changes** into a single branch with one regenerated,
coherent lockfile.

| PR(s) | Update | Status |
|---|---|---|
| #214 | GitHub Actions group — checkout@v6, setup-node@v6,
setup-python@v6, github-script@v9, upload-artifact@v7 | ✅ Applied |
| #215 | prettier 3.8.3, @clack/prompts 1.4.0, semver 7.8.0, yaml 2.9.0,
@modelcontextprotocol/sdk 1.29.0, esbuild 0.28.0 | ✅ Applied |
| #216 / #221 / #224 / #227 | typescript 6.0.3 | ✅ Applied |
| #217 / #222 / #223 | @types/node 25.9.0 | ✅ Applied |
| #220 / #226 | vitest 4.1.6 | ✅ Applied |
| #218 / #225 | zod 4 | ⏸️ Excluded — requires code migration |
| #219 | express 5 | ⏸️ Excluded — requires code migration |
| #215 (partial) | tree-sitter 0.25.0 | ⏸️ Excluded — see below |

**tree-sitter 0.25.0** ships no prebuilt binaries and its `binding.gyp`
hard-codes `-std=c++17`, which fails to compile against Node 20+/24
C++20 `cppgc` headers — a hard `npm install` blocker (would also break
CI). Kept at `^0.21.1`; all other #215 updates applied.

zod 4 and express 5 are excluded here because they require real source
changes; tracked separately.

Verified: `tsc --build` clean, esbuild bundles, **1010 unit tests
pass**, **zero source files modified**.
2026-05-19 12:57:07 +02:00
pFornagiel 12f6ff1115 chore: bump to 0.7.1 2026-05-15 16:55:57 +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 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
pFornagiel 60ae7cb767 chore: bump versions to 0.7.0 2026-05-11 16:44:03 +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
2026-05-08 12:48:23 +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
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
2026-04-30 15:51:32 +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
2026-04-30 13:08:22 +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
2026-04-30 12:45:57 +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
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