@phnx-labs/agents-cli 1.20.74 → 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.
Files changed (199) hide show
  1. package/CHANGELOG.md +893 -0
  2. package/README.md +36 -10
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.js +21 -3
  5. package/dist/commands/attach.d.ts +10 -0
  6. package/dist/commands/attach.js +41 -0
  7. package/dist/commands/computer.js +3 -3
  8. package/dist/commands/detach-core.d.ts +46 -0
  9. package/dist/commands/detach-core.js +61 -0
  10. package/dist/commands/detach.d.ts +5 -0
  11. package/dist/commands/detach.js +171 -0
  12. package/dist/commands/doctor.js +18 -0
  13. package/dist/commands/exec.js +31 -3
  14. package/dist/commands/feed.d.ts +7 -1
  15. package/dist/commands/feed.js +47 -4
  16. package/dist/commands/go.d.ts +9 -2
  17. package/dist/commands/go.js +16 -6
  18. package/dist/commands/harness.d.ts +14 -0
  19. package/dist/commands/harness.js +145 -0
  20. package/dist/commands/import.js +38 -8
  21. package/dist/commands/inspect.js +5 -2
  22. package/dist/commands/profiles.d.ts +20 -0
  23. package/dist/commands/profiles.js +68 -21
  24. package/dist/commands/repo.js +14 -0
  25. package/dist/commands/routines.js +15 -2
  26. package/dist/commands/secrets.js +44 -21
  27. package/dist/commands/sessions-browser.js +1 -1
  28. package/dist/commands/sessions-export.js +19 -5
  29. package/dist/commands/sessions-migrate.d.ts +29 -0
  30. package/dist/commands/sessions-migrate.js +596 -0
  31. package/dist/commands/sessions-picker.js +16 -1
  32. package/dist/commands/sessions.d.ts +36 -13
  33. package/dist/commands/sessions.js +210 -40
  34. package/dist/commands/setup-computer.js +2 -2
  35. package/dist/commands/ssh.js +71 -35
  36. package/dist/commands/status.js +10 -2
  37. package/dist/commands/versions.js +15 -6
  38. package/dist/commands/view.d.ts +5 -0
  39. package/dist/commands/view.js +24 -4
  40. package/dist/commands/watchdog.d.ts +7 -2
  41. package/dist/commands/watchdog.js +73 -57
  42. package/dist/index.js +58 -7
  43. package/dist/lib/activity.d.ts +14 -4
  44. package/dist/lib/activity.js +17 -5
  45. package/dist/lib/actor.d.ts +50 -0
  46. package/dist/lib/actor.js +166 -0
  47. package/dist/lib/agent-spec/provider.js +2 -1
  48. package/dist/lib/agent-spec/resolve.js +19 -5
  49. package/dist/lib/agent-spec/types.d.ts +9 -1
  50. package/dist/lib/agents.d.ts +21 -0
  51. package/dist/lib/agents.js +70 -6
  52. package/dist/lib/cloud/codex.d.ts +2 -0
  53. package/dist/lib/cloud/codex.js +14 -3
  54. package/dist/lib/cloud/session-index.d.ts +32 -0
  55. package/dist/lib/cloud/session-index.js +58 -0
  56. package/dist/lib/cloud/store.d.ts +7 -0
  57. package/dist/lib/cloud/store.js +25 -0
  58. package/dist/lib/config-transfer.js +4 -0
  59. package/dist/lib/daemon.js +18 -2
  60. package/dist/lib/devices/connect.d.ts +15 -1
  61. package/dist/lib/devices/connect.js +15 -1
  62. package/dist/lib/devices/fleet.d.ts +21 -0
  63. package/dist/lib/devices/fleet.js +26 -0
  64. package/dist/lib/devices/health-report.d.ts +38 -0
  65. package/dist/lib/devices/health-report.js +214 -0
  66. package/dist/lib/devices/health.js +4 -1
  67. package/dist/lib/devices/reachability.d.ts +31 -0
  68. package/dist/lib/devices/reachability.js +40 -0
  69. package/dist/lib/devices/registry.d.ts +33 -0
  70. package/dist/lib/devices/registry.js +37 -0
  71. package/dist/lib/devices/resolve-target.d.ts +15 -27
  72. package/dist/lib/devices/resolve-target.js +63 -102
  73. package/dist/lib/devices/sync.d.ts +18 -0
  74. package/dist/lib/devices/sync.js +23 -1
  75. package/dist/lib/devices/tailscale.d.ts +3 -0
  76. package/dist/lib/devices/tailscale.js +1 -0
  77. package/dist/lib/events.d.ts +1 -1
  78. package/dist/lib/exec.d.ts +66 -0
  79. package/dist/lib/exec.js +195 -6
  80. package/dist/lib/feed-post.d.ts +63 -0
  81. package/dist/lib/feed-post.js +204 -0
  82. package/dist/lib/fleet/apply.d.ts +32 -2
  83. package/dist/lib/fleet/apply.js +97 -10
  84. package/dist/lib/fleet/types.d.ts +11 -0
  85. package/dist/lib/fs-walk.d.ts +13 -0
  86. package/dist/lib/fs-walk.js +16 -7
  87. package/dist/lib/hooks.d.ts +1 -0
  88. package/dist/lib/hooks.js +156 -0
  89. package/dist/lib/hosts/dispatch.d.ts +7 -0
  90. package/dist/lib/hosts/dispatch.js +2 -0
  91. package/dist/lib/hosts/passthrough.js +2 -0
  92. package/dist/lib/hosts/reconnect.d.ts +78 -0
  93. package/dist/lib/hosts/reconnect.js +127 -0
  94. package/dist/lib/hosts/registry.d.ts +75 -14
  95. package/dist/lib/hosts/registry.js +205 -30
  96. package/dist/lib/hosts/remote-cmd.js +1 -0
  97. package/dist/lib/hosts/run-target.d.ts +13 -5
  98. package/dist/lib/hosts/run-target.js +26 -10
  99. package/dist/lib/hosts/session-index.d.ts +18 -1
  100. package/dist/lib/hosts/session-index.js +37 -2
  101. package/dist/lib/hosts/session-marker.d.ts +33 -0
  102. package/dist/lib/hosts/session-marker.js +51 -0
  103. package/dist/lib/hosts/tasks.d.ts +8 -4
  104. package/dist/lib/import.d.ts +2 -0
  105. package/dist/lib/import.js +35 -2
  106. package/dist/lib/menubar/install-menubar.d.ts +20 -0
  107. package/dist/lib/menubar/install-menubar.js +45 -1
  108. package/dist/lib/migrate.d.ts +16 -0
  109. package/dist/lib/migrate.js +36 -0
  110. package/dist/lib/models.d.ts +27 -0
  111. package/dist/lib/models.js +54 -1
  112. package/dist/lib/overdue.d.ts +7 -2
  113. package/dist/lib/overdue.js +18 -11
  114. package/dist/lib/picker.js +4 -1
  115. package/dist/lib/platform/process.d.ts +17 -0
  116. package/dist/lib/platform/process.js +70 -0
  117. package/dist/lib/profiles-presets.js +9 -7
  118. package/dist/lib/profiles.d.ts +31 -0
  119. package/dist/lib/profiles.js +70 -0
  120. package/dist/lib/pty-server.d.ts +2 -10
  121. package/dist/lib/pty-server.js +4 -38
  122. package/dist/lib/registry.d.ts +1 -1
  123. package/dist/lib/registry.js +48 -8
  124. package/dist/lib/rotate.d.ts +18 -0
  125. package/dist/lib/rotate.js +28 -0
  126. package/dist/lib/routines.d.ts +26 -0
  127. package/dist/lib/routines.js +19 -2
  128. package/dist/lib/runner.js +104 -7
  129. package/dist/lib/secrets/agent.d.ts +66 -1
  130. package/dist/lib/secrets/agent.js +154 -90
  131. package/dist/lib/secrets/bundles.d.ts +34 -0
  132. package/dist/lib/secrets/bundles.js +179 -19
  133. package/dist/lib/secrets/index.d.ts +15 -1
  134. package/dist/lib/secrets/index.js +21 -3
  135. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  136. package/dist/lib/secrets/rc-hygiene.js +154 -0
  137. package/dist/lib/secrets/remote.d.ts +7 -4
  138. package/dist/lib/secrets/remote.js +7 -4
  139. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  140. package/dist/lib/secrets/sync-commands.js +21 -0
  141. package/dist/lib/self-update.d.ts +23 -2
  142. package/dist/lib/self-update.js +86 -5
  143. package/dist/lib/session/active.d.ts +114 -7
  144. package/dist/lib/session/active.js +323 -52
  145. package/dist/lib/session/cloud.js +2 -1
  146. package/dist/lib/session/db.d.ts +48 -0
  147. package/dist/lib/session/db.js +238 -14
  148. package/dist/lib/session/detached.d.ts +30 -0
  149. package/dist/lib/session/detached.js +92 -0
  150. package/dist/lib/session/discover.d.ts +465 -2
  151. package/dist/lib/session/discover.js +1648 -468
  152. package/dist/lib/session/fork.js +2 -1
  153. package/dist/lib/session/hook-sessions.d.ts +43 -0
  154. package/dist/lib/session/hook-sessions.js +135 -0
  155. package/dist/lib/session/linear.d.ts +6 -0
  156. package/dist/lib/session/linear.js +55 -0
  157. package/dist/lib/session/migrate-targets.d.ts +65 -0
  158. package/dist/lib/session/migrate-targets.js +94 -0
  159. package/dist/lib/session/migrations.d.ts +37 -0
  160. package/dist/lib/session/migrations.js +60 -0
  161. package/dist/lib/session/parse.d.ts +18 -0
  162. package/dist/lib/session/parse.js +130 -30
  163. package/dist/lib/session/pid-registry.d.ts +34 -2
  164. package/dist/lib/session/pid-registry.js +49 -2
  165. package/dist/lib/session/render.d.ts +7 -2
  166. package/dist/lib/session/render.js +22 -10
  167. package/dist/lib/session/short-id.d.ts +17 -0
  168. package/dist/lib/session/short-id.js +20 -0
  169. package/dist/lib/session/state.js +6 -2
  170. package/dist/lib/shims.d.ts +1 -1
  171. package/dist/lib/shims.js +20 -6
  172. package/dist/lib/sqlite.d.ts +3 -2
  173. package/dist/lib/sqlite.js +27 -4
  174. package/dist/lib/staleness/detectors/commands.js +1 -1
  175. package/dist/lib/staleness/detectors/workflows.js +13 -2
  176. package/dist/lib/staleness/writers/commands.js +7 -7
  177. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  178. package/dist/lib/startup/command-registry.d.ts +1 -0
  179. package/dist/lib/startup/command-registry.js +3 -0
  180. package/dist/lib/state.d.ts +1 -1
  181. package/dist/lib/state.js +1 -32
  182. package/dist/lib/teams/agents.d.ts +19 -10
  183. package/dist/lib/teams/agents.js +40 -39
  184. package/dist/lib/types.d.ts +37 -2
  185. package/dist/lib/usage.d.ts +33 -1
  186. package/dist/lib/usage.js +172 -12
  187. package/dist/lib/versions.d.ts +9 -2
  188. package/dist/lib/versions.js +37 -8
  189. package/dist/lib/watchdog/log.d.ts +43 -0
  190. package/dist/lib/watchdog/log.js +69 -0
  191. package/dist/lib/watchdog/routine.d.ts +44 -0
  192. package/dist/lib/watchdog/routine.js +69 -0
  193. package/dist/lib/watchdog/runner.d.ts +51 -7
  194. package/dist/lib/watchdog/runner.js +239 -64
  195. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  196. package/dist/lib/watchdog/watchdog.js +31 -16
  197. package/dist/lib/workflows.d.ts +16 -0
  198. package/dist/lib/workflows.js +110 -1
  199. 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
- // Grok sessions are rich (summary.json + events.jsonl + chat_history.jsonl + updates.jsonl)
1019
- // This is a minimal stub for now so grok appears in `agents sessions`.
1020
- // Full parser (with subagents, tool calls, etc.) can be expanded later.
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 content = fs.readFileSync(filePath, 'utf-8');
1023
- // If it's a summary.json, create a basic event
1024
- if (filePath.endsWith('summary.json')) {
1025
- const summary = JSON.parse(content);
1026
- return [{
1027
- timestamp: summary.created_at || new Date().toISOString(),
1028
- type: 'session_start',
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
- // For JSONL files (events, chat_history, updates), return basic parsed lines
1035
- if (filePath.endsWith('.jsonl')) {
1036
- const lines = content.trim().split('\n').filter(Boolean);
1037
- return lines.slice(0, 50).map((line, i) => {
1038
- try {
1039
- const obj = JSON.parse(line);
1040
- return {
1041
- timestamp: obj.timestamp || obj.ts || new Date().toISOString(),
1042
- type: obj.type || obj.method || 'grok_event',
1043
- content: typeof obj.content === 'string' ? obj.content : JSON.stringify(obj).slice(0, 200),
1044
- agent: 'grok',
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
- catch {
1048
- return { timestamp: new Date().toISOString(), type: 'raw', content: line.slice(0, 200), agent: 'grok' };
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
- catch { }
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
- /** Remove entries whose pid is no longer alive. Best-effort housekeeping. */
22
- export declare function prunePidSessionRegistry(isAlive: (pid: number) => boolean): void;
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
- /** Remove entries whose pid is no longer alive. Best-effort housekeeping. */
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) || isAlive(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 supports it.
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
- * Wrap label in an OSC 8 hyperlink when the terminal supports it.
33
- * Degrades to plain label otherwise.
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
- return `\x1b]8;;file://${absPath}\x1b\\${label}\x1b]8;;\x1b\\`;
42
- }
43
- return label;
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). Unknown mtime keeps the question.
382
- const questionFresh = ctx.mtimeMs == null || Date.now() - ctx.mtimeMs < PROSE_QUESTION_FRESH_MS;
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' } };
@@ -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 = 26;
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.
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 = 26;
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 === 'kimi'
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
@@ -867,7 +874,7 @@ function assertSafeVersion(version) {
867
874
  * KEEP IN SYNC with the `managedEnv` switch in `generateVersionedAliasScript`.
868
875
  * The colocated test `shims.isolation-capability.test.ts` enforces this.
869
876
  */
870
- export const CONFIG_ENV_ISOLATED_AGENTS = ['claude', 'codex', 'copilot', 'grok', 'kimi'];
877
+ export const CONFIG_ENV_ISOLATED_AGENTS = ['claude', 'codex', 'copilot', 'grok', 'kimi', 'opencode'];
871
878
  /**
872
879
  * Whether an agent supports a clean `--isolated` install — i.e. its config
873
880
  * location can be redirected by an env var so the isolated copy stays fully
@@ -912,13 +919,20 @@ export COPILOT_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${
912
919
  # aliases at the versioned home for isolation parity with the main shim.
913
920
  export GROK_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
914
921
  `
915
- : agent === 'kimi'
922
+ : agent === 'opencode'
916
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
+ ? `
917
931
  # Kimi Code CLI honors KIMI_CODE_HOME to relocate ~/.kimi-code (config.toml,
918
932
  # mcp.json, sessions, skills, hooks). Point direct aliases at the versioned home.
919
933
  export KIMI_CODE_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
920
934
  `
921
- : '';
935
+ : '';
922
936
  const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
923
937
  // Resolve the binary the same way the main shim does (see generateShimScript).
924
938
  // Grok and Droid do NOT ship into node_modules/.bin — Grok downloads a native
@@ -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 when the same source runs in tests (Bun) and
7
- * production (Node).
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,
@@ -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 when the same source runs in tests (Bun) and
7
- * production (Node).
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,
@@ -21,9 +22,31 @@ const sqliteMod = isBun
21
22
  // bun:sqlite exports `Database`; node:sqlite exports `DatabaseSync`.
22
23
  const NativeDatabase = sqliteMod.Database
23
24
  ?? sqliteMod.DatabaseSync;
25
+ /**
26
+ * bun:sqlite binds a named-parameter object ONLY when its keys carry the SQL
27
+ * sigil (`{ '@id': … }` for `VALUES (@id)`); bare keys (`{ id: … }`) match
28
+ * nothing and every parameter stays NULL, so the first NOT NULL column raises a
29
+ * constraint error and the write is lost. node:sqlite accepts the bare keys.
30
+ * `strict: true` makes bun accept them too, so the bare-key call shape this
31
+ * codebase uses works on both runtimes. node:sqlite has no such option and
32
+ * rejects a second argument that isn't an object, so the argument list is built
33
+ * per runtime.
34
+ *
35
+ * The two runtimes are still not interchangeable at the edges, and only bun's
36
+ * half is exercised by the shipped binary rather than by vitest — so keep binds
37
+ * inside the intersection:
38
+ * - omit a named key: bun throws `Missing parameter "x"`, node binds NULL.
39
+ * - pass an extra named key: bun accepts it, node throws `Unknown named
40
+ * parameter`.
41
+ * - use sigil keys (`{'@id': …}`): node accepts them, strict bun rejects them.
42
+ * - a single non-plain object positional arg (e.g. `run(new Date())`) reaches
43
+ * the named path via bindArgs below and throws under strict bun.
44
+ */
45
+ const NATIVE_ARGS = isBun ? [{ strict: true }] : [];
24
46
  function bindArgs(params) {
25
47
  // Both bindings accept positional `(a, b, c)` and named `({ a, b, c })`
26
- // forms. Pass an object through unchanged so callers using named binds work.
48
+ // forms bun only under `strict: true` (see NATIVE_ARGS). Pass an object
49
+ // through unchanged so callers using named binds work.
27
50
  if (params.length === 1 &&
28
51
  params[0] !== null &&
29
52
  typeof params[0] === 'object' &&
@@ -51,7 +74,7 @@ class StatementImpl {
51
74
  class Database {
52
75
  inner;
53
76
  constructor(filename) {
54
- this.inner = new NativeDatabase(filename);
77
+ this.inner = new NativeDatabase(filename, ...NATIVE_ARGS);
55
78
  }
56
79
  prepare(sql) {
57
80
  return new StatementImpl(this.inner.prepare(sql));
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Commands detector — mirrors versions.ts:343-357. Inspects the version home,
3
3
  * returns command names. Honors the commands-as-skills marker for skills-only
4
- * agents (grok, kimi, Codex >= 0.117.0, …); falls back to scanning
4
+ * agents (kimi, Codex >= 0.117.0, …); falls back to scanning
5
5
  * `{agentDir}/<commandsSubdir>/` for the native path.
6
6
  */
7
7
  import * as fs from 'fs';
@@ -1,12 +1,14 @@
1
1
  /**
2
- * Workflows detector — scans `{versionHome}/workflows/` for subdirectories
3
- * containing WORKFLOW.md. Mirrors versions.ts:551-558.
2
+ * Workflows detector — scans the agent-specific on-disk layout for synced
3
+ * workflow names (Claude WORKFLOW.md trees, Kimi flow skills, Antigravity
4
+ * global_workflows, Goose recipes, OpenClaw Lobster, Grok Rhai).
4
5
  */
5
6
  import * as fs from 'fs';
6
7
  import * as os from 'os';
7
8
  import * as path from 'path';
8
9
  import * as yaml from 'yaml';
9
10
  import { capableAgents } from '../../capabilities.js';
11
+ import { grokWorkflowMarker } from '../../workflows.js';
10
12
  import { lazyAgentMap } from '../writers/lazy-map.js';
11
13
  function buildWorkflowsDetector(agent) {
12
14
  return {
@@ -93,6 +95,15 @@ function buildWorkflowsDetector(agent) {
93
95
  })
94
96
  .map(d => d.name.slice(0, -'.lobster'.length));
95
97
  }
98
+ if (agent === 'grok') {
99
+ const workflowsDir = path.join(versionHome, '.grok', 'workflows');
100
+ if (!fs.existsSync(workflowsDir))
101
+ return [];
102
+ return fs.readdirSync(workflowsDir, { withFileTypes: true })
103
+ .filter(d => d.isFile() && d.name.endsWith('.rhai') && !d.name.startsWith('.'))
104
+ .filter(d => grokWorkflowMarker(path.join(workflowsDir, d.name)) === d.name.slice(0, -'.rhai'.length))
105
+ .map(d => d.name.slice(0, -'.rhai'.length));
106
+ }
96
107
  const workflowsDir = path.join(versionHome, 'workflows');
97
108
  if (!fs.existsSync(workflowsDir))
98
109
  return [];
@@ -4,10 +4,10 @@
4
4
  * Two physical formats, picked per-(agent, version) at write time:
5
5
  *
6
6
  * - command-as-skill — fires when `shouldInstallCommandAsSkill(agent, version)`
7
- * is true. Used for grok (no native commands, but skills/) and Codex
8
- * >= 0.117.0 (commands capability ends, skills capability remains).
9
- * Writes `{agentDir}/skills/<name>/SKILL.md` with the `agents_command`
10
- * marker; the agent picks it up as a slash-command equivalent.
7
+ * is true. Used for Codex >= 0.117.0 (commands capability ends, skills
8
+ * capability remains) and agents with skills but no native command-file dir
9
+ * such as kimi. Writes `{agentDir}/skills/<name>/SKILL.md` with the
10
+ * `agents_command` marker; the agent picks it up as a slash-command equivalent.
11
11
  *
12
12
  * - native command file — `{agentDir}/<commandsSubdir>/<name>.md` (or .toml
13
13
  * when the agent's format is toml). Standard path for Claude, Codex
@@ -83,8 +83,8 @@ function buildCommandsWriter(agent) {
83
83
  // Built lazily on first access — see lazy-map.ts for the cycle rationale.
84
84
  //
85
85
  // Registration covers two cases:
86
- // - native commands (claude, codex < 0.117.0, gemini, etc.) — `commands` cap
87
- // - commands-as-skills (grok, codex >= 0.117.0)
86
+ // - native commands (claude, codex < 0.117.0, gemini, grok, etc.) — `commands` cap
87
+ // - commands-as-skills (kimi, codex >= 0.117.0)
88
88
  //
89
89
  // Agents that have skills but use a NATIVE non-file slash-command system
90
90
  // (openclaw → Gateway-based commands) are NOT registered. They declare
@@ -97,7 +97,7 @@ export const commandsWriters = lazyAgentMap(() => {
97
97
  if (cfg.capabilities.commands === false && (!cfg.skillsDir || cfg.skillsDir === ''))
98
98
  continue;
99
99
  // Skills-capable agent with no native command-file dir: convert commands to
100
- // skills by default (grok, kimi, …). Opt out only agents with their own
100
+ // skills by default (kimi, …). Opt out only agents with their own
101
101
  // slash-command runtime (openclaw).
102
102
  if (cfg.capabilities.commands === false && (!cfg.commandsSubdir || cfg.commandsSubdir === '')) {
103
103
  if (cfg.nativeCommandRuntime)
@@ -1,6 +1,8 @@
1
1
  /**
2
- * Workflows writer — copies each workflow directory into
3
- * `<versionHome>/workflows/<name>/` via `syncWorkflowToVersion`.
2
+ * Workflows writer — copies each selected workflow into the agent-specific
3
+ * on-disk layout via `syncWorkflowToVersion` (Claude: WORKFLOW.md tree under
4
+ * `{versionHome}/workflows/`; Grok: native `.rhai` under
5
+ * `{versionHome}/.grok/workflows/`).
4
6
  */
5
7
  import type { AgentId } from '../../types.js';
6
8
  import type { ResourceWriter } from './types.js';
@@ -57,6 +57,7 @@ export declare const loadApply: ModuleLoader;
57
57
  export declare const loadCheck: ModuleLoader;
58
58
  export declare const loadStatus: ModuleLoader;
59
59
  export declare const loadProfiles: ModuleLoader;
60
+ export declare const loadHarness: ModuleLoader;
60
61
  export declare const loadSecrets: ModuleLoader;
61
62
  export declare const loadLogin: ModuleLoader;
62
63
  export declare const loadWallet: ModuleLoader;