@okrapdf/cli 0.4.6 → 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/cli.d.ts.map +1 -1
- package/dist/cli.js +14 -2
- package/dist/cli.js.map +1 -1
- 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 +349 -318
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/chat.test.js +4 -0
- package/dist/commands/chat.test.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/plugins.d.ts +3 -0
- package/dist/commands/plugins.d.ts.map +1 -0
- package/dist/commands/plugins.js +26 -0
- package/dist/commands/plugins.js.map +1 -0
- 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 +16 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +246 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/shortcuts.js +2 -2
- package/dist/commands/shortcuts.js.map +1 -1
- package/dist/commands/task.d.ts +0 -8
- package/dist/commands/task.d.ts.map +1 -1
- package/dist/commands/task.js +9 -190
- package/dist/commands/task.js.map +1 -1
- package/dist/lib/agent-renderer.d.ts.map +1 -1
- package/dist/lib/agent-renderer.js +17 -1
- package/dist/lib/agent-renderer.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/federation.d.ts +14 -0
- package/dist/lib/federation.d.ts.map +1 -0
- package/dist/lib/federation.js +51 -0
- package/dist/lib/federation.js.map +1 -0
- 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/plugins.d.ts +24 -0
- package/dist/lib/plugins.d.ts.map +1 -0
- package/dist/lib/plugins.js +55 -0
- package/dist/lib/plugins.js.map +1 -0
- package/dist/lib/runtime.js +1 -1
- package/dist/lib/runtime.js.map +1 -1
- 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 +11 -0
- package/dist/lib/task-agent.d.ts.map +1 -0
- package/dist/lib/task-agent.js +200 -0
- package/dist/lib/task-agent.js.map +1 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.d.ts.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,320 +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';
|
|
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';
|
|
34
26
|
const WSS_URL = getAgentSessionUrl();
|
|
35
27
|
const SANDBOX_TEMPLATE = 'okra-claude-agent-sdk';
|
|
36
28
|
const SANDBOX_TIMEOUT_MS = 120_000;
|
|
37
29
|
export function createChatCommand() {
|
|
38
30
|
const chat = new Command('chat')
|
|
39
|
-
.description('Chat with
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
.
|
|
44
|
-
.requiredOption('-m, --message <message>', 'Message to send')
|
|
45
|
-
.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)')
|
|
46
36
|
.option('-s, --schema <schema>', 'Structured output: built-in name, inline JSON, or file path')
|
|
37
|
+
.option('--model <model>', 'Model to use')
|
|
47
38
|
.option('--share', 'Generate a public share link')
|
|
48
|
-
.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
console.log('\nYou can also pass inline JSON or a file path:');
|
|
56
|
-
console.log(' --schema \'{"type":"object","properties":{"name":{"type":"string"}}}\'');
|
|
57
|
-
console.log(' --schema ./my-schema.json');
|
|
58
|
-
process.exit(0);
|
|
59
|
-
}
|
|
60
|
-
// Resolve schema (built-in name, inline JSON, or file path)
|
|
61
|
-
let resolvedSchema;
|
|
62
|
-
if (options.schema) {
|
|
63
|
-
const result = resolveSchema(options.schema);
|
|
64
|
-
if ('error' in result) {
|
|
65
|
-
error(result.error);
|
|
66
|
-
process.exit(EXIT_CODES.INVALID_ARGS);
|
|
67
|
-
}
|
|
68
|
-
resolvedSchema = result.schema;
|
|
69
|
-
}
|
|
70
|
-
if (!document) {
|
|
71
|
-
error('Provide a document argument, or use `okra task` for multi-doc queries');
|
|
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"');
|
|
72
45
|
process.exit(EXIT_CODES.INVALID_ARGS);
|
|
73
|
-
return; // unreachable, helps TS narrow
|
|
74
46
|
}
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
process.
|
|
79
|
-
});
|
|
80
|
-
const jobId = resolved.jobId;
|
|
81
|
-
const apiKey = getApiKey();
|
|
82
|
-
if (!apiKey) {
|
|
83
|
-
error('Not authenticated. Run `okra auth login` first.');
|
|
84
|
-
process.exit(EXIT_CODES.AUTH_ERROR);
|
|
85
|
-
}
|
|
86
|
-
const quiet = isQuietMode();
|
|
87
|
-
// In schema mode, all human-friendly output goes to stderr (gh CLI pattern).
|
|
88
|
-
// stdout is reserved exclusively for the JSON result.
|
|
89
|
-
const spinnerStream = resolvedSchema ? process.stderr : undefined;
|
|
90
|
-
const useSpinners = !quiet && (resolvedSchema ? process.stderr.isTTY : process.stdout.isTTY);
|
|
91
|
-
// Suppress library console.log that pollutes stdout
|
|
92
|
-
// (AgentSessionClient logs "[AgentSession] ..." to console.log)
|
|
93
|
-
// Must happen before client.connect() so connect/connected logs are caught.
|
|
94
|
-
const origConsoleLog = console.log;
|
|
95
|
-
if (resolvedSchema) {
|
|
96
|
-
console.log = (...args) => {
|
|
97
|
-
const first = typeof args[0] === 'string' ? args[0] : '';
|
|
98
|
-
if (first.startsWith('[AgentSession]'))
|
|
99
|
-
return;
|
|
100
|
-
origConsoleLog(...args);
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
// 1. Fetch bootstrap (archive URL + metadata)
|
|
104
|
-
const bootSpinner = useSpinners
|
|
105
|
-
? createSpinner('Preparing document...', { stream: spinnerStream })
|
|
106
|
-
: null;
|
|
107
|
-
bootSpinner?.start();
|
|
108
|
-
let archiveUrl;
|
|
109
|
-
let totalPages;
|
|
110
|
-
try {
|
|
111
|
-
const bootstrap = await fetchBootstrap(jobId);
|
|
112
|
-
archiveUrl = bootstrap.archiveUrl;
|
|
113
|
-
totalPages = bootstrap.metadata.totalPages;
|
|
114
|
-
bootSpinner?.succeed(`Document ready (${totalPages} pages)`);
|
|
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`));
|
|
115
51
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const renderer = resolvedSchema
|
|
131
|
-
? new SilentCollectorRenderer()
|
|
132
|
-
: new AgentRenderer();
|
|
133
|
-
const client = new AgentSessionClient({
|
|
134
|
-
url: WSS_URL,
|
|
135
|
-
sessionId: jobId,
|
|
136
|
-
autoConnect: false,
|
|
137
|
-
autoReconnect: false,
|
|
138
|
-
});
|
|
139
|
-
// Gate: ignore replayed agent events until we've sent our message.
|
|
140
|
-
// After SANDBOX_STATUS resolves we wait 100ms for EVENTS_BATCH to drain,
|
|
141
|
-
// then flip this flag and send. Events arriving before the flip are replay.
|
|
142
|
-
let messageSent = false;
|
|
143
|
-
// 3. Register event handlers
|
|
144
|
-
const sandboxSpinner = useSpinners
|
|
145
|
-
? createSpinner('Booting sandbox...', { stream: spinnerStream })
|
|
146
|
-
: null;
|
|
147
|
-
// Promise that resolves when sandbox is ready.
|
|
148
|
-
// Two paths to resolution (matching web client Redux slice):
|
|
149
|
-
// 1. READY { sandboxId } — sandbox already running (reconnect)
|
|
150
|
-
// 2. SANDBOX_STATUS { status: 'ready' } — fresh boot completed
|
|
151
|
-
let resolveSandboxReady;
|
|
152
|
-
const sandboxReady = new Promise((resolve, reject) => {
|
|
153
|
-
resolveSandboxReady = resolve;
|
|
154
|
-
const timeout = setTimeout(() => {
|
|
155
|
-
reject(new Error('Sandbox boot timed out'));
|
|
156
|
-
}, SANDBOX_TIMEOUT_MS);
|
|
157
|
-
client.on('SANDBOX_STATUS', (event) => {
|
|
158
|
-
if (event.status === 'ready') {
|
|
159
|
-
clearTimeout(timeout);
|
|
160
|
-
resolve();
|
|
161
|
-
}
|
|
162
|
-
else if (event.status === 'error') {
|
|
163
|
-
clearTimeout(timeout);
|
|
164
|
-
reject(new Error(event.error || 'Sandbox boot failed'));
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
client.on('READY', (event) => {
|
|
169
|
-
if (event.sandboxId) {
|
|
170
|
-
// Sandbox already running on reconnect.
|
|
171
|
-
// Match web client behavior: READY { sandboxId } means sandbox is alive.
|
|
172
|
-
// (Redux slice setReady sets sandboxStatus = 'ready' when sandboxId present)
|
|
173
|
-
// Server validated sandbox health before including sandboxId — trust it.
|
|
174
|
-
resolveSandboxReady();
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
sandboxSpinner?.start();
|
|
178
|
-
client.send({
|
|
179
|
-
type: 'START_SANDBOX',
|
|
180
|
-
template: SANDBOX_TEMPLATE,
|
|
181
|
-
bootstrap: { archiveUrl },
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
// Wait for EVENTS_BATCH replay to finish (or timeout if no replay).
|
|
186
|
-
const replayDone = new Promise((resolve) => {
|
|
187
|
-
sandboxReady.then(() => {
|
|
188
|
-
setTimeout(resolve, 100);
|
|
189
|
-
}).catch(() => resolve());
|
|
190
|
-
});
|
|
191
|
-
// Promise that resolves when agent is done
|
|
192
|
-
let resolveAgent;
|
|
193
|
-
let rejectAgent;
|
|
194
|
-
const agentDone = new Promise((resolve, reject) => {
|
|
195
|
-
resolveAgent = resolve;
|
|
196
|
-
rejectAgent = reject;
|
|
197
|
-
});
|
|
198
|
-
client.on('LIFECYCLE', (event) => {
|
|
199
|
-
if (sandboxSpinner?.isSpinning) {
|
|
200
|
-
sandboxSpinner.text = event.message || event.phase;
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
// Agent events — gated on messageSent to skip replay
|
|
204
|
-
client.on('AGENT_STARTED', (_event) => {
|
|
205
|
-
if (!messageSent)
|
|
206
|
-
return;
|
|
207
|
-
renderer.onAgentStarted();
|
|
208
|
-
});
|
|
209
|
-
// Capture structured_output from SDK result event
|
|
210
|
-
let structuredOutput = undefined;
|
|
211
|
-
client.on('AGENT_MESSAGE', (event) => {
|
|
212
|
-
if (!messageSent)
|
|
213
|
-
return;
|
|
214
|
-
// Runner sends subtype:"result" with is_error:true on SDK failure
|
|
215
|
-
if (event.subtype === 'result' && event.is_error) {
|
|
216
|
-
const errMsg = event.result || 'Agent failed (no details)';
|
|
217
|
-
renderer.onAgentError(String(errMsg));
|
|
218
|
-
rejectAgent(new Error(String(errMsg)));
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
// Capture SDK structured_output from result event
|
|
222
|
-
if (event.subtype === 'result' && event.structured_output != null) {
|
|
223
|
-
structuredOutput = event.structured_output;
|
|
224
|
-
}
|
|
225
|
-
renderer.onAgentMessage(event.message?.content);
|
|
226
|
-
});
|
|
227
|
-
client.on('AGENT_DONE', (event) => {
|
|
228
|
-
if (!messageSent)
|
|
229
|
-
return;
|
|
230
|
-
renderer.onAgentDone(event.total_cost_usd, event.duration_ms);
|
|
231
|
-
resolveAgent();
|
|
232
|
-
});
|
|
233
|
-
client.on('AGENT_ERROR', (event) => {
|
|
234
|
-
if (!messageSent)
|
|
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;
|
|
60
|
+
}
|
|
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);
|
|
235
66
|
return;
|
|
236
|
-
renderer.onAgentError(event.error);
|
|
237
|
-
rejectAgent(new Error(event.error));
|
|
238
|
-
});
|
|
239
|
-
client.on('ERROR', (event) => {
|
|
240
|
-
error(`Server error: ${event.msg}`);
|
|
241
|
-
rejectAgent(new Error(event.msg));
|
|
242
|
-
});
|
|
243
|
-
// 4. Connect
|
|
244
|
-
client.connect();
|
|
245
|
-
// 5. Wait for sandbox
|
|
246
|
-
try {
|
|
247
|
-
await sandboxReady;
|
|
248
|
-
if (sandboxSpinner?.isSpinning) {
|
|
249
|
-
sandboxSpinner.succeed('Sandbox ready');
|
|
250
67
|
}
|
|
68
|
+
await runSingleDocChat({
|
|
69
|
+
jobId: docIds[0],
|
|
70
|
+
message,
|
|
71
|
+
schema: options.schema,
|
|
72
|
+
share: options.share,
|
|
73
|
+
model: options.model,
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
251
76
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
error(
|
|
255
|
-
|
|
256
|
-
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
77
|
+
// Multi-doc (2+ docs or collection)
|
|
78
|
+
if (options.schema) {
|
|
79
|
+
error('--schema is not supported for multi-document chat');
|
|
80
|
+
process.exit(EXIT_CODES.INVALID_ARGS);
|
|
257
81
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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,
|
|
88
|
+
});
|
|
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,
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
270
114
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const systemPrompt = buildSystemPrompt({ schema: resolvedSchema });
|
|
276
|
-
// Send raw AGENT_MESSAGE so author metadata is preserved across
|
|
277
|
-
// older @steventsao/agent-session client versions used by the CLI.
|
|
278
|
-
const wsMessage = {
|
|
279
|
-
type: 'AGENT_MESSAGE',
|
|
280
|
-
content: options.message,
|
|
281
|
-
model: { id: 'claude-sonnet-4-20250514', provider: 'anthropic' },
|
|
282
|
-
systemPrompt,
|
|
283
|
-
agentType: 'claude-code',
|
|
284
|
-
author: { name: 'okra-cli', clientType: 'cli' },
|
|
285
|
-
};
|
|
286
|
-
// Send JSON Schema for SDK-native structured output
|
|
287
|
-
if (resolvedSchema) {
|
|
288
|
-
wsMessage.outputFormat = {
|
|
289
|
-
type: 'json_schema',
|
|
290
|
-
schema: resolvedSchema.jsonSchema,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
client.send(wsMessage);
|
|
294
|
-
// 8. Wait for agent completion
|
|
295
|
-
try {
|
|
296
|
-
await agentDone;
|
|
297
|
-
if (options.share) {
|
|
298
|
-
info(`Share URL: ${shareUrl}`);
|
|
299
|
-
}
|
|
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;
|
|
300
119
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
// Restore console.log before outputting results
|
|
306
|
-
if (resolvedSchema)
|
|
307
|
-
console.log = origConsoleLog;
|
|
308
|
-
// 8b. Structured output: prefer SDK structured_output, fall back to text extraction
|
|
309
|
-
// Write JSON to stdout via process.stdout.write (not console.log) to keep
|
|
310
|
-
// a clean machine-readable stream — gh CLI pattern.
|
|
311
|
-
if (resolvedSchema && !process.exitCode) {
|
|
312
|
-
let parsed = structuredOutput ?? null;
|
|
313
|
-
// Fallback: extract JSON from collected text if SDK didn't return structured_output
|
|
314
|
-
if (parsed === null) {
|
|
315
|
-
const raw = renderer.getCollectedText();
|
|
316
|
-
parsed = extractJson(raw);
|
|
317
|
-
if (parsed === null) {
|
|
318
|
-
process.stderr.write('Failed to extract JSON from agent response.\n');
|
|
319
|
-
process.stderr.write('Raw output:\n' + raw + '\n');
|
|
320
|
-
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
if (parsed !== null && !process.exitCode) {
|
|
324
|
-
// For built-in schemas, validate with Zod; for custom schemas, output as-is
|
|
325
|
-
if (resolvedSchema.zodSchema) {
|
|
326
|
-
const result = resolvedSchema.zodSchema.safeParse(parsed);
|
|
327
|
-
if (result.success) {
|
|
328
|
-
process.stdout.write(JSON.stringify(result.data, null, 2) + '\n');
|
|
329
|
-
}
|
|
330
|
-
else {
|
|
331
|
-
process.stderr.write('Schema validation failed:\n');
|
|
332
|
-
for (const issue of result.error.issues) {
|
|
333
|
-
process.stderr.write(` - ${issue.path.join('.')}: ${issue.message}\n`);
|
|
334
|
-
}
|
|
335
|
-
process.stderr.write('\nRaw JSON:\n' + JSON.stringify(parsed, null, 2) + '\n');
|
|
336
|
-
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
else {
|
|
340
|
-
// Custom schema — SDK already validated via constrained decoding
|
|
341
|
-
process.stdout.write(JSON.stringify(parsed, null, 2) + '\n');
|
|
342
|
-
}
|
|
343
|
-
}
|
|
120
|
+
if (options.socket) {
|
|
121
|
+
await runLegacySocketChat(document, options.message, options);
|
|
122
|
+
return;
|
|
344
123
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
124
|
+
await runSingleDocChat({
|
|
125
|
+
jobId: document,
|
|
126
|
+
message: options.message,
|
|
127
|
+
schema: options.schema,
|
|
128
|
+
share: options.share,
|
|
129
|
+
model: options.model,
|
|
130
|
+
});
|
|
348
131
|
});
|
|
349
132
|
// ── view subcommand ──────────────────────────────────────────────────
|
|
350
133
|
const view = new Command('view')
|
|
@@ -361,7 +144,6 @@ export function createChatCommand() {
|
|
|
361
144
|
const chalk = (await import('chalk')).default;
|
|
362
145
|
const got = (await import('got')).default;
|
|
363
146
|
const { openInBrowser } = await import('../lib/browser.js');
|
|
364
|
-
// Resolve events source
|
|
365
147
|
let events;
|
|
366
148
|
if (opts.file) {
|
|
367
149
|
try {
|
|
@@ -405,7 +187,6 @@ export function createChatCommand() {
|
|
|
405
187
|
info('No events found');
|
|
406
188
|
process.exit(0);
|
|
407
189
|
}
|
|
408
|
-
// Output modes
|
|
409
190
|
if (opts.jsonl) {
|
|
410
191
|
for (const evt of events) {
|
|
411
192
|
console.log(JSON.stringify(evt));
|
|
@@ -429,10 +210,261 @@ export function createChatCommand() {
|
|
|
429
210
|
}
|
|
430
211
|
process.exit(0);
|
|
431
212
|
});
|
|
432
|
-
chat.addCommand(send
|
|
213
|
+
chat.addCommand(send);
|
|
433
214
|
chat.addCommand(view);
|
|
434
215
|
return chat;
|
|
435
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
|
+
}
|
|
436
468
|
function parseJsonl(raw) {
|
|
437
469
|
return raw
|
|
438
470
|
.split('\n')
|
|
@@ -447,7 +479,6 @@ function parseJsonl(raw) {
|
|
|
447
479
|
})
|
|
448
480
|
.filter((e) => e !== null);
|
|
449
481
|
}
|
|
450
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
451
482
|
function renderTerminalTranscript(events, chalk) {
|
|
452
483
|
for (const evt of events) {
|
|
453
484
|
const ts = evt.timestamp ? chalk.dim(new Date(evt.timestamp).toLocaleTimeString() + ' ') : '';
|