@code-yeongyu/senpi 2026.11.15-2 → 2026.11.15-3

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 (132) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/bundle/chunks/{chunk-Q4RJ4WLE.js → chunk-225TDKTA.js} +4 -4
  3. package/dist/bundle/chunks/{chunk-QSU2456D.js → chunk-3KR3CDGM.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-AOTHPTMU.js → chunk-5GCZFK4X.js} +2 -2
  5. package/dist/bundle/chunks/{chunk-BMADOJHZ.js → chunk-5QIFH624.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-THYIV26N.js → chunk-GQEXLLWJ.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-3RILB2OD.js → chunk-OJOPOUAY.js} +9 -9
  8. package/dist/bundle/chunks/{chunk-64FJ4QQJ.js → chunk-PPCAJQDJ.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-74HCKLRH.js → chunk-TEXE4HT6.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-RRFDLGX6.js → chunk-TNYFCGI7.js} +156 -156
  11. package/dist/bundle/chunks/{cli-main-GNQY2LTU.js → cli-main-CV3ODX4A.js} +1 -1
  12. package/dist/bundle/chunks/{hand-entry-D3DNH6XL.js → hand-entry-2NN7ZTY6.js} +1 -1
  13. package/dist/bundle/chunks/{interactive-mode-ATBPY4M5.js → interactive-mode-TGHZWM2T.js} +1 -1
  14. package/dist/bundle/chunks/{main-OMFVSMUR.js → main-B7WEY4JW.js} +1 -1
  15. package/dist/bundle/chunks/{multi-session-host-ACQ5N4UB.js → multi-session-host-53YLL2OD.js} +1 -1
  16. package/dist/bundle/chunks/{package-manager-cli-2VMRV4HL.js → package-manager-cli-2U54AVKW.js} +1 -1
  17. package/dist/bundle/chunks/{role-BTH5PVME.js → role-4IXZZAL4.js} +1 -1
  18. package/dist/bundle/chunks/{rpc-mode-XUK5SYKH.js → rpc-mode-RRYXENML.js} +1 -1
  19. package/dist/bundle/chunks/{runtime-session-VO3X4OE3.js → runtime-session-KKHEYZOH.js} +1 -1
  20. package/dist/bundle/chunks/{session-control-endpoint-Y5EIFFPY.js → session-control-endpoint-HVULSIUR.js} +1 -1
  21. package/dist/bundle/chunks/session-worker.js +331 -331
  22. package/dist/bundle/cli.js +2 -2
  23. package/dist/bundle/index.js +1 -1
  24. package/dist/bundle/rpc-entry.js +1 -1
  25. package/dist/bundle/runtime-manifest.json +1 -1
  26. package/dist/core/agent-session.d.ts +6 -0
  27. package/dist/core/agent-session.js +34 -3
  28. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.d.ts +2 -0
  29. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +1 -0
  30. package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +2 -0
  31. package/dist/core/extensions/builtin/anthropic-subscription/failover.js +22 -4
  32. package/dist/core/extensions/builtin/anthropic-subscription/index.js +7 -2
  33. package/dist/core/extensions/builtin/anthropic-subscription/limit-recheck.d.ts +15 -0
  34. package/dist/core/extensions/builtin/anthropic-subscription/limit-recheck.js +55 -0
  35. package/dist/core/extensions/builtin/anthropic-subscription/session-stream.d.ts +1 -0
  36. package/dist/core/extensions/builtin/anthropic-subscription/session-stream.js +1 -0
  37. package/dist/core/extensions/builtin/anthropic-subscription/stream-guidance.js +8 -5
  38. package/dist/core/extensions/builtin/anthropic-subscription/stream.d.ts +1 -1
  39. package/dist/core/extensions/builtin/anthropic-subscription/stream.js +3 -1
  40. package/dist/core/extensions/builtin/terminal/extension-state.js +15 -0
  41. package/dist/core/extensions/builtin/terminal/notify.d.ts +2 -1
  42. package/dist/core/extensions/builtin/terminal/notify.js +15 -6
  43. package/dist/core/extensions/builtin/terminal/session-bundle.d.ts +5 -1
  44. package/dist/core/extensions/builtin/terminal/session-bundle.js +41 -1
  45. package/dist/core/extensions/builtin/terminal/tools/context.d.ts +4 -0
  46. package/dist/core/extensions/loader.js +5 -0
  47. package/dist/core/extensions/runner.d.ts +1 -0
  48. package/dist/core/extensions/runner.js +5 -0
  49. package/dist/core/extensions/types.d.ts +7 -0
  50. package/dist/core/run-background-jobs.d.ts +33 -0
  51. package/dist/core/run-background-jobs.js +47 -0
  52. package/dist/core/tools/bash-core.d.ts +50 -0
  53. package/dist/core/tools/bash-core.js +169 -0
  54. package/dist/core/tools/bash-schema.d.ts +48 -0
  55. package/dist/core/tools/bash-schema.js +34 -0
  56. package/dist/core/tools/bash.d.ts +4 -70
  57. package/dist/core/tools/bash.js +32 -409
  58. package/dist/core/tools/coding-tools-core.d.ts +24 -0
  59. package/dist/core/tools/coding-tools-core.js +14 -0
  60. package/dist/core/tools/edit-core.d.ts +56 -0
  61. package/dist/core/tools/edit-core.js +175 -0
  62. package/dist/core/tools/edit-diff.d.ts +0 -5
  63. package/dist/core/tools/edit-diff.js +0 -32
  64. package/dist/core/tools/edit-preview.d.ts +7 -0
  65. package/dist/core/tools/edit-preview.js +34 -0
  66. package/dist/core/tools/edit.d.ts +5 -43
  67. package/dist/core/tools/edit.js +10 -158
  68. package/dist/core/tools/encoding.d.ts +10 -0
  69. package/dist/core/tools/encoding.js +58 -0
  70. package/dist/core/tools/file-tool-host.d.ts +25 -0
  71. package/dist/core/tools/file-tool-host.js +109 -0
  72. package/dist/core/tools/fs-error-code.d.ts +2 -0
  73. package/dist/core/tools/fs-error-code.js +15 -0
  74. package/dist/core/tools/machine-output-spill.d.ts +11 -0
  75. package/dist/core/tools/machine-output-spill.js +65 -0
  76. package/dist/core/tools/machine.d.ts +4 -24
  77. package/dist/core/tools/machine.js +4 -44
  78. package/dist/core/tools/output-accumulator-core.d.ts +84 -0
  79. package/dist/core/tools/output-accumulator-core.js +200 -0
  80. package/dist/core/tools/output-accumulator.d.ts +9 -61
  81. package/dist/core/tools/output-accumulator.js +90 -265
  82. package/dist/core/tools/path-utils.js +3 -48
  83. package/dist/core/tools/posix-signals.d.ts +3 -0
  84. package/dist/core/tools/posix-signals.js +113 -0
  85. package/dist/core/tools/read-core.d.ts +64 -0
  86. package/dist/core/tools/read-core.js +180 -0
  87. package/dist/core/tools/read-text.d.ts +23 -0
  88. package/dist/core/tools/read-text.js +90 -0
  89. package/dist/core/tools/read.d.ts +6 -47
  90. package/dist/core/tools/read.js +13 -218
  91. package/dist/core/tools/renderers/bash.d.ts +0 -1
  92. package/dist/core/tools/renderers/bash.js +0 -1
  93. package/dist/core/tools/renderers/edit.d.ts +1 -1
  94. package/dist/core/tools/renderers/edit.js +1 -1
  95. package/dist/core/tools/secret-output-redactor.js +2 -1
  96. package/dist/core/tools/shell-operations.d.ts +53 -0
  97. package/dist/core/tools/shell-operations.js +115 -0
  98. package/dist/core/tools/shell-output.d.ts +20 -0
  99. package/dist/core/tools/shell-output.js +152 -0
  100. package/dist/core/tools/tail-window.js +5 -7
  101. package/dist/core/tools/tool-machine.d.ts +30 -0
  102. package/dist/core/tools/tool-machine.js +47 -0
  103. package/dist/core/tools/truncate.js +15 -14
  104. package/dist/core/tools/write-core.d.ts +47 -0
  105. package/dist/core/tools/write-core.js +108 -0
  106. package/dist/core/tools/write-result.d.ts +0 -1
  107. package/dist/core/tools/write-result.js +0 -13
  108. package/dist/core/tools/write.d.ts +6 -31
  109. package/dist/core/tools/write.js +25 -77
  110. package/dist/core/turn-origin.d.ts +4 -2
  111. package/dist/core/turn-origin.js +15 -8
  112. package/dist/modes/interactive/session-control-session-commands.js +1 -1
  113. package/dist/modes/rpc/connection-handler.js +2 -2
  114. package/dist/modes/rpc/effect-commands.d.ts +1 -4
  115. package/dist/modes/rpc/method-classes-schema-session.d.ts +1 -0
  116. package/dist/modes/rpc/method-classes-schema-session.js +5 -1
  117. package/dist/modes/rpc/method-classes-schema.d.ts +1 -0
  118. package/dist/modes/rpc/pause-command-validation.js +2 -0
  119. package/dist/modes/rpc/rpc-mutation-response-types.d.ts +2 -4
  120. package/dist/modes/rpc/rpc-session-command-types.d.ts +1 -0
  121. package/dist/modes/rpc/session-control-actions-data.d.ts +7 -3
  122. package/dist/modes/rpc/session-control-actions-data.js +12 -6
  123. package/dist/tools-core.d.ts +14 -0
  124. package/dist/tools-core.js +9 -0
  125. package/dist/utils/image-mime.d.ts +4 -0
  126. package/dist/utils/image-mime.js +99 -0
  127. package/dist/utils/mime.d.ts +1 -1
  128. package/dist/utils/mime.js +2 -97
  129. package/docs/extensions.md +7 -1
  130. package/docs/rpc.md +14 -0
  131. package/package.json +11 -7
  132. package/vendor/senpi-machine-ops/files/local-files.js +34 -5
@@ -0,0 +1,53 @@
1
+ import type { ExecShell } from "../../../vendor/senpi-machine-ops/index.js";
2
+ import type { ToolMachine } from "./tool-machine.ts";
3
+ /**
4
+ * Pluggable operations for the bash tool.
5
+ * Override these to delegate command execution to remote systems (for example SSH).
6
+ */
7
+ export interface BashOperations {
8
+ /**
9
+ * Execute a command and stream output.
10
+ * @param command The command to execute
11
+ * @param cwd Working directory
12
+ * @param options Execution options
13
+ * @returns Promise resolving to the exit code. Report signal terminations as 128 + signal number;
14
+ * a null exit code is treated as a failed command.
15
+ */
16
+ exec: (command: string, cwd: string, options: {
17
+ onData: (data: Buffer, stream?: "stdout" | "stderr") => void;
18
+ signal?: AbortSignal;
19
+ timeout?: number;
20
+ env?: NodeJS.ProcessEnv;
21
+ redactSecretsInOutput?: boolean;
22
+ }) => Promise<{
23
+ exitCode: number | null;
24
+ }>;
25
+ }
26
+ /** What a shell run needs from the host it runs for: the shell, the cwd check, the environment and pid tracking. */
27
+ export interface ShellExecHost {
28
+ /** The shell the command runs in; undefined lets the machine choose (`/bin/sh -c` on POSIX). */
29
+ resolveShell(): ExecShell | undefined;
30
+ /** Rejects when `cwd` does not exist where the command runs. */
31
+ checkCwd(cwd: string, signal: AbortSignal | undefined): Promise<void>;
32
+ /** "exact" runs with the tool's environment alone; "overlay" lays it over the machine's curated base. */
33
+ readonly envMode: "exact" | "overlay";
34
+ /** The environment when the caller passes none. */
35
+ defaultEnv(): NodeJS.ProcessEnv | undefined;
36
+ /** The number of a terminating signal, for the 128 + n exit code. */
37
+ signalNumber(signal: string): number | undefined;
38
+ onSpawn?(pid: number): void;
39
+ onExit?(pid: number): void;
40
+ /** Runs once the exec settles, success or not. */
41
+ afterExec?(): void;
42
+ }
43
+ /**
44
+ * A host that knows only the tool machine: the machine's shell and environment, the cwd checked there, and the signal
45
+ * numbers of the machine's own OS.
46
+ */
47
+ export declare function machineShellExecHost(machine: ToolMachine, shell?: ExecShell): ShellExecHost;
48
+ /**
49
+ * Shell execution used by the built-in shell tools: one `process.exec` of the operation layer (own process group,
50
+ * abort/timeout kill the group). The tool keeps its own output handling.
51
+ */
52
+ export declare function createShellOperations(shellName: string, machine: ToolMachine, host: ShellExecHost): BashOperations;
53
+ //# sourceMappingURL=shell-operations.d.ts.map
@@ -0,0 +1,115 @@
1
+ import { posixSignalTable } from "./posix-signals.js";
2
+ const MAX_TIMEOUT_MS = 2_147_483_647;
3
+ const MAX_TIMEOUT_SECONDS = MAX_TIMEOUT_MS / 1000;
4
+ function resolveTimeoutMs(timeout) {
5
+ if (timeout === undefined)
6
+ return undefined;
7
+ if (!Number.isFinite(timeout) || timeout <= 0) {
8
+ throw new Error("Invalid timeout: must be a finite number of seconds");
9
+ }
10
+ const timeoutMs = timeout * 1000;
11
+ if (timeoutMs > MAX_TIMEOUT_MS) {
12
+ throw new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`);
13
+ }
14
+ return timeoutMs;
15
+ }
16
+ /**
17
+ * A host that knows only the tool machine: the machine's shell and environment, the cwd checked there, and the signal
18
+ * numbers of the machine's own OS.
19
+ */
20
+ export function machineShellExecHost(machine, shell) {
21
+ return {
22
+ resolveShell: () => shell,
23
+ checkCwd: async (cwd, signal) => {
24
+ await machine.ops.files.stat({ path: cwd }, machine.context(signal));
25
+ },
26
+ envMode: "overlay",
27
+ defaultEnv: () => undefined,
28
+ signalNumber: (signal) => posixSignalTable(machine.ops.info.host().os)[signal],
29
+ };
30
+ }
31
+ /**
32
+ * Shell execution used by the built-in shell tools: one `process.exec` of the operation layer (own process group,
33
+ * abort/timeout kill the group). The tool keeps its own output handling.
34
+ */
35
+ export function createShellOperations(shellName, machine, host) {
36
+ return {
37
+ exec: async (command, cwd, { onData, signal, timeout, env }) => {
38
+ const timeoutMs = resolveTimeoutMs(timeout);
39
+ if (signal?.aborted) {
40
+ throw new Error("aborted");
41
+ }
42
+ const shell = host.resolveShell();
43
+ try {
44
+ await host.checkCwd(cwd, signal);
45
+ }
46
+ catch {
47
+ throw new Error(`Working directory does not exist: ${cwd}\nCannot execute ${shellName} commands.`);
48
+ }
49
+ let streamCallbackError;
50
+ let hasStreamCallbackError = false;
51
+ // A failing output callback stops the command the same way an abort does.
52
+ const callbackAbort = new AbortController();
53
+ const failStream = (error) => {
54
+ if (hasStreamCallbackError)
55
+ return;
56
+ streamCallbackError = error;
57
+ hasStreamCallbackError = true;
58
+ callbackAbort.abort();
59
+ };
60
+ const handleData = (data, stream) => {
61
+ if (hasStreamCallbackError)
62
+ return;
63
+ try {
64
+ const callbackResult = onData(data, stream);
65
+ if (callbackResult && typeof callbackResult.then === "function") {
66
+ void Promise.resolve(callbackResult).catch(failStream);
67
+ }
68
+ }
69
+ catch (error) {
70
+ failStream(error);
71
+ }
72
+ };
73
+ const execSignal = signal ? AbortSignal.any([signal, callbackAbort.signal]) : callbackAbort.signal;
74
+ let result;
75
+ try {
76
+ result = await machine.ops.process.exec({
77
+ command,
78
+ cwd,
79
+ env: env ?? host.defaultEnv(),
80
+ envMode: host.envMode,
81
+ ...(shell !== undefined ? { shell } : {}),
82
+ timeoutMs: timeoutMs ?? Number.POSITIVE_INFINITY,
83
+ onOutput: handleData,
84
+ ...(host.onSpawn !== undefined ? { onSpawn: host.onSpawn } : {}),
85
+ ...(host.onExit !== undefined ? { onExit: host.onExit } : {}),
86
+ }, machine.context(execSignal));
87
+ }
88
+ catch (error) {
89
+ if (hasStreamCallbackError)
90
+ throw streamCallbackError;
91
+ if (signal?.aborted)
92
+ throw new Error("aborted");
93
+ throw error;
94
+ }
95
+ finally {
96
+ host.afterExec?.();
97
+ }
98
+ if (hasStreamCallbackError) {
99
+ throw streamCallbackError;
100
+ }
101
+ if (signal?.aborted) {
102
+ throw new Error("aborted");
103
+ }
104
+ if (result.timedOut) {
105
+ throw new Error(`timeout:${timeout}`);
106
+ }
107
+ // A signal-killed shell has no exit code. Use the standard shell convention so
108
+ // callers do not mistake the termination for a successful command.
109
+ return {
110
+ exitCode: result.exitCode ?? (result.signal ? 128 + (host.signalNumber(result.signal) ?? 0) : 1),
111
+ };
112
+ },
113
+ };
114
+ }
115
+ //# sourceMappingURL=shell-operations.js.map
@@ -0,0 +1,20 @@
1
+ import type { AgentToolUpdateCallback } from "@earendil-works/pi-agent-core";
2
+ import type { TextContent } from "@earendil-works/pi-ai";
3
+ import type { BashToolDetails } from "./bash-schema.ts";
4
+ import type { OutputAccumulator, OutputSnapshot } from "./output-accumulator-core.ts";
5
+ export interface FormattedShellOutput {
6
+ text: string;
7
+ content: TextContent[];
8
+ details: BashToolDetails | undefined;
9
+ }
10
+ /**
11
+ * One shell run's output stream: optional secret redaction per stream, throttled partial updates, and the terminal
12
+ * flush that settles the spill before the result is built.
13
+ */
14
+ export declare function createShellOutput(output: OutputAccumulator, onUpdate: AgentToolUpdateCallback<BashToolDetails | undefined> | undefined, redactSecretsInOutput: boolean): {
15
+ handleData: (data: Uint8Array, stream?: string) => void;
16
+ finishOutput: () => Promise<OutputSnapshot>;
17
+ formatOutput: (snapshot: OutputSnapshot, emptyText?: string) => FormattedShellOutput;
18
+ clearUpdateTimer: () => void;
19
+ };
20
+ //# sourceMappingURL=shell-output.d.ts.map
@@ -0,0 +1,152 @@
1
+ import { modelOnlyText } from "./model-only-text.js";
2
+ import { SecretOutputRedactor } from "./secret-output-redactor.js";
3
+ import { DEFAULT_MAX_BYTES, formatSize } from "./truncate.js";
4
+ const BASH_UPDATE_THROTTLE_MS = 100;
5
+ /**
6
+ * One shell run's output stream: optional secret redaction per stream, throttled partial updates, and the terminal
7
+ * flush that settles the spill before the result is built.
8
+ */
9
+ export function createShellOutput(output, onUpdate, redactSecretsInOutput) {
10
+ const streams = redactSecretsInOutput
11
+ ? new Map()
12
+ : undefined;
13
+ let acceptingOutput = true;
14
+ let updateTimer;
15
+ let updateDirty = false;
16
+ let lastUpdateAt = 0;
17
+ let outputUpdateError;
18
+ let hasOutputUpdateError = false;
19
+ const emitOutputUpdate = () => {
20
+ if (!onUpdate || !updateDirty || hasOutputUpdateError)
21
+ return;
22
+ updateDirty = false;
23
+ lastUpdateAt = Date.now();
24
+ const snapshot = output.snapshot({ persistIfTruncated: true });
25
+ try {
26
+ onUpdate({
27
+ content: [{ type: "text", text: snapshot.content || "" }],
28
+ details: {
29
+ truncation: snapshot.truncation.truncated ? snapshot.truncation : undefined,
30
+ fullOutputPath: snapshot.fullOutputPath,
31
+ },
32
+ });
33
+ }
34
+ catch (error) {
35
+ outputUpdateError = error;
36
+ hasOutputUpdateError = true;
37
+ }
38
+ };
39
+ const clearUpdateTimer = () => {
40
+ if (updateTimer) {
41
+ clearTimeout(updateTimer);
42
+ updateTimer = undefined;
43
+ }
44
+ };
45
+ const scheduleOutputUpdate = () => {
46
+ if (!onUpdate)
47
+ return;
48
+ updateDirty = true;
49
+ const delay = BASH_UPDATE_THROTTLE_MS - (Date.now() - lastUpdateAt);
50
+ if (delay <= 0) {
51
+ clearUpdateTimer();
52
+ emitOutputUpdate();
53
+ return;
54
+ }
55
+ updateTimer ??= setTimeout(() => {
56
+ updateTimer = undefined;
57
+ emitOutputUpdate();
58
+ }, delay);
59
+ };
60
+ const handleData = (data, stream = "stdout") => {
61
+ if (!acceptingOutput)
62
+ return;
63
+ if (streams) {
64
+ let state = streams.get(stream);
65
+ if (!state) {
66
+ state = { redactor: new SecretOutputRedactor(), decoder: new TextDecoder() };
67
+ streams.set(stream, state);
68
+ }
69
+ output.appendText(state.redactor.push(state.decoder.decode(data, { stream: true })));
70
+ }
71
+ else
72
+ output.append(data);
73
+ scheduleOutputUpdate();
74
+ };
75
+ const finishOutput = async () => {
76
+ acceptingOutput = false;
77
+ clearUpdateTimer();
78
+ let primaryError;
79
+ let hasPrimaryError = false;
80
+ let snapshot;
81
+ try {
82
+ if (streams) {
83
+ for (const { redactor, decoder } of streams.values()) {
84
+ output.appendText(redactor.push(decoder.decode()) + redactor.finish());
85
+ }
86
+ updateDirty = true;
87
+ }
88
+ output.finish();
89
+ emitOutputUpdate();
90
+ snapshot = output.snapshot({ persistIfTruncated: true });
91
+ if (hasOutputUpdateError) {
92
+ primaryError = outputUpdateError;
93
+ hasPrimaryError = true;
94
+ }
95
+ }
96
+ catch (error) {
97
+ primaryError = error;
98
+ hasPrimaryError = true;
99
+ }
100
+ try {
101
+ await output.closeTempFile();
102
+ }
103
+ catch (closeError) {
104
+ if (hasPrimaryError) {
105
+ throw new AggregateError([primaryError, closeError], "Bash output finalization and spill cleanup failed");
106
+ }
107
+ throw closeError;
108
+ }
109
+ if (hasPrimaryError) {
110
+ try {
111
+ await output.removeTempFile();
112
+ }
113
+ catch (unlinkError) {
114
+ if (!(unlinkError instanceof Error))
115
+ throw unlinkError;
116
+ throw new AggregateError([primaryError, unlinkError], "Bash output finalization and spill cleanup failed");
117
+ }
118
+ throw primaryError;
119
+ }
120
+ if (snapshot === undefined) {
121
+ throw new Error("Bash output finalization produced no snapshot");
122
+ }
123
+ return snapshot;
124
+ };
125
+ const formatOutput = (snapshot, emptyText = "(no output)") => {
126
+ const truncation = snapshot.truncation;
127
+ const text = snapshot.content || emptyText;
128
+ let notice;
129
+ let details;
130
+ if (truncation.truncated) {
131
+ details = { truncation, fullOutputPath: snapshot.fullOutputPath };
132
+ const startLine = truncation.totalLines - truncation.outputLines + 1;
133
+ const endLine = truncation.totalLines;
134
+ if (truncation.lastLinePartial) {
135
+ const lastLineSize = formatSize(output.getLastLineBytes());
136
+ notice = `[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${snapshot.fullOutputPath}]`;
137
+ }
138
+ else if (truncation.truncatedBy === "lines") {
139
+ notice = `[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${snapshot.fullOutputPath}]`;
140
+ }
141
+ else {
142
+ notice = `[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${snapshot.fullOutputPath}]`;
143
+ }
144
+ }
145
+ const content = [{ type: "text", text: notice === undefined ? text : `${text}\n` }];
146
+ if (notice !== undefined)
147
+ content.push(modelOnlyText(notice));
148
+ return { text: notice === undefined ? text : `${text}\n\n${notice}`, content, details };
149
+ };
150
+ return { handleData, finishOutput, formatOutput, clearUpdateTimer };
151
+ }
152
+ //# sourceMappingURL=shell-output.js.map
@@ -1,7 +1,5 @@
1
+ import { decodeUtf8, encodeUtf8, utf8ByteLength as byteLength } from "./encoding.js";
1
2
  const MAX_PENDING_TAIL_CHUNKS = 10;
2
- function byteLength(text) {
3
- return Buffer.byteLength(text, "utf-8");
4
- }
5
3
  export class TailWindow {
6
4
  constructor(maxBytes) {
7
5
  this.chunks = [];
@@ -34,13 +32,13 @@ export class TailWindow {
34
32
  return this.startsAtBoundary;
35
33
  }
36
34
  replaceWithTail(text, startsAtLineBoundary) {
37
- const buffer = Buffer.from(text, "utf-8");
35
+ const buffer = encodeUtf8(text);
38
36
  let start = Math.max(0, buffer.length - this.maxBytes);
39
37
  while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) {
40
38
  start++;
41
39
  }
42
40
  this.startsAtBoundary = start === 0 ? startsAtLineBoundary : buffer[start - 1] === 0x0a;
43
- const tail = buffer.subarray(start).toString("utf-8");
41
+ const tail = decodeUtf8(buffer.subarray(start));
44
42
  this.chunks = tail.length > 0 ? [tail] : [];
45
43
  this.bytes = byteLength(tail);
46
44
  }
@@ -61,13 +59,13 @@ export class TailWindow {
61
59
  if (this.bytes <= this.maxBytes) {
62
60
  return;
63
61
  }
64
- const buffer = Buffer.from(this.joined(), "utf-8");
62
+ const buffer = encodeUtf8(this.joined());
65
63
  let start = Math.max(0, buffer.length - this.maxBytes);
66
64
  while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) {
67
65
  start++;
68
66
  }
69
67
  this.startsAtBoundary = start === 0 ? this.startsAtBoundary : buffer[start - 1] === 0x0a;
70
- const tail = buffer.subarray(start).toString("utf-8");
68
+ const tail = decodeUtf8(buffer.subarray(start));
71
69
  this.chunks = tail.length > 0 ? [tail] : [];
72
70
  this.bytes = byteLength(tail);
73
71
  }
@@ -0,0 +1,30 @@
1
+ import type { MachineOps, OpContext, OpName, PolicyHooks } from "../../../vendor/senpi-machine-ops/index.js";
2
+ /**
3
+ * The hooks the tool path honours: only `beforeOp`, once per tool call. The tools run the extension filesystem policy
4
+ * themselves and sub-operations carry no hooks, so `filesystem` and `afterOp` are rejected at compile time instead of
5
+ * being silently ignored.
6
+ */
7
+ export type ToolPathPolicy = Pick<PolicyHooks, "beforeOp"> & {
8
+ readonly filesystem?: never;
9
+ readonly afterOp?: never;
10
+ };
11
+ /** One MachineOps bound to the tool path: principal `local-session`, no scope root (the tools resolve paths). */
12
+ export interface ToolMachine {
13
+ readonly ops: MachineOps;
14
+ /**
15
+ * Runs `PolicyHooks.beforeOp` once for one tool call; a denial rejects with `Error(reason)` before any effect.
16
+ * Without a `beforeOp` it returns undefined synchronously, so a tool that awaits only a returned promise keeps
17
+ * its exact pre-layer timing (bash emits its first partial update before any await).
18
+ */
19
+ begin(op: OpName, args: unknown, signal: AbortSignal | undefined): Promise<void> | undefined;
20
+ /** Context for a sub-operation inside a call `begin` already admitted. */
21
+ context(signal: AbortSignal | undefined, pathLock?: "caller"): OpContext;
22
+ }
23
+ /** Binds the tool path to the MachineOps `resolveOps` returns, read on every use. */
24
+ export declare function bindToolMachine(resolveOps: () => MachineOps, policy?: ToolPathPolicy): ToolMachine;
25
+ /** A ToolMachine over a host-supplied MachineOps (a remote device, a sandbox): the tools' only effect channel. */
26
+ export declare function createToolMachine(options: {
27
+ ops: MachineOps;
28
+ policy?: ToolPathPolicy;
29
+ }): ToolMachine;
30
+ //# sourceMappingURL=tool-machine.d.ts.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Tool-path limits over the machine's own defaults. The tools keep their own truncation (truncate.ts, the output
3
+ * accumulator), so the operation layer reads files whole and streams exec output uncaptured.
4
+ */
5
+ function toolPathLimits(machineLimits) {
6
+ return {
7
+ ...machineLimits,
8
+ readMaxBytes: Number.POSITIVE_INFINITY,
9
+ execStdoutMaxBytes: 0,
10
+ execStderrMaxBytes: 0,
11
+ execTimeoutDefaultMs: Number.POSITIVE_INFINITY,
12
+ };
13
+ }
14
+ const NEVER_ABORTED = new AbortController().signal;
15
+ /** Binds the tool path to the MachineOps `resolveOps` returns, read on every use. */
16
+ export function bindToolMachine(resolveOps, policy) {
17
+ const context = (signal, pathLock) => ({
18
+ scope: { id: "local-session" },
19
+ principal: { kind: "local-session", id: "local-session" },
20
+ signal: signal ?? NEVER_ABORTED,
21
+ limits: toolPathLimits(resolveOps().limits),
22
+ // The tools run the extension filesystem policy themselves (with their own error text) and the gate below
23
+ // already ran once for the call, so sub-operations carry no hooks.
24
+ policy: {},
25
+ ...(pathLock !== undefined ? { pathLock } : {}),
26
+ });
27
+ return {
28
+ get ops() {
29
+ return resolveOps();
30
+ },
31
+ begin(op, args, signal) {
32
+ const beforeOp = policy?.beforeOp;
33
+ if (beforeOp === undefined)
34
+ return undefined;
35
+ return beforeOp(op, args, { ...context(signal), policy: policy ?? {} }).then((decision) => {
36
+ if (!decision.allow)
37
+ throw new Error(decision.reason);
38
+ });
39
+ },
40
+ context,
41
+ };
42
+ }
43
+ /** A ToolMachine over a host-supplied MachineOps (a remote device, a sandbox): the tools' only effect channel. */
44
+ export function createToolMachine(options) {
45
+ return bindToolMachine(() => options.ops, options.policy);
46
+ }
47
+ //# sourceMappingURL=tool-machine.js.map
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * Never returns partial lines (except bash tail truncation edge case).
9
9
  */
10
+ import { decodeUtf8, encodeUtf8, utf8ByteLength } from "./encoding.js";
10
11
  export const DEFAULT_MAX_LINES = 2000;
11
12
  export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
12
13
  export const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line
@@ -44,7 +45,7 @@ export function formatSize(bytes) {
44
45
  export function truncateHead(content, options = {}) {
45
46
  const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
46
47
  const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
47
- const totalBytes = Buffer.byteLength(content, "utf-8");
48
+ const totalBytes = utf8ByteLength(content);
48
49
  const lines = splitLinesForCounting(content);
49
50
  const totalLines = lines.length;
50
51
  // Check if no truncation needed
@@ -64,7 +65,7 @@ export function truncateHead(content, options = {}) {
64
65
  };
65
66
  }
66
67
  // Check if first line alone exceeds byte limit
67
- const firstLineBytes = Buffer.byteLength(lines[0], "utf-8");
68
+ const firstLineBytes = utf8ByteLength(lines[0]);
68
69
  if (firstLineBytes > maxBytes) {
69
70
  return {
70
71
  content: "",
@@ -86,7 +87,7 @@ export function truncateHead(content, options = {}) {
86
87
  let truncatedBy = "lines";
87
88
  for (let i = 0; i < lines.length && i < maxLines; i++) {
88
89
  const line = lines[i];
89
- const lineBytes = Buffer.byteLength(line, "utf-8") + (i > 0 ? 1 : 0); // +1 for newline
90
+ const lineBytes = utf8ByteLength(line) + (i > 0 ? 1 : 0); // +1 for newline
90
91
  if (outputBytesCount + lineBytes > maxBytes) {
91
92
  truncatedBy = "bytes";
92
93
  break;
@@ -99,7 +100,7 @@ export function truncateHead(content, options = {}) {
99
100
  truncatedBy = "lines";
100
101
  }
101
102
  const outputContent = outputLinesArr.join("\n");
102
- const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
103
+ const finalOutputBytes = utf8ByteLength(outputContent);
103
104
  return {
104
105
  content: outputContent,
105
106
  truncated: true,
@@ -123,7 +124,7 @@ export function truncateHead(content, options = {}) {
123
124
  export function truncateTail(content, options = {}) {
124
125
  const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
125
126
  const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
126
- const totalBytes = Buffer.byteLength(content, "utf-8");
127
+ const totalBytes = utf8ByteLength(content);
127
128
  const lines = splitLinesForCounting(content);
128
129
  const totalLines = lines.length;
129
130
  // Check if no truncation needed
@@ -149,7 +150,7 @@ export function truncateTail(content, options = {}) {
149
150
  let lastLinePartial = false;
150
151
  for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {
151
152
  const line = lines[i];
152
- const lineBytes = Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline
153
+ const lineBytes = utf8ByteLength(line) + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline
153
154
  if (outputBytesCount + lineBytes > maxBytes) {
154
155
  truncatedBy = "bytes";
155
156
  // Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,
@@ -157,7 +158,7 @@ export function truncateTail(content, options = {}) {
157
158
  if (outputLinesArr.length === 0) {
158
159
  const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);
159
160
  outputLinesArr.unshift(truncatedLine);
160
- outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8");
161
+ outputBytesCount = utf8ByteLength(truncatedLine);
161
162
  lastLinePartial = true;
162
163
  }
163
164
  break;
@@ -170,7 +171,7 @@ export function truncateTail(content, options = {}) {
170
171
  truncatedBy = "lines";
171
172
  }
172
173
  const outputContent = outputLinesArr.join("\n");
173
- const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
174
+ const finalOutputBytes = utf8ByteLength(outputContent);
174
175
  return {
175
176
  content: outputContent,
176
177
  truncated: true,
@@ -190,7 +191,7 @@ export function truncateTail(content, options = {}) {
190
191
  * Handles multi-byte UTF-8 characters correctly.
191
192
  */
192
193
  function truncateStringToBytesFromEnd(str, maxBytes) {
193
- const buf = Buffer.from(str, "utf-8");
194
+ const buf = encodeUtf8(str);
194
195
  if (buf.length <= maxBytes) {
195
196
  return str;
196
197
  }
@@ -200,7 +201,7 @@ function truncateStringToBytesFromEnd(str, maxBytes) {
200
201
  while (start < buf.length && (buf[start] & 0xc0) === 0x80) {
201
202
  start++;
202
203
  }
203
- return buf.slice(start).toString("utf-8");
204
+ return decodeUtf8(buf.slice(start));
204
205
  }
205
206
  /**
206
207
  * Truncate a single line to max characters, adding [truncated] suffix.
@@ -217,7 +218,7 @@ export function truncateLine(line, maxChars = GREP_MAX_LINE_LENGTH) {
217
218
  * `…N chars truncated…` marker, like Codex does for tool output. Cuts only at character boundaries.
218
219
  */
219
220
  export function truncateMiddle(content, maxBytes) {
220
- const buf = Buffer.from(content, "utf-8");
221
+ const buf = encodeUtf8(content);
221
222
  const totalLines = splitLinesForCounting(content).length;
222
223
  if (buf.length <= maxBytes) {
223
224
  return { content, truncated: false, removedChars: 0, totalBytes: buf.length, totalLines };
@@ -230,9 +231,9 @@ export function truncateMiddle(content, maxBytes) {
230
231
  let tailStart = buf.length - (maxBytes - Math.floor(maxBytes / 2));
231
232
  while (tailStart < buf.length && !isBoundary(tailStart))
232
233
  tailStart++;
233
- const head = buf.subarray(0, headEnd).toString("utf-8");
234
- const tail = buf.subarray(tailStart).toString("utf-8");
235
- const removedChars = Array.from(buf.subarray(headEnd, tailStart).toString("utf-8")).length;
234
+ const head = decodeUtf8(buf.subarray(0, headEnd));
235
+ const tail = decodeUtf8(buf.subarray(tailStart));
236
+ const removedChars = Array.from(decodeUtf8(buf.subarray(headEnd, tailStart))).length;
236
237
  return {
237
238
  content: `${head}…${removedChars} chars truncated…${tail}`,
238
239
  truncated: true,
@@ -0,0 +1,47 @@
1
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
2
+ import { type Static, Type } from "typebox";
3
+ import type { FilesystemPolicyChecker, ToolDefinition } from "../extensions/types.ts";
4
+ import { type FileMutationQueue, type ToolPathCanonicalizer, type ToolPathResolver } from "./file-tool-host.ts";
5
+ import type { ToolMachine } from "./tool-machine.ts";
6
+ import { type WriteBaseline, type WriteToolDetails } from "./write-result.ts";
7
+ export declare const writeSchema: Type.TObject<{
8
+ path: Type.TString;
9
+ content: Type.TString;
10
+ }>;
11
+ export declare const writeToolSystemPromptContribution: {
12
+ readonly snippet: "Create or overwrite files";
13
+ readonly guidelines: readonly ["write only for new files or full rewrites."];
14
+ readonly fullGuidelines: readonly ["Use write only for new files or complete rewrites."];
15
+ };
16
+ export type WriteToolInput = Static<typeof writeSchema>;
17
+ /**
18
+ * Pluggable operations for the write tool.
19
+ * Override these to delegate file writing to remote systems (for example SSH).
20
+ */
21
+ export interface WriteOperations {
22
+ /** Write content to a file */
23
+ writeFile: (absolutePath: string, content: string) => Promise<void>;
24
+ /** Create directory recursively */
25
+ mkdir: (dir: string) => Promise<void>;
26
+ }
27
+ export interface CoreWriteToolOptions {
28
+ /** Operation layer and policy gate. */
29
+ machine: ToolMachine;
30
+ /** Custom operations for file writing. Default: writes through `machine` */
31
+ operations?: WriteOperations;
32
+ /** Extension-registered filesystem policy checker. */
33
+ filesystemPolicy?: FilesystemPolicyChecker;
34
+ /** Default: POSIX resolution against the cwd. */
35
+ resolvePath?: ToolPathResolver;
36
+ /** The directory handed to `operations.mkdir`. Default: POSIX dirname. */
37
+ dirname?: (absolutePath: string) => string;
38
+ /** Default: one queue per path string in this isolate, shared with the core edit tool. */
39
+ withMutationQueue?: FileMutationQueue;
40
+ /** The path `filesystemPolicy` judges. Default: the resolved path as is. */
41
+ canonicalizePath?: ToolPathCanonicalizer;
42
+ /** The content before the write, for the result's patch. Default: read on `machine`; none with custom operations. */
43
+ readBaseline?: (absolutePath: string) => Promise<WriteBaseline>;
44
+ }
45
+ export declare function createCoreWriteToolDefinition(cwd: string, options: CoreWriteToolOptions): ToolDefinition<typeof writeSchema, WriteToolDetails | undefined>;
46
+ export declare function createCoreWriteTool(cwd: string, options: CoreWriteToolOptions): AgentTool<typeof writeSchema>;
47
+ //# sourceMappingURL=write-core.d.ts.map