@phnx-labs/agents-cli 1.22.40 → 1.22.42

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 (201) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +14 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/bootstrap.js +0 -1
  5. package/dist/cli/command-registry.d.ts +2 -0
  6. package/dist/cli/command-registry.js +5 -2
  7. package/dist/commands/accounts.d.ts +29 -0
  8. package/dist/commands/accounts.js +220 -29
  9. package/dist/commands/alias.js +1 -1
  10. package/dist/commands/auth.d.ts +9 -0
  11. package/dist/commands/auth.js +104 -0
  12. package/dist/commands/browser-sessions-picker.js +42 -62
  13. package/dist/commands/browser.d.ts +15 -0
  14. package/dist/commands/browser.js +145 -78
  15. package/dist/commands/computer-actions.d.ts +1 -1
  16. package/dist/commands/computer-actions.js +1 -1
  17. package/dist/commands/computer-sessions-picker.js +32 -55
  18. package/dist/commands/computer.d.ts +1 -1
  19. package/dist/commands/computer.js +2 -2
  20. package/dist/commands/doctor.d.ts +1 -1
  21. package/dist/commands/doctor.js +3 -3
  22. package/dist/commands/feed.js +4 -12
  23. package/dist/commands/hooks.js +1 -1
  24. package/dist/commands/import.js +1 -1
  25. package/dist/commands/insights.js +163 -85
  26. package/dist/commands/inspect.js +2 -2
  27. package/dist/commands/mine.js +1 -1
  28. package/dist/commands/org.d.ts +6 -0
  29. package/dist/commands/org.js +174 -0
  30. package/dist/commands/packages.js +1 -1
  31. package/dist/commands/prune.js +1 -1
  32. package/dist/commands/routines.js +1 -1
  33. package/dist/commands/run-account-picker.d.ts +19 -0
  34. package/dist/commands/run-account-picker.js +81 -0
  35. package/dist/commands/secrets.js +87 -97
  36. package/dist/commands/sessions-picker-factory.d.ts +40 -0
  37. package/dist/commands/sessions-picker-factory.js +67 -0
  38. package/dist/commands/sessions-picker.d.ts +1 -0
  39. package/dist/commands/sessions-picker.js +27 -5
  40. package/dist/commands/sessions-share.d.ts +25 -0
  41. package/dist/commands/sessions-share.js +166 -0
  42. package/dist/commands/sessions.js +2 -0
  43. package/dist/commands/setup-browser.js +5 -5
  44. package/dist/commands/setup-mine.js +1 -1
  45. package/dist/commands/setup-preferences.js +4 -4
  46. package/dist/commands/setup.js +5 -3
  47. package/dist/commands/sync.js +1 -1
  48. package/dist/commands/versions.js +1 -1
  49. package/dist/commands/view.js +1 -1
  50. package/dist/commands/webhook.js +14 -9
  51. package/dist/lib/account-capabilities.d.ts +10 -0
  52. package/dist/lib/account-capabilities.js +34 -0
  53. package/dist/lib/agent-spec/agents.d.ts +584 -0
  54. package/dist/lib/agent-spec/agents.js +3127 -0
  55. package/dist/lib/agent-spec/provider.js +1 -1
  56. package/dist/lib/agents.d.ts +2 -584
  57. package/dist/lib/agents.js +2 -3127
  58. package/dist/lib/browser/cdp.js +4 -0
  59. package/dist/lib/browser/chrome.js +12 -1
  60. package/dist/lib/browser/drivers/local.d.ts +8 -2
  61. package/dist/lib/browser/drivers/local.js +8 -2
  62. package/dist/lib/browser/drivers/ssh.d.ts +8 -2
  63. package/dist/lib/browser/drivers/ssh.js +11 -4
  64. package/dist/lib/browser/hygiene.d.ts +1 -1
  65. package/dist/lib/browser/hygiene.js +5 -1
  66. package/dist/lib/browser/profiles.d.ts +77 -4
  67. package/dist/lib/browser/profiles.js +121 -12
  68. package/dist/lib/browser/runtime-state.d.ts +11 -5
  69. package/dist/lib/browser/runtime-state.js +13 -9
  70. package/dist/lib/browser/service.d.ts +91 -30
  71. package/dist/lib/browser/service.js +342 -207
  72. package/dist/lib/browser/types.d.ts +96 -4
  73. package/dist/lib/browser/types.js +51 -0
  74. package/dist/lib/catchup.js +1 -1
  75. package/dist/lib/computer/actions.d.ts +1 -1
  76. package/dist/lib/computer/actions.js +1 -1
  77. package/dist/lib/{computer-rpc.js → computer/computer-rpc.js} +5 -5
  78. package/dist/lib/computer/dispatch.d.ts +1 -1
  79. package/dist/lib/computer/download.d.ts +1 -1
  80. package/dist/lib/computer/download.js +3 -3
  81. package/dist/lib/{ssh-tunnel.d.ts → computer/ssh-tunnel.d.ts} +1 -1
  82. package/dist/lib/{ssh-tunnel.js → computer/ssh-tunnel.js} +12 -12
  83. package/dist/lib/daemon/daemon.js +1 -1
  84. package/dist/lib/{runner.d.ts → daemon/runner.d.ts} +10 -10
  85. package/dist/lib/{runner.js → daemon/runner.js} +59 -118
  86. package/dist/lib/daemon-webhooks.d.ts +3 -1
  87. package/dist/lib/daemon-webhooks.js +12 -7
  88. package/dist/lib/device-config.js +1 -1
  89. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  90. package/dist/lib/devices/doctor-findings.js +3 -3
  91. package/dist/lib/devices/fleet-inventory.js +1 -1
  92. package/dist/lib/devices/harness-inventory.js +1 -1
  93. package/dist/lib/devices/registry.js +4 -22
  94. package/dist/lib/doctor-diff.d.ts +1 -1
  95. package/dist/lib/doctor-diff.js +1 -1
  96. package/dist/lib/drift.js +1 -1
  97. package/dist/lib/entitlement.d.ts +31 -0
  98. package/dist/lib/entitlement.js +137 -0
  99. package/dist/lib/exec.js +39 -202
  100. package/dist/lib/feed/feed.js +2 -8
  101. package/dist/lib/fs-atomic.d.ts +7 -0
  102. package/dist/lib/fs-atomic.js +9 -0
  103. package/dist/lib/harness/adapter.d.ts +160 -0
  104. package/dist/lib/harness/adapter.js +47 -0
  105. package/dist/lib/harness/adapters/claude.d.ts +2 -0
  106. package/dist/lib/harness/adapters/claude.js +110 -0
  107. package/dist/lib/harness/adapters/codex.d.ts +2 -0
  108. package/dist/lib/harness/adapters/codex.js +83 -0
  109. package/dist/lib/harness/adapters/copilot.d.ts +2 -0
  110. package/dist/lib/harness/adapters/copilot.js +23 -0
  111. package/dist/lib/harness/adapters/cursor.d.ts +2 -0
  112. package/dist/lib/harness/adapters/cursor.js +38 -0
  113. package/dist/lib/harness/adapters/droid.d.ts +2 -0
  114. package/dist/lib/harness/adapters/droid.js +17 -0
  115. package/dist/lib/harness/adapters/grok.d.ts +2 -0
  116. package/dist/lib/harness/adapters/grok.js +14 -0
  117. package/dist/lib/harness/adapters/kimi.d.ts +2 -0
  118. package/dist/lib/harness/adapters/kimi.js +49 -0
  119. package/dist/lib/harness/adapters/muse.d.ts +2 -0
  120. package/dist/lib/harness/adapters/muse.js +44 -0
  121. package/dist/lib/harness/adapters/opencode.d.ts +2 -0
  122. package/dist/lib/harness/adapters/opencode.js +14 -0
  123. package/dist/lib/harness/exec-config-version.d.ts +8 -0
  124. package/dist/lib/harness/exec-config-version.js +22 -0
  125. package/dist/lib/harness/index.d.ts +1 -0
  126. package/dist/lib/harness/index.js +34 -0
  127. package/dist/lib/{hooks.d.ts → hooks/install.d.ts} +1 -1
  128. package/dist/lib/{hooks.js → hooks/install.js} +11 -11
  129. package/dist/lib/import.js +1 -1
  130. package/dist/lib/installations/index.d.ts +4 -4
  131. package/dist/lib/installations/index.js +3 -4
  132. package/dist/lib/installations/migrate.js +6 -6
  133. package/dist/lib/{shims.d.ts → installations/shims.d.ts} +2 -2
  134. package/dist/lib/{shims.js → installations/shims.js} +12 -114
  135. package/dist/lib/installations/store.d.ts +141 -3
  136. package/dist/lib/installations/store.js +475 -5
  137. package/dist/lib/installations/versions.d.ts +1 -126
  138. package/dist/lib/installations/versions.js +5 -465
  139. package/dist/lib/isolation-boundary-report.d.ts +1 -1
  140. package/dist/lib/isolation-boundary-report.js +1 -1
  141. package/dist/lib/mailbox-gc.js +2 -3
  142. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  143. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  144. package/dist/lib/monitors/dispatch.d.ts +1 -1
  145. package/dist/lib/monitors/dispatch.js +2 -2
  146. package/dist/lib/monitors/state.d.ts +1 -1
  147. package/dist/lib/observe-aliases.d.ts +2 -2
  148. package/dist/lib/observe-aliases.js +2 -11
  149. package/dist/lib/plugins/plugins.js +1 -1
  150. package/dist/lib/plugins/skills.js +1 -1
  151. package/dist/lib/prix-account.d.ts +158 -0
  152. package/dist/lib/prix-account.js +214 -0
  153. package/dist/lib/project-key.js +7 -0
  154. package/dist/lib/project-launch.js +1 -1
  155. package/dist/lib/refresh.js +2 -2
  156. package/dist/lib/resource-inventory.d.ts +1 -1
  157. package/dist/lib/resource-inventory.js +1 -1
  158. package/dist/lib/rules/rules.js +1 -1
  159. package/dist/lib/scheduling/routines.js +1 -1
  160. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  161. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  162. package/dist/lib/secrets/bundles.d.ts +1 -1
  163. package/dist/lib/secrets/bundles.js +1 -1
  164. package/dist/lib/secrets/headless.d.ts +16 -0
  165. package/dist/lib/secrets/headless.js +21 -0
  166. package/dist/lib/secrets/remote.d.ts +9 -7
  167. package/dist/lib/secrets/remote.js +18 -9
  168. package/dist/lib/self-heal/checks/hook-runtime.js +1 -1
  169. package/dist/lib/self-heal/checks/path.js +1 -1
  170. package/dist/lib/self-heal/checks/shadowing.js +1 -1
  171. package/dist/lib/self-heal/checks/shims.js +1 -1
  172. package/dist/lib/session/discover.d.ts +39 -0
  173. package/dist/lib/session/discover.js +3 -3
  174. package/dist/lib/session/parse.js +24 -42
  175. package/dist/lib/session/recovery.js +1 -1
  176. package/dist/lib/session/share-html.d.ts +55 -0
  177. package/dist/lib/session/share-html.js +319 -0
  178. package/dist/lib/settings-manifest.d.ts +2 -0
  179. package/dist/lib/settings-manifest.js +81 -3
  180. package/dist/lib/share/publish.d.ts +38 -0
  181. package/dist/lib/share/publish.js +81 -8
  182. package/dist/lib/staleness/prune.d.ts +1 -1
  183. package/dist/lib/staleness/prune.js +1 -1
  184. package/dist/lib/staleness/writers/hooks.js +1 -1
  185. package/dist/lib/startup/command-registry.d.ts +2 -1
  186. package/dist/lib/startup/command-registry.js +5 -3
  187. package/dist/lib/teams/agents.js +2 -19
  188. package/dist/lib/teams/parsers.js +20 -26
  189. package/dist/lib/teams/registry.js +2 -21
  190. package/dist/lib/triggers/handlers.d.ts +37 -2
  191. package/dist/lib/triggers/handlers.js +57 -6
  192. package/dist/lib/triggers/webhook.d.ts +80 -6
  193. package/dist/lib/triggers/webhook.js +128 -4
  194. package/dist/lib/types.d.ts +1 -1
  195. package/dist/lib/uninstall.js +1 -1
  196. package/dist/lib/version.d.ts +9 -2
  197. package/dist/lib/version.js +11 -9
  198. package/dist/lib/wrap.d.ts +33 -0
  199. package/dist/lib/wrap.js +70 -0
  200. package/package.json +1 -1
  201. /package/dist/lib/{computer-rpc.d.ts → computer/computer-rpc.d.ts} +0 -0
@@ -5,7 +5,7 @@
5
5
  // and PATH-order problem are POSIX; Windows resolves via the registry PATH).
6
6
  import { resultOf } from '../types.js';
7
7
  import { AGENTS } from '../../agents.js';
8
- import { getPathShadowingExecutable, adoptShadowingLauncher, listAgentsWithNonIsolatedInstalledVersions, } from '../../shims.js';
8
+ import { getPathShadowingExecutable, adoptShadowingLauncher, listAgentsWithNonIsolatedInstalledVersions, } from '../../installations/shims.js';
9
9
  import { getGlobalDefault } from '../../installations/versions.js';
10
10
  export const shadowingCheck = {
11
11
  id: 'shadowing',
@@ -4,7 +4,7 @@
4
4
  // background so the shim schema settles without user-facing churn.
5
5
  import { resultOf } from '../types.js';
6
6
  import { AGENTS } from '../../agents.js';
7
- import { createShim, ensureShimCurrent, ensureVersionedAliasCurrent, isShimCurrent, isVersionedAliasCurrent, removeShim, shimExists, shimPointsAtLiveInstall, removeLegacyUserShim, listAgentsWithInstalledVersions, listAgentsWithNonIsolatedInstalledVersions, listShimFileNames, pruneOrphanedCommandShim, } from '../../shims.js';
7
+ import { createShim, ensureShimCurrent, ensureVersionedAliasCurrent, isShimCurrent, isVersionedAliasCurrent, removeShim, shimExists, shimPointsAtLiveInstall, removeLegacyUserShim, listAgentsWithInstalledVersions, listAgentsWithNonIsolatedInstalledVersions, listShimFileNames, pruneOrphanedCommandShim, } from '../../installations/shims.js';
8
8
  import { listInstalledVersions } from '../../installations/versions.js';
9
9
  export const shimsCheck = {
10
10
  id: 'shims',
@@ -644,6 +644,29 @@ export declare function scanClaudeSessionIncremental(filePath: string, fromOffse
644
644
  newOffset: number;
645
645
  toolCalls: IndexedToolCall[];
646
646
  }>;
647
+ /**
648
+ * Decide full-vs-incremental for one Claude file and parse it uniformly, always
649
+ * returning a finalized scan plus the continuation to persist. Both branches run
650
+ * through the SAME reducer (via {@link scanClaudeSessionIncremental}), so the row
651
+ * an append produces is identical to a from-scratch full reparse by construction
652
+ * (the B-1 parity harness proves this at the function level).
653
+ *
654
+ * INCREMENTAL when a prior continuation exists AND the file grew past the
655
+ * persisted offset AND its mtime did not go backwards — an in-place append.
656
+ * FULL (from byte 0, fresh state) otherwise: cold start (no prior), truncation /
657
+ * rewrite (size shrank to at or below the offset), or a clock rewind / restore
658
+ * (mtime older than the last parse). A FULL parse still produces a continuation,
659
+ * so the file's very next append can go incremental.
660
+ *
661
+ * `mode` is returned so the caller (and tests) can confirm which branch ran.
662
+ */
663
+ export declare function scanClaudeSessionResumable(filePath: string, prior: ClaudeParserState | null, currentFileMtimeMs: number, currentFileSize: number, priorFileMtimeMs?: number): Promise<{
664
+ scan: ClaudeSessionScan;
665
+ newState: ClaudeParserState;
666
+ newOffset: number;
667
+ toolCalls: IndexedToolCall[];
668
+ mode: 'full' | 'incremental';
669
+ }>;
647
670
  /** Test seam: read the (incremental, full) Claude parse counters. */
648
671
  export declare function __claudeScanBranchCountsForTest(): {
649
672
  incremental: number;
@@ -776,6 +799,22 @@ export declare function scanCodexSessionIncremental(filePath: string, fromOffset
776
799
  newOffset: number;
777
800
  toolCalls: IndexedToolCall[];
778
801
  }>;
802
+ /**
803
+ * Decide full-vs-incremental for one Codex rollout and parse it uniformly,
804
+ * always returning a finalized scan plus the continuation to persist. Both
805
+ * branches run through the SAME reducer (via {@link scanCodexSessionIncremental}),
806
+ * so an append produces a row identical to a from-scratch full reparse by
807
+ * construction. INCREMENTAL when a prior continuation exists AND the file grew
808
+ * past the persisted offset AND its mtime did not go backwards; FULL (from byte
809
+ * 0, fresh state) otherwise (cold start, truncation/rewrite, clock rewind).
810
+ */
811
+ export declare function scanCodexSessionResumable(filePath: string, prior: CodexParserState | null, currentFileMtimeMs: number, currentFileSize: number, priorFileMtimeMs?: number): Promise<{
812
+ scan: CodexSessionScan;
813
+ newState: CodexParserState;
814
+ newOffset: number;
815
+ toolCalls: IndexedToolCall[];
816
+ mode: 'full' | 'incremental';
817
+ }>;
779
818
  /** Test seam: read the (incremental, full) Codex parse counters. */
780
819
  export declare function __codexScanBranchCountsForTest(): {
781
820
  incremental: number;
@@ -22,7 +22,7 @@ import { AGENTS, agentConfigDirName, getCliVersion, resolveOpenCodeAccountId } f
22
22
  import { walkForFilesWithStat } from '../fs-walk.js';
23
23
  import { hasCommand } from '../cli-resources.js';
24
24
  import { execFileShellSpec } from '../platform/exec.js';
25
- import { getConfigSymlinkVersion } from '../shims.js';
25
+ import { getConfigSymlinkVersion } from '../installations/shims.js';
26
26
  import { SESSION_AGENTS } from './types.js';
27
27
  import { deriveShortId } from './short-id.js';
28
28
  import { buildClaudeAccountIndex, resolveClaudeAccount } from './claude-accounts.js';
@@ -3491,7 +3491,7 @@ function freshClaudeParserState() {
3491
3491
  *
3492
3492
  * `mode` is returned so the caller (and tests) can confirm which branch ran.
3493
3493
  */
3494
- async function scanClaudeSessionResumable(filePath, prior, currentFileMtimeMs, currentFileSize, priorFileMtimeMs) {
3494
+ export async function scanClaudeSessionResumable(filePath, prior, currentFileMtimeMs, currentFileSize, priorFileMtimeMs) {
3495
3495
  // File size + mtime cannot distinguish an APPEND from an in-place rewrite or a
3496
3496
  // restore that dropped DIFFERENT, larger content at the same path: both grow
3497
3497
  // the file and move mtime forward. Resuming from the stored offset across that
@@ -3977,7 +3977,7 @@ async function codexSessionIdentityAt(filePath, maxBytes = 1_048_576) {
3977
3977
  * past the persisted offset AND its mtime did not go backwards; FULL (from byte
3978
3978
  * 0, fresh state) otherwise (cold start, truncation/rewrite, clock rewind).
3979
3979
  */
3980
- async function scanCodexSessionResumable(filePath, prior, currentFileMtimeMs, currentFileSize, priorFileMtimeMs) {
3980
+ export async function scanCodexSessionResumable(filePath, prior, currentFileMtimeMs, currentFileSize, priorFileMtimeMs) {
3981
3981
  // File size + mtime cannot distinguish an APPEND from an in-place rewrite or a
3982
3982
  // restore that dropped a DIFFERENT, larger rollout at the same path: both grow
3983
3983
  // the file and move mtime forward. Resuming from the stored offset across that
@@ -117,53 +117,35 @@ function truncateNormalizedToolOutput(output, maxChars) {
117
117
  return output;
118
118
  return `${output.slice(0, maxChars)}\n\n[Output truncated: ${output.length - maxChars} characters omitted.]`;
119
119
  }
120
+ /**
121
+ * Registry-dispatch table: each `SessionAgentId` to its offline transcript
122
+ * parser. Replaces the per-harness `switch` — the harness axis of Move 3. Kept
123
+ * as its own table (not on the HarnessAdapter registry) because its id domain is
124
+ * `SessionAgentId`: `rush` is a session agent with no `AgentId`, and the offline
125
+ * transcript reader is a deliberately separate concern from live team events.
126
+ * The `Record` is total, so a new session harness must add an entry here.
127
+ */
128
+ const TRANSCRIPT_PARSERS = {
129
+ claude: (filePath, opts) => parseClaude(filePath, opts),
130
+ codex: (filePath) => parseCodex(filePath),
131
+ gemini: (filePath) => parseGemini(filePath),
132
+ antigravity: (filePath) => parseAntigravity(filePath),
133
+ opencode: (filePath) => parseOpenCode(filePath),
134
+ grok: (filePath) => parseGrok(filePath),
135
+ rush: (filePath) => parseRush(filePath),
136
+ openclaw: () => [], // OpenClaw sessions don't have parseable files yet
137
+ hermes: (filePath) => parseHermes(filePath),
138
+ kimi: (filePath) => parseKimi(filePath),
139
+ droid: (filePath) => parseDroid(filePath),
140
+ cursor: (filePath) => parseCursor(filePath),
141
+ muse: (filePath) => parseMuse(filePath),
142
+ };
120
143
  export function parseSession(filePath, agent, opts = {}) {
121
144
  const detected = agent || detectAgent(filePath);
122
145
  if (!detected) {
123
146
  throw new Error(`Cannot detect agent type from path: ${filePath}`);
124
147
  }
125
- let events;
126
- switch (detected) {
127
- case 'claude':
128
- events = parseClaude(filePath, opts);
129
- break;
130
- case 'codex':
131
- events = parseCodex(filePath);
132
- break;
133
- case 'gemini':
134
- events = parseGemini(filePath);
135
- break;
136
- case 'antigravity':
137
- events = parseAntigravity(filePath);
138
- break;
139
- case 'opencode':
140
- events = parseOpenCode(filePath);
141
- break;
142
- case 'grok':
143
- events = parseGrok(filePath);
144
- break;
145
- case 'rush':
146
- events = parseRush(filePath);
147
- break;
148
- case 'openclaw':
149
- events = [];
150
- break; // OpenClaw sessions don't have parseable files yet
151
- case 'hermes':
152
- events = parseHermes(filePath);
153
- break;
154
- case 'kimi':
155
- events = parseKimi(filePath);
156
- break;
157
- case 'droid':
158
- events = parseDroid(filePath);
159
- break;
160
- case 'cursor':
161
- events = parseCursor(filePath);
162
- break;
163
- case 'muse':
164
- events = parseMuse(filePath);
165
- break;
166
- }
148
+ const events = TRANSCRIPT_PARSERS[detected](filePath, opts);
167
149
  // Chokepoint: every string field that originated in an untrusted session
168
150
  // file gets stripped of terminal escapes here, so renderers downstream can
169
151
  // safely splat values into chalk/console output. Same pass flags
@@ -5,7 +5,7 @@ import { isSelfHost } from '../devices/self-host.js';
5
5
  import { nativeResume } from '../exec.js';
6
6
  import { machineId, normalizeHost } from '../machine-id.js';
7
7
  import { collectRunCandidates, formatNoHealthyAccountError, pickBalancedCandidate, readinessFromCandidate, } from '../accounting/rotate.js';
8
- import { getVersionHomePath } from '../installations/versions.js';
8
+ import { getVersionHomePath } from '../installations/store.js';
9
9
  export class SessionRecoveryError extends Error {
10
10
  constructor(message) {
11
11
  super(message);
@@ -0,0 +1,55 @@
1
+ import type { SessionMeta } from './types.js';
2
+ export interface SessionHtmlOptions {
3
+ /** Whether the Markdown was rendered with redaction on — shown in the footer. */
4
+ redacted?: boolean;
5
+ }
6
+ /** One metadata chip in the page header. */
7
+ interface Chip {
8
+ label: string;
9
+ value: string;
10
+ }
11
+ export declare function escapeHtml(text: string): string;
12
+ /**
13
+ * Lift every folded reasoning block out of the Markdown, leaving a sentinel.
14
+ *
15
+ * `renderer.html` escapes raw HTML unconditionally — the transcript body is model
16
+ * and tool output, so nothing in it may round-trip as markup. That also escaped
17
+ * the `<details>` wrapper `--reasoning fold` emits, which is why the disclosure
18
+ * element has to be reconstructed here instead of allowlisted through.
19
+ *
20
+ * Allowlisting was tried twice and is the wrong shape: marked hands the same
21
+ * markup over as several different raw strings depending on context (a whole-token
22
+ * gate passed the opening tag and escaped the closing one in a real document), and
23
+ * a bare `<details>` in ordinary prose is a complete INLINE html token — so prose
24
+ * that merely mentions the tag would emit a live, unclosed element that swallows
25
+ * every later turn into it. Reconstructing both ends here makes the pairing
26
+ * structural: it cannot be unbalanced by anything the transcript contains.
27
+ */
28
+ export declare function liftFoldBlocks(markdown: string): {
29
+ text: string;
30
+ blocks: string[];
31
+ };
32
+ /** Human duration — "13 minutes", never "12m 49s". */
33
+ export declare function formatDuration(ms: number): string;
34
+ /**
35
+ * The chips shown under the title.
36
+ *
37
+ * Deliberately excludes `account` (an email — the publish-time sensitive-content
38
+ * scan rejects those, and correctly), `cwd`, and `machine`: a published transcript
39
+ * should not carry the operator's identity or local paths in its chrome. Host and
40
+ * repo already ride in the share object's provenance metadata, which is the right
41
+ * home for them.
42
+ */
43
+ export declare function buildChips(session: SessionMeta): Chip[];
44
+ /**
45
+ * The page's `<title>` — `deriveLabel()` reads this as the share's gallery label.
46
+ *
47
+ * The document heading is derived from the session's first prompt, which in a real
48
+ * session is routinely a pasted URL plus a file path plus the actual request. Left
49
+ * whole it renders as a three-line wall above the transcript and an unreadable
50
+ * gallery row, so it is collapsed to one line and cut at a word boundary. `--label`
51
+ * overrides it outright.
52
+ */
53
+ export declare function sessionPageTitle(session: SessionMeta, markdown: string): string;
54
+ export declare function renderSessionHtmlDocument(session: SessionMeta, markdown: string, options?: SessionHtmlOptions): string;
55
+ export {};
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Wrap an already-rendered session transcript in ONE self-contained HTML page for
3
+ * `agents sessions share`.
4
+ *
5
+ * Takes the Markdown document as an argument rather than producing it, so the
6
+ * decisions that make a transcript safe to publish — redaction, synthetic-turn
7
+ * filtering, tool-output truncation, reasoning visibility — stay in
8
+ * `renderSessionMarkdownDocument()` and the HTML path can never drift from the
9
+ * Markdown one. That also keeps this a pure presentation function with no import
10
+ * back into the command layer.
11
+ *
12
+ * Self-contained on purpose: an inline <style>, no external assets, no CDN, no
13
+ * dependency on the artifacts-cli host CLI (which is not configured on every box,
14
+ * RUSH-2728). The page is uploaded verbatim to the share Worker.
15
+ *
16
+ * Terminal-coded per the agents-cli brand (#0a0a0a bg, #a3e635 lime accent,
17
+ * JetBrains Mono), with a light theme under `prefers-color-scheme: light` and an
18
+ * in-page toggle so a published link is readable in bright light and dim alike.
19
+ */
20
+ import { marked, Renderer } from 'marked';
21
+ export function escapeHtml(text) {
22
+ return text
23
+ .replace(/&/g, '&amp;')
24
+ .replace(/</g, '&lt;')
25
+ .replace(/>/g, '&gt;')
26
+ .replace(/"/g, '&quot;')
27
+ .replace(/'/g, '&#39;');
28
+ }
29
+ /**
30
+ * A transcript is untrusted text: it carries whatever the model wrote and whatever
31
+ * a tool printed. marked passes raw HTML through by default, so a session that
32
+ * merely *discussed* a `<script>` tag would ship an executable one on a public URL.
33
+ * Escaping the html token's raw source neutralizes that without mangling the
34
+ * visible text, and non-http(s) link schemes are dropped so `javascript:` cannot
35
+ * ride in through a Markdown link.
36
+ */
37
+ /**
38
+ * The only raw HTML the transcript renderer itself emits: `--reasoning fold` wraps
39
+ * each thinking block in a disclosure element (`session/render.ts` — the `fold`
40
+ * branch pushes `<details>\n<summary>Reasoning</summary>` and `</details>`).
41
+ *
42
+ * Escaping those along with everything else published `&lt;details&gt;` as visible
43
+ * text and no collapsing, so `--reasoning fold` shipped broken. They are allowed
44
+ * back through by EXACT match on the token's raw source, not by parsing tags:
45
+ * attribute-free `details`/`summary` carry no script, no URL, and no event handler,
46
+ * and an exact-string gate cannot be widened by crafted input the way a tag parser
47
+ * can. Anything else — including `<details onclick=…>` — still escapes.
48
+ */
49
+ /**
50
+ * Sentinel standing in for one folded reasoning block while the document goes
51
+ * through marked. Deliberately not valid Markdown or HTML, and any pre-existing
52
+ * occurrence in the transcript is neutralized before substitution.
53
+ */
54
+ const FOLD_SENTINEL = 'aGeNtSfOlDbLoCk';
55
+ // The neutralization below is only sound because this literal is BORDER-FREE: its
56
+ // first character occurs nowhere else in it, so no occurrence can overlap another
57
+ // and splitting on it cannot leave a fresh one behind. A bordered literal (`aba`
58
+ // splits into a leftover `a` + `ba`) is defeatable. Keep that property if you
59
+ // ever change the string.
60
+ /** The exact block `session/render.ts` pushes on its `fold` branch. */
61
+ const FOLD_BLOCK = /<details>\n<summary>Reasoning<\/summary>\n\n([\s\S]*?)\n\n<\/details>/g;
62
+ /**
63
+ * Lift every folded reasoning block out of the Markdown, leaving a sentinel.
64
+ *
65
+ * `renderer.html` escapes raw HTML unconditionally — the transcript body is model
66
+ * and tool output, so nothing in it may round-trip as markup. That also escaped
67
+ * the `<details>` wrapper `--reasoning fold` emits, which is why the disclosure
68
+ * element has to be reconstructed here instead of allowlisted through.
69
+ *
70
+ * Allowlisting was tried twice and is the wrong shape: marked hands the same
71
+ * markup over as several different raw strings depending on context (a whole-token
72
+ * gate passed the opening tag and escaped the closing one in a real document), and
73
+ * a bare `<details>` in ordinary prose is a complete INLINE html token — so prose
74
+ * that merely mentions the tag would emit a live, unclosed element that swallows
75
+ * every later turn into it. Reconstructing both ends here makes the pairing
76
+ * structural: it cannot be unbalanced by anything the transcript contains.
77
+ */
78
+ export function liftFoldBlocks(markdown) {
79
+ const blocks = [];
80
+ // A transcript that literally contains the sentinel must not be able to forge a
81
+ // disclosure element, so break any occurrence before inserting our own.
82
+ const safe = markdown.split(FOLD_SENTINEL).join(`${FOLD_SENTINEL[0]}​${FOLD_SENTINEL.slice(1)}`);
83
+ const text = safe.replace(FOLD_BLOCK, (_match, inner) => {
84
+ blocks.push(inner);
85
+ return `${FOLD_SENTINEL}${blocks.length - 1}${FOLD_SENTINEL}`;
86
+ });
87
+ return { text, blocks };
88
+ }
89
+ function safeRenderer() {
90
+ const renderer = new Renderer();
91
+ renderer.html = ({ raw }) => escapeHtml(raw);
92
+ const isSafeHref = (href) => /^(https?:|mailto:|#|\/)/i.test(href.trim());
93
+ renderer.link = ({ href, title, tokens }) => {
94
+ const text = renderer.parser.parseInline(tokens);
95
+ if (!isSafeHref(href))
96
+ return text;
97
+ const attrs = title ? ` title="${escapeHtml(title)}"` : '';
98
+ return `<a href="${escapeHtml(href)}"${attrs} rel="noopener noreferrer nofollow">${text}</a>`;
99
+ };
100
+ renderer.image = ({ href, title, text }) => {
101
+ if (!isSafeHref(href))
102
+ return escapeHtml(text);
103
+ const attrs = title ? ` title="${escapeHtml(title)}"` : '';
104
+ return `<img src="${escapeHtml(href)}" alt="${escapeHtml(text)}"${attrs} loading="lazy" />`;
105
+ };
106
+ return renderer;
107
+ }
108
+ /** Human duration — "13 minutes", never "12m 49s". */
109
+ export function formatDuration(ms) {
110
+ const minutes = Math.round(ms / 60_000);
111
+ if (minutes < 1)
112
+ return 'under a minute';
113
+ if (minutes < 60)
114
+ return `${minutes} minute${minutes === 1 ? '' : 's'}`;
115
+ const hours = Math.round(ms / 3_600_000);
116
+ if (hours < 24)
117
+ return `${hours} hour${hours === 1 ? '' : 's'}`;
118
+ const days = Math.round(ms / 86_400_000);
119
+ return `${days} day${days === 1 ? '' : 's'}`;
120
+ }
121
+ /**
122
+ * The chips shown under the title.
123
+ *
124
+ * Deliberately excludes `account` (an email — the publish-time sensitive-content
125
+ * scan rejects those, and correctly), `cwd`, and `machine`: a published transcript
126
+ * should not carry the operator's identity or local paths in its chrome. Host and
127
+ * repo already ride in the share object's provenance metadata, which is the right
128
+ * home for them.
129
+ */
130
+ export function buildChips(session) {
131
+ const chips = [{ label: 'agent', value: session.agent }];
132
+ if (session.model)
133
+ chips.push({ label: 'model', value: session.model });
134
+ if (session.mode)
135
+ chips.push({ label: 'mode', value: session.mode });
136
+ if (session.project)
137
+ chips.push({ label: 'project', value: session.project });
138
+ if (session.gitBranch)
139
+ chips.push({ label: 'branch', value: session.gitBranch });
140
+ if (session.ticketId)
141
+ chips.push({ label: 'ticket', value: session.ticketId });
142
+ const date = (session.timestamp || '').slice(0, 10);
143
+ if (date)
144
+ chips.push({ label: 'date', value: date });
145
+ if (session.durationMs)
146
+ chips.push({ label: 'duration', value: formatDuration(session.durationMs) });
147
+ if (session.messageCount)
148
+ chips.push({ label: 'turns', value: String(session.messageCount) });
149
+ if (session.toolCallCount)
150
+ chips.push({ label: 'tools', value: String(session.toolCallCount) });
151
+ return chips;
152
+ }
153
+ /** Longest title that stays one readable line in the header and the gallery. */
154
+ const TITLE_MAX = 90;
155
+ /**
156
+ * The page's `<title>` — `deriveLabel()` reads this as the share's gallery label.
157
+ *
158
+ * The document heading is derived from the session's first prompt, which in a real
159
+ * session is routinely a pasted URL plus a file path plus the actual request. Left
160
+ * whole it renders as a three-line wall above the transcript and an unreadable
161
+ * gallery row, so it is collapsed to one line and cut at a word boundary. `--label`
162
+ * overrides it outright.
163
+ */
164
+ export function sessionPageTitle(session, markdown) {
165
+ const heading = /^#\s+(.+)$/m.exec(markdown)?.[1]?.replace(/\s+/g, ' ').trim();
166
+ if (!heading)
167
+ return `${session.agent} session ${session.shortId || session.id}`;
168
+ if (heading.length <= TITLE_MAX)
169
+ return heading;
170
+ const cut = heading.slice(0, TITLE_MAX);
171
+ const lastSpace = cut.lastIndexOf(' ');
172
+ // A single unbroken token longer than the cap (a URL) has no boundary to cut on;
173
+ // a hard slice beats returning the whole thing.
174
+ return `${(lastSpace > TITLE_MAX / 2 ? cut.slice(0, lastSpace) : cut).trimEnd()}…`;
175
+ }
176
+ export function renderSessionHtmlDocument(session, markdown, options = {}) {
177
+ const title = sessionPageTitle(session, markdown);
178
+ // The <h1> is re-rendered as the page header, so drop it from the body to
179
+ // avoid printing the title twice.
180
+ const { text, blocks } = liftFoldBlocks(markdown.replace(/^#\s+.+\n/, ''));
181
+ const render = (md) => marked.parse(md, { renderer: safeRenderer(), async: false });
182
+ // Reconstruct each disclosure element around its own separately-rendered
183
+ // content, so the open/close pairing is ours and cannot be unbalanced by the
184
+ // transcript.
185
+ const body = blocks.reduce((html, inner, i) => html.replace(new RegExp(`<p>${FOLD_SENTINEL}${i}${FOLD_SENTINEL}</p>`),
186
+ // A FUNCTION replacement, never a string one. String.replace expands `$&`,
187
+ // `` $` ``, `$'` and `$n` inside a string replacement, and marked turns
188
+ // `& < > " '` into entities — so reasoning text containing `$'` (bash
189
+ // ANSI-C quoting), `$&` (sed), or `$<` (make) would splice the matched
190
+ // sentinel back into the page and destroy the character after it. A
191
+ // function's return value is inserted verbatim.
192
+ () => `<details><summary>Reasoning</summary>\n${render(inner)}</details>`), render(text));
193
+ const chips = buildChips(session)
194
+ .map((c) => `<span class="chip"><span class="k">${escapeHtml(c.label)}</span>${escapeHtml(c.value)}</span>`)
195
+ .join('\n ');
196
+ const redacted = options.redacted !== false;
197
+ return `<!DOCTYPE html>
198
+ <html lang="en" data-theme="auto">
199
+ <head>
200
+ <meta charset="utf-8" />
201
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
202
+ <meta name="robots" content="noindex" />
203
+ <title>${escapeHtml(title)}</title>
204
+ <style>
205
+ :root {
206
+ --bg: #0a0a0a; --panel: #121212; --border: #262626; --fg: #e5e5e5;
207
+ --dim: #737373; --accent: #a3e635; --quote: #1a1a1a;
208
+ }
209
+ html[data-theme="light"] {
210
+ --bg: #fafafa; --panel: #ffffff; --border: #e5e5e5; --fg: #171717;
211
+ --dim: #737373; --accent: #4d7c0f; --quote: #f5f5f5;
212
+ }
213
+ @media (prefers-color-scheme: light) {
214
+ html[data-theme="auto"] {
215
+ --bg: #fafafa; --panel: #ffffff; --border: #e5e5e5; --fg: #171717;
216
+ --dim: #737373; --accent: #4d7c0f; --quote: #f5f5f5;
217
+ }
218
+ }
219
+ * { box-sizing: border-box; }
220
+ body {
221
+ margin: 0; background: var(--bg); color: var(--fg);
222
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Inter, sans-serif;
223
+ font-size: 15px; line-height: 1.65;
224
+ }
225
+ header {
226
+ border-bottom: 1px solid var(--border); padding: 28px 20px 20px;
227
+ }
228
+ header .inner, main { max-width: 900px; margin: 0 auto; }
229
+ header .mark {
230
+ color: var(--accent); font-weight: 700; letter-spacing: .5px; font-size: 12px;
231
+ text-transform: uppercase; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace;
232
+ }
233
+ header h1 { font-size: 24px; line-height: 1.3; margin: 10px 0 14px; }
234
+ .chips { display: flex; flex-wrap: wrap; gap: 6px; }
235
+ .chip {
236
+ font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; font-size: 11px;
237
+ color: var(--fg); background: var(--panel); border: 1px solid var(--border);
238
+ border-radius: 10px; padding: 2px 9px;
239
+ }
240
+ .chip .k { color: var(--dim); margin-right: 6px; }
241
+ .toggle {
242
+ float: right; cursor: pointer; background: none; border: 1px solid var(--border);
243
+ color: var(--dim); border-radius: 6px; padding: 2px 8px; font-size: 14px;
244
+ }
245
+ main { padding: 24px 20px 64px; }
246
+ h2 {
247
+ font-size: 13px; color: var(--accent); border-bottom: 1px solid var(--border);
248
+ padding-bottom: 6px; margin: 36px 0 14px; text-transform: uppercase;
249
+ letter-spacing: 1px; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace;
250
+ }
251
+ h3 { font-size: 15px; margin: 26px 0 8px; }
252
+ h4 { font-size: 13px; color: var(--dim); margin: 20px 0 6px; font-weight: 600; }
253
+ a { color: var(--accent); }
254
+ blockquote {
255
+ margin: 0 0 20px; padding: 12px 16px; background: var(--quote);
256
+ border-left: 2px solid var(--border); border-radius: 0 6px 6px 0; color: var(--dim);
257
+ }
258
+ blockquote p { margin: 0 0 6px; }
259
+ blockquote p:last-child { margin: 0; }
260
+ pre {
261
+ background: var(--panel); border: 1px solid var(--border); border-radius: 6px;
262
+ padding: 12px 14px; overflow-x: auto;
263
+ }
264
+ code {
265
+ font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; font-size: 12.5px;
266
+ }
267
+ :not(pre) > code {
268
+ background: var(--panel); border: 1px solid var(--border);
269
+ border-radius: 4px; padding: 1px 5px;
270
+ }
271
+ hr { border: none; border-top: 1px solid var(--border); margin: 32px 0; }
272
+ details { margin: 0 0 12px; }
273
+ summary { cursor: pointer; color: var(--dim); font-size: 13px; }
274
+ table { border-collapse: collapse; width: 100%; font-size: 13px; }
275
+ th, td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; }
276
+ footer {
277
+ max-width: 900px; margin: 0 auto; padding: 0 20px 48px;
278
+ color: var(--dim); font-size: 12px;
279
+ font-family: ui-monospace, "JetBrains Mono", Menlo, monospace;
280
+ }
281
+ footer a { color: var(--dim); }
282
+ </style>
283
+ </head>
284
+ <body>
285
+ <header>
286
+ <div class="inner">
287
+ <button class="toggle" id="theme" title="Toggle light and dark">&#9689;</button>
288
+ <div class="mark">agents session</div>
289
+ <h1>${escapeHtml(title)}</h1>
290
+ <div class="chips">
291
+ ${chips}
292
+ </div>
293
+ </div>
294
+ </header>
295
+ <main>
296
+ ${body}
297
+ </main>
298
+ <footer>
299
+ ${redacted ? 'Secret-redacted transcript' : 'Unredacted transcript'} rendered by
300
+ <a href="https://agents-cli.sh">agents-cli</a> &middot; <code>agents sessions share</code>
301
+ </footer>
302
+ <script>
303
+ (function () {
304
+ var root = document.documentElement;
305
+ var saved = null;
306
+ try { saved = localStorage.getItem('agents-share-theme'); } catch (e) {}
307
+ if (saved) root.setAttribute('data-theme', saved);
308
+ document.getElementById('theme').addEventListener('click', function () {
309
+ var dark = getComputedStyle(root).getPropertyValue('--bg').trim() === '#0a0a0a';
310
+ var next = dark ? 'light' : 'dark';
311
+ root.setAttribute('data-theme', next);
312
+ try { localStorage.setItem('agents-share-theme', next); } catch (e) {}
313
+ });
314
+ })();
315
+ </script>
316
+ </body>
317
+ </html>
318
+ `;
319
+ }
@@ -13,6 +13,8 @@
13
13
  * version home. It never overwrites a value the target already has: scalars
14
14
  * keep the target's value, objects merge recursively, arrays union. That makes
15
15
  * the operation idempotent and safe to run on every `agents add` / `agents use`.
16
+ * (One scoped exception: the 'claude-trust' strategy promotes a stamped-default
17
+ * `hasTrustDialogAccepted: false` to `true` — see the note on that entry.)
16
18
  */
17
19
  import type { AgentId } from './types.js';
18
20
  export interface CarryForwardResult {