@phnx-labs/agents-cli 1.20.52 → 1.20.53

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 (107) hide show
  1. package/CHANGELOG.md +57 -2
  2. package/README.md +12 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/cloud.d.ts +3 -0
  5. package/dist/commands/cloud.js +2 -1
  6. package/dist/commands/exec.js +65 -1
  7. package/dist/commands/feed.d.ts +29 -0
  8. package/dist/commands/feed.js +237 -32
  9. package/dist/commands/memory.d.ts +9 -0
  10. package/dist/commands/memory.js +164 -0
  11. package/dist/commands/message.d.ts +11 -6
  12. package/dist/commands/message.js +140 -5
  13. package/dist/commands/routines.js +12 -0
  14. package/dist/commands/secrets-migrate.d.ts +2 -1
  15. package/dist/commands/secrets-migrate.js +88 -13
  16. package/dist/commands/secrets.js +4 -1
  17. package/dist/commands/sessions.js +10 -1
  18. package/dist/commands/worktree.js +4 -2
  19. package/dist/index.js +16 -21
  20. package/dist/lib/agents.js +249 -17
  21. package/dist/lib/answer-router.d.ts +75 -0
  22. package/dist/lib/answer-router.js +149 -0
  23. package/dist/lib/ask-classifier.d.ts +71 -0
  24. package/dist/lib/ask-classifier.js +197 -0
  25. package/dist/lib/cloud/antigravity.d.ts +0 -2
  26. package/dist/lib/cloud/antigravity.js +2 -17
  27. package/dist/lib/cloud/codex.js +3 -18
  28. package/dist/lib/cloud/rush.js +3 -15
  29. package/dist/lib/cloud/stream.js +2 -0
  30. package/dist/lib/cloud/types.d.ts +21 -0
  31. package/dist/lib/cloud/types.js +81 -0
  32. package/dist/lib/crabbox/cli.d.ts +1 -1
  33. package/dist/lib/crabbox/cli.js +12 -2
  34. package/dist/lib/crabbox/lease.d.ts +13 -0
  35. package/dist/lib/crabbox/lease.js +11 -2
  36. package/dist/lib/crabbox/progress.d.ts +62 -0
  37. package/dist/lib/crabbox/progress.js +129 -0
  38. package/dist/lib/events.js +4 -1
  39. package/dist/lib/exec.js +19 -1
  40. package/dist/lib/feed-outcome.d.ts +101 -0
  41. package/dist/lib/feed-outcome.js +244 -0
  42. package/dist/lib/feed-policy.d.ts +30 -0
  43. package/dist/lib/feed-policy.js +133 -0
  44. package/dist/lib/feed.d.ts +127 -3
  45. package/dist/lib/feed.js +416 -40
  46. package/dist/lib/git.d.ts +17 -1
  47. package/dist/lib/git.js +20 -1
  48. package/dist/lib/hooks.js +522 -12
  49. package/dist/lib/hosts/passthrough.d.ts +3 -3
  50. package/dist/lib/hosts/passthrough.js +3 -4
  51. package/dist/lib/mailbox-gc.d.ts +22 -0
  52. package/dist/lib/mailbox-gc.js +161 -0
  53. package/dist/lib/mailbox.d.ts +26 -2
  54. package/dist/lib/mailbox.js +80 -5
  55. package/dist/lib/mcp.js +82 -0
  56. package/dist/lib/memory.d.ts +55 -0
  57. package/dist/lib/memory.js +274 -0
  58. package/dist/lib/notify.d.ts +16 -0
  59. package/dist/lib/notify.js +61 -0
  60. package/dist/lib/operator.d.ts +26 -0
  61. package/dist/lib/operator.js +107 -0
  62. package/dist/lib/plugins.d.ts +35 -0
  63. package/dist/lib/plugins.js +217 -0
  64. package/dist/lib/remote-agents-json.d.ts +14 -0
  65. package/dist/lib/remote-agents-json.js +94 -0
  66. package/dist/lib/resources/mcp.js +44 -0
  67. package/dist/lib/resources/memory.d.ts +15 -0
  68. package/dist/lib/resources/memory.js +46 -0
  69. package/dist/lib/resources/types.d.ts +2 -2
  70. package/dist/lib/runner.d.ts +43 -0
  71. package/dist/lib/runner.js +323 -74
  72. package/dist/lib/sandbox.js +6 -0
  73. package/dist/lib/secrets/bundles.js +38 -13
  74. package/dist/lib/secrets/icloud-import.d.ts +12 -3
  75. package/dist/lib/secrets/icloud-import.js +37 -7
  76. package/dist/lib/secrets/index.d.ts +106 -2
  77. package/dist/lib/secrets/index.js +603 -28
  78. package/dist/lib/session/active.d.ts +18 -0
  79. package/dist/lib/session/active.js +47 -17
  80. package/dist/lib/session/db.d.ts +9 -1
  81. package/dist/lib/session/db.js +18 -3
  82. package/dist/lib/session/discover.d.ts +13 -0
  83. package/dist/lib/session/discover.js +31 -0
  84. package/dist/lib/session/parse.d.ts +8 -0
  85. package/dist/lib/session/parse.js +42 -21
  86. package/dist/lib/session/remote-active.js +8 -89
  87. package/dist/lib/session/state.d.ts +11 -0
  88. package/dist/lib/session/state.js +37 -0
  89. package/dist/lib/session/tail.d.ts +23 -4
  90. package/dist/lib/session/tail.js +34 -16
  91. package/dist/lib/session/throughput.d.ts +30 -0
  92. package/dist/lib/session/throughput.js +86 -0
  93. package/dist/lib/shim-heal.d.ts +12 -3
  94. package/dist/lib/shim-heal.js +12 -6
  95. package/dist/lib/staleness/detectors/subagents.js +57 -3
  96. package/dist/lib/staleness/writers/hooks.js +7 -3
  97. package/dist/lib/staleness/writers/subagents.js +37 -6
  98. package/dist/lib/startup/command-registry.d.ts +1 -0
  99. package/dist/lib/startup/command-registry.js +2 -0
  100. package/dist/lib/subagents.d.ts +52 -0
  101. package/dist/lib/subagents.js +315 -12
  102. package/dist/lib/teams/worktree.d.ts +8 -0
  103. package/dist/lib/teams/worktree.js +8 -0
  104. package/dist/lib/types.d.ts +10 -2
  105. package/dist/lib/versions.js +38 -48
  106. package/package.json +4 -3
  107. package/scripts/postinstall.js +61 -1
@@ -1,69 +1,274 @@
1
1
  import chalk from 'chalk';
2
- import { ensureFeedPublishHook, listBlocks } from '../lib/feed.js';
3
- import { machineId } from '../lib/machine-id.js';
2
+ import { ensureFeedPublishHook, listBlocks, recordNotified } from '../lib/feed.js';
3
+ import { enrichBlocksFromSessions, groupBlocksByOutcome, isUnambiguousOutcomeAnswer, openBlocksForOutcome, stampBlockOutcomes, } from '../lib/feed-outcome.js';
4
+ import { classifyBlock, filterBlocksForFeed, suppressionDigest, } from '../lib/ask-classifier.js';
5
+ import { machineId, normalizeHost } from '../lib/machine-id.js';
4
6
  import { relTime } from '../lib/format.js';
5
- function renderBlock(b, localHost) {
7
+ import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
8
+ import { loadPolicy, applyPolicyToBlock, isPhoneUrgent } from '../lib/feed-policy.js';
9
+ import { notifyUrgentBlock } from '../lib/notify.js';
10
+ import { gcMailbox } from '../lib/mailbox-gc.js';
11
+ import { getActiveSessions } from '../lib/session/active.js';
12
+ import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
13
+ export const FEED_NO_FANOUT_ENV = 'AGENTS_FEED_LOCAL';
14
+ export function parseRemoteFeed(stdout, machine) {
15
+ let parsed;
16
+ try {
17
+ parsed = JSON.parse(stdout);
18
+ }
19
+ catch {
20
+ return [];
21
+ }
22
+ if (!Array.isArray(parsed))
23
+ return [];
24
+ const blocks = [];
25
+ for (const item of parsed) {
26
+ if (!item || typeof item !== 'object' || Array.isArray(item))
27
+ continue;
28
+ const block = item;
29
+ if (!block.blockId || !block.sessionId || !block.mailboxId || !block.questions?.length)
30
+ continue;
31
+ blocks.push({ ...block, host: machine });
32
+ }
33
+ return blocks;
34
+ }
35
+ /** Merge local and remote rows, keeping the first copy of a host/session block. */
36
+ export function mergeFeedBlocks(...groups) {
37
+ const byIdentity = new Map();
38
+ for (const block of groups.flat()) {
39
+ const key = `${normalizeHost(block.host)}:${block.blockId}`;
40
+ if (!byIdentity.has(key))
41
+ byIdentity.set(key, block);
42
+ }
43
+ return [...byIdentity.values()].sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts));
44
+ }
45
+ function hostToken(host) {
46
+ return normalizeHost(host.split('@').pop() || host);
47
+ }
48
+ export function shouldIncludeLocalFeed(hosts, self) {
49
+ return !hosts?.length || hosts.some((host) => hostToken(host) === self);
50
+ }
51
+ export function remoteFeedHostsToDial(hosts, self) {
52
+ if (!hosts?.length)
53
+ return undefined;
54
+ return hosts.filter((host) => hostToken(host) !== self);
55
+ }
56
+ function renderBlock(b, localHost, indent = '') {
6
57
  const host = b.host !== localHost ? chalk.yellow(` [${b.host}]`) : '';
7
58
  const runtime = chalk.gray(b.runtime);
8
59
  const age = chalk.gray(relTime(b.ts));
9
- console.log(`${chalk.cyan(b.mailboxId)}${host} ${runtime} ${age}`);
60
+ const cls = b.blockClass ? chalk.gray(`(${b.blockClass})`) : '';
61
+ const consequence = b.consequence && b.consequence !== 'normal' ? chalk.red(`[${b.consequence}]`) : '';
62
+ const cost = b.costOfDelay ? chalk.gray(`cost:${b.costOfDelay}`) : '';
63
+ console.log(`${indent}${chalk.cyan(b.mailboxId)}${host} ${runtime} ${age} ${cls} ${consequence} ${cost}`.trimEnd());
10
64
  for (const question of b.questions) {
11
65
  const header = question.header ? chalk.gray(`[${question.header}] `) : '';
12
- console.log(` ${header}${question.text}`);
66
+ console.log(`${indent} ${header}${question.text}`);
13
67
  if (question.options?.length) {
14
68
  for (let i = 0; i < question.options.length; i++) {
15
69
  const o = question.options[i];
16
70
  const desc = o.description ? chalk.gray(` -- ${o.description}`) : '';
17
- console.log(` ${chalk.dim(`${i + 1}.`)} ${o.label}${desc}`);
71
+ console.log(`${indent} ${chalk.dim(`${i + 1}.`)} ${o.label}${desc}`);
18
72
  }
19
73
  }
20
74
  }
21
- if (b.ticket || b.pr) {
22
- const meta = [b.ticket, b.pr].filter(Boolean).join(' ');
23
- console.log(` ${chalk.gray(meta)}`);
75
+ if (b.ticket || b.pr || b.worktreeSlug) {
76
+ const meta = [b.ticket, b.pr, b.worktreeSlug].filter(Boolean).join(' ');
77
+ console.log(`${indent} ${chalk.gray(meta)}`);
78
+ }
79
+ if (b.answer) {
80
+ const verified = b.answer.verified ? chalk.green('✓') : chalk.yellow('?');
81
+ const who = b.answer.answeredFrom + (b.answer.answeredBy ? ` (${b.answer.answeredBy})` : '');
82
+ console.log(`${indent} ${chalk.green('answered')} by ${who} ${verified}`);
83
+ }
84
+ if (b.parkedAt) {
85
+ console.log(`${indent} ${chalk.red('hard-parked')} ${relTime(b.parkedAt)}`);
86
+ }
87
+ if (b.defaultedAt) {
88
+ console.log(`${indent} ${chalk.yellow('defaulted')} ${relTime(b.defaultedAt)}`);
89
+ }
90
+ if (b.receipts && b.receipts.length > 0) {
91
+ const latest = b.receipts[b.receipts.length - 1];
92
+ console.log(`${indent} ${chalk.dim('delivery:')} ${latest.status}`);
93
+ }
94
+ if (b.continuedAt) {
95
+ console.log(`${indent} ${chalk.green('continued')} ${relTime(b.continuedAt)}`);
96
+ }
97
+ if (b.notifiedAt) {
98
+ console.log(`${indent} ${chalk.dim('notified')} ${relTime(b.notifiedAt)}`);
99
+ }
100
+ if (!b.answer && !b.parkedAt) {
101
+ console.log(`${indent} ${chalk.dim('reply:')} agents message ${b.mailboxId} "<answer>"`);
24
102
  }
25
- console.log(` ${chalk.dim('reply:')} agents message ${b.mailboxId} "<answer>"`);
26
103
  console.log();
27
104
  }
105
+ /** Human summary line for one outcome rollup. */
106
+ export function formatOutcomeHeader(group) {
107
+ const { agents, open, answered, parked } = group.counts;
108
+ const parts = [
109
+ `${agents} agent${agents === 1 ? '' : 's'}`,
110
+ open > 0 ? `${open} needs you` : null,
111
+ answered > 0 ? `${answered} answered` : null,
112
+ parked > 0 ? `${parked} parked` : null,
113
+ ].filter(Boolean);
114
+ return `${group.outcome.label} · ${parts.join(' · ')}`;
115
+ }
116
+ function renderOutcomeGroup(group, localHost) {
117
+ console.log(chalk.bold(formatOutcomeHeader(group)));
118
+ if (isUnambiguousOutcomeAnswer(group) && openBlocksForOutcome(group).length > 1) {
119
+ const ids = openBlocksForOutcome(group).map((b) => b.mailboxId).join(', ');
120
+ console.log(chalk.dim(` same question on ${openBlocksForOutcome(group).length} agents — fan-out safe: ${ids}`));
121
+ }
122
+ for (const b of group.blocks) {
123
+ renderBlock(b, localHost, ' ');
124
+ }
125
+ }
126
+ /** Map active sessions into the lightweight hints outcome enrichment needs. */
127
+ export function sessionHintsFromActive(sessions) {
128
+ return sessions.map((s) => ({
129
+ sessionId: s.sessionId,
130
+ agentId: s.agentId,
131
+ // Same precedence as mailboxIdForActiveSession (agentId ?? sessionId).
132
+ mailboxId: s.agentId ?? s.sessionId,
133
+ ticketId: s.ticket?.id,
134
+ prNumber: s.pr?.number,
135
+ prUrl: s.pr?.url,
136
+ worktreeSlug: s.worktree?.slug,
137
+ }));
138
+ }
28
139
  export function registerFeedCommand(program) {
29
140
  program
30
141
  .command('feed')
31
- .description('List open blocks -- decisions agents are waiting on')
32
- .option('--json', 'Output as JSON')
142
+ .description('List open blocks -- decisions agents are waiting on (grouped by outcome)')
143
+ .option('--json', 'Output as JSON (each block stamped with its outcome + ask class)')
144
+ .option('--flat', 'List one block per agent instead of grouping by outcome')
145
+ .option('--all', 'Include stalls/FYIs that policy would suppress (default: hide them)')
146
+ .option('--local', 'Only this machine -- skip the cross-machine SSH fan-out')
147
+ .option('-H, --host <target...>', 'Scope to remote machine(s) over SSH; repeatable')
148
+ .option('--device <target...>', 'Alias for --host; repeatable')
149
+ .option('--dispatch', 'Run stall suppression + default-on-no-answer policy and urgent notifications')
33
150
  .action(async (opts) => {
151
+ if (opts.device?.length)
152
+ opts.host = [...(opts.host ?? []), ...opts.device];
153
+ const self = machineId();
154
+ const includeLocal = shouldIncludeLocalFeed(opts.host, self);
34
155
  const setupWarnings = [];
35
- const hookInstall = ensureFeedPublishHook();
36
- if (hookInstall.error) {
37
- setupWarnings.push(hookInstall.error);
156
+ if (includeLocal) {
157
+ const hookInstall = ensureFeedPublishHook();
158
+ if (hookInstall.error) {
159
+ setupWarnings.push(hookInstall.error);
160
+ }
161
+ else {
162
+ const [{ iterHooksCapableVersions, parseHookManifest, registerHooksToSettings }, { getVersionHomePath }] = await Promise.all([
163
+ import('../lib/hooks.js'),
164
+ import('../lib/versions.js'),
165
+ ]);
166
+ const manifest = parseHookManifest({ warn: false });
167
+ for (const { agent, version } of iterHooksCapableVersions({ agent: 'claude' })) {
168
+ const result = registerHooksToSettings(agent, getVersionHomePath(agent, version), manifest);
169
+ if (result.errors.length > 0) {
170
+ setupWarnings.push(`${agent}@${version}: ${result.errors.join('; ')}`);
171
+ }
172
+ }
173
+ }
174
+ }
175
+ // Active sessions feed both the GC sweep and outcome enrichment (ticket/PR).
176
+ let sessions = [];
177
+ if (includeLocal) {
178
+ sessions = await getActiveSessions();
179
+ }
180
+ if (opts.dispatch && includeLocal) {
181
+ // Liveness sweep: drop messages to dead agents and retire stale blocks
182
+ // before we render the feed.
183
+ const activeBoxIds = new Set(sessions.map(mailboxIdForActiveSession).filter((id) => !!id));
184
+ const gcResult = gcMailbox(activeBoxIds);
185
+ if (gcResult.blocksRemoved > 0 || gcResult.messagesDroppedDead > 0) {
186
+ console.log(chalk.yellow(`gc: ${gcResult.messagesDroppedDead} dead messages, ${gcResult.blocksRemoved} stale blocks removed`));
187
+ }
38
188
  }
39
- else {
40
- const [{ iterHooksCapableVersions, parseHookManifest, registerHooksToSettings }, { getVersionHomePath }] = await Promise.all([
41
- import('../lib/hooks.js'),
42
- import('../lib/versions.js'),
43
- ]);
44
- const manifest = parseHookManifest({ warn: false });
45
- for (const { agent, version } of iterHooksCapableVersions({ agent: 'claude' })) {
46
- const result = registerHooksToSettings(agent, getVersionHomePath(agent, version), manifest);
47
- if (result.errors.length > 0) {
48
- setupWarnings.push(`${agent}@${version}: ${result.errors.join('; ')}`);
189
+ const localBlocks = includeLocal ? listBlocks() : [];
190
+ let blocks = localBlocks;
191
+ const forceLocal = opts.local === true || process.env[FEED_NO_FANOUT_ENV] === '1';
192
+ if (!forceLocal) {
193
+ const remoteHosts = remoteFeedHostsToDial(opts.host, self);
194
+ if (!opts.host?.length || (remoteHosts && remoteHosts.length > 0)) {
195
+ const remote = await gatherRemoteAgentsJson({
196
+ // Bare --json stays a block array so older peers and scripts keep working.
197
+ args: ['feed', '--json'],
198
+ noFanoutEnv: FEED_NO_FANOUT_ENV,
199
+ hosts: remoteHosts,
200
+ parse: parseRemoteFeed,
201
+ });
202
+ blocks = mergeFeedBlocks(localBlocks, remote.items);
203
+ }
204
+ }
205
+ // Fill missing ticket/PR/worktree from live session meta so outcome keys
206
+ // land even when the publish hook had no deliverable stamp.
207
+ if (sessions.length > 0) {
208
+ blocks = enrichBlocksFromSessions(blocks, sessionHintsFromActive(sessions));
209
+ }
210
+ // Stall suppression (RUSH-1477): auto-answer "should I…?" / "what's next?"
211
+ // so they never render. Applied on every feed read unless --all; --dispatch
212
+ // also applies so unattended ticks drain the stall backlog.
213
+ const filter = filterBlocksForFeed(blocks, {
214
+ apply: !opts.all || opts.dispatch === true,
215
+ });
216
+ if (!opts.all) {
217
+ blocks = filter.surfaced;
218
+ }
219
+ const digest = suppressionDigest(filter);
220
+ if (digest && !opts.json) {
221
+ console.log(chalk.dim(digest));
222
+ }
223
+ if (opts.dispatch) {
224
+ const policy = loadPolicy();
225
+ const now = new Date();
226
+ for (const b of blocks) {
227
+ const result = applyPolicyToBlock(b, policy, now);
228
+ if (result.action !== 'none') {
229
+ console.log(`${chalk.yellow('policy')} ${b.blockId}: ${result.action}`);
230
+ }
231
+ if (isPhoneUrgent(b, policy)) {
232
+ const notifyResult = await notifyUrgentBlock(b, { dryRun: opts.json });
233
+ if (notifyResult.ok && !notifyResult.skipped) {
234
+ recordNotified(b.blockId);
235
+ console.log(`${chalk.green('notified')} ${b.blockId}`);
236
+ }
237
+ else if (notifyResult.error) {
238
+ console.error(chalk.yellow(`Notification failed for ${b.blockId}: ${notifyResult.error}`));
239
+ }
49
240
  }
50
241
  }
51
242
  }
52
- const blocks = listBlocks();
53
243
  for (const warning of setupWarnings) {
54
244
  console.error(chalk.yellow(`Feed hook setup warning: ${warning}`));
55
245
  }
56
246
  if (opts.json) {
57
- console.log(JSON.stringify(blocks, null, 2));
247
+ // Always a block array (stamped with outcome + ask class) so remote fan-out
248
+ // and scripts keep a stable contract. Human grouping is text-only.
249
+ const stamped = stampBlockOutcomes(blocks).map((b) => ({
250
+ ...b,
251
+ ask: classifyBlock(b),
252
+ }));
253
+ console.log(JSON.stringify(stamped, null, 2));
58
254
  return;
59
255
  }
60
256
  if (blocks.length === 0) {
61
- console.log(chalk.gray('No open blocks.'));
257
+ console.log(chalk.gray(digest ? 'No open blocks after stall suppression.' : 'No open blocks.'));
258
+ return;
259
+ }
260
+ if (opts.flat) {
261
+ console.log(chalk.bold(`${blocks.length} open block${blocks.length === 1 ? '' : 's'}:\n`));
262
+ for (const b of blocks)
263
+ renderBlock(b, self);
62
264
  return;
63
265
  }
64
- const local = machineId();
65
- console.log(chalk.bold(`${blocks.length} open block${blocks.length === 1 ? '' : 's'}:\n`));
66
- for (const b of blocks)
67
- renderBlock(b, local);
266
+ const groups = groupBlocksByOutcome(blocks);
267
+ const openOutcomes = groups.filter((g) => g.counts.open > 0).length;
268
+ console.log(chalk.bold(`${groups.length} outcome${groups.length === 1 ? '' : 's'} · ${blocks.length} block${blocks.length === 1 ? '' : 's'}` +
269
+ (openOutcomes > 0 ? ` · ${openOutcomes} need you` : '') +
270
+ ':\n'));
271
+ for (const g of groups)
272
+ renderOutcomeGroup(g, self);
68
273
  });
69
274
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `agents memory` — first-class knowledge/facts resource (not rules).
3
+ *
4
+ * Mirrors the skills surface at list / add / remove / view / sync scale.
5
+ * Canonical storage: ~/.agents/memory/ (project > user > system layering).
6
+ */
7
+ import type { Command } from 'commander';
8
+ /** Register the `agents memory` command tree. */
9
+ export declare function registerMemoryCommands(program: Command): void;
@@ -0,0 +1,164 @@
1
+ /**
2
+ * `agents memory` — first-class knowledge/facts resource (not rules).
3
+ *
4
+ * Mirrors the skills surface at list / add / remove / view / sync scale.
5
+ * Canonical storage: ~/.agents/memory/ (project > user > system layering).
6
+ */
7
+ import chalk from 'chalk';
8
+ import * as fs from 'fs';
9
+ import { listMemoryFacts, addMemoryFact, removeMemoryFact, readMemoryFact, ensureUserMemoryDir, getUserMemoryDir, syncMemoryToVersionHome, } from '../lib/memory.js';
10
+ import { capableAgents } from '../lib/capabilities.js';
11
+ import { resolveAgentName, formatAgentError, agentLabel, ALL_AGENT_IDS } from '../lib/agents.js';
12
+ import { listInstalledVersions, getVersionHomePath, } from '../lib/versions.js';
13
+ import { supports } from '../lib/capabilities.js';
14
+ import { setHelpSections } from '../lib/help.js';
15
+ /** Register the `agents memory` command tree. */
16
+ export function registerMemoryCommands(program) {
17
+ const memoryCmd = program
18
+ .command('memory')
19
+ .description('Manage portable agent memory (facts, preferences, project knowledge)');
20
+ setHelpSections(memoryCmd, {
21
+ examples: `
22
+ # List all memory facts (project > user > system)
23
+ agents memory list
24
+
25
+ # Add a fact
26
+ agents memory add preferred-editor --body "User prefers vim keybindings"
27
+
28
+ # View a fact
29
+ agents memory view preferred-editor
30
+
31
+ # Remove a fact from the user layer
32
+ agents memory remove preferred-editor
33
+
34
+ # Fan out to all capable installed agent versions
35
+ agents memory sync
36
+ `,
37
+ notes: `
38
+ Memory is the knowledge store (learned facts), distinct from rules
39
+ (instructions / AGENTS.md persona). Stored under ~/.agents/memory/ and
40
+ synced into capable agents' version homes (claude/codex/openclaw/grok).
41
+
42
+ Format: MEMORY.md index + one <slug>.md file per fact.
43
+ `,
44
+ });
45
+ memoryCmd
46
+ .command('list')
47
+ .description('List memory facts from project, user, and system layers')
48
+ .option('--json', 'Emit machine-readable JSON')
49
+ .action((options) => {
50
+ const facts = listMemoryFacts(process.cwd());
51
+ if (options.json) {
52
+ process.stdout.write(JSON.stringify(facts, null, 2) + '\n');
53
+ return;
54
+ }
55
+ if (facts.length === 0) {
56
+ console.log(chalk.gray('No memory facts yet.'));
57
+ console.log(chalk.gray(` Add one: agents memory add <name> --body "..."`));
58
+ console.log(chalk.gray(` Dir: ${getUserMemoryDir()}`));
59
+ return;
60
+ }
61
+ console.log(chalk.bold(`Memory (${facts.length})`));
62
+ for (const f of facts) {
63
+ const layer = chalk.dim(`[${f.layer}]`);
64
+ console.log(` ${chalk.cyan(f.name)} ${layer} ${f.summary}`);
65
+ }
66
+ const capable = capableAgents('memory');
67
+ if (capable.length > 0) {
68
+ console.log(chalk.gray(`\nCapable agents: ${capable.join(', ')}`));
69
+ }
70
+ });
71
+ memoryCmd
72
+ .command('add <name>')
73
+ .description('Add or overwrite a user-layer memory fact')
74
+ .requiredOption('--body <text>', 'Fact body (markdown)')
75
+ .option('--sync', 'Immediately sync to all capable installed versions')
76
+ .action((name, options) => {
77
+ const filePath = addMemoryFact(name, options.body);
78
+ console.log(chalk.green(`Added memory fact: ${name}`));
79
+ console.log(chalk.gray(` ${filePath}`));
80
+ if (options.sync) {
81
+ for (const r of syncAllMemory(process.cwd())) {
82
+ console.log(chalk.gray(` synced ${r.agent}@${r.version} (${r.facts.length} facts)`));
83
+ }
84
+ }
85
+ else {
86
+ console.log(chalk.gray(' Run: agents memory sync (or agents sync) to fan out'));
87
+ }
88
+ });
89
+ memoryCmd
90
+ .command('remove <name>')
91
+ .alias('rm')
92
+ .description('Remove a user-layer memory fact')
93
+ .action((name) => {
94
+ if (removeMemoryFact(name)) {
95
+ console.log(chalk.green(`Removed memory fact: ${name}`));
96
+ }
97
+ else {
98
+ console.error(chalk.red(`No user-layer fact named "${name}"`));
99
+ process.exit(1);
100
+ }
101
+ });
102
+ memoryCmd
103
+ .command('view <name>')
104
+ .description('Print a memory fact (winning layer)')
105
+ .action((name) => {
106
+ const fact = readMemoryFact(name, process.cwd());
107
+ if (!fact) {
108
+ console.error(chalk.red(`No memory fact named "${name}"`));
109
+ process.exit(1);
110
+ }
111
+ console.log(chalk.dim(`# ${fact.name} [${fact.layer}] ${fact.path}`));
112
+ console.log(fs.readFileSync(fact.path, 'utf-8'));
113
+ });
114
+ memoryCmd
115
+ .command('sync [agent]')
116
+ .description('Copy canonical memory into capable agent version homes')
117
+ .option('-a, --agent <agent>', 'Limit to one agent (or agent@version)')
118
+ .action(async (agentArg, options) => {
119
+ ensureUserMemoryDir();
120
+ const input = agentArg || options.agent;
121
+ if (input) {
122
+ const parts = input.split('@');
123
+ const resolved = resolveAgentName(parts[0]);
124
+ if (!resolved) {
125
+ console.error(chalk.red(formatAgentError(parts[0], capableAgents('memory'))));
126
+ process.exit(1);
127
+ }
128
+ const agent = resolved;
129
+ const versions = parts[1]
130
+ ? [parts[1]]
131
+ : listInstalledVersions(agent);
132
+ if (versions.length === 0) {
133
+ console.log(chalk.yellow(`No installed versions of ${agentLabel(agent)}`));
134
+ return;
135
+ }
136
+ for (const version of versions) {
137
+ const home = getVersionHomePath(agent, version);
138
+ const facts = syncMemoryToVersionHome(agent, home, process.cwd());
139
+ console.log(chalk.green(`Synced ${facts.length} fact(s) → ${agent}@${version}`));
140
+ }
141
+ return;
142
+ }
143
+ const results = syncAllMemory(process.cwd());
144
+ if (results.length === 0) {
145
+ console.log(chalk.gray('No memory-capable agent versions installed.'));
146
+ return;
147
+ }
148
+ for (const r of results) {
149
+ console.log(chalk.green(`Synced ${r.facts.length} fact(s) → ${r.agent}@${r.version}`));
150
+ }
151
+ });
152
+ }
153
+ function syncAllMemory(cwd) {
154
+ const out = [];
155
+ for (const agent of ALL_AGENT_IDS) {
156
+ if (!supports(agent, 'memory').ok)
157
+ continue;
158
+ for (const version of listInstalledVersions(agent)) {
159
+ const home = getVersionHomePath(agent, version);
160
+ out.push({ agent, version, facts: syncMemoryToVersionHome(agent, home, cwd) });
161
+ }
162
+ }
163
+ return out;
164
+ }
@@ -1,15 +1,20 @@
1
1
  /**
2
- * `agents message <target> <text>` — send a message to a running agent.
2
+ * `agents message <target> <text>` — send a message to a running or parked agent.
3
3
  *
4
- * Unifies two delivery paths behind one verb:
5
- * - a live LOCAL/teams/loop agentenqueue into its file-spool mailbox; the
6
- * PreToolUse hook injects it at the agent's next tool call.
7
- * - a CLOUD task idthe existing provider follow-up path (was
8
- * `agents cloud message`).
4
+ * Delivery is routed by agent state (RUSH-1474):
5
+ * - running, between tool callsmailbox spool (PreToolUse inject)
6
+ * - parked on AskUserQuestion with a tmux/iterm/pty rail keystroke inject
7
+ * - parked headless (no rail)`agents run --resume <id> -- <answer>`
8
+ * - cloud task → provider.message()
9
9
  *
10
10
  * Cross-host is handled one layer up: `--host <h>` routes the whole command over
11
11
  * ssh via `REMOTE_PASSTHROUGH` (see src/lib/hosts/passthrough.ts), so the box is
12
12
  * written on the host that actually owns the agent.
13
+ *
14
+ * For local agents, the message is tied to the agent's current open feed block
15
+ * (if any). The first answer to a block wins: a second concurrent answer is
16
+ * rejected with the surface that already answered. Delivery receipts
17
+ * (queued → consumed → continued) are surfaced in the feed store.
13
18
  */
14
19
  import type { Command } from 'commander';
15
20
  export declare function registerMessageCommand(program: Command): void;