@phnx-labs/agents-cli 1.20.74 → 1.20.77

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 (231) hide show
  1. package/CHANGELOG.md +1055 -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 +83 -13
  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 +286 -15
  26. package/dist/commands/secrets.js +102 -37
  27. package/dist/commands/sessions-browser.js +12 -2
  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.d.ts +14 -1
  32. package/dist/commands/sessions-picker.js +184 -16
  33. package/dist/commands/sessions.d.ts +56 -14
  34. package/dist/commands/sessions.js +322 -62
  35. package/dist/commands/setup-computer.js +2 -2
  36. package/dist/commands/ssh.d.ts +13 -0
  37. package/dist/commands/ssh.js +110 -37
  38. package/dist/commands/status.js +10 -2
  39. package/dist/commands/versions.js +15 -6
  40. package/dist/commands/view.d.ts +5 -0
  41. package/dist/commands/view.js +24 -4
  42. package/dist/commands/watchdog.d.ts +7 -2
  43. package/dist/commands/watchdog.js +73 -57
  44. package/dist/index.js +59 -7
  45. package/dist/lib/activity.d.ts +17 -6
  46. package/dist/lib/activity.js +466 -40
  47. package/dist/lib/actor.d.ts +50 -0
  48. package/dist/lib/actor.js +166 -0
  49. package/dist/lib/agent-spec/provider.js +2 -1
  50. package/dist/lib/agent-spec/resolve.js +19 -5
  51. package/dist/lib/agent-spec/types.d.ts +9 -1
  52. package/dist/lib/agents.d.ts +21 -0
  53. package/dist/lib/agents.js +70 -6
  54. package/dist/lib/cloud/codex.d.ts +2 -0
  55. package/dist/lib/cloud/codex.js +14 -3
  56. package/dist/lib/cloud/session-index.d.ts +32 -0
  57. package/dist/lib/cloud/session-index.js +58 -0
  58. package/dist/lib/cloud/store.d.ts +7 -0
  59. package/dist/lib/cloud/store.js +25 -0
  60. package/dist/lib/config-transfer.js +4 -0
  61. package/dist/lib/daemon.d.ts +10 -1
  62. package/dist/lib/daemon.js +114 -11
  63. package/dist/lib/devices/connect.d.ts +15 -1
  64. package/dist/lib/devices/connect.js +15 -1
  65. package/dist/lib/devices/fleet.d.ts +37 -1
  66. package/dist/lib/devices/fleet.js +36 -2
  67. package/dist/lib/devices/health-report.d.ts +38 -0
  68. package/dist/lib/devices/health-report.js +214 -0
  69. package/dist/lib/devices/health.js +4 -1
  70. package/dist/lib/devices/reachability.d.ts +31 -0
  71. package/dist/lib/devices/reachability.js +40 -0
  72. package/dist/lib/devices/registry.d.ts +33 -0
  73. package/dist/lib/devices/registry.js +37 -0
  74. package/dist/lib/devices/resolve-target.d.ts +15 -27
  75. package/dist/lib/devices/resolve-target.js +63 -102
  76. package/dist/lib/devices/sync.d.ts +18 -0
  77. package/dist/lib/devices/sync.js +23 -1
  78. package/dist/lib/devices/tailscale.d.ts +3 -0
  79. package/dist/lib/devices/tailscale.js +1 -0
  80. package/dist/lib/events.d.ts +1 -1
  81. package/dist/lib/exec.d.ts +82 -0
  82. package/dist/lib/exec.js +218 -6
  83. package/dist/lib/feed-post.d.ts +63 -0
  84. package/dist/lib/feed-post.js +204 -0
  85. package/dist/lib/feed.d.ts +7 -1
  86. package/dist/lib/feed.js +96 -6
  87. package/dist/lib/fleet/apply.d.ts +32 -2
  88. package/dist/lib/fleet/apply.js +97 -10
  89. package/dist/lib/fleet/types.d.ts +11 -0
  90. package/dist/lib/fs-walk.d.ts +13 -0
  91. package/dist/lib/fs-walk.js +16 -7
  92. package/dist/lib/heal.d.ts +7 -4
  93. package/dist/lib/heal.js +10 -22
  94. package/dist/lib/hooks.d.ts +1 -0
  95. package/dist/lib/hooks.js +156 -0
  96. package/dist/lib/hosts/dispatch.d.ts +16 -0
  97. package/dist/lib/hosts/dispatch.js +26 -4
  98. package/dist/lib/hosts/passthrough.js +9 -2
  99. package/dist/lib/hosts/reconnect.d.ts +78 -0
  100. package/dist/lib/hosts/reconnect.js +127 -0
  101. package/dist/lib/hosts/registry.d.ts +75 -14
  102. package/dist/lib/hosts/registry.js +205 -30
  103. package/dist/lib/hosts/remote-cmd.d.ts +11 -0
  104. package/dist/lib/hosts/remote-cmd.js +32 -8
  105. package/dist/lib/hosts/remote-session-id.d.ts +45 -0
  106. package/dist/lib/hosts/remote-session-id.js +84 -0
  107. package/dist/lib/hosts/run-target.d.ts +17 -5
  108. package/dist/lib/hosts/run-target.js +30 -10
  109. package/dist/lib/hosts/session-index.d.ts +18 -1
  110. package/dist/lib/hosts/session-index.js +40 -5
  111. package/dist/lib/hosts/session-marker.d.ts +33 -0
  112. package/dist/lib/hosts/session-marker.js +51 -0
  113. package/dist/lib/hosts/tasks.d.ts +8 -4
  114. package/dist/lib/import.d.ts +2 -0
  115. package/dist/lib/import.js +35 -2
  116. package/dist/lib/menubar/install-menubar.d.ts +29 -0
  117. package/dist/lib/menubar/install-menubar.js +59 -1
  118. package/dist/lib/menubar/notify-desktop.d.ts +44 -0
  119. package/dist/lib/menubar/notify-desktop.js +78 -0
  120. package/dist/lib/migrate.d.ts +16 -0
  121. package/dist/lib/migrate.js +36 -0
  122. package/dist/lib/models.d.ts +27 -0
  123. package/dist/lib/models.js +54 -1
  124. package/dist/lib/overdue.d.ts +6 -2
  125. package/dist/lib/overdue.js +10 -36
  126. package/dist/lib/picker.js +4 -1
  127. package/dist/lib/platform/process.d.ts +17 -0
  128. package/dist/lib/platform/process.js +70 -0
  129. package/dist/lib/profiles-presets.js +9 -7
  130. package/dist/lib/profiles.d.ts +31 -0
  131. package/dist/lib/profiles.js +70 -0
  132. package/dist/lib/pty-server.d.ts +2 -10
  133. package/dist/lib/pty-server.js +4 -38
  134. package/dist/lib/registry.d.ts +1 -1
  135. package/dist/lib/registry.js +48 -8
  136. package/dist/lib/rotate.d.ts +18 -0
  137. package/dist/lib/rotate.js +28 -0
  138. package/dist/lib/routine-notify.d.ts +76 -0
  139. package/dist/lib/routine-notify.js +190 -0
  140. package/dist/lib/routines-placement.d.ts +38 -0
  141. package/dist/lib/routines-placement.js +79 -0
  142. package/dist/lib/routines-project.d.ts +97 -0
  143. package/dist/lib/routines-project.js +349 -0
  144. package/dist/lib/routines.d.ts +94 -0
  145. package/dist/lib/routines.js +93 -9
  146. package/dist/lib/runner.d.ts +12 -2
  147. package/dist/lib/runner.js +242 -20
  148. package/dist/lib/sandbox.js +10 -0
  149. package/dist/lib/secrets/account-token.d.ts +20 -0
  150. package/dist/lib/secrets/account-token.js +64 -0
  151. package/dist/lib/secrets/agent.d.ts +74 -4
  152. package/dist/lib/secrets/agent.js +181 -107
  153. package/dist/lib/secrets/bundles.d.ts +54 -6
  154. package/dist/lib/secrets/bundles.js +230 -34
  155. package/dist/lib/secrets/index.d.ts +26 -3
  156. package/dist/lib/secrets/index.js +42 -12
  157. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  158. package/dist/lib/secrets/rc-hygiene.js +154 -0
  159. package/dist/lib/secrets/remote.d.ts +7 -4
  160. package/dist/lib/secrets/remote.js +7 -4
  161. package/dist/lib/secrets/session-store.d.ts +6 -2
  162. package/dist/lib/secrets/session-store.js +65 -15
  163. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  164. package/dist/lib/secrets/sync-commands.js +21 -0
  165. package/dist/lib/secrets/unlock-hints.d.ts +27 -0
  166. package/dist/lib/secrets/unlock-hints.js +36 -0
  167. package/dist/lib/self-update.d.ts +23 -2
  168. package/dist/lib/self-update.js +86 -5
  169. package/dist/lib/session/active.d.ts +124 -7
  170. package/dist/lib/session/active.js +390 -56
  171. package/dist/lib/session/cloud.js +2 -1
  172. package/dist/lib/session/db.d.ts +54 -0
  173. package/dist/lib/session/db.js +320 -19
  174. package/dist/lib/session/detached.d.ts +30 -0
  175. package/dist/lib/session/detached.js +92 -0
  176. package/dist/lib/session/discover.d.ts +478 -3
  177. package/dist/lib/session/discover.js +1740 -472
  178. package/dist/lib/session/fork.js +2 -1
  179. package/dist/lib/session/hook-sessions.d.ts +43 -0
  180. package/dist/lib/session/hook-sessions.js +135 -0
  181. package/dist/lib/session/linear.d.ts +6 -0
  182. package/dist/lib/session/linear.js +55 -0
  183. package/dist/lib/session/migrate-targets.d.ts +65 -0
  184. package/dist/lib/session/migrate-targets.js +94 -0
  185. package/dist/lib/session/migrations.d.ts +37 -0
  186. package/dist/lib/session/migrations.js +60 -0
  187. package/dist/lib/session/parse.d.ts +18 -0
  188. package/dist/lib/session/parse.js +141 -32
  189. package/dist/lib/session/pid-registry.d.ts +34 -2
  190. package/dist/lib/session/pid-registry.js +49 -2
  191. package/dist/lib/session/prompt.d.ts +7 -0
  192. package/dist/lib/session/prompt.js +37 -0
  193. package/dist/lib/session/provenance.d.ts +7 -0
  194. package/dist/lib/session/render.d.ts +7 -2
  195. package/dist/lib/session/render.js +40 -26
  196. package/dist/lib/session/short-id.d.ts +17 -0
  197. package/dist/lib/session/short-id.js +20 -0
  198. package/dist/lib/session/state.d.ts +4 -0
  199. package/dist/lib/session/state.js +99 -13
  200. package/dist/lib/session/types.d.ts +25 -1
  201. package/dist/lib/session/types.js +8 -0
  202. package/dist/lib/shims.d.ts +1 -1
  203. package/dist/lib/shims.js +20 -6
  204. package/dist/lib/sqlite.d.ts +3 -2
  205. package/dist/lib/sqlite.js +27 -4
  206. package/dist/lib/staleness/detectors/commands.js +1 -1
  207. package/dist/lib/staleness/detectors/workflows.js +13 -2
  208. package/dist/lib/staleness/writers/commands.js +7 -7
  209. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  210. package/dist/lib/startup/command-registry.d.ts +1 -0
  211. package/dist/lib/startup/command-registry.js +3 -0
  212. package/dist/lib/state.d.ts +8 -4
  213. package/dist/lib/state.js +21 -37
  214. package/dist/lib/teams/agents.d.ts +19 -10
  215. package/dist/lib/teams/agents.js +40 -39
  216. package/dist/lib/types.d.ts +47 -2
  217. package/dist/lib/usage.d.ts +33 -1
  218. package/dist/lib/usage.js +172 -12
  219. package/dist/lib/versions.d.ts +9 -2
  220. package/dist/lib/versions.js +37 -8
  221. package/dist/lib/watchdog/log.d.ts +43 -0
  222. package/dist/lib/watchdog/log.js +69 -0
  223. package/dist/lib/watchdog/routine.d.ts +44 -0
  224. package/dist/lib/watchdog/routine.js +69 -0
  225. package/dist/lib/watchdog/runner.d.ts +51 -7
  226. package/dist/lib/watchdog/runner.js +239 -64
  227. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  228. package/dist/lib/watchdog/watchdog.js +31 -16
  229. package/dist/lib/workflows.d.ts +16 -0
  230. package/dist/lib/workflows.js +110 -1
  231. package/package.json +1 -1
@@ -9,6 +9,7 @@ import * as fs from 'fs';
9
9
  import { truncate } from '../format.js';
10
10
  import * as path from 'path';
11
11
  import Database from '../sqlite.js';
12
+ import { isSyntheticUserMessage } from './prompt.js';
12
13
  /**
13
14
  * Largest session file we will load into memory. Above this we throw a clean
14
15
  * error instead of OOMing or hitting V8's ERR_STRING_TOO_LONG. Aligns with
@@ -156,9 +157,17 @@ export function parseSession(filePath, agent) {
156
157
  }
157
158
  // Chokepoint: every string field that originated in an untrusted session
158
159
  // file gets stripped of terminal escapes here, so renderers downstream can
159
- // safely splat values into chalk/console output.
160
- for (const e of events)
160
+ // safely splat values into chalk/console output. Same pass flags
161
+ // harness-injected `role=user` scaffolding (Claude `<bash-input>`/`<bash-stdout>`
162
+ // from `!`-prefix runs, `<system-reminder>`, etc.) as `_synthetic` so turn
163
+ // slicing and `--include user` count only genuine user intent — one place,
164
+ // every harness, instead of per-consumer regex.
165
+ for (const e of events) {
161
166
  sanitizeEvent(e);
167
+ if (e.type === 'message' && e.role === 'user' && isSyntheticUserMessage(e.content)) {
168
+ e._synthetic = true;
169
+ }
170
+ }
162
171
  return events;
163
172
  }
164
173
  /** Infer the agent type from a session file path using known directory conventions. */
@@ -1014,44 +1023,144 @@ export function parseAntigravity(dbPath) {
1014
1023
  * Messages have role (user/assistant) and metadata.
1015
1024
  * Parts contain the actual content: text, tool, reasoning, patch, step-start/finish.
1016
1025
  */
1026
+ /**
1027
+ * Parse a Grok (xAI CLI) session into normalized events.
1028
+ *
1029
+ * A Grok session dir holds several files; the conversation transcript is
1030
+ * `chat_history.jsonl`, one JSON object per line with a `type`:
1031
+ * - `system` — the system prompt (skipped; not conversational)
1032
+ * - `user` — `content` is an array of `{ type: 'text', text }` blocks
1033
+ * - `assistant` — `content` is a string, plus a `tool_calls[]` array of
1034
+ * `{ id, name, arguments }` (arguments is a JSON string)
1035
+ * - `reasoning` — chain-of-thought; text (when present) lives in `summary`
1036
+ * - `tool_result` — `{ tool_call_id, content }`, correlated back to the call
1037
+ *
1038
+ * The scanner records `summary.json` as the session's filePath (see
1039
+ * `readGrokMeta` in discover.ts), so resolve `chat_history.jsonl` from the same
1040
+ * dir; also accept being handed the transcript file directly. Per-line
1041
+ * timestamps aren't stored, so every event carries the session's `created_at`
1042
+ * (from summary.json), falling back to the transcript's mtime.
1043
+ */
1017
1044
  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.
1045
+ const sessionDir = path.dirname(filePath);
1046
+ const historyPath = filePath.endsWith('chat_history.jsonl')
1047
+ ? filePath
1048
+ : path.join(sessionDir, 'chat_history.jsonl');
1049
+ if (!fs.existsSync(historyPath))
1050
+ return [];
1051
+ let timestamp;
1021
1052
  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
- }];
1053
+ const summaryPath = filePath.endsWith('summary.json')
1054
+ ? filePath
1055
+ : path.join(sessionDir, 'summary.json');
1056
+ if (fs.existsSync(summaryPath)) {
1057
+ const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf-8'));
1058
+ if (typeof summary?.created_at === 'string')
1059
+ timestamp = summary.created_at;
1033
1060
  }
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
- };
1061
+ }
1062
+ catch { /* fall back to mtime below */ }
1063
+ if (!timestamp) {
1064
+ let mtime = null;
1065
+ try {
1066
+ mtime = fs.statSync(historyPath).mtime;
1067
+ }
1068
+ catch {
1069
+ mtime = null;
1070
+ }
1071
+ timestamp = (mtime ?? new Date()).toISOString();
1072
+ }
1073
+ const content = safeReadSessionFile(historyPath);
1074
+ const lines = content.split('\n').filter(l => l.trim());
1075
+ const events = [];
1076
+ const toolCallMap = new Map();
1077
+ // Grok text is either a plain string (assistant) or an array of typed blocks
1078
+ // (user: `{ type: 'text', text }`; reasoning summary: `{ text | summary_text }`).
1079
+ const extractText = (raw) => {
1080
+ if (typeof raw === 'string')
1081
+ return raw.trim();
1082
+ if (Array.isArray(raw)) {
1083
+ return raw
1084
+ .map((part) => typeof part?.text === 'string'
1085
+ ? part.text
1086
+ : typeof part?.summary_text === 'string'
1087
+ ? part.summary_text
1088
+ : '')
1089
+ .join('')
1090
+ .trim();
1091
+ }
1092
+ return '';
1093
+ };
1094
+ for (const line of lines) {
1095
+ let msg;
1096
+ try {
1097
+ msg = JSON.parse(line);
1098
+ }
1099
+ catch {
1100
+ continue;
1101
+ }
1102
+ const type = msg?.type;
1103
+ if (type === 'user') {
1104
+ const text = extractText(msg.content);
1105
+ if (text)
1106
+ events.push({ type: 'message', agent: 'grok', timestamp, role: 'user', content: text });
1107
+ }
1108
+ else if (type === 'assistant') {
1109
+ const text = extractText(msg.content);
1110
+ if (text)
1111
+ events.push({ type: 'message', agent: 'grok', timestamp, role: 'assistant', content: text });
1112
+ const calls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
1113
+ for (const call of calls) {
1114
+ const toolName = typeof call?.name === 'string' ? call.name : 'unknown';
1115
+ let args = {};
1116
+ if (call?.arguments && typeof call.arguments === 'object') {
1117
+ args = call.arguments;
1046
1118
  }
1047
- catch {
1048
- return { timestamp: new Date().toISOString(), type: 'raw', content: line.slice(0, 200), agent: 'grok' };
1119
+ else if (typeof call?.arguments === 'string') {
1120
+ try {
1121
+ args = JSON.parse(call.arguments);
1122
+ }
1123
+ catch {
1124
+ args = { _raw: call.arguments };
1125
+ }
1049
1126
  }
1127
+ if (typeof call?.id === 'string')
1128
+ toolCallMap.set(call.id, toolName);
1129
+ events.push({
1130
+ type: 'tool_use',
1131
+ agent: 'grok',
1132
+ timestamp,
1133
+ tool: toolName,
1134
+ args,
1135
+ path: args.path || args.file_path || undefined,
1136
+ command: typeof args.command === 'string' ? args.command : undefined,
1137
+ });
1138
+ }
1139
+ }
1140
+ else if (type === 'reasoning') {
1141
+ const text = extractText(msg.summary);
1142
+ if (text)
1143
+ events.push({ type: 'thinking', agent: 'grok', timestamp, content: text });
1144
+ }
1145
+ else if (type === 'tool_result') {
1146
+ const callId = typeof msg.tool_call_id === 'string' ? msg.tool_call_id : undefined;
1147
+ const toolName = (callId && toolCallMap.get(callId)) || 'unknown';
1148
+ const output = typeof msg.content === 'string' ? msg.content : extractText(msg.content);
1149
+ const isError = typeof output === 'string' && output.startsWith('Error:');
1150
+ events.push({
1151
+ type: isError ? 'error' : 'tool_result',
1152
+ agent: 'grok',
1153
+ timestamp,
1154
+ tool: toolName,
1155
+ success: !isError,
1156
+ output: output.length > 500 ? output.slice(0, 497) + '...' : output,
1050
1157
  });
1158
+ if (callId)
1159
+ toolCallMap.delete(callId);
1051
1160
  }
1161
+ // `system` lines are the system prompt — intentionally skipped.
1052
1162
  }
1053
- catch { }
1054
- return [];
1163
+ return events;
1055
1164
  }
1056
1165
  export function parseOpenCode(filePath) {
1057
1166
  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));
@@ -5,6 +5,13 @@
5
5
  * wrapper tags, team-spawn boilerplate) so that only the real user intent
6
6
  * remains. Used by the session picker to show a human-readable topic line.
7
7
  */
8
+ /**
9
+ * True when a `role=user` message is harness-injected scaffolding, not a genuine
10
+ * user turn. Text-based so it is cross-harness by construction (the markers are
11
+ * the same wrappers across Claude/Codex/etc.); harnesses that already drop their
12
+ * own scaffolding at parse time simply never reach here.
13
+ */
14
+ export declare function isSyntheticUserMessage(raw: string | undefined): boolean;
8
15
  export declare const HEADLESS_PLAN_MODE_PREFIX = "You are running in HEADLESS PLAN MODE.";
9
16
  /** Strip framework noise from a raw session prompt, returning only meaningful user text. */
10
17
  export declare function cleanSessionPrompt(raw: string): string;
@@ -26,6 +26,43 @@ const NOISE_LINE_PATTERNS = [
26
26
  ];
27
27
  /** XML tag prefix used by local-command messages. */
28
28
  const LOCAL_COMMAND_PREFIX = '<local-command-caveat>';
29
+ /**
30
+ * Patterns that mark a whole `role=user` message as harness-injected scaffolding
31
+ * rather than a genuine user turn: the `!`-prefix shell echo and its output,
32
+ * slash-command wrappers, injected reminders / task-notifications, the local
33
+ * caveat, the interrupt marker, skill bodies, and hook feedback. This is the
34
+ * single cross-harness list — `isSyntheticUserMessage` is consumed by the parser
35
+ * (`parseSession`) to flag such events `_synthetic`, so `--include user` and the
36
+ * `--first`/`--last` turn split see only real intent, and every consumer (the
37
+ * session picker, `session-recall`, external hooks) gets clean user turns without
38
+ * re-implementing the stripping. Extend this list, not each caller.
39
+ */
40
+ const SYNTHETIC_USER_MESSAGE_PATTERNS = [
41
+ /^\s*<\/?(?:bash-input|bash-stdout|bash-stderr)>/i,
42
+ /^\s*<\/?(?:command-name|command-message|command-args|command-contents)>/i,
43
+ /^\s*<\/?(?:local-command-stdout|local-command-stderr|local-command-caveat)>/i,
44
+ /^\s*<\/?system-reminder>/i,
45
+ /^\s*<\/?task-notification>/i,
46
+ /^\s*<\/?user-prompt-submit-hook>/i,
47
+ /^\s*<\/?persisted-output>/i,
48
+ /^\s*<permissions instructions>/i,
49
+ /^\s*<collaboration_mode>/i,
50
+ /^\s*Caveat: The messages below were generated by the user while running/i,
51
+ /^\s*\[Request interrupted/i,
52
+ /^\s*Base directory for this skill:/i,
53
+ /^\s*[A-Za-z][A-Za-z-]* hook feedback:/,
54
+ ];
55
+ /**
56
+ * True when a `role=user` message is harness-injected scaffolding, not a genuine
57
+ * user turn. Text-based so it is cross-harness by construction (the markers are
58
+ * the same wrappers across Claude/Codex/etc.); harnesses that already drop their
59
+ * own scaffolding at parse time simply never reach here.
60
+ */
61
+ export function isSyntheticUserMessage(raw) {
62
+ if (!raw)
63
+ return false;
64
+ return SYNTHETIC_USER_MESSAGE_PATTERNS.some(pattern => pattern.test(raw));
65
+ }
29
66
  // Prefix prepended to every Claude-in-plan-mode team spawn prompt.
30
67
  // Ends at a blank line before the real user task.
31
68
  export const HEADLESS_PLAN_MODE_PREFIX = 'You are running in HEADLESS PLAN MODE.';
@@ -37,6 +37,13 @@ export interface SessionProvenance {
37
37
  mux?: MuxLocation;
38
38
  /** Whether an existing rail can type back into this session (see module doc). */
39
39
  reply: ReplyRail;
40
+ /** The initiating device, resolved at query time from ssh.clientIp against the
41
+ * device registry (ssh transport only). Answers "who launched this and from
42
+ * where" without scraping ps/who/tailscale. */
43
+ origin?: {
44
+ device: string;
45
+ user?: string;
46
+ };
40
47
  }
41
48
  /** Env vars that carry provenance. Kept small so the macOS `ps` scan stays cheap. */
42
49
  export declare const PROVENANCE_ENV_KEYS: readonly ["SSH_CONNECTION", "SSH_TTY", "TMUX", "TMUX_PANE", "TERM_PROGRAM", "STY", "ITERM_SESSION_ID"];
@@ -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
  */
@@ -13,6 +13,7 @@ import { cleanSessionPrompt, extractSessionTopic } from './prompt.js';
13
13
  import { renderMarkdown } from '../markdown.js';
14
14
  import { redactSecrets } from '../redact.js';
15
15
  import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from './digest.js';
16
+ import { extractTodoProgressFromEvents } from './state.js';
16
17
  // ── Path helpers ──────────────────────────────────────────────────────────────
17
18
  /**
18
19
  * Return absPath relative to cwd; fall back to ~/… then absolute.
@@ -28,19 +29,31 @@ export function relativeToCwd(absPath, cwd) {
28
29
  }
29
30
  return absPath;
30
31
  }
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 &&
32
+ /** Best-effort feature-detect for OSC 8 hyperlink support in the current TTY. */
33
+ function supportsHyperlinks() {
34
+ return Boolean(process.stdout.isTTY &&
37
35
  (process.env.TERM_PROGRAM ||
38
36
  process.env.WT_SESSION ||
39
37
  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;
38
+ process.env.WEZTERM_PANE));
39
+ }
40
+ /** Wrap `label` in an OSC 8 hyperlink to `target`. Callers gate on {@link supportsHyperlinks}. */
41
+ function osc8(target, label) {
42
+ return `\x1b]8;;${target}\x1b\\${label}\x1b]8;;\x1b\\`;
43
+ }
44
+ /**
45
+ * Wrap a filesystem path label in an OSC 8 `file://` hyperlink when the terminal
46
+ * supports it. Degrades to the plain label otherwise.
47
+ */
48
+ export function linkPath(absPath, label) {
49
+ return supportsHyperlinks() ? osc8(`file://${absPath}`, label) : label;
50
+ }
51
+ /**
52
+ * Wrap a label in an OSC 8 hyperlink to an arbitrary URL (a PR on GitHub, a Linear
53
+ * issue, …) when the terminal supports it. Degrades to the plain label otherwise.
54
+ */
55
+ export function linkUrl(url, label) {
56
+ return supportsHyperlinks() ? osc8(url, label) : label;
44
57
  }
45
58
  // ── Command grouping ──────────────────────────────────────────────────────────
46
59
  /**
@@ -516,7 +529,7 @@ export function renderSummary(events, cwd) {
516
529
  // Commands with timestamps
517
530
  const cmdList = [];
518
531
  // Plan items
519
- const todoItems = [];
532
+ const todoItems = extractTodoProgressFromEvents(events)?.items.map(item => item.content) ?? [];
520
533
  let exitPlanContent = null;
521
534
  let planFilePath = null;
522
535
  // Subagent spawns
@@ -557,19 +570,6 @@ export function renderSummary(events, cwd) {
557
570
  recentActivity.push({ kind: 'cmd', label: cmd, ts });
558
571
  }
559
572
  }
560
- // Plan items: TodoWrite items + TaskCreate descriptions (project's task tracker)
561
- if (tool === 'TodoWrite' && Array.isArray(args.todos)) {
562
- for (const item of args.todos) {
563
- const text = item.content || item.text || String(item);
564
- if (text && !todoItems.includes(text))
565
- todoItems.push(text);
566
- }
567
- }
568
- if (tool === 'TaskCreate' && (args.description || args.prompt)) {
569
- const text = String(args.description || args.prompt || '').slice(0, 140);
570
- if (text && !todoItems.includes(text))
571
- todoItems.push(text);
572
- }
573
573
  if (tool === 'ExitPlanMode') {
574
574
  exitPlanContent = args.result || args.plan || args.content || null;
575
575
  }
@@ -784,6 +784,9 @@ export function parseRoleList(raw, flag) {
784
784
  return parts;
785
785
  }
786
786
  function roleOfEvent(e) {
787
+ // Synthetic scaffolding still maps to 'user' here so `--exclude user` drops it
788
+ // like any other user-role event (pre-flag behavior). `--include user`'s
789
+ // "genuine intent only" carve-out lives in applyRoleFilter, not here.
787
790
  if (e.type === 'message' && e.role === 'user')
788
791
  return 'user';
789
792
  if (e.type === 'message' && e.role === 'assistant')
@@ -805,7 +808,14 @@ function applyRoleFilter(events, opts) {
805
808
  const set = new Set(opts.include);
806
809
  return events.filter(e => {
807
810
  const role = roleOfEvent(e);
808
- return role !== null && set.has(role);
811
+ if (role === null)
812
+ return false;
813
+ // `--include user` means genuine user intent, so drop harness-injected
814
+ // `_synthetic` scaffolding (bash-input, system-reminder) even though it
815
+ // carries role=user. `--exclude user` still drops it via roleOfEvent.
816
+ if (role === 'user' && e._synthetic)
817
+ return false;
818
+ return set.has(role);
809
819
  });
810
820
  }
811
821
  if (opts.exclude && opts.exclude.length > 0) {
@@ -833,7 +843,11 @@ function applyTurnSlice(events, opts) {
833
843
  if (!Number.isFinite(n) || n <= 0) {
834
844
  throw new Error(`Turn count must be a positive integer, got ${n}`);
835
845
  }
836
- const isTurnStart = (e) => e.type === 'message' && e.role === 'user';
846
+ // A turn starts at a GENUINE user message harness-injected `_synthetic`
847
+ // scaffolding (`<bash-input>`/`<bash-stdout>`, etc.) does not start a turn,
848
+ // so `--first N` / `--last N` count real asks, not the jump command that
849
+ // opened the session and its shell output.
850
+ const isTurnStart = (e) => e.type === 'message' && e.role === 'user' && !e._synthetic;
837
851
  const turnStartIdx = [];
838
852
  for (let i = 0; i < events.length; i++)
839
853
  if (isTurnStart(events[i]))
@@ -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
+ }
@@ -126,6 +126,10 @@ export interface StateContext {
126
126
  * list, so a session with no plan carries no `todos` field.
127
127
  */
128
128
  export declare function extractTodoProgress(args?: Record<string, any>): TodoProgress | undefined;
129
+ /** Fold snapshot checklist tools and Claude TaskCreate/TaskUpdate event logs. */
130
+ export declare function extractTodoProgressFromEvents(events: SessionEvent[]): TodoProgress | undefined;
131
+ /** Derive a recency-ordered, de-duplicated list of directories touched by tools. */
132
+ export declare function extractRecentDirectoriesTouched(events: SessionEvent[], cwd?: string): string[] | undefined;
129
133
  /** Detect a worktree from the session cwd, per the `.agents/worktrees/<slug>/` convention. */
130
134
  export declare function detectWorktree(cwd?: string, branch?: string): DetectedWorktree | undefined;
131
135
  /** Detect a tracker ticket from free text (prompt/topic) then a branch name. */