@okrapdf/cli 0.4.7 → 0.5.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 (63) hide show
  1. package/dist/commands/auth.d.ts.map +1 -1
  2. package/dist/commands/auth.js +94 -26
  3. package/dist/commands/auth.js.map +1 -1
  4. package/dist/commands/chat.d.ts +6 -17
  5. package/dist/commands/chat.d.ts.map +1 -1
  6. package/dist/commands/chat.js +344 -332
  7. package/dist/commands/chat.js.map +1 -1
  8. package/dist/commands/elements.d.ts.map +1 -1
  9. package/dist/commands/elements.js +15 -12
  10. package/dist/commands/elements.js.map +1 -1
  11. package/dist/commands/jobs.d.ts.map +1 -1
  12. package/dist/commands/jobs.js +228 -13
  13. package/dist/commands/jobs.js.map +1 -1
  14. package/dist/commands/read.d.ts.map +1 -1
  15. package/dist/commands/read.js +11 -9
  16. package/dist/commands/read.js.map +1 -1
  17. package/dist/commands/search.d.ts.map +1 -1
  18. package/dist/commands/search.js +3 -24
  19. package/dist/commands/search.js.map +1 -1
  20. package/dist/lib/backends/do-adapters.test.d.ts +6 -0
  21. package/dist/lib/backends/do-adapters.test.d.ts.map +1 -0
  22. package/dist/lib/backends/do-adapters.test.js +311 -0
  23. package/dist/lib/backends/do-adapters.test.js.map +1 -0
  24. package/dist/lib/backends/do.d.ts +23 -0
  25. package/dist/lib/backends/do.d.ts.map +1 -0
  26. package/dist/lib/backends/do.js +340 -0
  27. package/dist/lib/backends/do.js.map +1 -0
  28. package/dist/lib/backends/index.d.ts +7 -0
  29. package/dist/lib/backends/index.d.ts.map +1 -0
  30. package/dist/lib/backends/index.js +8 -0
  31. package/dist/lib/backends/index.js.map +1 -0
  32. package/dist/lib/backends/okrapdf.d.ts +9 -0
  33. package/dist/lib/backends/okrapdf.d.ts.map +1 -0
  34. package/dist/lib/backends/okrapdf.js +50 -0
  35. package/dist/lib/backends/okrapdf.js.map +1 -0
  36. package/dist/lib/backends/types.d.ts +205 -0
  37. package/dist/lib/backends/types.d.ts.map +1 -0
  38. package/dist/lib/backends/types.js +7 -0
  39. package/dist/lib/backends/types.js.map +1 -0
  40. package/dist/lib/completion-stream.d.ts +22 -0
  41. package/dist/lib/completion-stream.d.ts.map +1 -0
  42. package/dist/lib/completion-stream.js +69 -0
  43. package/dist/lib/completion-stream.js.map +1 -0
  44. package/dist/lib/config.d.ts +4 -0
  45. package/dist/lib/config.d.ts.map +1 -1
  46. package/dist/lib/config.js +6 -0
  47. package/dist/lib/config.js.map +1 -1
  48. package/dist/lib/general-chat.d.ts +11 -0
  49. package/dist/lib/general-chat.d.ts.map +1 -0
  50. package/dist/lib/general-chat.js +69 -0
  51. package/dist/lib/general-chat.js.map +1 -0
  52. package/dist/lib/multi-doc-chat.d.ts +20 -0
  53. package/dist/lib/multi-doc-chat.d.ts.map +1 -0
  54. package/dist/lib/multi-doc-chat.js +152 -0
  55. package/dist/lib/multi-doc-chat.js.map +1 -0
  56. package/dist/lib/single-doc-chat.d.ts +14 -0
  57. package/dist/lib/single-doc-chat.d.ts.map +1 -0
  58. package/dist/lib/single-doc-chat.js +165 -0
  59. package/dist/lib/single-doc-chat.js.map +1 -0
  60. package/dist/lib/task-agent.d.ts.map +1 -1
  61. package/dist/lib/task-agent.js +29 -10
  62. package/dist/lib/task-agent.js.map +1 -1
  63. package/package.json +3 -1
@@ -1,22 +1,11 @@
1
1
  /**
2
- * Agent-driven chat via WebSocket (same session as web UI)
2
+ * Unified chat command.
3
3
  *
4
- * okra chat <jobId> -m "what tables are in this document?"
5
- *
6
- * Both the web UI and CLI are clients to the same Cloudflare Durable Object.
7
- * On reconnect the DO replays persisted events (EVENTS_BATCH) containing old
8
- * AGENT_MESSAGE, AGENT_DONE, etc. We must ignore those and only render events
9
- * triggered by OUR new message.
10
- *
11
- * Server message order on reconnect:
12
- * Frame 1: CONNECTED
13
- * Frame 2: READY (with sandboxId if running)
14
- * Frame 3: SANDBOX_STATUS (live, not replayed)
15
- * Frame 4: EVENTS_BATCH (replayed history — arrives AFTER SANDBOX_STATUS)
16
- *
17
- * Because EVENTS_BATCH arrives after SANDBOX_STATUS resolves, a simple
18
- * messageSent flag doesn't work. Instead we use a global listener to detect
19
- * EVENTS_BATCH and wait for it before sending our message.
4
+ * okra chat "what is EBITDA?" → general (no doc)
5
+ * okra chat "how many pages?" --doc ocr-XXX → single-doc
6
+ * okra chat "compare revenue" --doc ocr-XXX,ocr-YYY → multi-doc
7
+ * okra chat "compare all" -c my-collection → multi-doc (collection)
8
+ * okra chat send ocr-XXX -m "question" → legacy compat
20
9
  */
21
10
  import '../lib/ws-polyfill.js';
22
11
  import { Command } from 'commander';
@@ -31,339 +20,114 @@ import { createSpinner } from '../lib/progress.js';
31
20
  import { OkraApiError, EXIT_CODES, patch as apiPatch } from '../lib/client.js';
32
21
  import { resolveSchema, extractJson, listBuiltinNames, } from '../lib/structured-output.js';
33
22
  import { resolveJobId } from '../lib/resolver.js';
34
- import { runTaskAgentQuery } from '../lib/task-agent.js';
23
+ import { runSingleDocChat } from '../lib/single-doc-chat.js';
24
+ import { runGeneralChat } from '../lib/general-chat.js';
25
+ import { runMultiDocChat } from '../lib/multi-doc-chat.js';
35
26
  const WSS_URL = getAgentSessionUrl();
36
27
  const SANDBOX_TEMPLATE = 'okra-claude-agent-sdk';
37
28
  const SANDBOX_TIMEOUT_MS = 120_000;
38
29
  export function createChatCommand() {
39
30
  const chat = new Command('chat')
40
- .description('Chat with a document via agent session');
41
- // Default subcommand: okra chat <document> -m "msg"
42
- const send = new Command('send')
43
- .description('Send a message to a document chat session')
44
- .argument('[document]', 'Job ID (ocr-*), source ref (arxiv/1706.03762), or URL')
45
- .requiredOption('-m, --message <message>', 'Message to send')
46
- .option('-c, --collection <nameOrId>', 'Use a named collection or collection ID (task-agent mode)')
47
- .option('-o, --output <format>', 'Output format (text, json)', 'text')
31
+ .description('Chat with documents or ask general questions')
32
+ .argument('[message]', 'Message to send (positional)')
33
+ .option('-d, --doc <ids>', 'Comma-separated job IDs (ocr-*)')
34
+ .option('-c, --collection <nameOrId>', 'Collection name or ID')
35
+ .option('-m, --message <message>', 'Message (alternative to positional)')
48
36
  .option('-s, --schema <schema>', 'Structured output: built-in name, inline JSON, or file path')
49
- .option('--model <model>', 'Model to use', 'claude-sonnet-4-20250514')
37
+ .option('--model <model>', 'Model to use')
50
38
  .option('--share', 'Generate a public share link')
51
- .action(async (document, options) => {
52
- // Handle --schema help
53
- if (options.schema === 'help') {
54
- console.log('Built-in schemas:');
55
- for (const name of listBuiltinNames()) {
56
- console.log(` ${name}`);
57
- }
58
- console.log('\nYou can also pass inline JSON or a file path:');
59
- console.log(' --schema \'{"type":"object","properties":{"name":{"type":"string"}}}\'');
60
- console.log(' --schema ./my-schema.json');
61
- process.exit(0);
39
+ .option('--socket', 'Use legacy WebSocket + E2B sandbox path')
40
+ .option('--tools <type>', 'Tool mode: sandbox | code_interpreter (stub)')
41
+ .action(async (positionalMessage, options) => {
42
+ const message = positionalMessage || options.message;
43
+ if (!message) {
44
+ error('Provide a message: okra chat "your question" or okra chat -m "your question"');
45
+ process.exit(EXIT_CODES.INVALID_ARGS);
62
46
  }
63
- // Resolve schema (built-in name, inline JSON, or file path)
64
- let resolvedSchema;
65
- if (options.schema) {
66
- const result = resolveSchema(options.schema);
67
- if ('error' in result) {
68
- error(result.error);
69
- process.exit(EXIT_CODES.INVALID_ARGS);
70
- }
71
- resolvedSchema = result.schema;
47
+ // --tools stub
48
+ if (options.tools) {
49
+ const chalk = (await import('chalk')).default;
50
+ process.stderr.write(chalk.yellow(`Warning: --tools ${options.tools} is not yet implemented. Proceeding without tools.\n`));
51
+ }
52
+ // Route based on --doc / -c presence
53
+ const docIds = options.doc?.split(',').map(s => s.trim()).filter(Boolean) || [];
54
+ const hasCollection = Boolean(options.collection);
55
+ const docCount = docIds.length;
56
+ if (!options.doc && !hasCollection) {
57
+ // General chat — no documents
58
+ await runGeneralChat({ message, model: options.model });
59
+ return;
72
60
  }
73
- const taskAgentMode = Boolean(options.collection) || Boolean(document?.includes(','));
74
- if (taskAgentMode) {
75
- if (resolvedSchema) {
76
- error('--schema is currently supported only for single-document sandbox chat');
77
- process.exit(EXIT_CODES.INVALID_ARGS);
61
+ if (docCount === 1 && !hasCollection) {
62
+ // Single-doc chat
63
+ if (options.socket) {
64
+ // Legacy WS + E2B sandbox path
65
+ await runLegacySocketChat(docIds[0], message, options);
66
+ return;
78
67
  }
79
- await runTaskAgentQuery({
80
- documents: document,
81
- message: options.message,
82
- collection: options.collection,
68
+ await runSingleDocChat({
69
+ jobId: docIds[0],
70
+ message,
71
+ schema: options.schema,
83
72
  share: options.share,
84
73
  model: options.model,
85
- sourceCommand: 'chat',
86
74
  });
87
75
  return;
88
76
  }
89
- if (!document) {
90
- error('Provide a document argument (or use -c/--collection for task-agent mode)');
77
+ // Multi-doc (2+ docs or collection)
78
+ if (options.schema) {
79
+ error('--schema is not supported for multi-document chat');
91
80
  process.exit(EXIT_CODES.INVALID_ARGS);
92
- return; // unreachable, helps TS narrow
93
81
  }
94
- // Resolve document reference to jobId
95
- const resolved = await resolveJobId(document).catch((err) => {
96
- error(err instanceof Error ? err.message : String(err));
97
- process.exit(EXIT_CODES.GENERAL_ERROR);
82
+ await runMultiDocChat({
83
+ message,
84
+ documents: docIds.length > 0 ? docIds.join(',') : undefined,
85
+ collection: options.collection,
86
+ model: options.model,
87
+ share: options.share,
98
88
  });
99
- const jobId = resolved.jobId;
100
- const apiKey = getApiKey();
101
- if (!apiKey) {
102
- error('Not authenticated. Run `okra auth login` first.');
103
- process.exit(EXIT_CODES.AUTH_ERROR);
104
- }
105
- const quiet = isQuietMode();
106
- // In schema mode, all human-friendly output goes to stderr (gh CLI pattern).
107
- // stdout is reserved exclusively for the JSON result.
108
- const spinnerStream = resolvedSchema ? process.stderr : undefined;
109
- const useSpinners = !quiet && (resolvedSchema ? true : process.stdout.isTTY);
110
- // Suppress library console.log that pollutes stdout
111
- // (AgentSessionClient logs "[AgentSession] ..." to console.log)
112
- // Must happen before client.connect() so connect/connected logs are caught.
113
- const origConsoleLog = console.log;
114
- if (resolvedSchema) {
115
- console.log = (...args) => {
116
- const first = typeof args[0] === 'string' ? args[0] : '';
117
- if (first.startsWith('[AgentSession]'))
118
- return;
119
- origConsoleLog(...args);
120
- };
121
- }
122
- // 1. Fetch bootstrap (archive URL + metadata)
123
- const bootSpinner = useSpinners
124
- ? createSpinner('Preparing document...', { stream: spinnerStream })
125
- : null;
126
- bootSpinner?.start();
127
- let archiveUrl;
128
- let totalPages;
129
- try {
130
- const bootstrap = await fetchBootstrap(jobId);
131
- archiveUrl = bootstrap.archiveUrl;
132
- totalPages = bootstrap.metadata.totalPages;
133
- bootSpinner?.succeed(`Document ready (${totalPages} pages)`);
134
- }
135
- catch (err) {
136
- if (err instanceof OkraApiError) {
137
- bootSpinner?.fail(`Failed to prepare document (${err.statusCode})`);
138
- error(err.message);
139
- if (err.details)
140
- error(JSON.stringify(err.details));
141
- }
142
- else {
143
- bootSpinner?.fail('Failed to prepare document');
144
- error(err instanceof Error ? err.message : String(err));
145
- }
146
- process.exit(EXIT_CODES.GENERAL_ERROR);
147
- }
148
- // 2. Connect WebSocket
149
- const renderer = resolvedSchema
150
- ? new SilentCollectorRenderer()
151
- : new AgentRenderer();
152
- const client = new AgentSessionClient({
153
- url: WSS_URL,
154
- sessionId: jobId,
155
- autoConnect: false,
156
- autoReconnect: false,
157
- });
158
- // Gate: ignore replayed agent events until we've sent our message.
159
- // After SANDBOX_STATUS resolves we wait 100ms for EVENTS_BATCH to drain,
160
- // then flip this flag and send. Events arriving before the flip are replay.
161
- let messageSent = false;
162
- // 3. Register event handlers
163
- const sandboxSpinner = useSpinners
164
- ? createSpinner('Booting sandbox...', { stream: spinnerStream })
165
- : null;
166
- // Promise that resolves when sandbox is ready.
167
- // Two paths to resolution (matching web client Redux slice):
168
- // 1. READY { sandboxId } — sandbox already running (reconnect)
169
- // 2. SANDBOX_STATUS { status: 'ready' } — fresh boot completed
170
- let resolveSandboxReady;
171
- const sandboxReady = new Promise((resolve, reject) => {
172
- resolveSandboxReady = resolve;
173
- const timeout = setTimeout(() => {
174
- reject(new Error('Sandbox boot timed out'));
175
- }, SANDBOX_TIMEOUT_MS);
176
- client.on('SANDBOX_STATUS', (event) => {
177
- if (event.status === 'ready') {
178
- clearTimeout(timeout);
179
- resolve();
180
- }
181
- else if (event.status === 'error') {
182
- clearTimeout(timeout);
183
- reject(new Error(event.error || 'Sandbox boot failed'));
184
- }
89
+ });
90
+ // ── Legacy `send` subcommand ──────────────────────────────────────────
91
+ // okra chat send ocr-XXX -m "question"
92
+ const send = new Command('send')
93
+ .description('Send a message (legacy — use `okra chat "msg" --doc <id>` instead)')
94
+ .argument('[document]', 'Job ID (ocr-*), source ref, or URL')
95
+ .requiredOption('-m, --message <message>', 'Message to send')
96
+ .option('-c, --collection <nameOrId>', 'Collection name or ID')
97
+ .option('-s, --schema <schema>', 'Structured output schema')
98
+ .option('--model <model>', 'Model to use', 'claude-sonnet-4-20250514')
99
+ .option('--share', 'Generate a public share link')
100
+ .option('--socket', 'Use legacy WebSocket + E2B sandbox path')
101
+ .action(async (document, options) => {
102
+ const hasMultipleDocs = document?.includes(',');
103
+ const hasCollection = Boolean(options.collection);
104
+ if (hasCollection || hasMultipleDocs) {
105
+ // Multi-doc via orchestrator
106
+ await runMultiDocChat({
107
+ message: options.message,
108
+ documents: document,
109
+ collection: options.collection,
110
+ model: options.model,
111
+ share: options.share,
185
112
  });
186
- });
187
- client.on('READY', (event) => {
188
- if (event.sandboxId) {
189
- // Sandbox already running on reconnect.
190
- // Match web client behavior: READY { sandboxId } means sandbox is alive.
191
- // (Redux slice setReady sets sandboxStatus = 'ready' when sandboxId present)
192
- // Server validated sandbox health before including sandboxId — trust it.
193
- resolveSandboxReady();
194
- }
195
- else {
196
- sandboxSpinner?.start();
197
- client.send({
198
- type: 'START_SANDBOX',
199
- template: SANDBOX_TEMPLATE,
200
- bootstrap: { archiveUrl },
201
- });
202
- }
203
- });
204
- // Wait for EVENTS_BATCH replay to finish (or timeout if no replay).
205
- const replayDone = new Promise((resolve) => {
206
- sandboxReady.then(() => {
207
- setTimeout(resolve, 100);
208
- }).catch(() => resolve());
209
- });
210
- // Promise that resolves when agent is done
211
- let resolveAgent;
212
- let rejectAgent;
213
- const agentDone = new Promise((resolve, reject) => {
214
- resolveAgent = resolve;
215
- rejectAgent = reject;
216
- });
217
- client.on('LIFECYCLE', (event) => {
218
- if (sandboxSpinner?.isSpinning) {
219
- sandboxSpinner.text = event.message || event.phase;
220
- }
221
- });
222
- // Agent events — gated on messageSent to skip replay
223
- client.on('AGENT_STARTED', (_event) => {
224
- if (!messageSent)
225
- return;
226
- renderer.onAgentStarted();
227
- });
228
- // Capture structured_output from SDK result event
229
- let structuredOutput = undefined;
230
- client.on('AGENT_MESSAGE', (event) => {
231
- if (!messageSent)
232
- return;
233
- // Runner sends subtype:"result" with is_error:true on SDK failure
234
- if (event.subtype === 'result' && event.is_error) {
235
- const errMsg = event.result || 'Agent failed (no details)';
236
- renderer.onAgentError(String(errMsg));
237
- rejectAgent(new Error(String(errMsg)));
238
- return;
239
- }
240
- // Capture SDK structured_output from result event
241
- if (event.subtype === 'result' && event.structured_output != null) {
242
- structuredOutput = event.structured_output;
243
- }
244
- renderer.onAgentMessage(event.message?.content);
245
- });
246
- client.on('AGENT_DONE', (event) => {
247
- if (!messageSent)
248
- return;
249
- renderer.onAgentDone(event.total_cost_usd, event.duration_ms);
250
- resolveAgent();
251
- });
252
- client.on('AGENT_ERROR', (event) => {
253
- if (!messageSent)
254
- return;
255
- renderer.onAgentError(event.error);
256
- rejectAgent(new Error(event.error));
257
- });
258
- client.on('ERROR', (event) => {
259
- error(`Server error: ${event.msg}`);
260
- rejectAgent(new Error(event.msg));
261
- });
262
- // 4. Connect
263
- client.connect();
264
- // 5. Wait for sandbox
265
- try {
266
- await sandboxReady;
267
- if (sandboxSpinner?.isSpinning) {
268
- sandboxSpinner.succeed('Sandbox ready');
269
- }
270
- }
271
- catch (err) {
272
- sandboxSpinner?.fail('Sandbox boot failed');
273
- error(err instanceof Error ? err.message : String(err));
274
- client.disconnect();
275
- process.exit(EXIT_CODES.GENERAL_ERROR);
276
- }
277
- // 6. Wait for replay to drain before sending
278
- await replayDone;
279
- // 6b. --share: set is_public and print share URL
280
- const shareUrl = `https://okrapdf.com/share/${jobId}`;
281
- if (options.share) {
282
- try {
283
- await apiPatch(`api/share/${jobId}`, { is_public: true });
284
- info(`Share URL: ${shareUrl}`);
285
- }
286
- catch (err) {
287
- error(`Failed to enable sharing: ${err instanceof Error ? err.message : String(err)}`);
288
- }
113
+ return;
289
114
  }
290
- // 7. Send message — flip gate so subsequent events are rendered
291
- messageSent = true;
292
- if (!resolvedSchema)
293
- info(`Sending: ${options.message}`);
294
- const systemPrompt = buildSystemPrompt({ schema: resolvedSchema });
295
- // Send raw AGENT_MESSAGE so author metadata is preserved across
296
- // older @steventsao/agent-session client versions used by the CLI.
297
- const wsMessage = {
298
- type: 'AGENT_MESSAGE',
299
- content: options.message,
300
- model: { id: options.model, provider: 'anthropic' },
301
- systemPrompt,
302
- agentType: 'claude-code',
303
- author: { name: 'okra-cli', clientType: 'cli' },
304
- };
305
- // Send JSON Schema for SDK-native structured output
306
- if (resolvedSchema) {
307
- wsMessage.outputFormat = {
308
- type: 'json_schema',
309
- schema: resolvedSchema.jsonSchema,
310
- };
311
- }
312
- client.send(wsMessage);
313
- // 8. Wait for agent completion
314
- try {
315
- await agentDone;
316
- if (options.share) {
317
- info(`Share URL: ${shareUrl}`);
318
- }
115
+ if (!document) {
116
+ error('Provide a document argument (or use -c/--collection for multi-doc mode)');
117
+ process.exit(EXIT_CODES.INVALID_ARGS);
118
+ return;
319
119
  }
320
- catch (err) {
321
- error(err instanceof Error ? err.message : String(err));
322
- process.exitCode = EXIT_CODES.GENERAL_ERROR;
323
- }
324
- // Restore console.log before outputting results
325
- if (resolvedSchema)
326
- console.log = origConsoleLog;
327
- // 8b. Structured output: prefer SDK structured_output, fall back to text extraction
328
- // Write JSON to stdout via process.stdout.write (not console.log) to keep
329
- // a clean machine-readable stream — gh CLI pattern.
330
- if (resolvedSchema && !process.exitCode) {
331
- let parsed = structuredOutput ?? null;
332
- // Fallback: extract JSON from collected text if SDK didn't return structured_output
333
- if (parsed === null) {
334
- const raw = renderer.getCollectedText();
335
- parsed = extractJson(raw);
336
- if (parsed === null) {
337
- process.stderr.write('Failed to extract JSON from agent response.\n');
338
- process.stderr.write('Raw output:\n' + raw + '\n');
339
- process.exitCode = EXIT_CODES.VALIDATION_ERROR;
340
- }
341
- }
342
- if (parsed !== null && !process.exitCode) {
343
- // For built-in schemas, validate with Zod; for custom schemas, output as-is
344
- if (resolvedSchema.zodSchema) {
345
- const result = resolvedSchema.zodSchema.safeParse(parsed);
346
- if (result.success) {
347
- process.stdout.write(JSON.stringify(result.data, null, 2) + '\n');
348
- }
349
- else {
350
- process.stderr.write('Schema validation failed:\n');
351
- for (const issue of result.error.issues) {
352
- process.stderr.write(` - ${issue.path.join('.')}: ${issue.message}\n`);
353
- }
354
- process.stderr.write('\nRaw JSON:\n' + JSON.stringify(parsed, null, 2) + '\n');
355
- process.exitCode = EXIT_CODES.VALIDATION_ERROR;
356
- }
357
- }
358
- else {
359
- // Custom schema — SDK already validated via constrained decoding
360
- process.stdout.write(JSON.stringify(parsed, null, 2) + '\n');
361
- }
362
- }
120
+ if (options.socket) {
121
+ await runLegacySocketChat(document, options.message, options);
122
+ return;
363
123
  }
364
- // 9. Disconnect and exit
365
- client.disconnect();
366
- process.exit(process.exitCode ?? 0);
124
+ await runSingleDocChat({
125
+ jobId: document,
126
+ message: options.message,
127
+ schema: options.schema,
128
+ share: options.share,
129
+ model: options.model,
130
+ });
367
131
  });
368
132
  // ── view subcommand ──────────────────────────────────────────────────
369
133
  const view = new Command('view')
@@ -380,7 +144,6 @@ export function createChatCommand() {
380
144
  const chalk = (await import('chalk')).default;
381
145
  const got = (await import('got')).default;
382
146
  const { openInBrowser } = await import('../lib/browser.js');
383
- // Resolve events source
384
147
  let events;
385
148
  if (opts.file) {
386
149
  try {
@@ -424,7 +187,6 @@ export function createChatCommand() {
424
187
  info('No events found');
425
188
  process.exit(0);
426
189
  }
427
- // Output modes
428
190
  if (opts.jsonl) {
429
191
  for (const evt of events) {
430
192
  console.log(JSON.stringify(evt));
@@ -448,10 +210,261 @@ export function createChatCommand() {
448
210
  }
449
211
  process.exit(0);
450
212
  });
451
- chat.addCommand(send, { isDefault: true });
213
+ chat.addCommand(send);
452
214
  chat.addCommand(view);
453
215
  return chat;
454
216
  }
217
+ // ── Legacy WebSocket + E2B sandbox path ─────────────────────────────────
218
+ async function runLegacySocketChat(document, message, options) {
219
+ // Resolve schema
220
+ let resolvedSchema;
221
+ if (options.schema) {
222
+ if (options.schema === 'help') {
223
+ console.log('Built-in schemas:');
224
+ for (const name of listBuiltinNames()) {
225
+ console.log(` ${name}`);
226
+ }
227
+ process.exit(0);
228
+ }
229
+ const result = resolveSchema(options.schema);
230
+ if ('error' in result) {
231
+ error(result.error);
232
+ process.exit(EXIT_CODES.INVALID_ARGS);
233
+ }
234
+ resolvedSchema = result.schema;
235
+ }
236
+ const apiKey = getApiKey();
237
+ if (!apiKey) {
238
+ error('Not authenticated. Run `okra auth login` first.');
239
+ process.exit(EXIT_CODES.AUTH_ERROR);
240
+ }
241
+ const resolved = await resolveJobId(document).catch((err) => {
242
+ error(err instanceof Error ? err.message : String(err));
243
+ process.exit(EXIT_CODES.GENERAL_ERROR);
244
+ });
245
+ const jobId = resolved.jobId;
246
+ const model = options.model || 'claude-sonnet-4-20250514';
247
+ const quiet = isQuietMode();
248
+ const spinnerStream = resolvedSchema ? process.stderr : undefined;
249
+ const useSpinners = !quiet && (resolvedSchema ? true : process.stdout.isTTY);
250
+ const origConsoleLog = console.log;
251
+ if (resolvedSchema) {
252
+ console.log = (...args) => {
253
+ const first = typeof args[0] === 'string' ? args[0] : '';
254
+ if (first.startsWith('[AgentSession]'))
255
+ return;
256
+ origConsoleLog(...args);
257
+ };
258
+ }
259
+ const bootSpinner = useSpinners
260
+ ? createSpinner('Preparing document...', { stream: spinnerStream })
261
+ : null;
262
+ bootSpinner?.start();
263
+ let archiveUrl;
264
+ let totalPages;
265
+ try {
266
+ const bootstrap = await fetchBootstrap(jobId);
267
+ archiveUrl = bootstrap.archiveUrl;
268
+ totalPages = bootstrap.metadata.totalPages;
269
+ bootSpinner?.succeed(`Document ready (${totalPages} pages)`);
270
+ }
271
+ catch (err) {
272
+ if (err instanceof OkraApiError) {
273
+ bootSpinner?.fail(`Failed to prepare document (${err.statusCode})`);
274
+ error(err.message);
275
+ if (err.details)
276
+ error(JSON.stringify(err.details));
277
+ }
278
+ else {
279
+ bootSpinner?.fail('Failed to prepare document');
280
+ error(err instanceof Error ? err.message : String(err));
281
+ }
282
+ process.exit(EXIT_CODES.GENERAL_ERROR);
283
+ }
284
+ const renderer = resolvedSchema
285
+ ? new SilentCollectorRenderer()
286
+ : new AgentRenderer();
287
+ const client = new AgentSessionClient({
288
+ url: WSS_URL,
289
+ sessionId: jobId,
290
+ autoConnect: false,
291
+ autoReconnect: false,
292
+ });
293
+ let messageSent = false;
294
+ const sandboxSpinner = useSpinners
295
+ ? createSpinner('Booting sandbox...', { stream: spinnerStream })
296
+ : null;
297
+ let resolveSandboxReady;
298
+ const sandboxReady = new Promise((resolve, reject) => {
299
+ resolveSandboxReady = resolve;
300
+ const timeout = setTimeout(() => {
301
+ reject(new Error('Sandbox boot timed out'));
302
+ }, SANDBOX_TIMEOUT_MS);
303
+ client.on('SANDBOX_STATUS', (event) => {
304
+ if (event.status === 'ready') {
305
+ clearTimeout(timeout);
306
+ resolve();
307
+ }
308
+ else if (event.status === 'error') {
309
+ clearTimeout(timeout);
310
+ reject(new Error(event.error || 'Sandbox boot failed'));
311
+ }
312
+ });
313
+ });
314
+ client.on('READY', (event) => {
315
+ if (event.sandboxId) {
316
+ resolveSandboxReady();
317
+ }
318
+ else {
319
+ sandboxSpinner?.start();
320
+ client.send({
321
+ type: 'START_SANDBOX',
322
+ template: SANDBOX_TEMPLATE,
323
+ bootstrap: { archiveUrl },
324
+ });
325
+ }
326
+ });
327
+ const replayDone = new Promise((resolve) => {
328
+ sandboxReady.then(() => {
329
+ setTimeout(resolve, 100);
330
+ }).catch(() => resolve());
331
+ });
332
+ let resolveAgent;
333
+ let rejectAgent;
334
+ const agentDone = new Promise((resolve, reject) => {
335
+ resolveAgent = resolve;
336
+ rejectAgent = reject;
337
+ });
338
+ client.on('LIFECYCLE', (event) => {
339
+ if (sandboxSpinner?.isSpinning) {
340
+ sandboxSpinner.text = event.message || event.phase;
341
+ }
342
+ });
343
+ client.on('AGENT_STARTED', (_event) => {
344
+ if (!messageSent)
345
+ return;
346
+ renderer.onAgentStarted();
347
+ });
348
+ let structuredOutput = undefined;
349
+ client.on('AGENT_MESSAGE', (event) => {
350
+ if (!messageSent)
351
+ return;
352
+ if (event.subtype === 'result' && event.is_error) {
353
+ const errMsg = event.result || 'Agent failed (no details)';
354
+ renderer.onAgentError(String(errMsg));
355
+ rejectAgent(new Error(String(errMsg)));
356
+ return;
357
+ }
358
+ if (event.subtype === 'result' && event.structured_output != null) {
359
+ structuredOutput = event.structured_output;
360
+ }
361
+ renderer.onAgentMessage(event.message?.content);
362
+ });
363
+ client.on('AGENT_DONE', (event) => {
364
+ if (!messageSent)
365
+ return;
366
+ renderer.onAgentDone(event.total_cost_usd, event.duration_ms);
367
+ resolveAgent();
368
+ });
369
+ client.on('AGENT_ERROR', (event) => {
370
+ if (!messageSent)
371
+ return;
372
+ renderer.onAgentError(event.error);
373
+ rejectAgent(new Error(event.error));
374
+ });
375
+ client.on('ERROR', (event) => {
376
+ error(`Server error: ${event.msg}`);
377
+ rejectAgent(new Error(event.msg));
378
+ });
379
+ client.connect();
380
+ try {
381
+ await sandboxReady;
382
+ if (sandboxSpinner?.isSpinning) {
383
+ sandboxSpinner.succeed('Sandbox ready');
384
+ }
385
+ }
386
+ catch (err) {
387
+ sandboxSpinner?.fail('Sandbox boot failed');
388
+ error(err instanceof Error ? err.message : String(err));
389
+ client.disconnect();
390
+ process.exit(EXIT_CODES.GENERAL_ERROR);
391
+ }
392
+ await replayDone;
393
+ const shareUrl = `https://okrapdf.com/share/${jobId}`;
394
+ if (options.share) {
395
+ try {
396
+ await apiPatch(`api/share/${jobId}`, { is_public: true });
397
+ info(`Share URL: ${shareUrl}`);
398
+ }
399
+ catch (err) {
400
+ error(`Failed to enable sharing: ${err instanceof Error ? err.message : String(err)}`);
401
+ }
402
+ }
403
+ messageSent = true;
404
+ if (!resolvedSchema)
405
+ info(`Sending: ${message}`);
406
+ const systemPrompt = buildSystemPrompt({ schema: resolvedSchema });
407
+ const wsMessage = {
408
+ type: 'AGENT_MESSAGE',
409
+ content: message,
410
+ model: { id: model, provider: 'anthropic' },
411
+ systemPrompt,
412
+ agentType: 'claude-code',
413
+ author: { name: 'okra-cli', clientType: 'cli' },
414
+ };
415
+ if (resolvedSchema) {
416
+ wsMessage.outputFormat = {
417
+ type: 'json_schema',
418
+ schema: resolvedSchema.jsonSchema,
419
+ };
420
+ }
421
+ client.send(wsMessage);
422
+ try {
423
+ await agentDone;
424
+ if (options.share) {
425
+ info(`Share URL: ${shareUrl}`);
426
+ }
427
+ }
428
+ catch (err) {
429
+ error(err instanceof Error ? err.message : String(err));
430
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
431
+ }
432
+ if (resolvedSchema)
433
+ console.log = origConsoleLog;
434
+ if (resolvedSchema && !process.exitCode) {
435
+ let parsed = structuredOutput ?? null;
436
+ if (parsed === null) {
437
+ const raw = renderer.getCollectedText();
438
+ parsed = extractJson(raw);
439
+ if (parsed === null) {
440
+ process.stderr.write('Failed to extract JSON from agent response.\n');
441
+ process.stderr.write('Raw output:\n' + raw + '\n');
442
+ process.exitCode = EXIT_CODES.VALIDATION_ERROR;
443
+ }
444
+ }
445
+ if (parsed !== null && !process.exitCode) {
446
+ if (resolvedSchema.zodSchema) {
447
+ const result = resolvedSchema.zodSchema.safeParse(parsed);
448
+ if (result.success) {
449
+ process.stdout.write(JSON.stringify(result.data, null, 2) + '\n');
450
+ }
451
+ else {
452
+ process.stderr.write('Schema validation failed:\n');
453
+ for (const issue of result.error.issues) {
454
+ process.stderr.write(` - ${issue.path.join('.')}: ${issue.message}\n`);
455
+ }
456
+ process.stderr.write('\nRaw JSON:\n' + JSON.stringify(parsed, null, 2) + '\n');
457
+ process.exitCode = EXIT_CODES.VALIDATION_ERROR;
458
+ }
459
+ }
460
+ else {
461
+ process.stdout.write(JSON.stringify(parsed, null, 2) + '\n');
462
+ }
463
+ }
464
+ }
465
+ client.disconnect();
466
+ process.exit(process.exitCode ?? 0);
467
+ }
455
468
  function parseJsonl(raw) {
456
469
  return raw
457
470
  .split('\n')
@@ -466,7 +479,6 @@ function parseJsonl(raw) {
466
479
  })
467
480
  .filter((e) => e !== null);
468
481
  }
469
- /* eslint-disable @typescript-eslint/no-explicit-any */
470
482
  function renderTerminalTranscript(events, chalk) {
471
483
  for (const evt of events) {
472
484
  const ts = evt.timestamp ? chalk.dim(new Date(evt.timestamp).toLocaleTimeString() + ' ') : '';