@covibes/zeroshot 5.2.1 → 5.4.0

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 (98) hide show
  1. package/CHANGELOG.md +174 -189
  2. package/README.md +226 -195
  3. package/cli/commands/providers.js +149 -0
  4. package/cli/index.js +3145 -2366
  5. package/cli/lib/first-run.js +40 -3
  6. package/cli/message-formatters-normal.js +28 -6
  7. package/cluster-templates/base-templates/debug-workflow.json +24 -78
  8. package/cluster-templates/base-templates/full-workflow.json +99 -316
  9. package/cluster-templates/base-templates/single-worker.json +23 -15
  10. package/cluster-templates/base-templates/worker-validator.json +105 -36
  11. package/cluster-templates/conductor-bootstrap.json +9 -7
  12. package/lib/docker-config.js +14 -1
  13. package/lib/git-remote-utils.js +165 -0
  14. package/lib/id-detector.js +10 -7
  15. package/lib/provider-defaults.js +62 -0
  16. package/lib/provider-detection.js +59 -0
  17. package/lib/provider-names.js +57 -0
  18. package/lib/settings/claude-auth.js +78 -0
  19. package/lib/settings.js +298 -15
  20. package/lib/stream-json-parser.js +4 -238
  21. package/package.json +27 -6
  22. package/scripts/setup-merge-queue.sh +170 -0
  23. package/scripts/validate-templates.js +100 -0
  24. package/src/agent/agent-config.js +140 -63
  25. package/src/agent/agent-context-builder.js +336 -165
  26. package/src/agent/agent-hook-executor.js +337 -67
  27. package/src/agent/agent-lifecycle.js +386 -287
  28. package/src/agent/agent-stuck-detector.js +7 -7
  29. package/src/agent/agent-task-executor.js +944 -683
  30. package/src/agent/output-extraction.js +217 -0
  31. package/src/agent/output-reformatter.js +175 -0
  32. package/src/agent/schema-utils.js +146 -0
  33. package/src/agent-wrapper.js +112 -31
  34. package/src/agents/git-pusher-template.js +285 -0
  35. package/src/claude-task-runner.js +145 -44
  36. package/src/config-router.js +13 -13
  37. package/src/config-validator.js +1049 -563
  38. package/src/input-helpers.js +65 -0
  39. package/src/isolation-manager.js +499 -320
  40. package/src/issue-providers/README.md +305 -0
  41. package/src/issue-providers/azure-devops-provider.js +273 -0
  42. package/src/issue-providers/base-provider.js +232 -0
  43. package/src/issue-providers/github-provider.js +179 -0
  44. package/src/issue-providers/gitlab-provider.js +241 -0
  45. package/src/issue-providers/index.js +196 -0
  46. package/src/issue-providers/jira-provider.js +239 -0
  47. package/src/ledger.js +50 -11
  48. package/src/lib/safe-exec.js +88 -0
  49. package/src/orchestrator.js +1348 -757
  50. package/src/preflight.js +306 -149
  51. package/src/process-metrics.js +98 -56
  52. package/src/providers/anthropic/cli-builder.js +73 -0
  53. package/src/providers/anthropic/index.js +204 -0
  54. package/src/providers/anthropic/models.js +23 -0
  55. package/src/providers/anthropic/output-parser.js +177 -0
  56. package/src/providers/base-provider.js +251 -0
  57. package/src/providers/capabilities.js +60 -0
  58. package/src/providers/google/cli-builder.js +55 -0
  59. package/src/providers/google/index.js +116 -0
  60. package/src/providers/google/models.js +24 -0
  61. package/src/providers/google/output-parser.js +101 -0
  62. package/src/providers/index.js +91 -0
  63. package/src/providers/openai/cli-builder.js +133 -0
  64. package/src/providers/openai/index.js +136 -0
  65. package/src/providers/openai/models.js +21 -0
  66. package/src/providers/openai/output-parser.js +143 -0
  67. package/src/providers/opencode/cli-builder.js +42 -0
  68. package/src/providers/opencode/index.js +103 -0
  69. package/src/providers/opencode/models.js +55 -0
  70. package/src/providers/opencode/output-parser.js +122 -0
  71. package/src/schemas/sub-cluster.js +68 -39
  72. package/src/status-footer.js +94 -48
  73. package/src/sub-cluster-wrapper.js +92 -36
  74. package/src/task-runner.js +8 -6
  75. package/src/template-resolver.js +12 -9
  76. package/src/tui/data-poller.js +123 -99
  77. package/src/tui/formatters.js +4 -3
  78. package/src/tui/keybindings.js +259 -318
  79. package/src/tui/layout.js +20 -3
  80. package/src/tui/renderer.js +11 -21
  81. package/task-lib/attachable-watcher.js +150 -111
  82. package/task-lib/claude-recovery.js +156 -0
  83. package/task-lib/commands/episodes.js +105 -0
  84. package/task-lib/commands/list.js +3 -3
  85. package/task-lib/commands/logs.js +231 -189
  86. package/task-lib/commands/resume.js +3 -2
  87. package/task-lib/commands/run.js +12 -3
  88. package/task-lib/commands/schedules.js +111 -61
  89. package/task-lib/config.js +0 -2
  90. package/task-lib/runner.js +128 -50
  91. package/task-lib/scheduler.js +3 -3
  92. package/task-lib/store.js +464 -152
  93. package/task-lib/tui/formatters.js +94 -45
  94. package/task-lib/tui/renderer.js +13 -3
  95. package/task-lib/tui.js +73 -32
  96. package/task-lib/watcher.js +157 -100
  97. package/src/agents/git-pusher-agent.json +0 -20
  98. package/src/github.js +0 -103
@@ -37,11 +37,9 @@ class Renderer {
37
37
  * @param {Array} clusters - Array of cluster objects
38
38
  */
39
39
  renderClustersTable(clusters) {
40
- if (!clusters || !Array.isArray(clusters)) {
41
- clusters = [];
42
- }
40
+ const clusterList = !clusters || !Array.isArray(clusters) ? [] : clusters;
43
41
 
44
- const data = clusters.map((c) => {
42
+ const data = clusterList.map((c) => {
45
43
  if (!c) return ['', '', '', ''];
46
44
 
47
45
  const icon = stateIcon(c.state || 'unknown');
@@ -68,23 +66,19 @@ class Renderer {
68
66
  * @param {Map} resourceStats - Map of PID -> {cpu, memory}
69
67
  */
70
68
  renderSystemStats(clusters, resourceStats) {
71
- if (!clusters || !Array.isArray(clusters)) {
72
- clusters = [];
73
- }
74
- if (!resourceStats || !(resourceStats instanceof Map)) {
75
- resourceStats = new Map();
76
- }
69
+ const clusterList = !clusters || !Array.isArray(clusters) ? [] : clusters;
70
+ const statsMap = !resourceStats || !(resourceStats instanceof Map) ? new Map() : resourceStats;
77
71
 
78
72
  // Calculate aggregate stats
79
- const activeClusters = clusters.filter((c) => c && c.state === 'running').length;
80
- const totalAgents = clusters.reduce((sum, c) => sum + (c?.agentCount || 0), 0);
73
+ const activeClusters = clusterList.filter((c) => c && c.state === 'running').length;
74
+ const totalAgents = clusterList.reduce((sum, c) => sum + (c?.agentCount || 0), 0);
81
75
 
82
76
  // Calculate average CPU and memory from resource stats
83
77
  let totalCpu = 0;
84
78
  let totalMemory = 0;
85
79
  let statCount = 0;
86
80
 
87
- resourceStats.forEach((stat) => {
81
+ statsMap.forEach((stat) => {
88
82
  if (stat && typeof stat.cpu === 'number' && typeof stat.memory === 'number') {
89
83
  totalCpu += stat.cpu;
90
84
  totalMemory += stat.memory;
@@ -125,18 +119,14 @@ class Renderer {
125
119
  return;
126
120
  }
127
121
 
128
- if (!agents || !Array.isArray(agents)) {
129
- agents = [];
130
- }
131
- if (!resourceStats || !(resourceStats instanceof Map)) {
132
- resourceStats = new Map();
133
- }
122
+ const agentList = !agents || !Array.isArray(agents) ? [] : agents;
123
+ const statsMap = !resourceStats || !(resourceStats instanceof Map) ? new Map() : resourceStats;
134
124
 
135
- const data = agents.map((a) => {
125
+ const data = agentList.map((a) => {
136
126
  if (!a) return ['', '', '', '', '', ''];
137
127
 
138
128
  const pid = a.pid;
139
- const stats = resourceStats.get(pid) || { cpu: 0, memory: 0 };
129
+ const stats = statsMap.get(pid) || { cpu: 0, memory: 0 };
140
130
 
141
131
  const agentId = truncate(a.id || '', 12);
142
132
  const role = truncate(a.role || '', 12);
@@ -1,42 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Attachable Watcher - spawns Claude with PTY for attach/detach support
5
- *
4
+ * Attachable Watcher - spawns a CLI process with PTY for attach/detach support
6
5
  * Runs detached from parent, provides Unix socket for attach clients.
7
- * Uses node-pty for proper terminal emulation.
8
- *
9
- * Key differences from legacy watcher.js:
10
- * - Uses AttachServer (node-pty) instead of child_process.spawn
11
- * - Creates Unix socket at ~/.zeroshot/sockets/task-<id>.sock
12
- * - Supports multiple attached clients
13
- * - Still writes to log file for backward compatibility
14
- *
15
- * CRITICAL: Global error handlers installed FIRST to catch silent crashes
16
6
  */
17
7
 
18
8
  import { appendFileSync, existsSync, mkdirSync } from 'fs';
19
9
  import { join } from 'path';
20
10
  import { homedir } from 'os';
21
11
  import { updateTask } from './store.js';
12
+ import { detectStreamingModeError, recoverStructuredOutput } from './claude-recovery.js';
13
+ import { createRequire } from 'module';
22
14
 
23
15
  // ═══════════════════════════════════════════════════════════════════════════
24
16
  // 🔴 CRITICAL: Global error handlers - MUST be installed BEFORE any async ops
25
17
  // Without these, uncaught errors cause SILENT process death (no logs, no status)
26
18
  // ═══════════════════════════════════════════════════════════════════════════
27
19
 
28
- // Parse args early so we can log errors to the correct file
29
20
  const [, , taskIdArg, cwdArg, logFileArg, argsJsonArg, configJsonArg] = process.argv;
30
21
 
31
- /**
32
- * Emergency logger - works even if main log function isn't ready
33
- */
34
22
  function emergencyLog(msg) {
35
23
  if (logFileArg) {
36
24
  try {
37
25
  appendFileSync(logFileArg, msg);
38
26
  } catch {
39
- // Last resort - stderr
40
27
  process.stderr.write(msg);
41
28
  }
42
29
  } else {
@@ -44,9 +31,6 @@ function emergencyLog(msg) {
44
31
  }
45
32
  }
46
33
 
47
- /**
48
- * Mark task as failed and exit
49
- */
50
34
  function crashWithError(error, source) {
51
35
  const timestamp = Date.now();
52
36
  const errorMsg = error instanceof Error ? error.stack || error.message : String(error);
@@ -54,7 +38,6 @@ function crashWithError(error, source) {
54
38
  emergencyLog(`\n[${timestamp}][CRASH] ${source}: ${errorMsg}\n`);
55
39
  emergencyLog(`[${timestamp}][CRASH] Process terminating due to unhandled error\n`);
56
40
 
57
- // Try to update task status - may fail if error is in store.js itself
58
41
  if (taskIdArg) {
59
42
  try {
60
43
  updateTask(taskIdArg, {
@@ -67,11 +50,9 @@ function crashWithError(error, source) {
67
50
  }
68
51
  }
69
52
 
70
- // Exit with error code
71
53
  process.exit(1);
72
54
  }
73
55
 
74
- // Install handlers IMMEDIATELY
75
56
  process.on('uncaughtException', (error) => {
76
57
  crashWithError(error, 'uncaughtException');
77
58
  });
@@ -80,24 +61,19 @@ process.on('unhandledRejection', (reason) => {
80
61
  crashWithError(reason, 'unhandledRejection');
81
62
  });
82
63
 
83
- // Import attach infrastructure from src package (CommonJS)
84
- import { createRequire } from 'module';
85
64
  const require = createRequire(import.meta.url);
86
65
  const { AttachServer } = require('../src/attach');
87
- const { getClaudeCommand } = require('../lib/settings.js');
66
+ const { normalizeProviderName } = require('../lib/provider-names');
88
67
 
89
- // Use the args parsed earlier (during error handler setup)
90
68
  const taskId = taskIdArg;
91
69
  const cwd = cwdArg;
92
70
  const logFile = logFileArg;
93
71
  const args = JSON.parse(argsJsonArg);
94
72
  const config = configJsonArg ? JSON.parse(configJsonArg) : {};
95
73
 
96
- // Socket path for attach
97
74
  const SOCKET_DIR = join(homedir(), '.zeroshot', 'sockets');
98
75
  const socketPath = join(SOCKET_DIR, `${taskId}.sock`);
99
76
 
100
- // Ensure socket directory exists
101
77
  if (!existsSync(SOCKET_DIR)) {
102
78
  mkdirSync(SOCKET_DIR, { recursive: true });
103
79
  }
@@ -106,33 +82,133 @@ function log(msg) {
106
82
  appendFileSync(logFile, msg);
107
83
  }
108
84
 
109
- // Build environment - inherit user's auth method (API key or subscription)
110
- const env = { ...process.env };
111
-
112
- // Add model flag - priority: config.model > ANTHROPIC_MODEL env var
113
- const claudeArgs = [...args];
114
- const model = config.model || env.ANTHROPIC_MODEL;
115
- if (model && !claudeArgs.includes('--model')) {
116
- claudeArgs.unshift('--model', model);
117
- }
85
+ const providerName = normalizeProviderName(config.provider || 'claude');
86
+ const enableRecovery = providerName === 'claude';
118
87
 
119
- // Get configured Claude command (supports custom commands like 'ccr code')
120
- const { command: claudeCommand, args: claudeExtraArgs } = getClaudeCommand();
121
- const finalArgs = [...claudeExtraArgs, ...claudeArgs];
88
+ const env = { ...process.env, ...(config.env || {}) };
89
+ const command = config.command || 'claude';
90
+ const finalArgs = [...args];
122
91
 
123
- // For JSON schema output with silent mode, track final result
124
92
  const silentJsonMode =
125
- config.outputFormat === 'json' && config.jsonSchema && config.silentJsonOutput;
126
- let finalResultJson = null;
93
+ config.outputFormat === 'json' && config.jsonSchema && config.silentJsonOutput && enableRecovery;
127
94
 
128
- // Buffer for incomplete lines
95
+ let finalResultJson = null;
129
96
  let outputBuffer = '';
97
+ let streamingModeError = null;
98
+
99
+ function splitBufferLines(buffer, chunk) {
100
+ const nextBuffer = buffer + chunk;
101
+ const lines = nextBuffer.split('\n');
102
+ const remaining = lines.pop() || '';
103
+ return { lines, remaining };
104
+ }
105
+
106
+ function captureStreamingError(line, timestamp) {
107
+ if (!enableRecovery) {
108
+ return false;
109
+ }
110
+
111
+ const detectedError = detectStreamingModeError(line);
112
+ if (!detectedError) {
113
+ return false;
114
+ }
115
+
116
+ streamingModeError = { ...detectedError, timestamp };
117
+ return true;
118
+ }
119
+
120
+ function maybeCaptureStructuredOutput(line) {
121
+ try {
122
+ const json = JSON.parse(line);
123
+ if (json.structured_output) {
124
+ finalResultJson = line;
125
+ }
126
+ } catch {
127
+ // Not JSON, skip
128
+ }
129
+ }
130
+
131
+ function handleSilentJsonLines(lines, timestamp) {
132
+ for (const line of lines) {
133
+ if (!line.trim()) continue;
134
+ if (captureStreamingError(line, timestamp)) {
135
+ continue;
136
+ }
137
+ maybeCaptureStructuredOutput(line);
138
+ }
139
+ }
140
+
141
+ function handleStreamingLines(lines, timestamp) {
142
+ for (const line of lines) {
143
+ if (captureStreamingError(line, timestamp)) {
144
+ continue;
145
+ }
146
+ log(`[${timestamp}]${line}\n`);
147
+ }
148
+ }
149
+
150
+ function flushOutputBuffer(timestamp) {
151
+ if (!outputBuffer.trim()) {
152
+ return;
153
+ }
154
+
155
+ if (!enableRecovery) {
156
+ if (!silentJsonMode) {
157
+ log(`[${timestamp}]${outputBuffer}\n`);
158
+ }
159
+ return;
160
+ }
161
+
162
+ if (captureStreamingError(outputBuffer, timestamp)) {
163
+ return;
164
+ }
165
+
166
+ if (silentJsonMode) {
167
+ maybeCaptureStructuredOutput(outputBuffer);
168
+ return;
169
+ }
170
+
171
+ log(`[${timestamp}]${outputBuffer}\n`);
172
+ }
173
+
174
+ function attemptRecovery(code, timestamp) {
175
+ if (!(enableRecovery && code !== 0 && streamingModeError?.sessionId)) {
176
+ return null;
177
+ }
178
+
179
+ const recovered = recoverStructuredOutput(streamingModeError.sessionId);
180
+ if (recovered?.payload) {
181
+ const recoveredLine = JSON.stringify(recovered.payload);
182
+ if (silentJsonMode) {
183
+ finalResultJson = recoveredLine;
184
+ } else {
185
+ log(`[${timestamp}]${recoveredLine}\n`);
186
+ }
187
+ } else if (streamingModeError.line) {
188
+ if (silentJsonMode) {
189
+ log(streamingModeError.line + '\n');
190
+ } else {
191
+ log(`[${streamingModeError.timestamp}]${streamingModeError.line}\n`);
192
+ }
193
+ }
194
+
195
+ return recovered;
196
+ }
197
+
198
+ function writeCompletionFooter(code, signal) {
199
+ if (config.outputFormat === 'json') {
200
+ return;
201
+ }
202
+
203
+ log(`\n${'='.repeat(50)}\n`);
204
+ log(`Finished: ${new Date().toISOString()}\n`);
205
+ log(`Exit code: ${code}, Signal: ${signal}\n`);
206
+ }
130
207
 
131
- // Create AttachServer to spawn Claude with PTY
132
208
  const server = new AttachServer({
133
209
  id: taskId,
134
210
  socketPath,
135
- command: claudeCommand,
211
+ command,
136
212
  args: finalArgs,
137
213
  cwd,
138
214
  env,
@@ -140,96 +216,62 @@ const server = new AttachServer({
140
216
  rows: 30,
141
217
  });
142
218
 
143
- // Handle output from PTY
144
219
  server.on('output', (data) => {
145
220
  const chunk = data.toString();
146
221
  const timestamp = Date.now();
147
222
 
223
+ const { lines, remaining } = splitBufferLines(outputBuffer, chunk);
224
+ outputBuffer = remaining;
225
+
148
226
  if (silentJsonMode) {
149
- // Parse each line to find structured_output
150
- outputBuffer += chunk;
151
- const lines = outputBuffer.split('\n');
152
- outputBuffer = lines.pop() || '';
153
-
154
- for (const line of lines) {
155
- if (!line.trim()) continue;
156
- try {
157
- const json = JSON.parse(line);
158
- if (json.structured_output) {
159
- finalResultJson = line;
160
- }
161
- } catch {
162
- // Not JSON, skip
163
- }
164
- }
227
+ handleSilentJsonLines(lines, timestamp);
165
228
  } else {
166
- // Normal mode - stream with timestamps
167
- outputBuffer += chunk;
168
- const lines = outputBuffer.split('\n');
169
- outputBuffer = lines.pop() || '';
170
-
171
- for (const line of lines) {
172
- log(`[${timestamp}]${line}\n`);
173
- }
229
+ handleStreamingLines(lines, timestamp);
174
230
  }
175
231
  });
176
232
 
177
- // Handle process exit
178
- server.on('exit', ({ exitCode, signal }) => {
233
+ server.on('exit', async ({ exitCode, signal }) => {
179
234
  const timestamp = Date.now();
180
235
  const code = exitCode;
181
236
 
182
- // Flush remaining buffered output
183
- if (outputBuffer.trim()) {
184
- if (silentJsonMode) {
185
- try {
186
- const json = JSON.parse(outputBuffer);
187
- if (json.structured_output) {
188
- finalResultJson = outputBuffer;
189
- }
190
- } catch {
191
- // Not valid JSON
192
- }
193
- } else {
194
- log(`[${timestamp}]${outputBuffer}\n`);
195
- }
196
- }
237
+ flushOutputBuffer(timestamp);
238
+
239
+ const recovered = attemptRecovery(code, timestamp);
197
240
 
198
- // In silent JSON mode, log ONLY the final structured_output JSON
199
241
  if (silentJsonMode && finalResultJson) {
200
242
  log(finalResultJson + '\n');
201
243
  }
202
244
 
203
- // Skip footer for pure JSON output
204
- if (config.outputFormat !== 'json') {
205
- log(`\n${'='.repeat(50)}\n`);
206
- log(`Finished: ${new Date().toISOString()}\n`);
207
- log(`Exit code: ${code}, Signal: ${signal}\n`);
245
+ writeCompletionFooter(code, signal);
246
+
247
+ const resolvedCode = recovered?.payload ? 0 : code;
248
+ const status = resolvedCode === 0 ? 'completed' : 'failed';
249
+ try {
250
+ await updateTask(taskId, {
251
+ status,
252
+ exitCode: resolvedCode,
253
+ error: resolvedCode !== 0 && signal ? `Killed by ${signal}` : null,
254
+ socketPath: null,
255
+ });
256
+ } catch (updateError) {
257
+ log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
208
258
  }
209
259
 
210
- // Simple status: completed if exit 0, failed otherwise
211
- const status = code === 0 ? 'completed' : 'failed';
212
- updateTask(taskId, {
213
- status,
214
- exitCode: code,
215
- error: signal ? `Killed by ${signal}` : null,
216
- socketPath: null, // Clear socket path on exit
217
- });
218
-
219
- // Give clients time to receive exit message before exiting
220
260
  setTimeout(() => {
221
261
  process.exit(0);
222
262
  }, 500);
223
263
  });
224
264
 
225
- // Handle errors
226
- server.on('error', (err) => {
265
+ server.on('error', async (err) => {
227
266
  log(`\nError: ${err.message}\n`);
228
- updateTask(taskId, { status: 'failed', error: err.message });
267
+ try {
268
+ await updateTask(taskId, { status: 'failed', error: err.message });
269
+ } catch (updateError) {
270
+ log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
271
+ }
229
272
  process.exit(1);
230
273
  });
231
274
 
232
- // Handle client attach/detach for logging
233
275
  server.on('clientAttach', ({ clientId }) => {
234
276
  log(`[${Date.now()}][ATTACH] Client attached: ${clientId.slice(0, 8)}...\n`);
235
277
  });
@@ -238,11 +280,9 @@ server.on('clientDetach', ({ clientId }) => {
238
280
  log(`[${Date.now()}][DETACH] Client detached: ${clientId.slice(0, 8)}...\n`);
239
281
  });
240
282
 
241
- // Start the server
242
283
  try {
243
284
  await server.start();
244
285
 
245
- // Update task with PID and socket path
246
286
  updateTask(taskId, {
247
287
  pid: server.pid,
248
288
  socketPath,
@@ -258,7 +298,6 @@ try {
258
298
  process.exit(1);
259
299
  }
260
300
 
261
- // Handle process signals for cleanup
262
301
  process.on('SIGTERM', async () => {
263
302
  log(`[${Date.now()}][SYSTEM] Received SIGTERM, stopping...\n`);
264
303
  await server.stop('SIGTERM');
@@ -0,0 +1,156 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ export const STREAMING_MODE_ERROR = 'only prompt commands are supported in streaming mode';
6
+
7
+ export function detectStreamingModeError(line) {
8
+ const trimmed = typeof line === 'string' ? line.trim() : '';
9
+ if (!trimmed.startsWith('{')) return null;
10
+
11
+ try {
12
+ const parsed = JSON.parse(trimmed);
13
+ if (
14
+ parsed &&
15
+ parsed.type === 'result' &&
16
+ parsed.is_error === true &&
17
+ Array.isArray(parsed.errors) &&
18
+ parsed.errors.includes(STREAMING_MODE_ERROR) &&
19
+ typeof parsed.session_id === 'string'
20
+ ) {
21
+ return {
22
+ sessionId: parsed.session_id,
23
+ line: trimmed,
24
+ };
25
+ }
26
+ } catch {
27
+ // Ignore parse errors - not JSON
28
+ }
29
+
30
+ return null;
31
+ }
32
+
33
+ function findSessionJsonlPath(sessionId) {
34
+ const claudeDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
35
+ const projectsDir = join(claudeDir, 'projects');
36
+ if (!existsSync(projectsDir)) return null;
37
+
38
+ const target = `${sessionId}.jsonl`;
39
+ const queue = [projectsDir];
40
+
41
+ while (queue.length > 0) {
42
+ const dir = queue.pop();
43
+ if (!dir) continue;
44
+
45
+ let entries;
46
+ try {
47
+ entries = readdirSync(dir, { withFileTypes: true });
48
+ } catch {
49
+ continue;
50
+ }
51
+
52
+ for (const entry of entries) {
53
+ if (entry.isFile() && entry.name === target) {
54
+ return join(dir, entry.name);
55
+ }
56
+ if (entry.isDirectory()) {
57
+ queue.push(join(dir, entry.name));
58
+ }
59
+ }
60
+ }
61
+
62
+ return null;
63
+ }
64
+
65
+ export function recoverStructuredOutput(sessionId) {
66
+ const jsonlPath = findSessionJsonlPath(sessionId);
67
+ if (!jsonlPath) return null;
68
+
69
+ const fileContents = readJsonlFile(jsonlPath);
70
+ if (!fileContents) return null;
71
+
72
+ const { structuredOutput, usage } = findStructuredOutput(fileContents);
73
+
74
+ if (!structuredOutput) return null;
75
+
76
+ return {
77
+ payload: buildStructuredOutputPayload(sessionId, structuredOutput, usage),
78
+ sourcePath: jsonlPath,
79
+ };
80
+ }
81
+
82
+ function readJsonlFile(jsonlPath) {
83
+ try {
84
+ return readFileSync(jsonlPath, 'utf8');
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ function findStructuredOutput(fileContents) {
91
+ const lines = fileContents.split('\n');
92
+ let structuredOutput = null;
93
+ let usage = null;
94
+
95
+ for (const line of lines) {
96
+ const entry = parseJsonLine(line);
97
+ if (!entry) {
98
+ continue;
99
+ }
100
+
101
+ const extracted = extractStructuredOutputFromEntry(entry);
102
+ if (extracted) {
103
+ structuredOutput = extracted.structuredOutput;
104
+ usage = extracted.usage;
105
+ }
106
+ }
107
+
108
+ return { structuredOutput, usage };
109
+ }
110
+
111
+ function parseJsonLine(line) {
112
+ if (!line.trim()) return null;
113
+ try {
114
+ return JSON.parse(line);
115
+ } catch {
116
+ // Skip invalid JSON lines
117
+ return null;
118
+ }
119
+ }
120
+
121
+ function extractStructuredOutputFromEntry(entry) {
122
+ const message = entry?.message;
123
+ const content = message?.content;
124
+ if (!Array.isArray(content)) return null;
125
+
126
+ let structuredOutput = null;
127
+ let usage = null;
128
+ for (const block of content) {
129
+ if (block?.type === 'tool_use' && block?.name === 'StructuredOutput' && block?.input) {
130
+ structuredOutput = block.input;
131
+ usage = message?.usage && typeof message.usage === 'object' ? message.usage : null;
132
+ }
133
+ }
134
+
135
+ if (!structuredOutput) {
136
+ return null;
137
+ }
138
+
139
+ return { structuredOutput, usage };
140
+ }
141
+
142
+ function buildStructuredOutputPayload(sessionId, structuredOutput, usage) {
143
+ const payload = {
144
+ type: 'result',
145
+ subtype: 'success',
146
+ is_error: false,
147
+ structured_output: structuredOutput,
148
+ session_id: sessionId,
149
+ };
150
+
151
+ if (usage) {
152
+ payload.usage = usage;
153
+ }
154
+
155
+ return payload;
156
+ }