@workerdeck/core 2.15.0 → 3.0.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.
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, 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";
4
+ import { CHECKLIST_TEXT_MAX, ENGINE_CAPABILITIES, PEER_MENTION_MAX, PROTOCOL_VERSION, SHELL_COMMAND_MAX, 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_INPUT_MAX, SHELL_INTERACTIVE_NOTE, SHELL_READ_DEFAULT_LINES, SHELL_READ_MAX_LINES, SHELL_WAIT_DEFAULT_MS, SHELL_WAIT_MAX_MS, 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";
@@ -281,9 +281,14 @@ const STRING_SEQUENCE = /(?:\x1b[PX^_]|[\x90\x98\x9e\x9f])[\s\S]*?(?:\x1b\\|\x9c
281
281
  const CSI_SEQUENCE = /(?:\x1b\[|\x9b)[0-?]*[ -/]*[@-~]/g;
282
282
  const ESCAPE_SEQUENCE = /\x1b[ -/]*[0-~]/g;
283
283
  const CONTROL_CHARS = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g;
284
+ const REDRAW_SEQUENCE = /(?:\x1b\[|\x9b)(?:\?(?:1049|1047|47)h|\d*[AF]|\d+;\d+[Hf]|\d+d)/;
285
+ const TTY_REDRAW_CARRY = 16;
284
286
  function ttyText(raw) {
285
287
  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
288
  }
289
+ function ttyRedraws(raw) {
290
+ return REDRAW_SEQUENCE.test(raw);
291
+ }
287
292
  function splitLines(text) {
288
293
  if (text === "") return [];
289
294
  return (text.endsWith("\n") ? text.slice(0, -1) : text).split("\n");
@@ -414,9 +419,13 @@ function localCommandEvent(text, uuid, shell) {
414
419
  };
415
420
  }
416
421
  function shellInlineText(shell, text) {
417
- const head = inlineHead(text);
418
- const body = [`$ ${shell.command}`, ...head.lines];
419
- if (head.more) body.push(INLINE_MORE);
422
+ const body = [`$ ${shell.command}`];
423
+ if (shell.interactive) body.push(`[${SHELL_INTERACTIVE_NOTE}]`);
424
+ else {
425
+ const head = inlineHead(text);
426
+ body.push(...head.lines);
427
+ if (head.more) body.push(INLINE_MORE);
428
+ }
420
429
  const end = shellEndLine(shell);
421
430
  if (end) body.push(end);
422
431
  return frame(shellTag(shell), body);
@@ -451,6 +460,7 @@ function shellContextText(shell, text, options = {}) {
451
460
  }
452
461
  var LocalCommandQueue = class {
453
462
  #entries = [];
463
+ #rows = /* @__PURE__ */ new Set();
454
464
  #emit;
455
465
  constructor(emit) {
456
466
  this.#emit = emit;
@@ -470,6 +480,22 @@ var LocalCommandQueue = class {
470
480
  const info = { ...input.info() };
471
481
  this.#emit(shellInlineText(info, input.text()), uuid, info);
472
482
  };
483
+ if (input.info().owner === "agent") {
484
+ const settle = () => {
485
+ if (input.info().status !== "running") {
486
+ unsubscribe();
487
+ this.#rows.delete(unsubscribe);
488
+ }
489
+ };
490
+ const unsubscribe = input.subscribe(() => {
491
+ row();
492
+ settle();
493
+ });
494
+ this.#rows.add(unsubscribe);
495
+ row();
496
+ settle();
497
+ return;
498
+ }
473
499
  row();
474
500
  const unsubscribe = input.subscribe(row);
475
501
  this.#entries.push({
@@ -507,6 +533,8 @@ var LocalCommandQueue = class {
507
533
  clear() {
508
534
  for (const entry of this.#entries) if (entry.kind === "shell") entry.unsubscribe();
509
535
  this.#entries = [];
536
+ for (const unsubscribe of this.#rows) unsubscribe();
537
+ this.#rows.clear();
510
538
  }
511
539
  };
512
540
  function render(entry) {
@@ -1499,23 +1527,125 @@ function peerDirectoryHandle() {
1499
1527
  //#endregion
1500
1528
  //#region src/lib/shells.ts
1501
1529
  const SHELL_REFUSAL = "shell commands are not available on this session";
1530
+ const SHELL_WRITE_REFUSAL = "the agent may not start, type into or kill shells on this session (shell.agentWrite is read-only)";
1531
+ const SHELL_KEYS_MAX = 64;
1502
1532
  const SHELL_DIRECTORY_SLOT = Symbol.for("workerdeck.shells.directory");
1533
+ const CTRL_LETTERS = "abcdefghijklmnopqrstuvwxyz";
1534
+ const NAMED_KEYS = {
1535
+ enter: "\r",
1536
+ return: "\r",
1537
+ tab: " ",
1538
+ "shift-tab": "\x1B[Z",
1539
+ escape: "\x1B",
1540
+ esc: "\x1B",
1541
+ backspace: "",
1542
+ delete: "\x1B[3~",
1543
+ insert: "\x1B[2~",
1544
+ pageup: "\x1B[5~",
1545
+ pagedown: "\x1B[6~",
1546
+ space: " ",
1547
+ f1: "\x1BOP",
1548
+ f2: "\x1BOQ",
1549
+ f3: "\x1BOR",
1550
+ f4: "\x1BOS",
1551
+ f5: "\x1B[15~",
1552
+ f6: "\x1B[17~",
1553
+ f7: "\x1B[18~",
1554
+ f8: "\x1B[19~",
1555
+ f9: "\x1B[20~",
1556
+ f10: "\x1B[21~",
1557
+ f11: "\x1B[23~",
1558
+ f12: "\x1B[24~"
1559
+ };
1560
+ const CURSOR_KEYS = {
1561
+ up: "A",
1562
+ down: "B",
1563
+ right: "C",
1564
+ left: "D",
1565
+ home: "H",
1566
+ end: "F"
1567
+ };
1568
+ const SHELL_KEY_NAMES = [
1569
+ "enter",
1570
+ "tab",
1571
+ "shift-tab",
1572
+ "escape",
1573
+ "backspace",
1574
+ "delete",
1575
+ "insert",
1576
+ "up",
1577
+ "down",
1578
+ "left",
1579
+ "right",
1580
+ "home",
1581
+ "end",
1582
+ "pageup",
1583
+ "pagedown",
1584
+ "space",
1585
+ "f1 to f12",
1586
+ "ctrl-a to ctrl-z",
1587
+ "any single printable character"
1588
+ ];
1589
+ const waitForShape = z.union([z.string().min(1), z.array(z.string().min(1)).min(1).max(8)]).optional().describe("Wait until this text (or any of these) appears, the shell exits, or timeoutMs passes, then return the view. A plain case-sensitive substring, not a pattern.");
1590
+ const timeoutShape = z.number().int().min(0).max(SHELL_WAIT_MAX_MS).optional().describe(`With waitFor: how long to wait (default ${SHELL_WAIT_DEFAULT_MS}, at most ${SHELL_WAIT_MAX_MS})`);
1503
1591
  const SHELL_TOOL_SHAPES = {
1504
1592
  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.",
1593
+ description: "List the shells of this session: the commands your user ran with `$` and the ones you started with shell_run. Each has an id, a command, an owner (user or agent), whether it is still running, its exit code, how much output it produced and whether it is interactive (a program that redraws the screen). Call this before shell_read, shell_write or shell_kill to find the shell id.",
1506
1594
  shape: {}
1507
1595
  },
1508
1596
  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.",
1597
+ description: "Read one shell's output. Use it to answer \"what is the dev server printing now\", to see more of a command whose output was summarised in the conversation, or with waitFor to block until a server is ready. Reading never writes to the shell and never interrupts it.",
1510
1598
  shape: {
1511
1599
  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})`)
1600
+ view: z.enum(["lines", "screen"]).optional().describe("lines: the trailing lines of the output as plain text. screen: what the terminal shows right now, the only readable view of a program that redraws (a TUI, a dev server dashboard, a multi-line progress display). Defaults to screen for an interactive shell and lines otherwise."),
1601
+ tail: z.number().int().min(1).optional().describe(`With view lines: how many trailing lines to return (default ${SHELL_READ_DEFAULT_LINES}, capped at ${SHELL_READ_MAX_LINES})`),
1602
+ waitFor: z.union([z.string().min(1), z.array(z.string().min(1)).min(1).max(8)]).optional().describe("Wait until the view contains this text (or any of these), the shell exits, or timeoutMs passes, then read. A plain case-sensitive substring, not a pattern. Text already there counts, so a server that was ready before the call returns at once."),
1603
+ timeoutMs: timeoutShape
1604
+ }
1605
+ },
1606
+ shell_run: {
1607
+ description: "Start a shell command of your own in the session's working directory, in a real terminal (a PTY) that keeps running after this call returns: a dev server, a watcher, a TUI. The result names the new shell id; use shell_read to watch it, shell_write to type into it and shell_kill to stop it. With waitFor the call blocks until the output contains that text (or the command exits or timeoutMs passes), so \"start vite and wait for 'ready in'\" is one call. This needs the user's approval and every shell you start is visible to them. For a command that just runs and finishes, prefer your ordinary shell tool.",
1608
+ shape: {
1609
+ command: z.string().min(1).max(SHELL_COMMAND_MAX).refine((command) => command.trim() !== "" && !command.includes("\0"), "command must be non-empty text").describe("The command line, run by the user's login shell in the session's working directory"),
1610
+ waitFor: waitForShape,
1611
+ timeoutMs: timeoutShape
1612
+ }
1613
+ },
1614
+ shell_write: {
1615
+ description: `Type into a shell you started with shell_run: literal text in data, named keys in keys, or both (data is sent first, then the keys in order). To press Enter use keys ["enter"]; a "\\n" in data ends a line only for line-mode programs, not for a TUI. Key names: ${SHELL_KEY_NAMES.join(", ")}. With waitFor the call blocks until the shell prints that text AFTER your keystrokes (earlier output never counts), then returns the view, so "press r and wait for 'reloaded'" is one call. Typing into a shell the user started is refused unless they granted it through shell_request_write.`,
1616
+ shape: {
1617
+ shellId: z.string().describe("The shell id from shell_run or shell_list"),
1618
+ data: z.string().max(SHELL_INPUT_MAX).optional().describe("Literal text to type, sent as-is"),
1619
+ keys: z.array(z.string().min(1)).min(1).max(64).optional().describe("Named keys to press after data, in order, e.g. [\"r\"], [\"enter\"], [\"ctrl-c\"], [\"down\", \"down\", \"enter\"]"),
1620
+ waitFor: waitForShape,
1621
+ timeoutMs: timeoutShape
1622
+ }
1623
+ },
1624
+ shell_kill: {
1625
+ description: "Stop a shell you started with shell_run, killing its whole process tree. Use it when the user asks you to stop the server or when you are done with it. Killing a shell the user started is refused unless they granted it through shell_request_write.",
1626
+ shape: { shellId: z.string().describe("The shell id from shell_run or shell_list") }
1627
+ },
1628
+ shell_request_write: {
1629
+ description: "Ask the user to let you type into (and kill) a shell THEY started with `$`, for example to answer a prompt in a script or to drive a TUI they launched. The user sees the shell and your reason and allows or denies; once allowed, shell_write and shell_kill work on it until the shell ends or the user revokes the grant. Never needed for a shell you started with shell_run.",
1630
+ shape: {
1631
+ shellId: z.string().describe("The id of a running shell the user started, from shell_list"),
1632
+ reason: z.string().min(1).max(500).describe("One sentence the user reads on the approval card: what you will type and why")
1513
1633
  }
1514
1634
  }
1515
1635
  };
1516
1636
  const SHELL_TOOL_NAMES = Object.keys(SHELL_TOOL_SHAPES);
1517
- function shellToolSpecs() {
1518
- return SHELL_TOOL_NAMES.map((name) => {
1637
+ const SHELL_READ_TOOL_NAMES = ["shell_list", "shell_read"];
1638
+ const SHELL_WRITE_TOOL_NAMES = [
1639
+ "shell_run",
1640
+ "shell_write",
1641
+ "shell_kill",
1642
+ "shell_request_write"
1643
+ ];
1644
+ function shellToolNames(write) {
1645
+ return write ? [...SHELL_READ_TOOL_NAMES, ...SHELL_WRITE_TOOL_NAMES] : [...SHELL_READ_TOOL_NAMES];
1646
+ }
1647
+ function shellToolSpecs(write = false) {
1648
+ return shellToolNames(write).map((name) => {
1519
1649
  const { description, shape } = SHELL_TOOL_SHAPES[name];
1520
1650
  return {
1521
1651
  name,
@@ -1527,10 +1657,59 @@ function shellToolSpecs() {
1527
1657
  function isShellToolName(name) {
1528
1658
  return Object.hasOwn(SHELL_TOOL_SHAPES, name);
1529
1659
  }
1660
+ function isShellWriteToolName(name) {
1661
+ return SHELL_WRITE_TOOL_NAMES.includes(name);
1662
+ }
1663
+ function shellWriteToolOf(toolName) {
1664
+ const bare = toolName.startsWith("mcp__") ? toolName.slice(toolName.indexOf("__", 5) + 2) : toolName;
1665
+ return isShellWriteToolName(bare) ? bare : void 0;
1666
+ }
1667
+ function shellToolOf(toolName) {
1668
+ const bare = toolName.startsWith("mcp__") ? toolName.slice(toolName.indexOf("__", 5) + 2) : toolName;
1669
+ return isShellToolName(bare) ? bare : void 0;
1670
+ }
1671
+ function shellToolNeedsCard(toolName, agentWrite) {
1672
+ const tool = shellWriteToolOf(toolName);
1673
+ if (tool === void 0 || agentWrite === void 0) return false;
1674
+ return agentWrite === "gated" || tool === "shell_request_write";
1675
+ }
1530
1676
  function clampShellTail(tail) {
1531
1677
  if (tail === void 0 || !Number.isFinite(tail)) return SHELL_READ_DEFAULT_LINES;
1532
1678
  return Math.max(1, Math.min(Math.floor(tail), SHELL_READ_MAX_LINES));
1533
1679
  }
1680
+ function agentMayWrite(shell, from) {
1681
+ return shell.sessionId === from && (shell.owner === "agent" || shell.agentWrite === true);
1682
+ }
1683
+ function shellOwnershipRefusal(shellId) {
1684
+ return `shell ${shellId} was started by the user; the agent may only type into or kill shells it started, or one the user granted through shell_request_write`;
1685
+ }
1686
+ function shellWriteDeniedText(name, message) {
1687
+ return `the user denied this ${name}${message ? `: ${message}` : ""}`;
1688
+ }
1689
+ function encodeShellKey(key, options = {}) {
1690
+ if ([...key].length === 1) {
1691
+ const code = key.codePointAt(0);
1692
+ return code < 32 || code === 127 ? void 0 : key;
1693
+ }
1694
+ const name = key.toLowerCase().replace(/\+/g, "-").replace(/^control-/, "ctrl-").replace(/^c-/, "ctrl-");
1695
+ const named = NAMED_KEYS[name];
1696
+ if (named !== void 0) return named;
1697
+ const cursor = CURSOR_KEYS[name];
1698
+ if (cursor !== void 0) return `\x1b${options.applicationCursorKeys ? "O" : "["}${cursor}`;
1699
+ if (name.startsWith("ctrl-") && name.length === 6) {
1700
+ const index = CTRL_LETTERS.indexOf(name[5]);
1701
+ if (index !== -1) return String.fromCharCode(index + 1);
1702
+ }
1703
+ }
1704
+ function encodeShellKeys(keys, options = {}) {
1705
+ let out = "";
1706
+ for (const key of keys) {
1707
+ const encoded = encodeShellKey(key, options);
1708
+ if (encoded === void 0) throw new Error(`unknown key ${JSON.stringify(key)}; use one of ${SHELL_KEY_NAMES.join(", ")}`);
1709
+ out += encoded;
1710
+ }
1711
+ return out;
1712
+ }
1534
1713
  function shellSummary(info) {
1535
1714
  return {
1536
1715
  id: info.id,
@@ -1545,7 +1724,9 @@ function shellSummary(info) {
1545
1724
  exitCode: info.exitCode,
1546
1725
  endReason: info.endReason,
1547
1726
  bytes: info.bytes,
1548
- capped: info.capped
1727
+ capped: info.capped,
1728
+ interactive: info.interactive,
1729
+ agentWrite: info.agentWrite
1549
1730
  };
1550
1731
  }
1551
1732
  function shellTail(text, tail) {
@@ -1553,17 +1734,22 @@ function shellTail(text, tail) {
1553
1734
  const all = body.length === 0 ? [] : body.split("\n");
1554
1735
  const kept = all.slice(Math.max(0, all.length - tail));
1555
1736
  return {
1737
+ view: "lines",
1556
1738
  text: kept.join("\n"),
1557
1739
  lines: kept.length,
1558
1740
  totalLines: all.length,
1559
1741
  truncated: kept.length < all.length
1560
1742
  };
1561
1743
  }
1562
- async function runShellTool(shells, from, name, args) {
1744
+ async function runShellTool(shells, from, name, args, options = {}) {
1563
1745
  if (!isShellToolName(name)) return {
1564
1746
  text: `unknown shell tool: ${name}`,
1565
1747
  isError: true
1566
1748
  };
1749
+ if (isShellWriteToolName(name) && options.write !== true) return {
1750
+ text: SHELL_WRITE_REFUSAL,
1751
+ isError: true
1752
+ };
1567
1753
  try {
1568
1754
  switch (name) {
1569
1755
  case "shell_list": {
@@ -1576,7 +1762,13 @@ async function runShellTool(shells, from, name, args) {
1576
1762
  case "shell_read": {
1577
1763
  const input = z.object(SHELL_TOOL_SHAPES.shell_read.shape).safeParse(args ?? {});
1578
1764
  if (!input.success) return invalidArguments(name, input.error);
1579
- const result = await shells.read(from, input.data.shellId, { tail: clampShellTail(input.data.tail) });
1765
+ const { shellId, view, tail, waitFor, timeoutMs } = input.data;
1766
+ const result = await shells.read(from, shellId, {
1767
+ tail: clampShellTail(tail),
1768
+ view,
1769
+ waitFor: needles(waitFor),
1770
+ timeoutMs
1771
+ });
1580
1772
  if (!result) return {
1581
1773
  text: `no such shell: ${input.data.shellId}`,
1582
1774
  isError: true
@@ -1586,6 +1778,76 @@ async function runShellTool(shells, from, name, args) {
1586
1778
  isError: false
1587
1779
  };
1588
1780
  }
1781
+ case "shell_run": {
1782
+ const input = z.object(SHELL_TOOL_SHAPES.shell_run.shape).safeParse(args ?? {});
1783
+ if (!input.success) return invalidArguments(name, input.error);
1784
+ const { command, waitFor, timeoutMs } = input.data;
1785
+ return {
1786
+ text: shellReadText(await shells.run(from, {
1787
+ command,
1788
+ waitFor: needles(waitFor),
1789
+ timeoutMs
1790
+ }), "started"),
1791
+ isError: false
1792
+ };
1793
+ }
1794
+ case "shell_write": {
1795
+ const input = z.object(SHELL_TOOL_SHAPES.shell_write.shape).safeParse(args ?? {});
1796
+ if (!input.success) return invalidArguments(name, input.error);
1797
+ const { shellId, data, keys, waitFor, timeoutMs } = input.data;
1798
+ if (!data && !keys) return {
1799
+ text: `invalid arguments for ${name}: give data, keys or both`,
1800
+ isError: true
1801
+ };
1802
+ try {
1803
+ encodeShellKeys(keys ?? []);
1804
+ } catch (error) {
1805
+ return {
1806
+ text: `invalid arguments for ${name}: ${error instanceof Error ? error.message : String(error)}`,
1807
+ isError: true
1808
+ };
1809
+ }
1810
+ const result = await shells.write(from, shellId, {
1811
+ data,
1812
+ keys,
1813
+ waitFor: needles(waitFor),
1814
+ timeoutMs
1815
+ });
1816
+ if (!result) return {
1817
+ text: `no such shell: ${shellId}`,
1818
+ isError: true
1819
+ };
1820
+ return {
1821
+ text: shellReadText(result, "typed"),
1822
+ isError: false
1823
+ };
1824
+ }
1825
+ case "shell_kill": {
1826
+ const input = z.object(SHELL_TOOL_SHAPES.shell_kill.shape).safeParse(args ?? {});
1827
+ if (!input.success) return invalidArguments(name, input.error);
1828
+ const result = await shells.kill(from, input.data.shellId);
1829
+ if (!result) return {
1830
+ text: `no such shell: ${input.data.shellId}`,
1831
+ isError: true
1832
+ };
1833
+ return {
1834
+ text: shellKillText(result),
1835
+ isError: false
1836
+ };
1837
+ }
1838
+ case "shell_request_write": {
1839
+ const input = z.object(SHELL_TOOL_SHAPES.shell_request_write.shape).safeParse(args ?? {});
1840
+ if (!input.success) return invalidArguments(name, input.error);
1841
+ const shell = await shells.grant(from, input.data.shellId);
1842
+ if (!shell) return {
1843
+ text: `no such shell: ${input.data.shellId}`,
1844
+ isError: true
1845
+ };
1846
+ return {
1847
+ text: `granted: you may now type into and kill shell #${shell.ordinal} ${shell.id} ($ ${shell.command}) until it ends or the user revokes it`,
1848
+ isError: false
1849
+ };
1850
+ }
1589
1851
  }
1590
1852
  } catch (error) {
1591
1853
  return {
@@ -1594,18 +1856,43 @@ async function runShellTool(shells, from, name, args) {
1594
1856
  };
1595
1857
  }
1596
1858
  }
1859
+ function needles(waitFor) {
1860
+ if (waitFor === void 0) return;
1861
+ return typeof waitFor === "string" ? [waitFor] : waitFor;
1862
+ }
1597
1863
  function invalidArguments(name, error) {
1598
1864
  return {
1599
1865
  text: `invalid arguments for ${name}: ${error.issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; ")}`,
1600
1866
  isError: true
1601
1867
  };
1602
1868
  }
1603
- function shellReadText(result) {
1869
+ function shellReadText(result, verb) {
1604
1870
  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}]`;
1871
+ const scope = result.view === "screen" ? "the current screen" : result.truncated ? `showing the last ${result.lines} of ${result.totalLines} lines` : `showing all ${result.lines} lines`;
1872
+ const parts = [
1873
+ `$ ${shell.command}`,
1874
+ ...verb ? [verb] : [],
1875
+ shellState(shell),
1876
+ ...shell.interactive ? ["interactive"] : [],
1877
+ scope
1878
+ ];
1879
+ if (result.wait) parts.push(waitText(result.wait));
1880
+ const head = `[shell #${shell.ordinal} ${shell.id}: ${parts.join(", ")}]`;
1607
1881
  return result.text ? `${head}\n${result.text}` : `${head}\n(no output)`;
1608
1882
  }
1883
+ function shellKillText(result) {
1884
+ const { shell } = result;
1885
+ const name = `shell #${shell.ordinal} ${shell.id} ($ ${shell.command})`;
1886
+ return result.killed ? `killed ${name}` : `${name} had already ended (${shellState(shell)})`;
1887
+ }
1888
+ function waitText(wait) {
1889
+ const seconds = `${(wait.ms / 1e3).toFixed(1)}s`;
1890
+ switch (wait.outcome) {
1891
+ case "matched": return `found ${JSON.stringify(wait.match)} after ${seconds}`;
1892
+ case "exited": return `the shell ended before the text appeared (${seconds})`;
1893
+ case "timeout": return `the text did not appear within ${seconds}`;
1894
+ }
1895
+ }
1609
1896
  function shellState(shell) {
1610
1897
  if (shell.status === "running") return "still running";
1611
1898
  if (shell.exitCode !== void 0) return `exited ${shell.exitCode}`;
@@ -1625,7 +1912,11 @@ function shellDirectoryHandle() {
1625
1912
  };
1626
1913
  return {
1627
1914
  list: async (from) => resolve().list(from),
1628
- read: async (from, shellId, options) => resolve().read(from, shellId, options)
1915
+ read: async (from, shellId, options) => resolve().read(from, shellId, options),
1916
+ run: async (from, options) => resolve().run(from, options),
1917
+ write: async (from, shellId, options) => resolve().write(from, shellId, options),
1918
+ kill: async (from, shellId) => resolve().kill(from, shellId),
1919
+ grant: async (from, shellId) => resolve().grant(from, shellId)
1629
1920
  };
1630
1921
  }
1631
1922
  //#endregion
@@ -1887,6 +2178,7 @@ var SessionRunner = class {
1887
2178
  cwd: this.#cwd,
1888
2179
  profile: this.#config.profile,
1889
2180
  engine: "claude",
2181
+ shellAgentWrite: this.#config.shells ? this.#config.shellAgentWrite : void 0,
1890
2182
  capabilities: ENGINE_CAPABILITIES.claude,
1891
2183
  model: this.#model ?? this.#config.model,
1892
2184
  permissionMode: this.#permissionMode,
@@ -2154,8 +2446,8 @@ var SessionRunner = class {
2154
2446
  }],
2155
2447
  isError: output.isError
2156
2448
  };
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);
2449
+ })) : [], ...shells ? shellToolNames(this.#config.shellAgentWrite !== void 0).map((name) => tool(name, SHELL_TOOL_SHAPES[name].description, SHELL_TOOL_SHAPES[name].shape, async (args) => {
2450
+ const output = await runShellTool(shells, this.id, name, args, { write: this.#config.shellAgentWrite !== void 0 });
2159
2451
  return {
2160
2452
  content: [{
2161
2453
  type: "text",
@@ -2390,6 +2682,10 @@ var SessionRunner = class {
2390
2682
  delete request.expiresAt;
2391
2683
  return Promise.resolve(this.#resolveQuestionByPolicy(request, questionBehavior));
2392
2684
  }
2685
+ if (this.#allowsShellToolByPolicy(toolName)) {
2686
+ delete request.expiresAt;
2687
+ return Promise.resolve(this.#allowByPolicy(request));
2688
+ }
2393
2689
  return new Promise((resolve) => {
2394
2690
  const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => {
2395
2691
  const pending = this.#pending.get(id);
@@ -2417,6 +2713,27 @@ var SessionRunner = class {
2417
2713
  this.#setStatus("awaiting_approval");
2418
2714
  });
2419
2715
  };
2716
+ #allowsShellToolByPolicy(toolName) {
2717
+ if ((this.#config.shells ? shellToolOf(toolName) : void 0) === void 0) return false;
2718
+ return shellWriteToolOf(toolName) === void 0 || !shellToolNeedsCard(toolName, this.#config.shellAgentWrite);
2719
+ }
2720
+ #allowByPolicy(request) {
2721
+ this.#emit({
2722
+ type: "permission_requested",
2723
+ request
2724
+ });
2725
+ this.#emit({
2726
+ type: "permission_resolved",
2727
+ requestId: request.id,
2728
+ behavior: "allow",
2729
+ resolvedBy: "policy"
2730
+ });
2731
+ return {
2732
+ behavior: "allow",
2733
+ updatedInput: request.input,
2734
+ toolUseID: request.toolUseId
2735
+ };
2736
+ }
2420
2737
  #resolveQuestionByPolicy(request, mode) {
2421
2738
  this.#emit({
2422
2739
  type: "permission_requested",
@@ -2619,6 +2936,7 @@ var AiSdkRunner = class {
2619
2936
  cwd: this.#config.cwd ?? "",
2620
2937
  profile: this.#config.profile,
2621
2938
  engine: "provider",
2939
+ shellAgentWrite: this.#config.shells ? this.#config.shellAgentWrite : void 0,
2622
2940
  capabilities: this.#config.shouldApprove ? {
2623
2941
  ...ENGINE_CAPABILITIES.provider,
2624
2942
  interactiveApprovals: true
@@ -3831,15 +4149,16 @@ function createToolContext(options) {
3831
4149
  }
3832
4150
  if (options.shells) {
3833
4151
  const shells = options.shells;
4152
+ const write = options.shellWrite === true;
3834
4153
  const selfId = options.selfId ?? (() => options.sessionId);
3835
- for (const name of SHELL_TOOL_NAMES) definitions.push({
4154
+ for (const name of shellToolNames(write)) definitions.push({
3836
4155
  name,
3837
4156
  trust: "authoritative",
3838
4157
  tool: tool$1({
3839
4158
  description: SHELL_TOOL_SHAPES[name].description,
3840
4159
  inputSchema: z.object(SHELL_TOOL_SHAPES[name].shape),
3841
4160
  execute: async (args) => {
3842
- const output = await runShellTool(shells, selfId(), name, args);
4161
+ const output = await runShellTool(shells, selfId(), name, args, { write });
3843
4162
  return output.isError ? { error: output.text } : { result: output.text };
3844
4163
  }
3845
4164
  })
@@ -4206,6 +4525,7 @@ function createEngineSession(options) {
4206
4525
  onFileDelivered: options.capabilities?.deliverFiles === false || !isGranted("deliverFiles") ? void 0 : (file) => runner?.emitFileDelivered(file),
4207
4526
  peers: options.config.peers,
4208
4527
  shells: options.config.shells,
4528
+ shellWrite: options.config.shellAgentWrite !== void 0,
4209
4529
  selfId: () => runner?.id ?? "pending"
4210
4530
  });
4211
4531
  const declaredServers = options.profile?.session?.mcpServers;
@@ -5148,6 +5468,28 @@ function codexAnswers(questions, answers) {
5148
5468
  }
5149
5469
  return out;
5150
5470
  }
5471
+ const SHELL_WRITE_CHANNEL = {
5472
+ describe: (raw) => {
5473
+ const call = raw;
5474
+ const input = typeof call.arguments === "object" && call.arguments !== null ? call.arguments : {};
5475
+ return {
5476
+ toolName: call.tool,
5477
+ input,
5478
+ title: `Agent wants to run ${call.tool}`,
5479
+ displayName: call.tool
5480
+ };
5481
+ },
5482
+ itemId: (raw) => raw.callId,
5483
+ allow: (_raw, updatedInput) => ({ response: {
5484
+ allowed: true,
5485
+ updatedInput
5486
+ } }),
5487
+ deny: (_raw, _interrupt, _offered, message) => ({ response: {
5488
+ allowed: false,
5489
+ message
5490
+ } })
5491
+ };
5492
+ const SHELL_WRITE_GATE_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits"]);
5151
5493
  function decisionChannel(describe, itemId) {
5152
5494
  return {
5153
5495
  describe,
@@ -5368,6 +5710,7 @@ var CodexRunner = class {
5368
5710
  cwd: this.#cwd,
5369
5711
  profile: this.#config.profile,
5370
5712
  engine: "codex",
5713
+ shellAgentWrite: this.#config.shells ? this.#config.shellAgentWrite : void 0,
5371
5714
  capabilities: ENGINE_CAPABILITIES.codex,
5372
5715
  model: this.#model ?? this.#resolvedModel,
5373
5716
  permissionMode: this.#permissionMode,
@@ -5755,7 +6098,7 @@ var CodexRunner = class {
5755
6098
  };
5756
6099
  if (this.#model) options.model = this.#model;
5757
6100
  if (this.#instructions !== void 0) options.developerInstructions = this.#instructions;
5758
- const dynamic = [...this.#config.peers ? peerToolSpecs() : [], ...this.#config.shells ? shellToolSpecs() : []];
6101
+ const dynamic = [...this.#config.peers ? peerToolSpecs() : [], ...this.#config.shells ? shellToolSpecs(this.#config.shellAgentWrite !== void 0) : []];
5759
6102
  if (dynamic.length) options.dynamicTools = dynamic.map((spec) => ({
5760
6103
  type: "function",
5761
6104
  ...spec
@@ -6193,7 +6536,20 @@ var CodexRunner = class {
6193
6536
  };
6194
6537
  }
6195
6538
  if (shells && isShellToolName(call.tool)) {
6196
- const output = await runShellTool(shells, this.id, call.tool, call.arguments);
6539
+ const write = this.#config.shellAgentWrite !== void 0;
6540
+ let args = call.arguments;
6541
+ if (write && shellToolNeedsCard(call.tool, this.#config.shellAgentWrite) && SHELL_WRITE_GATE_MODES.has(this.#permissionMode)) {
6542
+ const verdict = await this.#requestApproval(SHELL_WRITE_CHANNEL, method, call, void 0);
6543
+ if (!verdict.allowed) return {
6544
+ success: false,
6545
+ contentItems: [{
6546
+ type: "inputText",
6547
+ text: shellWriteDeniedText(call.tool, verdict.message)
6548
+ }]
6549
+ };
6550
+ args = verdict.updatedInput ?? args;
6551
+ }
6552
+ const output = await runShellTool(shells, this.id, call.tool, args, { write });
6197
6553
  return {
6198
6554
  success: !output.isError,
6199
6555
  contentItems: [{
@@ -6292,7 +6648,7 @@ var CodexRunner = class {
6292
6648
  message = "codex offered no plain accept for this request (only broader session/policy grants), denied instead";
6293
6649
  sent = pending.channel.deny(pending.params, false, pending.offered);
6294
6650
  }
6295
- } else sent = pending.channel.deny(pending.params, decision.interrupt === true, pending.offered);
6651
+ } else sent = pending.channel.deny(pending.params, decision.interrupt === true, pending.offered, message);
6296
6652
  pending.respond(sent.response);
6297
6653
  this.#emit({
6298
6654
  type: "permission_resolved",
@@ -6962,6 +7318,6 @@ function getEngineAdapter(engine) {
6962
7318
  return ADAPTERS[engine ?? "claude"];
6963
7319
  }
6964
7320
  //#endregion
6965
- 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 };
7321
+ 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_KEYS_MAX, SHELL_KEY_NAMES, SHELL_READ_TOOL_NAMES, SHELL_REFUSAL, SHELL_TOOL_NAMES, SHELL_TOOL_SHAPES, SHELL_WRITE_REFUSAL, SHELL_WRITE_TOOL_NAMES, SUPPORTED_ATTACHMENT_TYPES, SessionRunner, TTY_REDRAW_CARRY, agentMayWrite, attachmentContentBlocks, attachmentKind, attachmentRef, checkClaudeAuth, clampShellTail, claudeAdapter, codexAdapter, composeInstructions, connectAppServer, connectMcpTools, countLines, createEngineSession, createToolContext, createWebFetch, encodeShellKey, encodeShellKeys, getEngineAdapter, headTail, htmlToMarkdown, installPeerDirectory, installShellDirectory, installedPeerDirectory, installedShellDirectory, isHostAllowed, isLocalShellSource, isPrivateAddress, isShellToolName, isShellWriteToolName, 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, shellKillText, shellOwnershipRefusal, shellReadText, shellSummary, shellTail, shellToolNames, shellToolNeedsCard, shellToolOf, shellToolSpecs, shellWriteDeniedText, shellWriteToolOf, splitLines, toApiMessage, toExecutionResult, truncateResultBlocks, ttyRedraws, ttyText, withHostTools, withMcpTools, withPeerContext };
6966
7322
 
6967
7323
  //# sourceMappingURL=index.mjs.map