@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.
- package/CHANGELOG.md +57 -2
- package/README.md +12 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.d.ts +3 -0
- package/dist/commands/cloud.js +2 -1
- package/dist/commands/exec.js +65 -1
- package/dist/commands/feed.d.ts +29 -0
- package/dist/commands/feed.js +237 -32
- package/dist/commands/memory.d.ts +9 -0
- package/dist/commands/memory.js +164 -0
- package/dist/commands/message.d.ts +11 -6
- package/dist/commands/message.js +140 -5
- package/dist/commands/routines.js +12 -0
- package/dist/commands/secrets-migrate.d.ts +2 -1
- package/dist/commands/secrets-migrate.js +88 -13
- package/dist/commands/secrets.js +4 -1
- package/dist/commands/sessions.js +10 -1
- package/dist/commands/worktree.js +4 -2
- package/dist/index.js +16 -21
- package/dist/lib/agents.js +249 -17
- package/dist/lib/answer-router.d.ts +75 -0
- package/dist/lib/answer-router.js +149 -0
- package/dist/lib/ask-classifier.d.ts +71 -0
- package/dist/lib/ask-classifier.js +197 -0
- package/dist/lib/cloud/antigravity.d.ts +0 -2
- package/dist/lib/cloud/antigravity.js +2 -17
- package/dist/lib/cloud/codex.js +3 -18
- package/dist/lib/cloud/rush.js +3 -15
- package/dist/lib/cloud/stream.js +2 -0
- package/dist/lib/cloud/types.d.ts +21 -0
- package/dist/lib/cloud/types.js +81 -0
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +12 -2
- package/dist/lib/crabbox/lease.d.ts +13 -0
- package/dist/lib/crabbox/lease.js +11 -2
- package/dist/lib/crabbox/progress.d.ts +62 -0
- package/dist/lib/crabbox/progress.js +129 -0
- package/dist/lib/events.js +4 -1
- package/dist/lib/exec.js +19 -1
- package/dist/lib/feed-outcome.d.ts +101 -0
- package/dist/lib/feed-outcome.js +244 -0
- package/dist/lib/feed-policy.d.ts +30 -0
- package/dist/lib/feed-policy.js +133 -0
- package/dist/lib/feed.d.ts +127 -3
- package/dist/lib/feed.js +416 -40
- package/dist/lib/git.d.ts +17 -1
- package/dist/lib/git.js +20 -1
- package/dist/lib/hooks.js +522 -12
- package/dist/lib/hosts/passthrough.d.ts +3 -3
- package/dist/lib/hosts/passthrough.js +3 -4
- package/dist/lib/mailbox-gc.d.ts +22 -0
- package/dist/lib/mailbox-gc.js +161 -0
- package/dist/lib/mailbox.d.ts +26 -2
- package/dist/lib/mailbox.js +80 -5
- package/dist/lib/mcp.js +82 -0
- package/dist/lib/memory.d.ts +55 -0
- package/dist/lib/memory.js +274 -0
- package/dist/lib/notify.d.ts +16 -0
- package/dist/lib/notify.js +61 -0
- package/dist/lib/operator.d.ts +26 -0
- package/dist/lib/operator.js +107 -0
- package/dist/lib/plugins.d.ts +35 -0
- package/dist/lib/plugins.js +217 -0
- package/dist/lib/remote-agents-json.d.ts +14 -0
- package/dist/lib/remote-agents-json.js +94 -0
- package/dist/lib/resources/mcp.js +44 -0
- package/dist/lib/resources/memory.d.ts +15 -0
- package/dist/lib/resources/memory.js +46 -0
- package/dist/lib/resources/types.d.ts +2 -2
- package/dist/lib/runner.d.ts +43 -0
- package/dist/lib/runner.js +323 -74
- package/dist/lib/sandbox.js +6 -0
- package/dist/lib/secrets/bundles.js +38 -13
- package/dist/lib/secrets/icloud-import.d.ts +12 -3
- package/dist/lib/secrets/icloud-import.js +37 -7
- package/dist/lib/secrets/index.d.ts +106 -2
- package/dist/lib/secrets/index.js +603 -28
- package/dist/lib/session/active.d.ts +18 -0
- package/dist/lib/session/active.js +47 -17
- package/dist/lib/session/db.d.ts +9 -1
- package/dist/lib/session/db.js +18 -3
- package/dist/lib/session/discover.d.ts +13 -0
- package/dist/lib/session/discover.js +31 -0
- package/dist/lib/session/parse.d.ts +8 -0
- package/dist/lib/session/parse.js +42 -21
- package/dist/lib/session/remote-active.js +8 -89
- package/dist/lib/session/state.d.ts +11 -0
- package/dist/lib/session/state.js +37 -0
- package/dist/lib/session/tail.d.ts +23 -4
- package/dist/lib/session/tail.js +34 -16
- package/dist/lib/session/throughput.d.ts +30 -0
- package/dist/lib/session/throughput.js +86 -0
- package/dist/lib/shim-heal.d.ts +12 -3
- package/dist/lib/shim-heal.js +12 -6
- package/dist/lib/staleness/detectors/subagents.js +57 -3
- package/dist/lib/staleness/writers/hooks.js +7 -3
- package/dist/lib/staleness/writers/subagents.js +37 -6
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/subagents.d.ts +52 -0
- package/dist/lib/subagents.js +315 -12
- package/dist/lib/teams/worktree.d.ts +8 -0
- package/dist/lib/teams/worktree.js +8 -0
- package/dist/lib/types.d.ts +10 -2
- package/dist/lib/versions.js +38 -48
- package/package.json +4 -3
- package/scripts/postinstall.js +61 -1
package/dist/commands/feed.js
CHANGED
|
@@ -1,69 +1,274 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { ensureFeedPublishHook, listBlocks } from '../lib/feed.js';
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
4
|
+
* Delivery is routed by agent state (RUSH-1474):
|
|
5
|
+
* - running, between tool calls → mailbox 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;
|