@phnx-labs/agents-cli 1.20.73 → 1.20.76
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 +989 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +31 -1
- package/dist/commands/exec.js +31 -3
- package/dist/commands/export.d.ts +11 -0
- package/dist/commands/export.js +215 -0
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +87 -9
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +15 -2
- package/dist/commands/secrets.js +44 -21
- package/dist/commands/sessions-browser.js +1 -1
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.js +16 -1
- package/dist/commands/sessions.d.ts +36 -13
- package/dist/commands/sessions.js +210 -40
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/setup.js +6 -1
- package/dist/commands/ssh.js +71 -35
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +51 -11
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +99 -31
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -7
- package/dist/lib/activity.d.ts +14 -4
- package/dist/lib/activity.js +17 -5
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +56 -0
- package/dist/lib/agents.js +133 -7
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.d.ts +16 -0
- package/dist/lib/config-transfer.js +60 -0
- package/dist/lib/daemon.js +18 -2
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +21 -0
- package/dist/lib/devices/fleet.js +26 -0
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +66 -0
- package/dist/lib/exec.js +195 -6
- package/dist/lib/export.d.ts +72 -0
- package/dist/lib/export.js +269 -0
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +7 -0
- package/dist/lib/hosts/dispatch.js +2 -0
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +13 -5
- package/dist/lib/hosts/run-target.js +26 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +37 -2
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +25 -0
- package/dist/lib/import.js +107 -2
- package/dist/lib/isolation-boundary-report.d.ts +10 -0
- package/dist/lib/isolation-boundary-report.js +35 -0
- package/dist/lib/menubar/install-menubar.d.ts +20 -0
- package/dist/lib/menubar/install-menubar.js +45 -1
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +7 -2
- package/dist/lib/overdue.js +18 -11
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/project-resources.js +20 -5
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routines.d.ts +26 -0
- package/dist/lib/routines.js +19 -2
- package/dist/lib/runner.js +104 -7
- package/dist/lib/secrets/agent.d.ts +66 -1
- package/dist/lib/secrets/agent.js +154 -90
- package/dist/lib/secrets/bundles.d.ts +34 -0
- package/dist/lib/secrets/bundles.js +179 -19
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +21 -3
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +114 -7
- package/dist/lib/session/active.js +323 -52
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +48 -0
- package/dist/lib/session/db.js +238 -14
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +465 -2
- package/dist/lib/session/discover.js +1648 -468
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +130 -30
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +22 -10
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.js +6 -2
- package/dist/lib/shims.d.ts +27 -1
- package/dist/lib/shims.js +88 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +5 -0
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +15 -36
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +48 -2
- package/dist/lib/uninstall.js +1 -45
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +22 -2
- package/dist/lib/versions.js +98 -10
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
|
@@ -1014,44 +1014,144 @@ export function parseAntigravity(dbPath) {
|
|
|
1014
1014
|
* Messages have role (user/assistant) and metadata.
|
|
1015
1015
|
* Parts contain the actual content: text, tool, reasoning, patch, step-start/finish.
|
|
1016
1016
|
*/
|
|
1017
|
+
/**
|
|
1018
|
+
* Parse a Grok (xAI CLI) session into normalized events.
|
|
1019
|
+
*
|
|
1020
|
+
* A Grok session dir holds several files; the conversation transcript is
|
|
1021
|
+
* `chat_history.jsonl`, one JSON object per line with a `type`:
|
|
1022
|
+
* - `system` — the system prompt (skipped; not conversational)
|
|
1023
|
+
* - `user` — `content` is an array of `{ type: 'text', text }` blocks
|
|
1024
|
+
* - `assistant` — `content` is a string, plus a `tool_calls[]` array of
|
|
1025
|
+
* `{ id, name, arguments }` (arguments is a JSON string)
|
|
1026
|
+
* - `reasoning` — chain-of-thought; text (when present) lives in `summary`
|
|
1027
|
+
* - `tool_result` — `{ tool_call_id, content }`, correlated back to the call
|
|
1028
|
+
*
|
|
1029
|
+
* The scanner records `summary.json` as the session's filePath (see
|
|
1030
|
+
* `readGrokMeta` in discover.ts), so resolve `chat_history.jsonl` from the same
|
|
1031
|
+
* dir; also accept being handed the transcript file directly. Per-line
|
|
1032
|
+
* timestamps aren't stored, so every event carries the session's `created_at`
|
|
1033
|
+
* (from summary.json), falling back to the transcript's mtime.
|
|
1034
|
+
*/
|
|
1017
1035
|
export function parseGrok(filePath) {
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1036
|
+
const sessionDir = path.dirname(filePath);
|
|
1037
|
+
const historyPath = filePath.endsWith('chat_history.jsonl')
|
|
1038
|
+
? filePath
|
|
1039
|
+
: path.join(sessionDir, 'chat_history.jsonl');
|
|
1040
|
+
if (!fs.existsSync(historyPath))
|
|
1041
|
+
return [];
|
|
1042
|
+
let timestamp;
|
|
1021
1043
|
try {
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
content: summary.session_summary || 'Grok session',
|
|
1030
|
-
agent: 'grok',
|
|
1031
|
-
metadata: { sessionId: summary.id, cwd: summary.cwd },
|
|
1032
|
-
}];
|
|
1044
|
+
const summaryPath = filePath.endsWith('summary.json')
|
|
1045
|
+
? filePath
|
|
1046
|
+
: path.join(sessionDir, 'summary.json');
|
|
1047
|
+
if (fs.existsSync(summaryPath)) {
|
|
1048
|
+
const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf-8'));
|
|
1049
|
+
if (typeof summary?.created_at === 'string')
|
|
1050
|
+
timestamp = summary.created_at;
|
|
1033
1051
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1052
|
+
}
|
|
1053
|
+
catch { /* fall back to mtime below */ }
|
|
1054
|
+
if (!timestamp) {
|
|
1055
|
+
let mtime = null;
|
|
1056
|
+
try {
|
|
1057
|
+
mtime = fs.statSync(historyPath).mtime;
|
|
1058
|
+
}
|
|
1059
|
+
catch {
|
|
1060
|
+
mtime = null;
|
|
1061
|
+
}
|
|
1062
|
+
timestamp = (mtime ?? new Date()).toISOString();
|
|
1063
|
+
}
|
|
1064
|
+
const content = safeReadSessionFile(historyPath);
|
|
1065
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
1066
|
+
const events = [];
|
|
1067
|
+
const toolCallMap = new Map();
|
|
1068
|
+
// Grok text is either a plain string (assistant) or an array of typed blocks
|
|
1069
|
+
// (user: `{ type: 'text', text }`; reasoning summary: `{ text | summary_text }`).
|
|
1070
|
+
const extractText = (raw) => {
|
|
1071
|
+
if (typeof raw === 'string')
|
|
1072
|
+
return raw.trim();
|
|
1073
|
+
if (Array.isArray(raw)) {
|
|
1074
|
+
return raw
|
|
1075
|
+
.map((part) => typeof part?.text === 'string'
|
|
1076
|
+
? part.text
|
|
1077
|
+
: typeof part?.summary_text === 'string'
|
|
1078
|
+
? part.summary_text
|
|
1079
|
+
: '')
|
|
1080
|
+
.join('')
|
|
1081
|
+
.trim();
|
|
1082
|
+
}
|
|
1083
|
+
return '';
|
|
1084
|
+
};
|
|
1085
|
+
for (const line of lines) {
|
|
1086
|
+
let msg;
|
|
1087
|
+
try {
|
|
1088
|
+
msg = JSON.parse(line);
|
|
1089
|
+
}
|
|
1090
|
+
catch {
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
const type = msg?.type;
|
|
1094
|
+
if (type === 'user') {
|
|
1095
|
+
const text = extractText(msg.content);
|
|
1096
|
+
if (text)
|
|
1097
|
+
events.push({ type: 'message', agent: 'grok', timestamp, role: 'user', content: text });
|
|
1098
|
+
}
|
|
1099
|
+
else if (type === 'assistant') {
|
|
1100
|
+
const text = extractText(msg.content);
|
|
1101
|
+
if (text)
|
|
1102
|
+
events.push({ type: 'message', agent: 'grok', timestamp, role: 'assistant', content: text });
|
|
1103
|
+
const calls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
|
|
1104
|
+
for (const call of calls) {
|
|
1105
|
+
const toolName = typeof call?.name === 'string' ? call.name : 'unknown';
|
|
1106
|
+
let args = {};
|
|
1107
|
+
if (call?.arguments && typeof call.arguments === 'object') {
|
|
1108
|
+
args = call.arguments;
|
|
1046
1109
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1110
|
+
else if (typeof call?.arguments === 'string') {
|
|
1111
|
+
try {
|
|
1112
|
+
args = JSON.parse(call.arguments);
|
|
1113
|
+
}
|
|
1114
|
+
catch {
|
|
1115
|
+
args = { _raw: call.arguments };
|
|
1116
|
+
}
|
|
1049
1117
|
}
|
|
1118
|
+
if (typeof call?.id === 'string')
|
|
1119
|
+
toolCallMap.set(call.id, toolName);
|
|
1120
|
+
events.push({
|
|
1121
|
+
type: 'tool_use',
|
|
1122
|
+
agent: 'grok',
|
|
1123
|
+
timestamp,
|
|
1124
|
+
tool: toolName,
|
|
1125
|
+
args,
|
|
1126
|
+
path: args.path || args.file_path || undefined,
|
|
1127
|
+
command: typeof args.command === 'string' ? args.command : undefined,
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
else if (type === 'reasoning') {
|
|
1132
|
+
const text = extractText(msg.summary);
|
|
1133
|
+
if (text)
|
|
1134
|
+
events.push({ type: 'thinking', agent: 'grok', timestamp, content: text });
|
|
1135
|
+
}
|
|
1136
|
+
else if (type === 'tool_result') {
|
|
1137
|
+
const callId = typeof msg.tool_call_id === 'string' ? msg.tool_call_id : undefined;
|
|
1138
|
+
const toolName = (callId && toolCallMap.get(callId)) || 'unknown';
|
|
1139
|
+
const output = typeof msg.content === 'string' ? msg.content : extractText(msg.content);
|
|
1140
|
+
const isError = typeof output === 'string' && output.startsWith('Error:');
|
|
1141
|
+
events.push({
|
|
1142
|
+
type: isError ? 'error' : 'tool_result',
|
|
1143
|
+
agent: 'grok',
|
|
1144
|
+
timestamp,
|
|
1145
|
+
tool: toolName,
|
|
1146
|
+
success: !isError,
|
|
1147
|
+
output: output.length > 500 ? output.slice(0, 497) + '...' : output,
|
|
1050
1148
|
});
|
|
1149
|
+
if (callId)
|
|
1150
|
+
toolCallMap.delete(callId);
|
|
1051
1151
|
}
|
|
1152
|
+
// `system` lines are the system prompt — intentionally skipped.
|
|
1052
1153
|
}
|
|
1053
|
-
|
|
1054
|
-
return [];
|
|
1154
|
+
return events;
|
|
1055
1155
|
}
|
|
1056
1156
|
export function parseOpenCode(filePath) {
|
|
1057
1157
|
const [dbPath, sessionId] = filePath.split('#');
|
|
@@ -4,6 +4,23 @@ export interface PidSessionEntry {
|
|
|
4
4
|
/** The launch session id. Present for agents launched with a known id (Claude). */
|
|
5
5
|
sessionId?: string;
|
|
6
6
|
cwd?: string;
|
|
7
|
+
/**
|
|
8
|
+
* The launch id minted by `ag run` and exported to the child as
|
|
9
|
+
* `AGENT_LAUNCH_ID`. The agent's SessionStart hook records the SAME id in its
|
|
10
|
+
* own state file (`terminals/sessions/<pid>.json`, `launch_id`), so the two
|
|
11
|
+
* records reconcile by this key even when the hook runs under a DIFFERENT pid
|
|
12
|
+
* (tmux pane leaf, cmd.exe wrapper) — see resolveHookSessionId in
|
|
13
|
+
* session/hook-sessions.ts. This
|
|
14
|
+
* is how a non-Claude launch (whose id we don't know at spawn) gets an exact
|
|
15
|
+
* session id at listing time instead of the newest-jsonl heuristic.
|
|
16
|
+
*/
|
|
17
|
+
launchId?: string;
|
|
18
|
+
/**
|
|
19
|
+
* `AGENT_TERMINAL_ID` when the launch inherited one (a Factory VS Code tab).
|
|
20
|
+
* A secondary join key to the hook's `terminal_id`, for agents the extension
|
|
21
|
+
* spawned. Best-effort — undefined outside the extension.
|
|
22
|
+
*/
|
|
23
|
+
terminalId?: string;
|
|
7
24
|
/**
|
|
8
25
|
* `$TMUX_PANE` at launch — stored for diagnostics and possible future
|
|
9
26
|
* disambiguation. NOT currently consulted on read: the listing path keys
|
|
@@ -18,5 +35,20 @@ export declare function extractSessionIdArg(args: string[]): string | undefined;
|
|
|
18
35
|
export declare function writePidSessionEntry(entry: PidSessionEntry): void;
|
|
19
36
|
/** Look up a live pid's recorded session. Returns undefined if absent/corrupt. */
|
|
20
37
|
export declare function readPidSessionEntry(pid: number): PidSessionEntry | undefined;
|
|
21
|
-
/**
|
|
22
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Every recorded launch, one entry per live-or-dead pid. Used to index launches
|
|
40
|
+
* by their `tmuxPane` so the authoritative tmux source can attribute a pane it did
|
|
41
|
+
* NOT wrap (an agent bare-spawned into an existing pane) to its exact launch —
|
|
42
|
+
* the caller filters to live pids. Best-effort: unreadable/corrupt files are
|
|
43
|
+
* skipped, a missing dir yields `[]`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function listPidSessionEntries(): PidSessionEntry[];
|
|
46
|
+
/**
|
|
47
|
+
* Remove entries whose pid is no longer alive. Best-effort housekeeping.
|
|
48
|
+
*
|
|
49
|
+
* `isAlive` receives the entry's recorded `startedAtMs` so it can reject a pid
|
|
50
|
+
* that a newer process recycled (a "zombie" registry entry), not just one that
|
|
51
|
+
* no longer exists. An unreadable/corrupt entry falls back to the pid-only
|
|
52
|
+
* check.
|
|
53
|
+
*/
|
|
54
|
+
export declare function prunePidSessionRegistry(isAlive: (pid: number, startedAtMs?: number) => boolean): void;
|
|
@@ -81,7 +81,43 @@ export function readPidSessionEntry(pid) {
|
|
|
81
81
|
}
|
|
82
82
|
return undefined;
|
|
83
83
|
}
|
|
84
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Every recorded launch, one entry per live-or-dead pid. Used to index launches
|
|
86
|
+
* by their `tmuxPane` so the authoritative tmux source can attribute a pane it did
|
|
87
|
+
* NOT wrap (an agent bare-spawned into an existing pane) to its exact launch —
|
|
88
|
+
* the caller filters to live pids. Best-effort: unreadable/corrupt files are
|
|
89
|
+
* skipped, a missing dir yields `[]`.
|
|
90
|
+
*/
|
|
91
|
+
export function listPidSessionEntries() {
|
|
92
|
+
let files;
|
|
93
|
+
try {
|
|
94
|
+
files = fs.readdirSync(pidRegistryDir()).filter(f => f.endsWith('.json'));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const out = [];
|
|
100
|
+
for (const f of files) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(pidRegistryDir(), f), 'utf8'));
|
|
103
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.pid === 'number') {
|
|
104
|
+
out.push(parsed);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
/* raced with a writer/pruner, or corrupt — skip */
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Remove entries whose pid is no longer alive. Best-effort housekeeping.
|
|
115
|
+
*
|
|
116
|
+
* `isAlive` receives the entry's recorded `startedAtMs` so it can reject a pid
|
|
117
|
+
* that a newer process recycled (a "zombie" registry entry), not just one that
|
|
118
|
+
* no longer exists. An unreadable/corrupt entry falls back to the pid-only
|
|
119
|
+
* check.
|
|
120
|
+
*/
|
|
85
121
|
export function prunePidSessionRegistry(isAlive) {
|
|
86
122
|
let files;
|
|
87
123
|
try {
|
|
@@ -92,7 +128,18 @@ export function prunePidSessionRegistry(isAlive) {
|
|
|
92
128
|
}
|
|
93
129
|
for (const f of files) {
|
|
94
130
|
const pid = Number(f.slice(0, -'.json'.length));
|
|
95
|
-
if (!Number.isInteger(pid)
|
|
131
|
+
if (!Number.isInteger(pid))
|
|
132
|
+
continue;
|
|
133
|
+
let startedAtMs;
|
|
134
|
+
try {
|
|
135
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(pidRegistryDir(), f), 'utf8'));
|
|
136
|
+
if (typeof parsed?.startedAtMs === 'number')
|
|
137
|
+
startedAtMs = parsed.startedAtMs;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
/* unreadable/corrupt — fall back to the pid-only liveness check */
|
|
141
|
+
}
|
|
142
|
+
if (isAlive(pid, startedAtMs))
|
|
96
143
|
continue;
|
|
97
144
|
try {
|
|
98
145
|
fs.unlinkSync(path.join(pidRegistryDir(), f));
|
|
@@ -12,10 +12,15 @@ import type { SessionEvent, SessionMeta } from './types.js';
|
|
|
12
12
|
*/
|
|
13
13
|
export declare function relativeToCwd(absPath: string, cwd?: string): string;
|
|
14
14
|
/**
|
|
15
|
-
* Wrap label in an OSC 8 hyperlink when the terminal
|
|
16
|
-
* Degrades to plain label otherwise.
|
|
15
|
+
* Wrap a filesystem path label in an OSC 8 `file://` hyperlink when the terminal
|
|
16
|
+
* supports it. Degrades to the plain label otherwise.
|
|
17
17
|
*/
|
|
18
18
|
export declare function linkPath(absPath: string, label: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Wrap a label in an OSC 8 hyperlink to an arbitrary URL (a PR on GitHub, a Linear
|
|
21
|
+
* issue, …) when the terminal supports it. Degrades to the plain label otherwise.
|
|
22
|
+
*/
|
|
23
|
+
export declare function linkUrl(url: string, label: string): string;
|
|
19
24
|
/**
|
|
20
25
|
* Unwrap wrapper prefixes to find the actual executable.
|
|
21
26
|
*/
|
|
@@ -28,19 +28,31 @@ export function relativeToCwd(absPath, cwd) {
|
|
|
28
28
|
}
|
|
29
29
|
return absPath;
|
|
30
30
|
}
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*/
|
|
35
|
-
export function linkPath(absPath, label) {
|
|
36
|
-
if (process.stdout.isTTY &&
|
|
31
|
+
/** Best-effort feature-detect for OSC 8 hyperlink support in the current TTY. */
|
|
32
|
+
function supportsHyperlinks() {
|
|
33
|
+
return Boolean(process.stdout.isTTY &&
|
|
37
34
|
(process.env.TERM_PROGRAM ||
|
|
38
35
|
process.env.WT_SESSION ||
|
|
39
36
|
process.env.KITTY_WINDOW_ID ||
|
|
40
|
-
process.env.WEZTERM_PANE))
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
process.env.WEZTERM_PANE));
|
|
38
|
+
}
|
|
39
|
+
/** Wrap `label` in an OSC 8 hyperlink to `target`. Callers gate on {@link supportsHyperlinks}. */
|
|
40
|
+
function osc8(target, label) {
|
|
41
|
+
return `\x1b]8;;${target}\x1b\\${label}\x1b]8;;\x1b\\`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Wrap a filesystem path label in an OSC 8 `file://` hyperlink when the terminal
|
|
45
|
+
* supports it. Degrades to the plain label otherwise.
|
|
46
|
+
*/
|
|
47
|
+
export function linkPath(absPath, label) {
|
|
48
|
+
return supportsHyperlinks() ? osc8(`file://${absPath}`, label) : label;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Wrap a label in an OSC 8 hyperlink to an arbitrary URL (a PR on GitHub, a Linear
|
|
52
|
+
* issue, …) when the terminal supports it. Degrades to the plain label otherwise.
|
|
53
|
+
*/
|
|
54
|
+
export function linkUrl(url, label) {
|
|
55
|
+
return supportsHyperlinks() ? osc8(url, label) : label;
|
|
44
56
|
}
|
|
45
57
|
// ── Command grouping ──────────────────────────────────────────────────────────
|
|
46
58
|
/**
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical derivation of a session's 8-char `shortId`.
|
|
3
|
+
*
|
|
4
|
+
* Every session parser used to inline `id.slice(0, 8)` — some with a
|
|
5
|
+
* `.replace(prefix, '')` strip first (`session_`, `api-`, `ses_`). That strip is
|
|
6
|
+
* the bug: an id that is *only* its prefix (a bare `session_` directory, an id of
|
|
7
|
+
* exactly `api-`) strips to `''`, and `''.slice(0, 8)` is still `''`. An empty
|
|
8
|
+
* shortId then passes the `short_id TEXT NOT NULL` constraint (empty string is not
|
|
9
|
+
* NULL) yet matches nothing in the `short_id LIKE ?` picker lookups — a silently
|
|
10
|
+
* corrupt index row.
|
|
11
|
+
*
|
|
12
|
+
* This helper is the single source of truth: it strips the optional prefix, takes
|
|
13
|
+
* the first 8 chars, and — the whole point — guarantees a NON-EMPTY result by
|
|
14
|
+
* falling back to the unstripped id when the strip empties it, so a row always
|
|
15
|
+
* stays addressable by its shortId.
|
|
16
|
+
*/
|
|
17
|
+
export declare function deriveShortId(id: string, stripPrefix?: RegExp): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical derivation of a session's 8-char `shortId`.
|
|
3
|
+
*
|
|
4
|
+
* Every session parser used to inline `id.slice(0, 8)` — some with a
|
|
5
|
+
* `.replace(prefix, '')` strip first (`session_`, `api-`, `ses_`). That strip is
|
|
6
|
+
* the bug: an id that is *only* its prefix (a bare `session_` directory, an id of
|
|
7
|
+
* exactly `api-`) strips to `''`, and `''.slice(0, 8)` is still `''`. An empty
|
|
8
|
+
* shortId then passes the `short_id TEXT NOT NULL` constraint (empty string is not
|
|
9
|
+
* NULL) yet matches nothing in the `short_id LIKE ?` picker lookups — a silently
|
|
10
|
+
* corrupt index row.
|
|
11
|
+
*
|
|
12
|
+
* This helper is the single source of truth: it strips the optional prefix, takes
|
|
13
|
+
* the first 8 chars, and — the whole point — guarantees a NON-EMPTY result by
|
|
14
|
+
* falling back to the unstripped id when the strip empties it, so a row always
|
|
15
|
+
* stays addressable by its shortId.
|
|
16
|
+
*/
|
|
17
|
+
export function deriveShortId(id, stripPrefix) {
|
|
18
|
+
const stripped = stripPrefix ? id.replace(stripPrefix, '') : id;
|
|
19
|
+
return stripped.slice(0, 8) || id.slice(0, 8) || id;
|
|
20
|
+
}
|
|
@@ -378,8 +378,12 @@ export function inferActivity(events, ctx = {}) {
|
|
|
378
378
|
// A prose question takes a free-text reply (no select-list), so no options/keys.
|
|
379
379
|
// Unlike the structural plan/ask signals above, the prose heuristic DECAYS: a
|
|
380
380
|
// question nobody answered within PROSE_QUESTION_FRESH_MS is a session that
|
|
381
|
-
// ended, not one that needs you (RUSH-1522).
|
|
382
|
-
|
|
381
|
+
// ended, not one that needs you (RUSH-1522). A prose "?" is only "waiting on
|
|
382
|
+
// you" while the file is DEMONSTRABLY fresh — with no mtime signal at all we
|
|
383
|
+
// cannot assert freshness, so the heuristic must not fire (an unknown-age
|
|
384
|
+
// prose question is a session that ended, closing RUSH-1522's null-mtime hole
|
|
385
|
+
// where a null mtime kept the question forever).
|
|
386
|
+
const questionFresh = ctx.mtimeMs != null && Date.now() - ctx.mtimeMs < PROSE_QUESTION_FRESH_MS;
|
|
383
387
|
if (questionFresh && looksLikeQuestion(last.content ?? '')) {
|
|
384
388
|
const text = oneLine(last.content ?? '');
|
|
385
389
|
return { ...base, activity: 'waiting_input', awaitingReason: 'question', question: { text, reason: 'question' } };
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export interface ConflictInfo {
|
|
|
77
77
|
* top-level entry add/remove — deep edits to plugin contents won't
|
|
78
78
|
* trigger auto-resync, run `agents sync` for that.
|
|
79
79
|
*/
|
|
80
|
-
export declare const SHIM_SCHEMA_VERSION =
|
|
80
|
+
export declare const SHIM_SCHEMA_VERSION = 27;
|
|
81
81
|
/**
|
|
82
82
|
* Generate the full bash shim script for the given agent. The returned string
|
|
83
83
|
* is written to ~/.agents/shims/{cliCommand} and made executable.
|
|
@@ -508,6 +508,32 @@ export declare function addShimsToPath(overrides?: {
|
|
|
508
508
|
shimsDir?: string;
|
|
509
509
|
}): ShimPathResult;
|
|
510
510
|
export declare function listAgentsWithInstalledVersions(): AgentId[];
|
|
511
|
+
/**
|
|
512
|
+
* Thrown when an operation would carry an isolated-only agent across the isolation
|
|
513
|
+
* boundary. Callers that can explain the situation catch it and print guidance; the
|
|
514
|
+
* throw is what makes the boundary a property of the code rather than a convention
|
|
515
|
+
* every future call site has to remember.
|
|
516
|
+
*/
|
|
517
|
+
export declare class IsolationBoundaryError extends Error {
|
|
518
|
+
readonly agent: AgentId;
|
|
519
|
+
readonly operation: string;
|
|
520
|
+
constructor(agent: AgentId, operation: string);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* True when EVERY installed version of `agent` is isolated (and at least one is).
|
|
524
|
+
*
|
|
525
|
+
* This is the switch: installing with `--isolated` is itself the act of opting in,
|
|
526
|
+
* so there is no mode to set and none to forget. It is per-agent, so an isolated
|
|
527
|
+
* codex constrains nothing about claude. And the escape hatch is inherent — remove
|
|
528
|
+
* the isolated copies and the agent is ordinary again, which means the state that
|
|
529
|
+
* grants protection is the same state you delete to drop it.
|
|
530
|
+
*
|
|
531
|
+
* An agent with any NORMAL version is not protected: that install already owns the
|
|
532
|
+
* launcher and the real `~/.<agent>`, so there is no boundary left to defend.
|
|
533
|
+
*/
|
|
534
|
+
export declare function isIsolationProtected(agent: AgentId): boolean;
|
|
535
|
+
/** Refuse `operation` when `agent` is isolated-only. */
|
|
536
|
+
export declare function assertIsolationBoundary(agent: AgentId, operation: string): void;
|
|
511
537
|
export declare function listAgentsWithNonIsolatedInstalledVersions(): AgentId[];
|
|
512
538
|
/**
|
|
513
539
|
* Resource diff between two versions. Each field lists resources present in
|
package/dist/lib/shims.js
CHANGED
|
@@ -222,7 +222,7 @@ async function promptConflictStrategy(conflictInfos) {
|
|
|
222
222
|
// The old dispatcher checked only the global dir, so a pinned grok that
|
|
223
223
|
// installed into the versioned home fell through to the "not installed"
|
|
224
224
|
// error.
|
|
225
|
-
export const SHIM_SCHEMA_VERSION =
|
|
225
|
+
export const SHIM_SCHEMA_VERSION = 27;
|
|
226
226
|
/** Internal marker string used to embed the schema version in shim scripts. */
|
|
227
227
|
const SHIM_VERSION_MARKER = 'agents-shim-version:';
|
|
228
228
|
function shellQuote(value) {
|
|
@@ -278,13 +278,20 @@ export COPILOT_HOME="$VERSION_DIR/home/${configDirName}"
|
|
|
278
278
|
# This gives agents-cli full versioned isolation + resource sync for grok.
|
|
279
279
|
export GROK_HOME="$VERSION_DIR/home/.grok"
|
|
280
280
|
`
|
|
281
|
-
: agent === '
|
|
281
|
+
: agent === 'opencode'
|
|
282
282
|
? `
|
|
283
|
+
# OpenCode reads plugins, agents, commands, and other config-directory
|
|
284
|
+
# resources from OPENCODE_CONFIG_DIR. Point it at the versioned global config
|
|
285
|
+
# tree where agents-cli syncs OpenCode resources.
|
|
286
|
+
export OPENCODE_CONFIG_DIR="$VERSION_DIR/home/.config/opencode"
|
|
287
|
+
`
|
|
288
|
+
: agent === 'kimi'
|
|
289
|
+
? `
|
|
283
290
|
# Kimi Code CLI honors KIMI_CODE_HOME to relocate ~/.kimi-code (config.toml,
|
|
284
291
|
# mcp.json, sessions, skills, hooks). Point it at the versioned home.
|
|
285
292
|
export KIMI_CODE_HOME="$VERSION_DIR/home/${configDirName}"
|
|
286
293
|
`
|
|
287
|
-
|
|
294
|
+
: '';
|
|
288
295
|
const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
|
|
289
296
|
return `#!/bin/bash
|
|
290
297
|
# Auto-generated by agents-cli - do not edit
|
|
@@ -661,6 +668,9 @@ export function shimTargetsFor(platform) {
|
|
|
661
668
|
* Create a shim for an agent.
|
|
662
669
|
*/
|
|
663
670
|
export function createShim(agent) {
|
|
671
|
+
// A bare shim puts agents-cli first on PATH for this agent — the opposite of what
|
|
672
|
+
// an isolated-only install promises.
|
|
673
|
+
assertIsolationBoundary(agent, 'create the bare shim');
|
|
664
674
|
ensureAgentsDir();
|
|
665
675
|
const shimsDir = getShimsDir();
|
|
666
676
|
const agentConfig = AGENTS[agent];
|
|
@@ -864,7 +874,7 @@ function assertSafeVersion(version) {
|
|
|
864
874
|
* KEEP IN SYNC with the `managedEnv` switch in `generateVersionedAliasScript`.
|
|
865
875
|
* The colocated test `shims.isolation-capability.test.ts` enforces this.
|
|
866
876
|
*/
|
|
867
|
-
export const CONFIG_ENV_ISOLATED_AGENTS = ['claude', 'codex', 'copilot', 'grok', 'kimi'];
|
|
877
|
+
export const CONFIG_ENV_ISOLATED_AGENTS = ['claude', 'codex', 'copilot', 'grok', 'kimi', 'opencode'];
|
|
868
878
|
/**
|
|
869
879
|
* Whether an agent supports a clean `--isolated` install — i.e. its config
|
|
870
880
|
* location can be redirected by an env var so the isolated copy stays fully
|
|
@@ -909,13 +919,20 @@ export COPILOT_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${
|
|
|
909
919
|
# aliases at the versioned home for isolation parity with the main shim.
|
|
910
920
|
export GROK_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
|
|
911
921
|
`
|
|
912
|
-
: agent === '
|
|
922
|
+
: agent === 'opencode'
|
|
913
923
|
? `
|
|
924
|
+
# OpenCode reads plugins, agents, commands, and other config-directory
|
|
925
|
+
# resources from OPENCODE_CONFIG_DIR. Point direct aliases at the versioned
|
|
926
|
+
# global config tree where agents-cli syncs OpenCode resources.
|
|
927
|
+
export OPENCODE_CONFIG_DIR="$HOME/.agents/.history/versions/${agent}/${version}/home/.config/opencode"
|
|
928
|
+
`
|
|
929
|
+
: agent === 'kimi'
|
|
930
|
+
? `
|
|
914
931
|
# Kimi Code CLI honors KIMI_CODE_HOME to relocate ~/.kimi-code (config.toml,
|
|
915
932
|
# mcp.json, sessions, skills, hooks). Point direct aliases at the versioned home.
|
|
916
933
|
export KIMI_CODE_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
|
|
917
934
|
`
|
|
918
|
-
|
|
935
|
+
: '';
|
|
919
936
|
const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
|
|
920
937
|
// Resolve the binary the same way the main shim does (see generateShimScript).
|
|
921
938
|
// Grok and Droid do NOT ship into node_modules/.bin — Grok downloads a native
|
|
@@ -1322,6 +1339,8 @@ export function carryForwardAuthFiles(agent, toConfigDir) {
|
|
|
1322
1339
|
}
|
|
1323
1340
|
}
|
|
1324
1341
|
export async function switchConfigSymlink(agent, version) {
|
|
1342
|
+
// Moves the user's real ~/.<agent> aside and symlinks it into a version home.
|
|
1343
|
+
assertIsolationBoundary(agent, 'repoint your real config directory');
|
|
1325
1344
|
const configPath = getAgentConfigPath(agent);
|
|
1326
1345
|
const versionConfigPath = getVersionConfigPath(agent, version);
|
|
1327
1346
|
// Ensure version config directory exists
|
|
@@ -1426,6 +1445,8 @@ export async function switchConfigSymlink(agent, version) {
|
|
|
1426
1445
|
* ALL installed versions so they inherit the current account.
|
|
1427
1446
|
*/
|
|
1428
1447
|
export function switchHomeFileSymlinks(agent, version) {
|
|
1448
|
+
// Same, for home-level files such as ~/.claude.json.
|
|
1449
|
+
assertIsolationBoundary(agent, 'repoint your home-level config files');
|
|
1429
1450
|
const agentConfig = AGENTS[agent];
|
|
1430
1451
|
const homeFiles = agentConfig.homeFiles;
|
|
1431
1452
|
if (!homeFiles || homeFiles.length === 0)
|
|
@@ -2076,6 +2097,9 @@ function canonicalOrNull(p) {
|
|
|
2076
2097
|
* - Never records our own shim as the "original" (would loop).
|
|
2077
2098
|
*/
|
|
2078
2099
|
export function adoptShadowingLauncher(agent, overrides) {
|
|
2100
|
+
// Repoints the user's OWN launcher symlink at our shim — the most invasive thing
|
|
2101
|
+
// in the codebase, and the one with no isolated-scoped equivalent at all.
|
|
2102
|
+
assertIsolationBoundary(agent, 'adopt your launcher');
|
|
2079
2103
|
const shimsDir = overrides?.shimsDir ?? getShimsDir();
|
|
2080
2104
|
const shimPath = path.join(shimsDir, AGENTS[agent].cliCommand);
|
|
2081
2105
|
const shimReal = canonical(shimPath);
|
|
@@ -2431,6 +2455,64 @@ export function listAgentsWithInstalledVersions() {
|
|
|
2431
2455
|
function isInstalledVersionIsolated(agent, version) {
|
|
2432
2456
|
return fs.existsSync(path.join(getVersionsDir(), agent, version, '.isolated'));
|
|
2433
2457
|
}
|
|
2458
|
+
/**
|
|
2459
|
+
* Thrown when an operation would carry an isolated-only agent across the isolation
|
|
2460
|
+
* boundary. Callers that can explain the situation catch it and print guidance; the
|
|
2461
|
+
* throw is what makes the boundary a property of the code rather than a convention
|
|
2462
|
+
* every future call site has to remember.
|
|
2463
|
+
*/
|
|
2464
|
+
export class IsolationBoundaryError extends Error {
|
|
2465
|
+
agent;
|
|
2466
|
+
operation;
|
|
2467
|
+
constructor(agent, operation) {
|
|
2468
|
+
super(`${agent} is installed only as isolated copies; "${operation}" would adopt it into your local setup.`);
|
|
2469
|
+
this.agent = agent;
|
|
2470
|
+
this.operation = operation;
|
|
2471
|
+
this.name = 'IsolationBoundaryError';
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
/**
|
|
2475
|
+
* True when EVERY installed version of `agent` is isolated (and at least one is).
|
|
2476
|
+
*
|
|
2477
|
+
* This is the switch: installing with `--isolated` is itself the act of opting in,
|
|
2478
|
+
* so there is no mode to set and none to forget. It is per-agent, so an isolated
|
|
2479
|
+
* codex constrains nothing about claude. And the escape hatch is inherent — remove
|
|
2480
|
+
* the isolated copies and the agent is ordinary again, which means the state that
|
|
2481
|
+
* grants protection is the same state you delete to drop it.
|
|
2482
|
+
*
|
|
2483
|
+
* An agent with any NORMAL version is not protected: that install already owns the
|
|
2484
|
+
* launcher and the real `~/.<agent>`, so there is no boundary left to defend.
|
|
2485
|
+
*/
|
|
2486
|
+
export function isIsolationProtected(agent) {
|
|
2487
|
+
const agentVersionsDir = path.join(getVersionsDir(), agent);
|
|
2488
|
+
let dirs;
|
|
2489
|
+
try {
|
|
2490
|
+
dirs = fs.readdirSync(agentVersionsDir, { withFileTypes: true })
|
|
2491
|
+
.filter((e) => e.isDirectory())
|
|
2492
|
+
.map((e) => e.name);
|
|
2493
|
+
}
|
|
2494
|
+
catch {
|
|
2495
|
+
return false;
|
|
2496
|
+
}
|
|
2497
|
+
// Count only directories that are actually an install. A bare version dir is
|
|
2498
|
+
// scaffolding, not a non-isolated version — and treating it as one would disable
|
|
2499
|
+
// protection at exactly the wrong moment: an adopting path that does
|
|
2500
|
+
// `mkdirSync(<version>/home)` before adopting would flip this to false with its own
|
|
2501
|
+
// first line and then walk straight through the gates. Ignoring scaffolding biases
|
|
2502
|
+
// the predicate toward protecting, which is the safe direction to fail in.
|
|
2503
|
+
const installed = dirs.filter((v) => {
|
|
2504
|
+
const dir = path.join(agentVersionsDir, v);
|
|
2505
|
+
return fs.existsSync(path.join(dir, 'node_modules')) || fs.existsSync(path.join(dir, 'package.json'));
|
|
2506
|
+
});
|
|
2507
|
+
if (installed.length === 0)
|
|
2508
|
+
return false;
|
|
2509
|
+
return installed.every((v) => isInstalledVersionIsolated(agent, v));
|
|
2510
|
+
}
|
|
2511
|
+
/** Refuse `operation` when `agent` is isolated-only. */
|
|
2512
|
+
export function assertIsolationBoundary(agent, operation) {
|
|
2513
|
+
if (isIsolationProtected(agent))
|
|
2514
|
+
throw new IsolationBoundaryError(agent, operation);
|
|
2515
|
+
}
|
|
2434
2516
|
export function listAgentsWithNonIsolatedInstalledVersions() {
|
|
2435
2517
|
const versionsDir = getVersionsDir();
|
|
2436
2518
|
if (!fs.existsSync(versionsDir)) {
|
package/dist/lib/sqlite.d.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Picks `bun:sqlite` under Bun and `node:sqlite` under Node (>=22.5). Avoids
|
|
5
5
|
* the native `better-sqlite3` addon entirely so there is no prebuild compile
|
|
6
|
-
* and no Node/Bun ABI mismatch
|
|
7
|
-
*
|
|
6
|
+
* and no Node/Bun ABI mismatch. Both runtimes are production: `dist/index.js`
|
|
7
|
+
* runs under Node, while the signed standalone `dist/bin/agents` (what the
|
|
8
|
+
* shims exec) embeds Bun.
|
|
8
9
|
*
|
|
9
10
|
* Exposes the small better-sqlite3-shaped surface area the rest of the
|
|
10
11
|
* codebase already uses: `prepare/exec/pragma/transaction/close` on the DB,
|