@workerdeck/core 2.13.0 → 2.14.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/build/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import { createSdkMcpServer, getSessionInfo, getSessionMessages, listSessions, query, tool } from "@anthropic-ai/claude-agent-sdk";
4
- import { CHECKLIST_TEXT_MAX, ENGINE_CAPABILITIES, PEER_MENTION_MAX, PROTOCOL_VERSION, SUBAGENT_HISTORY, TOOL_RESULT_HEAD_CHARS, addTokenUsage, byModelTotalTokens, canonicalModel, contextReading, costOfByModel, emptyTokenUsage, imagePartRef, mergeByModel, parseTodoWriteInput, peerDeliveredPrefix, replayCoalesceKey, replayRetains, sanitizeToolTitle, snapshotRetains, tokenUsageFromWire, totalTokens, transcriptActivity, transcriptContent, transcriptProse } from "@workerdeck/protocol";
4
+ import { CHECKLIST_TEXT_MAX, ENGINE_CAPABILITIES, PEER_MENTION_MAX, PROTOCOL_VERSION, SHELL_CONTEXT_FLUSH_MAX_CHARS, SHELL_CONTEXT_HEAD_CHARS, SHELL_CONTEXT_HEAD_LINES, SHELL_CONTEXT_TAIL_CHARS, SHELL_CONTEXT_TAIL_LINES, SHELL_INLINE_CHARS, SHELL_INLINE_LINES, SHELL_INLINE_LINE_CHARS, SHELL_READ_DEFAULT_LINES, SHELL_READ_MAX_LINES, SUBAGENT_HISTORY, TOOL_RESULT_HEAD_CHARS, addTokenUsage, byModelTotalTokens, canonicalModel, contextReading, costOfByModel, emptyTokenUsage, imagePartRef, logCoalesceKey, mergeByModel, parseTodoWriteInput, peerDeliveredPrefix, replayCoalesceKey, replayRetains, sanitizeToolTitle, snapshotRetains, tokenUsageFromWire, totalTokens, transcriptActivity, transcriptContent, transcriptProse } from "@workerdeck/protocol";
5
5
  import { z } from "zod";
6
6
  import { ToolLoopAgent, generateText, isStepCount, tool as tool$1 } from "ai";
7
7
  import { execFile, spawn } from "node:child_process";
@@ -275,7 +275,110 @@ var InputQueue = class {
275
275
  }
276
276
  };
277
277
  //#endregion
278
+ //#region src/lib/tty-text.ts
279
+ const OSC_SEQUENCE = /(?:\x1b\]|\x9d)[\s\S]*?(?:\x07|\x1b\\|\x9c)/g;
280
+ const STRING_SEQUENCE = /(?:\x1b[PX^_]|[\x90\x98\x9e\x9f])[\s\S]*?(?:\x1b\\|\x9c)/g;
281
+ const CSI_SEQUENCE = /(?:\x1b\[|\x9b)[0-?]*[ -/]*[@-~]/g;
282
+ const ESCAPE_SEQUENCE = /\x1b[ -/]*[0-~]/g;
283
+ const CONTROL_CHARS = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g;
284
+ function ttyText(raw) {
285
+ return raw.replace(OSC_SEQUENCE, "").replace(STRING_SEQUENCE, "").replace(CSI_SEQUENCE, "").replace(ESCAPE_SEQUENCE, "").replace(CONTROL_CHARS, "").replace(/\r\n/g, "\n").split("\n").map((line) => overwritten(line).trimEnd()).join("\n");
286
+ }
287
+ function splitLines(text) {
288
+ if (text === "") return [];
289
+ return (text.endsWith("\n") ? text.slice(0, -1) : text).split("\n");
290
+ }
291
+ function countLines(text) {
292
+ if (text === "") return 0;
293
+ let count = 1;
294
+ for (let at = text.indexOf("\n"); at !== -1; at = text.indexOf("\n", at + 1)) count++;
295
+ return text.endsWith("\n") ? count - 1 : count;
296
+ }
297
+ function headTail(text, opts) {
298
+ const lines = splitLines(text);
299
+ const totalLines = lines.length;
300
+ const totalBytes = Buffer.byteLength(text, "utf8");
301
+ const body = lines.join("\n");
302
+ if (totalLines <= opts.headLines + opts.tailLines && body.length <= opts.headChars + opts.tailChars) return {
303
+ head: body,
304
+ tail: "",
305
+ omittedLines: 0,
306
+ totalLines,
307
+ totalBytes
308
+ };
309
+ const tail = takeTail(lines, Math.min(opts.tailLines, totalLines), opts.tailChars);
310
+ const head = takeHead(lines, Math.min(opts.headLines, totalLines - tail.lines), opts.headChars);
311
+ return {
312
+ head: head.text,
313
+ tail: tail.text,
314
+ omittedLines: totalLines - head.lines - tail.lines,
315
+ totalLines,
316
+ totalBytes
317
+ };
318
+ }
319
+ function overwritten(line) {
320
+ if (!line.includes("\r")) return line;
321
+ const segments = line.split("\r");
322
+ for (let i = segments.length - 1; i >= 0; i--) if (segments[i] !== "") return segments[i];
323
+ return "";
324
+ }
325
+ function takeHead(lines, maxLines, maxChars) {
326
+ let taken = 0;
327
+ let length = 0;
328
+ while (taken < maxLines) {
329
+ const next = length + lines[taken].length + (taken > 0 ? 1 : 0);
330
+ if (next > maxChars) break;
331
+ length = next;
332
+ taken++;
333
+ }
334
+ if (taken === 0) return {
335
+ text: maxLines > 0 ? lines[0].slice(0, maxChars) : "",
336
+ lines: 0
337
+ };
338
+ return {
339
+ text: lines.slice(0, taken).join("\n"),
340
+ lines: taken
341
+ };
342
+ }
343
+ function takeTail(lines, maxLines, maxChars) {
344
+ let taken = 0;
345
+ let length = 0;
346
+ while (taken < maxLines) {
347
+ const next = length + lines[lines.length - 1 - taken].length + (taken > 0 ? 1 : 0);
348
+ if (next > maxChars) break;
349
+ length = next;
350
+ taken++;
351
+ }
352
+ if (taken === 0) {
353
+ const last = lines[lines.length - 1];
354
+ return {
355
+ text: maxLines > 0 && maxChars > 0 ? last.slice(-maxChars) : "",
356
+ lines: 0
357
+ };
358
+ }
359
+ return {
360
+ text: lines.slice(lines.length - taken).join("\n"),
361
+ lines: taken
362
+ };
363
+ }
364
+ //#endregion
278
365
  //#region src/lib/local-command.ts
366
+ const OUTPUT_POINTER = "the full output is in the transcript";
367
+ const INLINE_MORE = "[... more output ...]";
368
+ const INLINE_CLIPPED = " [...]";
369
+ const INLINE_SCAN_CHARS = SHELL_INLINE_LINES * (SHELL_INLINE_LINE_CHARS + 1);
370
+ const TAIL_ONLY = {
371
+ headLines: 0,
372
+ headChars: 0,
373
+ tailLines: SHELL_CONTEXT_TAIL_LINES,
374
+ tailChars: SHELL_CONTEXT_TAIL_CHARS
375
+ };
376
+ const HEAD_AND_TAIL = {
377
+ headLines: SHELL_CONTEXT_HEAD_LINES,
378
+ headChars: SHELL_CONTEXT_HEAD_CHARS,
379
+ tailLines: SHELL_CONTEXT_TAIL_LINES,
380
+ tailChars: SHELL_CONTEXT_TAIL_CHARS
381
+ };
279
382
  const LOCAL_COMMAND_CAVEAT = "<local-command-caveat>Caveat: the messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.</local-command-caveat>";
280
383
  function localCommandTranscript(result) {
281
384
  const tag = result.exitCode === 0 ? "local-command-stdout" : "local-command-stderr";
@@ -294,6 +397,222 @@ function localCommandContext(pending) {
294
397
  function isSlashCommand(text) {
295
398
  return /^\s*\/[A-Za-z]/.test(text);
296
399
  }
400
+ function isLocalShellSource(input) {
401
+ return typeof input.subscribe === "function";
402
+ }
403
+ function localCommandEvent(text, uuid, shell) {
404
+ return {
405
+ type: "user_message",
406
+ message: {
407
+ role: "user",
408
+ content: text
409
+ },
410
+ parentToolUseId: null,
411
+ synthetic: true,
412
+ uuid,
413
+ ...shell ? { shell } : {}
414
+ };
415
+ }
416
+ function shellInlineText(shell, text) {
417
+ const head = inlineHead(text);
418
+ const body = [`$ ${shell.command}`, ...head.lines];
419
+ if (head.more) body.push(INLINE_MORE);
420
+ const end = shellEndLine(shell);
421
+ if (end) body.push(end);
422
+ return frame(shellTag(shell), body);
423
+ }
424
+ function shellContextText(shell, text, options = {}) {
425
+ const body = [`$ ${shell.command}`];
426
+ if (shell.status === "running") {
427
+ body.push(runningStatusLine(shell, text, options));
428
+ if (!options.collapsed && !options.previous) {
429
+ const part = headTail(text, TAIL_ONLY);
430
+ const shown = part.omittedLines > 0 ? part.tail : part.head;
431
+ if (part.omittedLines > 0) body.push(`[last ${count(part.totalLines - part.omittedLines)} of ${count(part.totalLines)} lines]`);
432
+ if (shown) body.push(shown);
433
+ }
434
+ } else if (options.collapsed) {
435
+ const part = headTail(text, {
436
+ headLines: 0,
437
+ headChars: 0,
438
+ tailLines: 0,
439
+ tailChars: 0
440
+ });
441
+ if (part.totalLines > 0) body.push(omissionLine(shell, part.totalLines, part.totalLines, part.totalBytes));
442
+ } else {
443
+ const part = headTail(text, HEAD_AND_TAIL);
444
+ if (part.head) body.push(part.head);
445
+ if (part.omittedLines > 0) body.push(omissionLine(shell, part.omittedLines, part.totalLines, part.totalBytes));
446
+ if (part.tail) body.push(part.tail);
447
+ }
448
+ const end = shellEndLine(shell);
449
+ if (end) body.push(end);
450
+ return frame(shellTag(shell), body);
451
+ }
452
+ var LocalCommandQueue = class {
453
+ #entries = [];
454
+ #emit;
455
+ constructor(emit) {
456
+ this.#emit = emit;
457
+ }
458
+ push(input) {
459
+ if (!isLocalShellSource(input)) {
460
+ const text = localCommandTranscript(input);
461
+ this.#entries.push({
462
+ kind: "text",
463
+ text
464
+ });
465
+ this.#emit(text, randomUUID(), void 0);
466
+ return;
467
+ }
468
+ const uuid = randomUUID();
469
+ const row = () => {
470
+ const info = { ...input.info() };
471
+ this.#emit(shellInlineText(info, input.text()), uuid, info);
472
+ };
473
+ row();
474
+ const unsubscribe = input.subscribe(row);
475
+ this.#entries.push({
476
+ kind: "shell",
477
+ source: input,
478
+ unsubscribe
479
+ });
480
+ }
481
+ take() {
482
+ if (this.#entries.length === 0) return;
483
+ const rendered = this.#entries.map((entry) => render(entry));
484
+ const texts = capped(rendered);
485
+ const remaining = [];
486
+ this.#entries.forEach((entry, i) => {
487
+ if (entry.kind === "text") return;
488
+ if (rendered[i].settled) {
489
+ entry.unsubscribe();
490
+ return;
491
+ }
492
+ entry.flushedLines = rendered[i].lines;
493
+ remaining.push(entry);
494
+ });
495
+ this.#entries = remaining;
496
+ return localCommandContext(texts);
497
+ }
498
+ materialize() {
499
+ return capped(this.#entries.map((entry) => render(entry)));
500
+ }
501
+ restore(texts) {
502
+ for (const text of texts) this.#entries.push({
503
+ kind: "text",
504
+ text
505
+ });
506
+ }
507
+ clear() {
508
+ for (const entry of this.#entries) if (entry.kind === "shell") entry.unsubscribe();
509
+ this.#entries = [];
510
+ }
511
+ };
512
+ function render(entry) {
513
+ if (entry.kind === "text") return {
514
+ full: entry.text,
515
+ settled: true,
516
+ lines: 0
517
+ };
518
+ const info = entry.source.info();
519
+ const text = entry.source.text();
520
+ const previous = entry.flushedLines === void 0 ? void 0 : { lines: entry.flushedLines };
521
+ return {
522
+ full: shellContextText(info, text, { previous }),
523
+ collapsed: () => shellContextText(info, text, {
524
+ previous,
525
+ collapsed: true
526
+ }),
527
+ settled: info.status !== "running",
528
+ lines: countLines(text)
529
+ };
530
+ }
531
+ function capped(rendered) {
532
+ const texts = Array.from({ length: rendered.length });
533
+ let budget = SHELL_CONTEXT_FLUSH_MAX_CHARS;
534
+ for (let i = rendered.length - 1; i >= 0; i--) {
535
+ const entry = rendered[i];
536
+ const text = entry.collapsed === void 0 || entry.full.length <= budget ? entry.full : entry.collapsed();
537
+ texts[i] = text;
538
+ budget -= text.length;
539
+ }
540
+ return texts;
541
+ }
542
+ function frame(tag, body) {
543
+ return `<${tag}>${body.join("\n")}</${tag}>`;
544
+ }
545
+ function inlineHead(text) {
546
+ const total = text.length;
547
+ const window = total > INLINE_SCAN_CHARS ? text.slice(0, INLINE_SCAN_CHARS) : text;
548
+ const lines = [];
549
+ let at = 0;
550
+ let chars = 0;
551
+ while (lines.length < SHELL_INLINE_LINES && at < total) {
552
+ const segment = window.slice(at, at + SHELL_INLINE_LINE_CHARS + 1);
553
+ const newline = segment.indexOf("\n");
554
+ const clipped = newline === -1 && segment.length > SHELL_INLINE_LINE_CHARS;
555
+ const line = clipped ? segment.slice(0, SHELL_INLINE_LINE_CHARS) + INLINE_CLIPPED : newline === -1 ? segment : segment.slice(0, newline);
556
+ const next = chars + line.length + (lines.length > 0 ? 1 : 0);
557
+ if (lines.length > 0 && next > SHELL_INLINE_CHARS) return {
558
+ lines,
559
+ more: true
560
+ };
561
+ lines.push(line);
562
+ chars = next;
563
+ if (clipped) return {
564
+ lines,
565
+ more: true
566
+ };
567
+ at = newline === -1 ? total : at + newline + 1;
568
+ }
569
+ return {
570
+ lines,
571
+ more: at < total
572
+ };
573
+ }
574
+ function shellTag(shell) {
575
+ return shell.status === "running" || shell.exitCode === 0 ? "local-command-stdout" : "local-command-stderr";
576
+ }
577
+ function shellName(shell) {
578
+ return `shell #${shell.ordinal} (${shell.id})`;
579
+ }
580
+ function shellEndLine(shell) {
581
+ if (shell.status === "running" || shell.exitCode === 0) return;
582
+ if (typeof shell.exitCode === "number") return `[exit ${shell.exitCode}]`;
583
+ switch (shell.endReason) {
584
+ case "killed": return "[killed]";
585
+ case "timeout": return "[timed out]";
586
+ case "server_stopped": return "[killed: the gateway stopped]";
587
+ case "server_restarted": return "[ended: the gateway restarted; the process may still be running]";
588
+ case "spawn_failed": return "[failed to start]";
589
+ default: return "[ended]";
590
+ }
591
+ }
592
+ function omissionLine(shell, omitted, total, bytes) {
593
+ const span = omitted === total ? `all ${count(total)} lines omitted (${size(bytes)})` : `${count(omitted)} of ${count(total)} lines omitted (${size(bytes)} in all)`;
594
+ return `[${shellName(shell)}: ${span}; ${OUTPUT_POINTER}]`;
595
+ }
596
+ function runningStatusLine(shell, text, options) {
597
+ const lines = countLines(text);
598
+ const progress = options.previous ? lines > options.previous.lines ? `${count(lines - options.previous.lines)} new lines since the last message` : "no new lines since the last message" : `${count(lines)} lines so far`;
599
+ return `[${shellName(shell)} is still running as of the time this was written (started ${elapsed(Date.now() - shell.startedAt)} ago, ${progress}). Nothing from it streams into this conversation; ${OUTPUT_POINTER}.]`;
600
+ }
601
+ function count(n) {
602
+ return n.toLocaleString("en-US");
603
+ }
604
+ function size(bytes) {
605
+ if (bytes < 1024) return `${bytes} B`;
606
+ if (bytes < 1048576) return `${Math.round(bytes / 1024)} KiB`;
607
+ return `${(bytes / 1048576).toFixed(1)} MiB`;
608
+ }
609
+ function elapsed(ms) {
610
+ const seconds = Math.max(0, Math.floor(ms / 1e3));
611
+ if (seconds < 60) return `${seconds}s`;
612
+ const minutes = Math.floor(seconds / 60);
613
+ if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
614
+ return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
615
+ }
297
616
  //#endregion
298
617
  //#region src/lib/patch.ts
299
618
  const MAX_PATCH_LINES = 400;
@@ -774,23 +1093,36 @@ var EventLog = class {
774
1093
  };
775
1094
  this.#lastActivityAt = event.ts;
776
1095
  this.#fold(event);
777
- this.#events.push(event);
1096
+ this.#retain(event);
778
1097
  return event;
779
1098
  }
780
1099
  at(seq) {
781
1100
  return this.#events.find((event) => event.seq === seq);
782
1101
  }
783
1102
  restore(events, seq, lastActivityAt) {
784
- this.#events = [...events];
1103
+ this.#events = [];
785
1104
  this.#seq = seq;
786
1105
  this.#activityCount = 0;
787
1106
  this.#proseCount = 0;
788
1107
  this.#contextUsage = void 0;
789
1108
  this.#checklist = void 0;
790
1109
  this.#resetSeq = 0;
791
- for (const event of this.#events) this.#fold(event);
1110
+ for (const event of events) {
1111
+ this.#fold(event);
1112
+ this.#retain(event);
1113
+ }
792
1114
  this.#lastActivityAt = lastActivityAt;
793
1115
  }
1116
+ #retain(event) {
1117
+ const key = logCoalesceKey(event);
1118
+ if (key !== void 0) {
1119
+ for (let index = this.#events.length - 1; index >= 0; index--) if (logCoalesceKey(this.#events[index]) === key) {
1120
+ this.#events.splice(index, 1);
1121
+ break;
1122
+ }
1123
+ }
1124
+ this.#events.push(event);
1125
+ }
794
1126
  #fold(event) {
795
1127
  this.#activityCount += transcriptActivity(event);
796
1128
  this.#proseCount += transcriptProse(event);
@@ -1041,7 +1373,7 @@ async function runPeerTool(peers, from, name, args) {
1041
1373
  }
1042
1374
  case "peers_peek": {
1043
1375
  const input = z.object(PEER_TOOL_SHAPES.peers_peek.shape).safeParse(args ?? {});
1044
- if (!input.success) return invalidArguments(name, input.error);
1376
+ if (!input.success) return invalidArguments$1(name, input.error);
1045
1377
  const peek = await peers.peek(from, input.data.sessionId, { recent: input.data.recent });
1046
1378
  if (!peek) return {
1047
1379
  text: `no such session: ${input.data.sessionId}`,
@@ -1054,7 +1386,7 @@ async function runPeerTool(peers, from, name, args) {
1054
1386
  }
1055
1387
  case "peers_send": {
1056
1388
  const input = z.object(PEER_TOOL_SHAPES.peers_send.shape).safeParse(args ?? {});
1057
- if (!input.success) return invalidArguments(name, input.error);
1389
+ if (!input.success) return invalidArguments$1(name, input.error);
1058
1390
  const result = await peers.send(from, input.data.sessionId, input.data.text);
1059
1391
  if (!result.delivered) return {
1060
1392
  text: `not delivered: ${result.reason}`,
@@ -1074,7 +1406,7 @@ async function runPeerTool(peers, from, name, args) {
1074
1406
  };
1075
1407
  }
1076
1408
  }
1077
- function invalidArguments(name, error) {
1409
+ function invalidArguments$1(name, error) {
1078
1410
  return {
1079
1411
  text: `invalid arguments for ${name}: ${error.issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; ")}`,
1080
1412
  isError: true
@@ -1165,6 +1497,138 @@ function peerDirectoryHandle() {
1165
1497
  };
1166
1498
  }
1167
1499
  //#endregion
1500
+ //#region src/lib/shells.ts
1501
+ const SHELL_REFUSAL = "shell commands are not available on this session";
1502
+ const SHELL_DIRECTORY_SLOT = Symbol.for("workerdeck.shells.directory");
1503
+ const SHELL_TOOL_SHAPES = {
1504
+ shell_list: {
1505
+ description: "List the shell commands your user has run in this session with `$`: id, command, whether it is still running, its exit code and how much output it produced. Call this before shell_read to find the shell id.",
1506
+ shape: {}
1507
+ },
1508
+ shell_read: {
1509
+ description: "Read the end of one shell command's output, as a terminal would show it. Use it to answer \"what is the dev server printing now\" or to see more of a command whose output was summarised in the conversation. Reading never writes to the shell and never interrupts it.",
1510
+ shape: {
1511
+ shellId: z.string().describe("The shell id from shell_list"),
1512
+ tail: z.number().int().min(1).optional().describe(`How many trailing lines to return (default ${SHELL_READ_DEFAULT_LINES}, capped at ${SHELL_READ_MAX_LINES})`)
1513
+ }
1514
+ }
1515
+ };
1516
+ const SHELL_TOOL_NAMES = Object.keys(SHELL_TOOL_SHAPES);
1517
+ function shellToolSpecs() {
1518
+ return SHELL_TOOL_NAMES.map((name) => {
1519
+ const { description, shape } = SHELL_TOOL_SHAPES[name];
1520
+ return {
1521
+ name,
1522
+ description,
1523
+ inputSchema: z.toJSONSchema(z.object(shape))
1524
+ };
1525
+ });
1526
+ }
1527
+ function isShellToolName(name) {
1528
+ return Object.hasOwn(SHELL_TOOL_SHAPES, name);
1529
+ }
1530
+ function clampShellTail(tail) {
1531
+ if (tail === void 0 || !Number.isFinite(tail)) return SHELL_READ_DEFAULT_LINES;
1532
+ return Math.max(1, Math.min(Math.floor(tail), SHELL_READ_MAX_LINES));
1533
+ }
1534
+ function shellSummary(info) {
1535
+ return {
1536
+ id: info.id,
1537
+ ordinal: info.ordinal,
1538
+ command: info.command,
1539
+ label: info.label,
1540
+ cwd: info.cwd,
1541
+ owner: info.owner,
1542
+ status: info.status,
1543
+ startedAt: info.startedAt,
1544
+ endedAt: info.endedAt,
1545
+ exitCode: info.exitCode,
1546
+ endReason: info.endReason,
1547
+ bytes: info.bytes,
1548
+ capped: info.capped
1549
+ };
1550
+ }
1551
+ function shellTail(text, tail) {
1552
+ const body = text.endsWith("\n") ? text.slice(0, -1) : text;
1553
+ const all = body.length === 0 ? [] : body.split("\n");
1554
+ const kept = all.slice(Math.max(0, all.length - tail));
1555
+ return {
1556
+ text: kept.join("\n"),
1557
+ lines: kept.length,
1558
+ totalLines: all.length,
1559
+ truncated: kept.length < all.length
1560
+ };
1561
+ }
1562
+ async function runShellTool(shells, from, name, args) {
1563
+ if (!isShellToolName(name)) return {
1564
+ text: `unknown shell tool: ${name}`,
1565
+ isError: true
1566
+ };
1567
+ try {
1568
+ switch (name) {
1569
+ case "shell_list": {
1570
+ const rows = await shells.list(from);
1571
+ return {
1572
+ text: rows.length ? JSON.stringify(rows, null, 2) : "No shell commands have run in this session.",
1573
+ isError: false
1574
+ };
1575
+ }
1576
+ case "shell_read": {
1577
+ const input = z.object(SHELL_TOOL_SHAPES.shell_read.shape).safeParse(args ?? {});
1578
+ if (!input.success) return invalidArguments(name, input.error);
1579
+ const result = await shells.read(from, input.data.shellId, { tail: clampShellTail(input.data.tail) });
1580
+ if (!result) return {
1581
+ text: `no such shell: ${input.data.shellId}`,
1582
+ isError: true
1583
+ };
1584
+ return {
1585
+ text: shellReadText(result),
1586
+ isError: false
1587
+ };
1588
+ }
1589
+ }
1590
+ } catch (error) {
1591
+ return {
1592
+ text: error instanceof Error ? error.message : String(error),
1593
+ isError: true
1594
+ };
1595
+ }
1596
+ }
1597
+ function invalidArguments(name, error) {
1598
+ return {
1599
+ text: `invalid arguments for ${name}: ${error.issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; ")}`,
1600
+ isError: true
1601
+ };
1602
+ }
1603
+ function shellReadText(result) {
1604
+ const { shell } = result;
1605
+ const scope = result.truncated ? `showing the last ${result.lines} of ${result.totalLines} lines` : `showing all ${result.lines} lines`;
1606
+ const head = `[shell #${shell.ordinal} ${shell.id}: $ ${shell.command}, ${shellState(shell)}, ${scope}]`;
1607
+ return result.text ? `${head}\n${result.text}` : `${head}\n(no output)`;
1608
+ }
1609
+ function shellState(shell) {
1610
+ if (shell.status === "running") return "still running";
1611
+ if (shell.exitCode !== void 0) return `exited ${shell.exitCode}`;
1612
+ return `ended (${shell.endReason ?? "exit"})`;
1613
+ }
1614
+ function installShellDirectory(directory) {
1615
+ globalThis[SHELL_DIRECTORY_SLOT] = directory;
1616
+ }
1617
+ function installedShellDirectory() {
1618
+ return globalThis[SHELL_DIRECTORY_SLOT];
1619
+ }
1620
+ function shellDirectoryHandle() {
1621
+ const resolve = () => {
1622
+ const directory = installedShellDirectory();
1623
+ if (!directory) throw new Error(SHELL_REFUSAL);
1624
+ return directory;
1625
+ };
1626
+ return {
1627
+ list: async (from) => resolve().list(from),
1628
+ read: async (from, shellId, options) => resolve().read(from, shellId, options)
1629
+ };
1630
+ }
1631
+ //#endregion
1168
1632
  //#region src/engines/claude/subagents.ts
1169
1633
  const SPAWNER_NAMES = /* @__PURE__ */ new Set(["Task", "Agent"]);
1170
1634
  var SubagentTracker = class {
@@ -1372,7 +1836,7 @@ var SessionRunner = class {
1372
1836
  #cost = new CostLedger();
1373
1837
  #numTurns;
1374
1838
  #input = new InputQueue();
1375
- #pendingLocalCommands = [];
1839
+ #localCommands = new LocalCommandQueue((text, uuid, shell) => this.#emit(localCommandEvent(text, uuid, shell)));
1376
1840
  #compactionId;
1377
1841
  #compactionTurns = 0;
1378
1842
  #idleWhileCompacting = false;
@@ -1497,25 +1961,12 @@ var SessionRunner = class {
1497
1961
  ...options?.origin ? { origin: options.origin } : {}
1498
1962
  });
1499
1963
  }
1500
- queueLocalCommand(result) {
1964
+ queueLocalCommand(input) {
1501
1965
  if (this.#closed) throw new Error("session is closed");
1502
- const text = localCommandTranscript(result);
1503
- this.#pendingLocalCommands.push(text);
1504
- this.#emit({
1505
- type: "user_message",
1506
- message: {
1507
- role: "user",
1508
- content: text
1509
- },
1510
- parentToolUseId: null,
1511
- synthetic: true,
1512
- uuid: randomUUID()
1513
- });
1966
+ this.#localCommands.push(input);
1514
1967
  }
1515
1968
  #takeLocalCommands() {
1516
- const context = localCommandContext(this.#pendingLocalCommands);
1517
- this.#pendingLocalCommands = [];
1518
- return context;
1969
+ return this.#localCommands.take();
1519
1970
  }
1520
1971
  async mcpServers() {
1521
1972
  const query = this.#query;
@@ -1543,7 +1994,7 @@ var SessionRunner = class {
1543
1994
  }
1544
1995
  async clearContext() {
1545
1996
  if (this.#status === "closed" || this.#status === "failed") throw new Error("session is closed");
1546
- this.#pendingLocalCommands = [];
1997
+ this.#localCommands.clear();
1547
1998
  this.sendMessage("/clear");
1548
1999
  }
1549
2000
  async setPermissionMode(mode) {
@@ -1574,7 +2025,7 @@ var SessionRunner = class {
1574
2025
  close(reason = "client") {
1575
2026
  if (this.#closed) return;
1576
2027
  this.#closed = true;
1577
- this.#pendingLocalCommands = [];
2028
+ this.#localCommands.clear();
1578
2029
  for (const [id, pending] of this.#pending) this.#settleApproval(id, pending, {
1579
2030
  behavior: "deny",
1580
2031
  message: "Session closed"
@@ -1692,8 +2143,9 @@ var SessionRunner = class {
1692
2143
  #mcpServersOption() {
1693
2144
  const declared = this.#config.mcpServers;
1694
2145
  const peers = this.#config.peers;
1695
- if (!peers) return declared;
1696
- const tools = PEER_TOOL_NAMES.map((name) => tool(name, PEER_TOOL_SHAPES[name].description, PEER_TOOL_SHAPES[name].shape, async (args) => {
2146
+ const shells = this.#config.shells;
2147
+ if (!peers && !shells) return declared;
2148
+ const tools = [...peers ? PEER_TOOL_NAMES.map((name) => tool(name, PEER_TOOL_SHAPES[name].description, PEER_TOOL_SHAPES[name].shape, async (args) => {
1697
2149
  const output = await runPeerTool(peers, this.id, name, args);
1698
2150
  return {
1699
2151
  content: [{
@@ -1702,7 +2154,16 @@ var SessionRunner = class {
1702
2154
  }],
1703
2155
  isError: output.isError
1704
2156
  };
1705
- }));
2157
+ })) : [], ...shells ? SHELL_TOOL_NAMES.map((name) => tool(name, SHELL_TOOL_SHAPES[name].description, SHELL_TOOL_SHAPES[name].shape, async (args) => {
2158
+ const output = await runShellTool(shells, this.id, name, args);
2159
+ return {
2160
+ content: [{
2161
+ type: "text",
2162
+ text: output.text
2163
+ }],
2164
+ isError: output.isError
2165
+ };
2166
+ })) : []];
1706
2167
  return {
1707
2168
  ...declared,
1708
2169
  [PEER_MCP_SERVER]: createSdkMcpServer({
@@ -1769,7 +2230,7 @@ var SessionRunner = class {
1769
2230
  this.#emit(body);
1770
2231
  if (body.type === "conversation_reset") {
1771
2232
  this.#cost.rollover();
1772
- this.#pendingLocalCommands = [];
2233
+ this.#localCommands.clear();
1773
2234
  if (body.sdkSessionId) this.#sdkSessionId = body.sdkSessionId;
1774
2235
  this.#fetchContextUsage();
1775
2236
  }
@@ -2079,7 +2540,7 @@ var AiSdkRunner = class {
2079
2540
  #statusDetail;
2080
2541
  #permissionMode;
2081
2542
  #messages = [];
2082
- #pendingLocalCommands = [];
2543
+ #localCommands = new LocalCommandQueue((text, uuid, shell) => this.#emit(localCommandEvent(text, uuid, shell)));
2083
2544
  #pendingToolCalls = /* @__PURE__ */ new Map();
2084
2545
  #dispatched = /* @__PURE__ */ new Set();
2085
2546
  #turnChain = Promise.resolve();
@@ -2114,7 +2575,7 @@ var AiSdkRunner = class {
2114
2575
  if (!state || !Array.isArray(state.messages)) throw new Error("session snapshot is missing its provider-engine state");
2115
2576
  this.#log.restore(snapshot.events, snapshot.seq, state.lastActivityAt);
2116
2577
  this.#messages = [...state.messages];
2117
- this.#pendingLocalCommands = [...state.pendingLocalCommands ?? []];
2578
+ this.#localCommands.restore(state.pendingLocalCommands ?? []);
2118
2579
  for (const call of state.pendingToolCalls) {
2119
2580
  const legacy = call.deferred;
2120
2581
  this.#pendingToolCalls.set(call.toolCallId, legacy === void 0 ? call : {
@@ -2206,6 +2667,7 @@ var AiSdkRunner = class {
2206
2667
  const snapshot = this.#buildSnapshot();
2207
2668
  this.#parked = true;
2208
2669
  this.#subscribers.clear();
2670
+ this.#localCommands.clear();
2209
2671
  try {
2210
2672
  Promise.resolve(this.#config.onClose?.()).catch(() => {});
2211
2673
  } catch {}
@@ -2222,6 +2684,7 @@ var AiSdkRunner = class {
2222
2684
  toolName: call.toolName,
2223
2685
  expiresAt: call.expiresAt
2224
2686
  }));
2687
+ const pendingLocalCommands = this.#localCommands.materialize();
2225
2688
  const state = {
2226
2689
  messages: this.#messages,
2227
2690
  pendingToolCalls: [...this.#pendingToolCalls.values()],
@@ -2232,7 +2695,7 @@ var AiSdkRunner = class {
2232
2695
  model: this.#modelAlias,
2233
2696
  lastActivityAt: this.#log.lastActivityAt,
2234
2697
  parkedAt: Date.now(),
2235
- ...this.#pendingLocalCommands.length ? { pendingLocalCommands: [...this.#pendingLocalCommands] } : {}
2698
+ ...pendingLocalCommands.length ? { pendingLocalCommands } : {}
2236
2699
  };
2237
2700
  return {
2238
2701
  engine: "provider",
@@ -2255,8 +2718,7 @@ var AiSdkRunner = class {
2255
2718
  mediaType: normalizeMediaType(attachment.mediaType),
2256
2719
  filename: attachment.name
2257
2720
  }));
2258
- const context = localCommandContext(this.#pendingLocalCommands);
2259
- this.#pendingLocalCommands = [];
2721
+ const context = this.#localCommands.take();
2260
2722
  const content = files.length || context ? [
2261
2723
  ...context ? [{
2262
2724
  type: "text",
@@ -2285,21 +2747,10 @@ var AiSdkRunner = class {
2285
2747
  });
2286
2748
  this.#scheduleTurn();
2287
2749
  }
2288
- queueLocalCommand(result) {
2750
+ queueLocalCommand(input) {
2289
2751
  if (this.#parked) throw new Error("session is parked");
2290
2752
  if (this.#closed) throw new Error("session is closed");
2291
- const text = localCommandTranscript(result);
2292
- this.#pendingLocalCommands.push(text);
2293
- this.#emit({
2294
- type: "user_message",
2295
- message: {
2296
- role: "user",
2297
- content: text
2298
- },
2299
- parentToolUseId: null,
2300
- synthetic: true,
2301
- uuid: randomUUID()
2302
- });
2753
+ this.#localCommands.push(input);
2303
2754
  }
2304
2755
  resolveToolCall(toolCallId, output, options) {
2305
2756
  if (!this.#settlePendingCall(toolCallId, output, options?.isError === true)) return false;
@@ -2401,7 +2852,7 @@ var AiSdkRunner = class {
2401
2852
  if (this.#closed) throw new Error("session is closed");
2402
2853
  if (this.#pendingToolCalls.size > 0) throw new Error("cannot clear context while tool calls are outstanding");
2403
2854
  this.#messages = [];
2404
- this.#pendingLocalCommands = [];
2855
+ this.#localCommands.clear();
2405
2856
  this.#emit({ type: "conversation_reset" });
2406
2857
  });
2407
2858
  this.#turnChain = run.then(() => void 0, () => void 0);
@@ -2483,7 +2934,7 @@ var AiSdkRunner = class {
2483
2934
  if (this.#closed || this.#parked) return;
2484
2935
  this.#closed = true;
2485
2936
  this.#abort?.abort();
2486
- this.#pendingLocalCommands = [];
2937
+ this.#localCommands.clear();
2487
2938
  this.#pendingToolCalls.clear();
2488
2939
  this.#dispatched.clear();
2489
2940
  for (const { timer } of this.#pendingApprovals.values()) clearTimeout(timer);
@@ -3378,6 +3829,22 @@ function createToolContext(options) {
3378
3829
  })
3379
3830
  });
3380
3831
  }
3832
+ if (options.shells) {
3833
+ const shells = options.shells;
3834
+ const selfId = options.selfId ?? (() => options.sessionId);
3835
+ for (const name of SHELL_TOOL_NAMES) definitions.push({
3836
+ name,
3837
+ trust: "authoritative",
3838
+ tool: tool$1({
3839
+ description: SHELL_TOOL_SHAPES[name].description,
3840
+ inputSchema: z.object(SHELL_TOOL_SHAPES[name].shape),
3841
+ execute: async (args) => {
3842
+ const output = await runShellTool(shells, selfId(), name, args);
3843
+ return output.isError ? { error: output.text } : { result: output.text };
3844
+ }
3845
+ })
3846
+ });
3847
+ }
3381
3848
  if (options.search) {
3382
3849
  const search = options.search;
3383
3850
  definitions.push({
@@ -3738,6 +4205,7 @@ function createEngineSession(options) {
3738
4205
  webFetch,
3739
4206
  onFileDelivered: options.capabilities?.deliverFiles === false || !isGranted("deliverFiles") ? void 0 : (file) => runner?.emitFileDelivered(file),
3740
4207
  peers: options.config.peers,
4208
+ shells: options.config.shells,
3741
4209
  selfId: () => runner?.id ?? "pending"
3742
4210
  });
3743
4211
  const declaredServers = options.profile?.session?.mcpServers;
@@ -4818,7 +5286,7 @@ var CodexRunner = class {
4818
5286
  #planType;
4819
5287
  #resolvedEffort;
4820
5288
  #queue = [];
4821
- #pendingLocalCommands = [];
5289
+ #localCommands = new LocalCommandQueue((text, uuid, shell) => this.#emit(localCommandEvent(text, uuid, shell)));
4822
5290
  #turnChain = Promise.resolve();
4823
5291
  #activeTurn;
4824
5292
  #connection;
@@ -5077,28 +5545,16 @@ var CodexRunner = class {
5077
5545
  type: "text",
5078
5546
  text
5079
5547
  });
5080
- const context = localCommandContext(this.#pendingLocalCommands);
5081
- this.#pendingLocalCommands = [];
5548
+ const context = this.#localCommands.take();
5082
5549
  if (context) parts.unshift({
5083
5550
  type: "text",
5084
5551
  text: context
5085
5552
  });
5086
5553
  return withSkillItems(parts, this.#skillPaths);
5087
5554
  }
5088
- queueLocalCommand(result) {
5555
+ queueLocalCommand(input) {
5089
5556
  if (this.#closed) throw new Error("session is closed");
5090
- const text = localCommandTranscript(result);
5091
- this.#pendingLocalCommands.push(text);
5092
- this.#emit({
5093
- type: "user_message",
5094
- message: {
5095
- role: "user",
5096
- content: text
5097
- },
5098
- parentToolUseId: null,
5099
- synthetic: true,
5100
- uuid: randomUUID()
5101
- });
5557
+ this.#localCommands.push(input);
5102
5558
  }
5103
5559
  resolvePermission(requestId, decision) {
5104
5560
  const pending = this.#approvals.get(requestId);
@@ -5146,7 +5602,7 @@ var CodexRunner = class {
5146
5602
  message: "the conversation was cleared"
5147
5603
  }, "policy");
5148
5604
  this.#resumedHistory = void 0;
5149
- this.#pendingLocalCommands = [];
5605
+ this.#localCommands.clear();
5150
5606
  this.#emit({
5151
5607
  type: "conversation_reset",
5152
5608
  sdkSessionId: this.#resumableThreadId()
@@ -5200,7 +5656,7 @@ var CodexRunner = class {
5200
5656
  if (this.#closed) return;
5201
5657
  this.#closed = true;
5202
5658
  this.#queue.length = 0;
5203
- this.#pendingLocalCommands = [];
5659
+ this.#localCommands.clear();
5204
5660
  for (const [id, pending] of this.#approvals) this.#settleApproval(id, pending, {
5205
5661
  behavior: "deny",
5206
5662
  message: "Session closed"
@@ -5293,7 +5749,8 @@ var CodexRunner = class {
5293
5749
  };
5294
5750
  if (this.#model) options.model = this.#model;
5295
5751
  if (this.#instructions !== void 0) options.developerInstructions = this.#instructions;
5296
- if (this.#config.peers) options.dynamicTools = peerToolSpecs().map((spec) => ({
5752
+ const dynamic = [...this.#config.peers ? peerToolSpecs() : [], ...this.#config.shells ? shellToolSpecs() : []];
5753
+ if (dynamic.length) options.dynamicTools = dynamic.map((spec) => ({
5297
5754
  type: "function",
5298
5755
  ...spec
5299
5756
  }));
@@ -5715,16 +6172,30 @@ var CodexRunner = class {
5715
6172
  async #answerServerRequest(method, params, wireId) {
5716
6173
  const channel = APPROVAL_CHANNELS[method];
5717
6174
  if (channel) return this.#requestApproval(channel, method, params, wireId);
5718
- if (method === "item/tool/call" && this.#config.peers) {
6175
+ if (method === "item/tool/call") {
5719
6176
  const call = params;
5720
- const output = await runPeerTool(this.#config.peers, this.id, call.tool, call.arguments);
5721
- return {
5722
- success: !output.isError,
5723
- contentItems: [{
5724
- type: "inputText",
5725
- text: output.text
5726
- }]
5727
- };
6177
+ const peers = this.#config.peers;
6178
+ const shells = this.#config.shells;
6179
+ if (peers && isPeerToolName(call.tool)) {
6180
+ const output = await runPeerTool(peers, this.id, call.tool, call.arguments);
6181
+ return {
6182
+ success: !output.isError,
6183
+ contentItems: [{
6184
+ type: "inputText",
6185
+ text: output.text
6186
+ }]
6187
+ };
6188
+ }
6189
+ if (shells && isShellToolName(call.tool)) {
6190
+ const output = await runShellTool(shells, this.id, call.tool, call.arguments);
6191
+ return {
6192
+ success: !output.isError,
6193
+ contentItems: [{
6194
+ type: "inputText",
6195
+ text: output.text
6196
+ }]
6197
+ };
6198
+ }
5728
6199
  }
5729
6200
  throw new JsonRpcError(-32601, `workerdeck does not handle server request '${method}'`);
5730
6201
  }
@@ -6485,6 +6956,6 @@ function getEngineAdapter(engine) {
6485
6956
  return ADAPTERS[engine ?? "claude"];
6486
6957
  }
6487
6958
  //#endregion
6488
- export { AiSdkRunner, BrowserBridgeExecutor, CLAUDE_CATALOG, CODEX_CATALOG, CodexRunner, CostLedger, DeferredExecutor, InputQueue, JsonRpcError, JsonRpcStdioConnection, PEER_MCP_SERVER, PEER_MESSAGE_MAX_CHARS, PEER_RECENT_DEFAULT, PEER_RECENT_MAX, PEER_TOOL_NAMES, PendingRequestRegistry, QuickJsExecutor, SUPPORTED_ATTACHMENT_TYPES, SessionRunner, attachmentContentBlocks, attachmentKind, attachmentRef, checkClaudeAuth, claudeAdapter, codexAdapter, composeInstructions, connectAppServer, connectMcpTools, createEngineSession, createToolContext, createWebFetch, getEngineAdapter, htmlToMarkdown, installPeerDirectory, installedPeerDirectory, isHostAllowed, isPrivateAddress, isSlashCommand, listCodexSessions, localCommandContext, localCommandTranscript, mcpStatusInfo, modelOptionsFromSdk, normalizeMediaType, normalizeSdkMessage, peerDirectoryHandle, peerMentionsEnvelope, peerMessageEnvelope, peerSummary, peerToolSpecs, providerAdapter, recentLines, replaySlice, resolveBundledClaudeExecutable, resolveBundledCodexExecutable, resolveInstructions, runPeerTool, toApiMessage, toExecutionResult, truncateResultBlocks, withHostTools, withMcpTools, withPeerContext };
6959
+ export { AiSdkRunner, BrowserBridgeExecutor, CLAUDE_CATALOG, CODEX_CATALOG, CodexRunner, CostLedger, DeferredExecutor, InputQueue, JsonRpcError, JsonRpcStdioConnection, LocalCommandQueue, PEER_MCP_SERVER, PEER_MESSAGE_MAX_CHARS, PEER_RECENT_DEFAULT, PEER_RECENT_MAX, PEER_TOOL_NAMES, PendingRequestRegistry, QuickJsExecutor, SHELL_REFUSAL, SHELL_TOOL_NAMES, SHELL_TOOL_SHAPES, SUPPORTED_ATTACHMENT_TYPES, SessionRunner, attachmentContentBlocks, attachmentKind, attachmentRef, checkClaudeAuth, clampShellTail, claudeAdapter, codexAdapter, composeInstructions, connectAppServer, connectMcpTools, countLines, createEngineSession, createToolContext, createWebFetch, getEngineAdapter, headTail, htmlToMarkdown, installPeerDirectory, installShellDirectory, installedPeerDirectory, installedShellDirectory, isHostAllowed, isLocalShellSource, isPrivateAddress, isShellToolName, isSlashCommand, listCodexSessions, localCommandContext, localCommandEvent, localCommandTranscript, mcpStatusInfo, modelOptionsFromSdk, normalizeMediaType, normalizeSdkMessage, peerDirectoryHandle, peerMentionsEnvelope, peerMessageEnvelope, peerSummary, peerToolSpecs, providerAdapter, recentLines, replaySlice, resolveBundledClaudeExecutable, resolveBundledCodexExecutable, resolveInstructions, runPeerTool, runShellTool, shellContextText, shellDirectoryHandle, shellInlineText, shellReadText, shellSummary, shellTail, shellToolSpecs, splitLines, toApiMessage, toExecutionResult, truncateResultBlocks, ttyText, withHostTools, withMcpTools, withPeerContext };
6489
6960
 
6490
6961
  //# sourceMappingURL=index.mjs.map