@phnx-labs/agents-cli 1.22.75 → 1.22.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/README.md +20 -8
  3. package/dist/bootstrap.js +7 -7
  4. package/dist/cli/command-registry.js +5 -0
  5. package/dist/commands/artifacts-setup.js +1 -1
  6. package/dist/commands/artifacts.js +1 -1
  7. package/dist/commands/auth.js +7 -1
  8. package/dist/commands/browser.js +104 -10
  9. package/dist/commands/commands.js +7 -6
  10. package/dist/commands/config.js +27 -4
  11. package/dist/commands/cost.js +6 -4
  12. package/dist/commands/doctor.d.ts +6 -5
  13. package/dist/commands/doctor.js +32 -274
  14. package/dist/commands/exec.d.ts +2 -0
  15. package/dist/commands/exec.js +9 -2
  16. package/dist/commands/harness.d.ts +1 -0
  17. package/dist/commands/harness.js +11 -3
  18. package/dist/commands/hooks.js +7 -6
  19. package/dist/commands/mcp.js +7 -6
  20. package/dist/commands/memory.js +7 -7
  21. package/dist/commands/monitors.js +3 -2
  22. package/dist/commands/open.d.ts +25 -12
  23. package/dist/commands/open.js +24 -10
  24. package/dist/commands/permissions.js +7 -6
  25. package/dist/commands/plugins.js +21 -17
  26. package/dist/commands/route.js +33 -16
  27. package/dist/commands/rules.js +7 -12
  28. package/dist/commands/sessions-share.js +1 -1
  29. package/dist/commands/setup-watchdog.js +2 -2
  30. package/dist/commands/setup.js +22 -1
  31. package/dist/commands/share.js +26 -10
  32. package/dist/commands/skills.js +7 -6
  33. package/dist/commands/subagents.js +7 -6
  34. package/dist/commands/sync.js +81 -10
  35. package/dist/commands/view.js +4 -1
  36. package/dist/commands/watchdog.d.ts +1 -1
  37. package/dist/commands/watchdog.js +10 -10
  38. package/dist/commands/webhook.d.ts +4 -0
  39. package/dist/commands/webhook.js +22 -4
  40. package/dist/commands/workflows.js +7 -6
  41. package/dist/lib/accounting/rotate.d.ts +3 -1
  42. package/dist/lib/accounting/rotate.js +8 -4
  43. package/dist/lib/auth-health.d.ts +2 -0
  44. package/dist/lib/auth-health.js +2 -0
  45. package/dist/lib/browser/chrome.d.ts +21 -0
  46. package/dist/lib/browser/chrome.js +60 -3
  47. package/dist/lib/browser/drivers/local.d.ts +21 -0
  48. package/dist/lib/browser/drivers/local.js +102 -9
  49. package/dist/lib/browser/profiles.d.ts +29 -1
  50. package/dist/lib/browser/profiles.js +50 -1
  51. package/dist/lib/browser/registry.js +10 -3
  52. package/dist/lib/browser/types.d.ts +18 -0
  53. package/dist/lib/config-keys.d.ts +7 -2
  54. package/dist/lib/config-keys.js +17 -2
  55. package/dist/lib/daemon/daemon.js +8 -0
  56. package/dist/lib/daemon/session-summarizer-service.d.ts +24 -0
  57. package/dist/lib/daemon/session-summarizer-service.js +39 -0
  58. package/dist/lib/daemon-services.d.ts +1 -1
  59. package/dist/lib/daemon-services.js +5 -0
  60. package/dist/lib/daemon-ticks.d.ts +2 -2
  61. package/dist/lib/daemon-ticks.js +2 -1
  62. package/dist/lib/deeplink/register.js +10 -9
  63. package/dist/lib/deeplink/url.d.ts +4 -4
  64. package/dist/lib/deeplink/url.js +4 -4
  65. package/dist/lib/device-config.js +25 -0
  66. package/dist/lib/devices/doctor-findings.d.ts +4 -4
  67. package/dist/lib/devices/doctor-findings.js +14 -8
  68. package/dist/lib/devices/registry.js +2 -0
  69. package/dist/lib/devices/stats-cache.d.ts +4 -0
  70. package/dist/lib/devices/stats-cache.js +19 -0
  71. package/dist/lib/drift-sync.d.ts +3 -1
  72. package/dist/lib/drift-sync.js +16 -5
  73. package/dist/lib/exec.d.ts +2 -0
  74. package/dist/lib/exec.js +16 -1
  75. package/dist/lib/fleet-shared-state.d.ts +8 -0
  76. package/dist/lib/git.js +6 -0
  77. package/dist/lib/heal.d.ts +4 -3
  78. package/dist/lib/heal.js +5 -4
  79. package/dist/lib/hosts/ready.d.ts +1 -1
  80. package/dist/lib/hosts/ready.js +16 -4
  81. package/dist/lib/hosts/reconnect.js +4 -2
  82. package/dist/lib/identity/client.d.ts +6 -0
  83. package/dist/lib/identity/index.d.ts +16 -0
  84. package/dist/lib/identity/index.js +25 -1
  85. package/dist/lib/installations/migrate.d.ts +10 -0
  86. package/dist/lib/installations/migrate.js +18 -0
  87. package/dist/lib/profiles.d.ts +2 -0
  88. package/dist/lib/profiles.js +28 -9
  89. package/dist/lib/project-resources.d.ts +23 -0
  90. package/dist/lib/project-resources.js +94 -0
  91. package/dist/lib/reconcile-and-repair.d.ts +109 -0
  92. package/dist/lib/reconcile-and-repair.js +267 -0
  93. package/dist/lib/routers.d.ts +12 -1
  94. package/dist/lib/routers.js +30 -1
  95. package/dist/lib/scheduling/routines.js +8 -2
  96. package/dist/lib/session/active.d.ts +13 -0
  97. package/dist/lib/session/db.d.ts +47 -7
  98. package/dist/lib/session/db.js +114 -12
  99. package/dist/lib/session/mirror.js +58 -0
  100. package/dist/lib/session/remote/watch.js +22 -2
  101. package/dist/lib/session/session-cache.d.ts +19 -0
  102. package/dist/lib/session/session-cache.js +46 -0
  103. package/dist/lib/session/types.d.ts +34 -0
  104. package/dist/lib/share/backend.d.ts +6 -4
  105. package/dist/lib/share/backend.js +10 -8
  106. package/dist/lib/share/config.d.ts +4 -3
  107. package/dist/lib/share/config.js +10 -1
  108. package/dist/lib/share/delete.d.ts +1 -1
  109. package/dist/lib/share/delete.js +1 -1
  110. package/dist/lib/share/html.d.ts +1 -1
  111. package/dist/lib/share/html.js +1 -1
  112. package/dist/lib/share/provision.d.ts +1 -1
  113. package/dist/lib/share/provision.js +2 -2
  114. package/dist/lib/share/publish.d.ts +23 -7
  115. package/dist/lib/share/publish.js +58 -12
  116. package/dist/lib/share/worker-template.js +221 -60
  117. package/dist/lib/startup/command-registry.js +2 -2
  118. package/dist/lib/state.d.ts +58 -1
  119. package/dist/lib/state.js +138 -16
  120. package/dist/lib/summarizer/config.d.ts +46 -0
  121. package/dist/lib/summarizer/config.js +83 -0
  122. package/dist/lib/summarizer/pass.d.ts +45 -0
  123. package/dist/lib/summarizer/pass.js +112 -0
  124. package/dist/lib/summarizer/summarize.d.ts +68 -0
  125. package/dist/lib/summarizer/summarize.js +120 -0
  126. package/dist/lib/teams/agents.d.ts +4 -3
  127. package/dist/lib/teams/agents.js +12 -4
  128. package/dist/lib/teams/scheduler.d.ts +4 -2
  129. package/dist/lib/teams/scheduler.js +6 -6
  130. package/dist/lib/tmux/session.d.ts +2 -0
  131. package/dist/lib/tmux/session.js +7 -1
  132. package/dist/lib/types.d.ts +20 -0
  133. package/dist/lib/verbs.d.ts +23 -0
  134. package/dist/lib/verbs.js +24 -0
  135. package/dist/lib/view-types.d.ts +4 -0
  136. package/dist/lib/watchdog/rotate.d.ts +1 -1
  137. package/dist/lib/watchdog/rotate.js +1 -1
  138. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,126 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.77
4
+
5
+ - **`agents repo pull` / `agents sync` no longer wedge on a CLI-regenerated `~/.agents/agents.yaml` (PHNX-3968).** The shared central config was rewritten by config commands without being committed, so between the daemon's publish ticks the tree sat dirty on `agents.yaml`; an incoming peer commit that also touched it made the pull refuse with *"incoming changes touch uncommitted paths: agents.yaml"*, forcing manual autostash gymnastics on the fleet. Central config edits are now committed synchronously by the command that makes them (after the meta lock releases, skipped for the daemon whose own publish tick owns its commits), so the tree is clean at rest and the trip cannot occur. A genuinely dirty-and-differing central file now *refuses* the pull (data-safe, self-heals on the next commit) rather than silently taking the remote copy. The user-repo `CHANGELOG.md` (a duplicate of the shipped one) is de-tracked via `.git/info/exclude` so it stops churning the tree. Source: `cli/src/lib/state.ts`, `cli/src/lib/git.ts`, `cli/src/lib/browser/registry.ts`, `cli/src/lib/project-resources.ts`.
6
+
7
+ ## 1.22.76
8
+
9
+ - **Per-session summarizer: bound the mirror publish (PHNX-3939 follow-up).** The fleet session mirror now caps `checkpoints`/`summaryChecklist` on the **publish** side (50/100 items, 400-char text) exactly as it already did on consume, so a box's own summarizer output can never ride unbounded into the git-synced `daemon-state.json` every peer fetches — matching the `snippet()` cap already applied to `goal`. Source: `cli/src/lib/session/mirror.ts`.
10
+
11
+ - **Per-session summarizer on the session stream (PHNX-3939).** A new opt-in daemon
12
+ service computes a 1–2 line **goal**, progress **checkpoints**, and a detailed
13
+ **checklist** for each running session and delivers them on `agents sessions watch`
14
+ / `agents feed watch --json` (as `goal` / `checkpoints` / `summaryChecklist` /
15
+ `summaryState`), so the AGI extension sidebar can render them. It is **off by
16
+ default and never on the request path**: the model call runs only in the
17
+ background `session-summarizer` service, reader-gated and bounded per tick, and a
18
+ computed summary is cached in the transcript-keyed `session_summaries` table so it
19
+ never recomputes on unchanged bytes and `agents sessions <id>` timing is unchanged.
20
+ Enable it with `agents config set summarizer.enabled on` plus a local
21
+ Anthropic-wire endpoint — `agents config set summarizer.baseUrl http://localhost:11434`
22
+ and `agents config set summarizer.model qwen2.5:3b` (env overrides:
23
+ `AGENTS_SUMMARIZER_ENABLED` / `AGENTS_SUMMARIZER_BASEURL` / `AGENTS_SUMMARIZER_MODEL`).
24
+ Summaries ride the fleet session mirror, so a peer's sessions carry them too. When
25
+ disabled or unconfigured, zero model calls are made and rows read
26
+ `summaryState: "skipped"`. Source: `cli/src/lib/summarizer/`,
27
+ `cli/src/lib/daemon/session-summarizer-service.ts`, `cli/src/lib/session/db.ts`.
28
+
29
+ - **`agents config unset project.root` now actually clears it.** `projectRoot` is a
30
+ machine-local key persisted to the device doc, which `writeMeta` only clears when
31
+ the key is present-but-falsy; the unset path dropped the key entirely, so
32
+ `agents config get project.root` still returned the old value afterward. Source:
33
+ `cli/src/commands/config.ts`.
34
+
35
+ ---
36
+ type: fixed
37
+ ---
38
+
39
+ Keep fleet account and device routing consistent: poolless teams now use the active worker pool, remote readiness rejects stale account evidence, removed devices lose cached stats, partial device writes preserve unrelated metadata, paths are passed without shell reparsing, and harness forks can translate across native hosts.
40
+
41
+ - **Managed share: collision recovery, race-safe republish, and OAuth avatars
42
+ (PHNX-3942, folding PHNX-3547 + PHNX-3579).** Three correctness gaps on the
43
+ artifact-sharing path are closed. (1) A same-local-part account move no longer
44
+ dead-ends on the permanent 409: the Worker's `__handles/<handle>` claim now records
45
+ the verified email, and a request whose verified email matches the claim but whose
46
+ userId differs (the same human re-authenticated under a new account id) transfers
47
+ the claim and re-stamps the old account's objects instead of refusing; when the
48
+ handle genuinely belongs to someone else, `agents artifacts share --handle <name>`
49
+ publishes under an alternate first-writer-claimed namespace (new `x-share-handle`
50
+ Worker header). (2) A concurrent republish no longer silently discards a body: the
51
+ Worker's read → archive-revision → overwrite sequence is now a bounded
52
+ compare-and-swap loop (`onlyIf.etagMatches`, the primitive PATCH and the usage
53
+ ledger already used) — a conflicting write 409s the put, the loser re-reads,
54
+ archives the winner's body as a revision, and lands its own as canonical, so every
55
+ accepted body survives as canonical or revision. (3) Share attribution prefers the
56
+ hosted OAuth profile image: the Phoenix session now carries `avatarUrl` (captured
57
+ at login and refreshed from `/api/v1/auth/me`), and publish stamps it before
58
+ falling back to the email Gravatar. Also, publish failures stop appending
59
+ 'Check the write token' to quota/rate rejections (413/429) — the Worker's actual
60
+ reason is surfaced, and only 401/403 mention the token (or `agents auth login` on
61
+ the managed endpoint). Source: `cli/src/lib/share/worker-template.ts`,
62
+ `cli/src/lib/share/publish.ts`, `cli/src/lib/identity/client.ts`,
63
+ `cli/src/lib/identity/index.ts`, `cli/src/commands/auth.ts`,
64
+ `cli/src/commands/share.ts`.
65
+
66
+ - **Release recovery and exclusion now hold under rebases, interruption, and concurrent releasers (PHNX-3945).** A retry validates the merged target version and the recorded/fetched release PR identity, then tags and promotes the exact attested PR head even when the merge tree differs. Attestation fallback is capped at 45 seconds total (15-second optional fetch plus a 30-second poll), content-addressed records publish through a same-directory atomic rename, and release-lease acquisition/reclamation uses one expected-old-ref compare-and-swap instead of overwritable custom-ref pushes or a delete/create gap.
67
+
68
+ ---
69
+ type: changed
70
+ ---
71
+
72
+ - **`agents watchdog` uses the standard `enable`/`disable` verbs (PHNX-3949).**
73
+ The device-local daemon watchdog pass is now toggled with
74
+ `agents watchdog enable` / `agents watchdog disable`, matching the
75
+ `enable`/`disable` shape `menubar` and `daemon` already use for the same
76
+ concept. The old `on` / `off` spellings keep working as aliases, so no existing
77
+ invocation or script breaks. Source: `cli/src/commands/watchdog.ts`.
78
+
79
+ ---
80
+ type: changed
81
+ ---
82
+
83
+ - **CLI verb vocabulary unified behind one canonical alias table (PHNX-3949).**
84
+ A new `cli/src/lib/verbs.ts` defines the standard short-form aliases
85
+ (`list`→`ls`, `remove`→`rm`, `view`→`show`, `rename`→`mv`) and a `withAliases`
86
+ helper, applied uniformly across `skills`, `hooks`, `commands`, `rules`,
87
+ `permissions`, `mcp`, `subagents`, `workflows`, and `memory` so every resource
88
+ group answers to the same shortcuts. No primary command name changes. Source:
89
+ `cli/src/lib/verbs.ts`.
90
+ - **`agents route` now uses the standard verbs and pluralizes its noun (PHNX-3949).**
91
+ The router command group matches the shape `harness` already set: read-one is
92
+ `route view <name>` (old `show` kept as an alias), create is `route add <name>`
93
+ (old `create` kept as an alias), `list` gains the `ls` short alias, and the
94
+ whole tree resolves under the plural noun `agents routes ...`. New
95
+ `route rename <old> <new>` re-keys a user-layer router in place, preserving
96
+ every field (harness allowlists, weights, linked accounts, hijack flag); it
97
+ fails loud on a missing source, a name collision in any layer, or a router
98
+ that resolves from a non-user layer. Source: `cli/src/commands/route.ts`,
99
+ `cli/src/lib/routers.ts`.
100
+ - **`plugins` moves to the canonical `view`/`add` verbs, and `monitors` gains an
101
+ `rm` alias (PHNX-3949).** `agents plugins info` is now `agents plugins view`
102
+ and `agents plugins install` is now `agents plugins add`; the old names keep
103
+ working as aliases, so no existing invocation breaks. `plugins list` gains an
104
+ `ls` alias and `plugins remove` gains `rm`. `agents monitors remove <name>`
105
+ gains the `rm` alias. Source: `cli/src/commands/plugins.ts`,
106
+ `cli/src/commands/monitors.ts`.
107
+ - **`agents open` is now the machine-only `_callback` verb; handler setup moved under `agents setup url-scheme` (PHNX-3949).**
108
+ `agents open <url>` was never a user command — it is the OS callback the
109
+ `agents://` deep-link handler invokes when you click a session link in a
110
+ rendered artifact. It is now hidden as the machine-only `agents _callback`
111
+ verb (humans resume with `agents sessions resume`), and the register /
112
+ unregister / status plumbing moved to a visible `agents setup url-scheme`
113
+ group. Nothing breaks: `agents open <url>` stays a hidden alias so OS handlers
114
+ written by older CLIs keep resolving until `agents setup url-scheme register`
115
+ re-writes them to the new verb, and `agents open register|unregister|status`
116
+ still work as hidden back-compat subcommands. Source:
117
+ `cli/src/commands/open.ts`, `cli/src/commands/setup.ts`,
118
+ `cli/src/lib/deeplink/register.ts`.
119
+
120
+ - **One canonical Comet: attach-only browser profiles + closed port-squat (PHNX-3967).** A browser profile now carries a `launchPolicy` (`attach-only` | `launch`) and a durable `--user-data-dir`. An attach-only profile (`agents browser profiles create <name> --browser comet --attach-only`, and Arc always) NEVER spawns a rival window — agents attach to the browser you already started with remote debugging, else fail loud with the exact `open -a <app> --args --remote-debugging-port=<port> --user-data-dir=<durable>` relaunch. Before adopting an endpoint the local driver now verifies ownership beyond browser family: it compares the running instance's `--user-data-dir` to the profile's durable dir and rejects a foreign port-squatter (e.g. a logged-out `/tmp` Comet on the canonical port) instead of driving it — and if an occupant is serving CDP but its `--user-data-dir` can't be read, the guard refuses to attach to the unverified instance (fail loud) rather than driving it silently, with `profiles doctor` surfacing the same unverified state instead of a green check. The durable dir lives under `~/.agents/.history/browser-profiles/<name>/` (outside `.cache`), so a one-time sign-in survives quit+relaunch and `profiles remove`. `agents browser profiles doctor` flags a foreign-user-data-dir Comet on the canonical port. Source: `apps/cli/src/lib/browser/drivers/local.ts`, `apps/cli/src/lib/browser/{profiles,chrome,types}.ts`, `apps/cli/src/commands/browser.ts`.
121
+
122
+ - **`agents doctor` is now diagnose-only; `agents sync` is the single fixer.** `doctor --fix` is removed — running it prints a one-line redirect to `agents sync` and exits non-zero — and every `doctor` remediation now points at `agents sync`. `sync` gains the repairs `doctor --fix` used to own: managed-hook-runtime-shim repair (the "source mismatch" fixer), hook re-wiring, and full home-rot heal, running on **every** sync path — the umbrella, agent-scoped, single-version, and the `agents sync status` / menu-bar drift path — so one `agents sync` now converges a version that `doctor` flags, instead of two commands that could disagree. Repair failures surface in `--json` output and the process exit code. The machine-wide stale-CLI purge no longer runs on a routine sync; it is now the explicit `agents sync --prune-clis` flag. Source: `cli/src/lib/reconcile-and-repair.ts`, `cli/src/commands/sync.ts`, `cli/src/commands/doctor.ts`, `cli/src/lib/drift-sync.ts`.
123
+
3
124
  ## 1.22.75
4
125
 
5
126
  - **Account labels are per-harness, and survive a daemon publish (PHNX-3887).** One
package/README.md CHANGED
@@ -493,6 +493,18 @@ Slack). Owner delivery attempts every selected channel, reports partial
493
493
  failures, and safely forwards Rush-backed channels from headless workers to a
494
494
  capable Mac; ordinary milestone posts remain record-only unless configured.
495
495
 
496
+ An optional **per-session summarizer** (off by default) adds a daemon-computed
497
+ `goal`, progress `checkpoints`, and a detailed `summaryChecklist` to each session
498
+ row on the `feed watch` / `sessions watch` stream. It never runs on a request
499
+ path — the model call lives in a background daemon service, cached per transcript
500
+ so it never recomputes on unchanged bytes. Turn it on with a local model endpoint:
501
+
502
+ ```bash
503
+ agents config set summarizer.enabled on
504
+ agents config set summarizer.baseUrl http://localhost:11434 # Ollama / vLLM / LiteLLM
505
+ agents config set summarizer.model qwen2.5:3b
506
+ ```
507
+
496
508
  Top-level questions and waiting notifications publish one atomic open-block record per session, including the mailbox id, host, runtime, and every answer option. The default view collapses agents under the **outcome** they serve (Linear ticket, PR, worktree slug, or Unassigned) so a 1,100-agent fleet reads as dozens of deliverables. Answered, resumed, and stopped blocks clear automatically; Task subagents are excluded. The rendered reply command uses the same mailbox id with `agents message`, so the decision routes back to the agent that asked it.
497
509
 
498
510
  ### Auto-nudge stalls
@@ -504,7 +516,7 @@ agents watchdog --nudge # actually inject "Continue." into the stalled split
504
516
  agents watchdog --watch # daemon loop: a tick every --interval
505
517
  ```
506
518
 
507
- `agents watchdog` detects a stalled session, resolves the *exact* terminal split it lives in (tmux, iTerm, VSCodium, or a raw pty), and injects a nudge -- `Continue.` by default, or set `--text`. Its timestamped default output shows attention-worthy sessions with their agent, host app, machine, project, activity, age, path, latest preview, and decision reason; `--verbose` restores healthy/non-actionable inspections. It's dry by default; `--nudge` acts on a single tick. `agents watchdog on|off` controls the device-local daemon pass, which runs once every three minutes. Steer a single run with `agents watchdog policy <id> off | keep | handsoff`.
519
+ `agents watchdog` detects a stalled session, resolves the *exact* terminal split it lives in (tmux, iTerm, VSCodium, or a raw pty), and injects a nudge -- `Continue.` by default, or set `--text`. Its timestamped default output shows attention-worthy sessions with their agent, host app, machine, project, activity, age, path, latest preview, and decision reason; `--verbose` restores healthy/non-actionable inspections. It's dry by default; `--nudge` acts on a single tick. `agents watchdog enable|disable` controls the device-local daemon pass, which runs once every three minutes. Steer a single run with `agents watchdog policy <id> off | keep | handsoff`.
508
520
 
509
521
  A stalled session whose tail shows a hard account limit ("You've hit your weekly limit · resets …") is **rotated in place** instead of nudged: the watchdog gates on the same healthy-account selection `agents run auto` makes (zero healthy → one skip event per cooldown window, terminal untouched), injects the harness's exit sequence, relaunches `agents run auto --interactive --session-id <uuid>` in the *same* tab, then replays the old session's resume once the new TUI is live. Default on; `agents watchdog rotate off` disables it (nudging stays on).
510
522
 
@@ -623,7 +635,7 @@ A **router** is a reusable, task-typed allowlist -- which harnesses, which model
623
635
 
624
636
  ```bash
625
637
  # Scope a router to two harnesses, capped at a tier
626
- agents route create research --harness gemini,kimi --tier cheap,default
638
+ agents route add research --harness gemini,kimi --tier cheap,default # alias: create
627
639
 
628
640
  # Narrow one harness's model set
629
641
  agents route allow research kimi kimi-k2
@@ -632,11 +644,12 @@ agents route allow research kimi kimi-k2
632
644
  agents route link-account research gemini personal
633
645
  agents route link-account research kimi work
634
646
 
635
- agents route show research
647
+ agents route view research # alias: show
648
+ agents route rename research prod # rename a router, preserving its config
636
649
  agents route list --json
637
650
  ```
638
651
 
639
- Router YAML has no secrets -- safe to `agents repo push` to a shared repo. Harness ids and model/tier tokens are validated on `create`/`allow`: an unknown harness or an unverifiable model id fails loud and writes nothing. Routers resolve as a layered resource (project > user > system, same as profiles).
652
+ Router YAML has no secrets -- safe to `agents repo push` to a shared repo. Harness ids and model/tier tokens are validated on `add`/`allow`: an unknown harness or an unverifiable model id fails loud and writes nothing. Routers resolve as a layered resource (project > user > system, same as profiles). The old verbs `create`/`show` remain as aliases.
640
653
 
641
654
  ---
642
655
 
@@ -903,9 +916,8 @@ Resolution is project > user > system: a `<repo>/.agents/workflows/<name>/` over
903
916
  Bundle skills, commands, hooks, MCP servers, settings, and permissions under a single manifest. One source dir at `~/.agents/plugins/<name>/`, mirrored into every installed Claude / OpenClaw version automatically.
904
917
 
905
918
  ```bash
906
- # Install from a git URL or local path
907
- agents plugins install hivemind@https://github.com/activeloopai/hivemind.git
908
- agents plugins install ./my-plugin
919
+ # Add (install) from a git URL or local path — `install` is an alias for `add`
920
+ agents plugins add hivemind@https://github.com/activeloopai/hivemind.git
909
921
  agents plugins add ./my-plugin
910
922
 
911
923
  # Apply to one agent (default version) or all supported
@@ -934,7 +946,7 @@ Plugins that ship `hooks/`, `.mcp.json`, `bin/`, `scripts/`, `settings.json` (no
934
946
 
935
947
  ```bash
936
948
  # Hooks-bearing plugins copy in but stay disabled by default
937
- agents plugins install hivemind@https://github.com/activeloopai/hivemind.git \
949
+ agents plugins add hivemind@https://github.com/activeloopai/hivemind.git \
938
950
  --allow-exec-surfaces
939
951
 
940
952
  # Same gate on re-sync (e.g., after upstream updates)
package/dist/bootstrap.js CHANGED
@@ -265,15 +265,15 @@ function maybeWarnMultiInstall() {
265
265
  for (const info of inventory) {
266
266
  console.error(chalk.gray(` ${info.packageRoot} ${info.version} (${info.note})`));
267
267
  }
268
- // RUSH-2705/2713: only advertise `agents doctor --fix` for copies it will
269
- // really delete. A duplicate --fix won't auto-purge (a healthy >=1.22.30 peer,
270
- // OR a pre-1.22.30 copy left alone only because no fixed peer exists — the
271
- // latter is genuinely vulnerable, not healthy) makes --fix a remedy that
272
- // no-ops forever — name the manual removal command instead.
268
+ // RUSH-2705/2713: only advertise the `agents sync --prune-clis` purge for copies it
269
+ // will really delete. A duplicate sync won't auto-purge (a healthy >=1.22.30
270
+ // peer, OR a pre-1.22.30 copy left alone only because no fixed peer exists — the
271
+ // latter is genuinely vulnerable, not healthy) makes it a remedy that no-ops
272
+ // forever — name the manual removal command instead.
273
273
  const peers = inventory.filter((info) => !info.running);
274
274
  console.error(chalk.gray('Upgrades apply to the running copy.'));
275
275
  if (peers.some((info) => info.autoPurgeable)) {
276
- console.error(chalk.gray('Purge npx-cache / legacy / pre-1.22.30 copies with: agents doctor --fix'));
276
+ console.error(chalk.gray('Purge npx-cache / legacy / pre-1.22.30 copies with: agents sync --prune-clis'));
277
277
  }
278
278
  for (const peer of peers.filter((info) => !info.autoPurgeable)) {
279
279
  console.error(chalk.gray(`Remove the ${peer.version} copy at ${peer.packageRoot} with: ${manualUninstallCommand(peer.packageRoot)}`));
@@ -731,7 +731,7 @@ async function runUpgrade(version, options) {
731
731
  console.log(chalk.gray(`Purged ${purge.removed.length} stale agents-cli install${purge.removed.length === 1 ? '' : 's'} (npx-cache / legacy / pre-1.22.30).`));
732
732
  }
733
733
  if (purge.failed.length > 0) {
734
- console.log(chalk.yellow(`Could not purge ${purge.failed.length} stale install${purge.failed.length === 1 ? '' : 's'}; re-run agents doctor --fix.`));
734
+ console.log(chalk.yellow(`Could not purge ${purge.failed.length} stale install${purge.failed.length === 1 ? '' : 's'}; re-run agents sync --prune-clis.`));
735
735
  }
736
736
  // RUSH-2705/2713: duplicates --fix won't auto-purge (a healthy
737
737
  // >=1.22.30 peer, or a pre-1.22.30 copy with no fixed peer to fall back
@@ -157,6 +157,10 @@ export const COMMAND_LOADERS = {
157
157
  monitors: [loadMonitors],
158
158
  projects: [loadProjects],
159
159
  run: [loadRun],
160
+ // `_callback` is the machine-only agents:// deep-link verb; `open` is its
161
+ // hidden back-compat alias (OS handlers written by older CLIs call it). Both
162
+ // tokens must lazy-load the same module so either resolves. See commands/open.ts.
163
+ _callback: [loadOpen],
160
164
  open: [loadOpen],
161
165
  reconnect: [loadReconnect],
162
166
  fork: [loadFork],
@@ -167,6 +171,7 @@ export const COMMAND_LOADERS = {
167
171
  restore: [loadRestore],
168
172
  doctor: [loadDoctor],
169
173
  route: [loadRoute],
174
+ routes: [loadRoute],
170
175
  harness: [loadHarness],
171
176
  harnesses: [loadHarness],
172
177
  secrets: [loadSecrets],
@@ -98,7 +98,7 @@ export async function runShareWizard() {
98
98
  }
99
99
  const { input } = await import('@inquirer/prompts');
100
100
  const baseUrl = await input({
101
- message: 'Endpoint base URL (e.g. https://share.agents-cli.sh)',
101
+ message: 'Endpoint base URL (e.g. https://share.getrush.ai)',
102
102
  validate: (v) => (v.trim().startsWith('http') ? true : 'Enter the full https:// URL of the endpoint.'),
103
103
  });
104
104
  await runShareJoin(baseUrl.trim());
@@ -31,7 +31,7 @@ export function registerArtifactsCommands(program) {
31
31
 
32
32
  # Or provision your own Cloudflare endpoint (or join a teammate's)
33
33
  agents artifacts setup
34
- agents artifacts share join https://share.agents-cli.sh
34
+ agents artifacts share join https://share.getrush.ai
35
35
 
36
36
  # What's live in your namespace, and taking one down
37
37
  agents artifacts share list
@@ -26,7 +26,13 @@ async function login() {
26
26
  await sleep(interval);
27
27
  const poll = await pollDeviceToken(grant.device_code);
28
28
  if (poll.status === 'authorized') {
29
- writeSession({ access_token: poll.access_token, email: poll.user.email, userId: poll.user.id });
29
+ const avatarUrl = poll.user.avatar_url ?? poll.user.picture;
30
+ writeSession({
31
+ access_token: poll.access_token,
32
+ email: poll.user.email,
33
+ userId: poll.user.id,
34
+ ...(avatarUrl ? { avatarUrl } : {}),
35
+ });
30
36
  console.log(chalk.green(`\n Signed in as ${poll.user.email}.`));
31
37
  return;
32
38
  }
@@ -3,13 +3,13 @@ import chalk from 'chalk';
3
3
  import { spawnSync } from 'node:child_process';
4
4
  import * as fs from 'fs';
5
5
  import * as path from 'path';
6
- import { listProfiles, getProfile, createProfile, deleteProfile, getConfiguredDefaultProfileName, resolveProfileRef, resolveProfileRefForStart, getProfileRuntimeDir, extractConfiguredPort, findFreeProfilePort, getEndpointPresets, formatProfilesTable, editProfile, renameProfile, assertRegistrableProfileName, isProfileLaunchableHere, } from '../lib/browser/profiles.js';
6
+ import { listProfiles, getProfile, createProfile, deleteProfile, getConfiguredDefaultProfileName, resolveProfileRef, resolveProfileRefForStart, getProfileRuntimeDir, extractConfiguredPort, findFreeProfilePort, getEndpointPresets, formatProfilesTable, editProfile, renameProfile, assertRegistrableProfileName, isProfileLaunchableHere, isAttachOnlyProfile, resolveProfileDataDir, normalizeDataDir, } from '../lib/browser/profiles.js';
7
7
  import { declaringDevices, migrateCentralBrowserProfiles, profileKind } from '../lib/browser/registry.js';
8
8
  import { resolveActor } from '../lib/actor.js';
9
9
  import { loginsForProfile, profilesLoggedInto, serviceForUrl, loginsWithAccountsForProfile, accountsForProfile, credKeysForService, AUTH_SIGNATURES, } from '../lib/browser/login-detection.js';
10
10
  import { parseSecretRef } from '../lib/browser/secret-ref.js';
11
11
  import { readAndResolveBundleEnv, bundleExists, readBundle, describeBundle } from '../lib/secrets/bundles.js';
12
- import { findBrowserPath, getPortOccupant, isLauncherScript, listInstalledBrowsers } from '../lib/browser/chrome.js';
12
+ import { findBrowserPath, getPortOccupant, getProcessUserDataDir, isLauncherScript, listInstalledBrowsers } from '../lib/browser/chrome.js';
13
13
  import { listProfileCacheDirs, removeProfileCache, listAllProfileSnapshots, buildProfilePrunePlan, pruneProfiles, PRUNE_REASON_TEXT, identityLoopbackMismatch, } from '../lib/browser/runtime-state.js';
14
14
  import { DEFAULT_VIEWPORT, parseWindowSize, parseWindowPosition } from '../lib/browser/devices.js';
15
15
  import { runBrowserSessionsCommand } from './browser-sessions-picker.js';
@@ -610,6 +610,8 @@ function registerProfilesCommands(browser) {
610
610
  .option('--binary <path>', 'Absolute path to the browser/app binary (required with --browser custom)')
611
611
  .option('--electron', 'Treat this profile as an Electron desktop app: never call Target.createTarget; bind to the visible window using --target-filter or the skip-invisible heuristic')
612
612
  .option('--target-filter <expr>', 'Pick the existing CDP page target to drive (Electron apps, and Arc — which reuses an open tab / Space rather than creating one). Format: url:<substring> or title:<substring>')
613
+ .option('--attach-only', 'Never spawn a rival window: attach to a browser you already started with remote debugging, else fail loud with a relaunch hint (PHNX-3967). The model for a canonical signed-in Comet; Arc is always attach-only. Pairs with a durable --user-data-dir.')
614
+ .option('--user-data-dir <path>', "Absolute durable --user-data-dir for this profile's browser. Default for attach-only: ~/.agents/.history/browser-profiles/<name>/chrome-data (outside .cache, so a one-time sign-in survives relaunch).")
613
615
  .action(async (name, opts) => {
614
616
  try {
615
617
  assertRegistrableProfileName(name);
@@ -676,6 +678,10 @@ function registerProfilesCommands(browser) {
676
678
  viewport.x = pos.x;
677
679
  viewport.y = pos.y;
678
680
  }
681
+ if (opts.userDataDir && !path.isAbsolute(String(opts.userDataDir))) {
682
+ console.error('--user-data-dir must be an absolute path');
683
+ process.exit(1);
684
+ }
679
685
  const profile = {
680
686
  name,
681
687
  description: opts.description,
@@ -684,6 +690,8 @@ function registerProfilesCommands(browser) {
684
690
  electron: opts.electron || undefined,
685
691
  targetFilter: opts.targetFilter,
686
692
  endpoints,
693
+ launchPolicy: opts.attachOnly ? 'attach-only' : undefined,
694
+ userDataDir: opts.userDataDir ? String(opts.userDataDir) : undefined,
687
695
  secrets: opts.secrets,
688
696
  chrome: opts.headless ? { headless: true } : undefined,
689
697
  viewport,
@@ -693,6 +701,15 @@ function registerProfilesCommands(browser) {
693
701
  console.log(port !== undefined
694
702
  ? `Added "${name}" on ${machineId()} (port ${port}).`
695
703
  : `Added "${name}" on ${machineId()}.`);
704
+ // Attach-only profiles never launch a browser — tell the user the one-time
705
+ // relaunch that makes their canonical instance attachable, pinned to the
706
+ // durable data dir so the sign-in persists (PHNX-3967).
707
+ if (isAttachOnlyProfile(profile) && profile.browser !== 'arc' && port !== undefined) {
708
+ const app = profile.browser === 'comet' ? 'Comet' : profile.browser;
709
+ console.log(`attach-only: agents will not spawn ${app}. Start the canonical one once with:\n` +
710
+ ` open -a ${app} --args --remote-debugging-port=${port} --user-data-dir=${resolveProfileDataDir(profile)}\n` +
711
+ `Sign in in that window; the data dir is durable so the login survives quit+relaunch.`);
712
+ }
696
713
  // Warn (don't fail) if the declared secrets bundle doesn't exist yet — it
697
714
  // may be created later, but a typo should surface now.
698
715
  if (opts.secrets && !bundleExists(opts.secrets)) {
@@ -717,6 +734,9 @@ function registerProfilesCommands(browser) {
717
734
  .option('--electron', 'Treat this profile as an Electron desktop app')
718
735
  .option('--no-electron', 'Stop treating it as an Electron app')
719
736
  .option('--target-filter <expr>', "url:<substring> or title:<substring>; consulted on Electron and Arc profiles (pass '' to clear)")
737
+ .option('--attach-only', 'Make this profile attach-only (never spawn a rival window) — PHNX-3967')
738
+ .option('--launch', 'Undo attach-only: let agents launch the browser when nothing is serving CDP on the port')
739
+ .option('--user-data-dir <path>', "Absolute durable --user-data-dir (pass '' to clear back to the default durable dir)")
720
740
  .option('--json', 'Output machine-readable JSON')
721
741
  .action(async (name, opts) => {
722
742
  // The browser type and the name are identity, not settings: both key the
@@ -734,6 +754,14 @@ function registerProfilesCommands(browser) {
734
754
  process.exit(1);
735
755
  }
736
756
  }
757
+ if (opts.attachOnly && opts.launch) {
758
+ console.error('Pass either --attach-only or --launch, not both.');
759
+ process.exit(1);
760
+ }
761
+ if (opts.userDataDir && !path.isAbsolute(String(opts.userDataDir))) {
762
+ console.error('--user-data-dir must be an absolute path');
763
+ process.exit(1);
764
+ }
737
765
  const patch = {};
738
766
  if (opts.description !== undefined)
739
767
  patch.description = opts.description || undefined;
@@ -745,6 +773,15 @@ function registerProfilesCommands(browser) {
745
773
  patch.binary = opts.binary;
746
774
  if (opts.targetFilter !== undefined)
747
775
  patch.targetFilter = opts.targetFilter || undefined;
776
+ // launchPolicy toggle: --attach-only sets it, --launch clears it (back to
777
+ // the default launch behavior). commander leaves each undefined when unset.
778
+ if (opts.attachOnly)
779
+ patch.launchPolicy = 'attach-only';
780
+ else if (opts.launch)
781
+ patch.launchPolicy = undefined;
782
+ // '' clears back to the default durable dir; any other value pins it.
783
+ if (opts.userDataDir !== undefined)
784
+ patch.userDataDir = opts.userDataDir || undefined;
748
785
  // commander maps --electron/--no-electron and --headless/--no-headless onto
749
786
  // one boolean each, and leaves it undefined when neither was passed.
750
787
  if (opts.electron !== undefined)
@@ -879,6 +916,13 @@ function registerProfilesCommands(browser) {
879
916
  console.log(`Binary: ${profile.binary}`);
880
917
  if (profile.electron)
881
918
  console.log(`Electron: true`);
919
+ if (isAttachOnlyProfile(profile)) {
920
+ const why = profile.browser === 'arc' ? ' (Arc is always attach-only)' : '';
921
+ console.log(`Launch policy: attach-only${why} — agents attach, never spawn a rival window`);
922
+ if (profile.browser !== 'arc') {
923
+ console.log(`User-data-dir: ${resolveProfileDataDir(profile)} (durable)`);
924
+ }
925
+ }
882
926
  if (profile.targetFilter)
883
927
  console.log(`Target filter: ${profile.targetFilter}`);
884
928
  if (profile.description)
@@ -1049,6 +1093,17 @@ function registerProfilesCommands(browser) {
1049
1093
  \`prune\` only considers profiles this device declares. It never removes a
1050
1094
  profile that is in use, the configured default, or the \`auto-chrome\`
1051
1095
  profile a setup wizard created.
1096
+
1097
+ Arc is your PERSONAL browser and Comet is what agents drive (PHNX-3967).
1098
+ Arc is single-instance with no debug port, so agents can only attach to a
1099
+ running Arc, never launch one — it stays yours. Point agents at Comet as
1100
+ one canonical signed-in profile that never spawns a second window:
1101
+ agents browser profiles create agents-comet --browser comet --attach-only
1102
+ An \`--attach-only\` profile (and Arc, always) attaches to a browser you
1103
+ already started with remote debugging and fails loud with the relaunch
1104
+ command otherwise, pinned to a durable --user-data-dir so the sign-in
1105
+ survives quit+relaunch. A foreign browser squatting the canonical port is
1106
+ rejected, not driven — \`agents browser profiles doctor <name>\` flags it.
1052
1107
  `,
1053
1108
  });
1054
1109
  profiles
@@ -1157,12 +1212,47 @@ function registerProfilesCommands(browser) {
1157
1212
  try {
1158
1213
  const { browser } = await discoverBrowserWsUrl(port, 'localhost', profile.name);
1159
1214
  verifyBrowserIdentity(browser, profile.browser, port);
1160
- checks.push({
1161
- label: 'port',
1162
- ok: true,
1163
- detail: `${port} serving ${browser} (pid ${occupant.pid})`,
1164
- });
1165
- attachingToExistingBrowser = true;
1215
+ // Port-squat check (PHNX-3967): for an attach-only profile, a browser
1216
+ // of the right FAMILY on the canonical port is not enough — confirm
1217
+ // its --user-data-dir is this profile's durable dir. A logged-out
1218
+ // /tmp Comet answering CDP here would otherwise pass as ready and get
1219
+ // driven as if it were the credentialed browser.
1220
+ const expectedDir = resolveProfileDataDir(profile);
1221
+ const ownershipChecked = isAttachOnlyProfile(profile) && profile.browser !== 'arc';
1222
+ const runningDir = ownershipChecked ? getProcessUserDataDir(occupant.pid) : null;
1223
+ if (runningDir && normalizeDataDir(runningDir) !== normalizeDataDir(expectedDir)) {
1224
+ checks.push({
1225
+ label: 'port',
1226
+ ok: false,
1227
+ detail: `${port} serving ${browser} (pid ${occupant.pid}) but from a FOREIGN ` +
1228
+ `user-data-dir ${runningDir} (expected ${expectedDir}). This is a ` +
1229
+ `port-squatter, not this profile's browser — agents will refuse to drive ` +
1230
+ `it. Close it (\`kill ${occupant.pid}\`) and relaunch the canonical browser ` +
1231
+ `with --user-data-dir=${expectedDir}.`,
1232
+ });
1233
+ }
1234
+ else if (ownershipChecked && !runningDir) {
1235
+ // Attach-only, an occupant is serving CDP, but its --user-data-dir
1236
+ // couldn't be read — ownership is UNVERIFIED. Surface it rather than
1237
+ // report a silent green, matching the runtime guard which refuses to
1238
+ // attach to an unverifiable instance (PHNX-3967).
1239
+ checks.push({
1240
+ label: 'port',
1241
+ ok: false,
1242
+ detail: `${port} serving ${browser} (pid ${occupant.pid}) but its --user-data-dir ` +
1243
+ `could not be read, so ownership can't be confirmed. agents will refuse to ` +
1244
+ `attach to an unverified instance. Relaunch the canonical browser with ` +
1245
+ `--user-data-dir=${expectedDir} so the attach-only guard can verify it.`,
1246
+ });
1247
+ }
1248
+ else {
1249
+ checks.push({
1250
+ label: 'port',
1251
+ ok: true,
1252
+ detail: `${port} serving ${browser} (pid ${occupant.pid})`,
1253
+ });
1254
+ attachingToExistingBrowser = true;
1255
+ }
1166
1256
  }
1167
1257
  catch (err) {
1168
1258
  const msg = err instanceof Error ? err.message : String(err);
@@ -1174,8 +1264,12 @@ function registerProfilesCommands(browser) {
1174
1264
  }
1175
1265
  }
1176
1266
  }
1177
- // 3. User-data-dir exists and is writable
1178
- const userDataDir = path.join(getProfileRuntimeDir(name), 'chrome-data');
1267
+ // 3. User-data-dir exists and is writable. An attach-only profile's login
1268
+ // lives in the DURABLE dir (outside .cache) the canonical browser is
1269
+ // launched with; a launch-policy profile uses the managed cache dir.
1270
+ const userDataDir = isAttachOnlyProfile(profile) && profile.browser !== 'arc'
1271
+ ? resolveProfileDataDir(profile)
1272
+ : path.join(getProfileRuntimeDir(name), 'chrome-data');
1179
1273
  try {
1180
1274
  if (!fs.existsSync(userDataDir)) {
1181
1275
  checks.push({
@@ -1,3 +1,4 @@
1
+ import { withAliases } from '../lib/verbs.js';
1
2
  import chalk from 'chalk';
2
3
  import ora from 'ora';
3
4
  import * as fs from 'fs';
@@ -42,8 +43,8 @@ When to use:
42
43
  - New version: 'agents commands add --agents claude@2.1.112' to carry commands forward
43
44
  - Custom tooling: write a command markdown file, test it, then share via 'agents commands add ~/my-cmd.md'
44
45
  `);
45
- commandsCmd
46
- .command('list [agent]')
46
+ withAliases(commandsCmd
47
+ .command('list [agent]'), 'list')
47
48
  .option('--json', 'Emit machine-readable JSON instead of the table/picker')
48
49
  .description('Show which slash commands are installed and which agent versions they are synced to')
49
50
  .option('-a, --agent <agent>', 'Filter to a specific agent (alternative to positional arg)')
@@ -265,8 +266,8 @@ Examples:
265
266
  process.exit(1);
266
267
  }
267
268
  });
268
- commandsCmd
269
- .command('remove [name]')
269
+ withAliases(commandsCmd
270
+ .command('remove [name]'), 'remove')
270
271
  .description('Delete a command from agents (interactive picker if no name given)')
271
272
  .option('-a, --agents <list>', 'Limit removal to specific agents (e.g., claude,codex)')
272
273
  .addHelpText('after', `
@@ -405,8 +406,8 @@ Examples:
405
406
  console.error(chalk.gray('Use: agents prune cleanup commands (or `agents prune cleanup` for everything)'));
406
407
  process.exit(1);
407
408
  });
408
- commandsCmd
409
- .command('view [name]')
409
+ withAliases(commandsCmd
410
+ .command('view [name]'), 'view')
410
411
  .description('Read the full content of a command file with markdown rendering')
411
412
  .addHelpText('after', `
412
413
  Examples:
@@ -45,6 +45,8 @@ function parseValue(key, parsed, raw) {
45
45
  return raw.trim();
46
46
  case 'project':
47
47
  return raw.trim();
48
+ case 'summarizer':
49
+ return parsed.property === 'enabled' ? parseBool(raw, key) : raw.trim();
48
50
  case 'device': {
49
51
  const property = parsed.property;
50
52
  switch (property) {
@@ -126,6 +128,10 @@ function setConfig(parsed, value) {
126
128
  case 'project':
127
129
  setProjectRoot(value);
128
130
  return;
131
+ case 'summarizer': {
132
+ setConfigValue(`summarizer.${parsed.property}`, value);
133
+ return;
134
+ }
129
135
  case 'device': {
130
136
  const configName = devicePropertyToConfigName(parsed.property);
131
137
  if (parsed.property === 'notes') {
@@ -176,10 +182,18 @@ function unsetConfig(parsed) {
176
182
  }
177
183
  case 'project': {
178
184
  const had = getProjectRoot() !== undefined;
179
- updateMeta((meta) => {
180
- const { projectRoot: _projectRoot, ...rest } = meta;
181
- return rest;
182
- });
185
+ // `projectRoot` is a machine-local key that `writeMeta` persists to the
186
+ // device doc; it only clears that doc when the key is PRESENT-but-falsy
187
+ // (`writesProjectRoot` needs the own-property, state.ts). Dropping the key
188
+ // from the returned object left the device-doc value in place, so
189
+ // `config get` still returned it after unset. Set it undefined instead.
190
+ updateMeta((meta) => ({ ...meta, projectRoot: undefined }));
191
+ return had;
192
+ }
193
+ case 'summarizer': {
194
+ const name = `summarizer.${parsed.property}`;
195
+ const had = getConfigValue(name).value !== undefined;
196
+ unsetConfigValue(name);
183
197
  return had;
184
198
  }
185
199
  case 'device': {
@@ -212,6 +226,8 @@ function getConfig(parsed) {
212
226
  }
213
227
  case 'project':
214
228
  return getProjectRoot();
229
+ case 'summarizer':
230
+ return getConfigValue(`summarizer.${parsed.property}`).value;
215
231
  case 'device': {
216
232
  const configName = devicePropertyToConfigName(parsed.property);
217
233
  return getConfigValue(configName, { device: parsed.device }).value;
@@ -287,6 +303,13 @@ function* listCentralConfigEntries() {
287
303
  const key = 'browser.device';
288
304
  yield { key, value: browserDevice, hint: configKeyStorageHint(parseConfigKey(key)) };
289
305
  }
306
+ // Session-summarizer keys (PHNX-3939) — user scope, syncs fleet-wide.
307
+ for (const key of ['summarizer.enabled', 'summarizer.baseUrl', 'summarizer.model']) {
308
+ const value = getConfigValue(key).value;
309
+ if (value !== undefined) {
310
+ yield { key, value, hint: configKeyStorageHint(parseConfigKey(key)) };
311
+ }
312
+ }
290
313
  }
291
314
  /** Collect device-scope config entries. */
292
315
  function* listDeviceConfigEntries(device) {
@@ -11,12 +11,13 @@ export function registerCostCommand(insightsCmd) {
11
11
  .description('Roll up $ cost and duration across local agent sessions')
12
12
  .option('--json', 'Output the rollup as JSON')
13
13
  .option('--since <time>', 'Only sessions newer than this (e.g., 7d, 4w, or ISO date)')
14
- .option('--by <dimension>', 'Group the breakdown by: agent (default), project, day, or account (the Claude org that produced each session)')
14
+ .option('--by <dimension>', 'Group the breakdown by: agent (default), project, day, model, or account (the Claude org that produced each session)')
15
15
  .addHelpText('after', `
16
16
  Examples:
17
17
  agents insights cost Daily histogram + top sessions + per-agent breakdown
18
18
  agents insights cost --since 30d Last 30 days only
19
19
  agents insights cost --by project Break down by project instead of agent
20
+ agents insights cost --by model --json Per-model output tokens and recorded cost
20
21
  agents insights cost --by day --json Machine-readable daily rollup
21
22
 
22
23
  Cost is computed offline from a versioned per-model price table (${PRICING_VERSION}).
@@ -35,9 +36,9 @@ Cost is computed offline from a versioned per-model price table (${PRICING_VERSI
35
36
  function resolveGroup(by) {
36
37
  if (by === undefined)
37
38
  return 'agent';
38
- if (by === 'agent' || by === 'project' || by === 'day' || by === 'account')
39
+ if (by === 'agent' || by === 'project' || by === 'day' || by === 'model' || by === 'account')
39
40
  return by;
40
- console.error(chalk.red('error: --by must be one of: agent, project, day, account'));
41
+ console.error(chalk.red('error: --by must be one of: agent, project, day, model, account'));
41
42
  process.exit(1);
42
43
  }
43
44
  async function costAction(options) {
@@ -111,7 +112,8 @@ async function costAction(options) {
111
112
  const groupLabel = groupBy === 'agent' ? 'agent'
112
113
  : groupBy === 'project' ? 'project'
113
114
  : groupBy === 'account' ? 'account'
114
- : 'day';
115
+ : groupBy === 'model' ? 'model'
116
+ : 'day';
115
117
  out.push(chalk.bold(`By ${groupLabel}`));
116
118
  const cols = terminalWidth();
117
119
  const costW2 = Math.max(...breakdown.map(r => formatUsd(r.costUsd).length), 4);