@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
@@ -13,16 +13,26 @@
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 * as fs from 'fs';
18
20
  import * as path from 'path';
19
21
  import * as TOML from 'smol-toml';
22
+ import { atomicWriteFileSync } from './fs-atomic.js';
20
23
  import { getBackupsDir } from './state.js';
21
24
  const SETTINGS_MANIFEST = {
22
25
  claude: [
23
26
  { rel: '.claude/settings.json', strategy: 'json-merge' },
24
27
  { rel: '.claude/settings.local.json', strategy: 'copy-if-absent' },
25
28
  { rel: '.claude/keybindings.json', strategy: 'copy-if-absent' },
29
+ // `.claude.json` holds the login (oauthAccount) and per-session stats, so it
30
+ // must never merge wholesale — but it is also where Claude records workspace
31
+ // trust (`projects[<path>].hasTrustDialogAccepted`). Without carrying that,
32
+ // every newly pinned version re-shows the trust dialog once per project
33
+ // (issue #2776). The 'claude-trust' strategy projects ONLY the trust flags
34
+ // out of the source file; credentials and stats stay per-version.
35
+ { rel: '.claude.json', strategy: 'claude-trust' },
26
36
  ],
27
37
  codex: [
28
38
  {
@@ -33,9 +43,10 @@ const SETTINGS_MANIFEST = {
33
43
  // `.codex/auth.json` is deliberately NOT carried forward. Copying it seeded
34
44
  // every new Codex version with the current default's ChatGPT token, so two
35
45
  // installed versions always reported the same account and could never sign
36
- // into separate accounts. Claude omits its credential (`.claude.json`) for
37
- // the same reason a version home holds its own login, keeping accounts
38
- // per-version. A fresh Codex version installs signed-out; run `codex login`
46
+ // into separate accounts. Claude never merges `.claude.json` for the same
47
+ // reason (its 'claude-trust' entry extracts only the trust flags) a
48
+ // version home holds its own login, keeping accounts per-version. A fresh
49
+ // Codex version installs signed-out; run `codex login`
39
50
  // (or `agents run codex --version <v>`) inside it to authenticate.
40
51
  { rel: '.codex/instructions.md', strategy: 'copy-if-absent' },
41
52
  { rel: '.codex/hooks.json', strategy: 'copy-if-absent' },
@@ -70,6 +81,21 @@ export function fillGaps(target, source) {
70
81
  }
71
82
  return out;
72
83
  }
84
+ /**
85
+ * Project paths the source `.claude.json` records an accepted trust dialog for.
86
+ * Only an explicit `true` counts: Claude Code never persists a decline (the
87
+ * dialog exits without writing), so `false` is only ever the stamped default —
88
+ * not a user decision worth propagating. Verified against Claude Code
89
+ * 2.1.219/2.1.220 (decline paths exit via code 1 / code 0 without a config
90
+ * write); if a future Claude Code starts persisting declines, this premise —
91
+ * and the false→true promotion in the 'claude-trust' case — must be revisited.
92
+ */
93
+ function trustedClaudeProjects(source) {
94
+ const projects = isPlainObject(source.projects) ? source.projects : {};
95
+ return Object.entries(projects)
96
+ .filter(([, project]) => isPlainObject(project) && project.hasTrustDialogAccepted === true)
97
+ .map(([projectPath]) => projectPath);
98
+ }
73
99
  function stripStateKeys(obj, stateKeys) {
74
100
  if (!stateKeys?.length)
75
101
  return obj;
@@ -128,6 +154,58 @@ export function carryForwardSettings(agent, fromHome, toHome) {
128
154
  result.applied.push(entry.rel);
129
155
  break;
130
156
  }
157
+ case 'claude-trust': {
158
+ // Projection, not a merge: pull ONLY `projects[<path>].hasTrustDialogAccepted`
159
+ // out of the source `.claude.json`. Everything else in that file
160
+ // (oauthAccount, onboarding state, per-session stats) stays per-version.
161
+ // Trust granted anywhere wins over the target's stamped-default `false`
162
+ // (headless runs create project entries with the flag unset-as-false
163
+ // without ever showing the dialog), but a target entry's other keys
164
+ // are preserved untouched.
165
+ const source = JSON.parse(fs.readFileSync(sourcePath, 'utf-8'));
166
+ const trusted = trustedClaudeProjects(source);
167
+ if (trusted.length === 0)
168
+ break;
169
+ const targetExists = fs.existsSync(targetPath);
170
+ const parsedTarget = targetExists
171
+ ? JSON.parse(fs.readFileSync(targetPath, 'utf-8'))
172
+ : {};
173
+ // A target that isn't an object (or whose `projects` isn't) is not
174
+ // ours to repair — skip rather than clobber it with a rebuilt shape.
175
+ if (!isPlainObject(parsedTarget))
176
+ break;
177
+ const targetObj = parsedTarget;
178
+ if ('projects' in targetObj && !isPlainObject(targetObj.projects))
179
+ break;
180
+ const targetProjects = isPlainObject(targetObj.projects) ? { ...targetObj.projects } : {};
181
+ let changed = false;
182
+ for (const projectPath of trusted) {
183
+ const existing = isPlainObject(targetProjects[projectPath])
184
+ ? targetProjects[projectPath]
185
+ : {};
186
+ if (existing.hasTrustDialogAccepted === true)
187
+ continue;
188
+ targetProjects[projectPath] = { ...existing, hasTrustDialogAccepted: true };
189
+ changed = true;
190
+ }
191
+ if (!changed)
192
+ break;
193
+ if (targetExists) {
194
+ backupFile(backupRoot, toHome, entry.rel);
195
+ result.backupDir = backupRoot;
196
+ }
197
+ else {
198
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
199
+ }
200
+ // Atomic (tmp + rename): a running Claude session rewrites this exact
201
+ // file (it holds the login and session stats), so a plain write risks
202
+ // a reader seeing a partial file. The OUTSIDE `.claude.json` is the
203
+ // real file (the INSIDE `.claude/.claude.json` symlink resolves to it
204
+ // and survives the rename).
205
+ atomicWriteFileSync(targetPath, JSON.stringify({ ...targetObj, projects: targetProjects }, null, 2) + '\n');
206
+ result.applied.push(entry.rel);
207
+ break;
208
+ }
131
209
  case 'json-merge':
132
210
  case 'toml-merge': {
133
211
  const parse = entry.strategy === 'json-merge'
@@ -129,6 +129,25 @@ export declare function assertMetadataSize(customMetadata: Record<string, string
129
129
  * an unhandled `TypeError: Invalid value` and crashes the publish outright.
130
130
  */
131
131
  export declare function sanitizeLabel(text: string): string;
132
+ /**
133
+ * Make a free-text value safe to put in an HTTP header.
134
+ *
135
+ * `fetch` encodes header values as a **ByteString**, so any code point above 255
136
+ * throws `TypeError: Cannot convert argument to a ByteString` — an unhandled
137
+ * crash with a stack trace, mid-publish, after the body has already been read.
138
+ * Reproduced by publishing a session whose title ended in `…` (U+2026), and
139
+ * reachable by any emoji, curly quote, accented name, or CJK text in a `--label`,
140
+ * a `--meta` value, or a repo name.
141
+ *
142
+ * The transliterations above cover what actually shows up; anything else outside
143
+ * latin1 is dropped, and a value that transliterates to nothing at all (a title
144
+ * written entirely in a non-latin script) degrades to a marker rather than an
145
+ * empty header. Lossy on purpose: carrying full Unicode needs percent-encoding
146
+ * here AND a matching decode in the Worker, which every already-deployed Worker
147
+ * would render as `%E2%80%A6` until its operator ran `agents artifacts share
148
+ * update` — tracked as RUSH-2786.
149
+ */
150
+ export declare function toHeaderValue(text: string): string;
132
151
  /**
133
152
  * Best-effort human title when `--label` is omitted: the HTML `<title>`, else a
134
153
  * Markdown frontmatter `title:`, else the filename. Always returns something —
@@ -150,6 +169,25 @@ export interface SensitiveHit {
150
169
  /** Short redacted sample so the error names *what* was found without dumping it. */
151
170
  sample: string;
152
171
  }
172
+ /**
173
+ * Mask every email address in `text`, so a page can be published without tripping
174
+ * {@link scanShareContent}.
175
+ *
176
+ * Lives here, beside the scanner, rather than in `lib/redact.ts`: it exists solely
177
+ * to satisfy this gate, and sharing {@link EMAIL_RE} is what makes the masking
178
+ * *sufficient* to clear it rather than merely reducing the hit count. Two copies of
179
+ * the pattern in two modules would drift, and the failure mode of that drift is a
180
+ * refused publish at runtime.
181
+ *
182
+ * Deliberately NOT part of `redactSecrets`: an email is not a credential, and a
183
+ * transcript rendered for a private gist or local review reads better with the real
184
+ * author addresses intact. It only becomes a leak once the text is PUBLISHED — a
185
+ * world-readable page carrying seven of them is the RUSH-2428 incident.
186
+ *
187
+ * The whole address goes, domain included — a personal domain identifies its owner
188
+ * as surely as the local part does, and keeping it would buy the reader nothing.
189
+ */
190
+ export declare function redactEmails(text: string): string;
153
191
  /**
154
192
  * Scan a text body for email addresses and credential-shaped strings. Returns
155
193
  * the first few hits (deduped by kind+sample). Binary / non-text bodies yield
@@ -91,6 +91,49 @@ export function assertMetadataSize(customMetadata) {
91
91
  export function sanitizeLabel(text) {
92
92
  return text.replace(/\s+/g, ' ').trim();
93
93
  }
94
+ /**
95
+ * Typographic characters that reach a header constantly — a curly quote from a
96
+ * pasted prompt, an em dash from prose, the ellipsis a truncated title ends on —
97
+ * each mapped to the ASCII form a reader loses nothing by seeing.
98
+ */
99
+ const HEADER_TRANSLITERATIONS = [
100
+ [/[‘’‚‛]/g, "'"],
101
+ [/[“”„‟]/g, '"'],
102
+ [/[–—―]/g, '-'],
103
+ [/…/g, '...'],
104
+ [/[    ]/g, ' '],
105
+ [/[•·]/g, '-'],
106
+ ];
107
+ /**
108
+ * Make a free-text value safe to put in an HTTP header.
109
+ *
110
+ * `fetch` encodes header values as a **ByteString**, so any code point above 255
111
+ * throws `TypeError: Cannot convert argument to a ByteString` — an unhandled
112
+ * crash with a stack trace, mid-publish, after the body has already been read.
113
+ * Reproduced by publishing a session whose title ended in `…` (U+2026), and
114
+ * reachable by any emoji, curly quote, accented name, or CJK text in a `--label`,
115
+ * a `--meta` value, or a repo name.
116
+ *
117
+ * The transliterations above cover what actually shows up; anything else outside
118
+ * latin1 is dropped, and a value that transliterates to nothing at all (a title
119
+ * written entirely in a non-latin script) degrades to a marker rather than an
120
+ * empty header. Lossy on purpose: carrying full Unicode needs percent-encoding
121
+ * here AND a matching decode in the Worker, which every already-deployed Worker
122
+ * would render as `%E2%80%A6` until its operator ran `agents artifacts share
123
+ * update` — tracked as RUSH-2786.
124
+ */
125
+ export function toHeaderValue(text) {
126
+ let safe = text;
127
+ for (const [pattern, replacement] of HEADER_TRANSLITERATIONS)
128
+ safe = safe.replace(pattern, replacement);
129
+ safe = safe.replace(/[^\x20-\x7E\xA0-\xFF]/g, '').replace(/\s+/g, ' ').trim();
130
+ // An input that was empty to begin with stays empty — `--meta note=` means an
131
+ // empty note, not an unnamed one. The marker is only for a value that HAD
132
+ // content and lost all of it to the latin1 fold.
133
+ if (safe)
134
+ return safe;
135
+ return text.trim() ? '(unnamed)' : '';
136
+ }
94
137
  /**
95
138
  * Best-effort human title when `--label` is omitted: the HTML `<title>`, else a
96
139
  * Markdown frontmatter `title:`, else the filename. Always returns something —
@@ -141,6 +184,27 @@ export function resolveExpire(spec) {
141
184
  }
142
185
  /** Email addresses — the RUSH-2428 incident page carried seven of these. */
143
186
  const EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
187
+ /**
188
+ * Mask every email address in `text`, so a page can be published without tripping
189
+ * {@link scanShareContent}.
190
+ *
191
+ * Lives here, beside the scanner, rather than in `lib/redact.ts`: it exists solely
192
+ * to satisfy this gate, and sharing {@link EMAIL_RE} is what makes the masking
193
+ * *sufficient* to clear it rather than merely reducing the hit count. Two copies of
194
+ * the pattern in two modules would drift, and the failure mode of that drift is a
195
+ * refused publish at runtime.
196
+ *
197
+ * Deliberately NOT part of `redactSecrets`: an email is not a credential, and a
198
+ * transcript rendered for a private gist or local review reads better with the real
199
+ * author addresses intact. It only becomes a leak once the text is PUBLISHED — a
200
+ * world-readable page carrying seven of them is the RUSH-2428 incident.
201
+ *
202
+ * The whole address goes, domain included — a personal domain identifies its owner
203
+ * as surely as the local part does, and keeping it would buy the reader nothing.
204
+ */
205
+ export function redactEmails(text) {
206
+ return text.replace(EMAIL_RE, '[EMAIL]');
207
+ }
144
208
  /**
145
209
  * Credential-shaped strings that an agent routinely dumps into reports: GitHub
146
210
  * PATs, OpenAI/Anthropic/etc. API keys, AWS access keys, Slack tokens, and a
@@ -430,20 +494,29 @@ export async function publishToEndpoint(filePath, endpoint, opts = {}) {
430
494
  h['x-share-expires-at'] = expiresAt;
431
495
  if (unlisted)
432
496
  h['x-share-visibility'] = 'unlisted';
497
+ // Every free-text header goes through toHeaderValue: a non-latin1 code point
498
+ // anywhere in a label, a repo name, or a --meta value throws inside fetch and
499
+ // crashes the publish outright.
433
500
  if (provenance.agent)
434
- h['x-share-agent'] = provenance.agent;
501
+ h['x-share-agent'] = toHeaderValue(provenance.agent);
435
502
  if (provenance.session)
436
- h['x-share-session'] = provenance.session;
503
+ h['x-share-session'] = toHeaderValue(provenance.session);
437
504
  if (provenance.host)
438
- h['x-share-host'] = provenance.host;
505
+ h['x-share-host'] = toHeaderValue(provenance.host);
439
506
  if (provenance.repo)
440
- h['x-share-repo'] = provenance.repo;
507
+ h['x-share-repo'] = toHeaderValue(provenance.repo);
441
508
  if (provenance.date)
442
- h['x-share-date'] = provenance.date;
443
- h['x-share-label'] = label;
509
+ h['x-share-date'] = toHeaderValue(provenance.date);
510
+ h['x-share-label'] = toHeaderValue(label);
444
511
  h['x-share-label-source'] = labelSource;
445
- if (Object.keys(meta).length > 0)
446
- h['x-share-meta'] = JSON.stringify(meta);
512
+ // Per VALUE, before JSON.stringify folding the serialized form would rewrite
513
+ // a curly quote inside a value into a bare `"`, which is structural in JSON and
514
+ // makes the Worker's JSON.parse throw. It swallows that error, so every --meta
515
+ // key would silently vanish on a 200.
516
+ if (Object.keys(meta).length > 0) {
517
+ const headerMeta = Object.fromEntries(Object.entries(meta).map(([k, v]) => [toHeaderValue(k), toHeaderValue(v)]));
518
+ h['x-share-meta'] = JSON.stringify(headerMeta);
519
+ }
447
520
  if (opts.noRevision)
448
521
  h['x-share-no-revision'] = '1';
449
522
  return h;
@@ -42,7 +42,7 @@ import type { SyncManifest } from './types.js';
42
42
  * - hooks — a version-home hook script is coupled to a settings.json /
43
43
  * hooks.json REGISTRATION. Pruning the last hook to zero would delete the
44
44
  * script but must also GC its registration in the same pass, and that lands
45
- * in `src/lib/hooks.ts` — a Windows-portable-path surface the CI windows leg
45
+ * in `src/lib/hooks/install.ts` — a Windows-portable-path surface the CI windows leg
46
46
  * gates on. Split to RUSH-2456 so this PR stays Linux-only-green; hook FILES
47
47
  * are still reconciled by the in-write orphan sweep (versions.ts, gated on
48
48
  * `hooksToSync > 0`).
@@ -5,7 +5,7 @@ import { getWriter, getDetector } from './registry.js';
5
5
  * - hooks — a version-home hook script is coupled to a settings.json /
6
6
  * hooks.json REGISTRATION. Pruning the last hook to zero would delete the
7
7
  * script but must also GC its registration in the same pass, and that lands
8
- * in `src/lib/hooks.ts` — a Windows-portable-path surface the CI windows leg
8
+ * in `src/lib/hooks/install.ts` — a Windows-portable-path surface the CI windows leg
9
9
  * gates on. Split to RUSH-2456 so this PR stays Linux-only-green; hook FILES
10
10
  * are still reconciled by the in-write orphan sweep (versions.ts, gated on
11
11
  * `hooksToSync > 0`).
@@ -9,7 +9,7 @@ import { agentConfigDirName } from '../../agents.js';
9
9
  import * as path from 'path';
10
10
  import { capableAgents } from '../../capabilities.js';
11
11
  import { safeJoin } from '../../paths.js';
12
- import { registerHooksToSettings } from '../../hooks.js';
12
+ import { registerHooksToSettings } from '../../hooks/install.js';
13
13
  import { resolveHookSource } from './sources.js';
14
14
  import { lazyAgentMap } from './lazy-map.js';
15
15
  function removePath(target) {
@@ -20,7 +20,8 @@ export declare const KNOWN_TOP_LEVEL_COMMANDS: ReadonlySet<string>;
20
20
  * `set` moved under `agents models`/`agents config` (RUSH-2579); `share` moved
21
21
  * under `agents artifacts share` (RUSH-2580). login/logout/budget/bench/mine/
22
22
  * cost/output/profiles/snapshot/cp/resume/roster moved under nested homes
23
- * (cli-surface-consolidate).
23
+ * (cli-surface-consolidate). `timeline` was removed as a duplicated surface —
24
+ * use `agents feed --filter updates` (RUSH-2692).
24
25
  */
25
26
  export declare const RETIRED_TOP_LEVEL_COMMANDS: ReadonlySet<string>;
26
27
  export declare function isKnownTopLevelCommand(name: string): boolean;
@@ -1,5 +1,5 @@
1
1
  const LOADED_COMMAND_NAMES = [
2
- 'accounts', 'view', 'inspect', 'feedback', 'commands', 'hooks', 'skills', 'rules', 'memory',
2
+ 'accounts', 'auth', 'org', 'view', 'inspect', 'feedback', 'commands', 'hooks', 'skills', 'rules', 'memory',
3
3
  'permissions', 'mcp', 'clis', 'subagents', 'plugins', 'workflows', 'add', 'use', 'list',
4
4
  'remove', 'rm', 'purge', 'update', 'prune', 'import', 'registry', 'search', 'install',
5
5
  'routines', 'monitors', 'projects', 'run', 'open', 'reconnect', 'fork', 'config',
@@ -9,7 +9,7 @@ const LOADED_COMMAND_NAMES = [
9
9
  'alias', 'pty', 'tmux', 'watchdog', 'browser', 'computer', 'logs', 'events',
10
10
  'ssh', 'devices', 'fleet', 'repos', 'repo', 'setup', 'uninstall', 'upgrade', 'sessions',
11
11
  'teams', 'tickets', 'cloud', 'message', 'send', 'notify', 'feed', 'inbox',
12
- 'timeline', 'mailboxes', 'mailbox', 'serve', 'artifacts', 'unshare', 'audit', 'webhooks',
12
+ 'mailboxes', 'mailbox', 'serve', 'artifacts', 'unshare', 'audit', 'webhooks',
13
13
  'humans', 'daemon',
14
14
  ];
15
15
  const INLINE_COMMAND_NAMES = [
@@ -40,7 +40,8 @@ export const KNOWN_TOP_LEVEL_COMMANDS = new Set([
40
40
  * `set` moved under `agents models`/`agents config` (RUSH-2579); `share` moved
41
41
  * under `agents artifacts share` (RUSH-2580). login/logout/budget/bench/mine/
42
42
  * cost/output/profiles/snapshot/cp/resume/roster moved under nested homes
43
- * (cli-surface-consolidate).
43
+ * (cli-surface-consolidate). `timeline` was removed as a duplicated surface —
44
+ * use `agents feed --filter updates` (RUSH-2692).
44
45
  */
45
46
  export const RETIRED_TOP_LEVEL_COMMANDS = new Set([
46
47
  'webhook',
@@ -58,6 +59,7 @@ export const RETIRED_TOP_LEVEL_COMMANDS = new Set([
58
59
  'roster',
59
60
  'set',
60
61
  'share',
62
+ 'timeline',
61
63
  ]);
62
64
  export function isKnownTopLevelCommand(name) {
63
65
  return KNOWN_TOP_LEVEL_COMMANDS.has(name);
@@ -34,6 +34,7 @@ import { resolveRemoteOsSync } from '../hosts/remote-os.js';
34
34
  import { pullRemoteLogDelta, REMOTE_MIRROR_MAX_BYTES } from '../hosts/progress.js';
35
35
  import { createRemoteWorktree, ensureRemoteRepo } from './remoteWorktree.js';
36
36
  import { getTeam, isTeamDisbanded } from './registry.js';
37
+ import { atomicWriteJsonSync } from '../fs-atomic.js';
37
38
  import { resolvePlacement, classifyExclusions, NoViableDeviceError } from './scheduler.js';
38
39
  import { probePoolSignals } from './placement-probe.js';
39
40
  import { readMaxConcurrentCaps } from '../device-config.js';
@@ -321,24 +322,6 @@ function extractTimestamp(raw) {
321
322
  }
322
323
  return null;
323
324
  }
324
- /**
325
- * Atomic JSON write: writes to a unique sibling tmp file then renames over the
326
- * target. rename(2) is atomic on POSIX, so a process killed mid-write leaves
327
- * either the previous valid meta.json or the new one, never a torn,
328
- * unparseable file (RUSH-2429). Mirrors the identical pattern already used by
329
- * `teams/registry.ts`'s `atomicWriteJson`.
330
- */
331
- async function atomicWriteJson(p, data) {
332
- const tmp = `${p}.tmp.${process.pid}.${randomUUID()}`;
333
- await fs.writeFile(tmp, JSON.stringify(data, null, 2));
334
- try {
335
- await fs.rename(tmp, p);
336
- }
337
- catch (err) {
338
- await fs.unlink(tmp).catch(() => { });
339
- throw err;
340
- }
341
- }
342
325
  /** Resolve a mode string to a validated Mode, falling back to the given default. */
343
326
  export function resolveMode(requestedMode, defaultMode = 'plan') {
344
327
  const normalizedDefault = normalizeModeValue(defaultMode);
@@ -1011,7 +994,7 @@ export class AgentProcess {
1011
994
  remote_log_offset: this.remoteLogOffset,
1012
995
  };
1013
996
  const metaPath = await this.getMetaPath();
1014
- await atomicWriteJson(metaPath, meta);
997
+ atomicWriteJsonSync(metaPath, meta);
1015
998
  }
1016
999
  /**
1017
1000
  * Rename an unreadable meta.json out of the way so it stops silently
@@ -9,34 +9,28 @@
9
9
  import { extractFileOpsFromBash } from './file_ops.js';
10
10
  const claudeToolUseMap = new Map();
11
11
  const droidToolUseMap = new Map();
12
+ /**
13
+ * Registry-dispatch table: each teams `AgentType` to its live team-event
14
+ * normalizer. Replaces the per-type name-chain — the harness axis of Move 3.
15
+ * warp (and any unlisted type) is absent, so it falls through to the generic
16
+ * unknown-event shape below, exactly as the old `else` did.
17
+ */
18
+ const TEAM_EVENT_NORMALIZERS = {
19
+ codex: normalizeCodex,
20
+ cursor: normalizeCursor,
21
+ gemini: normalizeGemini,
22
+ claude: normalizeClaude,
23
+ opencode: normalizeOpencode,
24
+ grok: normalizeGrok,
25
+ antigravity: normalizeAntigravity,
26
+ kimi: normalizeKimi,
27
+ droid: normalizeDroid,
28
+ };
12
29
  /** Normalize a raw JSON event from any agent type into an array of unified event objects. */
13
30
  export function normalizeEvents(agentType, raw) {
14
- if (agentType === 'codex') {
15
- return normalizeCodex(raw);
16
- }
17
- else if (agentType === 'cursor') {
18
- return normalizeCursor(raw);
19
- }
20
- else if (agentType === 'gemini') {
21
- return normalizeGemini(raw);
22
- }
23
- else if (agentType === 'claude') {
24
- return normalizeClaude(raw);
25
- }
26
- else if (agentType === 'opencode') {
27
- return normalizeOpencode(raw);
28
- }
29
- else if (agentType === 'grok') {
30
- return normalizeGrok(raw);
31
- }
32
- else if (agentType === 'antigravity') {
33
- return normalizeAntigravity(raw);
34
- }
35
- else if (agentType === 'kimi') {
36
- return normalizeKimi(raw);
37
- }
38
- else if (agentType === 'droid') {
39
- return normalizeDroid(raw);
31
+ const normalizer = TEAM_EVENT_NORMALIZERS[agentType];
32
+ if (normalizer) {
33
+ return normalizer(raw);
40
34
  }
41
35
  const timestamp = new Date().toISOString();
42
36
  return [{
@@ -10,32 +10,13 @@
10
10
  import * as fs from 'fs/promises';
11
11
  import * as fsSync from 'fs';
12
12
  import * as path from 'path';
13
- import { randomBytes } from 'crypto';
14
13
  import lockfile from 'proper-lockfile';
15
14
  import { getTeamsRegistryPath } from '../state.js';
16
15
  import { emit } from '../feed/events.js';
16
+ import { atomicWriteJsonSync } from '../fs-atomic.js';
17
17
  async function registryPath() {
18
18
  return getTeamsRegistryPath();
19
19
  }
20
- /**
21
- * Atomic JSON write: writes to a unique sibling tmp file then renames over
22
- * the target. rename(2) is atomic on POSIX, so a crashed write leaves the
23
- * old file untouched instead of producing a half-written registry that
24
- * loadTeams() would reject.
25
- */
26
- async function atomicWriteJson(p, data) {
27
- await fs.mkdir(path.dirname(p), { recursive: true });
28
- const tmp = `${p}.tmp.${process.pid}.${randomBytes(4).toString('hex')}`;
29
- const body = JSON.stringify(data, null, 2);
30
- await fs.writeFile(tmp, body);
31
- try {
32
- await fs.rename(tmp, p);
33
- }
34
- catch (err) {
35
- await fs.unlink(tmp).catch(() => { });
36
- throw err;
37
- }
38
- }
39
20
  /**
40
21
  * Run `fn` while holding an exclusive cross-process lock on the registry
41
22
  * file. proper-lockfile requires the target to exist, so we touch it first.
@@ -91,7 +72,7 @@ export async function loadTeams() {
91
72
  }
92
73
  async function saveTeams(reg) {
93
74
  const p = await registryPath();
94
- await atomicWriteJson(p, reg);
75
+ atomicWriteJsonSync(p, reg);
95
76
  }
96
77
  /** Create a new team. Throws if a team with the same name already exists. */
97
78
  export async function createTeam(name, options) {
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { JobConfig, RunMeta, WebhookContext } from '../scheduling/routines.js';
9
9
  import type { AgentId } from '../types.js';
10
- import type { IncomingWebhook, WebhookSource } from './webhook.js';
10
+ import type { IncomingWebhook, SlackPayload, WebhookSource } from './webhook.js';
11
11
  export interface WebhookHandler {
12
12
  name: string;
13
13
  enabled?: boolean;
@@ -21,6 +21,11 @@ export interface WebhookHandler {
21
21
  label?: string;
22
22
  repo?: string;
23
23
  branch?: string;
24
+ /** Slack (source: slack) only — match one slash command, e.g. `/agents`. */
25
+ command?: string;
26
+ /** Slack (source: slack) only — restrict to one channel id (`C0…`). Lets a
27
+ * channel imply a project via a per-channel handler. */
28
+ channel?: string;
24
29
  /**
25
30
  * Where to run the action. A device name (`yosemite-s0`) runs there over SSH;
26
31
  * `fleet` picks any eligible online worker; `fleet/<platform>` (or
@@ -92,10 +97,40 @@ export declare function resolveHandlerHost(host: string | undefined): HandlerHos
92
97
  export declare function listHandlers(cwd?: string): WebhookHandler[];
93
98
  /** Pure matcher: does this handler match the incoming webhook? */
94
99
  export declare function handlerMatchesWebhook(handler: WebhookHandler, webhook: IncomingWebhook): boolean;
100
+ /**
101
+ * The `{{slack.*}}` substitution namespace: a Slack message split into an
102
+ * agent-actionable project + prompt, plus the coordinates a reply threads into.
103
+ */
104
+ export interface SlackMessageContext {
105
+ /** The message with a leading bot mention stripped. */
106
+ text: string;
107
+ /** The `PROJECT:` token at the head of the message, or '' when absent. */
108
+ project: string;
109
+ /** The request — the text after `PROJECT:`, or the whole message. */
110
+ prompt: string;
111
+ /** Channel id to reply into (`C0…`). */
112
+ channel: string;
113
+ /** Thread ts to reply into (empty for a slash command → reply to the channel). */
114
+ thread_ts: string;
115
+ /** Invoking user id (`U0…`). */
116
+ user: string;
117
+ /** Slash command name (`/agents`), or '' for an event delivery. */
118
+ command: string;
119
+ }
120
+ /**
121
+ * Split a Slack message into a `{{slack.*}}` context. Strips a leading bot
122
+ * mention (`<@U0BOT> …`), then reads an optional `PROJECT:` prefix — a single
123
+ * bare token followed by a colon and a space — as the project, with the
124
+ * remainder the prompt. `AGI: rebase my PR` → project `AGI`, prompt
125
+ * `rebase my PR`; a message with no such prefix leaves `project` empty and the
126
+ * whole text as the prompt. The token pattern forbids slashes, so a project
127
+ * value can never carry a path into a `cwd`/`project` substitution.
128
+ */
129
+ export declare function parseSlackMessage(payload: SlackPayload): SlackMessageContext;
95
130
  /**
96
131
  * Build the variable-substitution context for a webhook. Linear events expose
97
132
  * `issue` and `updatedFrom`; GitHub events expose `repository`, `pull_request`,
98
- * and `issue`.
133
+ * and `issue`; Slack events expose the `{{slack.*}}` namespace above.
99
134
  */
100
135
  export declare function buildWebhookContext(webhook: IncomingWebhook): WebhookContext;
101
136
  export interface ExecuteHandlerOptions {