@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.
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +94 -26
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/chat.d.ts +6 -17
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +344 -332
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/elements.d.ts.map +1 -1
- package/dist/commands/elements.js +15 -12
- package/dist/commands/elements.js.map +1 -1
- package/dist/commands/jobs.d.ts.map +1 -1
- package/dist/commands/jobs.js +228 -13
- package/dist/commands/jobs.js.map +1 -1
- package/dist/commands/read.d.ts.map +1 -1
- package/dist/commands/read.js +11 -9
- package/dist/commands/read.js.map +1 -1
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +3 -24
- package/dist/commands/search.js.map +1 -1
- package/dist/lib/backends/do-adapters.test.d.ts +6 -0
- package/dist/lib/backends/do-adapters.test.d.ts.map +1 -0
- package/dist/lib/backends/do-adapters.test.js +311 -0
- package/dist/lib/backends/do-adapters.test.js.map +1 -0
- package/dist/lib/backends/do.d.ts +23 -0
- package/dist/lib/backends/do.d.ts.map +1 -0
- package/dist/lib/backends/do.js +340 -0
- package/dist/lib/backends/do.js.map +1 -0
- package/dist/lib/backends/index.d.ts +7 -0
- package/dist/lib/backends/index.d.ts.map +1 -0
- package/dist/lib/backends/index.js +8 -0
- package/dist/lib/backends/index.js.map +1 -0
- package/dist/lib/backends/okrapdf.d.ts +9 -0
- package/dist/lib/backends/okrapdf.d.ts.map +1 -0
- package/dist/lib/backends/okrapdf.js +50 -0
- package/dist/lib/backends/okrapdf.js.map +1 -0
- package/dist/lib/backends/types.d.ts +205 -0
- package/dist/lib/backends/types.d.ts.map +1 -0
- package/dist/lib/backends/types.js +7 -0
- package/dist/lib/backends/types.js.map +1 -0
- package/dist/lib/completion-stream.d.ts +22 -0
- package/dist/lib/completion-stream.d.ts.map +1 -0
- package/dist/lib/completion-stream.js +69 -0
- package/dist/lib/completion-stream.js.map +1 -0
- package/dist/lib/config.d.ts +4 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +6 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/general-chat.d.ts +11 -0
- package/dist/lib/general-chat.d.ts.map +1 -0
- package/dist/lib/general-chat.js +69 -0
- package/dist/lib/general-chat.js.map +1 -0
- package/dist/lib/multi-doc-chat.d.ts +20 -0
- package/dist/lib/multi-doc-chat.d.ts.map +1 -0
- package/dist/lib/multi-doc-chat.js +152 -0
- package/dist/lib/multi-doc-chat.js.map +1 -0
- package/dist/lib/single-doc-chat.d.ts +14 -0
- package/dist/lib/single-doc-chat.d.ts.map +1 -0
- package/dist/lib/single-doc-chat.js +165 -0
- package/dist/lib/single-doc-chat.js.map +1 -0
- package/dist/lib/task-agent.d.ts.map +1 -1
- package/dist/lib/task-agent.js +29 -10
- package/dist/lib/task-agent.js.map +1 -1
- package/package.json +3 -1
package/dist/commands/chat.js
CHANGED
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Unified chat command.
|
|
3
3
|
*
|
|
4
|
-
* okra chat
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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 {
|
|
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
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
.
|
|
44
|
-
.
|
|
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'
|
|
37
|
+
.option('--model <model>', 'Model to use')
|
|
50
38
|
.option('--share', 'Generate a public share link')
|
|
51
|
-
.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
message
|
|
82
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
|
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() + ' ') : '';
|