@phnx-labs/agents-cli 1.21.3 → 1.22.1
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 +136 -0
- package/README.md +32 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/computer-actions.js +1 -0
- package/dist/commands/doctor.js +34 -2
- package/dist/commands/exec.d.ts +27 -0
- package/dist/commands/exec.js +123 -6
- package/dist/commands/models.js +36 -1
- package/dist/commands/projects.js +120 -62
- package/dist/commands/sessions-backfill.d.ts +32 -0
- package/dist/commands/sessions-backfill.js +186 -0
- package/dist/commands/sessions.d.ts +17 -1
- package/dist/commands/sessions.js +317 -18
- package/dist/commands/teams.js +1 -1
- package/dist/commands/worktree.d.ts +3 -3
- package/dist/commands/worktree.js +35 -4
- package/dist/lib/app-bundle-install.d.ts +17 -0
- package/dist/lib/app-bundle-install.js +94 -0
- package/dist/lib/daemon.d.ts +5 -1
- package/dist/lib/daemon.js +63 -14
- package/dist/lib/devices/doctor-findings.js +12 -4
- package/dist/lib/devices/doctor-overview-cache.d.ts +45 -0
- package/dist/lib/devices/doctor-overview-cache.js +168 -0
- package/dist/lib/devices/fleet.js +7 -2
- package/dist/lib/devices/resolve-target.d.ts +6 -0
- package/dist/lib/devices/resolve-target.js +9 -3
- package/dist/lib/devices/self-host.d.ts +9 -0
- package/dist/lib/devices/self-host.js +61 -0
- package/dist/lib/exec.js +39 -8
- package/dist/lib/hosts/dispatch.d.ts +12 -0
- package/dist/lib/hosts/dispatch.js +23 -6
- package/dist/lib/hosts/passthrough.js +8 -6
- package/dist/lib/hosts/reconnect.d.ts +38 -0
- package/dist/lib/hosts/reconnect.js +85 -4
- package/dist/lib/hosts/run-target.js +14 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.js +27 -23
- package/dist/lib/model-tiers.d.ts +54 -0
- package/dist/lib/model-tiers.js +229 -0
- package/dist/lib/models.d.ts +3 -0
- package/dist/lib/models.js +44 -7
- package/dist/lib/pricing/prices.json +16 -1
- package/dist/lib/project-focus.d.ts +42 -0
- package/dist/lib/project-focus.js +80 -0
- package/dist/lib/project-schedule.d.ts +75 -0
- package/dist/lib/project-schedule.js +110 -0
- package/dist/lib/project-status.d.ts +7 -0
- package/dist/lib/project-status.js +9 -0
- package/dist/lib/projects.d.ts +11 -2
- package/dist/lib/projects.js +57 -9
- package/dist/lib/redact.d.ts +2 -0
- package/dist/lib/redact.js +22 -0
- package/dist/lib/remote-agents-json.d.ts +2 -0
- package/dist/lib/remote-agents-json.js +3 -3
- package/dist/lib/rotate.d.ts +84 -1
- package/dist/lib/rotate.js +155 -5
- package/dist/lib/runner.d.ts +4 -2
- package/dist/lib/runner.js +13 -4
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/install-helper.js +28 -31
- package/dist/lib/session/bash-command.js +60 -9
- package/dist/lib/session/db.d.ts +7 -1
- package/dist/lib/session/db.js +301 -32
- package/dist/lib/session/discover.d.ts +40 -7
- package/dist/lib/session/discover.js +144 -83
- package/dist/lib/session/parse.d.ts +8 -1
- package/dist/lib/session/parse.js +83 -32
- package/dist/lib/session/remote-list.d.ts +71 -0
- package/dist/lib/session/remote-list.js +410 -2
- package/dist/lib/session/shell-programs.d.ts +15 -0
- package/dist/lib/session/shell-programs.js +359 -0
- package/dist/lib/session/tool-calls.d.ts +88 -0
- package/dist/lib/session/tool-calls.js +612 -0
- package/dist/lib/session/tool-index.d.ts +100 -0
- package/dist/lib/session/tool-index.js +773 -0
- package/dist/lib/session/tool-store.d.ts +15 -0
- package/dist/lib/session/tool-store.js +198 -0
- package/dist/lib/session/types.d.ts +7 -0
- package/dist/lib/state.d.ts +10 -1
- package/dist/lib/state.js +11 -2
- package/dist/lib/teams/remoteWorktree.d.ts +3 -4
- package/dist/lib/teams/remoteWorktree.js +3 -4
- package/dist/lib/teams/worktree.d.ts +11 -1
- package/dist/lib/teams/worktree.js +42 -4
- package/dist/lib/types.d.ts +17 -0
- package/dist/lib/types.js +17 -0
- package/dist/lib/versions.js +69 -22
- package/package.json +3 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type Database from '../sqlite.js';
|
|
2
|
+
import { type IndexedToolCall } from './tool-calls.js';
|
|
3
|
+
import type { SessionMeta } from './types.js';
|
|
4
|
+
export declare function canonicalToolLedgerPath(filePath: string): string;
|
|
5
|
+
/** Resolve the transcript that actually carries tool events for split-file harnesses. */
|
|
6
|
+
export declare function toolEvidenceSourcePath(filePath: string, agent: string): string;
|
|
7
|
+
/** Remove cached tool evidence when its source transcript no longer exists. */
|
|
8
|
+
export declare function purgeToolCalls(db: Database.Database, sessionId: string): void;
|
|
9
|
+
/** Purge deleted direct children when a transcript directory's stamp changes. */
|
|
10
|
+
export declare function purgeMissingToolCallsInDirectory(db: Database.Database, dirPath: string, currentFilePaths: string[]): number;
|
|
11
|
+
/** Persist one parser batch and its file stamp atomically. */
|
|
12
|
+
export declare function persistToolCalls(db: Database.Database, session: SessionMeta, calls: IndexedToolCall[], sourceStamp: {
|
|
13
|
+
fileMtimeMs: number;
|
|
14
|
+
fileSize: number;
|
|
15
|
+
}, mode?: 'replace' | 'append', maxSessionBytes?: number): void;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { TOOL_INDEX_VERSION, TOOL_INDEX_LIMIT_ORDINAL, TOOL_SESSION_EVIDENCE_MAX_BYTES, toolCallEvidenceBytes, toolCallKey, } from './tool-calls.js';
|
|
4
|
+
export function canonicalToolLedgerPath(filePath) {
|
|
5
|
+
let probe = path.resolve(filePath);
|
|
6
|
+
const suffix = [];
|
|
7
|
+
while (true) {
|
|
8
|
+
try {
|
|
9
|
+
return path.join(fs.realpathSync(probe), ...suffix.reverse());
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
const parent = path.dirname(probe);
|
|
13
|
+
if (parent === probe)
|
|
14
|
+
return path.resolve(filePath);
|
|
15
|
+
suffix.push(path.basename(probe));
|
|
16
|
+
probe = parent;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Resolve the transcript that actually carries tool events for split-file harnesses. */
|
|
21
|
+
export function toolEvidenceSourcePath(filePath, agent) {
|
|
22
|
+
if (agent === 'kimi' && path.basename(filePath) === 'state.json') {
|
|
23
|
+
return path.join(path.dirname(filePath), 'agents', 'main', 'wire.jsonl');
|
|
24
|
+
}
|
|
25
|
+
if (agent === 'grok' && path.basename(filePath) === 'summary.json') {
|
|
26
|
+
return path.join(path.dirname(filePath), 'chat_history.jsonl');
|
|
27
|
+
}
|
|
28
|
+
return filePath;
|
|
29
|
+
}
|
|
30
|
+
function deleteSessionCalls(db, sessionId) {
|
|
31
|
+
const keys = db.prepare(`SELECT call_key FROM tool_calls WHERE session_id = ?`).all(sessionId);
|
|
32
|
+
for (const { call_key } of keys) {
|
|
33
|
+
db.prepare(`DELETE FROM tool_call_programs WHERE call_key = ?`).run(call_key);
|
|
34
|
+
db.prepare(`DELETE FROM tool_program_occurrences WHERE call_key = ?`).run(call_key);
|
|
35
|
+
db.prepare(`DELETE FROM tool_call_text WHERE call_key = ?`).run(call_key);
|
|
36
|
+
}
|
|
37
|
+
db.prepare(`DELETE FROM tool_calls WHERE session_id = ?`).run(sessionId);
|
|
38
|
+
}
|
|
39
|
+
/** Remove cached tool evidence when its source transcript no longer exists. */
|
|
40
|
+
export function purgeToolCalls(db, sessionId) {
|
|
41
|
+
deleteSessionCalls(db, sessionId);
|
|
42
|
+
db.prepare(`DELETE FROM tool_scan_ledger WHERE session_id = ?`).run(sessionId);
|
|
43
|
+
}
|
|
44
|
+
/** Purge deleted direct children when a transcript directory's stamp changes. */
|
|
45
|
+
export function purgeMissingToolCallsInDirectory(db, dirPath, currentFilePaths) {
|
|
46
|
+
const normalizedDir = path.normalize(dirPath);
|
|
47
|
+
const prefix = normalizedDir.endsWith(path.sep) ? normalizedDir : normalizedDir + path.sep;
|
|
48
|
+
const present = new Set(currentFilePaths.map((filePath) => path.normalize(filePath)));
|
|
49
|
+
const rows = db.prepare(`
|
|
50
|
+
SELECT s.id, s.file_path, s.agent
|
|
51
|
+
FROM sessions s
|
|
52
|
+
WHERE substr(s.file_path, 1, ?) = ?
|
|
53
|
+
`).all(prefix.length, prefix);
|
|
54
|
+
let purged = 0;
|
|
55
|
+
for (const row of rows) {
|
|
56
|
+
const filePath = path.normalize(row.file_path);
|
|
57
|
+
if (path.dirname(filePath) !== normalizedDir || present.has(filePath))
|
|
58
|
+
continue;
|
|
59
|
+
purgeToolCalls(db, row.id);
|
|
60
|
+
purged++;
|
|
61
|
+
}
|
|
62
|
+
return purged;
|
|
63
|
+
}
|
|
64
|
+
/** Persist one parser batch and its file stamp atomically. */
|
|
65
|
+
export function persistToolCalls(db, session, calls, sourceStamp, mode = 'replace', maxSessionBytes = TOOL_SESSION_EVIDENCE_MAX_BYTES) {
|
|
66
|
+
const sourcePath = toolEvidenceSourcePath(session.filePath, session.agent);
|
|
67
|
+
const insertCall = db.prepare(`
|
|
68
|
+
INSERT INTO tool_calls (
|
|
69
|
+
call_key, session_id, ordinal, source_call_id, timestamp, tool, input,
|
|
70
|
+
outcome, exit_code, status_code, error_code, output, error, parse_error
|
|
71
|
+
, evidence_bytes
|
|
72
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
73
|
+
ON CONFLICT(session_id, ordinal) DO UPDATE SET
|
|
74
|
+
source_call_id = excluded.source_call_id,
|
|
75
|
+
timestamp = excluded.timestamp,
|
|
76
|
+
tool = excluded.tool,
|
|
77
|
+
input = excluded.input,
|
|
78
|
+
outcome = excluded.outcome,
|
|
79
|
+
exit_code = excluded.exit_code,
|
|
80
|
+
status_code = excluded.status_code,
|
|
81
|
+
error_code = excluded.error_code,
|
|
82
|
+
output = excluded.output,
|
|
83
|
+
error = excluded.error,
|
|
84
|
+
parse_error = excluded.parse_error,
|
|
85
|
+
evidence_bytes = excluded.evidence_bytes
|
|
86
|
+
`);
|
|
87
|
+
const insertProgram = db.prepare(`INSERT OR IGNORE INTO tool_call_programs (call_key, program) VALUES (?, ?)`);
|
|
88
|
+
const insertOccurrence = db.prepare(`
|
|
89
|
+
INSERT INTO tool_program_occurrences (call_key, occurrence_ordinal, program, role)
|
|
90
|
+
VALUES (?, ?, ?, ?)
|
|
91
|
+
`);
|
|
92
|
+
const insertText = db.prepare(`INSERT INTO tool_call_text (call_key, tool, input, output, error) VALUES (?, ?, ?, ?, ?)`);
|
|
93
|
+
const deletePrograms = db.prepare(`DELETE FROM tool_call_programs WHERE call_key = ?`);
|
|
94
|
+
const deleteOccurrences = db.prepare(`DELETE FROM tool_program_occurrences WHERE call_key = ?`);
|
|
95
|
+
const deleteText = db.prepare(`DELETE FROM tool_call_text WHERE call_key = ?`);
|
|
96
|
+
const deleteCall = db.prepare(`DELETE FROM tool_calls WHERE call_key = ?`);
|
|
97
|
+
const writeLedger = db.prepare(`
|
|
98
|
+
INSERT INTO tool_scan_ledger (
|
|
99
|
+
session_id, file_path, file_mtime_ms, file_size, extractor_version, indexed_at, call_count,
|
|
100
|
+
evidence_bytes
|
|
101
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
102
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
103
|
+
file_path = excluded.file_path,
|
|
104
|
+
file_mtime_ms = excluded.file_mtime_ms,
|
|
105
|
+
file_size = excluded.file_size,
|
|
106
|
+
extractor_version = excluded.extractor_version,
|
|
107
|
+
indexed_at = excluded.indexed_at,
|
|
108
|
+
call_count = excluded.call_count,
|
|
109
|
+
evidence_bytes = excluded.evidence_bytes
|
|
110
|
+
`);
|
|
111
|
+
const txn = db.transaction(() => {
|
|
112
|
+
if (mode === 'replace')
|
|
113
|
+
deleteSessionCalls(db, session.id);
|
|
114
|
+
const ledgerPath = canonicalToolLedgerPath(sourcePath);
|
|
115
|
+
const priorLedger = mode === 'append'
|
|
116
|
+
? db.prepare(`SELECT call_count, evidence_bytes FROM tool_scan_ledger WHERE session_id = ?`).get(session.id)
|
|
117
|
+
: undefined;
|
|
118
|
+
if (mode === 'append' && !priorLedger) {
|
|
119
|
+
throw new Error('Append tool evidence requires an existing tool ledger row.');
|
|
120
|
+
}
|
|
121
|
+
if (mode === 'append' && calls.length === 0) {
|
|
122
|
+
writeLedger.run(session.id, ledgerPath, sourceStamp.fileMtimeMs, sourceStamp.fileSize, TOOL_INDEX_VERSION, Date.now(), priorLedger.call_count, priorLedger.evidence_bytes);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const limitKey = toolCallKey(session.id, TOOL_INDEX_LIMIT_ORDINAL);
|
|
126
|
+
const existingSize = db.prepare(`
|
|
127
|
+
SELECT evidence_bytes FROM tool_calls WHERE session_id = ? AND ordinal = ?
|
|
128
|
+
`);
|
|
129
|
+
const priorLimit = mode === 'append'
|
|
130
|
+
? existingSize.get(session.id, TOOL_INDEX_LIMIT_ORDINAL)
|
|
131
|
+
: undefined;
|
|
132
|
+
deletePrograms.run(limitKey);
|
|
133
|
+
deleteOccurrences.run(limitKey);
|
|
134
|
+
deleteText.run(limitKey);
|
|
135
|
+
deleteCall.run(limitKey);
|
|
136
|
+
const existingRows = mode === 'append'
|
|
137
|
+
? calls.map((call) => ({
|
|
138
|
+
ordinal: call.ordinal,
|
|
139
|
+
bytes: existingSize.get(session.id, call.ordinal)?.evidence_bytes ?? 0,
|
|
140
|
+
}))
|
|
141
|
+
: [];
|
|
142
|
+
const existingByOrdinal = new Map(existingRows.map((row) => [row.ordinal, row.bytes]));
|
|
143
|
+
let storedBytes = priorLedger?.evidence_bytes ?? 0;
|
|
144
|
+
let count = priorLedger?.call_count ?? 0;
|
|
145
|
+
if (priorLimit) {
|
|
146
|
+
storedBytes -= priorLimit.evidence_bytes;
|
|
147
|
+
count--;
|
|
148
|
+
}
|
|
149
|
+
const limitCall = {
|
|
150
|
+
ordinal: TOOL_INDEX_LIMIT_ORDINAL,
|
|
151
|
+
timestamp: session.timestamp,
|
|
152
|
+
tool: 'index_limit',
|
|
153
|
+
programs: [],
|
|
154
|
+
programOccurrences: [],
|
|
155
|
+
input: `Tool evidence stopped at the ${Math.floor(maxSessionBytes / 1024 / 1024)} MiB per-session payload limit.`,
|
|
156
|
+
outcome: 'unknown',
|
|
157
|
+
parseError: 'Additional tool calls were not indexed for this session.',
|
|
158
|
+
};
|
|
159
|
+
const limitBytes = toolCallEvidenceBytes(limitCall);
|
|
160
|
+
const ordinaryLimit = Math.max(0, maxSessionBytes - limitBytes);
|
|
161
|
+
const accepted = [];
|
|
162
|
+
let truncated = false;
|
|
163
|
+
for (const call of [...calls].sort((a, b) => a.ordinal - b.ordinal)) {
|
|
164
|
+
const replacingBytes = existingByOrdinal.get(call.ordinal) ?? 0;
|
|
165
|
+
const nextBytes = storedBytes - replacingBytes + toolCallEvidenceBytes(call);
|
|
166
|
+
if (nextBytes > ordinaryLimit) {
|
|
167
|
+
truncated = true;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
storedBytes = nextBytes;
|
|
171
|
+
accepted.push(call);
|
|
172
|
+
if (!existingByOrdinal.has(call.ordinal) || replacingBytes === 0)
|
|
173
|
+
count++;
|
|
174
|
+
}
|
|
175
|
+
if (truncated) {
|
|
176
|
+
if (storedBytes + limitBytes <= maxSessionBytes) {
|
|
177
|
+
accepted.push(limitCall);
|
|
178
|
+
storedBytes += limitBytes;
|
|
179
|
+
count++;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for (const call of accepted) {
|
|
183
|
+
const key = toolCallKey(session.id, call.ordinal);
|
|
184
|
+
insertCall.run(key, session.id, call.ordinal, call.sourceCallId ?? null, call.timestamp, call.tool, call.input, call.outcome, call.exitCode ?? null, call.statusCode ?? null, call.errorCode ?? null, call.output ?? null, call.error ?? null, call.parseError ?? null, toolCallEvidenceBytes(call));
|
|
185
|
+
deletePrograms.run(key);
|
|
186
|
+
deleteOccurrences.run(key);
|
|
187
|
+
deleteText.run(key);
|
|
188
|
+
for (const program of call.programs)
|
|
189
|
+
insertProgram.run(key, program);
|
|
190
|
+
call.programOccurrences.forEach((occurrence, occurrenceOrdinal) => {
|
|
191
|
+
insertOccurrence.run(key, occurrenceOrdinal, occurrence.program, occurrence.role);
|
|
192
|
+
});
|
|
193
|
+
insertText.run(key, call.tool, call.input, call.output ?? '', call.error ?? '');
|
|
194
|
+
}
|
|
195
|
+
writeLedger.run(session.id, ledgerPath, sourceStamp.fileMtimeMs, sourceStamp.fileSize, TOOL_INDEX_VERSION, Date.now(), count, storedBytes);
|
|
196
|
+
});
|
|
197
|
+
txn();
|
|
198
|
+
}
|
|
@@ -24,10 +24,17 @@ export interface SessionEvent {
|
|
|
24
24
|
role?: 'user' | 'assistant';
|
|
25
25
|
content?: string;
|
|
26
26
|
tool?: string;
|
|
27
|
+
/** Harness-native call identity, used to correlate concurrent results. */
|
|
28
|
+
callId?: string;
|
|
27
29
|
args?: Record<string, any>;
|
|
28
30
|
path?: string;
|
|
29
31
|
command?: string;
|
|
30
32
|
success?: boolean;
|
|
33
|
+
/** Structured harness outcome; never inferred from free-text output. */
|
|
34
|
+
outcome?: 'ok' | 'error' | 'unknown';
|
|
35
|
+
exitCode?: number;
|
|
36
|
+
statusCode?: number;
|
|
37
|
+
errorCode?: string;
|
|
31
38
|
output?: string;
|
|
32
39
|
/** Internal: marks tool_use events from local commands */
|
|
33
40
|
_local?: boolean;
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -272,7 +272,16 @@ export declare function getCompanionDir(): string;
|
|
|
272
272
|
export declare function getBrowserRuntimeDir(): string;
|
|
273
273
|
/** Path to helper subprocess scratch (~/.agents/.cache/helpers/). */
|
|
274
274
|
export declare function getHelpersDir(): string;
|
|
275
|
-
/**
|
|
275
|
+
/**
|
|
276
|
+
* Path to scheduler daemon scratch (~/.agents/.cache/helpers/daemon/) — holds
|
|
277
|
+
* the daemon pid file, heartbeat, start lock, and log. AGENTS_DAEMON_DIR
|
|
278
|
+
* redirects it to a fork-private temp so daemon tests (which write pid/heartbeat
|
|
279
|
+
* files and acquire the real start lock) can never clobber a live daemon's state
|
|
280
|
+
* on a dev machine — the surgical mirror of AGENTS_DEVICES_DIR /
|
|
281
|
+
* AGENTS_HOOK_SHIMS_DIR, leaving HOME untouched. Read at CALL time (daemon.ts
|
|
282
|
+
* resolves every path helper through this), so tests/setup.ts can set it before
|
|
283
|
+
* the daemon module is exercised. Never set in production code.
|
|
284
|
+
*/
|
|
276
285
|
export declare function getDaemonDir(): string;
|
|
277
286
|
/** Path to PTY server scratch (~/.agents/.cache/helpers/pty/). */
|
|
278
287
|
export declare function getPtyDir(): string;
|
package/dist/lib/state.js
CHANGED
|
@@ -579,8 +579,17 @@ export function getCompanionDir() { return COMPANION_CACHE_DIR; }
|
|
|
579
579
|
export function getBrowserRuntimeDir() { return BROWSER_RUNTIME_DIR; }
|
|
580
580
|
/** Path to helper subprocess scratch (~/.agents/.cache/helpers/). */
|
|
581
581
|
export function getHelpersDir() { return HELPERS_DIR; }
|
|
582
|
-
/**
|
|
583
|
-
|
|
582
|
+
/**
|
|
583
|
+
* Path to scheduler daemon scratch (~/.agents/.cache/helpers/daemon/) — holds
|
|
584
|
+
* the daemon pid file, heartbeat, start lock, and log. AGENTS_DAEMON_DIR
|
|
585
|
+
* redirects it to a fork-private temp so daemon tests (which write pid/heartbeat
|
|
586
|
+
* files and acquire the real start lock) can never clobber a live daemon's state
|
|
587
|
+
* on a dev machine — the surgical mirror of AGENTS_DEVICES_DIR /
|
|
588
|
+
* AGENTS_HOOK_SHIMS_DIR, leaving HOME untouched. Read at CALL time (daemon.ts
|
|
589
|
+
* resolves every path helper through this), so tests/setup.ts can set it before
|
|
590
|
+
* the daemon module is exercised. Never set in production code.
|
|
591
|
+
*/
|
|
592
|
+
export function getDaemonDir() { return process.env.AGENTS_DAEMON_DIR ?? DAEMON_DIR; }
|
|
584
593
|
/** Path to PTY server scratch (~/.agents/.cache/helpers/pty/). */
|
|
585
594
|
export function getPtyDir() { return PTY_DIR; }
|
|
586
595
|
/** Path to tmux scratch (~/.agents/.cache/helpers/tmux/) — shared server socket + per-session meta JSONs. */
|
|
@@ -26,10 +26,9 @@ export declare function remoteDefaultBranch(target: string, repoPath: string): s
|
|
|
26
26
|
* Create a worktree on the host for a teammate, branched off the freshly-fetched
|
|
27
27
|
* default branch. Returns the absolute worktree path on the host.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
* origin
|
|
31
|
-
*
|
|
32
|
-
* checkout on the host can't fork the teammate off old code.
|
|
29
|
+
* Same base policy as local `createWorktree`: fetch origin, then
|
|
30
|
+
* `worktree add -b … origin/<default>` so a stale checkout can't fork the
|
|
31
|
+
* teammate off old code.
|
|
33
32
|
*/
|
|
34
33
|
export declare function createRemoteWorktree(target: string, repoPath: string, worktreeName: string): string;
|
|
35
34
|
/**
|
|
@@ -87,10 +87,9 @@ export function remoteDefaultBranch(target, repoPath) {
|
|
|
87
87
|
* Create a worktree on the host for a teammate, branched off the freshly-fetched
|
|
88
88
|
* default branch. Returns the absolute worktree path on the host.
|
|
89
89
|
*
|
|
90
|
-
*
|
|
91
|
-
* origin
|
|
92
|
-
*
|
|
93
|
-
* checkout on the host can't fork the teammate off old code.
|
|
90
|
+
* Same base policy as local `createWorktree`: fetch origin, then
|
|
91
|
+
* `worktree add -b … origin/<default>` so a stale checkout can't fork the
|
|
92
|
+
* teammate off old code.
|
|
94
93
|
*/
|
|
95
94
|
export function createRemoteWorktree(target, repoPath, worktreeName) {
|
|
96
95
|
assertValidSshTarget(target);
|
|
@@ -22,7 +22,17 @@ export declare const DEFAULT_DIFF_MAX_BYTES = 200000;
|
|
|
22
22
|
*/
|
|
23
23
|
export declare function gitDiff(worktreePath: string, maxBytes?: number): Promise<string>;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* Resolve the repo's default branch name (origin/HEAD → `main`/`master`/…).
|
|
26
|
+
* Refreshes origin/HEAD first so a repo cloned before the default was set resolves.
|
|
27
|
+
* Falls back to `main` when origin/HEAD isn't set.
|
|
28
|
+
*/
|
|
29
|
+
export declare function localDefaultBranch(gitRoot: string): Promise<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Create a new git worktree for a teammate, branched off the freshly-fetched
|
|
32
|
+
* default branch. Fetches `origin` first and bases the branch on
|
|
33
|
+
* `origin/<default>` so a stale local checkout cannot fork teammates off old
|
|
34
|
+
* code. Matches `createRemoteWorktree` — local and remote isolation share one
|
|
35
|
+
* base policy.
|
|
26
36
|
*
|
|
27
37
|
* @param repoDir - Directory inside the git repository
|
|
28
38
|
* @param worktreeName - Name for the worktree (used in path and branch)
|
|
@@ -77,7 +77,34 @@ export async function gitDiff(worktreePath, maxBytes = DEFAULT_DIFF_MAX_BYTES) {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
80
|
-
*
|
|
80
|
+
* Resolve the repo's default branch name (origin/HEAD → `main`/`master`/…).
|
|
81
|
+
* Refreshes origin/HEAD first so a repo cloned before the default was set resolves.
|
|
82
|
+
* Falls back to `main` when origin/HEAD isn't set.
|
|
83
|
+
*/
|
|
84
|
+
export async function localDefaultBranch(gitRoot) {
|
|
85
|
+
try {
|
|
86
|
+
await execFileAsync('git', ['remote', 'set-head', 'origin', '--auto'], { cwd: gitRoot });
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Offline / no origin — fall through to symbolic-ref or main.
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const { stdout } = await execFileAsync('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], { cwd: gitRoot });
|
|
93
|
+
const base = stdout.trim().replace(/^origin\//, '');
|
|
94
|
+
if (base)
|
|
95
|
+
return base;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// no origin/HEAD
|
|
99
|
+
}
|
|
100
|
+
return 'main';
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Create a new git worktree for a teammate, branched off the freshly-fetched
|
|
104
|
+
* default branch. Fetches `origin` first and bases the branch on
|
|
105
|
+
* `origin/<default>` so a stale local checkout cannot fork teammates off old
|
|
106
|
+
* code. Matches `createRemoteWorktree` — local and remote isolation share one
|
|
107
|
+
* base policy.
|
|
81
108
|
*
|
|
82
109
|
* @param repoDir - Directory inside the git repository
|
|
83
110
|
* @param worktreeName - Name for the worktree (used in path and branch)
|
|
@@ -90,10 +117,21 @@ export async function createWorktree(repoDir, worktreeName) {
|
|
|
90
117
|
const gitRoot = await getGitRoot(repoDir);
|
|
91
118
|
const worktreePath = safeJoin(path.join(gitRoot, '.agents', 'worktrees'), worktreeName);
|
|
92
119
|
const branchName = `agents/${worktreeName}`;
|
|
120
|
+
const base = await localDefaultBranch(gitRoot);
|
|
93
121
|
await fs.mkdir(path.dirname(worktreePath), { recursive: true });
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
122
|
+
// Fetch first so origin/<default> is current. Fail loud on network errors —
|
|
123
|
+
// silent fallback to HEAD is how swarms write on a days-stale base and only
|
|
124
|
+
// discover it at merge time.
|
|
125
|
+
try {
|
|
126
|
+
await execFileAsync('git', ['fetch', 'origin'], { cwd: gitRoot });
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
const detail = (err?.stderr || err?.message || String(err)).toString().trim();
|
|
130
|
+
throw new Error(`createWorktree: git fetch origin failed in ${gitRoot}` +
|
|
131
|
+
(detail ? `: ${detail}` : '') +
|
|
132
|
+
`. Cannot base a teammate worktree on a stale remote-tracking ref.`);
|
|
133
|
+
}
|
|
134
|
+
await execFileAsync('git', ['worktree', 'add', '-b', branchName, worktreePath, `origin/${base}`], { cwd: gitRoot });
|
|
97
135
|
return worktreePath;
|
|
98
136
|
}
|
|
99
137
|
/**
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -11,6 +11,23 @@ import type { FeedBroadcastConfig } from './feed-broadcast.js';
|
|
|
11
11
|
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes';
|
|
12
12
|
/** How `agents run <agent>` chooses an installed version when none is pinned. */
|
|
13
13
|
export type RunStrategy = 'pinned' | 'available' | 'balanced';
|
|
14
|
+
/**
|
|
15
|
+
* Reserved `<agent>` keyword for `agents run auto` — full-auto dispatch:
|
|
16
|
+
* host (14d launch affinity) → harness (best-account headroom, weighted) →
|
|
17
|
+
* account (balanced). Lives in lib (not commands/) because both the run
|
|
18
|
+
* command (exec.ts) and the host dispatch layer (hosts/dispatch.ts, which
|
|
19
|
+
* arms the chain-hop guard for remote `run auto`) must agree on it.
|
|
20
|
+
*/
|
|
21
|
+
export declare const RUN_AUTO_KEYWORD = "auto";
|
|
22
|
+
/**
|
|
23
|
+
* Env var a host dispatcher exports into the remote SHELL when it dispatches
|
|
24
|
+
* `agents run auto`: tells the remote CLI its host layer is already resolved,
|
|
25
|
+
* so it must not re-run affinity and chain-hop to a third host. It rides the
|
|
26
|
+
* shell-export prelude (hosts/dispatch.ts `remoteRunShellPrelude`) because
|
|
27
|
+
* `--env` flags only reach the spawned AGENT's env — the remote CLI's own
|
|
28
|
+
* process.env (which exec.ts `runAutoDefaultsToAffinity` reads) never sees them.
|
|
29
|
+
*/
|
|
30
|
+
export declare const RUN_AUTO_HOST_RESOLVED_ENV = "AGENTS_RUN_AUTO_HOST_RESOLVED";
|
|
14
31
|
/** Per-agent run strategy config. */
|
|
15
32
|
export interface AgentRunConfig {
|
|
16
33
|
strategy?: RunStrategy;
|
package/dist/lib/types.js
CHANGED
|
@@ -5,6 +5,23 @@
|
|
|
5
5
|
* configuration schemas, resource tracking, registry types, and permission
|
|
6
6
|
* formats for each supported agent.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Reserved `<agent>` keyword for `agents run auto` — full-auto dispatch:
|
|
10
|
+
* host (14d launch affinity) → harness (best-account headroom, weighted) →
|
|
11
|
+
* account (balanced). Lives in lib (not commands/) because both the run
|
|
12
|
+
* command (exec.ts) and the host dispatch layer (hosts/dispatch.ts, which
|
|
13
|
+
* arms the chain-hop guard for remote `run auto`) must agree on it.
|
|
14
|
+
*/
|
|
15
|
+
export const RUN_AUTO_KEYWORD = 'auto';
|
|
16
|
+
/**
|
|
17
|
+
* Env var a host dispatcher exports into the remote SHELL when it dispatches
|
|
18
|
+
* `agents run auto`: tells the remote CLI its host layer is already resolved,
|
|
19
|
+
* so it must not re-run affinity and chain-hop to a third host. It rides the
|
|
20
|
+
* shell-export prelude (hosts/dispatch.ts `remoteRunShellPrelude`) because
|
|
21
|
+
* `--env` flags only reach the spawned AGENT's env — the remote CLI's own
|
|
22
|
+
* process.env (which exec.ts `runAutoDefaultsToAffinity` reads) never sees them.
|
|
23
|
+
*/
|
|
24
|
+
export const RUN_AUTO_HOST_RESOLVED_ENV = 'AGENTS_RUN_AUTO_HOST_RESOLVED';
|
|
8
25
|
/** Every canonical mode in declaration order. Useful for iteration / validation. */
|
|
9
26
|
export const ALL_MODES = ['plan', 'edit', 'auto', 'skip'];
|
|
10
27
|
/** Canonical system repo cloned into ~/.agents/.system/. */
|
package/dist/lib/versions.js
CHANGED
|
@@ -1267,34 +1267,23 @@ export function isVersionIsolated(agent, version) {
|
|
|
1267
1267
|
return fs.existsSync(getIsolatedMarkerPath(agent, version));
|
|
1268
1268
|
}
|
|
1269
1269
|
/**
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
1272
|
-
*
|
|
1273
|
-
* its generic platform copy into the target version's isolated home so
|
|
1274
|
-
* `listInstalledVersions` and the shim resolve the right binary.
|
|
1270
|
+
* Move any `grok-<installedVersion>-...` binary (and its generic platform
|
|
1271
|
+
* copy) found directly under `sourceDownloads` into `targetDownloads`.
|
|
1272
|
+
* Returns true if the versioned binary was moved.
|
|
1275
1273
|
*/
|
|
1276
|
-
function
|
|
1277
|
-
const hostGrokLink = path.join(getHomeDir(), agentConfigDirName('grok'));
|
|
1278
|
-
let sourceDownloads;
|
|
1279
|
-
try {
|
|
1280
|
-
sourceDownloads = path.join(fs.readlinkSync(hostGrokLink), 'downloads');
|
|
1281
|
-
}
|
|
1282
|
-
catch {
|
|
1283
|
-
sourceDownloads = path.join(hostGrokLink, 'downloads');
|
|
1284
|
-
}
|
|
1285
|
-
const targetDownloads = path.join(getVersionHomePath('grok', installedVersion), agentConfigDirName('grok'), 'downloads');
|
|
1274
|
+
function tryMoveGrokDownloads(sourceDownloads, targetDownloads, installedVersion) {
|
|
1286
1275
|
if (!fs.existsSync(sourceDownloads))
|
|
1287
|
-
return;
|
|
1276
|
+
return false;
|
|
1288
1277
|
if (path.resolve(sourceDownloads) === path.resolve(targetDownloads))
|
|
1289
|
-
return;
|
|
1278
|
+
return false;
|
|
1290
1279
|
const entries = fs.readdirSync(sourceDownloads).filter((e) => e.startsWith('grok-'));
|
|
1291
1280
|
if (entries.length === 0)
|
|
1292
|
-
return;
|
|
1293
|
-
fs.mkdirSync(targetDownloads, { recursive: true });
|
|
1281
|
+
return false;
|
|
1294
1282
|
const escapedVersion = installedVersion.replace(/\./g, '\\.');
|
|
1295
1283
|
const versionedPattern = new RegExp(`^grok-${escapedVersion}-`);
|
|
1296
1284
|
const movedPaths = [];
|
|
1297
|
-
// Move the versioned binary first
|
|
1285
|
+
// Move the versioned binary first — only create the target dir once we
|
|
1286
|
+
// know there is something to move into it.
|
|
1298
1287
|
for (const entry of entries) {
|
|
1299
1288
|
if (!versionedPattern.test(entry))
|
|
1300
1289
|
continue;
|
|
@@ -1303,6 +1292,7 @@ function relocateGrokBinaryToVersionHome(installedVersion) {
|
|
|
1303
1292
|
if (fs.existsSync(dst))
|
|
1304
1293
|
continue;
|
|
1305
1294
|
try {
|
|
1295
|
+
fs.mkdirSync(targetDownloads, { recursive: true });
|
|
1306
1296
|
fs.renameSync(src, dst);
|
|
1307
1297
|
movedPaths.push(dst);
|
|
1308
1298
|
}
|
|
@@ -1311,7 +1301,7 @@ function relocateGrokBinaryToVersionHome(installedVersion) {
|
|
|
1311
1301
|
}
|
|
1312
1302
|
}
|
|
1313
1303
|
if (movedPaths.length === 0)
|
|
1314
|
-
return;
|
|
1304
|
+
return false;
|
|
1315
1305
|
// The installer also creates a generic platform binary (e.g. grok-macos-aarch64)
|
|
1316
1306
|
// that is a copy of the versioned binary. Move it too if its size matches.
|
|
1317
1307
|
const movedSize = fs.statSync(movedPaths[0]).size;
|
|
@@ -1331,6 +1321,42 @@ function relocateGrokBinaryToVersionHome(installedVersion) {
|
|
|
1331
1321
|
/* ignore per-file failures */
|
|
1332
1322
|
}
|
|
1333
1323
|
}
|
|
1324
|
+
return true;
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Grok's official installer writes into ~/.grok/downloads, which (because we
|
|
1328
|
+
* symlink ~/.grok to the active version home) resolves to the PREVIOUS default
|
|
1329
|
+
* home during `agents add grok@<new>`. Move the freshly-downloaded binary and
|
|
1330
|
+
* its generic platform copy into the target version's isolated home so
|
|
1331
|
+
* `listInstalledVersions` and the shim resolve the right binary.
|
|
1332
|
+
*
|
|
1333
|
+
* Self-healing: if `~/.grok`'s current target has nothing matching (e.g. a
|
|
1334
|
+
* PAST install mislabeled `installedVersion` as the literal string 'latest',
|
|
1335
|
+
* whose regex never matched the real filename, stranding the binary in
|
|
1336
|
+
* whatever version was default at the time), sweep every other installed
|
|
1337
|
+
* grok version home for the missing file before giving up.
|
|
1338
|
+
*/
|
|
1339
|
+
function relocateGrokBinaryToVersionHome(installedVersion) {
|
|
1340
|
+
const hostGrokLink = path.join(getHomeDir(), agentConfigDirName('grok'));
|
|
1341
|
+
let sourceDownloads;
|
|
1342
|
+
try {
|
|
1343
|
+
sourceDownloads = path.join(fs.readlinkSync(hostGrokLink), 'downloads');
|
|
1344
|
+
}
|
|
1345
|
+
catch {
|
|
1346
|
+
sourceDownloads = path.join(hostGrokLink, 'downloads');
|
|
1347
|
+
}
|
|
1348
|
+
const targetDownloads = path.join(getVersionHomePath('grok', installedVersion), agentConfigDirName('grok'), 'downloads');
|
|
1349
|
+
if (tryMoveGrokDownloads(sourceDownloads, targetDownloads, installedVersion))
|
|
1350
|
+
return;
|
|
1351
|
+
for (const version of listInstalledVersions('grok')) {
|
|
1352
|
+
if (version === installedVersion)
|
|
1353
|
+
continue;
|
|
1354
|
+
const candidate = path.join(getVersionHomePath('grok', version), agentConfigDirName('grok'), 'downloads');
|
|
1355
|
+
if (path.resolve(candidate) === path.resolve(sourceDownloads))
|
|
1356
|
+
continue; // already tried
|
|
1357
|
+
if (tryMoveGrokDownloads(candidate, targetDownloads, installedVersion))
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1334
1360
|
}
|
|
1335
1361
|
/**
|
|
1336
1362
|
* Install a specific version of an agent.
|
|
@@ -1376,7 +1402,28 @@ export async function installVersion(agent, version, onProgress, opts) {
|
|
|
1376
1402
|
await execAsync(script, { timeout: 120000 });
|
|
1377
1403
|
}
|
|
1378
1404
|
if (version === 'latest') {
|
|
1379
|
-
|
|
1405
|
+
// A freshly-installed self-updating binary (grok, …) can take a
|
|
1406
|
+
// moment to become resolvable on PATH right after the installer
|
|
1407
|
+
// exits — retry briefly rather than silently falling back to the
|
|
1408
|
+
// literal string 'latest'. That fallback used to corrupt version
|
|
1409
|
+
// bookkeeping downstream: it creates a bogus `versions/<agent>/latest/`
|
|
1410
|
+
// dir, and for grok specifically defeats relocateGrokBinaryToVersionHome
|
|
1411
|
+
// (its exact-filename regex can never match `grok-latest-...`), which
|
|
1412
|
+
// permanently strands the real downloaded binary in the PREVIOUS
|
|
1413
|
+
// default's downloads dir.
|
|
1414
|
+
let probed = await getCliVersionFromPath(agent);
|
|
1415
|
+
for (let attempt = 0; !probed && attempt < 2; attempt++) {
|
|
1416
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1417
|
+
probed = await getCliVersionFromPath(agent);
|
|
1418
|
+
}
|
|
1419
|
+
if (!probed) {
|
|
1420
|
+
return {
|
|
1421
|
+
success: false,
|
|
1422
|
+
installedVersion: version,
|
|
1423
|
+
error: `${agentConfig.name} installed but its version could not be determined after several attempts.`,
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
installedVersion = probed;
|
|
1380
1427
|
// Fold any stale literal `latest` dir from an earlier probe-failed
|
|
1381
1428
|
// install into the real version so it stops shadowing `agents view`.
|
|
1382
1429
|
await reconcileStaleLatestDir(agent, installedVersion);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.1",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
"@types/proper-lockfile": "4.1.4",
|
|
90
90
|
"@xterm/headless": "6.0.0",
|
|
91
91
|
"@zed-industries/agent-client-protocol": "0.4.5",
|
|
92
|
+
"acorn": "8.18.0",
|
|
92
93
|
"age-encryption": "0.3.0",
|
|
93
94
|
"chalk": "5.6.2",
|
|
94
95
|
"commander": "15.0.0",
|
|
@@ -101,6 +102,7 @@
|
|
|
101
102
|
"shlex": "3.0.0",
|
|
102
103
|
"simple-git": "3.36.0",
|
|
103
104
|
"smol-toml": "1.7.0",
|
|
105
|
+
"unbash": "4.0.5",
|
|
104
106
|
"ws": "^8.21.0",
|
|
105
107
|
"yaml": "2.9.0"
|
|
106
108
|
},
|