@phnx-labs/agents-cli 1.20.50 → 1.20.52

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 (124) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/commands/browser-picker.js +1 -18
  3. package/dist/commands/browser.js +215 -7
  4. package/dist/commands/cloud.js +7 -25
  5. package/dist/commands/computer.d.ts +1 -0
  6. package/dist/commands/computer.js +129 -8
  7. package/dist/commands/events.d.ts +1 -1
  8. package/dist/commands/events.js +2 -3
  9. package/dist/commands/exec.js +66 -8
  10. package/dist/commands/factory.js +9 -4
  11. package/dist/commands/feed.d.ts +9 -0
  12. package/dist/commands/feed.js +69 -0
  13. package/dist/commands/inspect.js +1 -11
  14. package/dist/commands/logs.d.ts +5 -1
  15. package/dist/commands/logs.js +248 -3
  16. package/dist/commands/mcp.js +9 -6
  17. package/dist/commands/message.js +1 -4
  18. package/dist/commands/profiles.js +1 -18
  19. package/dist/commands/repo.js +33 -14
  20. package/dist/commands/resource-view.d.ts +1 -0
  21. package/dist/commands/resource-view.js +5 -17
  22. package/dist/commands/secrets.d.ts +23 -0
  23. package/dist/commands/secrets.js +174 -70
  24. package/dist/commands/sessions-picker.js +1 -18
  25. package/dist/commands/sessions.js +6 -8
  26. package/dist/commands/teams-picker.js +1 -32
  27. package/dist/commands/teams.js +5 -27
  28. package/dist/commands/tmux.js +1 -3
  29. package/dist/commands/view.js +1 -9
  30. package/dist/commands/worktree.js +1 -4
  31. package/dist/index.js +6 -2
  32. package/dist/lib/agents.d.ts +0 -4
  33. package/dist/lib/agents.js +20 -33
  34. package/dist/lib/auto-dispatch-linear.d.ts +18 -0
  35. package/dist/lib/auto-dispatch-linear.js +107 -0
  36. package/dist/lib/auto-dispatch-provider.d.ts +10 -0
  37. package/dist/lib/auto-dispatch-provider.js +25 -0
  38. package/dist/lib/auto-dispatch.d.ts +87 -0
  39. package/dist/lib/auto-dispatch.js +142 -0
  40. package/dist/lib/browser/cdp.js +11 -2
  41. package/dist/lib/browser/drivers/ssh.d.ts +28 -10
  42. package/dist/lib/browser/drivers/ssh.js +57 -18
  43. package/dist/lib/browser/login-detection.d.ts +94 -0
  44. package/dist/lib/browser/login-detection.js +274 -0
  45. package/dist/lib/browser/profiles.d.ts +17 -8
  46. package/dist/lib/browser/profiles.js +27 -8
  47. package/dist/lib/browser/refs.js +1 -5
  48. package/dist/lib/browser/secret-ref.d.ts +10 -0
  49. package/dist/lib/browser/secret-ref.js +14 -0
  50. package/dist/lib/browser/service.js +14 -12
  51. package/dist/lib/cli-resources.d.ts +0 -2
  52. package/dist/lib/cli-resources.js +30 -13
  53. package/dist/lib/cloud/rush.d.ts +15 -24
  54. package/dist/lib/cloud/rush.js +7 -32
  55. package/dist/lib/crabbox/cli.js +4 -1
  56. package/dist/lib/crabbox/lease.d.ts +6 -0
  57. package/dist/lib/crabbox/lease.js +40 -10
  58. package/dist/lib/crabbox/runtimes.d.ts +38 -1
  59. package/dist/lib/crabbox/runtimes.js +98 -5
  60. package/dist/lib/daemon.d.ts +12 -9
  61. package/dist/lib/daemon.js +73 -17
  62. package/dist/lib/events.d.ts +31 -5
  63. package/dist/lib/events.js +288 -101
  64. package/dist/lib/exec.js +32 -14
  65. package/dist/lib/feed.d.ts +56 -0
  66. package/dist/lib/feed.js +251 -0
  67. package/dist/lib/format.d.ts +38 -0
  68. package/dist/lib/format.js +108 -0
  69. package/dist/lib/git.d.ts +21 -0
  70. package/dist/lib/git.js +92 -0
  71. package/dist/lib/hooks/cache.d.ts +9 -2
  72. package/dist/lib/hooks/cache.js +220 -8
  73. package/dist/lib/hooks.js +24 -10
  74. package/dist/lib/hosts/passthrough.js +1 -0
  75. package/dist/lib/platform/exec.d.ts +4 -1
  76. package/dist/lib/platform/exec.js +8 -2
  77. package/dist/lib/resources.d.ts +0 -8
  78. package/dist/lib/resources.js +0 -10
  79. package/dist/lib/rotate.js +2 -0
  80. package/dist/lib/runner.js +10 -2
  81. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  82. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  83. package/dist/lib/secrets/agent.d.ts +21 -0
  84. package/dist/lib/secrets/agent.js +63 -1
  85. package/dist/lib/secrets/bundles.d.ts +33 -1
  86. package/dist/lib/secrets/bundles.js +38 -8
  87. package/dist/lib/secrets/icloud-import.d.ts +70 -0
  88. package/dist/lib/secrets/icloud-import.js +173 -0
  89. package/dist/lib/secrets/index.d.ts +36 -0
  90. package/dist/lib/secrets/index.js +99 -9
  91. package/dist/lib/secrets/remote.js +1 -1
  92. package/dist/lib/secrets/sync.js +1 -1
  93. package/dist/lib/session/active.d.ts +11 -1
  94. package/dist/lib/session/active.js +3 -0
  95. package/dist/lib/session/db.d.ts +1 -4
  96. package/dist/lib/session/db.js +20 -25
  97. package/dist/lib/session/discover.d.ts +2 -2
  98. package/dist/lib/session/discover.js +60 -48
  99. package/dist/lib/session/parse.js +35 -34
  100. package/dist/lib/session/render.d.ts +7 -3
  101. package/dist/lib/session/render.js +15 -9
  102. package/dist/lib/session/state.d.ts +55 -0
  103. package/dist/lib/session/state.js +100 -11
  104. package/dist/lib/session/types.d.ts +9 -0
  105. package/dist/lib/shims.d.ts +9 -3
  106. package/dist/lib/shims.js +44 -8
  107. package/dist/lib/ssh-tunnel.d.ts +33 -2
  108. package/dist/lib/ssh-tunnel.js +94 -7
  109. package/dist/lib/staleness/types.d.ts +0 -1
  110. package/dist/lib/startup/command-registry.d.ts +1 -0
  111. package/dist/lib/startup/command-registry.js +2 -0
  112. package/dist/lib/state.d.ts +2 -0
  113. package/dist/lib/state.js +25 -8
  114. package/dist/lib/teams/agents.js +6 -3
  115. package/dist/lib/types.d.ts +24 -1
  116. package/dist/lib/versions.d.ts +0 -26
  117. package/dist/lib/versions.js +2 -145
  118. package/dist/lib/warn-unpushed.d.ts +40 -0
  119. package/dist/lib/warn-unpushed.js +128 -0
  120. package/dist/lib/whats-new.d.ts +5 -3
  121. package/dist/lib/whats-new.js +25 -5
  122. package/package.json +3 -1
  123. package/dist/lib/resources/index.d.ts +0 -53
  124. package/dist/lib/resources/index.js +0 -76
@@ -17,6 +17,14 @@
17
17
  import { summarizeToolUse } from './parse.js';
18
18
  /** A healthy live session writes several times a minute; 2 min ⇒ "recently active". */
19
19
  const ACTIVE_WINDOW_MS = 2 * 60_000;
20
+ /**
21
+ * A prose trailing question ("…?") is a HEURISTIC, so it decays: past this long
22
+ * with no session writes it stops classifying as waiting_input — otherwise a
23
+ * finished session that signed off with "anything else?" reads as needing input
24
+ * forever (RUSH-1522). The structural ExitPlanMode / AskUserQuestion signals are
25
+ * exempt: they are precise, still-unanswered decisions.
26
+ */
27
+ const PROSE_QUESTION_FRESH_MS = 30 * 60_000;
20
28
  /** Claude tool names that structurally mean "the agent handed control back to you". */
21
29
  const PLAN_TOOL = 'ExitPlanMode';
22
30
  const ASK_TOOL = 'AskUserQuestion';
@@ -130,6 +138,69 @@ export function extractCreatedTicket(text) {
130
138
  return `#${gh[1]}`;
131
139
  return undefined;
132
140
  }
141
+ /**
142
+ * Pull the plan markdown out of an `ExitPlanMode` tool_use event's args. The
143
+ * Claude tool schema is `{ plan: string }`; the transcript parser already
144
+ * lifts `input` onto `event.args`. Returns undefined for a missing/empty plan
145
+ * so consumers can rely on `plan?: string` truthiness.
146
+ */
147
+ export function extractPlanText(args) {
148
+ const plan = args?.plan;
149
+ if (typeof plan !== 'string')
150
+ return undefined;
151
+ const trimmed = plan.trim();
152
+ return trimmed ? plan : undefined;
153
+ }
154
+ /**
155
+ * Structured question from a Claude `AskUserQuestion` tool call. Its input is
156
+ * `{ questions: [{ question, header, options: [{label, description}] }] }` and the
157
+ * whole thing is already parsed onto `event.args` by the transcript parser — this
158
+ * surfaces the first question + its options (instead of collapsing to a generic
159
+ * "Asked you a question"). The prompt is a select-list, so each option carries its
160
+ * 1-based selection digit as `key`.
161
+ */
162
+ export function structuredQuestionFromAsk(args) {
163
+ const q = Array.isArray(args?.questions) ? args.questions[0] : undefined;
164
+ if (!q)
165
+ return undefined;
166
+ const text = oneLine(String(q.question ?? q.header ?? '')) || 'Asked you a question';
167
+ const raw = Array.isArray(q.options) ? q.options : [];
168
+ const options = [];
169
+ for (const o of raw) {
170
+ const label = typeof o === 'string' ? oneLine(o) : o?.label != null ? oneLine(String(o.label)) : '';
171
+ if (!label)
172
+ continue;
173
+ const description = typeof o === 'object' && o?.description != null ? oneLine(String(o.description)) : undefined;
174
+ options.push({ label, description, key: String(options.length + 1) });
175
+ }
176
+ return { text, reason: 'question', options: options.length ? options : undefined };
177
+ }
178
+ /**
179
+ * Canonical approve/deny choices for an interactive prompt that carries no
180
+ * agent-supplied option list: Claude's plan-review and permission dialogs. Approve
181
+ * is reliably option 1; deny/keep-planning maps to ESC, which cancels the prompt in
182
+ * every variant (2- or 3-option) — safer than guessing a digit that could differ.
183
+ */
184
+ function planReviewQuestion() {
185
+ return {
186
+ text: 'Plan ready — review it',
187
+ reason: 'plan_review',
188
+ options: [
189
+ { label: 'Approve plan', key: '1' },
190
+ { label: 'Keep planning', key: 'esc' },
191
+ ],
192
+ };
193
+ }
194
+ function permissionQuestion(preview) {
195
+ return {
196
+ text: preview ? `Permission — ${preview}` : 'Waiting on a permission prompt',
197
+ reason: 'permission',
198
+ options: [
199
+ { label: 'Approve', key: '1' },
200
+ { label: 'Deny', key: 'esc' },
201
+ ],
202
+ };
203
+ }
133
204
  /** Does an assistant message read as a question directed at the user? */
134
205
  function looksLikeQuestion(text) {
135
206
  const t = text.trim();
@@ -177,18 +248,27 @@ export function inferActivity(events, ctx = {}) {
177
248
  const lastToolUse = lastOf(meaningful, e => e.type === 'tool_use');
178
249
  // The most informative recent line: a message or tool call as-is, but for a
179
250
  // trailing tool_result/thinking show the tool *call* that produced it (its
180
- // command) rather than a bare "↳ Bash".
251
+ // command), or — failing that — the last assistant message, rather than a bare
252
+ // "↳ Bash" or a content-free "thinking…". This is what stops a trailing thinking
253
+ // block from masking the real turn (the defect behind the "Thinking…" panel).
181
254
  const previewSource = !last
182
255
  ? undefined
183
256
  : last.type === 'message' || last.type === 'tool_use'
184
257
  ? last
185
- : (lastToolUse ?? last);
258
+ : (lastToolUse ?? lastMsg ?? last);
259
+ // Last few assistant turns, most-recent last — context for the decision panel.
260
+ const tail = meaningful
261
+ .filter(e => e.type === 'message' && e.role === 'assistant' && e.content)
262
+ .slice(-3)
263
+ .map(e => oneLine(e.content ?? ''))
264
+ .filter(Boolean);
186
265
  const base = {
187
266
  activity: 'idle',
188
267
  lastRole: lastMsg?.role,
189
268
  lastEventKind: last?.type,
190
269
  lastActivityMs: ctx.mtimeMs,
191
270
  preview: previewSource ? describeEvent(previewSource) : undefined,
271
+ tail: tail.length ? tail : undefined,
192
272
  };
193
273
  if (!last)
194
274
  return base;
@@ -196,12 +276,15 @@ export function inferActivity(events, ctx = {}) {
196
276
  // tool and nothing has come after it.
197
277
  const lastPlanOrAsk = lastOf(meaningful, e => e.type === 'tool_use' && (e.tool === PLAN_TOOL || e.tool === ASK_TOOL));
198
278
  if (lastPlanOrAsk && meaningful.indexOf(lastPlanOrAsk) === meaningful.length - 1) {
199
- return {
200
- ...base,
201
- activity: 'waiting_input',
202
- awaitingReason: lastPlanOrAsk.tool === PLAN_TOOL ? 'plan_review' : 'question',
203
- preview: lastPlanOrAsk.tool === PLAN_TOOL ? 'Plan ready — awaiting your review' : 'Asked you a question',
204
- };
279
+ if (lastPlanOrAsk.tool === PLAN_TOOL) {
280
+ const question = planReviewQuestion();
281
+ const plan = extractPlanText(lastPlanOrAsk.args);
282
+ return { ...base, activity: 'waiting_input', awaitingReason: 'plan_review', preview: question.text, question, plan };
283
+ }
284
+ // AskUserQuestion: surface the real question + options (they're on `args`),
285
+ // not the generic "Asked you a question" that discarded them.
286
+ const question = structuredQuestionFromAsk(lastPlanOrAsk.args) ?? { text: 'Asked you a question', reason: 'question' };
287
+ return { ...base, activity: 'waiting_input', awaitingReason: 'question', preview: question.text, question };
205
288
  }
206
289
  // Pending tool call (tool_use with no following tool_result): mid-turn.
207
290
  if (last.type === 'tool_use') {
@@ -209,7 +292,7 @@ export function inferActivity(events, ctx = {}) {
209
292
  return { ...base, activity: 'working' };
210
293
  // Alive but the file hasn't moved — likely blocked on a permission prompt.
211
294
  if (ctx.pidAlive)
212
- return { ...base, activity: 'waiting_input', awaitingReason: 'permission' };
295
+ return { ...base, activity: 'waiting_input', awaitingReason: 'permission', question: permissionQuestion(base.preview) };
213
296
  return { ...base, activity: 'idle' };
214
297
  }
215
298
  // Thinking or a tool result just landed → agent is mid-turn if recently active.
@@ -223,8 +306,14 @@ export function inferActivity(events, ctx = {}) {
223
306
  return { ...base, activity: canWork ? 'working' : 'idle' };
224
307
  }
225
308
  // Assistant spoke last and stopped. A trailing question → waiting; else idle.
226
- if (looksLikeQuestion(last.content ?? '')) {
227
- return { ...base, activity: 'waiting_input', awaitingReason: 'question' };
309
+ // A prose question takes a free-text reply (no select-list), so no options/keys.
310
+ // Unlike the structural plan/ask signals above, the prose heuristic DECAYS: a
311
+ // question nobody answered within PROSE_QUESTION_FRESH_MS is a session that
312
+ // ended, not one that needs you (RUSH-1522). Unknown mtime keeps the question.
313
+ const questionFresh = ctx.mtimeMs == null || Date.now() - ctx.mtimeMs < PROSE_QUESTION_FRESH_MS;
314
+ if (questionFresh && looksLikeQuestion(last.content ?? '')) {
315
+ const text = oneLine(last.content ?? '');
316
+ return { ...base, activity: 'waiting_input', awaitingReason: 'question', question: { text, reason: 'question' } };
228
317
  }
229
318
  return { ...base, activity: 'idle' };
230
319
  }
@@ -97,6 +97,15 @@ export interface SessionMeta {
97
97
  * `isTeamOrigin` (which marks sessions spawned BY a team).
98
98
  */
99
99
  spawnedTeam?: string;
100
+ /**
101
+ * The plan markdown from the LAST `ExitPlanMode` tool call in the transcript
102
+ * (Claude sessions only), captured at scan time. Present whenever the session
103
+ * ever entered plan-review; consumers can pair it with a live
104
+ * `awaitingReason === 'plan_review'` to decide whether it is still pending.
105
+ * Fills the gap that forced the Factory extension to re-read raw JSONL to
106
+ * recover the plan text — the CLI now carries it on the metadata row.
107
+ */
108
+ plan?: string;
100
109
  /**
101
110
  * True when the session was spawned programmatically (SDK entrypoint) rather
102
111
  * than by a human at the Claude CLI. Captured at scan time from the JSONL
@@ -77,7 +77,7 @@ export interface ConflictInfo {
77
77
  * top-level entry add/remove — deep edits to plugin contents won't
78
78
  * trigger auto-resync, run `agents sync` for that.
79
79
  */
80
- export declare const SHIM_SCHEMA_VERSION = 25;
80
+ export declare const SHIM_SCHEMA_VERSION = 26;
81
81
  /**
82
82
  * Generate the full bash shim script for the given agent. The returned string
83
83
  * is written to ~/.agents/shims/{cliCommand} and made executable.
@@ -145,8 +145,14 @@ export declare function removeShim(agent: AgentId): boolean;
145
145
  * editor opens the script and the agent never launches. The `.cmd` is
146
146
  * now the only Windows artifact and carries this alias marker so
147
147
  * staleness checks read it directly.
148
- */
149
- export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 12;
148
+ * v13 — grok aliases resolve the binary from the versioned home's
149
+ * .grok/downloads first, then fall back to the global ~/.grok/downloads.
150
+ * The old template checked only the global dir, so a `grok@<version>`
151
+ * alias failed with "not installed" whenever the binary was staged into
152
+ * the versioned home (installer run with GROK_HOME set, or grok
153
+ * self-update under the shim).
154
+ */
155
+ export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 13;
150
156
  /**
151
157
  * Generate a versioned alias script that directly execs a specific version.
152
158
  * e.g., claude@2.0.65 -> directly runs that version's binary
package/dist/lib/shims.js CHANGED
@@ -214,7 +214,14 @@ async function promptConflictStrategy(conflictInfos) {
214
214
  // v25 — dispatcher self-recovery: if the baked AGENTS_BIN is gone (a removed/moved
215
215
  // dev build that generated the shim), resolve `agents` on PATH instead of
216
216
  // exiting 127, so a stale/vanished dev build can't brick every launch.
217
- export const SHIM_SCHEMA_VERSION = 25;
217
+ // v26 grok resolves its binary from the versioned home's .grok/downloads first
218
+ // ($VERSION_DIR/home/.grok/downloads, where the binary lands when the
219
+ // installer runs with GROK_HOME set or grok self-updates under the shim),
220
+ // then falls back to the global ~/.grok/downloads for pre-fix installs.
221
+ // The old dispatcher checked only the global dir, so a pinned grok that
222
+ // installed into the versioned home fell through to the "not installed"
223
+ // error.
224
+ export const SHIM_SCHEMA_VERSION = 26;
218
225
  /** Internal marker string used to embed the schema version in shim scripts. */
219
226
  const SHIM_VERSION_MARKER = 'agents-shim-version:';
220
227
  function shellQuote(value) {
@@ -468,11 +475,14 @@ fi
468
475
 
469
476
  VERSION_DIR="$AGENTS_USER_DIR/.history/versions/$AGENT/$VERSION"
470
477
 
471
- # Grok special case: binary lives in ~/.grok/downloads/, not node_modules.
472
- # We still use the agents-cli version dir purely for GROK_HOME isolation.
478
+ # Grok special case: binary lives in the versioned home's .grok/downloads (or,
479
+ # for pre-fix installs, the global ~/.grok/downloads), not node_modules. We
480
+ # still use the agents-cli version dir purely for GROK_HOME isolation.
473
481
  if [ "$AGENT" = "grok" ]; then
474
- # Try to find a matching binary for the pinned version in the global grok downloads dir.
475
- GROK_DOWNLOADS="$HOME/.grok/downloads"
482
+ # Check the versioned home first this is where the binary lands when the
483
+ # installer runs with GROK_HOME set (i.e. via the shim or a correct
484
+ # \`agents add grok\`), or when grok self-updates from within the shim.
485
+ GROK_DOWNLOADS="$VERSION_DIR/home/.grok/downloads"
476
486
  if [ -d "$GROK_DOWNLOADS" ]; then
477
487
  # Prefer a binary whose filename contains the exact version
478
488
  BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | grep -i "$VERSION" | head -1)
@@ -481,6 +491,17 @@ if [ "$AGENT" = "grok" ]; then
481
491
  BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | head -1)
482
492
  fi
483
493
  fi
494
+ # Fall back to the global grok home (binary installed without GROK_HOME set,
495
+ # e.g. an earlier \`agents add grok@latest\` before this resolution fix).
496
+ if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
497
+ GROK_DOWNLOADS="$HOME/.grok/downloads"
498
+ if [ -d "$GROK_DOWNLOADS" ]; then
499
+ BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | grep -i "$VERSION" | head -1)
500
+ if [ -z "$BINARY" ]; then
501
+ BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | head -1)
502
+ fi
503
+ fi
504
+ fi
484
505
  if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
485
506
  # Last resort: the adopted native launcher (recorded absolute path) if we
486
507
  # adopted grok, else whatever is on PATH. Prefer the adopted record — after
@@ -744,8 +765,14 @@ export function removeShim(agent) {
744
765
  * editor opens the script and the agent never launches. The `.cmd` is
745
766
  * now the only Windows artifact and carries this alias marker so
746
767
  * staleness checks read it directly.
768
+ * v13 — grok aliases resolve the binary from the versioned home's
769
+ * .grok/downloads first, then fall back to the global ~/.grok/downloads.
770
+ * The old template checked only the global dir, so a `grok@<version>`
771
+ * alias failed with "not installed" whenever the binary was staged into
772
+ * the versioned home (installer run with GROK_HOME set, or grok
773
+ * self-update under the shim).
747
774
  */
748
- export const VERSIONED_ALIAS_SCHEMA_VERSION = 12;
775
+ export const VERSIONED_ALIAS_SCHEMA_VERSION = 13;
749
776
  /** Internal marker string used to embed the schema version in versioned alias scripts. */
750
777
  const VERSIONED_ALIAS_VERSION_MARKER = 'agents-versioned-alias-version:';
751
778
  // The version string is interpolated into a generated bash script and into
@@ -822,13 +849,22 @@ export KIMI_CODE_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/
822
849
  // "agents __shim" which resolves via getBinaryPath() instead.
823
850
  const versionDir = `$HOME/.agents/.history/versions/${agent}/${version}`;
824
851
  const binaryResolution = agent === 'grok'
825
- ? `# Grok ships its native binary in ~/.grok/downloads, not node_modules.
826
- GROK_DOWNLOADS="$HOME/.grok/downloads"
852
+ ? `# Grok ships its native binary in the versioned home's .grok/downloads (or,
853
+ # for pre-fix installs, the global ~/.grok/downloads), not node_modules.
854
+ GROK_DOWNLOADS="${versionDir}/home/.grok/downloads"
827
855
  BINARY=""
828
856
  if [ -d "$GROK_DOWNLOADS" ]; then
829
857
  BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | grep -i "${version}" | head -1)
830
858
  [ -n "$BINARY" ] || BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | head -1)
831
859
  fi
860
+ # Fall back to the global grok home (binary installed without GROK_HOME set).
861
+ if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
862
+ GROK_GLOBAL_DOWNLOADS="$HOME/.grok/downloads"
863
+ if [ -d "$GROK_GLOBAL_DOWNLOADS" ]; then
864
+ BINARY=$(ls "$GROK_GLOBAL_DOWNLOADS"/grok-* 2>/dev/null | grep -i "${version}" | head -1)
865
+ [ -n "$BINARY" ] || BINARY=$(ls "$GROK_GLOBAL_DOWNLOADS"/grok-* 2>/dev/null | head -1)
866
+ fi
867
+ fi
832
868
  # Refuse a PATH match under our own shims dir — it resolves to this alias's
833
869
  # sibling dispatcher shim (shims dir is ahead of ~/.local/bin on PATH) and
834
870
  # re-execs forever. Fall through to the clean "not installed" error instead.
@@ -50,10 +50,41 @@ export declare const REMOTE_TASK_NAME = "AgentsComputerHelper";
50
50
  /** Basename of the cross-published exe under native/computer-win/dist. */
51
51
  export declare const WIN_HELPER_EXE = "computer-helper-win.exe";
52
52
  /**
53
- * Locate the cross-published Windows daemon exe. Only the local build output is
54
- * a candidate — `scripts/build-win.sh` writes it to packages/.../dist/.
53
+ * Locate a locally built Windows daemon exe a repo-checkout build output from
54
+ * `scripts/build-win.sh`, or one bundled next to the package. Local paths are
55
+ * only the first candidate; npm-installed CLIs have neither and fall through to
56
+ * the release-asset download (`ensureWinHelperExe`).
55
57
  */
56
58
  export declare function resolveWinHelperExe(): string | null;
59
+ /** GitHub repo whose `v<version>` releases carry the exe as an asset. */
60
+ export declare const WIN_HELPER_RELEASE_REPO = "phnx-labs/agents-cli";
61
+ /** Cache dir for downloaded helper exes, one subdir per release tag. */
62
+ export declare function winHelperCacheDir(version: string): string;
63
+ /** Release-asset URLs for the exe + its checksum at one exact `v<version>` tag. */
64
+ export declare function winHelperAssetUrls(version: string): {
65
+ exe: string;
66
+ sha256: string;
67
+ };
68
+ /**
69
+ * Parse the published `.sha256` asset — `sha256sum` format (`<hex> <name>`) or
70
+ * a bare hex digest. Throws on anything that does not lead with 64 hex chars.
71
+ */
72
+ export declare function parseSha256Asset(text: string): string;
73
+ /** Stream a file through sha256 — the exe is ~157MB, never read it whole. */
74
+ export declare function sha256File(file: string): Promise<string>;
75
+ /**
76
+ * Download the exe release asset for this CLI version, verify its sha256
77
+ * against the published `.sha256` asset, and cache it under the agents cache
78
+ * dir. Only the exact `v<version>` tag is consulted — a missing asset is a
79
+ * hard error naming that tag, never a silent fallback to another release.
80
+ */
81
+ export declare function downloadWinHelperExe(version: string): Promise<string>;
82
+ /**
83
+ * Resolve the helper exe for `setup --host`: local build outputs first (repo
84
+ * checkout / bundled), then the checksum-verified release-asset download for
85
+ * the running CLI version. Throws with the tag it checked when neither exists.
86
+ */
87
+ export declare function ensureWinHelperExe(version?: string): Promise<string>;
57
88
  /** Persisted per-device tunnel state so verbs can reconnect after `start --host`. */
58
89
  export interface RemoteTunnelState {
59
90
  device: string;
@@ -21,7 +21,9 @@ import * as net from 'net';
21
21
  import * as fs from 'fs';
22
22
  import * as path from 'path';
23
23
  import { fileURLToPath } from 'url';
24
- import { randomBytes } from 'crypto';
24
+ import { randomBytes, createHash } from 'crypto';
25
+ import { Readable } from 'stream';
26
+ import { pipeline } from 'stream/promises';
25
27
  import { sshExec, SSH_OPTS } from './ssh-exec.js';
26
28
  import { backgroundSpawnOptions } from './platform/process.js';
27
29
  import { encodePowerShell } from './browser/drivers/ssh.js';
@@ -29,6 +31,7 @@ import { getDevice } from './devices/registry.js';
29
31
  import { sshTargetFor } from './devices/connect.js';
30
32
  import { hostNameFor } from './devices/ssh-config.js';
31
33
  import { getCacheDir } from './state.js';
34
+ import { getCliVersion } from './version.js';
32
35
  import { openComputerClient, resolveTcpEndpoint } from './computer-rpc.js';
33
36
  /** Build the ssh argv (after the `ssh` program name) for an `-L` tunnel. Pure.
34
37
  *
@@ -89,8 +92,10 @@ export const REMOTE_TASK_NAME = 'AgentsComputerHelper';
89
92
  /** Basename of the cross-published exe under native/computer-win/dist. */
90
93
  export const WIN_HELPER_EXE = 'computer-helper-win.exe';
91
94
  /**
92
- * Locate the cross-published Windows daemon exe. Only the local build output is
93
- * a candidate — `scripts/build-win.sh` writes it to packages/.../dist/.
95
+ * Locate a locally built Windows daemon exe a repo-checkout build output from
96
+ * `scripts/build-win.sh`, or one bundled next to the package. Local paths are
97
+ * only the first candidate; npm-installed CLIs have neither and fall through to
98
+ * the release-asset download (`ensureWinHelperExe`).
94
99
  */
95
100
  export function resolveWinHelperExe() {
96
101
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -106,6 +111,89 @@ export function resolveWinHelperExe() {
106
111
  }
107
112
  return null;
108
113
  }
114
+ /** GitHub repo whose `v<version>` releases carry the exe as an asset. */
115
+ export const WIN_HELPER_RELEASE_REPO = 'phnx-labs/agents-cli';
116
+ /** Cache dir for downloaded helper exes, one subdir per release tag. */
117
+ export function winHelperCacheDir(version) {
118
+ return path.join(getCacheDir(), 'computer', 'win-helper', `v${version}`);
119
+ }
120
+ /** Release-asset URLs for the exe + its checksum at one exact `v<version>` tag. */
121
+ export function winHelperAssetUrls(version) {
122
+ const base = `https://github.com/${WIN_HELPER_RELEASE_REPO}/releases/download/v${version}`;
123
+ return { exe: `${base}/${WIN_HELPER_EXE}`, sha256: `${base}/${WIN_HELPER_EXE}.sha256` };
124
+ }
125
+ /**
126
+ * Parse the published `.sha256` asset — `sha256sum` format (`<hex> <name>`) or
127
+ * a bare hex digest. Throws on anything that does not lead with 64 hex chars.
128
+ */
129
+ export function parseSha256Asset(text) {
130
+ const m = text.trim().match(/^([A-Fa-f0-9]{64})(\s|$)/);
131
+ if (!m)
132
+ throw new Error(`malformed .sha256 release asset: ${JSON.stringify(text.slice(0, 80))}`);
133
+ return m[1].toLowerCase();
134
+ }
135
+ /** Stream a file through sha256 — the exe is ~157MB, never read it whole. */
136
+ export function sha256File(file) {
137
+ return new Promise((resolve, reject) => {
138
+ const hash = createHash('sha256');
139
+ fs.createReadStream(file)
140
+ .on('error', reject)
141
+ .on('data', (d) => hash.update(d))
142
+ .on('end', () => resolve(hash.digest('hex')));
143
+ });
144
+ }
145
+ /**
146
+ * Download the exe release asset for this CLI version, verify its sha256
147
+ * against the published `.sha256` asset, and cache it under the agents cache
148
+ * dir. Only the exact `v<version>` tag is consulted — a missing asset is a
149
+ * hard error naming that tag, never a silent fallback to another release.
150
+ */
151
+ export async function downloadWinHelperExe(version) {
152
+ const cached = path.join(winHelperCacheDir(version), WIN_HELPER_EXE);
153
+ if (fs.existsSync(cached))
154
+ return cached;
155
+ const tag = `v${version}`;
156
+ const { exe: exeUrl, sha256: shaUrl } = winHelperAssetUrls(version);
157
+ const missing = (status, url) => new Error(`no ${WIN_HELPER_EXE} release asset for tag ${tag} (HTTP ${status} on ${url}). ` +
158
+ `The Windows helper ships as a GitHub release asset per tagged CLI version; ` +
159
+ `from a repo checkout you can build it locally instead: bash scripts/build-win.sh`);
160
+ // Checksum first: it is tiny and 404s fast when the tag has no assets.
161
+ const shaRes = await fetch(shaUrl, { signal: AbortSignal.timeout(30_000) });
162
+ if (!shaRes.ok)
163
+ throw missing(shaRes.status, shaUrl);
164
+ const expected = parseSha256Asset(await shaRes.text());
165
+ console.error(`Downloading ${WIN_HELPER_EXE} ${tag} from GitHub releases (~160 MB)...`);
166
+ const exeRes = await fetch(exeUrl, { signal: AbortSignal.timeout(15 * 60_000) });
167
+ if (!exeRes.ok || !exeRes.body)
168
+ throw missing(exeRes.status, exeUrl);
169
+ fs.mkdirSync(path.dirname(cached), { recursive: true });
170
+ // Stream to a partial file and rename only after the checksum passes, so an
171
+ // interrupted download can never be picked up as a valid cache hit.
172
+ const partial = `${cached}.download`;
173
+ try {
174
+ await pipeline(Readable.fromWeb(exeRes.body), fs.createWriteStream(partial));
175
+ const actual = await sha256File(partial);
176
+ if (actual !== expected) {
177
+ throw new Error(`sha256 mismatch for ${exeUrl}: expected ${expected}, got ${actual}`);
178
+ }
179
+ fs.renameSync(partial, cached);
180
+ }
181
+ finally {
182
+ fs.rmSync(partial, { force: true });
183
+ }
184
+ return cached;
185
+ }
186
+ /**
187
+ * Resolve the helper exe for `setup --host`: local build outputs first (repo
188
+ * checkout / bundled), then the checksum-verified release-asset download for
189
+ * the running CLI version. Throws with the tag it checked when neither exists.
190
+ */
191
+ export async function ensureWinHelperExe(version = getCliVersion()) {
192
+ const local = resolveWinHelperExe();
193
+ if (local)
194
+ return local;
195
+ return downloadWinHelperExe(version);
196
+ }
109
197
  function remoteStateDir() {
110
198
  return path.join(getCacheDir(), 'computer', 'remote');
111
199
  }
@@ -249,10 +337,9 @@ function copyFileOverScp(target, remotePath, filePath, timeoutMs = 600_000) {
249
337
  */
250
338
  export async function setupRemoteHelper(name) {
251
339
  const { target } = await resolveRemoteDevice(name);
252
- const exe = resolveWinHelperExe();
253
- if (!exe) {
254
- throw new Error(`Windows helper exe not built. Run: bash scripts/build-win.sh`);
255
- }
340
+ // Local build output, else the checksum-verified GitHub release asset for
341
+ // this CLI version. Throws naming the tag it checked when neither exists.
342
+ const exe = await ensureWinHelperExe();
256
343
  const prep = sshExec(target, encodePowerShell(buildPushScript()), { timeoutMs: 60_000 });
257
344
  if (prep.code !== 0) {
258
345
  throw new Error(`preparing helper exe path on '${name}' failed (exit ${prep.code ?? 'null'}): ${prep.stderr.trim() || prep.stdout.trim()}`);
@@ -53,4 +53,3 @@ export interface SyncManifest {
53
53
  workflows?: Record<string, DirEntry>;
54
54
  plugins?: Record<string, PluginEntry>;
55
55
  }
56
- export type ResourceType = 'commands' | 'skills' | 'hooks' | 'mcp' | 'rules' | 'subagents' | 'workflows' | 'plugins' | 'permissions';
@@ -83,6 +83,7 @@ export declare const loadSessions: ModuleLoader;
83
83
  export declare const loadTeams: ModuleLoader;
84
84
  export declare const loadCloud: ModuleLoader;
85
85
  export declare const loadMessage: ModuleLoader;
86
+ export declare const loadFeed: ModuleLoader;
86
87
  export declare const loadServe: ModuleLoader;
87
88
  export declare const loadAudit: ModuleLoader;
88
89
  /**
@@ -61,6 +61,7 @@ export const loadSessions = async () => (await import('../../commands/sessions.j
61
61
  export const loadTeams = async () => (await import('../../commands/teams.js')).registerTeamsCommands;
62
62
  export const loadCloud = async () => (await import('../../commands/cloud.js')).registerCloudCommands;
63
63
  export const loadMessage = async () => (await import('../../commands/message.js')).registerMessageCommand;
64
+ export const loadFeed = async () => (await import('../../commands/feed.js')).registerFeedCommand;
64
65
  export const loadServe = async () => (await import('../../commands/serve.js')).registerServeCommand;
65
66
  export const loadAudit = async () => (await import('../../commands/audit.js')).registerAuditCommands;
66
67
  /**
@@ -154,6 +155,7 @@ export const COMMAND_LOADERS = {
154
155
  teams: [loadTeams],
155
156
  cloud: [loadCloud],
156
157
  message: [loadMessage],
158
+ feed: [loadFeed],
157
159
  serve: [loadServe],
158
160
  audit: [loadAudit],
159
161
  };
@@ -122,6 +122,8 @@ export declare function getProjectRoutinesDir(cwd?: string): string | null;
122
122
  export declare function getRunsDir(): string;
123
123
  /** Root for per-agent mailboxes (~/.agents/.history/mailbox/). */
124
124
  export declare function getMailboxRootDir(): string;
125
+ /** Root for open-block feed records (~/.agents/.history/feed/). */
126
+ export declare function getFeedDir(): string;
125
127
  /** Path to installed agent CLI binaries (~/.agents/.history/versions/). */
126
128
  export declare function getVersionsDir(): string;
127
129
  /** Path to version-switching shim scripts (~/.agents/.cache/shims/). */
package/dist/lib/state.js CHANGED
@@ -89,6 +89,7 @@ const TEAMS_AGENTS_DIR = path.join(HISTORY_DIR, 'teams', 'agents');
89
89
  const BACKUPS_DIR = path.join(HISTORY_DIR, 'backups');
90
90
  const TRASH_DIR = path.join(HISTORY_DIR, 'trash');
91
91
  const MAILBOX_DIR = path.join(HISTORY_DIR, 'mailbox');
92
+ const FEED_DIR = path.join(HISTORY_DIR, 'feed');
92
93
  // Cache bucket (regenerable).
93
94
  const SHIMS_DIR = path.join(CACHE_DIR, 'shims');
94
95
  const HOOK_SHIMS_DIR = path.join(SHIMS_DIR, 'hooks');
@@ -313,6 +314,8 @@ export function getProjectRoutinesDir(cwd = process.cwd()) {
313
314
  export function getRunsDir() { return RUNS_DIR; }
314
315
  /** Root for per-agent mailboxes (~/.agents/.history/mailbox/). */
315
316
  export function getMailboxRootDir() { return MAILBOX_DIR; }
317
+ /** Root for open-block feed records (~/.agents/.history/feed/). */
318
+ export function getFeedDir() { return FEED_DIR; }
316
319
  /** Path to installed agent CLI binaries (~/.agents/.history/versions/). */
317
320
  export function getVersionsDir() { return VERSIONS_DIR; }
318
321
  /** Path to version-switching shim scripts (~/.agents/.cache/shims/). */
@@ -569,25 +572,35 @@ function writeIfChanged(filePath, content) {
569
572
  /**
570
573
  * Partition the in-memory Meta across three files by sync-domain:
571
574
  * - central `~/.agents/agents.yaml` — portable, everything else
572
- * - device `~/.agents/devices/<machine>/agents.yaml` — `agents:` pins (per-device)
575
+ * - device `~/.agents/devices/<machine>/agents.yaml` — `agents:` pins +
576
+ * `defaultBrowserProfile:` (both per-device)
573
577
  * - history `~/.agents/.history/version-resources.json` — `versions:` (machine-local)
574
578
  * All callers funnel through writeMeta → here, so nothing else changes. Empty
575
579
  * `agents:` / `versions:` are not written (no empty committed files).
576
580
  */
577
581
  function writeMetaUnlocked(meta) {
578
- const { agents, versions, ...central } = meta;
582
+ const { agents, versions, defaultBrowserProfile, ...central } = meta;
579
583
  // Write the machine-local files FIRST, then strip central — so a crash mid-write
580
584
  // never removes pins/versions from central before they're persisted elsewhere.
581
585
  const devicePath = getDeviceMetaPath();
582
- if (agents && Object.keys(agents).length > 0) {
586
+ const hasAgents = !!agents && Object.keys(agents).length > 0;
587
+ const hasDefaultBrowser = !!defaultBrowserProfile;
588
+ if (hasAgents || hasDefaultBrowser) {
589
+ // Device-local doc carries `agents:` pins and `defaultBrowserProfile:` — both
590
+ // are per-machine and must never land in central agents.yaml (which syncs).
591
+ const deviceDoc = {};
592
+ if (hasAgents)
593
+ deviceDoc.agents = agents;
594
+ if (hasDefaultBrowser)
595
+ deviceDoc.defaultBrowserProfile = defaultBrowserProfile;
583
596
  fs.mkdirSync(path.dirname(devicePath), { recursive: true });
584
- writeIfChanged(devicePath, META_HEADER + yaml.stringify({ agents }));
597
+ writeIfChanged(devicePath, META_HEADER + yaml.stringify(deviceDoc));
585
598
  }
586
599
  else if (fs.existsSync(devicePath)) {
587
- // Every pin was cleared. Persist the emptied map instead of skipping the
588
- // write — otherwise the stale device file survives and overlayMachineLocal
589
- // re-applies the removed pin on the next read, leaving a dangling default
590
- // (e.g. a launcher pointing at a version that was just uninstalled).
600
+ // Every device-local value was cleared. Persist the emptied doc instead of
601
+ // skipping the write — otherwise the stale device file survives and
602
+ // overlayMachineLocal re-applies the removed pin/default on the next read,
603
+ // leaving a dangling value (e.g. a default pointing at a deleted profile).
591
604
  writeIfChanged(devicePath, META_HEADER + yaml.stringify({ agents: {} }));
592
605
  }
593
606
  if (versions && Object.keys(versions).length > 0) {
@@ -602,6 +615,8 @@ function writeMetaUnlocked(meta) {
602
615
  * Overlay this machine's local state onto a central-portable Meta:
603
616
  * - `agents:` from the device file (device wins; the union both preserves the
604
617
  * one-level merge and self-heals a pre-migration central that still has pins)
618
+ * - `defaultBrowserProfile:` from the device file (device is the sole source;
619
+ * the field is stripped from central on write, so nothing to merge against)
605
620
  * - `versions:` from the history JSON (wholesale replace; falls back to
606
621
  * whatever central carried when the history file doesn't exist yet)
607
622
  */
@@ -612,6 +627,8 @@ function overlayMachineLocal(meta) {
612
627
  const dm = yaml.parse(fs.readFileSync(devicePath, 'utf-8'));
613
628
  if (dm?.agents)
614
629
  meta.agents = { ...meta.agents, ...dm.agents };
630
+ if (dm?.defaultBrowserProfile)
631
+ meta.defaultBrowserProfile = dm.defaultBrowserProfile;
615
632
  }
616
633
  catch { /* ignore malformed device file */ }
617
634
  }
@@ -1693,6 +1693,7 @@ export class AgentManager {
1693
1693
  ];
1694
1694
  if (model)
1695
1695
  args.push('--model', model);
1696
+ args.push('--env', 'AGENTS_RUNTIME=teams');
1696
1697
  return args;
1697
1698
  }
1698
1699
  buildCommand(agentType, prompt, mode, model, cwd = null, sessionId = null, effort = 'medium', version = null, profileName = null) {
@@ -1704,9 +1705,11 @@ export class AgentManager {
1704
1705
  ];
1705
1706
  if (cwd)
1706
1707
  cmd.push('--cwd', cwd);
1707
- // Pin Claude's session UUID to our agent_id so its session file lands at
1708
- // ~/.claude/projects/.../<agent_id>.jsonl unified identity for status polling.
1709
- if (agentType === 'claude' && sessionId) {
1708
+ // Pin the session UUID to our agent_id so buildExecEnv keys
1709
+ // AGENTS_MAILBOX_DIR by the same id mailboxIdForActiveSession returns.
1710
+ // Claude also forwards --session-id to its CLI (unified identity);
1711
+ // other agents ignore the flag but still get the correct mailbox dir.
1712
+ if (sessionId) {
1710
1713
  cmd.push('--session-id', sessionId);
1711
1714
  }
1712
1715
  // Claude: grant access to the teammate's working directory.