@phnx-labs/agents-cli 1.20.72 → 1.20.74

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 (234) hide show
  1. package/CHANGELOG.md +395 -0
  2. package/README.md +40 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +12 -0
  5. package/dist/commands/activity.js +68 -0
  6. package/dist/commands/apply.d.ts +13 -0
  7. package/dist/commands/apply.js +28 -3
  8. package/dist/commands/browser.js +1 -1
  9. package/dist/commands/cloud.js +21 -11
  10. package/dist/commands/doctor.js +21 -5
  11. package/dist/commands/events.d.ts +11 -10
  12. package/dist/commands/events.js +30 -21
  13. package/dist/commands/exec.d.ts +34 -0
  14. package/dist/commands/exec.js +337 -39
  15. package/dist/commands/export.d.ts +11 -0
  16. package/dist/commands/export.js +215 -0
  17. package/dist/commands/feed.d.ts +13 -0
  18. package/dist/commands/feed.js +130 -29
  19. package/dist/commands/hq.d.ts +2 -0
  20. package/dist/commands/hq.js +58 -0
  21. package/dist/commands/import.js +51 -3
  22. package/dist/commands/lease.d.ts +19 -0
  23. package/dist/commands/lease.js +138 -11
  24. package/dist/commands/login.d.ts +2 -0
  25. package/dist/commands/login.js +123 -0
  26. package/dist/commands/logs.js +1 -1
  27. package/dist/commands/mine.d.ts +27 -0
  28. package/dist/commands/mine.js +207 -0
  29. package/dist/commands/monitors.js +79 -49
  30. package/dist/commands/plugins.js +1 -0
  31. package/dist/commands/profiles.js +158 -0
  32. package/dist/commands/repo.js +2 -60
  33. package/dist/commands/resources.d.ts +5 -0
  34. package/dist/commands/resources.js +95 -0
  35. package/dist/commands/routines.d.ts +2 -0
  36. package/dist/commands/routines.js +139 -37
  37. package/dist/commands/secrets.js +234 -47
  38. package/dist/commands/send.d.ts +15 -0
  39. package/dist/commands/send.js +62 -0
  40. package/dist/commands/sessions-tail.d.ts +1 -0
  41. package/dist/commands/sessions-tail.js +20 -9
  42. package/dist/commands/sessions.js +18 -3
  43. package/dist/commands/setup-mine.d.ts +18 -0
  44. package/dist/commands/setup-mine.js +106 -0
  45. package/dist/commands/setup-share.js +2 -2
  46. package/dist/commands/setup.js +9 -2
  47. package/dist/commands/share.d.ts +8 -1
  48. package/dist/commands/share.js +122 -37
  49. package/dist/commands/ssh.d.ts +9 -0
  50. package/dist/commands/ssh.js +196 -26
  51. package/dist/commands/sync.js +1 -1
  52. package/dist/commands/teams.js +11 -2
  53. package/dist/commands/versions.js +36 -5
  54. package/dist/commands/view.js +75 -27
  55. package/dist/index.js +80 -10
  56. package/dist/lib/activity.d.ts +124 -0
  57. package/dist/lib/activity.js +542 -0
  58. package/dist/lib/agents.d.ts +35 -0
  59. package/dist/lib/agents.js +79 -6
  60. package/dist/lib/artifact-actions.d.ts +1 -1
  61. package/dist/lib/artifact-actions.js +1 -1
  62. package/dist/lib/ask-classifier.d.ts +2 -1
  63. package/dist/lib/ask-classifier.js +3 -3
  64. package/dist/lib/auth-health.d.ts +32 -4
  65. package/dist/lib/auth-health.js +40 -1
  66. package/dist/lib/auto-pull.js +8 -1
  67. package/dist/lib/brand.d.ts +40 -0
  68. package/dist/lib/brand.js +122 -0
  69. package/dist/lib/browser/drivers/ssh.js +4 -7
  70. package/dist/lib/browser/service.js +6 -8
  71. package/dist/lib/channels/providers/index.d.ts +2 -0
  72. package/dist/lib/channels/providers/index.js +20 -0
  73. package/dist/lib/channels/providers/mailbox.d.ts +2 -0
  74. package/dist/lib/channels/providers/mailbox.js +26 -0
  75. package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
  76. package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
  77. package/dist/lib/channels/providers/rush.d.ts +6 -0
  78. package/dist/lib/channels/providers/rush.js +55 -0
  79. package/dist/lib/channels/registry.d.ts +42 -0
  80. package/dist/lib/channels/registry.js +20 -0
  81. package/dist/lib/channels/resolve.d.ts +11 -0
  82. package/dist/lib/channels/resolve.js +12 -0
  83. package/dist/lib/cli-resources.d.ts +18 -0
  84. package/dist/lib/cli-resources.js +53 -4
  85. package/dist/lib/cloud/codex.js +6 -3
  86. package/dist/lib/cloud/factory.d.ts +1 -0
  87. package/dist/lib/cloud/factory.js +10 -4
  88. package/dist/lib/cloud/rush.d.ts +3 -1
  89. package/dist/lib/cloud/rush.js +6 -1
  90. package/dist/lib/cloud/types.d.ts +2 -0
  91. package/dist/lib/codex-home.d.ts +35 -0
  92. package/dist/lib/codex-home.js +136 -0
  93. package/dist/lib/config-transfer.d.ts +16 -0
  94. package/dist/lib/config-transfer.js +56 -0
  95. package/dist/lib/crabbox/cli.d.ts +44 -0
  96. package/dist/lib/crabbox/cli.js +120 -20
  97. package/dist/lib/crabbox/lease.d.ts +52 -1
  98. package/dist/lib/crabbox/lease.js +117 -16
  99. package/dist/lib/crabbox/progress.d.ts +31 -0
  100. package/dist/lib/crabbox/progress.js +76 -0
  101. package/dist/lib/crabbox/runtimes.d.ts +4 -0
  102. package/dist/lib/crabbox/runtimes.js +33 -8
  103. package/dist/lib/crabbox/setup-copy.d.ts +87 -0
  104. package/dist/lib/crabbox/setup-copy.js +127 -0
  105. package/dist/lib/daemon.d.ts +5 -5
  106. package/dist/lib/daemon.js +9 -9
  107. package/dist/lib/drive-sync.js +6 -3
  108. package/dist/lib/event-stream.d.ts +36 -0
  109. package/dist/lib/event-stream.js +68 -0
  110. package/dist/lib/events.d.ts +1 -1
  111. package/dist/lib/exec.d.ts +11 -0
  112. package/dist/lib/exec.js +23 -2
  113. package/dist/lib/export.d.ts +72 -0
  114. package/dist/lib/export.js +269 -0
  115. package/dist/lib/feed-policy.d.ts +1 -1
  116. package/dist/lib/feed-policy.js +14 -9
  117. package/dist/lib/feed-ranking.d.ts +32 -0
  118. package/dist/lib/feed-ranking.js +224 -0
  119. package/dist/lib/feed.d.ts +35 -2
  120. package/dist/lib/feed.js +66 -3
  121. package/dist/lib/fleet/auth-sync.d.ts +53 -0
  122. package/dist/lib/fleet/auth-sync.js +92 -0
  123. package/dist/lib/fleet/remote-login.d.ts +187 -0
  124. package/dist/lib/fleet/remote-login.js +556 -0
  125. package/dist/lib/format.d.ts +30 -3
  126. package/dist/lib/format.js +34 -5
  127. package/dist/lib/fs-atomic.d.ts +7 -2
  128. package/dist/lib/fs-atomic.js +8 -12
  129. package/dist/lib/git.d.ts +16 -0
  130. package/dist/lib/git.js +79 -2
  131. package/dist/lib/heal.js +11 -3
  132. package/dist/lib/hosts/progress.d.ts +28 -10
  133. package/dist/lib/hosts/progress.js +79 -22
  134. package/dist/lib/hosts/remote-cmd.js +4 -0
  135. package/dist/lib/hq/floor.d.ts +87 -0
  136. package/dist/lib/hq/floor.js +226 -0
  137. package/dist/lib/import.d.ts +23 -0
  138. package/dist/lib/import.js +74 -2
  139. package/dist/lib/isolation-boundary-report.d.ts +10 -0
  140. package/dist/lib/isolation-boundary-report.js +35 -0
  141. package/dist/lib/menubar/install-menubar.js +14 -20
  142. package/dist/lib/notify.d.ts +1 -0
  143. package/dist/lib/notify.js +3 -3
  144. package/dist/lib/open-url.d.ts +2 -0
  145. package/dist/lib/open-url.js +19 -0
  146. package/dist/lib/openclaw-keychain.d.ts +56 -0
  147. package/dist/lib/openclaw-keychain.js +236 -0
  148. package/dist/lib/overdue.js +10 -0
  149. package/dist/lib/permissions.d.ts +44 -1
  150. package/dist/lib/permissions.js +284 -7
  151. package/dist/lib/project-launch.d.ts +6 -12
  152. package/dist/lib/project-launch.js +13 -228
  153. package/dist/lib/project-resources.d.ts +7 -0
  154. package/dist/lib/project-resources.js +306 -0
  155. package/dist/lib/pty-client.d.ts +27 -0
  156. package/dist/lib/pty-client.js +136 -10
  157. package/dist/lib/refresh.js +14 -10
  158. package/dist/lib/resource-profiles.d.ts +26 -0
  159. package/dist/lib/resource-profiles.js +157 -0
  160. package/dist/lib/resources/permissions.js +7 -1
  161. package/dist/lib/resources/types.d.ts +1 -1
  162. package/dist/lib/resources.d.ts +1 -1
  163. package/dist/lib/resources.js +32 -3
  164. package/dist/lib/routines.d.ts +11 -0
  165. package/dist/lib/routines.js +56 -15
  166. package/dist/lib/runner.d.ts +1 -0
  167. package/dist/lib/runner.js +66 -5
  168. package/dist/lib/secrets/bundles.d.ts +16 -3
  169. package/dist/lib/secrets/bundles.js +206 -37
  170. package/dist/lib/secrets/icloud-import.d.ts +2 -2
  171. package/dist/lib/secrets/icloud-import.js +9 -6
  172. package/dist/lib/secrets/mcp.js +1 -1
  173. package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
  174. package/dist/lib/secrets/vault-age-helper.js +34 -0
  175. package/dist/lib/secrets/vault.d.ts +49 -0
  176. package/dist/lib/secrets/vault.js +397 -0
  177. package/dist/lib/self-heal/checks/path.js +3 -1
  178. package/dist/lib/self-heal/checks/shadowing.js +10 -2
  179. package/dist/lib/self-heal/checks/shims.js +19 -11
  180. package/dist/lib/session/cloud.d.ts +2 -2
  181. package/dist/lib/session/cloud.js +2 -2
  182. package/dist/lib/session/db.d.ts +4 -0
  183. package/dist/lib/session/db.js +44 -7
  184. package/dist/lib/session/discover.d.ts +2 -0
  185. package/dist/lib/session/discover.js +114 -3
  186. package/dist/lib/session/stream-render.d.ts +3 -0
  187. package/dist/lib/session/stream-render.js +130 -0
  188. package/dist/lib/session/types.d.ts +6 -0
  189. package/dist/lib/share/analytics.d.ts +13 -0
  190. package/dist/lib/share/analytics.js +45 -0
  191. package/dist/lib/share/config.d.ts +19 -5
  192. package/dist/lib/share/config.js +44 -8
  193. package/dist/lib/share/provision.d.ts +58 -6
  194. package/dist/lib/share/provision.js +97 -22
  195. package/dist/lib/share/publish.d.ts +36 -13
  196. package/dist/lib/share/publish.js +55 -8
  197. package/dist/lib/share/worker-template.js +83 -17
  198. package/dist/lib/shims.d.ts +35 -0
  199. package/dist/lib/shims.js +159 -13
  200. package/dist/lib/ssh-exec.d.ts +14 -0
  201. package/dist/lib/ssh-exec.js +57 -0
  202. package/dist/lib/staleness/detectors/hooks.js +25 -1
  203. package/dist/lib/staleness/detectors/permissions.js +66 -0
  204. package/dist/lib/staleness/detectors/workflows.js +20 -0
  205. package/dist/lib/staleness/writers/hooks.js +58 -4
  206. package/dist/lib/staleness/writers/permissions.js +2 -2
  207. package/dist/lib/staleness/writers/skills.js +1 -1
  208. package/dist/lib/staleness/writers/sources.d.ts +10 -1
  209. package/dist/lib/staleness/writers/sources.js +68 -1
  210. package/dist/lib/star-nudge.d.ts +45 -0
  211. package/dist/lib/star-nudge.js +91 -0
  212. package/dist/lib/startup/command-registry.d.ts +7 -1
  213. package/dist/lib/startup/command-registry.js +19 -3
  214. package/dist/lib/state.d.ts +2 -0
  215. package/dist/lib/state.js +17 -4
  216. package/dist/lib/subagents-registry.d.ts +2 -0
  217. package/dist/lib/subagents-registry.js +3 -0
  218. package/dist/lib/teams/parsers.js +214 -6
  219. package/dist/lib/teams/supervisor.d.ts +7 -0
  220. package/dist/lib/teams/supervisor.js +2 -1
  221. package/dist/lib/template.d.ts +1 -1
  222. package/dist/lib/template.js +1 -1
  223. package/dist/lib/triggers/webhook.js +36 -3
  224. package/dist/lib/types.d.ts +84 -0
  225. package/dist/lib/uninstall.js +1 -45
  226. package/dist/lib/version.d.ts +40 -0
  227. package/dist/lib/version.js +94 -16
  228. package/dist/lib/versions.d.ts +24 -0
  229. package/dist/lib/versions.js +269 -78
  230. package/dist/lib/workflows.d.ts +2 -0
  231. package/dist/lib/workflows.js +88 -0
  232. package/package.json +2 -1
  233. package/dist/commands/daemon.d.ts +0 -10
  234. package/dist/commands/daemon.js +0 -121
@@ -313,7 +313,9 @@ export const AGENTS = {
313
313
  skillsDir: path.join(HOME, '.opencode', 'skills'),
314
314
  // Plugins: TS/JS modules auto-loaded from ~/.config/opencode/plugins/ (global)
315
315
  // and .opencode/plugins/ (project). Not Claude marketplace format — see
316
- // installOpenCodePlugin in plugins.ts. No native shell hooks (plugins only).
316
+ // installOpenCodePlugin in plugins.ts. OpenCode v1.18.4 exposes lifecycle
317
+ // hooks through plugin modules (event/tool/etc. functions), not a native
318
+ // opencode.json shell-command hooks block.
317
319
  hooksDir: 'hooks',
318
320
  instructionsFile: 'AGENTS.md',
319
321
  format: 'markdown',
@@ -341,7 +343,9 @@ export const AGENTS = {
341
343
  // tools.alsoAllow (allow) / tools.deny (deny). OpenClaw gates at tool
342
344
  // granularity only, so sub-command/path/domain patterns are skipped.
343
345
  // OpenClaw is self-updating (no pinned since), so `true` is correct.
344
- capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: false, plugins: true, subagents: true, rules: { file: 'workspace/AGENTS.md' }, workflows: false, memory: true, modes: ['plan', 'edit', 'skip'] },
346
+ // Workflows sync as Lobster `.lobster` files under `.openclaw/workflows/`;
347
+ // the Lobster tool runs them by receiving the file path as `pipeline`.
348
+ capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: false, plugins: true, subagents: true, rules: { file: 'workspace/AGENTS.md' }, workflows: true, memory: true, modes: ['plan', 'edit', 'skip'] },
345
349
  },
346
350
  copilot: {
347
351
  id: 'copilot',
@@ -366,7 +370,7 @@ export const AGENTS = {
366
370
  format: 'markdown',
367
371
  variableSyntax: '$ARGUMENTS',
368
372
  supportsHooks: true,
369
- capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: true, commands: true, plugins: true, subagents: { since: '0.0.353' }, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['plan', 'edit', 'auto', 'skip'] },
373
+ capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: true, subagents: { since: '0.0.353' }, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['plan', 'edit', 'auto', 'skip'] },
370
374
  },
371
375
  amp: {
372
376
  id: 'amp',
@@ -650,7 +654,11 @@ export const AGENTS = {
650
654
  mcp: true,
651
655
  mcpHttp: true,
652
656
  mcpHeaders: false,
653
- allowlist: false,
657
+ // Permissions: ~/.hermes/config.yaml carries `command_allowlist` for
658
+ // always-approved command globs and `approvals.deny` for unconditional
659
+ // command blocks. It is command-glob only; session `/tools` toggles are
660
+ // intentionally not persisted by agents-cli.
661
+ allowlist: true,
654
662
  skills: true,
655
663
  commands: false,
656
664
  plugins: true,
@@ -691,7 +699,10 @@ export const AGENTS = {
691
699
  mcp: true,
692
700
  mcpHttp: true,
693
701
  mcpHeaders: false,
694
- allowlist: false,
702
+ // Permissions: ~/.forge/permissions.yaml (or $FORGE_CONFIG/permissions.yaml)
703
+ // carries ordered operation-family glob policies. It is active only when
704
+ // `.forge.toml` sets `restricted = true`; MCP tools bypass this file.
705
+ allowlist: true,
695
706
  skills: true,
696
707
  commands: true,
697
708
  plugins: false,
@@ -849,7 +860,20 @@ export async function getCliState(agentId) {
849
860
  }
850
861
  }
851
862
  }
852
- // Non-version-managed: single PATH lookup + cached version read
863
+ return getUnmanagedCliState(agentId);
864
+ }
865
+ /**
866
+ * Resolve the agent's OWN install — the one agents-cli does not manage — by plain
867
+ * PATH lookup, ignoring the version dirs entirely.
868
+ *
869
+ * Callers that specifically mean "the user's own globally-installed CLI" must use
870
+ * this rather than `getCliState`, whose managed fast path reports an installed
871
+ * version (any version dir, in readdir order) and would therefore hand back an
872
+ * isolated copy — a copy that is deliberately unreachable from PATH — labelled as
873
+ * the global install.
874
+ */
875
+ export async function getUnmanagedCliState(agentId) {
876
+ const agent = AGENTS[agentId];
853
877
  // Special case for grok: it manages its own binaries in ~/.grok/downloads/
854
878
  if (agentId === 'grok') {
855
879
  const grokBin = resolveGrokBinary();
@@ -1311,6 +1335,48 @@ function isValidOpenCodeCredential(value) {
1311
1335
  default: return false;
1312
1336
  }
1313
1337
  }
1338
+ /**
1339
+ * Whether a Claude version home's credential file is present but carries no
1340
+ * token — the "must have a real credential" floor (see
1341
+ * `isValidOpenCodeCredential`) applied to claude.
1342
+ *
1343
+ * A FAILED OAuth refresh leaves exactly this state behind: Claude Code rewrites
1344
+ * `.claude/.credentials.json` with `accessToken: ""`, `refreshToken: ""` and
1345
+ * `expiresAt: 0`, keeping only the descriptive fields (`subscriptionType`,
1346
+ * `rateLimitTier`, `refreshTokenExpiresAt`). Everything we derive from
1347
+ * `.claude.json` — email, plan — still looks healthy, so the install reported
1348
+ * `signedIn: true`, `agents view` drew usage bars for it, and balanced rotation
1349
+ * (whose `authValid` is just "email present") kept picking it — every pick dying
1350
+ * at spawn on "OAuth session expired and could not be refreshed".
1351
+ *
1352
+ * Only decidable off macOS: there the login Keychain is the canonical store and
1353
+ * this file is not authoritative, and probing the Keychain would raise an
1354
+ * authorization sheet per installed version on every `agents run` — the reason
1355
+ * rotation stopped calling `isClaudeAuthValid` at all. Off macOS the file IS the
1356
+ * only store, so a token-less file is proof of signed-out. `platform` is a
1357
+ * parameter so both branches are testable on any host.
1358
+ *
1359
+ * Sync, no Keychain, no network — safe on the `agents run` hot path.
1360
+ */
1361
+ export function isClaudeCredentialFileBlank(base, platform = process.platform) {
1362
+ if (platform === 'darwin')
1363
+ return false;
1364
+ try {
1365
+ const raw = fs.readFileSync(path.join(base, '.claude', '.credentials.json'), 'utf-8');
1366
+ const oauth = JSON.parse(raw).claudeAiOauth;
1367
+ if (!oauth)
1368
+ return false;
1369
+ const nonEmpty = (v) => typeof v === 'string' && v.trim().length > 0;
1370
+ return !nonEmpty(oauth.accessToken) && !nonEmpty(oauth.refreshToken);
1371
+ }
1372
+ catch {
1373
+ // No file (a Keychain-backed home, or never logged in here) or an
1374
+ // unreadable/corrupt one: not positive evidence of a blank credential, so
1375
+ // leave the existing signal alone rather than declaring a working install
1376
+ // signed out.
1377
+ return false;
1378
+ }
1379
+ }
1314
1380
  export async function getAccountInfo(agentId, home) {
1315
1381
  const base = home || os.homedir();
1316
1382
  const empty = {
@@ -1347,6 +1413,13 @@ export async function getAccountInfo(agentId, home) {
1347
1413
  const accountId = normalizeIdentityPart(oa?.accountUuid);
1348
1414
  const organizationId = normalizeIdentityPart(oa?.organizationUuid);
1349
1415
  const email = oa?.emailAddress || null;
1416
+ // Credential floor: a blanked credential file means this home cannot
1417
+ // authenticate, whatever `.claude.json` still says. Report it signed out
1418
+ // so `agents view` prompts a re-login and rotation routes around it,
1419
+ // instead of handing runs to an install that dies at spawn.
1420
+ if (email && isClaudeCredentialFileBlank(base)) {
1421
+ return { ...empty, lastActive };
1422
+ }
1350
1423
  const accountKey = buildIdentityKey(agentId, [
1351
1424
  ['account', accountId],
1352
1425
  ['org', organizationId],
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Artifact action types and validation.
3
- * Ported from agents/halo/cli/pkg/config.
3
+ * Ported from the legacy agent CLI config helpers.
4
4
  *
5
5
  * Artifact actions map tool invocations to artifact labels, allowing agents to
6
6
  * trigger tools automatically when specific artifacts are produced.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Artifact action types and validation.
3
- * Ported from agents/halo/cli/pkg/config.
3
+ * Ported from the legacy agent CLI config helpers.
4
4
  *
5
5
  * Artifact actions map tool invocations to artifact labels, allowing agents to
6
6
  * trigger tools automatically when specific artifacts are produced.
@@ -50,7 +50,7 @@ export interface SuppressResult {
50
50
  * Auto-answer + remove a suppressible block so it never renders as a card.
51
51
  * Logs the stall as answered by policy:stall-suppression.
52
52
  */
53
- export declare function suppressStallBlock(block: OpenBlock, root?: string): SuppressResult;
53
+ export declare function suppressStallBlock(block: OpenBlock, root?: string, mailboxRoot?: string): SuppressResult;
54
54
  export interface FeedFilterResult {
55
55
  /** Blocks that should render as cards. */
56
56
  surfaced: OpenBlock[];
@@ -66,6 +66,7 @@ export interface FeedFilterResult {
66
66
  export declare function filterBlocksForFeed(blocks: OpenBlock[], opts?: {
67
67
  apply?: boolean;
68
68
  root?: string;
69
+ mailboxRoot?: string;
69
70
  }): FeedFilterResult;
70
71
  /** Human digest: "31 stalls auto-resolved by policy". */
71
72
  export declare function suppressionDigest(result: FeedFilterResult): string;
@@ -108,7 +108,7 @@ export function classifyBlock(block) {
108
108
  * Auto-answer + remove a suppressible block so it never renders as a card.
109
109
  * Logs the stall as answered by policy:stall-suppression.
110
110
  */
111
- export function suppressStallBlock(block, root) {
111
+ export function suppressStallBlock(block, root, mailboxRoot) {
112
112
  const c = classifyBlock(block);
113
113
  if (!c.suppress || !c.autoAnswer) {
114
114
  return { blockId: block.blockId, class: c.class, rule: c.rule, autoAnswer: '', suppressed: false };
@@ -124,7 +124,7 @@ export function suppressStallBlock(block, root) {
124
124
  }
125
125
  try {
126
126
  // Mailbox lives under the global mailbox root (not the feed dir).
127
- const msgId = enqueue(mailboxDir(block.mailboxId), {
127
+ const msgId = enqueue(mailboxDir(block.mailboxId, mailboxRoot), {
128
128
  to: block.mailboxId,
129
129
  text: c.autoAnswer,
130
130
  from: 'stall-suppression',
@@ -164,7 +164,7 @@ export function filterBlocksForFeed(blocks, opts) {
164
164
  counts[c.class] += 1;
165
165
  if (c.suppress) {
166
166
  if (apply) {
167
- const r = suppressStallBlock(block, opts?.root);
167
+ const r = suppressStallBlock(block, opts?.root, opts?.mailboxRoot);
168
168
  suppressed.push(r);
169
169
  if (!r.suppressed)
170
170
  surfaced.push(block);
@@ -35,18 +35,46 @@ export declare function verdictLabel(verdict: AuthVerdict): string;
35
35
  /** Roll a set of verdicts (one host×agent's installs) into counts for a matrix cell. */
36
36
  export interface VerdictSummary {
37
37
  live: number;
38
+ /**
39
+ * unverified — signed in, but this agent has no in-repo live-probe endpoint
40
+ * (codex/grok). A benign, neutral state: the account is present and usable, we
41
+ * just can't complete a 2xx to prove it. It must NOT be lumped with the soft
42
+ * `warn` bucket, or a fully-logged-in codex/grok fleet reads as half-degraded
43
+ * (the exact "cry wolf" the ping matrix used to produce).
44
+ */
45
+ present: number;
38
46
  /** revoked — the server rejected the token (401/403). Genuinely needs re-login. */
39
47
  bad: number;
40
48
  /**
41
- * expired / rate_limited / unverified / error — degraded or unknown, but NOT
42
- * "re-login now". `expired` is soft for kimi/droid (their CLIs refresh the
43
- * token on next launch; we don't refresh on the read path), so it must not be
44
- * lumped with revoked or we'd cry wolf on a self-healing token.
49
+ * expired / rate_limited / error — degraded or unknown, but NOT "re-login now".
50
+ * `expired` is soft for kimi/droid (their CLIs refresh the token on next launch;
51
+ * we don't refresh on the read path), so it must not be lumped with revoked or
52
+ * we'd cry wolf on a self-healing token.
45
53
  */
46
54
  warn: number;
47
55
  total: number;
48
56
  }
49
57
  export declare function summarizeVerdicts(verdicts: AuthVerdict[]): VerdictSummary;
58
+ /** A resolved display color; the caller maps it to chalk. Pure, so it's unit-tested. */
59
+ export type AuthCellColor = 'green' | 'yellow' | 'red' | 'gray' | 'dim';
60
+ /**
61
+ * Color for a single verdict in the per-account (`--verbose`) breakdown. This is
62
+ * the one source of truth shared with {@link authCellColor} so the matrix and the
63
+ * account list can never drift — they did: the matrix painted `expired` yellow
64
+ * while the verbose list painted it *red* (lumped with revoked), directly against
65
+ * the {@link VerdictSummary} contract. Red is reserved for `revoked` (the only
66
+ * "re-login now"); `unverified` is a neutral signed-in state (gray);
67
+ * `expired`/`rate_limited`/`error` are soft (yellow).
68
+ */
69
+ export declare function verdictColor(verdict: AuthVerdict): AuthCellColor;
70
+ /**
71
+ * Color for a matrix cell that rolls up several accounts. Red only when a token
72
+ * was genuinely rejected (`revoked`); yellow for soft/expired; green when at least
73
+ * one account is live-verified and none are soft/revoked; gray when accounts are
74
+ * present but unverifiable (codex/grok) — never the alarming yellow the old
75
+ * renderer used, which made a fully-logged-in fleet read as half-broken.
76
+ */
77
+ export declare function authCellColor(summary: VerdictSummary): AuthCellColor;
50
78
  /** Verdicts that mean "this token was rejected by the server — re-login required". */
51
79
  export declare function isDeadVerdict(verdict: AuthVerdict): boolean;
52
80
  /**
@@ -82,17 +82,56 @@ export function verdictLabel(verdict) {
82
82
  }
83
83
  export function summarizeVerdicts(verdicts) {
84
84
  let live = 0;
85
+ let present = 0;
85
86
  let bad = 0;
86
87
  let warn = 0;
87
88
  for (const v of verdicts) {
88
89
  if (v === 'live')
89
90
  live++;
91
+ else if (v === 'unverified')
92
+ present++;
90
93
  else if (v === 'revoked')
91
94
  bad++;
92
95
  else
93
96
  warn++;
94
97
  }
95
- return { live, bad, warn, total: verdicts.length };
98
+ return { live, present, bad, warn, total: verdicts.length };
99
+ }
100
+ /**
101
+ * Color for a single verdict in the per-account (`--verbose`) breakdown. This is
102
+ * the one source of truth shared with {@link authCellColor} so the matrix and the
103
+ * account list can never drift — they did: the matrix painted `expired` yellow
104
+ * while the verbose list painted it *red* (lumped with revoked), directly against
105
+ * the {@link VerdictSummary} contract. Red is reserved for `revoked` (the only
106
+ * "re-login now"); `unverified` is a neutral signed-in state (gray);
107
+ * `expired`/`rate_limited`/`error` are soft (yellow).
108
+ */
109
+ export function verdictColor(verdict) {
110
+ switch (verdict) {
111
+ case 'live': return 'green';
112
+ case 'revoked': return 'red';
113
+ case 'unverified': return 'gray';
114
+ case 'unconfigured': return 'dim';
115
+ default: return 'yellow'; // expired / rate_limited / error — soft, self-healing/indeterminate
116
+ }
117
+ }
118
+ /**
119
+ * Color for a matrix cell that rolls up several accounts. Red only when a token
120
+ * was genuinely rejected (`revoked`); yellow for soft/expired; green when at least
121
+ * one account is live-verified and none are soft/revoked; gray when accounts are
122
+ * present but unverifiable (codex/grok) — never the alarming yellow the old
123
+ * renderer used, which made a fully-logged-in fleet read as half-broken.
124
+ */
125
+ export function authCellColor(summary) {
126
+ if (summary.total === 0)
127
+ return 'dim';
128
+ if (summary.bad > 0)
129
+ return 'red';
130
+ if (summary.warn > 0)
131
+ return 'yellow';
132
+ if (summary.live > 0)
133
+ return 'green';
134
+ return 'gray'; // all present/unverifiable — signed in, neutral
96
135
  }
97
136
  /** Verdicts that mean "this token was rejected by the server — re-login required". */
98
137
  export function isDeadVerdict(verdict) {
@@ -38,10 +38,17 @@ export function spawnDetachedSync() {
38
38
  if (!fs.existsSync(workerPath))
39
39
  return;
40
40
  try {
41
+ // Scrub AGENTS_BRAND so the background sync always reconciles the FULL
42
+ // resource set into the shared agent homes. Otherwise a branded foreground
43
+ // invocation (e.g. `jack …`) would leak its curated/reduced profile into the
44
+ // detached sync and silently strip skills/plugins for the plain `agents`
45
+ // user (last-writer-wins on shared homes). Brand curation must stay a
46
+ // foreground, in-process view — never a background mutation of shared state.
47
+ const { AGENTS_BRAND: _brand, ...unbrandedEnv } = process.env;
41
48
  const child = spawn(process.execPath, [workerPath], {
42
49
  ...backgroundSpawnOptions(),
43
50
  stdio: 'ignore',
44
- env: process.env,
51
+ env: unbrandedEnv,
45
52
  });
46
53
  child.unref();
47
54
  }
@@ -0,0 +1,40 @@
1
+ import type { BrandConfig } from './types.js';
2
+ /** The default (unbranded) program name. */
3
+ export declare const DEFAULT_CLI_NAME = "agents";
4
+ /**
5
+ * Resolve the name this invocation runs under. The brand shim exports
6
+ * `AGENTS_BRAND`; when unset (a normal `agents`/`ag` call) we are unbranded and
7
+ * everything is byte-identical to before.
8
+ */
9
+ export declare function resolveBrandName(): string;
10
+ /** The active brand name, or null when unbranded. */
11
+ export declare function activeBrandName(): string | null;
12
+ /** True when this process is running under a brand (not the plain `agents` CLI). */
13
+ export declare function isBranded(): boolean;
14
+ /** Names that would clobber an agent CLI shim or the `agents`/`ag` binary. */
15
+ export declare function reservedBrandNames(): Set<string>;
16
+ /** Validate a proposed brand name; returns an error string or null when ok. */
17
+ export declare function validateBrandName(name: string): string | null;
18
+ /** All configured brands, keyed by name. */
19
+ export declare function listBrands(): Record<string, BrandConfig>;
20
+ /** One brand's config, or undefined. */
21
+ export declare function getBrandConfig(name: string): BrandConfig | undefined;
22
+ /**
23
+ * The active brand's config (from AGENTS_BRAND), or null when unbranded or when
24
+ * the brand is explicitly disabled (`enabled: false`) — a disabled brand's shim
25
+ * still works as a plain pass-through but applies no command/resource curation.
26
+ */
27
+ export declare function getActiveBrandConfig(): BrandConfig | null;
28
+ /**
29
+ * The resource-profile preset name a brand pins, or null. Read on the hot path
30
+ * by resource-profiles.ts to make the active profile brand-scoped.
31
+ */
32
+ export declare function brandProfileName(): string | null;
33
+ /** Built-in top-level commands the active brand has turned off. */
34
+ export declare function disabledCommandsForActiveBrand(): Set<string>;
35
+ /** The preset name a brand owns (one resource profile per brand). */
36
+ export declare function brandPresetName(name: string): string;
37
+ /** Create or replace a brand's config in agents.yaml. */
38
+ export declare function upsertBrand(cfg: BrandConfig): void;
39
+ /** Remove a brand's config (leaves its resource preset unless `purgePreset`). */
40
+ export declare function removeBrand(name: string, purgePreset?: boolean): void;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Brand ("white-label") support — let a user run agents-cli under their own
3
+ * personally-named binary (e.g. `jack` instead of `agents`), skinned with their
4
+ * name and pinned to the exact feature set they enable.
5
+ *
6
+ * A brand is minted by `agents setup mine` / `agents mine init <name>`, which
7
+ * writes a pure pass-through shim (`~/.agents/.cache/shims/<name>`) that sets
8
+ * `AGENTS_BRAND=<name>` then execs the real agents-cli entrypoint. The entrypoint
9
+ * reads `AGENTS_BRAND` to (a) present its own name/help/errors as the brand, and
10
+ * (b) apply the brand's config: a list of disabled built-in commands plus an
11
+ * optional resource-profile preset that curates skills/plugins/mcp/etc.
12
+ *
13
+ * Brand config lives in `meta.brands[<name>]` (agents.yaml), so it rides
14
+ * `agents repo push/pull` across the fleet like every other user config. The
15
+ * curated resource set reuses the existing resource-profile engine — a brand
16
+ * pins a preset name (`meta.profiles.presets[...]`); see resource-profiles.ts,
17
+ * where the active profile is resolved brand-first.
18
+ */
19
+ import { readMeta, updateMeta } from './state.js';
20
+ import { ALL_AGENT_IDS, AGENTS } from './agents.js';
21
+ /** The default (unbranded) program name. */
22
+ export const DEFAULT_CLI_NAME = 'agents';
23
+ /** Valid brand names: a letter, then letters/digits/_/- (matches alias rules). */
24
+ const BRAND_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
25
+ /**
26
+ * Resolve the name this invocation runs under. The brand shim exports
27
+ * `AGENTS_BRAND`; when unset (a normal `agents`/`ag` call) we are unbranded and
28
+ * everything is byte-identical to before.
29
+ */
30
+ export function resolveBrandName() {
31
+ const raw = process.env.AGENTS_BRAND?.trim();
32
+ if (raw && BRAND_NAME_PATTERN.test(raw))
33
+ return raw;
34
+ return DEFAULT_CLI_NAME;
35
+ }
36
+ /** The active brand name, or null when unbranded. */
37
+ export function activeBrandName() {
38
+ const name = resolveBrandName();
39
+ return name === DEFAULT_CLI_NAME ? null : name;
40
+ }
41
+ /** True when this process is running under a brand (not the plain `agents` CLI). */
42
+ export function isBranded() {
43
+ return activeBrandName() !== null;
44
+ }
45
+ /** Names that would clobber an agent CLI shim or the `agents`/`ag` binary. */
46
+ export function reservedBrandNames() {
47
+ const reserved = new Set([DEFAULT_CLI_NAME, 'ag']);
48
+ for (const id of ALL_AGENT_IDS)
49
+ reserved.add(AGENTS[id].cliCommand);
50
+ return reserved;
51
+ }
52
+ /** Validate a proposed brand name; returns an error string or null when ok. */
53
+ export function validateBrandName(name) {
54
+ if (!BRAND_NAME_PATTERN.test(name)) {
55
+ return `Invalid name "${name}". Use a letter, then letters, digits, _ or -.`;
56
+ }
57
+ if (reservedBrandNames().has(name)) {
58
+ return `"${name}" is reserved (collides with an agent CLI or the agents binary).`;
59
+ }
60
+ return null;
61
+ }
62
+ /** All configured brands, keyed by name. */
63
+ export function listBrands() {
64
+ return readMeta().brands ?? {};
65
+ }
66
+ /** One brand's config, or undefined. */
67
+ export function getBrandConfig(name) {
68
+ return listBrands()[name];
69
+ }
70
+ /**
71
+ * The active brand's config (from AGENTS_BRAND), or null when unbranded or when
72
+ * the brand is explicitly disabled (`enabled: false`) — a disabled brand's shim
73
+ * still works as a plain pass-through but applies no command/resource curation.
74
+ */
75
+ export function getActiveBrandConfig() {
76
+ const name = activeBrandName();
77
+ if (!name)
78
+ return null;
79
+ const cfg = getBrandConfig(name);
80
+ if (!cfg || cfg.enabled === false)
81
+ return null;
82
+ return cfg;
83
+ }
84
+ /**
85
+ * The resource-profile preset name a brand pins, or null. Read on the hot path
86
+ * by resource-profiles.ts to make the active profile brand-scoped.
87
+ */
88
+ export function brandProfileName() {
89
+ const cfg = getActiveBrandConfig();
90
+ return cfg?.profile ?? null;
91
+ }
92
+ /** Built-in top-level commands the active brand has turned off. */
93
+ export function disabledCommandsForActiveBrand() {
94
+ const cfg = getActiveBrandConfig();
95
+ return new Set(cfg?.disabledCommands ?? []);
96
+ }
97
+ /** The preset name a brand owns (one resource profile per brand). */
98
+ export function brandPresetName(name) {
99
+ return `mine-${name}`;
100
+ }
101
+ /** Create or replace a brand's config in agents.yaml. */
102
+ export function upsertBrand(cfg) {
103
+ updateMeta((meta) => {
104
+ const brands = { ...(meta.brands ?? {}) };
105
+ brands[cfg.name] = cfg;
106
+ return { ...meta, brands };
107
+ });
108
+ }
109
+ /** Remove a brand's config (leaves its resource preset unless `purgePreset`). */
110
+ export function removeBrand(name, purgePreset = false) {
111
+ updateMeta((meta) => {
112
+ const brands = { ...(meta.brands ?? {}) };
113
+ delete brands[name];
114
+ let profiles = meta.profiles;
115
+ if (purgePreset && profiles?.presets) {
116
+ const presets = { ...profiles.presets };
117
+ delete presets[brandPresetName(name)];
118
+ profiles = { ...profiles, presets };
119
+ }
120
+ return { ...meta, brands, ...(profiles ? { profiles } : {}) };
121
+ });
122
+ }
@@ -5,11 +5,8 @@ import { getPortOccupant } from '../chrome.js';
5
5
  import { parseEndpointUrl } from '../profiles.js';
6
6
  import { writeProfileRuntime, clearProfileRuntime } from '../runtime-state.js';
7
7
  // shellQuote lives in the shared ssh-exec helper (single choke point); re-export
8
- // so existing importers of `shellQuote` from this module keep working. SSH_OPTS
9
- // is the shared hardened baseline (BatchMode + ConnectTimeout + keepalive) —
10
- // reuse it so the raw-ssh spawns below fail fast on an unreachable host instead
11
- // of hanging on the default ~127s TCP timeout, rather than re-listing options.
12
- import { shellQuote, SSH_OPTS, assertValidSshTarget } from '../../ssh-exec.js';
8
+ // so existing importers of `shellQuote` from this module keep working.
9
+ import { shellQuote, assertValidSshTarget, controlOpts, sshConnectOpts } from '../../ssh-exec.js';
13
10
  export { shellQuote };
14
11
  // The `ssh -L` tunnel spawn is shared with `agents computer --host`; it lives in
15
12
  // the single ssh-tunnel helper. Calling it with no options preserves this
@@ -309,7 +306,7 @@ export async function ensureRemoteBrowser(user, host, browserType, port, remoteO
309
306
  // macOS stops at the first non-option, so any `-o` after `user@host` would
310
307
  // be swallowed into the remote command instead of applied.
311
308
  const child = spawn('ssh', [
312
- ...SSH_OPTS,
309
+ ...sshConnectOpts(controlOpts()),
313
310
  `${user}@${host}`,
314
311
  remoteCmd,
315
312
  ], { stdio: ['ignore', 'ignore', 'pipe'], windowsHide: true });
@@ -384,7 +381,7 @@ function runSSHCommand(user, host, cmd) {
384
381
  // host is bounded by ConnectTimeout (the local 3s kill below only bounds a
385
382
  // connected-but-slow command, not the connect itself). Options precede the
386
383
  // target — see ensureRemoteBrowser.
387
- const child = spawn('ssh', [...SSH_OPTS, `${user}@${host}`, cmd], {
384
+ const child = spawn('ssh', [...sshConnectOpts(controlOpts()), `${user}@${host}`, cmd], {
388
385
  stdio: 'ignore',
389
386
  windowsHide: true,
390
387
  });
@@ -1,8 +1,6 @@
1
1
  import * as fs from 'fs';
2
2
  import * as os from 'os';
3
3
  import * as path from 'path';
4
- import { execFile } from 'child_process';
5
- import { promisify } from 'util';
6
4
  import { BrowserCdpConnectionError, CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity, } from './cdp.js';
7
5
  import { getProfile, getProfileRuntimeDir, getProfileDownloadsDir, getProfileSessionsDir, getBrowserRuntimeDir, listProfiles, extractConfiguredPort, resolveEndpoint, } from './profiles.js';
8
6
  import { killChrome, getRunningChromeInfo, launchBrowser, allocatePort } from './chrome.js';
@@ -16,6 +14,7 @@ import { clickAtCoords, hoverAtCoords, scrollAtCoords, typeText, pressKey, focus
16
14
  import { typeEditorText } from './editor.js';
17
15
  import { detectUploadPattern, stageUploadFile, uploadToDropTarget, uploadToFileInput, uploadViaFileChooser, } from './upload.js';
18
16
  import { emit } from '../events.js';
17
+ import { sshExecAsync } from '../ssh-exec.js';
19
18
  function isPathInside(candidate, dir) {
20
19
  const rel = path.relative(dir, candidate);
21
20
  return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
@@ -149,7 +148,6 @@ export function pickWindowTarget(targets, filter) {
149
148
  return visible;
150
149
  return pages[0];
151
150
  }
152
- const execFileP = promisify(execFile);
153
151
  /**
154
152
  * Parse a `--since`/`--until` value. Accepts ISO-8601 absolute timestamps
155
153
  * or relative offsets like `30s`, `5m`, `2h`, `1d`.
@@ -166,11 +164,11 @@ export function parseSinceUntil(s) {
166
164
  return new Date(Date.now() - n * unitMs[m[2]]);
167
165
  }
168
166
  async function execSSH(host, cmd) {
169
- const { stdout } = await execFileP('ssh', [host, cmd], {
170
- timeout: 10_000,
171
- maxBuffer: 10_000_000,
172
- });
173
- return stdout;
167
+ const res = await sshExecAsync(host, cmd, { timeoutMs: 10_000 });
168
+ if (res.code !== 0) {
169
+ throw new Error(`ssh ${host} failed${res.stderr.trim() ? `: ${res.stderr.trim()}` : ''}`);
170
+ }
171
+ return res.stdout;
174
172
  }
175
173
  export function readNewestMatchingRemoteFileCommand(dir, prefix, tailLines) {
176
174
  const glob = `${shellQuote(dir)}/${prefix}*.jsonl`;
@@ -0,0 +1,2 @@
1
+ /** Register all built-in providers once (idempotent). */
2
+ export declare function registerBuiltinProviders(): void;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Provider barrel — importing this registers every built-in channel provider.
3
+ * "App" providers register themselves the same way (registerChannelProvider),
4
+ * so nothing here is privileged over an extension.
5
+ */
6
+ import { registerChannelProvider } from '../registry.js';
7
+ import { mailboxProvider } from './mailbox.js';
8
+ import { rushProviders } from './rush.js';
9
+ import { openclawTelegramProvider } from './openclaw-telegram.js';
10
+ let registered = false;
11
+ /** Register all built-in providers once (idempotent). */
12
+ export function registerBuiltinProviders() {
13
+ if (registered)
14
+ return;
15
+ registered = true;
16
+ registerChannelProvider(mailboxProvider);
17
+ for (const p of rushProviders)
18
+ registerChannelProvider(p);
19
+ registerChannelProvider(openclawTelegramProvider);
20
+ }
@@ -0,0 +1,2 @@
1
+ import type { ChannelProvider } from '../registry.js';
2
+ export declare const mailboxProvider: ChannelProvider;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Mailbox channel provider — delivers to an agent's file-based mailbox.
3
+ *
4
+ * The mailbox is one channel among many. This provider is a thin wrapper over
5
+ * the same `enqueue` seam that `agents message` uses (commands/message.ts), so
6
+ * both callers share one spool at ~/.agents/.history/mailbox/<id>/.
7
+ */
8
+ import { mailboxDir, enqueue, isValidMailboxId } from '../../mailbox.js';
9
+ export const mailboxProvider = {
10
+ name: 'mailbox',
11
+ async send(text, opts) {
12
+ if (!isValidMailboxId(opts.target)) {
13
+ return { ok: false, channel: 'mailbox', id: opts.target, error: `invalid mailbox id '${opts.target}'` };
14
+ }
15
+ if (opts.dryRun) {
16
+ return { ok: true, channel: 'mailbox', id: opts.target };
17
+ }
18
+ try {
19
+ const msgId = enqueue(mailboxDir(opts.target), { to: opts.target, text, from: opts.from });
20
+ return { ok: true, channel: 'mailbox', id: opts.target, msgId };
21
+ }
22
+ catch (err) {
23
+ return { ok: false, channel: 'mailbox', id: opts.target, error: err.message };
24
+ }
25
+ },
26
+ };
@@ -0,0 +1,2 @@
1
+ import type { ChannelProvider } from '../registry.js';
2
+ export declare const openclawTelegramProvider: ChannelProvider;