@nodetool-ai/cli-chat 0.0.0-stage → 0.8.9-beta.1

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 (107) hide show
  1. package/README.md +13 -2
  2. package/dist/.nodetool-build-stamp +1 -0
  3. package/dist/ExecutionTree.d.ts +20 -0
  4. package/dist/ExecutionTree.d.ts.map +1 -0
  5. package/dist/ExecutionTree.js +100 -0
  6. package/dist/ExecutionTree.js.map +1 -0
  7. package/dist/agent-toolbelt.d.ts +15 -0
  8. package/dist/agent-toolbelt.d.ts.map +1 -0
  9. package/dist/agent-toolbelt.js +28 -0
  10. package/dist/agent-toolbelt.js.map +1 -0
  11. package/dist/app.d.ts +40 -0
  12. package/dist/app.d.ts.map +1 -0
  13. package/dist/app.js +1070 -0
  14. package/dist/app.js.map +1 -0
  15. package/dist/bash-tool.d.ts +13 -0
  16. package/dist/bash-tool.d.ts.map +1 -0
  17. package/dist/bash-tool.js +85 -0
  18. package/dist/bash-tool.js.map +1 -0
  19. package/dist/chat-codeact.d.ts +93 -0
  20. package/dist/chat-codeact.d.ts.map +1 -0
  21. package/dist/chat-codeact.js +179 -0
  22. package/dist/chat-codeact.js.map +1 -0
  23. package/dist/chat-context.d.ts +18 -0
  24. package/dist/chat-context.d.ts.map +1 -0
  25. package/dist/chat-context.js +32 -0
  26. package/dist/chat-context.js.map +1 -0
  27. package/dist/chat-entry.d.ts +13 -0
  28. package/dist/chat-entry.d.ts.map +1 -0
  29. package/dist/chat-entry.js +233 -0
  30. package/dist/chat-entry.js.map +1 -0
  31. package/dist/chat-media.d.ts +3 -0
  32. package/dist/chat-media.d.ts.map +1 -0
  33. package/dist/chat-media.js +46 -0
  34. package/dist/chat-media.js.map +1 -0
  35. package/dist/chat-prompts.d.ts +20 -0
  36. package/dist/chat-prompts.d.ts.map +1 -0
  37. package/dist/chat-prompts.js +42 -0
  38. package/dist/chat-prompts.js.map +1 -0
  39. package/dist/chat-sessions.d.ts +25 -0
  40. package/dist/chat-sessions.d.ts.map +1 -0
  41. package/dist/chat-sessions.js +153 -0
  42. package/dist/chat-sessions.js.map +1 -0
  43. package/dist/commands/agent.d.ts +33 -0
  44. package/dist/commands/agent.d.ts.map +1 -0
  45. package/dist/commands/agent.js +516 -0
  46. package/dist/commands/agent.js.map +1 -0
  47. package/dist/commands/tools.d.ts +11 -0
  48. package/dist/commands/tools.d.ts.map +1 -0
  49. package/dist/commands/tools.js +206 -0
  50. package/dist/commands/tools.js.map +1 -0
  51. package/dist/diagnose.d.ts +139 -0
  52. package/dist/diagnose.d.ts.map +1 -0
  53. package/dist/diagnose.js +435 -0
  54. package/dist/diagnose.js.map +1 -0
  55. package/dist/index.d.ts +14 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +40 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/markdown.d.ts +9 -0
  60. package/dist/markdown.d.ts.map +1 -0
  61. package/dist/markdown.js +53 -0
  62. package/dist/markdown.js.map +1 -0
  63. package/dist/nodetool.d.ts +3 -0
  64. package/dist/nodetool.d.ts.map +1 -0
  65. package/dist/nodetool.js +4 -0
  66. package/dist/nodetool.js.map +1 -0
  67. package/dist/permission-gate.d.ts +55 -0
  68. package/dist/permission-gate.d.ts.map +1 -0
  69. package/dist/permission-gate.js +118 -0
  70. package/dist/permission-gate.js.map +1 -0
  71. package/dist/readline-input.d.ts +23 -0
  72. package/dist/readline-input.d.ts.map +1 -0
  73. package/dist/readline-input.js +210 -0
  74. package/dist/readline-input.js.map +1 -0
  75. package/dist/settings.d.ts +19 -0
  76. package/dist/settings.d.ts.map +1 -0
  77. package/dist/settings.js +146 -0
  78. package/dist/settings.js.map +1 -0
  79. package/dist/stdin.d.ts +52 -0
  80. package/dist/stdin.d.ts.map +1 -0
  81. package/dist/stdin.js +363 -0
  82. package/dist/stdin.js.map +1 -0
  83. package/dist/terminal-clipboard.d.ts +3 -0
  84. package/dist/terminal-clipboard.d.ts.map +1 -0
  85. package/dist/terminal-clipboard.js +24 -0
  86. package/dist/terminal-clipboard.js.map +1 -0
  87. package/dist/terminal-mouse.d.ts +24 -0
  88. package/dist/terminal-mouse.d.ts.map +1 -0
  89. package/dist/terminal-mouse.js +84 -0
  90. package/dist/terminal-mouse.js.map +1 -0
  91. package/dist/terminal-screen.d.ts +41 -0
  92. package/dist/terminal-screen.d.ts.map +1 -0
  93. package/dist/terminal-screen.js +228 -0
  94. package/dist/terminal-screen.js.map +1 -0
  95. package/dist/tool-format.d.ts +54 -0
  96. package/dist/tool-format.d.ts.map +1 -0
  97. package/dist/tool-format.js +360 -0
  98. package/dist/tool-format.js.map +1 -0
  99. package/dist/useExecutionState.d.ts +44 -0
  100. package/dist/useExecutionState.d.ts.map +1 -0
  101. package/dist/useExecutionState.js +285 -0
  102. package/dist/useExecutionState.js.map +1 -0
  103. package/dist/websocket-client.d.ts +159 -0
  104. package/dist/websocket-client.d.ts.map +1 -0
  105. package/dist/websocket-client.js +677 -0
  106. package/dist/websocket-client.js.map +1 -0
  107. package/package.json +73 -4
@@ -0,0 +1,677 @@
1
+ /**
2
+ * WebSocket client for connecting the CLI to a NodeTool server.
3
+ * Handles chat (stateful, with thread history), inference (stateless, for agent mode),
4
+ * and workflow commands (run_job, cancel_job, get_status).
5
+ */
6
+ import WebSocket from "ws";
7
+ import { processingMessageSchema } from "@nodetool-ai/protocol";
8
+ import { isNumber, isRecord, isString } from "@nodetool-ai/cli-common/predicates";
9
+ /** Read `key` as a string, or report it absent when the wire says otherwise. */
10
+ function wireString(frame, key) {
11
+ const value = frame[key];
12
+ return isString(value) ? value : undefined;
13
+ }
14
+ /** Read `key` as a number, or report it absent when the wire says otherwise. */
15
+ function wireNumber(frame, key) {
16
+ const value = frame[key];
17
+ return isNumber(value) ? value : undefined;
18
+ }
19
+ function toolCallArgs(frame) {
20
+ const args = frame["args"];
21
+ return isRecord(args) ? args : {};
22
+ }
23
+ /** Read a `message` frame's `tool_calls`; anything else reads as none. */
24
+ function parseToolCalls(frame) {
25
+ const raw = frame["tool_calls"];
26
+ if (!Array.isArray(raw))
27
+ return [];
28
+ const calls = [];
29
+ for (const entry of raw) {
30
+ if (!isRecord(entry))
31
+ continue;
32
+ const call = entry;
33
+ calls.push({
34
+ id: wireString(call, "id") ?? "",
35
+ name: wireString(call, "name") ?? "",
36
+ args: toolCallArgs(call)
37
+ });
38
+ }
39
+ return calls;
40
+ }
41
+ function parsePlan(value) {
42
+ if (!isRecord(value) ||
43
+ !isString(value["title"]) ||
44
+ !Array.isArray(value["tasks"]))
45
+ return null;
46
+ const tasks = [];
47
+ for (const task of value["tasks"]) {
48
+ if (!isRecord(task) ||
49
+ !isString(task["id"]) ||
50
+ !isString(task["title"]) ||
51
+ !Array.isArray(task["steps"]))
52
+ return null;
53
+ const steps = [];
54
+ for (const step of task["steps"]) {
55
+ if (!isRecord(step) ||
56
+ !isString(step["id"]) ||
57
+ !isString(step["instructions"]))
58
+ return null;
59
+ steps.push({ id: step["id"], instructions: step["instructions"] });
60
+ }
61
+ const dependencies = task["depends_on"];
62
+ tasks.push({
63
+ id: task["id"],
64
+ title: task["title"],
65
+ depends_on: Array.isArray(dependencies)
66
+ ? dependencies.filter(isString)
67
+ : [],
68
+ steps
69
+ });
70
+ }
71
+ return { title: value["title"], tasks };
72
+ }
73
+ function assistantText(content) {
74
+ if (isString(content))
75
+ return content;
76
+ if (!Array.isArray(content))
77
+ return "";
78
+ return content
79
+ .flatMap((block) => isRecord(block) && block["type"] === "text" && isString(block["text"])
80
+ ? [block["text"]]
81
+ : [])
82
+ .join("");
83
+ }
84
+ /**
85
+ * Turn one decoded frame into the domain value it stands for, or null when the
86
+ * client has nothing to do with it. Fields the wire gets wrong read as absent
87
+ * and fall back to the same defaults the generators used to apply inline.
88
+ */
89
+ function parseServerFrame(frame) {
90
+ const type = wireString(frame, "type");
91
+ // A command-level failure has no `type`, only `error`.
92
+ if (type === undefined) {
93
+ const error = wireString(frame, "error");
94
+ return error === undefined ? null : { type: "error", message: error };
95
+ }
96
+ switch (type) {
97
+ case "chunk":
98
+ return {
99
+ type: "chunk",
100
+ content: wireString(frame, "content") ?? "",
101
+ done: frame["done"] === true
102
+ };
103
+ case "message": {
104
+ const role = wireString(frame, "role");
105
+ if (role === "tool") {
106
+ return {
107
+ type: "tool_result",
108
+ toolCallId: wireString(frame, "tool_call_id") ?? "",
109
+ name: wireString(frame, "name") ?? "",
110
+ content: wireString(frame, "content") ?? ""
111
+ };
112
+ }
113
+ if (role === "assistant") {
114
+ return {
115
+ type: "assistant_message",
116
+ toolCalls: parseToolCalls(frame),
117
+ content: frame["content"]
118
+ };
119
+ }
120
+ return null;
121
+ }
122
+ case "tool_call":
123
+ return {
124
+ type: "tool_call",
125
+ id: wireString(frame, "tool_call_id") ?? wireString(frame, "id") ?? "",
126
+ name: wireString(frame, "name") ?? "",
127
+ args: toolCallArgs(frame)
128
+ };
129
+ case "tool_approval_request":
130
+ return {
131
+ type,
132
+ approvalId: wireString(frame, "approval_id") ?? "",
133
+ threadId: wireString(frame, "thread_id") ?? "",
134
+ toolName: wireString(frame, "tool_name") ?? "",
135
+ category: wireString(frame, "category") ?? "",
136
+ message: wireString(frame, "message") ?? "",
137
+ description: wireString(frame, "description") ?? "",
138
+ args: toolCallArgs(frame)
139
+ };
140
+ case "plan_approval_request": {
141
+ const plan = parsePlan(frame["plan"]);
142
+ if (!plan)
143
+ return {
144
+ type: "error",
145
+ message: "The server sent an invalid approval plan."
146
+ };
147
+ return {
148
+ type,
149
+ approvalId: wireString(frame, "approval_id") ?? "",
150
+ threadId: wireString(frame, "thread_id") ?? null,
151
+ plan
152
+ };
153
+ }
154
+ case "secret_request":
155
+ return {
156
+ type,
157
+ approvalId: wireString(frame, "approval_id") ?? "",
158
+ threadId: wireString(frame, "thread_id") ?? "",
159
+ key: wireString(frame, "key") ?? "",
160
+ description: wireString(frame, "description") ?? null,
161
+ reason: wireString(frame, "reason") ?? null,
162
+ helpUrl: wireString(frame, "help_url") ?? null
163
+ };
164
+ case "job_update":
165
+ return {
166
+ type: "job_update",
167
+ status: wireString(frame, "status") ?? "unknown",
168
+ jobId: wireString(frame, "job_id"),
169
+ workflowId: wireString(frame, "workflow_id"),
170
+ error: wireString(frame, "error"),
171
+ result: frame["result"]
172
+ };
173
+ case "node_update":
174
+ return {
175
+ type: "node_update",
176
+ nodeId: wireString(frame, "node_id") ?? "",
177
+ status: wireString(frame, "status") ?? "unknown",
178
+ error: wireString(frame, "error")
179
+ };
180
+ case "output_update":
181
+ return {
182
+ type: "output_update",
183
+ nodeId: wireString(frame, "node_id") ?? "",
184
+ value: frame["value"],
185
+ outputType: wireString(frame, "output_type")
186
+ };
187
+ case "node_progress":
188
+ return {
189
+ type: "node_progress",
190
+ nodeId: wireString(frame, "node_id") ?? "",
191
+ progress: wireNumber(frame, "progress") ?? 0,
192
+ total: wireNumber(frame, "total")
193
+ };
194
+ case "error":
195
+ return {
196
+ type: "error",
197
+ message: wireString(frame, "message") ?? "Unknown error"
198
+ };
199
+ case "inference_done":
200
+ return { type: "inference_done" };
201
+ case "generation_stopped":
202
+ return { type: "generation_stopped" };
203
+ default: {
204
+ const parsed = processingMessageSchema.safeParse(frame);
205
+ return parsed.success
206
+ ? { type: "processing", message: parsed.data }
207
+ : null;
208
+ }
209
+ }
210
+ }
211
+ export class WebSocketChatClient {
212
+ wsUrl;
213
+ ws = null;
214
+ disconnectedMessage = "Not connected to the NodeTool server.";
215
+ contentQueue = [];
216
+ contentWaiters = [];
217
+ constructor(wsUrl) {
218
+ this.wsUrl = wsUrl;
219
+ }
220
+ async connect() {
221
+ return new Promise((resolve, reject) => {
222
+ const ws = new WebSocket(this.wsUrl);
223
+ this.ws = ws;
224
+ this.disconnectedMessage =
225
+ "Connection to the NodeTool server closed before the response completed.";
226
+ // Guard so the connect promise settles exactly once: the "error" listener
227
+ // stays attached for the socket's lifetime, so without this a post-connect
228
+ // error would call reject() on an already-resolved promise and be silently
229
+ // swallowed (and an unhandled "error" event would otherwise crash Node).
230
+ let settled = false;
231
+ ws.on("open", () => {
232
+ if (this.ws !== ws) {
233
+ ws.close();
234
+ return;
235
+ }
236
+ // Switch server to text/JSON mode
237
+ ws.send(JSON.stringify({ command: "set_mode", data: { mode: "text" } }));
238
+ if (!settled) {
239
+ settled = true;
240
+ resolve();
241
+ }
242
+ });
243
+ ws.on("error", (err) => {
244
+ this.disconnectedMessage = `NodeTool connection failed: ${err.message}`;
245
+ if (this.ws === ws)
246
+ this.ws = null;
247
+ if (!settled) {
248
+ settled = true;
249
+ reject(err);
250
+ this.drainWaiters();
251
+ return;
252
+ }
253
+ // Error after a successful connect: tear down and unblock any waiting
254
+ // generators so they terminate instead of hanging forever.
255
+ this.ws = null;
256
+ this.drainWaiters();
257
+ });
258
+ ws.on("message", (data) => {
259
+ try {
260
+ const msg = JSON.parse(isString(data) ? data : data.toString("utf8"));
261
+ if (isRecord(msg))
262
+ this.handleMessage(msg);
263
+ }
264
+ catch {
265
+ // ignore malformed messages
266
+ }
267
+ });
268
+ ws.on("close", () => {
269
+ if (this.ws === ws)
270
+ this.ws = null;
271
+ if (!settled) {
272
+ settled = true;
273
+ reject(new Error(this.disconnectedMessage));
274
+ }
275
+ this.drainWaiters();
276
+ });
277
+ });
278
+ }
279
+ /** A dropped connection is a failed response, never a successful completion. */
280
+ drainWaiters() {
281
+ const waiters = this.contentWaiters;
282
+ this.contentWaiters = [];
283
+ for (const waiter of waiters)
284
+ waiter({ type: "error", message: this.disconnectedMessage });
285
+ }
286
+ handleMessage(msg) {
287
+ // Auto-respond to server pings
288
+ if (msg["type"] === "ping") {
289
+ this.send({ type: "pong", ts: Date.now() / 1000 });
290
+ return;
291
+ }
292
+ // Route content events to waiting generators
293
+ const parsed = parseServerFrame(msg);
294
+ if (!parsed)
295
+ return;
296
+ const processing = processingMessageSchema.safeParse(msg);
297
+ const frame = {
298
+ ...parsed,
299
+ scopeThreadId: wireString(msg, "thread_id"),
300
+ parentToolCallId: wireString(msg, "parent_tool_call_id"),
301
+ ...(processing.success && { processing: processing.data })
302
+ };
303
+ const waiter = this.contentWaiters.shift();
304
+ if (waiter) {
305
+ waiter(frame);
306
+ }
307
+ else {
308
+ this.contentQueue.push(frame);
309
+ }
310
+ }
311
+ send(data) {
312
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
313
+ throw new Error(this.disconnectedMessage);
314
+ }
315
+ this.ws.send(JSON.stringify(data));
316
+ }
317
+ nextContent(signal) {
318
+ if (signal?.aborted)
319
+ return Promise.resolve(null);
320
+ const queued = this.contentQueue.shift();
321
+ if (queued)
322
+ return Promise.resolve(queued);
323
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
324
+ return Promise.resolve({
325
+ type: "error",
326
+ message: this.disconnectedMessage
327
+ });
328
+ }
329
+ return new Promise((resolve) => {
330
+ const receive = (event) => {
331
+ signal?.removeEventListener("abort", cancel);
332
+ resolve(event);
333
+ };
334
+ const cancel = () => {
335
+ const index = this.contentWaiters.indexOf(receive);
336
+ if (index !== -1)
337
+ this.contentWaiters.splice(index, 1);
338
+ receive(null);
339
+ };
340
+ this.contentWaiters.push(receive);
341
+ signal?.addEventListener("abort", cancel, { once: true });
342
+ });
343
+ }
344
+ /** Stateful chat — messages are saved to DB, thread history is loaded. */
345
+ async *chat(content, threadId, model, provider, tools, options = {}) {
346
+ if (options.signal?.aborted) {
347
+ yield { type: "done" };
348
+ return;
349
+ }
350
+ this.contentQueue.length = 0;
351
+ this.send({
352
+ command: "chat_message",
353
+ data: {
354
+ role: "user",
355
+ content,
356
+ thread_id: threadId,
357
+ model,
358
+ provider,
359
+ tools: tools ?? [],
360
+ ...(options.permissionMode && {
361
+ permission_mode: options.permissionMode
362
+ })
363
+ }
364
+ });
365
+ const cancel = () => {
366
+ if (this.ws?.readyState === WebSocket.OPEN)
367
+ this.stop(threadId);
368
+ };
369
+ options.signal?.addEventListener("abort", cancel, { once: true });
370
+ let streamedText = "";
371
+ const announcedCalls = new Set();
372
+ const completedCalls = new Set();
373
+ try {
374
+ while (true) {
375
+ const event = await this.nextContent(options.signal);
376
+ if (!event) {
377
+ yield { type: "done" };
378
+ return;
379
+ }
380
+ if (event.scopeThreadId && event.scopeThreadId !== threadId)
381
+ continue;
382
+ if (event.type === "chunk") {
383
+ if (event.processing?.type === "chunk" &&
384
+ (event.processing.thinking ||
385
+ event.parentToolCallId ||
386
+ event.processing.subtask_depth)) {
387
+ yield { type: "processing", message: event.processing };
388
+ continue;
389
+ }
390
+ if (event.content) {
391
+ if (!event.parentToolCallId)
392
+ streamedText += event.content;
393
+ yield { type: "chunk", content: event.content };
394
+ }
395
+ // A delegated loop finishing does not finish its parent turn.
396
+ if (event.done && !event.parentToolCallId) {
397
+ yield { type: "done" };
398
+ return;
399
+ }
400
+ }
401
+ else if (event.type === "assistant_message") {
402
+ if (event.content != null) {
403
+ const fullText = assistantText(event.content);
404
+ const text = fullText.startsWith(streamedText)
405
+ ? fullText.slice(streamedText.length)
406
+ : fullText;
407
+ if (!event.parentToolCallId)
408
+ streamedText = "";
409
+ if (text || Array.isArray(event.content)) {
410
+ yield { type: "assistant_message", content: event.content, text };
411
+ }
412
+ }
413
+ for (const call of event.toolCalls) {
414
+ if (announcedCalls.has(call.id))
415
+ continue;
416
+ announcedCalls.add(call.id);
417
+ yield { type: "tool_call", ...call };
418
+ }
419
+ }
420
+ else if (event.type === "tool_call") {
421
+ yield {
422
+ type: "client_tool_call",
423
+ id: event.id,
424
+ name: event.name,
425
+ args: event.args,
426
+ threadId: event.scopeThreadId ?? threadId
427
+ };
428
+ }
429
+ else if (event.type === "tool_result") {
430
+ if (completedCalls.has(event.toolCallId))
431
+ continue;
432
+ completedCalls.add(event.toolCallId);
433
+ yield {
434
+ type: "tool_result",
435
+ id: event.toolCallId,
436
+ name: event.name,
437
+ content: event.content
438
+ };
439
+ }
440
+ else if (event.type === "output_update") {
441
+ yield {
442
+ type: "output_update",
443
+ node_id: event.nodeId,
444
+ value: event.value,
445
+ output_type: event.outputType
446
+ };
447
+ }
448
+ else if (event.type === "generation_stopped") {
449
+ yield { type: "done" };
450
+ return;
451
+ }
452
+ else if (event.type === "error") {
453
+ yield { type: "error", message: event.message };
454
+ return;
455
+ }
456
+ else if (event.type === "tool_approval_request" ||
457
+ event.type === "plan_approval_request" ||
458
+ event.type === "secret_request") {
459
+ yield event;
460
+ }
461
+ else if (event.type === "processing") {
462
+ const message = event.message;
463
+ if (message.type === "tool_call_update" &&
464
+ message.tool_call_id &&
465
+ !announcedCalls.has(message.tool_call_id)) {
466
+ announcedCalls.add(message.tool_call_id);
467
+ yield {
468
+ type: "tool_call",
469
+ id: message.tool_call_id,
470
+ name: message.name,
471
+ args: message.args
472
+ };
473
+ }
474
+ else if (message.type === "tool_result_update" &&
475
+ message.tool_call_id &&
476
+ !completedCalls.has(message.tool_call_id)) {
477
+ completedCalls.add(message.tool_call_id);
478
+ yield {
479
+ type: "tool_result",
480
+ id: message.tool_call_id,
481
+ name: message.name ?? "",
482
+ content: JSON.stringify(message.result)
483
+ };
484
+ }
485
+ yield { type: "processing", message };
486
+ }
487
+ else if (event.processing) {
488
+ yield { type: "processing", message: event.processing };
489
+ }
490
+ }
491
+ }
492
+ finally {
493
+ options.signal?.removeEventListener("abort", cancel);
494
+ }
495
+ }
496
+ /** Stateless inference — takes full messages array, streams back, no DB. Used by agent mode. */
497
+ async *inference(messages, model, provider, tools) {
498
+ this.send({
499
+ command: "inference",
500
+ data: { messages, model, provider, tools: tools ?? [] }
501
+ });
502
+ while (true) {
503
+ const event = await this.nextContent();
504
+ if (!event) {
505
+ yield { type: "done" };
506
+ return;
507
+ }
508
+ if (event.type === "chunk") {
509
+ if (event.content) {
510
+ yield { type: "chunk", content: event.content };
511
+ }
512
+ if (event.done) {
513
+ yield { type: "done" };
514
+ return;
515
+ }
516
+ }
517
+ else if (event.type === "tool_call") {
518
+ yield {
519
+ type: "tool_call",
520
+ id: event.id,
521
+ name: event.name,
522
+ args: event.args
523
+ };
524
+ }
525
+ else if (event.type === "inference_done" ||
526
+ event.type === "generation_stopped") {
527
+ yield { type: "done" };
528
+ return;
529
+ }
530
+ else if (event.type === "error") {
531
+ yield { type: "error", message: event.message };
532
+ return;
533
+ }
534
+ }
535
+ }
536
+ /**
537
+ * Run a workflow job. Streams job_update, node_update, output_update events.
538
+ * Terminates when a terminal job_update is received (completed/failed/cancelled).
539
+ */
540
+ async *runJob(opts) {
541
+ this.contentQueue.length = 0;
542
+ this.send({
543
+ command: "run_job",
544
+ data: {
545
+ workflow_id: opts.workflowId,
546
+ graph: opts.graph,
547
+ params: opts.params ?? {},
548
+ job_id: opts.jobId
549
+ }
550
+ });
551
+ yield* this.consumeJobEvents();
552
+ }
553
+ /** Reconnect to a running job's event stream. */
554
+ async *reconnectJob(jobId, workflowId) {
555
+ this.contentQueue.length = 0;
556
+ this.send({
557
+ command: "reconnect_job",
558
+ data: { job_id: jobId, workflow_id: workflowId }
559
+ });
560
+ yield* this.consumeJobEvents();
561
+ }
562
+ /** Shared job event consumer used by runJob and reconnectJob. */
563
+ async *consumeJobEvents() {
564
+ while (true) {
565
+ const event = await this.nextContent();
566
+ if (!event) {
567
+ yield { type: "done" };
568
+ return;
569
+ }
570
+ if (event.type === "job_update") {
571
+ yield {
572
+ type: "job_update",
573
+ status: event.status,
574
+ job_id: event.jobId,
575
+ workflow_id: event.workflowId,
576
+ error: event.error,
577
+ result: event.result
578
+ };
579
+ if (["completed", "failed", "cancelled", "error"].includes(event.status)) {
580
+ yield { type: "done" };
581
+ return;
582
+ }
583
+ }
584
+ else if (event.type === "node_update") {
585
+ yield {
586
+ type: "node_update",
587
+ node_id: event.nodeId,
588
+ status: event.status,
589
+ error: event.error
590
+ };
591
+ }
592
+ else if (event.type === "output_update") {
593
+ yield {
594
+ type: "output_update",
595
+ node_id: event.nodeId,
596
+ value: event.value,
597
+ output_type: event.outputType
598
+ };
599
+ }
600
+ else if (event.type === "node_progress") {
601
+ yield {
602
+ type: "node_progress",
603
+ node_id: event.nodeId,
604
+ progress: event.progress,
605
+ total: event.total
606
+ };
607
+ }
608
+ else if (event.type === "error") {
609
+ yield { type: "error", message: event.message };
610
+ return;
611
+ }
612
+ else if (event.type === "generation_stopped") {
613
+ yield { type: "done" };
614
+ return;
615
+ }
616
+ }
617
+ }
618
+ /** Cancel a running job. */
619
+ cancelJob(jobId) {
620
+ this.send({ command: "cancel_job", data: { job_id: jobId } });
621
+ }
622
+ /** Get status of a job or all active jobs. */
623
+ getStatus(jobId) {
624
+ this.send({ command: "get_status", data: jobId ? { job_id: jobId } : {} });
625
+ }
626
+ /** Stop in-progress generation. Pass threadId for chat, omit for inference. */
627
+ stop(threadId) {
628
+ const data = {};
629
+ if (threadId)
630
+ data.thread_id = threadId;
631
+ this.send({ command: "stop", data });
632
+ }
633
+ respondToolApproval(approvalId, decision) {
634
+ this.send({
635
+ type: "tool_approval_response",
636
+ approval_id: approvalId,
637
+ decision
638
+ });
639
+ }
640
+ respondPlanApproval(approvalId, decision, feedback) {
641
+ this.send({
642
+ type: "plan_approval_response",
643
+ approval_id: approvalId,
644
+ decision,
645
+ ...(feedback && { feedback })
646
+ });
647
+ }
648
+ respondSecretRequest(approvalId, status) {
649
+ this.send({
650
+ type: "secret_request_response",
651
+ approval_id: approvalId,
652
+ status
653
+ });
654
+ }
655
+ respondToolResult(id, threadId, result, ok = true) {
656
+ this.send({
657
+ type: "tool_result",
658
+ tool_call_id: id,
659
+ thread_id: threadId,
660
+ result,
661
+ ok
662
+ });
663
+ }
664
+ setPermissionMode(threadId, mode) {
665
+ this.send({
666
+ command: "set_permission_mode",
667
+ data: { thread_id: threadId, permission_mode: mode }
668
+ });
669
+ }
670
+ disconnect() {
671
+ const ws = this.ws;
672
+ this.ws = null;
673
+ ws?.close();
674
+ this.drainWaiters();
675
+ }
676
+ }
677
+ //# sourceMappingURL=websocket-client.js.map