@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,180 @@
1
+ import { selectedReadFolder } from "@earendil-works/pi-agent-core/read-folders";
2
+ import { Type } from "typebox";
3
+ import { detectSupportedImageMimeType, IMAGE_TYPE_SNIFF_BYTES } from "../../utils/image-mime.js";
4
+ import { decodeUtf8, encodeBase64 } from "./encoding.js";
5
+ import { lexicalCanonicalPath, resolvePosixToolPath, resolveReadPathVariants, } from "./file-tool-host.js";
6
+ import { wrapToolDefinition } from "./tool-definition-wrapper.js";
7
+ import { shapeTextRead } from "./read-text.js";
8
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "./truncate.js";
9
+ const readSchema = Type.Object({
10
+ path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
11
+ offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
12
+ limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
13
+ });
14
+ export const readToolSystemPromptContribution = {
15
+ snippet: "Read file contents",
16
+ guidelines: ["Files: read, not cat/sed."],
17
+ fullGuidelines: ["Use read to examine files instead of cat or sed."],
18
+ };
19
+ const LOCAL_URI_SCHEME = /^local:\/\//i;
20
+ const LOCAL_URI_GUIDANCE = "local:// URIs resolve only inside eval cells via the kernel read()/write() helpers; the read tool takes filesystem paths. Re-read this with the eval read() helper, or retry with the plain absolute file path.";
21
+ const INLINE_IMAGE_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
22
+ const inlineImageAsIs = async (bytes, mimeType) => INLINE_IMAGE_TYPES.has(mimeType)
23
+ ? { ok: true, data: encodeBase64(bytes), mimeType, hints: [] }
24
+ : { ok: false, message: "[Image omitted: could not be converted to a supported inline image format.]" };
25
+ function machineReadPath(machine) {
26
+ const exists = (path) => machine.ops.files.stat({ path }, machine.context(undefined)).then(() => true, () => false);
27
+ return (path, cwd) => resolveReadPathVariants(path, cwd, resolvePosixToolPath, exists);
28
+ }
29
+ /** The default read operations: the shared operation layer, called with the context of one admitted call. */
30
+ function machineReadOperations(machine, signal, detectImageMimeType) {
31
+ const ctx = machine.context(signal);
32
+ return {
33
+ readFile: async (path) => (await machine.ops.files.read({ path }, ctx)).bytes,
34
+ access: async (path) => {
35
+ await machine.ops.files.stat({ path, access: "read" }, ctx);
36
+ },
37
+ detectImageMimeType: detectImageMimeType ??
38
+ (async (path) => {
39
+ const head = await machine.ops.files.read({ path, offset: 0, limitBytes: IMAGE_TYPE_SNIFF_BYTES }, ctx);
40
+ return detectSupportedImageMimeType(head.bytes);
41
+ }),
42
+ };
43
+ }
44
+ function getNonVisionImageNote(model) {
45
+ if (!model || model.input.includes("image")) {
46
+ return undefined;
47
+ }
48
+ return "[Current model does not support images. The image will be omitted from this request.]";
49
+ }
50
+ export function createCoreReadToolDefinition(cwd, options) {
51
+ const autoResizeImages = options.autoResizeImages ?? true;
52
+ const fallbackResizeOptions = options.resizeOptions;
53
+ const contextModel = options.contextModel ?? true;
54
+ const customOps = options.operations;
55
+ const machine = options.machine;
56
+ const filesystemPolicy = options.filesystemPolicy;
57
+ const resolveReadPath = options.resolveReadPath ?? machineReadPath(machine);
58
+ const canonicalizePath = options.canonicalizePath ?? lexicalCanonicalPath;
59
+ const processImage = options.processImage ?? inlineImageAsIs;
60
+ return {
61
+ name: "read",
62
+ workspaceEffect: "read",
63
+ label: "read",
64
+ description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp, bmp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete. A .js or .json file of 100+ lines read without offset/limit may come back folded: bodies are replaced by markers and a footer lists the offset/limit ranges to reread; markers are not source, so reread those ranges before quoting or editing them.`,
65
+ promptSnippet: readToolSystemPromptContribution.snippet,
66
+ promptGuidelines: [...readToolSystemPromptContribution.guidelines],
67
+ fullPromptGuidelines: [...readToolSystemPromptContribution.fullGuidelines],
68
+ parameters: readSchema,
69
+ constrainedSampling: { type: "json_schema", strict: "prefer" },
70
+ async execute(_toolCallId, { path, offset, limit }, signal, _onUpdate, ctx) {
71
+ if (LOCAL_URI_SCHEME.test(path)) {
72
+ throw new Error(LOCAL_URI_GUIDANCE);
73
+ }
74
+ return new Promise((resolve, reject) => {
75
+ if (signal?.aborted) {
76
+ reject(new Error("Operation aborted"));
77
+ return;
78
+ }
79
+ let aborted = false;
80
+ const onAbort = () => {
81
+ aborted = true;
82
+ reject(new Error("Operation aborted"));
83
+ };
84
+ signal?.addEventListener("abort", onAbort, { once: true });
85
+ (async () => {
86
+ try {
87
+ const admitted = machine.begin("files.read", { path, offset, limit }, signal);
88
+ if (admitted !== undefined)
89
+ await admitted;
90
+ if (aborted)
91
+ return;
92
+ const ops = customOps ?? machineReadOperations(machine, signal, options.detectImageMimeType);
93
+ const absolutePath = await resolveReadPath(path, ctx?.cwd || cwd);
94
+ if (aborted)
95
+ return;
96
+ if (filesystemPolicy) {
97
+ const decision = await filesystemPolicy({
98
+ operation: "read",
99
+ canonicalPath: await canonicalizePath(absolutePath),
100
+ toolName: "read",
101
+ });
102
+ if (!decision.allow)
103
+ throw new Error(decision.reason);
104
+ if (aborted)
105
+ return;
106
+ }
107
+ // Check if file exists and is readable.
108
+ await ops.access(absolutePath);
109
+ if (aborted)
110
+ return;
111
+ const mimeType = ops.detectImageMimeType ? await ops.detectImageMimeType(absolutePath) : undefined;
112
+ let content;
113
+ let details;
114
+ const model = contextModel ? ctx?.model : undefined;
115
+ const nonVisionImageNote = getNonVisionImageNote(model);
116
+ if (mimeType) {
117
+ // Read image as binary.
118
+ const buffer = await ops.readFile(absolutePath);
119
+ const processed = await processImage(buffer, mimeType, {
120
+ autoResizeImages,
121
+ resizeOptions: model?.inputLimits?.images?.resize ?? fallbackResizeOptions,
122
+ });
123
+ if (!processed.ok) {
124
+ let textNote = `Read image file [${mimeType}]\n${processed.message}`;
125
+ if (nonVisionImageNote)
126
+ textNote += `\n${nonVisionImageNote}`;
127
+ content = [{ type: "text", text: textNote }];
128
+ }
129
+ else {
130
+ let textNote = `Read image file [${processed.mimeType}]`;
131
+ if (processed.hints.length > 0)
132
+ textNote += `\n${processed.hints.join("\n")}`;
133
+ if (nonVisionImageNote)
134
+ textNote += `\n${nonVisionImageNote}`;
135
+ content = [
136
+ { type: "text", text: textNote },
137
+ { type: "image", data: processed.data, mimeType: processed.mimeType },
138
+ ];
139
+ }
140
+ }
141
+ else {
142
+ // Read text content.
143
+ const buffer = await ops.readFile(absolutePath);
144
+ if (aborted)
145
+ return;
146
+ const shaped = await shapeTextRead({
147
+ path,
148
+ absolutePath,
149
+ textContent: decodeUtf8(buffer),
150
+ offset,
151
+ limit,
152
+ folder: options.folder,
153
+ isAborted: () => aborted,
154
+ });
155
+ if (shaped === undefined)
156
+ return;
157
+ content = shaped.content;
158
+ details = shaped.details;
159
+ }
160
+ if (aborted)
161
+ return;
162
+ signal?.removeEventListener("abort", onAbort);
163
+ resolve({ content, details });
164
+ }
165
+ catch (error) {
166
+ signal?.removeEventListener("abort", onAbort);
167
+ if (!aborted)
168
+ reject(error);
169
+ }
170
+ })();
171
+ });
172
+ },
173
+ };
174
+ }
175
+ /** The read tool for a host whose only effect channel is `machine`; reads fold like the local tool's by default. */
176
+ export function createCoreReadTool(cwd, options) {
177
+ return wrapToolDefinition(createCoreReadToolDefinition(cwd, { folder: selectedReadFolder, ...options }));
178
+ }
179
+ export { readSchema };
180
+ //# sourceMappingURL=read-core.js.map
@@ -0,0 +1,23 @@
1
+ import { type ReadFolder } from "@earendil-works/pi-agent-core/read-folders";
2
+ import type { TextContent } from "@earendil-works/pi-ai";
3
+ import { type TruncationResult } from "./truncate.ts";
4
+ export interface ReadToolDetails {
5
+ truncation?: TruncationResult;
6
+ }
7
+ /**
8
+ * What the model sees for a text file: the selected lines, folded or truncated, with a continuation notice for the
9
+ * model alone. Undefined when the read was aborted meanwhile.
10
+ */
11
+ export declare function shapeTextRead(input: {
12
+ path: string;
13
+ absolutePath: string;
14
+ textContent: string;
15
+ offset: number | undefined;
16
+ limit: number | undefined;
17
+ folder: ReadFolder | undefined;
18
+ isAborted: () => boolean;
19
+ }): Promise<{
20
+ content: TextContent[];
21
+ details: ReadToolDetails | undefined;
22
+ } | undefined>;
23
+ //# sourceMappingURL=read-text.d.ts.map
@@ -0,0 +1,90 @@
1
+ import { createDefaultReadSummary, prepareReadFolder, } from "@earendil-works/pi-agent-core/read-folders";
2
+ import { utf8ByteLength } from "./encoding.js";
3
+ import { modelOnlyText } from "./model-only-text.js";
4
+ import { DEFAULT_MAX_BYTES, formatSize, truncateHead } from "./truncate.js";
5
+ /**
6
+ * What the model sees for a text file: the selected lines, folded or truncated, with a continuation notice for the
7
+ * model alone. Undefined when the read was aborted meanwhile.
8
+ */
9
+ export async function shapeTextRead(input) {
10
+ const { path, absolutePath, textContent, offset, limit, folder, isAborted } = input;
11
+ let details;
12
+ const allLines = textContent.split("\n");
13
+ const totalFileLines = allLines.length;
14
+ // Apply offset if specified. Convert from 1-indexed input to 0-indexed array access.
15
+ const startLine = offset ? Math.max(0, offset - 1) : 0;
16
+ const startLineDisplay = startLine + 1;
17
+ // Check if offset is out of bounds.
18
+ if (startLine >= allLines.length) {
19
+ throw new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);
20
+ }
21
+ let selectedContent;
22
+ let userLimitedLines;
23
+ // If limit is specified by the user, honor it first. Otherwise truncateHead decides.
24
+ if (limit !== undefined) {
25
+ const endLine = Math.min(startLine + limit, allLines.length);
26
+ selectedContent = allLines.slice(startLine, endLine).join("\n");
27
+ userLimitedLines = endLine - startLine;
28
+ }
29
+ else {
30
+ selectedContent = allLines.slice(startLine).join("\n");
31
+ }
32
+ // Apply truncation, respecting both line and byte limits.
33
+ const truncation = truncateHead(selectedContent);
34
+ // A selected grammar loads lazily here, on the first structural read for its language.
35
+ const prepared = await prepareReadFolder(absolutePath, folder);
36
+ if (isAborted())
37
+ return undefined;
38
+ const summary = createDefaultReadSummary({
39
+ path: absolutePath,
40
+ text: textContent,
41
+ offset,
42
+ limit,
43
+ folder: prepared,
44
+ truncated: truncation.truncated,
45
+ });
46
+ let outputText;
47
+ let notice;
48
+ if (summary) {
49
+ outputText = summary.text;
50
+ }
51
+ else if (truncation.firstLineExceedsLimit) {
52
+ // First line alone exceeds the byte limit. Point the model at a bash fallback.
53
+ const firstLineSize = formatSize(utf8ByteLength(allLines[startLine] ?? ""));
54
+ outputText = "";
55
+ notice = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;
56
+ details = { truncation };
57
+ }
58
+ else if (truncation.truncated) {
59
+ // Truncation occurred. Build an actionable continuation notice.
60
+ const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
61
+ const nextOffset = endLineDisplay + 1;
62
+ outputText = truncation.content;
63
+ if (truncation.truncatedBy === "lines") {
64
+ notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;
65
+ }
66
+ else {
67
+ notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue.]`;
68
+ }
69
+ details = { truncation };
70
+ }
71
+ else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
72
+ // User-specified limit stopped early, but the file still has more content.
73
+ const remaining = allLines.length - (startLine + userLimitedLines);
74
+ const nextOffset = startLine + userLimitedLines + 1;
75
+ outputText = truncation.content;
76
+ notice = `[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`;
77
+ }
78
+ else {
79
+ // No truncation and no remaining user-limited content.
80
+ outputText = truncation.content;
81
+ }
82
+ // Text adapters join parts with a newline; retain the original blank line.
83
+ const content = outputText || notice === undefined
84
+ ? [{ type: "text", text: notice === undefined ? outputText : `${outputText}\n` }]
85
+ : [];
86
+ if (notice !== undefined)
87
+ content.push(modelOnlyText(notice));
88
+ return { content, details };
89
+ }
90
+ //# sourceMappingURL=read-text.js.map
@@ -1,53 +1,12 @@
1
- import { type AgentTool, type ReadFolder } from "@earendil-works/pi-agent-core";
2
- import type { ModelImageResizeOptions } from "@earendil-works/pi-ai";
3
- import { type Static, Type } from "typebox";
4
- import type { FilesystemPolicyChecker, ToolDefinition } from "../extensions/types.ts";
5
- import { type ToolMachine } from "./machine.ts";
1
+ import { type AgentTool } from "@earendil-works/pi-agent-core";
2
+ import type { ToolDefinition } from "../extensions/types.ts";
3
+ import { type CoreReadToolOptions, type ReadToolDetails, type readSchema } from "./read-core.ts";
6
4
  import { type ReadRenderState } from "./renderers/read.ts";
7
- import { type TruncationResult } from "./truncate.ts";
8
- declare const readSchema: Type.TObject<{
9
- path: Type.TString;
10
- offset: Type.TOptional<Type.TNumber>;
11
- limit: Type.TOptional<Type.TNumber>;
12
- }>;
13
- export declare const readToolSystemPromptContribution: {
14
- readonly snippet: "Read file contents";
15
- readonly guidelines: readonly ["Files: read, not cat/sed."];
16
- readonly fullGuidelines: readonly ["Use read to examine files instead of cat or sed."];
17
- };
18
- export type ReadToolInput = Static<typeof readSchema>;
19
- export interface ReadToolDetails {
20
- truncation?: TruncationResult;
21
- }
22
- /**
23
- * Pluggable operations for the read tool.
24
- * Override these to delegate file reading to remote systems (for example SSH).
25
- */
26
- export interface ReadOperations {
27
- /** Read file contents as a Buffer */
28
- readFile: (absolutePath: string) => Promise<Buffer>;
29
- /** Check if file is readable (throw if not) */
30
- access: (absolutePath: string) => Promise<void>;
31
- /** Detect image MIME type, return null or undefined for non-images */
32
- detectImageMimeType?: (absolutePath: string) => Promise<string | null | undefined>;
33
- }
34
- export interface ReadToolOptions {
35
- /** Structural folder. Default options select the measured folder; omit here for verbatim reads. */
36
- folder?: ReadFolder;
37
- /** Whether to auto-resize images. Default: true */
38
- autoResizeImages?: boolean;
39
- /** Fallback resize profile when the execution context has no model metadata. */
40
- resizeOptions?: ModelImageResizeOptions;
41
- /** Read the model from the execution context. False for a runtime without a session, whose context refuses `model` (senpi#5223). Default: true */
42
- contextModel?: boolean;
43
- /** Custom operations for file reading. Default: local filesystem */
44
- operations?: ReadOperations;
45
- /** Extension-registered filesystem policy checker. */
46
- filesystemPolicy?: FilesystemPolicyChecker;
5
+ export { type ReadOperations, type ReadToolDetails, type ReadToolInput, readToolSystemPromptContribution, } from "./read-core.ts";
6
+ export interface ReadToolOptions extends Pick<CoreReadToolOptions, "folder" | "autoResizeImages" | "resizeOptions" | "contextModel" | "operations" | "filesystemPolicy"> {
47
7
  /** Operation layer and policy gate. Default: the process-wide local machine. */
48
- machine?: ToolMachine;
8
+ machine?: CoreReadToolOptions["machine"];
49
9
  }
50
10
  export declare function createReadToolDefinition(cwd: string, options?: ReadToolOptions): ToolDefinition<typeof readSchema, ReadToolDetails | undefined, ReadRenderState>;
51
11
  export declare function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema>;
52
- export {};
53
12
  //# sourceMappingURL=read.d.ts.map
@@ -1,229 +1,24 @@
1
- import { createDefaultReadSummary, prepareReadFolder, selectedReadFolder, } from "@earendil-works/pi-agent-core";
2
- import { Type } from "typebox";
1
+ import { selectedReadFolder } from "@earendil-works/pi-agent-core";
3
2
  import { processImage } from "../../utils/image-process.js";
4
3
  import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
5
4
  import { canonicalizeFilesystemPath } from "./filesystem-policy.js";
6
- import { asBuffer, defaultToolMachine } from "./machine.js";
7
- import { modelOnlyText } from "./model-only-text.js";
5
+ import { defaultToolMachine } from "./machine.js";
8
6
  import { resolveReadPathAsync } from "./path-utils.js";
7
+ import { createCoreReadToolDefinition, } from "./read-core.js";
9
8
  import { readRenderers } from "./renderers/read.js";
10
9
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
11
- import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "./truncate.js";
12
- const readSchema = Type.Object({
13
- path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
14
- offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
15
- limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
16
- });
17
- export const readToolSystemPromptContribution = {
18
- snippet: "Read file contents",
19
- guidelines: ["Files: read, not cat/sed."],
20
- fullGuidelines: ["Use read to examine files instead of cat or sed."],
21
- };
22
- const LOCAL_URI_SCHEME = /^local:\/\//i;
23
- const LOCAL_URI_GUIDANCE = "local:// URIs resolve only inside eval cells via the kernel read()/write() helpers; the read tool takes filesystem paths. Re-read this with the eval read() helper, or retry with the plain absolute file path.";
24
- /** The default read operations: the shared operation layer, called with the context of one admitted call. */
25
- function machineReadOperations(machine, signal) {
26
- const ctx = machine.context(signal);
27
- return {
28
- readFile: async (path) => asBuffer((await machine.ops.files.read({ path }, ctx)).bytes),
29
- access: async (path) => {
30
- await machine.ops.files.stat({ path, access: "read" }, ctx);
31
- },
32
- detectImageMimeType: detectSupportedImageMimeTypeFromFile,
33
- };
34
- }
35
- function getNonVisionImageNote(model) {
36
- if (!model || model.input.includes("image")) {
37
- return undefined;
38
- }
39
- return "[Current model does not support images. The image will be omitted from this request.]";
40
- }
10
+ export { readToolSystemPromptContribution, } from "./read-core.js";
41
11
  export function createReadToolDefinition(cwd, options = { folder: selectedReadFolder }) {
42
- const autoResizeImages = options?.autoResizeImages ?? true;
43
- const fallbackResizeOptions = options?.resizeOptions;
44
- const contextModel = options?.contextModel ?? true;
45
- const customOps = options?.operations;
46
- const machine = options?.machine ?? defaultToolMachine();
47
- const filesystemPolicy = options?.filesystemPolicy;
48
12
  return {
49
- ...(options?.operations !== undefined || filesystemPolicy !== undefined ? { serverPinned: true } : {}),
50
- name: "read",
51
- workspaceEffect: "read",
52
- label: "read",
53
- description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp, bmp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete. A .js or .json file of 100+ lines read without offset/limit may come back folded: bodies are replaced by markers and a footer lists the offset/limit ranges to reread; markers are not source, so reread those ranges before quoting or editing them.`,
54
- promptSnippet: readToolSystemPromptContribution.snippet,
55
- promptGuidelines: [...readToolSystemPromptContribution.guidelines],
56
- fullPromptGuidelines: [...readToolSystemPromptContribution.fullGuidelines],
57
- parameters: readSchema,
58
- constrainedSampling: { type: "json_schema", strict: "prefer" },
59
- async execute(_toolCallId, { path, offset, limit }, signal, _onUpdate, ctx) {
60
- if (LOCAL_URI_SCHEME.test(path)) {
61
- throw new Error(LOCAL_URI_GUIDANCE);
62
- }
63
- return new Promise((resolve, reject) => {
64
- if (signal?.aborted) {
65
- reject(new Error("Operation aborted"));
66
- return;
67
- }
68
- let aborted = false;
69
- const onAbort = () => {
70
- aborted = true;
71
- reject(new Error("Operation aborted"));
72
- };
73
- signal?.addEventListener("abort", onAbort, { once: true });
74
- (async () => {
75
- try {
76
- const admitted = machine.begin("files.read", { path, offset, limit }, signal);
77
- if (admitted !== undefined)
78
- await admitted;
79
- if (aborted)
80
- return;
81
- const ops = customOps ?? machineReadOperations(machine, signal);
82
- const absolutePath = await resolveReadPathAsync(path, ctx?.cwd || cwd);
83
- if (aborted)
84
- return;
85
- if (filesystemPolicy) {
86
- const decision = await filesystemPolicy({
87
- operation: "read",
88
- canonicalPath: await canonicalizeFilesystemPath(absolutePath),
89
- toolName: "read",
90
- });
91
- if (!decision.allow)
92
- throw new Error(decision.reason);
93
- if (aborted)
94
- return;
95
- }
96
- // Check if file exists and is readable.
97
- await ops.access(absolutePath);
98
- if (aborted)
99
- return;
100
- const mimeType = ops.detectImageMimeType ? await ops.detectImageMimeType(absolutePath) : undefined;
101
- let content;
102
- let details;
103
- const model = contextModel ? ctx?.model : undefined;
104
- const nonVisionImageNote = getNonVisionImageNote(model);
105
- if (mimeType) {
106
- // Read image as binary.
107
- const buffer = await ops.readFile(absolutePath);
108
- const processed = await processImage(buffer, mimeType, {
109
- autoResizeImages,
110
- resizeOptions: model?.inputLimits?.images?.resize ?? fallbackResizeOptions,
111
- });
112
- if (!processed.ok) {
113
- let textNote = `Read image file [${mimeType}]\n${processed.message}`;
114
- if (nonVisionImageNote)
115
- textNote += `\n${nonVisionImageNote}`;
116
- content = [{ type: "text", text: textNote }];
117
- }
118
- else {
119
- let textNote = `Read image file [${processed.mimeType}]`;
120
- if (processed.hints.length > 0)
121
- textNote += `\n${processed.hints.join("\n")}`;
122
- if (nonVisionImageNote)
123
- textNote += `\n${nonVisionImageNote}`;
124
- content = [
125
- { type: "text", text: textNote },
126
- { type: "image", data: processed.data, mimeType: processed.mimeType },
127
- ];
128
- }
129
- }
130
- else {
131
- // Read text content.
132
- const buffer = await ops.readFile(absolutePath);
133
- if (aborted)
134
- return;
135
- const textContent = buffer.toString("utf-8");
136
- const allLines = textContent.split("\n");
137
- const totalFileLines = allLines.length;
138
- // Apply offset if specified. Convert from 1-indexed input to 0-indexed array access.
139
- const startLine = offset ? Math.max(0, offset - 1) : 0;
140
- const startLineDisplay = startLine + 1;
141
- // Check if offset is out of bounds.
142
- if (startLine >= allLines.length) {
143
- throw new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);
144
- }
145
- let selectedContent;
146
- let userLimitedLines;
147
- // If limit is specified by the user, honor it first. Otherwise truncateHead decides.
148
- if (limit !== undefined) {
149
- const endLine = Math.min(startLine + limit, allLines.length);
150
- selectedContent = allLines.slice(startLine, endLine).join("\n");
151
- userLimitedLines = endLine - startLine;
152
- }
153
- else {
154
- selectedContent = allLines.slice(startLine).join("\n");
155
- }
156
- // Apply truncation, respecting both line and byte limits.
157
- const truncation = truncateHead(selectedContent);
158
- // A selected grammar loads lazily here, on the first structural read for its language.
159
- const folder = await prepareReadFolder(absolutePath, options.folder);
160
- if (aborted)
161
- return;
162
- const summary = createDefaultReadSummary({
163
- path: absolutePath,
164
- text: textContent,
165
- offset,
166
- limit,
167
- folder,
168
- truncated: truncation.truncated,
169
- });
170
- let outputText;
171
- let notice;
172
- if (summary) {
173
- outputText = summary.text;
174
- }
175
- else if (truncation.firstLineExceedsLimit) {
176
- // First line alone exceeds the byte limit. Point the model at a bash fallback.
177
- const firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], "utf-8"));
178
- outputText = "";
179
- notice = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;
180
- details = { truncation };
181
- }
182
- else if (truncation.truncated) {
183
- // Truncation occurred. Build an actionable continuation notice.
184
- const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
185
- const nextOffset = endLineDisplay + 1;
186
- outputText = truncation.content;
187
- if (truncation.truncatedBy === "lines") {
188
- notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;
189
- }
190
- else {
191
- notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue.]`;
192
- }
193
- details = { truncation };
194
- }
195
- else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
196
- // User-specified limit stopped early, but the file still has more content.
197
- const remaining = allLines.length - (startLine + userLimitedLines);
198
- const nextOffset = startLine + userLimitedLines + 1;
199
- outputText = truncation.content;
200
- notice = `[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`;
201
- }
202
- else {
203
- // No truncation and no remaining user-limited content.
204
- outputText = truncation.content;
205
- }
206
- // Text adapters join parts with a newline; retain the original blank line.
207
- content =
208
- outputText || notice === undefined
209
- ? [{ type: "text", text: notice === undefined ? outputText : `${outputText}\n` }]
210
- : [];
211
- if (notice !== undefined)
212
- content.push(modelOnlyText(notice));
213
- }
214
- if (aborted)
215
- return;
216
- signal?.removeEventListener("abort", onAbort);
217
- resolve({ content, details });
218
- }
219
- catch (error) {
220
- signal?.removeEventListener("abort", onAbort);
221
- if (!aborted)
222
- reject(error);
223
- }
224
- })();
225
- });
226
- },
13
+ ...(options.operations !== undefined || options.filesystemPolicy !== undefined ? { serverPinned: true } : {}),
14
+ ...createCoreReadToolDefinition(cwd, {
15
+ ...options,
16
+ machine: options.machine ?? defaultToolMachine(),
17
+ resolveReadPath: resolveReadPathAsync,
18
+ canonicalizePath: canonicalizeFilesystemPath,
19
+ detectImageMimeType: detectSupportedImageMimeTypeFromFile,
20
+ processImage,
21
+ }),
227
22
  ...readRenderers,
228
23
  };
229
24
  }
@@ -6,7 +6,6 @@
6
6
  * tool definition, so the tool's public shape is unchanged.
7
7
  */
8
8
  import type { ToolDefinition } from "../../extensions/types.ts";
9
- export declare const BASH_UPDATE_THROTTLE_MS = 100;
10
9
  /** Shell renderers are shared by bash and powershell, which differ only in the prompt they display. */
11
10
  export declare function createShellRenderers(prompt: string): Pick<ToolDefinition<any, any>, "renderCall" | "renderResult">;
12
11
  //# sourceMappingURL=bash.d.ts.map
@@ -11,7 +11,6 @@ import { truncateToVisualLines } from "../../../modes/interactive/components/vis
11
11
  import { highlightCode, theme } from "../../../modes/interactive/theme/theme.js";
12
12
  import { getTextOutput, invalidArgText, normalizeDisplayText, replaceTabs, str } from "../render-utils.js";
13
13
  const BASH_PREVIEW_LINES = 5;
14
- export const BASH_UPDATE_THROTTLE_MS = 100;
15
14
  class BashResultRenderComponent extends Container {
16
15
  constructor() {
17
16
  super(...arguments);
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { Box } from "@earendil-works/pi-tui";
9
9
  import type { ToolDefinition } from "../../extensions/types.ts";
10
- import { type EditDiffError, type EditDiffResult } from "../edit-diff.ts";
10
+ import type { EditDiffError, EditDiffResult } from "../edit-diff.ts";
11
11
  type EditPreview = EditDiffResult | EditDiffError;
12
12
  export type EditRenderState = {
13
13
  callComponent?: EditCallRenderComponent;
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
9
9
  import { countDiffChanges, renderToolDiff } from "../diff-render.js";
10
- import { computeEditsDiff } from "../edit-diff.js";
10
+ import { computeEditsDiff } from "../edit-preview.js";
11
11
  import { renderToolPath, str } from "../render-utils.js";
12
12
  function createEditCallRenderComponent() {
13
13
  return Object.assign(new Box(1, 1, (text) => text), {
@@ -1,3 +1,4 @@
1
+ import { decodeBase64UrlUtf8 } from "./encoding.js";
1
2
  const REDACTED = "<redacted>";
2
3
  const TOKEN_CHAR = /[A-Za-z0-9_./+~=-]/;
3
4
  const SECRET_KEY = /(?:^|_)(?:KEY|TOKEN|SECRET|AUTH|AUTHORIZATION)$|PASSWORD/i;
@@ -32,7 +33,7 @@ function isJwt(token) {
32
33
  if (!header)
33
34
  return false;
34
35
  try {
35
- const value = JSON.parse(Buffer.from(header, "base64url").toString("utf8"));
36
+ const value = JSON.parse(decodeBase64UrlUtf8(header));
36
37
  return typeof value === "object" && value !== null && "alg" in value && typeof value.alg === "string";
37
38
  }
38
39
  catch {