kritya 0.8.2-beta

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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +434 -0
  3. package/dist/agent/budget.js +25 -0
  4. package/dist/agent/compactor.js +78 -0
  5. package/dist/agent/contextWarning.js +6 -0
  6. package/dist/agent/killSwitch.js +111 -0
  7. package/dist/agent/loop.js +543 -0
  8. package/dist/agent/memory.js +145 -0
  9. package/dist/agent/plugins.js +408 -0
  10. package/dist/agent/skills.js +210 -0
  11. package/dist/agent/skillsCli.js +51 -0
  12. package/dist/agent/systemPrompt.js +122 -0
  13. package/dist/agent/tokens.js +55 -0
  14. package/dist/agent/toolExecutor.js +331 -0
  15. package/dist/agent/workflow.js +496 -0
  16. package/dist/agent/worktree.js +116 -0
  17. package/dist/atomicWrite.js +109 -0
  18. package/dist/audit/audit.js +230 -0
  19. package/dist/audit/cli.js +135 -0
  20. package/dist/commands/custom.js +69 -0
  21. package/dist/commands/mcpCommand.js +433 -0
  22. package/dist/commands/pluginsCommand.js +204 -0
  23. package/dist/commands/registry.js +739 -0
  24. package/dist/config/config.js +214 -0
  25. package/dist/config/debug.js +19 -0
  26. package/dist/config/models.js +82 -0
  27. package/dist/config/retention.js +24 -0
  28. package/dist/config/winAcl.js +38 -0
  29. package/dist/crash.js +102 -0
  30. package/dist/engine.js +113 -0
  31. package/dist/git/git.js +46 -0
  32. package/dist/headless.js +274 -0
  33. package/dist/hooks/hooks.js +122 -0
  34. package/dist/index.js +670 -0
  35. package/dist/lsp/client.js +413 -0
  36. package/dist/lsp/manager.js +0 -0
  37. package/dist/lsp/registry.js +62 -0
  38. package/dist/mcp/callback.js +141 -0
  39. package/dist/mcp/client.js +945 -0
  40. package/dist/mcp/login.js +117 -0
  41. package/dist/mcp/oauth.js +345 -0
  42. package/dist/mcp/servers.js +114 -0
  43. package/dist/mcp/spawnWin.js +86 -0
  44. package/dist/mcp/tokens.js +97 -0
  45. package/dist/mcp/transport.js +295 -0
  46. package/dist/net/urlSafety.js +158 -0
  47. package/dist/permissions/danger.js +96 -0
  48. package/dist/permissions/permissions.js +62 -0
  49. package/dist/permissions/rules.js +69 -0
  50. package/dist/plugins/discover.js +108 -0
  51. package/dist/plugins/mcp.js +80 -0
  52. package/dist/provider/client.js +460 -0
  53. package/dist/provider/switchyardClient.js +46 -0
  54. package/dist/provider/switchyardSidecar.js +245 -0
  55. package/dist/provider/textToolCalls.js +140 -0
  56. package/dist/repomap/repoMap.js +168 -0
  57. package/dist/repomap/symbols.js +190 -0
  58. package/dist/session/store.js +328 -0
  59. package/dist/shell/background.js +131 -0
  60. package/dist/shell/sandbox.js +396 -0
  61. package/dist/telemetry/metrics.js +172 -0
  62. package/dist/telemetry/otlp.js +128 -0
  63. package/dist/telemetry/tracer.js +240 -0
  64. package/dist/tools/askUser.js +64 -0
  65. package/dist/tools/bg.js +59 -0
  66. package/dist/tools/common.js +158 -0
  67. package/dist/tools/deepResearch.js +126 -0
  68. package/dist/tools/diff.js +70 -0
  69. package/dist/tools/document/docx.js +44 -0
  70. package/dist/tools/document/pdf.js +190 -0
  71. package/dist/tools/document/pptx.js +148 -0
  72. package/dist/tools/document/types.js +26 -0
  73. package/dist/tools/document/xlsx.js +87 -0
  74. package/dist/tools/document.js +389 -0
  75. package/dist/tools/edit.js +56 -0
  76. package/dist/tools/fetchUrl.js +218 -0
  77. package/dist/tools/fuzzyMatch.js +74 -0
  78. package/dist/tools/glob.js +34 -0
  79. package/dist/tools/grep.js +85 -0
  80. package/dist/tools/ignore.js +28 -0
  81. package/dist/tools/index.js +66 -0
  82. package/dist/tools/ls.js +27 -0
  83. package/dist/tools/lsp.js +239 -0
  84. package/dist/tools/notebook.js +215 -0
  85. package/dist/tools/read.js +32 -0
  86. package/dist/tools/repoMap.js +26 -0
  87. package/dist/tools/secretScan.js +143 -0
  88. package/dist/tools/shell.js +134 -0
  89. package/dist/tools/skills.js +69 -0
  90. package/dist/tools/subagent.js +57 -0
  91. package/dist/tools/tasks.js +43 -0
  92. package/dist/tools/webSearch.js +97 -0
  93. package/dist/tools/write.js +47 -0
  94. package/dist/tools/writeAgent.js +72 -0
  95. package/dist/trust/aiDisclosure.js +45 -0
  96. package/dist/trust/mcpTrust.js +111 -0
  97. package/dist/trust/trust.js +231 -0
  98. package/dist/types.js +1 -0
  99. package/dist/ui/AiDisclosurePrompt.js +23 -0
  100. package/dist/ui/App.js +449 -0
  101. package/dist/ui/Banner.js +71 -0
  102. package/dist/ui/ElicitationPrompt.js +46 -0
  103. package/dist/ui/Markdown.js +148 -0
  104. package/dist/ui/McpTrustPrompt.js +42 -0
  105. package/dist/ui/ModelPicker.js +19 -0
  106. package/dist/ui/PermissionPrompt.js +27 -0
  107. package/dist/ui/SelectList.js +24 -0
  108. package/dist/ui/Spinner.js +12 -0
  109. package/dist/ui/StatusLine.js +11 -0
  110. package/dist/ui/TranscriptItem.js +13 -0
  111. package/dist/ui/TrustPrompt.js +12 -0
  112. package/dist/ui/highlight.js +137 -0
  113. package/dist/ui/inline.js +241 -0
  114. package/dist/ui/mermaid.js +98 -0
  115. package/dist/ui/table.js +148 -0
  116. package/dist/ui/toolOutputPreview.js +34 -0
  117. package/dist/ui/useAgent.js +534 -0
  118. package/dist/ui/useKillSwitch.js +65 -0
  119. package/dist/ui/useSessionResume.js +39 -0
  120. package/dist/ui/useUsageBudget.js +149 -0
  121. package/dist/ui/viewport.js +71 -0
  122. package/dist/undo/undo.js +293 -0
  123. package/dist/version.js +4 -0
  124. package/package.json +122 -0
@@ -0,0 +1,87 @@
1
+ import ExcelJS from "exceljs";
2
+ const CELL_REF_RE = /^[A-Za-z]{1,3}[1-9][0-9]*$/;
3
+ export async function readXlsx(buf) {
4
+ const workbook = new ExcelJS.Workbook();
5
+ // exceljs's bundled types pin an older @types/node Buffer shape; the
6
+ // runtime accepts any Node Buffer.
7
+ await workbook.xlsx.load(buf);
8
+ const parts = [];
9
+ workbook.eachSheet((sheet) => {
10
+ parts.push(`## ${sheet.name}`);
11
+ sheet.eachRow((row) => {
12
+ const cells = row.values.slice(1).map((v) => cellToString(v));
13
+ parts.push(cells.join("\t"));
14
+ });
15
+ parts.push("");
16
+ });
17
+ return parts.join("\n");
18
+ }
19
+ function cellToString(v) {
20
+ if (v === null || v === undefined)
21
+ return "";
22
+ if (typeof v === "object" && "text" in v) {
23
+ return String(v.text);
24
+ }
25
+ if (typeof v === "object" && "result" in v) {
26
+ return String(v.result);
27
+ }
28
+ return String(v);
29
+ }
30
+ /** Display an existing or new cell value as a short string, including formulas. */
31
+ function describeValue(v) {
32
+ if (v === null || v === undefined)
33
+ return "";
34
+ if (typeof v === "object" && "formula" in v) {
35
+ return "=" + String(v.formula);
36
+ }
37
+ return cellToString(v);
38
+ }
39
+ /** Convert a model-supplied value into what exceljs expects; a leading "=" becomes a formula. */
40
+ function toCellValue(value) {
41
+ if (typeof value === "string" && value.startsWith("=")) {
42
+ return { formula: value.slice(1), date1904: false };
43
+ }
44
+ return value;
45
+ }
46
+ /**
47
+ * Change specific cells of an existing workbook in place, preserving every
48
+ * other cell, formula, and sheet. Returns the new file bytes and a record of
49
+ * what changed (old -> new) for the permission preview and result message.
50
+ */
51
+ export async function editXlsx(buf, edits) {
52
+ const workbook = new ExcelJS.Workbook();
53
+ await workbook.xlsx.load(buf);
54
+ const applied = [];
55
+ for (const edit of edits) {
56
+ if (!CELL_REF_RE.test(edit.cell)) {
57
+ throw new Error(`Invalid cell reference "${edit.cell}" (expected A1 notation like "B7")`);
58
+ }
59
+ const ws = edit.sheet ? workbook.getWorksheet(edit.sheet) : workbook.worksheets[0];
60
+ if (!ws) {
61
+ const names = workbook.worksheets.map((w) => w.name).join(", ");
62
+ throw new Error(`Worksheet "${edit.sheet}" not found. Sheets: ${names}`);
63
+ }
64
+ const cell = ws.getCell(edit.cell.toUpperCase());
65
+ const oldValue = describeValue(cell.value);
66
+ cell.value = toCellValue(edit.value);
67
+ applied.push({
68
+ sheet: ws.name,
69
+ cell: edit.cell.toUpperCase(),
70
+ oldValue,
71
+ newValue: describeValue(cell.value),
72
+ });
73
+ }
74
+ const arrayBuffer = await workbook.xlsx.writeBuffer();
75
+ return { buf: Buffer.from(arrayBuffer), applied };
76
+ }
77
+ export async function writeXlsx(sheets) {
78
+ const workbook = new ExcelJS.Workbook();
79
+ for (const sheet of sheets) {
80
+ const ws = workbook.addWorksheet(sheet.name);
81
+ for (const row of sheet.rows) {
82
+ ws.addRow(row);
83
+ }
84
+ }
85
+ const arrayBuffer = await workbook.xlsx.writeBuffer();
86
+ return Buffer.from(arrayBuffer);
87
+ }
@@ -0,0 +1,389 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { writeFileAtomic } from "../atomicWrite.js";
4
+ import { resolveSafe, truncateResult } from "./common.js";
5
+ import { readDocx, writeDocx } from "./document/docx.js";
6
+ import { readXlsx, writeXlsx, editXlsx } from "./document/xlsx.js";
7
+ import { readPptx, writePptx } from "./document/pptx.js";
8
+ import { readPdf, writePdf, editPdf } from "./document/pdf.js";
9
+ const READABLE_EXTENSIONS = [".docx", ".xlsx", ".pptx", ".pdf"];
10
+ const BLOCK_SCHEMA = {
11
+ type: "array",
12
+ description: "Flowed content blocks, in document order.",
13
+ items: {
14
+ type: "object",
15
+ properties: {
16
+ type: {
17
+ type: "string",
18
+ enum: ["heading1", "heading2", "heading3", "paragraph", "bullet", "numbered"],
19
+ },
20
+ text: { type: "string" },
21
+ },
22
+ required: ["type", "text"],
23
+ },
24
+ };
25
+ export const readDocumentTool = {
26
+ name: "read_document",
27
+ description: "Read a Word (.docx), Excel (.xlsx), PowerPoint (.pptx), or PDF (.pdf) file from the " +
28
+ "workspace and return its text content. For plain text, Markdown, or CSV files use read_file instead.",
29
+ parameters: {
30
+ type: "object",
31
+ properties: {
32
+ path: { type: "string", description: "File path relative to the workspace root" },
33
+ },
34
+ required: ["path"],
35
+ },
36
+ requiresPermission: false,
37
+ summarize: (args) => `Read ${args.path}`,
38
+ async execute(args, ctx) {
39
+ const relPath = String(args.path);
40
+ const abs = resolveSafe(ctx.workspace, relPath);
41
+ const ext = path.extname(abs).toLowerCase();
42
+ const buf = await fs.readFile(abs);
43
+ switch (ext) {
44
+ case ".docx":
45
+ return truncateResult(await readDocx(buf));
46
+ case ".xlsx":
47
+ return truncateResult(await readXlsx(buf));
48
+ case ".pptx":
49
+ return truncateResult(await readPptx(buf));
50
+ case ".pdf":
51
+ return truncateResult(await readPdf(buf));
52
+ default:
53
+ throw new Error(`Unsupported extension "${ext}" for read_document. Supported: ` +
54
+ `${READABLE_EXTENSIONS.join(", ")}. For text formats such as .md, .txt, or .csv, use read_file.`);
55
+ }
56
+ },
57
+ };
58
+ export const writeDocumentTool = {
59
+ name: "write_document",
60
+ description: "Create or overwrite a Word (.docx), Excel (.xlsx), PowerPoint (.pptx), or PDF (.pdf) file " +
61
+ "in the workspace from structured content. Always replaces the whole document. " +
62
+ "For .docx and .pdf, pass `content.blocks`. For .xlsx, pass `content.sheets`. For .pptx, pass `content.slides`.",
63
+ parameters: {
64
+ type: "object",
65
+ properties: {
66
+ path: { type: "string", description: "File path relative to the workspace root" },
67
+ content: {
68
+ type: "object",
69
+ properties: {
70
+ blocks: BLOCK_SCHEMA,
71
+ sheets: {
72
+ type: "array",
73
+ description: "Worksheets, for .xlsx.",
74
+ items: {
75
+ type: "object",
76
+ properties: {
77
+ name: { type: "string" },
78
+ rows: {
79
+ type: "array",
80
+ items: {
81
+ type: "array",
82
+ items: { type: ["string", "number", "boolean", "null"] },
83
+ },
84
+ },
85
+ },
86
+ required: ["name", "rows"],
87
+ },
88
+ },
89
+ slides: {
90
+ type: "array",
91
+ description: "Slides, for .pptx. Every slide needs its body text in `bullets` — a slide with " +
92
+ "only a `title` renders as a single line on an otherwise empty slide.",
93
+ items: {
94
+ type: "object",
95
+ properties: {
96
+ title: { type: "string", description: "Slide heading, one short line." },
97
+ bullets: {
98
+ type: "array",
99
+ items: { type: "string" },
100
+ description: "The slide's body, one string per bullet. Put all visible content here; " +
101
+ "aim for 3-6 bullets of at most ~15 words each.",
102
+ },
103
+ notes: {
104
+ type: "string",
105
+ description: "Speaker notes; not shown on the slide itself.",
106
+ },
107
+ },
108
+ required: ["title", "bullets"],
109
+ },
110
+ },
111
+ },
112
+ },
113
+ },
114
+ required: ["path", "content"],
115
+ },
116
+ requiresPermission: true,
117
+ summarize: (args) => `Write ${args.path}`,
118
+ async preview(args) {
119
+ const content = extractContent(args);
120
+ if (content.blocks)
121
+ return `${content.blocks.length} content block(s)`;
122
+ if (content.sheets)
123
+ return `${content.sheets.length} sheet(s)`;
124
+ if (content.slides)
125
+ return `${content.slides.length} slide(s)`;
126
+ return null;
127
+ },
128
+ async execute(args, ctx) {
129
+ const relPath = String(args.path);
130
+ const abs = resolveSafe(ctx.workspace, relPath);
131
+ const ext = path.extname(abs).toLowerCase();
132
+ const content = extractContent(args);
133
+ let buf;
134
+ switch (ext) {
135
+ case ".docx":
136
+ buf = await writeDocx(requireField(content.blocks, "blocks", ext, args));
137
+ break;
138
+ case ".pdf":
139
+ buf = await writePdf(requireField(content.blocks, "blocks", ext, args));
140
+ break;
141
+ case ".xlsx":
142
+ buf = await writeXlsx(requireField(content.sheets, "sheets", ext, args));
143
+ break;
144
+ case ".pptx":
145
+ buf = await writePptx(requireField(content.slides, "slides", ext, args));
146
+ break;
147
+ default:
148
+ throw new Error(`Unsupported extension "${ext}" for write_document. Supported: ` +
149
+ `${READABLE_EXTENSIONS.join(", ")}. For text formats such as .md, .txt, or .csv, use write_file.`);
150
+ }
151
+ await fs.mkdir(path.dirname(abs), { recursive: true });
152
+ ctx.undo?.snapshot(abs, relPath);
153
+ await writeFileAtomic(abs, buf);
154
+ return `Wrote ${relPath}`;
155
+ },
156
+ };
157
+ export const editSpreadsheetTool = {
158
+ name: "edit_spreadsheet",
159
+ description: "Change specific cells of an existing Excel (.xlsx) file in place, leaving every other cell, " +
160
+ "formula, and sheet untouched. Pass one or more edits, each with a cell in A1 notation and a " +
161
+ 'new value; a value beginning with "=" is stored as a formula. Use read_document first to ' +
162
+ "see the sheet. To create a new spreadsheet or replace one wholesale, use write_document instead.",
163
+ parameters: {
164
+ type: "object",
165
+ properties: {
166
+ path: { type: "string", description: "File path relative to the workspace root" },
167
+ edits: {
168
+ type: "array",
169
+ description: "Cell changes to apply, in order.",
170
+ items: {
171
+ type: "object",
172
+ properties: {
173
+ sheet: {
174
+ type: "string",
175
+ description: "Sheet name; defaults to the first worksheet if omitted.",
176
+ },
177
+ cell: { type: "string", description: 'Cell in A1 notation, e.g. "B7".' },
178
+ value: {
179
+ type: ["string", "number", "boolean", "null"],
180
+ description: 'New value; a string starting with "=" becomes a formula.',
181
+ },
182
+ },
183
+ required: ["cell", "value"],
184
+ },
185
+ },
186
+ },
187
+ required: ["path", "edits"],
188
+ },
189
+ requiresPermission: true,
190
+ summarize: (args) => {
191
+ const edits = (args.edits ?? []);
192
+ return `Edit ${args.path} (${edits.length} cell${edits.length === 1 ? "" : "s"})`;
193
+ },
194
+ async preview(args, ctx) {
195
+ const edits = (args.edits ?? []);
196
+ if (edits.length === 0)
197
+ return null;
198
+ try {
199
+ const abs = resolveSafe(ctx.workspace, String(args.path));
200
+ const buf = await fs.readFile(abs);
201
+ const { applied } = await editXlsx(buf, edits);
202
+ return applied
203
+ .map((a) => `${a.sheet}!${a.cell}: ${a.oldValue || "(empty)"} → ${a.newValue}`)
204
+ .join("\n");
205
+ }
206
+ catch {
207
+ // Fall back to a value-only preview if the file can't be read yet.
208
+ return edits
209
+ .map((e) => `${e.sheet ? e.sheet + "!" : ""}${e.cell} → ${String(e.value)}`)
210
+ .join("\n");
211
+ }
212
+ },
213
+ async execute(args, ctx) {
214
+ const relPath = String(args.path);
215
+ const abs = resolveSafe(ctx.workspace, relPath);
216
+ const ext = path.extname(abs).toLowerCase();
217
+ if (ext !== ".xlsx") {
218
+ throw new Error(`edit_spreadsheet only supports .xlsx files, got "${ext}"`);
219
+ }
220
+ const edits = (args.edits ?? []);
221
+ if (edits.length === 0)
222
+ throw new Error("edit_spreadsheet: `edits` must be non-empty");
223
+ const buf = await fs.readFile(abs);
224
+ const { buf: out, applied } = await editXlsx(buf, edits);
225
+ ctx.undo?.snapshot(abs, relPath);
226
+ await writeFileAtomic(abs, out);
227
+ const detail = applied.map((a) => `${a.sheet}!${a.cell}=${a.newValue}`).join(", ");
228
+ return `Updated ${applied.length} cell(s) in ${relPath}: ${detail}`;
229
+ },
230
+ };
231
+ /** Build a validated PdfEdit from the tool arguments. */
232
+ function toPdfEdit(args) {
233
+ const op = String(args.op);
234
+ const asPages = (v, field) => {
235
+ if (!Array.isArray(v) || v.length === 0) {
236
+ throw new Error(`edit_pdf: "${field}" must be a non-empty array of page numbers`);
237
+ }
238
+ return v.map(Number);
239
+ };
240
+ switch (op) {
241
+ case "delete_pages":
242
+ return { op, pages: asPages(args.pages, "pages") };
243
+ case "extract_pages":
244
+ return { op, pages: asPages(args.pages, "pages") };
245
+ case "reorder_pages":
246
+ return { op, order: asPages(args.order, "order") };
247
+ case "rotate_page":
248
+ if (args.page === undefined || args.degrees === undefined) {
249
+ throw new Error('edit_pdf: rotate_page requires "page" and "degrees"');
250
+ }
251
+ return { op, page: Number(args.page), degrees: Number(args.degrees) };
252
+ default:
253
+ throw new Error(`edit_pdf: unknown op "${op}"`);
254
+ }
255
+ }
256
+ export const editPdfTool = {
257
+ name: "edit_pdf",
258
+ description: "Modify the pages of an existing PDF (.pdf) in place: delete_pages, rotate_page, " +
259
+ "reorder_pages, or extract_pages (write chosen pages to a new file). Page numbers are " +
260
+ "1-based. This does structural page operations only — PDF text cannot be reliably " +
261
+ "find-and-replaced. To create a PDF from text, use write_document.",
262
+ parameters: {
263
+ type: "object",
264
+ properties: {
265
+ path: { type: "string", description: "File path relative to the workspace root" },
266
+ op: {
267
+ type: "string",
268
+ enum: ["delete_pages", "rotate_page", "reorder_pages", "extract_pages"],
269
+ },
270
+ pages: {
271
+ type: "array",
272
+ items: { type: "number" },
273
+ description: "1-based page numbers, for delete_pages and extract_pages.",
274
+ },
275
+ page: { type: "number", description: "1-based page number, for rotate_page." },
276
+ degrees: {
277
+ type: "number",
278
+ description: "Rotation in multiples of 90 (added to the page's current angle), for rotate_page.",
279
+ },
280
+ order: {
281
+ type: "array",
282
+ items: { type: "number" },
283
+ description: "New page order as a permutation of every page 1..N, for reorder_pages.",
284
+ },
285
+ out_path: {
286
+ type: "string",
287
+ description: "Destination for extract_pages (a new .pdf); the source file is left unchanged.",
288
+ },
289
+ },
290
+ required: ["path", "op"],
291
+ },
292
+ requiresPermission: true,
293
+ summarize: (args) => `Edit ${args.path} (${args.op})`,
294
+ async preview(args, ctx) {
295
+ try {
296
+ const abs = resolveSafe(ctx.workspace, String(args.path));
297
+ const buf = await fs.readFile(abs);
298
+ const { summary } = await editPdf(buf, toPdfEdit(args));
299
+ return summary;
300
+ }
301
+ catch (err) {
302
+ return err instanceof Error ? err.message : null;
303
+ }
304
+ },
305
+ async execute(args, ctx) {
306
+ const relPath = String(args.path);
307
+ const abs = resolveSafe(ctx.workspace, relPath);
308
+ if (path.extname(abs).toLowerCase() !== ".pdf") {
309
+ throw new Error(`edit_pdf only supports .pdf files, got "${path.extname(abs)}"`);
310
+ }
311
+ const edit = toPdfEdit(args);
312
+ const buf = await fs.readFile(abs);
313
+ const { buf: out, summary } = await editPdf(buf, edit);
314
+ // extract_pages writes a new file and leaves the source untouched; every other op edits in place.
315
+ if (edit.op === "extract_pages") {
316
+ if (args.out_path === undefined)
317
+ throw new Error("extract_pages requires `out_path`");
318
+ const outRel = String(args.out_path);
319
+ const outAbs = resolveSafe(ctx.workspace, outRel);
320
+ if (path.extname(outAbs).toLowerCase() !== ".pdf") {
321
+ throw new Error(`out_path must be a .pdf file, got "${path.extname(outAbs)}"`);
322
+ }
323
+ await fs.mkdir(path.dirname(outAbs), { recursive: true });
324
+ ctx.undo?.snapshot(outAbs, outRel);
325
+ await writeFileAtomic(outAbs, out);
326
+ return `${summary} → ${outRel}`;
327
+ }
328
+ ctx.undo?.snapshot(abs, relPath);
329
+ await writeFileAtomic(abs, out);
330
+ return `${summary} in ${relPath}`;
331
+ },
332
+ };
333
+ /** The payload field each extension is written from. */
334
+ const CONTENT_FIELD = {
335
+ ".docx": "blocks",
336
+ ".pdf": "blocks",
337
+ ".xlsx": "sheets",
338
+ ".pptx": "slides",
339
+ };
340
+ /**
341
+ * Pull blocks/sheets/slides out of the arguments however the model arranged
342
+ * them. The documented shape is `{path, content: {slides: [...]}}`, but models
343
+ * routinely flatten it to `{path, slides: [...]}` or send `content` as a JSON
344
+ * string. Rejecting those was a dead end for the caller: the payload was right
345
+ * there and the write failed anyway.
346
+ */
347
+ function extractContent(args) {
348
+ let raw = args.content;
349
+ if (typeof raw === "string") {
350
+ try {
351
+ raw = JSON.parse(raw);
352
+ }
353
+ catch {
354
+ raw = undefined; // fall through to the top-level fields
355
+ }
356
+ }
357
+ const nested = (raw && typeof raw === "object" ? raw : {});
358
+ const pick = (key) => {
359
+ for (const source of [nested, args]) {
360
+ const value = source[key];
361
+ if (Array.isArray(value) && value.length > 0)
362
+ return value;
363
+ }
364
+ return undefined;
365
+ };
366
+ return {
367
+ blocks: pick("blocks"),
368
+ sheets: pick("sheets"),
369
+ slides: pick("slides"),
370
+ };
371
+ }
372
+ function requireField(value, fieldName, ext, args) {
373
+ if (!value || value.length === 0) {
374
+ // Name what did arrive: the usual cause is content for a different
375
+ // extension, e.g. `blocks` sent to a .pptx.
376
+ const present = ["blocks", "sheets", "slides"].filter((k) => extractContent(args)[k]?.length);
377
+ const mismatch = present.length
378
+ ? ` Received "${present.join('", "')}" instead — that content belongs to ` +
379
+ `${present
380
+ .map((k) => Object.keys(CONTENT_FIELD)
381
+ .filter((e) => CONTENT_FIELD[e] === k)
382
+ .join("/"))
383
+ .join(", ")} files.`
384
+ : "";
385
+ throw new Error(`write_document: ${ext} files are written from "${fieldName}", which is required and must be ` +
386
+ `non-empty. Pass it as content.${fieldName} (a top-level "${fieldName}" is also accepted).${mismatch}`);
387
+ }
388
+ return value;
389
+ }
@@ -0,0 +1,56 @@
1
+ import fs from "node:fs/promises";
2
+ import { writeFileAtomic } from "../atomicWrite.js";
3
+ import { resolveSafe } from "./common.js";
4
+ import { diffLines } from "./diff.js";
5
+ import { applyEdit } from "./fuzzyMatch.js";
6
+ import { formatSecretWarning, scanForSecrets } from "./secretScan.js";
7
+ export const editFileTool = {
8
+ name: "edit_file",
9
+ description: "Replace a string in a file. Prefer an exact match of old_string (including whitespace); " +
10
+ "if the exact text isn't found, a whitespace-tolerant line match is attempted as a fallback. " +
11
+ "old_string must be unique in the file unless replace_all is true.",
12
+ parameters: {
13
+ type: "object",
14
+ properties: {
15
+ path: { type: "string", description: "File path relative to the workspace root" },
16
+ old_string: { type: "string", description: "Exact text to find" },
17
+ new_string: { type: "string", description: "Text to replace it with" },
18
+ replace_all: {
19
+ type: "boolean",
20
+ description: "Replace every occurrence instead of requiring uniqueness",
21
+ },
22
+ },
23
+ required: ["path", "old_string", "new_string"],
24
+ },
25
+ requiresPermission: true,
26
+ resultSummary: (output) => output.split("\n")[0].replace(/ in \S+$/, ""),
27
+ summarize: (args) => `Edit ${args.path}`,
28
+ async preview(args) {
29
+ return diffLines(String(args.old_string ?? ""), String(args.new_string ?? ""));
30
+ },
31
+ async execute(args, ctx) {
32
+ const abs = resolveSafe(ctx.workspace, String(args.path));
33
+ const oldStr = String(args.old_string);
34
+ const newStr = String(args.new_string);
35
+ const replaceAll = Boolean(args.replace_all);
36
+ if (oldStr === newStr)
37
+ throw new Error("old_string and new_string are identical");
38
+ const secrets = scanForSecrets(newStr);
39
+ if (secrets.length > 0) {
40
+ throw new Error(formatSecretWarning(secrets, String(args.path)));
41
+ }
42
+ const content = await fs.readFile(abs, "utf8");
43
+ const match = applyEdit(content, oldStr, newStr, replaceAll);
44
+ if (!match.matched) {
45
+ throw new Error(`old_string not found in ${args.path} (tried exact and whitespace-tolerant matching). ` +
46
+ `Read the file and retry with text copied from it.`);
47
+ }
48
+ if (match.count > 1 && !replaceAll) {
49
+ throw new Error(`old_string occurs ${match.count} times in ${args.path}. Provide a longer unique string or set replace_all.`);
50
+ }
51
+ ctx.undo?.snapshot(abs, String(args.path));
52
+ await writeFileAtomic(abs, match.result);
53
+ const fuzzy = match.strategy === "line-trimmed" ? " (matched ignoring whitespace)" : "";
54
+ return `Replaced ${replaceAll ? match.count : 1} occurrence(s) in ${args.path}${fuzzy}`;
55
+ },
56
+ };