@phnx-labs/agents-cli 1.22.52 → 1.22.54

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 (180) hide show
  1. package/CHANGELOG.md +336 -0
  2. package/README.md +42 -9
  3. package/dist/bootstrap.js +55 -154
  4. package/dist/cli/command-registry.d.ts +5 -0
  5. package/dist/cli/command-registry.js +8 -1
  6. package/dist/commands/accounts.js +220 -174
  7. package/dist/commands/apply.js +6 -3
  8. package/dist/commands/auth-mint.d.ts +8 -0
  9. package/dist/commands/auth-mint.js +96 -0
  10. package/dist/commands/auth.js +5 -1
  11. package/dist/commands/browser.js +1 -1
  12. package/dist/commands/cost.js +8 -2
  13. package/dist/commands/daemon.js +2 -2
  14. package/dist/commands/doctor.js +6 -1
  15. package/dist/commands/exec.js +26 -17
  16. package/dist/commands/fleet-capture.js +7 -0
  17. package/dist/commands/focus.d.ts +1 -0
  18. package/dist/commands/focus.js +4 -2
  19. package/dist/commands/go.d.ts +5 -4
  20. package/dist/commands/go.js +8 -7
  21. package/dist/commands/insights.js +9 -0
  22. package/dist/commands/monitors.js +85 -30
  23. package/dist/commands/output.js +8 -2
  24. package/dist/commands/repo.js +18 -0
  25. package/dist/commands/secrets.js +33 -14
  26. package/dist/commands/sessions-inject.js +8 -3
  27. package/dist/commands/sessions-picker.js +2 -1
  28. package/dist/commands/sessions.d.ts +20 -12
  29. package/dist/commands/sessions.js +94 -40
  30. package/dist/commands/setup-accounts.d.ts +8 -0
  31. package/dist/commands/setup-accounts.js +47 -0
  32. package/dist/commands/setup.d.ts +1 -1
  33. package/dist/commands/setup.js +11 -2
  34. package/dist/commands/share.d.ts +52 -3
  35. package/dist/commands/share.js +262 -18
  36. package/dist/commands/ssh.d.ts +7 -0
  37. package/dist/commands/ssh.js +53 -14
  38. package/dist/commands/status.js +14 -0
  39. package/dist/commands/sync.js +44 -0
  40. package/dist/commands/view.d.ts +3 -1
  41. package/dist/commands/view.js +5 -4
  42. package/dist/lib/account-registry.d.ts +15 -5
  43. package/dist/lib/account-registry.js +165 -53
  44. package/dist/lib/accounting/rotate.d.ts +20 -6
  45. package/dist/lib/accounting/rotate.js +38 -7
  46. package/dist/lib/accounting/usage.d.ts +37 -1
  47. package/dist/lib/accounting/usage.js +71 -6
  48. package/dist/lib/agent-spec/agents.d.ts +5 -2
  49. package/dist/lib/agent-spec/agents.js +25 -7
  50. package/dist/lib/analytics/mix-commands.js +12 -6
  51. package/dist/lib/answer-router.js +2 -1
  52. package/dist/lib/auth-mint.d.ts +150 -0
  53. package/dist/lib/auth-mint.js +434 -0
  54. package/dist/lib/browser/profiles.d.ts +18 -0
  55. package/dist/lib/browser/profiles.js +26 -1
  56. package/dist/lib/browser/registry.d.ts +44 -14
  57. package/dist/lib/browser/registry.js +141 -45
  58. package/dist/lib/browser/remote-control.d.ts +9 -7
  59. package/dist/lib/browser/remote-control.js +9 -7
  60. package/dist/lib/claude-account-token.d.ts +10 -0
  61. package/dist/lib/claude-account-token.js +14 -4
  62. package/dist/lib/config-drift.d.ts +37 -0
  63. package/dist/lib/config-drift.js +72 -0
  64. package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
  65. package/dist/lib/daemon/auth-sync-service.js +34 -0
  66. package/dist/lib/daemon/daemon.js +30 -4
  67. package/dist/lib/daemon/runner.js +10 -2
  68. package/dist/lib/daemon-services.d.ts +1 -1
  69. package/dist/lib/daemon-services.js +5 -0
  70. package/dist/lib/device-config.d.ts +3 -3
  71. package/dist/lib/device-config.js +8 -7
  72. package/dist/lib/devices/config-migration.js +147 -1
  73. package/dist/lib/devices/connect.d.ts +26 -0
  74. package/dist/lib/devices/connect.js +48 -1
  75. package/dist/lib/devices/device-docs.d.ts +35 -0
  76. package/dist/lib/devices/device-docs.js +163 -0
  77. package/dist/lib/devices/discovery-policy.d.ts +14 -2
  78. package/dist/lib/devices/discovery-policy.js +31 -21
  79. package/dist/lib/devices/doctor-findings.d.ts +5 -1
  80. package/dist/lib/devices/doctor-findings.js +19 -1
  81. package/dist/lib/devices/registry.d.ts +11 -5
  82. package/dist/lib/devices/registry.js +46 -18
  83. package/dist/lib/exec.d.ts +88 -30
  84. package/dist/lib/exec.js +138 -34
  85. package/dist/lib/feed/feed.d.ts +10 -2
  86. package/dist/lib/feed/feed.js +35 -2
  87. package/dist/lib/feed-broadcast.js +1 -1
  88. package/dist/lib/fleet/apply.d.ts +11 -0
  89. package/dist/lib/fleet/apply.js +23 -3
  90. package/dist/lib/fleet/auth-sync.js +5 -3
  91. package/dist/lib/help.d.ts +9 -0
  92. package/dist/lib/help.js +29 -1
  93. package/dist/lib/hosts/dispatch.d.ts +4 -3
  94. package/dist/lib/hosts/dispatch.js +12 -8
  95. package/dist/lib/hosts/passthrough.d.ts +1 -10
  96. package/dist/lib/hosts/passthrough.js +1 -13
  97. package/dist/lib/hosts/providers/local.d.ts +9 -3
  98. package/dist/lib/hosts/providers/local.js +23 -12
  99. package/dist/lib/hosts/reconnect.d.ts +7 -4
  100. package/dist/lib/hosts/reconnect.js +29 -25
  101. package/dist/lib/hosts/registry.js +4 -1
  102. package/dist/lib/hosts/remote-os.js +3 -1
  103. package/dist/lib/installations/versions.js +9 -1
  104. package/dist/lib/linux-userns.d.ts +58 -0
  105. package/dist/lib/linux-userns.js +116 -0
  106. package/dist/lib/memory.d.ts +26 -0
  107. package/dist/lib/memory.js +80 -1
  108. package/dist/lib/monitors/config.d.ts +11 -0
  109. package/dist/lib/monitors/config.js +8 -0
  110. package/dist/lib/monitors/engine.js +8 -1
  111. package/dist/lib/monitors/state.d.ts +37 -1
  112. package/dist/lib/monitors/state.js +79 -4
  113. package/dist/lib/permissions-registry.d.ts +2 -0
  114. package/dist/lib/permissions-registry.js +116 -14
  115. package/dist/lib/permissions.d.ts +5 -3
  116. package/dist/lib/permissions.js +25 -27
  117. package/dist/lib/profiles.d.ts +8 -7
  118. package/dist/lib/profiles.js +12 -0
  119. package/dist/lib/project-key.d.ts +9 -0
  120. package/dist/lib/project-key.js +11 -0
  121. package/dist/lib/secrets/bundles.d.ts +35 -0
  122. package/dist/lib/secrets/bundles.js +78 -1
  123. package/dist/lib/secrets/push.d.ts +3 -8
  124. package/dist/lib/secrets/push.js +18 -14
  125. package/dist/lib/secrets/remote.d.ts +9 -18
  126. package/dist/lib/secrets/remote.js +11 -26
  127. package/dist/lib/secrets/reserved-sync.d.ts +65 -0
  128. package/dist/lib/secrets/reserved-sync.js +129 -0
  129. package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
  130. package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
  131. package/dist/lib/self-heal/registry.js +4 -0
  132. package/dist/lib/self-heal/types.d.ts +1 -1
  133. package/dist/lib/session/active.d.ts +10 -1
  134. package/dist/lib/session/active.js +8 -5
  135. package/dist/lib/session/actor-sidecar.d.ts +7 -0
  136. package/dist/lib/session/actor-sidecar.js +2 -0
  137. package/dist/lib/session/db.d.ts +39 -4
  138. package/dist/lib/session/db.js +168 -31
  139. package/dist/lib/session/discover.d.ts +32 -4
  140. package/dist/lib/session/discover.js +126 -37
  141. package/dist/lib/session/insights.d.ts +14 -0
  142. package/dist/lib/session/insights.js +25 -2
  143. package/dist/lib/session/linear.js +1 -1
  144. package/dist/lib/session/live-metadata.js +1 -0
  145. package/dist/lib/session/pid-registry.d.ts +7 -0
  146. package/dist/lib/session/prompt.d.ts +15 -0
  147. package/dist/lib/session/prompt.js +21 -0
  148. package/dist/lib/session/shell-programs.d.ts +17 -0
  149. package/dist/lib/session/shell-programs.js +21 -0
  150. package/dist/lib/session/state.js +2 -1
  151. package/dist/lib/session/stream-render.js +2 -1
  152. package/dist/lib/session/tool-calls.js +2 -5
  153. package/dist/lib/session/trajectory-html.js +2 -1
  154. package/dist/lib/session/trajectory.js +3 -12
  155. package/dist/lib/session/types.d.ts +25 -0
  156. package/dist/lib/session/types.js +10 -0
  157. package/dist/lib/share/publish.d.ts +53 -5
  158. package/dist/lib/share/publish.js +99 -17
  159. package/dist/lib/share/worker-template.js +594 -64
  160. package/dist/lib/startup/root-command.js +2 -1
  161. package/dist/lib/state.d.ts +24 -0
  162. package/dist/lib/state.js +318 -54
  163. package/dist/lib/sync-status.d.ts +4 -0
  164. package/dist/lib/sync-status.js +3 -0
  165. package/dist/lib/terminal/resolve.d.ts +7 -0
  166. package/dist/lib/terminal/resolve.js +41 -2
  167. package/dist/lib/traces/classify.js +24 -19
  168. package/dist/lib/traces/insights.d.ts +67 -0
  169. package/dist/lib/traces/insights.js +178 -0
  170. package/dist/lib/traces/phenotype.d.ts +67 -0
  171. package/dist/lib/traces/phenotype.js +437 -0
  172. package/dist/lib/traces/segments.d.ts +133 -0
  173. package/dist/lib/traces/segments.js +301 -0
  174. package/dist/lib/traces/sync.d.ts +33 -0
  175. package/dist/lib/traces/sync.js +11 -2
  176. package/dist/lib/types.d.ts +47 -1
  177. package/dist/lib/usage-refresh.js +2 -1
  178. package/dist/lib/view-types.d.ts +2 -0
  179. package/dist/lib/watchdog/runner.js +18 -4
  180. package/package.json +2 -1
@@ -85,6 +85,20 @@ export interface InsightFacets {
85
85
  assistantTurns: number;
86
86
  toolCount: number;
87
87
  errorCount: number;
88
+ /**
89
+ * Shell invocations bucketed by the actual executable + first subcommand token, not
90
+ * lumped under the harness tool name. `bucketKey` collapses each command to
91
+ * `git commit`, `gh pr`, `agents ssh`, `find`, `ssh→git pull`, … so the tool mix says
92
+ * which binary ran, not just that `Bash` did. Cross-harness: keyed on `e.command`,
93
+ * which the codex parser sets for `exec_command` too, so it is not Claude-`Bash`-only.
94
+ */
95
+ bashCommands: Record<string, number>;
96
+ /**
97
+ * Same bucketing for the shell command that was running when a tool call failed —
98
+ * turns one `failed tool loop: Bash` line into per-binary failure counts
99
+ * (`git reconcile`, `find`, `gh`) so the friction is attributable to a command.
100
+ */
101
+ bashCommandFailures: Record<string, number>;
88
102
  /** Deterministic evidence buckets used by the actions-forward report. */
89
103
  frictionSignals: Record<string, number>;
90
104
  correctionSignals: Record<string, number>;
@@ -22,6 +22,7 @@
22
22
  */
23
23
  import { computeSummaryStats, shortenModel } from './render.js';
24
24
  import { classifyFileChanges, EDIT_TOOLS, WRITE_TOOLS } from './digest.js';
25
+ import { bucketKey } from './bash-command.js';
25
26
  /** File extension → language label. Mirrors the set `/insights` attributes by. */
26
27
  const LANGUAGE_BY_EXT = {
27
28
  '.ts': 'TypeScript', '.tsx': 'TypeScript', '.mts': 'TypeScript', '.cts': 'TypeScript',
@@ -82,6 +83,7 @@ function emptyFacets() {
82
83
  shellCommandsSeen: 0,
83
84
  messageHours: new Array(24).fill(0), userTurns: 0, assistantTurns: 0,
84
85
  toolCount: 0, errorCount: 0,
86
+ bashCommands: {}, bashCommandFailures: {},
85
87
  frictionSignals: {}, correctionSignals: {}, automationSignals: {},
86
88
  };
87
89
  }
@@ -172,6 +174,9 @@ export function computeInsightFacets(events, timezoneOffsetMinutes = new Date().
172
174
  /** Shortened model id of the last assistant activity (for stall attribution). */
173
175
  let lastAssistantModel = null;
174
176
  let lastFailedTool = null;
177
+ // Most recent shell command seen per tool, so an `error` event (which carries the
178
+ // failing tool name but not its command) can attribute the failure to a binary.
179
+ const lastCommandByTool = {};
175
180
  for (const e of events) {
176
181
  const ts = new Date(e.timestamp).getTime();
177
182
  const hasTs = !Number.isNaN(ts);
@@ -191,16 +196,27 @@ export function computeInsightFacets(events, timezoneOffsetMinutes = new Date().
191
196
  lastAssistantModel = m;
192
197
  }
193
198
  break;
194
- case 'error':
199
+ case 'error': {
195
200
  bump(f.errorCategories, categorizeError(e.content ?? e.output ?? ''));
196
201
  if (e.tool && e.tool === lastFailedTool)
197
202
  bump(f.frictionSignals, `failed tool loop: ${e.tool}`);
203
+ // Attribute the failure to the binary of that tool's most recent command,
204
+ // so `Bash` failures split into `git reconcile`/`find`/`gh` rather than one lump.
205
+ const failedCmd = e.tool ? lastCommandByTool[e.tool] : undefined;
206
+ if (failedCmd)
207
+ bump(f.bashCommandFailures, bucketKey(failedCmd));
198
208
  lastFailedTool = e.tool ?? null;
199
209
  classifyFriction(e.content ?? e.output ?? '', f.frictionSignals);
200
210
  break;
211
+ }
201
212
  case 'tool_result':
202
- if (e.success !== false && e.outcome !== 'error')
213
+ if (e.success !== false && e.outcome !== 'error') {
203
214
  lastFailedTool = null;
215
+ // Drop the succeeded tool's remembered command so a later failing call that
216
+ // carries no command of its own is not misattributed to this stale binary.
217
+ if (e.tool)
218
+ delete lastCommandByTool[e.tool];
219
+ }
204
220
  break;
205
221
  case 'message':
206
222
  if (e.role === 'assistant') {
@@ -277,6 +293,9 @@ export function computeInsightFacets(events, timezoneOffsetMinutes = new Date().
277
293
  f.gitCommits += countGitOp(e.command, 'commit');
278
294
  f.gitPushes += countGitOp(e.command, 'push');
279
295
  classifyAutomation(e.command, f.automationSignals);
296
+ bump(f.bashCommands, bucketKey(e.command));
297
+ if (toolName)
298
+ lastCommandByTool[toolName] = e.command;
280
299
  }
281
300
  break;
282
301
  }
@@ -430,6 +449,10 @@ export function mergeFacets(into, add) {
430
449
  bump(into.slashCommands, k, v);
431
450
  for (const [k, v] of Object.entries(add.errorCategories))
432
451
  bump(into.errorCategories, k, v);
452
+ for (const [k, v] of Object.entries(add.bashCommands ?? {}))
453
+ bump(into.bashCommands, k, v);
454
+ for (const [k, v] of Object.entries(add.bashCommandFailures ?? {}))
455
+ bump(into.bashCommandFailures, k, v);
433
456
  for (const [k, v] of Object.entries(add.frictionSignals ?? {}))
434
457
  bump(into.frictionSignals, k, v);
435
458
  for (const [k, v] of Object.entries(add.correctionSignals ?? {}))
@@ -9,7 +9,7 @@
9
9
  * 2. `workspaceUrlKey` in the linear-cli config (`~/.linear-cli/config.json`).
10
10
  *
11
11
  * When neither is set the ticket stays plain text — it's still shown, just not
12
- * linked. This keeps the OSS CLI free of any Rush/workspace-specific string.
12
+ * linked. This keeps the published CLI free of any Rush/workspace-specific string.
13
13
  */
14
14
  import fs from 'node:fs';
15
15
  import os from 'node:os';
@@ -37,6 +37,7 @@ export function activeSessionToSessionMeta(active, self, nowMs) {
37
37
  id,
38
38
  shortId: deriveShortId(id),
39
39
  agent: active.kind,
40
+ harness: active.harness,
40
41
  timestamp: new Date(startedMs).toISOString(),
41
42
  lastActivity: new Date(lastMs).toISOString(),
42
43
  // An empty file path is honest — a just-created session may have no
@@ -1,6 +1,13 @@
1
1
  export interface PidSessionEntry {
2
2
  pid: number;
3
3
  agent: string;
4
+ /**
5
+ * Custom harness / profile name when this pid was launched via
6
+ * `agents run <profile>` (e.g. `deepseek`). `agent` stays the HOST
7
+ * CLI (`claude`) so process matching and transcript discovery keep
8
+ * working. `sessions --active` displays this when set (PHNX-2935).
9
+ */
10
+ harness?: string;
4
11
  /** The launch session id. Present for agents launched with a known id (Claude). */
5
12
  sessionId?: string;
6
13
  cwd?: string;
@@ -69,5 +69,20 @@ export interface ClassifiedPrompt {
69
69
  export declare function classifyUserPrompt(raw: string | undefined, opts?: {
70
70
  hasImageAttachment?: boolean;
71
71
  }): ClassifiedPrompt;
72
+ /**
73
+ * Collapse a harness-generated session title when it is injected skill
74
+ * scaffolding; otherwise leave the title as the harness wrote it.
75
+ *
76
+ * Claude's `ai-title` and Cursor's `chatMeta.title` are both derived from the
77
+ * first turn, so a session opened with a skill gets named after the injected
78
+ * "Base directory for this skill: …" line. {@link classifyUserPrompt} reports
79
+ * `kind: 'skill'` only for that line, so collapse it to `/<skill>`. Empty or
80
+ * whitespace-only input yields `undefined` so the caller falls through to the
81
+ * first-prompt topic.
82
+ *
83
+ * A user-authored title (Claude `/rename` / `custom-title`) is never passed
84
+ * here — the caller keeps it verbatim.
85
+ */
86
+ export declare function cleanGeneratedSessionLabel(title: string | undefined): string | undefined;
72
87
  /** Extract a one-line topic from a raw user message, or undefined if the message is pure noise. */
73
88
  export declare function extractSessionTopic(raw: string): string | undefined;
@@ -201,6 +201,27 @@ export function classifyUserPrompt(raw, opts = {}) {
201
201
  const display = (cleanedFirst || firstLine).replace(IMAGE_PATH_RE, '[image]').trim();
202
202
  return { clean: display, kind: 'text' };
203
203
  }
204
+ /**
205
+ * Collapse a harness-generated session title when it is injected skill
206
+ * scaffolding; otherwise leave the title as the harness wrote it.
207
+ *
208
+ * Claude's `ai-title` and Cursor's `chatMeta.title` are both derived from the
209
+ * first turn, so a session opened with a skill gets named after the injected
210
+ * "Base directory for this skill: …" line. {@link classifyUserPrompt} reports
211
+ * `kind: 'skill'` only for that line, so collapse it to `/<skill>`. Empty or
212
+ * whitespace-only input yields `undefined` so the caller falls through to the
213
+ * first-prompt topic.
214
+ *
215
+ * A user-authored title (Claude `/rename` / `custom-title`) is never passed
216
+ * here — the caller keeps it verbatim.
217
+ */
218
+ export function cleanGeneratedSessionLabel(title) {
219
+ const trimmed = title?.trim();
220
+ if (!trimmed)
221
+ return undefined;
222
+ const classified = classifyUserPrompt(trimmed);
223
+ return classified.kind === 'skill' ? classified.clean : trimmed;
224
+ }
204
225
  /** Extract a one-line topic from a raw user message, or undefined if the message is pure noise. */
205
226
  export function extractSessionTopic(raw) {
206
227
  if (!raw.trim())
@@ -9,6 +9,23 @@ export interface ShellProgramOccurrence {
9
9
  program: string;
10
10
  role: ShellProgramRole;
11
11
  }
12
+ /**
13
+ * Harness tool NAMES that run a shell command — Claude's `Bash`, Codex's
14
+ * `exec_command`/`exec` (`exec` is newer gpt-5.6-sol), Droid's `Execute`, plus the
15
+ * generic `shell`/`run_shell_command`/`run_command`/`execute`. This is the single
16
+ * source of truth for "does this tool row carry a shell command?", consumed by the
17
+ * trajectory model (program resolution + command-arg extraction), the directory-touched
18
+ * scan in `state.ts`, the tool-call indexer, and stream rendering — replacing five
19
+ * hand-synced copies that had already drifted apart.
20
+ *
21
+ * Distinct from {@link SHELL_WRAPPERS}, which is interpreter binaries (`bash`, `sh`…)
22
+ * found INSIDE a command string, not the harness's tool name. Match through
23
+ * {@link isShellExecTool} so casing is normalized in one place — harnesses disagree on
24
+ * case (`Bash` vs `bash`, `Execute` vs `exec_command`).
25
+ */
26
+ export declare const SHELL_EXEC_TOOLS: Set<string>;
27
+ /** Case-insensitive membership test for {@link SHELL_EXEC_TOOLS}. */
28
+ export declare function isShellExecTool(tool: string | undefined): boolean;
12
29
  /** Return a word only when Bash expansion cannot change its value. */
13
30
  export declare function staticShellWord(word: Word | undefined): string | undefined;
14
31
  /** Parse Bash without executing it and return every statically identifiable program. */
@@ -1,5 +1,26 @@
1
1
  import path from 'path';
2
2
  import { parse, } from 'unbash';
3
+ /**
4
+ * Harness tool NAMES that run a shell command — Claude's `Bash`, Codex's
5
+ * `exec_command`/`exec` (`exec` is newer gpt-5.6-sol), Droid's `Execute`, plus the
6
+ * generic `shell`/`run_shell_command`/`run_command`/`execute`. This is the single
7
+ * source of truth for "does this tool row carry a shell command?", consumed by the
8
+ * trajectory model (program resolution + command-arg extraction), the directory-touched
9
+ * scan in `state.ts`, the tool-call indexer, and stream rendering — replacing five
10
+ * hand-synced copies that had already drifted apart.
11
+ *
12
+ * Distinct from {@link SHELL_WRAPPERS}, which is interpreter binaries (`bash`, `sh`…)
13
+ * found INSIDE a command string, not the harness's tool name. Match through
14
+ * {@link isShellExecTool} so casing is normalized in one place — harnesses disagree on
15
+ * case (`Bash` vs `bash`, `Execute` vs `exec_command`).
16
+ */
17
+ export const SHELL_EXEC_TOOLS = new Set([
18
+ 'bash', 'exec', 'execute', 'exec_command', 'run_command', 'run_shell_command', 'shell',
19
+ ]);
20
+ /** Case-insensitive membership test for {@link SHELL_EXEC_TOOLS}. */
21
+ export function isShellExecTool(tool) {
22
+ return tool !== undefined && SHELL_EXEC_TOOLS.has(tool.toLowerCase());
23
+ }
3
24
  const SHELL_WRAPPERS = new Set(['bash', 'sh', 'zsh', 'dash', 'ksh']);
4
25
  const PROGRAM_WRAPPERS = new Set(['command', 'builtin', 'env', 'nohup', 'sudo']);
5
26
  const MAX_NESTED_DEPTH = 3;
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import * as path from 'path';
18
18
  import { isCompletedTodoStatus, SNAPSHOT_TODO_TOOLS, summarizeToolUse } from './parse.js';
19
+ import { isShellExecTool } from './shell-programs.js';
19
20
  /**
20
21
  * Detect per-session rate-limit / usage-limit signals in assistant or error
21
22
  * text (RUSH-1523). Matches the same shapes the ext's prewarm detectBlockingPrompt
@@ -174,7 +175,7 @@ export function extractRecentDirectoriesTouched(events, cwd) {
174
175
  if (['Edit', 'Write', 'edit_file', 'write_file', 'create_file', 'edit', 'write'].includes(tool)) {
175
176
  add(args.file_path ?? args.filePath ?? args.path ?? event.path, true);
176
177
  }
177
- else if (['Bash', 'exec_command', 'exec', 'run_shell_command', 'shell', 'Execute'].includes(tool)) {
178
+ else if (isShellExecTool(tool)) {
178
179
  add(args.cwd ?? args.Cwd ?? args.workdir ?? args.working_directory ?? cwd);
179
180
  }
180
181
  }
@@ -2,6 +2,7 @@ import chalk from 'chalk';
2
2
  import { truncate } from '../format.js';
3
3
  import { parseClaudeContent, parseCodexContent, sanitizeEvents, summarizeToolUse } from './parse.js';
4
4
  import { linkPath, relativeToCwd } from './render.js';
5
+ import { isShellExecTool } from './shell-programs.js';
5
6
  const LINE_MAX = 120;
6
7
  function timeOf(event) {
7
8
  const d = new Date(event.timestamp);
@@ -11,7 +12,7 @@ function timeOf(event) {
11
12
  }
12
13
  function paintTool(tool) {
13
14
  const label = tool.padEnd(10);
14
- if (tool === 'Bash' || tool === 'exec_command')
15
+ if (isShellExecTool(tool))
15
16
  return chalk.yellow(label);
16
17
  if (tool === 'Edit' || tool === 'Write' || tool === 'Read')
17
18
  return chalk.cyan(label);
@@ -1,7 +1,7 @@
1
1
  import { createHash } from 'crypto';
2
2
  import { parse } from 'acorn';
3
3
  import { knownSecretValuesFromEnv, redactSecrets, sanitizeForTerminal } from '../redact.js';
4
- import { extractShellPrograms } from './shell-programs.js';
4
+ import { extractShellPrograms, isShellExecTool } from './shell-programs.js';
5
5
  export const TOOL_INPUT_MAX_BYTES = 16 * 1024;
6
6
  export const TOOL_SUCCESS_OUTPUT_MAX_BYTES = 1024;
7
7
  export const TOOL_ERROR_OUTPUT_MAX_BYTES = 4 * 1024;
@@ -13,9 +13,6 @@ export const TOOL_INDEX_LIMIT_ORDINAL = Number.MAX_SAFE_INTEGER;
13
13
  export const TOOL_TEXT_PROCESSING_MAX_BYTES = 64 * 1024;
14
14
  export const TOOL_SHELL_PARSE_MAX_BYTES = 64 * 1024;
15
15
  export const TOOL_INDEX_VERSION = 7;
16
- const SHELL_TOOLS = new Set([
17
- 'bash', 'exec', 'execute', 'exec_command', 'run_command', 'run_shell_command', 'shell',
18
- ]);
19
16
  const BASE64_BLOCK = /(?:[A-Za-z0-9+/]{256,}={0,2})/g;
20
17
  const SECRET_FIELD = /(?:token|secret|password|authorization|cookie|api[_-]?key|private[_-]?key)$/i;
21
18
  const KNOWN_SECRET_VALUES = knownSecretValuesFromEnv();
@@ -225,7 +222,7 @@ export function commandsFromCodexExec(source) {
225
222
  return commands;
226
223
  }
227
224
  function commandFor(tool, args, command) {
228
- if (!SHELL_TOOLS.has(tool.toLowerCase()))
225
+ if (!isShellExecTool(tool))
229
226
  return undefined;
230
227
  const direct = command
231
228
  ?? (typeof args?.command === 'string' ? args.command : undefined)
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { formatDuration, formatTokenCount } from './render.js';
16
16
  import { escapeHtml } from './share-html.js';
17
+ import { isShellExecTool } from './shell-programs.js';
17
18
  /**
18
19
  * The honest footer prefix for a rendered trace: the redacted label only when
19
20
  * something was actually redacted, else the "local only" disclaimer under
@@ -30,7 +31,7 @@ function toolColor(step) {
30
31
  if (step.kind === 'thinking')
31
32
  return '#3a3a55';
32
33
  const tool = (step.tool ?? '').toLowerCase();
33
- if (tool === 'bash' || tool === 'shell' || tool.includes('exec') || tool === 'run_command')
34
+ if (isShellExecTool(tool))
34
35
  return '#e0b341';
35
36
  if (tool === 'read' || tool === 'grep' || tool === 'glob' || tool === 'search' || tool === 'codebase_search')
36
37
  return '#4a9eff';
@@ -22,22 +22,13 @@
22
22
  */
23
23
  import { redactSecrets } from '../redact.js';
24
24
  import { computeSummaryStats } from './render.js';
25
- import { extractShellPrograms } from './shell-programs.js';
25
+ import { extractShellPrograms, isShellExecTool } from './shell-programs.js';
26
26
  const DEFAULT_IDLE_THRESHOLD_MS = 120_000;
27
27
  const DEFAULT_MAX_STEPS = 5000;
28
28
  const LABEL_MAX = 140;
29
29
  const DETAIL_MAX = 400;
30
30
  /** Tools that spawn an inline sub-agent inside THIS transcript (a `tool_use` row). */
31
31
  const INLINE_TASK_TOOLS = new Set(['Task', 'Agent']);
32
- /**
33
- * Shell tools whose command is worth resolving to an effective program — every
34
- * harness's shell-exec tool, kept in lockstep with the canonical set in
35
- * `state.ts` (`['Bash', 'exec_command', 'exec', 'run_shell_command', 'shell', 'Execute']`)
36
- * so Codex's `exec_command`/`exec` and the rest are covered, not just Claude's `Bash`.
37
- * `exec` is newer Codex (gpt-5.6-sol) whose real command the parser unwraps from a
38
- * JS cell into the event's `command` before this runs.
39
- */
40
- const SHELL_TOOLS = new Set(['Bash', 'exec_command', 'exec', 'run_shell_command', 'shell', 'Execute']);
41
32
  /**
42
33
  * Shell builtins/assignments that are rarely the POINT of a command — the action
43
34
  * is whatever runs after them (`export X=Y; git push` → `git`, `cd dir && bun test`
@@ -141,7 +132,7 @@ function stripLeadingShellNoise(command) {
141
132
  function toolLabel(tool, args, command, redact, knownSecrets) {
142
133
  let raw;
143
134
  const lower = tool.toLowerCase();
144
- if (lower === 'bash' || lower === 'shell' || lower === 'run_command' || lower.includes('exec')) {
135
+ if (isShellExecTool(tool)) {
145
136
  const cmd = command ?? stringArg(args, 'command', 'cmd', 'script');
146
137
  raw = cmd ? stripLeadingShellNoise(cmd) : cmd;
147
138
  }
@@ -247,7 +238,7 @@ export function buildTrajectory(events, meta, options = {}) {
247
238
  label: toolLabel(tool, e.args, e.command, redact, knownSecrets),
248
239
  delegation: INLINE_TASK_TOOLS.has(tool) ? 'inline-task' : undefined,
249
240
  callId: e.callId,
250
- program: SHELL_TOOLS.has(tool) ? effectiveProgram(e.command ?? (typeof e.args?.command === 'string' ? e.args.command : undefined)) : undefined,
241
+ program: isShellExecTool(tool) ? effectiveProgram(e.command ?? (typeof e.args?.command === 'string' ? e.args.command : undefined)) : undefined,
251
242
  exitCode: typeof e.exitCode === 'number' ? e.exitCode : undefined,
252
243
  },
253
244
  eventIndex: i,
@@ -31,6 +31,16 @@ export declare const AG_TMUX_NAME_RE: RegExp;
31
31
  /** Whether `name` is an `ag-<agent>-<shortid>` tmux session name. */
32
32
  export declare function isAgentTmuxAlias(name: string): boolean;
33
33
  export declare function isSessionTrackedAgent(agent: string): agent is SessionAgentId;
34
+ /**
35
+ * The name `agents sessions` shows for a run. A custom harness launched via a
36
+ * profile (`agents run deepseek`) keeps its host agent for transcript discovery
37
+ * and parsing (`claude`) and stamps the profile name on `harness`. Display
38
+ * prefers that stamp so a deepseek run is not listed as claude (PHNX-2935).
39
+ */
40
+ export declare function sessionDisplayAgent(session: {
41
+ agent: string;
42
+ harness?: string | null;
43
+ }): string;
34
44
  /** A single normalized event within a session (message, tool call, thinking, etc.). */
35
45
  export interface SessionEvent {
36
46
  type: 'message' | 'tool_use' | 'tool_result' | 'thinking' | 'error' | 'init' | 'result' | 'usage' | 'attachment' | 'hook' | 'interrupt';
@@ -154,6 +164,13 @@ export interface SessionMeta {
154
164
  id: string;
155
165
  shortId: string;
156
166
  agent: SessionAgentId;
167
+ /**
168
+ * Custom harness / profile name when this session was launched via
169
+ * `agents run <profile>` (e.g. `deepseek`). `agent` stays the HOST
170
+ * harness that produced the transcript (`claude`, …) so discovery and
171
+ * parsing keep working. Display surfaces this when set (PHNX-2935).
172
+ */
173
+ harness?: string;
157
174
  /** Where the indexed transcript came from. Routine rows are archived from a run directory. */
158
175
  origin?: 'cli' | 'routine';
159
176
  /** Routine name for transcripts archived from ~/.agents/.history/runs/<name>/<runId>/. */
@@ -367,6 +384,14 @@ export interface SessionMeta {
367
384
  _matchedTerms?: string[];
368
385
  /** BM25 relevance score from the most recent content-index search */
369
386
  _bm25Score?: number;
387
+ /**
388
+ * A short highlighted excerpt (`**term**`) around the best-matching FTS5
389
+ * column (label/topic/project/user content/assistant answer) for the most
390
+ * recent content-index search. Unlike `_matchedTerms`/`_bm25Score` this is
391
+ * NOT stripped from `--json` output — it's the searchable-context payload a
392
+ * content-search result carries, not internal ranking bookkeeping.
393
+ */
394
+ snippet?: string;
370
395
  }
371
396
  /** Output format for rendering a session's content. */
372
397
  export type ViewMode = 'summary' | 'markdown' | 'json';
@@ -31,3 +31,13 @@ export function isAgentTmuxAlias(name) {
31
31
  export function isSessionTrackedAgent(agent) {
32
32
  return SESSION_AGENTS.includes(agent);
33
33
  }
34
+ /**
35
+ * The name `agents sessions` shows for a run. A custom harness launched via a
36
+ * profile (`agents run deepseek`) keeps its host agent for transcript discovery
37
+ * and parsing (`claude`) and stamps the profile name on `harness`. Display
38
+ * prefers that stamp so a deepseek run is not listed as claude (PHNX-2935).
39
+ */
40
+ export function sessionDisplayAgent(session) {
41
+ const harness = session.harness?.trim();
42
+ return harness || session.agent;
43
+ }
@@ -53,6 +53,12 @@ export interface PublishOptions {
53
53
  writeToken?: string;
54
54
  /** DI seam for tests — override `readSession()`. `null` means signed out. */
55
55
  session?: import('../identity/client.js').PhoenixSession | null;
56
+ /**
57
+ * Override the sharer's avatar URL stamped on the object (test seam). When
58
+ * omitted it is derived from the signed-in email via {@link resolveShareAvatar};
59
+ * an empty string suppresses the stamp (initials-only bar).
60
+ */
61
+ avatar?: string;
56
62
  /** Force the BYO Cloudflare path even when signed in. */
57
63
  byo?: boolean;
58
64
  /** DI seam for tests — override the real HTTP PUT. */
@@ -83,6 +89,9 @@ export interface PublishOptions {
83
89
  provenance?: ShareProvenance;
84
90
  }
85
91
  export type ShareVisibility = 'public' | 'unlisted' | 'me' | 'org';
92
+ /** The visibility levels a share may carry — the Worker's own set, used to
93
+ * validate `--visibility` and `share visibility <target> <level>`. */
94
+ export declare const SHARE_VISIBILITY_LEVELS: readonly ShareVisibility[];
86
95
  export interface PublishResult {
87
96
  url: string;
88
97
  /** URL-safe object name, explicit (`--slug`) or deterministically derived. */
@@ -124,7 +133,7 @@ export interface ShareProvenance {
124
133
  * provenance the CLI sets automatically, plus `expires-at` / `visibility` /
125
134
  * `owner` which the Worker stamps itself.
126
135
  */
127
- export declare const RESERVED_META_KEYS: readonly ["expires-at", "visibility", "owner", "agent", "session", "host", "repo", "date", "label", "label-source"];
136
+ export declare const RESERVED_META_KEYS: readonly ["expires-at", "published-at", "visibility", "owner", "org_domain", "agent", "session", "host", "repo", "date", "avatar", "label", "label-source"];
128
137
  /**
129
138
  * Auto-capture publish provenance from the exec env, git, and the local clock.
130
139
  * Every field is present only when the environment genuinely carries it — a
@@ -139,6 +148,21 @@ export declare function resolveShareProvenance(opts?: {
139
148
  dir?: string;
140
149
  now?: Date;
141
150
  }): ShareProvenance;
151
+ /**
152
+ * The sharer's avatar URL, stamped so the share bar can show a real profile
153
+ * picture instead of only the initials circle. We key a Gravatar on the SHA-256
154
+ * of the signed-in user's lowercased email (Gravatar resolves either MD5 or
155
+ * SHA-256), with `d=404` so Gravatar returns 404 for a user who has none — the
156
+ * bar's `<img>` onerror then falls back to the initials circle. Only the hash
157
+ * lands in public metadata, never the raw email. Returns '' when signed out
158
+ * (BYO without a Phoenix session), leaving the bar on the initials circle.
159
+ *
160
+ * `opts.session === null` means "explicitly signed out" (a test seam / BYO) and
161
+ * yields ''; `undefined` reads the real persisted session.
162
+ */
163
+ export declare function resolveShareAvatar(opts?: {
164
+ session?: import('../identity/client.js').PhoenixSession | null;
165
+ }): string;
142
166
  /**
143
167
  * Parse repeated `--meta key=value` CLI args into a validated metadata record.
144
168
  * Keys are lowercase `[a-z0-9-]`, up to 64 characters, and may not collide with
@@ -172,12 +196,36 @@ export declare function sanitizeLabel(text: string): string;
172
196
  * The transliterations above cover what actually shows up; anything else outside
173
197
  * latin1 is dropped, and a value that transliterates to nothing at all (a title
174
198
  * written entirely in a non-latin script) degrades to a marker rather than an
175
- * empty header. Lossy on purpose: carrying full Unicode needs percent-encoding
176
- * here AND a matching decode in the Worker, which every already-deployed Worker
177
- * would render as `%E2%80%A6` until its operator ran `agents artifacts share
178
- * update` — tracked as RUSH-2786.
199
+ * empty header. This value is the **latin1-safe floor** every Worker can read: a
200
+ * pre-Unicode Worker only ever sees `x-share-<field>`, so it MUST stay folded.
201
+ * Full Unicode rides ALONGSIDE it in a percent-encoded companion header
202
+ * (`toPercentHeaderValue` / {@link needsUnicodeCompanion}), which a new Worker
203
+ * opts into via `x-share-encoding: percent` and an old one ignores — so a
204
+ * Japanese/emoji title renders in full on an updated Worker and still folds
205
+ * gracefully everywhere else (PHNX-2786).
179
206
  */
180
207
  export declare function toHeaderValue(text: string): string;
208
+ /**
209
+ * Whether text carries a code point above latin1 (U+00FF) — the meaningful
210
+ * display content `fetch`'s ByteString cannot hold, which {@link toHeaderValue}
211
+ * therefore transliterates or drops. This is the range worth carrying in the
212
+ * percent-encoded companion: an em dash, a curly quote, an emoji, or any
213
+ * CJK/Arabic/Hindi text. A plain accented latin1 name (`José`, é = U+00E9) is
214
+ * NOT lossy and needs no companion.
215
+ *
216
+ * `toHeaderValue` ALSO strips C0/C1 control characters (below U+0020, and
217
+ * U+007F–U+009F), which this deliberately does not flag: a raw ANSI/control
218
+ * sequence is not display text and must not be reconstructed into a page's
219
+ * rendered metadata, so it stays dropped on both the old and new Worker paths.
220
+ */
221
+ export declare function needsUnicodeCompanion(text: string): boolean;
222
+ /**
223
+ * Percent-encode a single-line free-text value for the `x-share-<field>-u`
224
+ * companion header. Whitespace is collapsed first (matching the folded value's
225
+ * single-line shape), then `encodeURIComponent` makes it pure-ASCII and
226
+ * header-safe. The Worker recovers the original with `decodeURIComponent`.
227
+ */
228
+ export declare function toPercentHeaderValue(text: string): string;
181
229
  /**
182
230
  * Best-effort human title when `--label` is omitted: the HTML `<title>`, else a
183
231
  * Markdown frontmatter `title:`, else the filename. Always returns something —