@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/message.js
CHANGED
|
@@ -1,15 +1,119 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
2
3
|
import { die } from '../lib/format.js';
|
|
3
4
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
4
5
|
import { getTaskById, updateTaskStatus } from '../lib/cloud/store.js';
|
|
5
6
|
import { resolveProvider } from '../lib/cloud/registry.js';
|
|
6
7
|
import { mailboxDir, enqueue } from '../lib/mailbox.js';
|
|
7
|
-
import { resolveMessageTarget } from '../lib/mailbox-target.js';
|
|
8
|
+
import { resolveMessageTarget, mailboxIdForActiveSession } from '../lib/mailbox-target.js';
|
|
9
|
+
import { blockIdForSession, listBlocks, readBlock, recordAnswer, recordMessageReceipt, } from '../lib/feed.js';
|
|
10
|
+
import { verifyOperatorIdentity } from '../lib/operator.js';
|
|
11
|
+
import { resolveAnswerRoute, resumeArgv, } from '../lib/answer-router.js';
|
|
12
|
+
import { injectIntoTerminal } from '../lib/terminal/inject.js';
|
|
13
|
+
/** Find the still-open block addressed to `mailboxId`, if any. */
|
|
14
|
+
function findOpenBlockForMailbox(mailboxId) {
|
|
15
|
+
// Fast path: the mailbox id is usually the session id, so the block id is
|
|
16
|
+
// directly derivable. This avoids scanning the whole feed store.
|
|
17
|
+
const direct = readBlock(blockIdForSession(mailboxId));
|
|
18
|
+
if (direct && direct.mailboxId === mailboxId)
|
|
19
|
+
return direct;
|
|
20
|
+
// Fallback: scan (agentId-based mailbox ids, rare).
|
|
21
|
+
return listBlocks().find((b) => b.mailboxId === mailboxId);
|
|
22
|
+
}
|
|
23
|
+
/** Live session whose mailbox id equals `mailboxId`. */
|
|
24
|
+
function findSessionForMailbox(mailboxId, sessions) {
|
|
25
|
+
return sessions.find((s) => mailboxIdForActiveSession(s) === mailboxId);
|
|
26
|
+
}
|
|
27
|
+
/** Claim first-answer-wins on the open block; dies if already answered / unauthorized. */
|
|
28
|
+
function claimBlockAnswer(block, opts) {
|
|
29
|
+
if (!block)
|
|
30
|
+
return;
|
|
31
|
+
const operatorId = opts.as;
|
|
32
|
+
// High-consequence answers require env-proven identity (AGENTS_OPERATOR_ID),
|
|
33
|
+
// not merely a caller-supplied known --as id (RUSH-1619).
|
|
34
|
+
const verified = verifyOperatorIdentity(operatorId);
|
|
35
|
+
const claim = recordAnswer(block.blockId, {
|
|
36
|
+
answeredBy: opts.from,
|
|
37
|
+
answeredFrom: opts.surface || 'cli',
|
|
38
|
+
operatorId,
|
|
39
|
+
verified,
|
|
40
|
+
});
|
|
41
|
+
if (!claim.ok) {
|
|
42
|
+
if ('unauthorized' in claim) {
|
|
43
|
+
die(`Not authorized: ${claim.reason}`);
|
|
44
|
+
}
|
|
45
|
+
const who = claim.existing.answeredFrom + (claim.existing.answeredBy ? ` (${claim.existing.answeredBy})` : '');
|
|
46
|
+
die(`This question was already answered by ${who}.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function deliverViaMailbox(mailboxId, text, block, opts) {
|
|
50
|
+
const msgId = enqueue(mailboxDir(mailboxId), {
|
|
51
|
+
to: mailboxId,
|
|
52
|
+
text,
|
|
53
|
+
from: opts.from,
|
|
54
|
+
blockId: block?.blockId,
|
|
55
|
+
});
|
|
56
|
+
if (block) {
|
|
57
|
+
recordMessageReceipt(block.blockId, {
|
|
58
|
+
msgId,
|
|
59
|
+
status: 'queued',
|
|
60
|
+
at: new Date().toISOString(),
|
|
61
|
+
from: opts.from,
|
|
62
|
+
});
|
|
63
|
+
console.log(chalk.green(`Queued message ${msgId} for ${mailboxId}. `) +
|
|
64
|
+
chalk.dim(`Answer tied to ${block.blockId}; the agent will see it at its next tool call.`));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
console.log(chalk.green(`Queued message ${msgId} for ${mailboxId}. `) +
|
|
68
|
+
chalk.dim('The agent will see it at its next tool call.'));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function deliverViaInject(route, mailboxId) {
|
|
72
|
+
if (!route.inject || route.payload == null) {
|
|
73
|
+
die(`Internal error: inject route missing target/payload for ${mailboxId}.`);
|
|
74
|
+
}
|
|
75
|
+
const result = await injectIntoTerminal(route.inject, route.payload, {
|
|
76
|
+
// Digit selection and free text both need Enter to submit the TUI choice.
|
|
77
|
+
enter: true,
|
|
78
|
+
// Digit+Enter as two writes is safer for Ink TUI.
|
|
79
|
+
combined: false,
|
|
80
|
+
});
|
|
81
|
+
if (!result.ok) {
|
|
82
|
+
die(`Failed to inject answer into ${route.inject.backend}: ${result.error ?? 'unknown error'}`);
|
|
83
|
+
}
|
|
84
|
+
console.log(chalk.green(`Answered ${mailboxId} via ${route.inject.backend}. `) +
|
|
85
|
+
chalk.dim(route.reason));
|
|
86
|
+
}
|
|
87
|
+
async function deliverViaResume(route, mailboxId) {
|
|
88
|
+
if (route.kind !== 'resume') {
|
|
89
|
+
die(`Internal error: resume route incomplete for ${mailboxId}.`);
|
|
90
|
+
}
|
|
91
|
+
const argv = resumeArgv(route);
|
|
92
|
+
// Spawn the same agents binary the user invoked (process.argv[1]) so version
|
|
93
|
+
// pins and wrappers stay consistent. Detach so the resume can take over a TTY
|
|
94
|
+
// when interactive; for feed answers we pass the prompt non-interactively.
|
|
95
|
+
const bin = process.argv[1] ?? 'agents';
|
|
96
|
+
const child = spawn(process.execPath, [bin, ...argv], {
|
|
97
|
+
stdio: 'inherit',
|
|
98
|
+
env: process.env,
|
|
99
|
+
});
|
|
100
|
+
const code = await new Promise((resolve) => {
|
|
101
|
+
child.on('exit', (c) => resolve(c ?? 1));
|
|
102
|
+
child.on('error', () => resolve(1));
|
|
103
|
+
});
|
|
104
|
+
if (code !== 0) {
|
|
105
|
+
die(`Resume of ${mailboxId} exited with code ${code}. Tried: agents ${argv.join(' ')}`);
|
|
106
|
+
}
|
|
107
|
+
console.log(chalk.green(`Resumed ${mailboxId} with answer. `) +
|
|
108
|
+
chalk.dim(route.reason));
|
|
109
|
+
}
|
|
8
110
|
export function registerMessageCommand(program) {
|
|
9
111
|
program
|
|
10
112
|
.command('message <target> <text>')
|
|
11
|
-
.description('Send a message to a running agent (
|
|
113
|
+
.description('Send a message to a running or parked agent (mailbox / PTY-select / resume by runtime).')
|
|
12
114
|
.option('--from <who>', 'Label recorded as the sender of this message')
|
|
115
|
+
.option('--as <operator>', 'Verified operator id answering a high-consequence block')
|
|
116
|
+
.option('--surface <surface>', 'Surface that is sending this answer (feed, terminal, etc.)', 'cli')
|
|
13
117
|
.action(async (target, text, opts) => {
|
|
14
118
|
if (!target.trim()) {
|
|
15
119
|
die('Target must be a session/agent id or cloud task id. Run `agents sessions --active` to list running agents.');
|
|
@@ -32,9 +136,40 @@ export function registerMessageCommand(program) {
|
|
|
32
136
|
}
|
|
33
137
|
case 'local': {
|
|
34
138
|
try {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
139
|
+
const block = findOpenBlockForMailbox(res.id);
|
|
140
|
+
const session = findSessionForMailbox(res.id, sessions);
|
|
141
|
+
const route = resolveAnswerRoute({
|
|
142
|
+
mailboxId: res.id,
|
|
143
|
+
answer: text,
|
|
144
|
+
block,
|
|
145
|
+
session,
|
|
146
|
+
});
|
|
147
|
+
if (route.kind === 'refuse') {
|
|
148
|
+
die(route.reason);
|
|
149
|
+
}
|
|
150
|
+
// First-answer-wins for any path that closes an open block.
|
|
151
|
+
claimBlockAnswer(block, opts);
|
|
152
|
+
if (route.kind === 'mailbox') {
|
|
153
|
+
await deliverViaMailbox(res.id, text, block, opts);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (route.kind === 'tmux' || route.kind === 'iterm' || route.kind === 'pty') {
|
|
157
|
+
await deliverViaInject(route, res.id);
|
|
158
|
+
if (block) {
|
|
159
|
+
recordMessageReceipt(block.blockId, {
|
|
160
|
+
msgId: `inject-${Date.now()}`,
|
|
161
|
+
status: 'queued',
|
|
162
|
+
at: new Date().toISOString(),
|
|
163
|
+
from: opts.from,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (route.kind === 'resume') {
|
|
169
|
+
await deliverViaResume(route, res.id);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
die(`Unknown delivery route: ${route.kind}`);
|
|
38
173
|
}
|
|
39
174
|
catch (err) {
|
|
40
175
|
die(err.message);
|
|
@@ -156,6 +156,18 @@ export function registerRoutinesCommands(program) {
|
|
|
156
156
|
|
|
157
157
|
The background scheduler auto-starts the first time you add a routine.
|
|
158
158
|
Manage it with 'agents routines start|stop|status'.
|
|
159
|
+
|
|
160
|
+
Version / credit failover (same semantics as 'agents run'):
|
|
161
|
+
- Omit 'version:' to let the configured run strategy (default: balanced)
|
|
162
|
+
pick a healthy install and skip accounts that are out of credits or
|
|
163
|
+
rate-limited. Pin with 'version: 2.1.x' when you want one install only.
|
|
164
|
+
- Foreground 'agents routines run' re-dispatches to the next healthy
|
|
165
|
+
same-agent account when a mid-run rate/usage limit is detected.
|
|
166
|
+
- Detached/daemon fires use the pre-flight pick only (next tick re-selects).
|
|
167
|
+
- Diagnostic lines log which account was picked, which were skipped, and
|
|
168
|
+
each failover hop: look for "[agents] routine <name>:" in the run log.
|
|
169
|
+
- Headless Claude auth: store CLAUDE_CODE_OAUTH_TOKEN in the 'claude'
|
|
170
|
+
secrets bundle so the daemon can inject it into routine spawns.
|
|
159
171
|
`,
|
|
160
172
|
});
|
|
161
173
|
routinesCmd
|
|
@@ -20,5 +20,6 @@
|
|
|
20
20
|
* writes and produces the backup.
|
|
21
21
|
*/
|
|
22
22
|
import type { Command } from 'commander';
|
|
23
|
-
/** Register
|
|
23
|
+
/** Register the keychain-migration commands (`migrate-acl`, `rekey`) on the
|
|
24
|
+
* parent secrets Command. */
|
|
24
25
|
export declare function registerSecretsMigrateAclCommand(secrets: Command): void;
|
|
@@ -23,21 +23,24 @@ import chalk from 'chalk';
|
|
|
23
23
|
import * as crypto from 'crypto';
|
|
24
24
|
import * as fs from 'fs';
|
|
25
25
|
import * as path from 'path';
|
|
26
|
-
import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainToken, listKeychainItems, listLegacyKeychainItems, listOrphanedKeychainItems, migrateOrphanedKeychainItems, setKeychainToken, } from '../lib/secrets/index.js';
|
|
26
|
+
import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainToken, listKeychainItems, listLegacyKeychainItems, listOrphanedKeychainItems, migrateOrphanedKeychainItems, rekeyServiceNames, rekeyStatus, setKeychainToken, withRawKeychainServiceNames, } from '../lib/secrets/index.js';
|
|
27
27
|
import { getBackupsDir } from '../lib/state.js';
|
|
28
28
|
import { encryptBlob, MIN_PASSPHRASE_LEN } from '../lib/secrets/sync.js';
|
|
29
29
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
30
30
|
const ITEM_PREFIX = 'agents-cli.';
|
|
31
31
|
function enumerateItems() {
|
|
32
32
|
const seen = new Map();
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
// Raw scope: operate on the literal enumerated names (see #316).
|
|
34
|
+
withRawKeychainServiceNames(() => {
|
|
35
|
+
for (const item of listKeychainItems(ITEM_PREFIX)) {
|
|
36
|
+
// hasKeychainToken with sync=false probes the non-synced keychain; the
|
|
37
|
+
// helper's list returns both. We don't try to distinguish — re-write with
|
|
38
|
+
// sync=false by default and only flip to sync=true if the value is only
|
|
39
|
+
// readable via the synced-only probe.
|
|
40
|
+
const localExists = hasKeychainToken(item);
|
|
41
|
+
seen.set(item, { item, sync: !localExists });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
41
44
|
return [...seen.values()];
|
|
42
45
|
}
|
|
43
46
|
function writeEncryptedBackup(records, passphrase) {
|
|
@@ -63,6 +66,12 @@ async function promptPassphrase() {
|
|
|
63
66
|
return first;
|
|
64
67
|
}
|
|
65
68
|
function migrateOne(record) {
|
|
69
|
+
// Raw scope: `item` is a literal service name enumerated from the helper —
|
|
70
|
+
// possibly a pre-re-key cleartext leftover — and this rewrite must target
|
|
71
|
+
// exactly that item, never its hashed transform (see #316).
|
|
72
|
+
return withRawKeychainServiceNames(() => migrateOneRaw(record));
|
|
73
|
+
}
|
|
74
|
+
function migrateOneRaw(record) {
|
|
66
75
|
const { item, value } = record;
|
|
67
76
|
// Delete + re-add to force macOS to bind a fresh ACL on the new item.
|
|
68
77
|
// SecItemUpdate preserves the existing ACL, so an in-place rewrite would
|
|
@@ -97,8 +106,10 @@ function migrateOne(record) {
|
|
|
97
106
|
}
|
|
98
107
|
return { item, status: 'ok' };
|
|
99
108
|
}
|
|
100
|
-
/** Register
|
|
109
|
+
/** Register the keychain-migration commands (`migrate-acl`, `rekey`) on the
|
|
110
|
+
* parent secrets Command. */
|
|
101
111
|
export function registerSecretsMigrateAclCommand(secrets) {
|
|
112
|
+
registerSecretsRekeyCommand(secrets);
|
|
102
113
|
secrets
|
|
103
114
|
.command('migrate-acl')
|
|
104
115
|
.description('Refresh legacy keychain ACLs and re-home items stranded in a stale access group. Dry-run by default.')
|
|
@@ -122,9 +133,14 @@ export function registerSecretsMigrateAclCommand(secrets) {
|
|
|
122
133
|
// pre-#279 non-concrete group, invisible to the pinned queries.
|
|
123
134
|
// Re-homed by the helper behind a single Touch ID.
|
|
124
135
|
// Default: only legacy stragglers for (a); `--all` forces a full rewrite.
|
|
125
|
-
|
|
136
|
+
// Raw scope: migrate-acl operates on the literal names the helper
|
|
137
|
+
// enumerates (which may include pre-re-key cleartext leftovers) — the
|
|
138
|
+
// #316 name-hashing transform must not rewrite them.
|
|
139
|
+
const names = opts.all
|
|
140
|
+
? withRawKeychainServiceNames(() => listKeychainItems(prefix))
|
|
141
|
+
: listLegacyKeychainItems(prefix);
|
|
126
142
|
const items = names.map((item) => {
|
|
127
|
-
const localExists = hasKeychainToken(item);
|
|
143
|
+
const localExists = withRawKeychainServiceNames(() => hasKeychainToken(item));
|
|
128
144
|
return { item, sync: !localExists };
|
|
129
145
|
});
|
|
130
146
|
const orphans = listOrphanedKeychainItems(prefix);
|
|
@@ -160,7 +176,7 @@ export function registerSecretsMigrateAclCommand(secrets) {
|
|
|
160
176
|
// read failure here counts the legacy items as failed but does NOT abort —
|
|
161
177
|
// the orphan sweep (b) is independent and must still run.
|
|
162
178
|
if (items.length > 0) {
|
|
163
|
-
const fetched = getKeychainTokens(items.map((i) => i.item));
|
|
179
|
+
const fetched = withRawKeychainServiceNames(() => getKeychainTokens(items.map((i) => i.item)));
|
|
164
180
|
const records = [];
|
|
165
181
|
for (const { item, sync } of items) {
|
|
166
182
|
const value = fetched.get(item);
|
|
@@ -257,3 +273,62 @@ export function registerSecretsMigrateAclCommand(secrets) {
|
|
|
257
273
|
}
|
|
258
274
|
});
|
|
259
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* `agents secrets rekey` — the one-time #316 migration: replace enumerable
|
|
278
|
+
* cleartext keychain service names (`agents-cli.secrets.<bundle>.<KEY>`, …)
|
|
279
|
+
* with opaque HMAC-hashed names. Normally runs automatically on the first
|
|
280
|
+
* interactive keychain use after upgrade; this command exists for headless
|
|
281
|
+
* machines, retrying after a cancelled Touch ID, and prefix-restricted test
|
|
282
|
+
* runs. Idempotent — safe to re-run any time.
|
|
283
|
+
*/
|
|
284
|
+
function registerSecretsRekeyCommand(secrets) {
|
|
285
|
+
secrets
|
|
286
|
+
.command('rekey')
|
|
287
|
+
.description('Replace enumerable keychain service names with opaque HMAC-hashed names (one-time; idempotent). macOS only.')
|
|
288
|
+
.option('--status', 'Report re-key state without changing anything')
|
|
289
|
+
.option('--prefix <p...>', 'Restrict to cleartext services starting with PREFIX (partial run: moves matching items but does NOT activate hashed naming)')
|
|
290
|
+
.action((opts) => {
|
|
291
|
+
try {
|
|
292
|
+
if (process.platform !== 'darwin') {
|
|
293
|
+
throw new Error('secrets rekey is macOS-only. Linux/Windows service names are not enumerable via the keychain helper.');
|
|
294
|
+
}
|
|
295
|
+
if (opts.status) {
|
|
296
|
+
const st = rekeyStatus();
|
|
297
|
+
console.log(st.migrated
|
|
298
|
+
? chalk.green('Hashed service names: ACTIVE')
|
|
299
|
+
: chalk.yellow('Hashed service names: not yet active'));
|
|
300
|
+
console.log(` HMAC key present: ${st.hasKey ? 'yes' : 'no'}`);
|
|
301
|
+
if (!st.enumerationOk) {
|
|
302
|
+
console.log(chalk.red(' Keychain enumeration unavailable (locked keybag?) — counts below are unreliable.'));
|
|
303
|
+
}
|
|
304
|
+
console.log(` Cleartext-named items remaining: ${st.cleartext.length}`);
|
|
305
|
+
if (st.pendingDeletes > 0) {
|
|
306
|
+
console.log(chalk.yellow(` Pending deletes from an interrupted run: ${st.pendingDeletes} (finished automatically on next use)`));
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
for (const p of opts.prefix ?? []) {
|
|
311
|
+
if (!p.startsWith(ITEM_PREFIX)) {
|
|
312
|
+
throw new Error(`--prefix must start with '${ITEM_PREFIX}' to avoid touching unrelated Keychain items (got '${p}').`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const report = rekeyServiceNames({
|
|
316
|
+
prefixes: opts.prefix,
|
|
317
|
+
announce: true,
|
|
318
|
+
log: (line) => console.log(line),
|
|
319
|
+
});
|
|
320
|
+
if (report.failed.length > 0)
|
|
321
|
+
process.exit(1);
|
|
322
|
+
if (report.nothingToDo && report.activated) {
|
|
323
|
+
console.log(chalk.green('Nothing to re-key — hashed service names are active.'));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
const msg = err.message;
|
|
328
|
+
console.error(chalk.red(msg));
|
|
329
|
+
// Touch ID cancel exits 4 (matching the helper's contract); re-running
|
|
330
|
+
// resumes where it left off.
|
|
331
|
+
process.exit(/touch id cancelled/i.test(msg) ? 4 : 1);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
package/dist/commands/secrets.js
CHANGED
|
@@ -208,10 +208,13 @@ async function importFromICloud(bundleName, opts) {
|
|
|
208
208
|
parts.push(`skipped ${result.skipped} (already set, pass --force)`);
|
|
209
209
|
if (result.missing.length)
|
|
210
210
|
parts.push(`unreadable (left in iCloud): ${result.missing.join(', ')}`);
|
|
211
|
+
if (result.unimportable.length)
|
|
212
|
+
parts.push(`reserved, not importable (left in iCloud): ${result.unimportable.join(', ')}`);
|
|
211
213
|
if (opts.purge)
|
|
212
214
|
parts.push(`purged ${result.purged} iCloud item(s)`);
|
|
213
215
|
const line = `${candidate.name}: ${parts.join(', ')}`;
|
|
214
|
-
|
|
216
|
+
const warn = result.missing.length > 0 || result.unimportable.length > 0;
|
|
217
|
+
console.log(warn ? chalk.yellow(line) : chalk.green(line));
|
|
215
218
|
}
|
|
216
219
|
}
|
|
217
220
|
/**
|
|
@@ -25,7 +25,7 @@ import { machineId, normalizeHost } from '../lib/session/sync/config.js';
|
|
|
25
25
|
import { gatherRemoteActive, NO_FANOUT_ENV } from '../lib/session/remote-active.js';
|
|
26
26
|
import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
|
|
27
27
|
import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/session/width.js';
|
|
28
|
-
import { discoverSessions, countSessionsInScope, resolveSessionById, searchContentIndex } from '../lib/session/discover.js';
|
|
28
|
+
import { discoverSessions, countSessionsInScope, resolveSessionById, searchContentIndex, getSessionRoots } from '../lib/session/discover.js';
|
|
29
29
|
import { filterTeamSessions } from '../lib/session/team-filter.js';
|
|
30
30
|
import { parseSession } from '../lib/session/parse.js';
|
|
31
31
|
import { runRemoteSessions, buildForwardedArgs } from '../lib/session/remote.js';
|
|
@@ -796,6 +796,14 @@ async function sessionsAction(query, options) {
|
|
|
796
796
|
if (options.device && options.device.length > 0) {
|
|
797
797
|
options.host = [...(options.host ?? []), ...options.device];
|
|
798
798
|
}
|
|
799
|
+
// --roots: emit the local session-scan directories, per agent, as JSON. A pure
|
|
800
|
+
// machine-readable query (no listing/render) — external watchers (the Factory
|
|
801
|
+
// extension's fs.watch) read it to track the same dirs the CLI scans, instead
|
|
802
|
+
// of hardcoding `~/.claude|.codex|.gemini`. Always local; ignores other flags.
|
|
803
|
+
if (options.roots) {
|
|
804
|
+
process.stdout.write(JSON.stringify(getSessionRoots(), null, 2) + '\n');
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
799
807
|
// --host WITHOUT --active. `--json` fans the recent listing out and emits ONE
|
|
800
808
|
// clean merged SessionMeta[] array (same shape as the local --json path), for
|
|
801
809
|
// scripts/extensions that JSON.parse a remote's history. Without --json it
|
|
@@ -2047,6 +2055,7 @@ export function registerSessionsCommands(program) {
|
|
|
2047
2055
|
.option('--artifacts', 'List all files written or edited during a session')
|
|
2048
2056
|
.option('--artifact <name>', 'Read a specific artifact by filename or path (outputs to stdout)')
|
|
2049
2057
|
.option('--active', 'Show only sessions running right now across terminals, teams, cloud, and headless agents')
|
|
2058
|
+
.option('--roots', 'With --json: emit the on-disk directories scanned for session transcripts, per agent (for external watchers)')
|
|
2050
2059
|
.option('--local', 'Only this machine — skip the cross-machine SSH fan-out (default listing and --active)')
|
|
2051
2060
|
.option('--waiting', 'With --active: show only sessions waiting on your input (exits non-zero if any)')
|
|
2052
2061
|
.option('--tree', 'Group the listing by directory; drops the id/version columns for readability')
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { die } from '../lib/format.js';
|
|
3
|
+
import { getGitRoot } from '../lib/git.js';
|
|
3
4
|
import { execFile } from 'child_process';
|
|
4
5
|
import { promisify } from 'util';
|
|
5
6
|
import * as fs from 'fs/promises';
|
|
@@ -14,10 +15,11 @@ function isValidTerminalId(id) {
|
|
|
14
15
|
// hostile or buggy caller can't inject path traversal or shell metachars.
|
|
15
16
|
return /^[A-Za-z0-9._-]+$/.test(id) && id.length > 0 && id.length <= 128;
|
|
16
17
|
}
|
|
18
|
+
// Thin command-layer wrapper over the shared lib/git.ts `getGitRoot`: turns its
|
|
19
|
+
// throw into a clean `die()` message. The git plumbing lives in one place now.
|
|
17
20
|
async function gitRoot(cwd) {
|
|
18
21
|
try {
|
|
19
|
-
|
|
20
|
-
return stdout.trim();
|
|
22
|
+
return await getGitRoot(cwd);
|
|
21
23
|
}
|
|
22
24
|
catch {
|
|
23
25
|
die(`Not inside a git repo: ${cwd}`);
|
package/dist/index.js
CHANGED
|
@@ -50,7 +50,7 @@ if (IS_DEV_BUILD) {
|
|
|
50
50
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
51
51
|
// first byte of output), the registry maps a command name to a thunk that
|
|
52
52
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
53
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadFeed, } from './lib/startup/command-registry.js';
|
|
53
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadFeed, } from './lib/startup/command-registry.js';
|
|
54
54
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
55
55
|
import { renderWhatsNew } from './lib/whats-new.js';
|
|
56
56
|
import { emit, redactArgs } from './lib/events.js';
|
|
@@ -73,6 +73,7 @@ program
|
|
|
73
73
|
.name('agents')
|
|
74
74
|
.description('Environment manager for AI agents')
|
|
75
75
|
.version(VERSION)
|
|
76
|
+
.option('--verbose', 'Show startup self-heal details on stderr')
|
|
76
77
|
.helpOption('-h, --help', 'Show help')
|
|
77
78
|
.addHelpCommand(false);
|
|
78
79
|
// ─── Audit backbone ────────────────────────────────────────────────────────────
|
|
@@ -207,6 +208,7 @@ Automation tips:
|
|
|
207
208
|
Options:
|
|
208
209
|
-V, --version Show version number
|
|
209
210
|
-h, --help Show help
|
|
211
|
+
--verbose Show startup self-heal details on stderr
|
|
210
212
|
|
|
211
213
|
System config lives in ~/.agents/.system/. Run 'agents <command> --help' for details.
|
|
212
214
|
`;
|
|
@@ -464,8 +466,8 @@ async function checkForUpdates() {
|
|
|
464
466
|
}
|
|
465
467
|
}
|
|
466
468
|
}
|
|
467
|
-
async function maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequested) {
|
|
468
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
469
|
+
async function maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequested, verboseStartup) {
|
|
470
|
+
if (!verboseStartup && (!process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
469
471
|
return;
|
|
470
472
|
}
|
|
471
473
|
// Pure documentation paths must never trigger interactive repair — mirrors
|
|
@@ -487,8 +489,12 @@ async function maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequ
|
|
|
487
489
|
// (a real native binary shadowing the shim) or is worth saying once (a PATH entry
|
|
488
490
|
// just added). Suppression is persistent and keyed to the condition — a new
|
|
489
491
|
// terminal no longer re-nags (the old per-PPID sentinel did, every shell).
|
|
490
|
-
const {
|
|
491
|
-
const
|
|
492
|
+
const { runInteractiveShimHeal } = await import('./lib/shim-heal.js');
|
|
493
|
+
const { summarizeSelfHeal } = await import('./lib/self-heal/registry.js');
|
|
494
|
+
const { noticeLines, report } = await runInteractiveShimHeal();
|
|
495
|
+
if (verboseStartup) {
|
|
496
|
+
process.stderr.write(`[agents] startup self-heal: ${summarizeSelfHeal(report)}\n`);
|
|
497
|
+
}
|
|
492
498
|
if (noticeLines) {
|
|
493
499
|
for (const line of noticeLines)
|
|
494
500
|
console.log(chalk.gray(line));
|
|
@@ -499,17 +505,8 @@ async function maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequ
|
|
|
499
505
|
// close over entry-point-local state (program re-parsing, VERSION, the npm
|
|
500
506
|
// upgrade helpers). The lazy registrar and the all-commands fallback below both
|
|
501
507
|
// call them, so the behavior is identical to the old eager registration.
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
p.command('memory', { hidden: true })
|
|
505
|
-
.allowUnknownOption()
|
|
506
|
-
.allowExcessArguments()
|
|
507
|
-
.action(() => {
|
|
508
|
-
console.error(chalk.red('"agents memory" has been renamed to "agents rules".'));
|
|
509
|
-
console.error(chalk.gray('Run "agents rules --help" for usage.\n'));
|
|
510
|
-
process.exit(1);
|
|
511
|
-
});
|
|
512
|
-
}
|
|
508
|
+
// memory is a first-class resource command (see commands/memory.ts via
|
|
509
|
+
// COMMAND_LOADERS). The old memory→rules tombstone was removed in RUSH-1330.
|
|
513
510
|
/** Deprecated `perms` alias — re-parses as `permissions`. */
|
|
514
511
|
function registerPermsAliasCommand(p) {
|
|
515
512
|
p.command('perms', { hidden: true })
|
|
@@ -616,9 +613,6 @@ async function reg(loader) {
|
|
|
616
613
|
*/
|
|
617
614
|
async function registerEagerForRequest(name) {
|
|
618
615
|
switch (name) {
|
|
619
|
-
case 'memory':
|
|
620
|
-
registerMemoryCommand(program);
|
|
621
|
-
return true;
|
|
622
616
|
case 'perms':
|
|
623
617
|
// The action re-parses as `permissions`, so that target must exist too.
|
|
624
618
|
registerPermsAliasCommand(program);
|
|
@@ -659,7 +653,7 @@ async function registerAllEagerCommands() {
|
|
|
659
653
|
await reg(loadHooks);
|
|
660
654
|
await reg(loadSkills);
|
|
661
655
|
await reg(loadRules);
|
|
662
|
-
|
|
656
|
+
await reg(loadMemory);
|
|
663
657
|
await reg(loadPermissions);
|
|
664
658
|
registerPermsAliasCommand(program);
|
|
665
659
|
await reg(loadMcp);
|
|
@@ -768,6 +762,7 @@ program.on('command:*', (operands) => {
|
|
|
768
762
|
// and whether the update check + background sync run at all.
|
|
769
763
|
const passedArgs = process.argv.slice(2);
|
|
770
764
|
const requestedCommand = passedArgs.find((arg) => !arg.startsWith('-'));
|
|
765
|
+
const verboseStartup = passedArgs.includes('--verbose');
|
|
771
766
|
// Help and version output are pure documentation — they must never gate on
|
|
772
767
|
// setup, otherwise `agents <cmd> --help` becomes useless on a fresh box.
|
|
773
768
|
const helpOrVersionRequested = passedArgs.some((arg) => arg === '--help' || arg === '-h' || arg === '--version' || arg === '-V');
|
|
@@ -922,7 +917,7 @@ if (passedArgs.length === 0) {
|
|
|
922
917
|
process.exit(0);
|
|
923
918
|
}
|
|
924
919
|
try {
|
|
925
|
-
await maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequested);
|
|
920
|
+
await maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequested, verboseStartup);
|
|
926
921
|
await program.parseAsync();
|
|
927
922
|
}
|
|
928
923
|
catch (err) {
|