@documonster/mcp 0.8.0-canary.sha.09791dab

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 (127) hide show
  1. package/LICENSE +205 -0
  2. package/README.md +253 -0
  3. package/dist/capabilities.d.ts +29 -0
  4. package/dist/capabilities.d.ts.map +1 -0
  5. package/dist/capabilities.js +188 -0
  6. package/dist/capabilities.js.map +1 -0
  7. package/dist/cli.d.ts +13 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +64 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/config.d.ts +65 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +189 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/errors.d.ts +66 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +96 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/index.d.ts +25 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +20 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/sandbox.d.ts +86 -0
  24. package/dist/sandbox.d.ts.map +1 -0
  25. package/dist/sandbox.js +224 -0
  26. package/dist/sandbox.js.map +1 -0
  27. package/dist/server.d.ts +22 -0
  28. package/dist/server.d.ts.map +1 -0
  29. package/dist/server.js +66 -0
  30. package/dist/server.js.map +1 -0
  31. package/dist/tools/archive-read.d.ts +31 -0
  32. package/dist/tools/archive-read.d.ts.map +1 -0
  33. package/dist/tools/archive-read.js +315 -0
  34. package/dist/tools/archive-read.js.map +1 -0
  35. package/dist/tools/archive-write.d.ts +9 -0
  36. package/dist/tools/archive-write.d.ts.map +1 -0
  37. package/dist/tools/archive-write.js +219 -0
  38. package/dist/tools/archive-write.js.map +1 -0
  39. package/dist/tools/chart.d.ts +93 -0
  40. package/dist/tools/chart.d.ts.map +1 -0
  41. package/dist/tools/chart.js +263 -0
  42. package/dist/tools/chart.js.map +1 -0
  43. package/dist/tools/doc-convert.d.ts +13 -0
  44. package/dist/tools/doc-convert.d.ts.map +1 -0
  45. package/dist/tools/doc-convert.js +209 -0
  46. package/dist/tools/doc-convert.js.map +1 -0
  47. package/dist/tools/doc-paginate.d.ts +14 -0
  48. package/dist/tools/doc-paginate.d.ts.map +1 -0
  49. package/dist/tools/doc-paginate.js +153 -0
  50. package/dist/tools/doc-paginate.js.map +1 -0
  51. package/dist/tools/doc-read.d.ts +14 -0
  52. package/dist/tools/doc-read.d.ts.map +1 -0
  53. package/dist/tools/doc-read.js +175 -0
  54. package/dist/tools/doc-read.js.map +1 -0
  55. package/dist/tools/doc-review.d.ts +17 -0
  56. package/dist/tools/doc-review.d.ts.map +1 -0
  57. package/dist/tools/doc-review.js +249 -0
  58. package/dist/tools/doc-review.js.map +1 -0
  59. package/dist/tools/doc-search.d.ts +20 -0
  60. package/dist/tools/doc-search.d.ts.map +1 -0
  61. package/dist/tools/doc-search.js +268 -0
  62. package/dist/tools/doc-search.js.map +1 -0
  63. package/dist/tools/doc-write.d.ts +11 -0
  64. package/dist/tools/doc-write.d.ts.map +1 -0
  65. package/dist/tools/doc-write.js +81 -0
  66. package/dist/tools/doc-write.js.map +1 -0
  67. package/dist/tools/document.d.ts +74 -0
  68. package/dist/tools/document.d.ts.map +1 -0
  69. package/dist/tools/document.js +157 -0
  70. package/dist/tools/document.js.map +1 -0
  71. package/dist/tools/form-fill.d.ts +20 -0
  72. package/dist/tools/form-fill.d.ts.map +1 -0
  73. package/dist/tools/form-fill.js +270 -0
  74. package/dist/tools/form-fill.js.map +1 -0
  75. package/dist/tools/formula-evaluate.d.ts +14 -0
  76. package/dist/tools/formula-evaluate.d.ts.map +1 -0
  77. package/dist/tools/formula-evaluate.js +192 -0
  78. package/dist/tools/formula-evaluate.js.map +1 -0
  79. package/dist/tools/fs-helpers.d.ts +106 -0
  80. package/dist/tools/fs-helpers.d.ts.map +1 -0
  81. package/dist/tools/fs-helpers.js +257 -0
  82. package/dist/tools/fs-helpers.js.map +1 -0
  83. package/dist/tools/help.d.ts +227 -0
  84. package/dist/tools/help.d.ts.map +1 -0
  85. package/dist/tools/help.js +262 -0
  86. package/dist/tools/help.js.map +1 -0
  87. package/dist/tools/index.d.ts +33 -0
  88. package/dist/tools/index.d.ts.map +1 -0
  89. package/dist/tools/index.js +78 -0
  90. package/dist/tools/index.js.map +1 -0
  91. package/dist/tools/inspect.d.ts +14 -0
  92. package/dist/tools/inspect.d.ts.map +1 -0
  93. package/dist/tools/inspect.js +463 -0
  94. package/dist/tools/inspect.js.map +1 -0
  95. package/dist/tools/pdf-edit.d.ts +26 -0
  96. package/dist/tools/pdf-edit.d.ts.map +1 -0
  97. package/dist/tools/pdf-edit.js +390 -0
  98. package/dist/tools/pdf-edit.js.map +1 -0
  99. package/dist/tools/result.d.ts +25 -0
  100. package/dist/tools/result.d.ts.map +1 -0
  101. package/dist/tools/result.js +43 -0
  102. package/dist/tools/result.js.map +1 -0
  103. package/dist/tools/sheet-edit.d.ts +16 -0
  104. package/dist/tools/sheet-edit.d.ts.map +1 -0
  105. package/dist/tools/sheet-edit.js +325 -0
  106. package/dist/tools/sheet-edit.js.map +1 -0
  107. package/dist/tools/sheet-read.d.ts +10 -0
  108. package/dist/tools/sheet-read.d.ts.map +1 -0
  109. package/dist/tools/sheet-read.js +139 -0
  110. package/dist/tools/sheet-read.js.map +1 -0
  111. package/dist/tools/sheet-write.d.ts +15 -0
  112. package/dist/tools/sheet-write.d.ts.map +1 -0
  113. package/dist/tools/sheet-write.js +290 -0
  114. package/dist/tools/sheet-write.js.map +1 -0
  115. package/dist/tools/spreadsheet.d.ts +100 -0
  116. package/dist/tools/spreadsheet.d.ts.map +1 -0
  117. package/dist/tools/spreadsheet.js +210 -0
  118. package/dist/tools/spreadsheet.js.map +1 -0
  119. package/dist/tools/template.d.ts +18 -0
  120. package/dist/tools/template.d.ts.map +1 -0
  121. package/dist/tools/template.js +209 -0
  122. package/dist/tools/template.js.map +1 -0
  123. package/dist/tools/types.d.ts +79 -0
  124. package/dist/tools/types.d.ts.map +1 -0
  125. package/dist/tools/types.js +18 -0
  126. package/dist/tools/types.js.map +1 -0
  127. package/package.json +74 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * `formula_evaluate` — run a spreadsheet formula against a supplied cell context.
3
+ *
4
+ * The most distinctive tool here. A model asked "is this XLOOKUP right?" can
5
+ * only answer from memory of Excel semantics, and it is often confidently wrong
6
+ * about match modes, error propagation and date serials. This turns the
7
+ * question into an execution: build a throwaway workbook, populate the context,
8
+ * evaluate with the real engine (450-odd functions, spill support), read back
9
+ * what Excel would actually show.
10
+ *
11
+ * Touches no files, so it is safe under --readonly.
12
+ */
13
+ import { Address, Cell, Workbook } from "documonster/excel";
14
+ import { calculateFormulas } from "documonster/excel/formula";
15
+ import { z } from "zod";
16
+ import { toolError } from "../errors.js";
17
+ import { textResult } from "./result.js";
18
+ import { MAX_EXCEL_COLUMNS, MAX_EXCEL_ROWS, normalizeFormula, renderCell } from "./spreadsheet.js";
19
+ import { defineTool } from "./types.js";
20
+ /** Default formula location, clear of typical context but with room to spill. */
21
+ const DEFAULT_PROBE = "Z1";
22
+ /** Cap on spilled cells reported back. */
23
+ const MAX_SPILL_REPORTED = 200;
24
+ /** Furthest a spill is probed in either direction. */
25
+ const MAX_SPILL_SPAN = 500;
26
+ export const formulaEvaluateTool = defineTool({
27
+ name: "formula_evaluate",
28
+ group: "excel",
29
+ title: "Evaluate a spreadsheet formula",
30
+ description: "Evaluate an Excel formula against cell values you supply, using the real calculation engine (450+ functions including XLOOKUP, LET, LAMBDA, dynamic arrays, financial and statistical functions). Use it to check a formula's result or semantics instead of reasoning about Excel from memory. Reads and writes no files.",
31
+ inputSchema: {
32
+ formula: z
33
+ .string()
34
+ .min(1)
35
+ .describe('The formula, with or without a leading "=", e.g. "=SUM(A1:A3)*1.08".'),
36
+ context: z
37
+ .record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()]))
38
+ .optional()
39
+ .describe('Cell values the formula references, by address: { "A1": 10, "A2": 20, "B1": "widget" }. Omit for self-contained formulas.'),
40
+ contextFormulas: z
41
+ .record(z.string(), z.string())
42
+ .optional()
43
+ .describe("Context cells that are themselves formulas, by address — for testing dependency chains."),
44
+ cell: z
45
+ .string()
46
+ .optional()
47
+ .describe('Cell where the formula is evaluated. Defaults to "Z1". Set this when testing ROW(), COLUMN() or relative references. It must not also appear in context.')
48
+ },
49
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
50
+ mutates: false,
51
+ handler: async (args, context) => {
52
+ const wb = Workbook.create();
53
+ const ws = Workbook.addWorksheet(wb, "Sheet1");
54
+ for (const [address, value] of Object.entries(args.context ?? {})) {
55
+ Cell.setValue(ws, requireAddress(address, "context"), value);
56
+ }
57
+ for (const [address, formula] of Object.entries(args.contextFormulas ?? {})) {
58
+ Cell.setValue(ws, requireAddress(address, "contextFormulas"), {
59
+ formula: normalizeFormula(formula)
60
+ });
61
+ }
62
+ const probe = requireAddress(args.cell ?? DEFAULT_PROBE, "cell");
63
+ if (probe in (args.context ?? {}) || probe in (args.contextFormulas ?? {})) {
64
+ throw toolError.invalidInput(`probe cell ${probe} is also present in the context`, "Choose a different `cell`; the formula result would otherwise overwrite that context value.");
65
+ }
66
+ const probeAddress = Address.decodeCell(probe);
67
+ Cell.setValue(ws, probe, { formula: normalizeFormula(args.formula) });
68
+ try {
69
+ calculateFormulas(wb);
70
+ }
71
+ catch (cause) {
72
+ throw toolError.invalidInput(`the formula could not be evaluated: ${cause instanceof Error ? cause.message : String(cause)}`, "Check the syntax — argument separators are commas, and sheet-qualified references need a sheet that exists in `context`.", { cause });
73
+ }
74
+ const lines = [
75
+ "# formula_evaluate",
76
+ "",
77
+ `- formula: \`=${normalizeFormula(args.formula)}\``,
78
+ `- evaluated at: \`${probe}\``,
79
+ `- result: **${describeResult(ws, probe)}**`,
80
+ `- displayed as: \`${Cell.getDisplayText(ws, probe)}\``
81
+ ];
82
+ const spill = collectSpill(ws, probeAddress.r + 1, probeAddress.c + 1);
83
+ if (spill.length > 0) {
84
+ lines.push(`- this is a dynamic array: it spills over ${spill.length + 1} cells`, "", "Spilled values:", "", ...[
85
+ `| offset | value |`,
86
+ `| --- | --- |`,
87
+ `| 0 (anchor) | ${Cell.getDisplayText(ws, probe)} |`
88
+ ], ...spill.slice(0, MAX_SPILL_REPORTED).map(entry => `| ${entry.label} | ${entry.text} |`), ...(spill.length > MAX_SPILL_REPORTED
89
+ ? [`[${spill.length - MAX_SPILL_REPORTED} more spilled value(s) not listed]`]
90
+ : []));
91
+ }
92
+ const contextCells = Object.keys(args.context ?? {}).length + Object.keys(args.contextFormulas ?? {}).length;
93
+ if (contextCells > 0) {
94
+ lines.push("", `Evaluated with ${contextCells} context cell(s).`);
95
+ const derived = Object.keys(args.contextFormulas ?? {});
96
+ if (derived.length > 0) {
97
+ lines.push("", "Context formula results:", "", ...derived.map(address => {
98
+ const upper = requireAddress(address, "contextFormulas");
99
+ return `- \`${upper}\` = ${Cell.getDisplayText(ws, upper)}`;
100
+ }));
101
+ }
102
+ }
103
+ return textResult(context.config, lines.join("\n"));
104
+ }
105
+ });
106
+ /**
107
+ * Describe the evaluated result, naming an Excel error explicitly.
108
+ *
109
+ * `#SPILL!` / `#NAME?` / `#DIV/0!` are answers, not failures — the caller
110
+ * usually asked precisely to find out whether one occurs, so they are reported
111
+ * as results rather than raised as tool errors.
112
+ */
113
+ function describeResult(ws, address) {
114
+ const value = Cell.getValue(ws, address);
115
+ if (value !== null && typeof value === "object" && "result" in value) {
116
+ const result = value.result;
117
+ if (result !== null &&
118
+ typeof result === "object" &&
119
+ result !== undefined &&
120
+ "error" in result) {
121
+ const code = result.error;
122
+ return `${String(code)} (an Excel error value, not a tool failure)`;
123
+ }
124
+ return formatScalar(result);
125
+ }
126
+ return formatScalar(value);
127
+ }
128
+ function formatScalar(value) {
129
+ if (value === null || value === undefined) {
130
+ return "(blank)";
131
+ }
132
+ if (typeof value === "number" || typeof value === "boolean") {
133
+ return String(value);
134
+ }
135
+ if (typeof value === "string") {
136
+ return JSON.stringify(value);
137
+ }
138
+ if (value instanceof Date) {
139
+ return value.toISOString();
140
+ }
141
+ return JSON.stringify(value);
142
+ }
143
+ /**
144
+ * Find cells the probe spilled into.
145
+ *
146
+ * Grows the rectangle outward from the anchor rather than sweeping the used
147
+ * area: the used area also contains the caller's context cells, so a sweep
148
+ * would cost thousands of reads and could report unrelated values as spill.
149
+ * A spill is contiguous, so stopping at the first empty row and column is both
150
+ * cheaper and more accurate.
151
+ */
152
+ function collectSpill(ws, anchorRow, anchorColumn) {
153
+ const width = measureRun(column => Cell.getValue(ws, anchorRow, anchorColumn + column), Math.min(MAX_SPILL_SPAN, MAX_EXCEL_COLUMNS - anchorColumn + 1));
154
+ const height = measureRun(row => Cell.getValue(ws, anchorRow + row, anchorColumn), Math.min(MAX_SPILL_SPAN, MAX_EXCEL_ROWS - anchorRow + 1));
155
+ const found = [];
156
+ for (let row = 0; row < height; row += 1) {
157
+ for (let column = 0; column < width; column += 1) {
158
+ if (row === 0 && column === 0) {
159
+ continue;
160
+ }
161
+ const value = Cell.getValue(ws, anchorRow + row, anchorColumn + column);
162
+ if (value === null || value === undefined) {
163
+ continue;
164
+ }
165
+ found.push({
166
+ label: `r+${row},c+${column}`,
167
+ text: renderCell(ws, anchorRow + row, anchorColumn + column, "values")
168
+ });
169
+ }
170
+ }
171
+ return found;
172
+ }
173
+ /** Length of the contiguous non-empty run starting at offset 0. */
174
+ function measureRun(read, limit) {
175
+ let length = 0;
176
+ while (length < limit) {
177
+ const value = read(length);
178
+ if (value === null || value === undefined) {
179
+ break;
180
+ }
181
+ length += 1;
182
+ }
183
+ return Math.max(1, length);
184
+ }
185
+ function requireAddress(address, field) {
186
+ const upper = address.trim().toUpperCase();
187
+ if (!/^[A-Z]{1,3}[1-9][0-9]*$/.test(upper)) {
188
+ throw toolError.invalidInput(`${field} key ${JSON.stringify(address)} is not a cell address`, 'Use plain A1 notation, e.g. "B2".');
189
+ }
190
+ return upper;
191
+ }
192
+ //# sourceMappingURL=formula-evaluate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formula-evaluate.js","sourceRoot":"","sources":["../../src/tools/formula-evaluate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Filesystem helpers shared by the write tools.
3
+ *
4
+ * Centralised because every tool that edits a user's file needs the same three
5
+ * behaviours, and six divergent copies is six chances to get one of them wrong:
6
+ * do not clobber silently, do not lose the original, and never leave a
7
+ * half-written file behind.
8
+ */
9
+ import type { WriteTarget } from "../sandbox.js";
10
+ /** True when a path exists. A missing file is not an error here. */
11
+ export declare function exists(target: string): Promise<boolean>;
12
+ /**
13
+ * Create a write target's parent directory.
14
+ *
15
+ * Needed because a model naturally writes `out/report.xlsx` without first
16
+ * checking that `out` exists, and none of the underlying writers create it.
17
+ */
18
+ export declare function ensureParent(target: string): Promise<void>;
19
+ /** Byte size of a file. */
20
+ export declare function sizeOf(target: string): Promise<number>;
21
+ /** Minimal optimistic-concurrency token for a file being edited. */
22
+ export interface FileFingerprint {
23
+ readonly size: number;
24
+ readonly mtimeMs: number;
25
+ readonly ino: number;
26
+ }
27
+ /** Capture the version a tool is about to read and edit. */
28
+ export declare function fingerprint(target: string): Promise<FileFingerprint>;
29
+ /**
30
+ * Refuse to overwrite a file another process changed while this tool worked.
31
+ *
32
+ * This closes the common lost-update race: read A, somebody saves B, then this
33
+ * tool writes an output derived from A over B. Node cannot bind every later
34
+ * operation to an `openat` directory descriptor cross-platform, but a version
35
+ * check immediately before backup/install prevents accidental concurrent edits
36
+ * from being silently discarded.
37
+ */
38
+ export declare function assertUnchanged(target: string, expected: FileFingerprint): Promise<void>;
39
+ /**
40
+ * Write bytes to `target` with atomic POSIX replacement / Windows rollback.
41
+ *
42
+ * The naive `writeFile(target, bytes)` truncates the destination before the new
43
+ * content is complete, so a serialization error, a full disk or a killed
44
+ * process destroys the user's file — which is precisely the outcome the
45
+ * "atomic" claim in the edit tools is supposed to rule out. Writing a sibling
46
+ * temporary file and renaming it makes the replacement a single filesystem
47
+ * operation on POSIX: either the old file or the new one, never a truncated
48
+ * hybrid. Windows uses the rollback-safe two-rename path described below.
49
+ *
50
+ * The temporary file is a sibling rather than in the OS temp directory so the
51
+ * rename stays within one filesystem; `rename` across devices fails with
52
+ * `EXDEV`.
53
+ */
54
+ export declare function writeFileAtomic(target: string, bytes: Uint8Array | string): Promise<void>;
55
+ /**
56
+ * Run `produce` to build a file's new bytes, then replace `target` atomically.
57
+ *
58
+ * Used where a library insists on writing to a path itself: it writes to the
59
+ * temporary sibling and the rename happens only after it returns successfully.
60
+ */
61
+ export declare function replaceAtomically(target: string, produce: (temporaryPath: string) => Promise<void>): Promise<void>;
62
+ /**
63
+ * Install a complete file only when `target` does not already exist.
64
+ *
65
+ * A pre-check followed by rename has a race: another process can create the
66
+ * target in between and POSIX rename will silently replace it. `link` is an
67
+ * atomic no-clobber operation on the same filesystem: it either installs the
68
+ * already-complete inode or fails with EEXIST.
69
+ */
70
+ export declare function writeFileNoClobber(target: string, produce: (temporaryPath: string) => Promise<void>): Promise<void>;
71
+ /** Select atomic replace or atomic no-clobber from a public overwrite flag. */
72
+ export declare function writeWithPolicy(target: string, overwrite: boolean, produce: (temporaryPath: string) => Promise<void>): Promise<void>;
73
+ /** Byte/string counterpart of {@link writeWithPolicy}. */
74
+ export declare function writeBytesWithPolicy(target: string, overwrite: boolean, bytes: Uint8Array | string): Promise<void>;
75
+ /**
76
+ * Copy the original aside before an in-place edit.
77
+ *
78
+ * Never overwrites an existing backup. A second edit would otherwise replace the
79
+ * only copy of the user's original with the output of the first edit, which
80
+ * defeats the purpose — the `.bak` a user reaches for is the pristine one, not
81
+ * the previous intermediate state. Later backups get a numbered suffix.
82
+ *
83
+ * @returns The path written, or `undefined` when the source does not exist.
84
+ */
85
+ export declare function backupOnce(target: WriteTarget): Promise<string | undefined>;
86
+ /** Describe a backup for a tool's report, relative to the caller's own path. */
87
+ export declare function describeBackup(display: string, backupPath: string | undefined): string[];
88
+ /**
89
+ * True when two resolved paths denote the same file.
90
+ *
91
+ * `out: "./report.xlsx"` resolves to the same file as `path: "report.xlsx"`, and
92
+ * a tool that decides "in place or not" by asking whether `out` was supplied
93
+ * would then skip the backup while still overwriting the input.
94
+ */
95
+ export declare function isSameFile(a: string, b: string): boolean;
96
+ /**
97
+ * Refuse an input document larger than the configured ceiling.
98
+ *
99
+ * `doc_inspect` tells the model that an oversized file "will be rejected", so
100
+ * every tool that opens a document has to honour that — otherwise the limit is
101
+ * advice, and a single huge file can exhaust the server's memory.
102
+ */
103
+ export declare function assertReadableSize(config: {
104
+ readonly maxFileSize: number;
105
+ }, target: string, display: string): Promise<number>;
106
+ //# sourceMappingURL=fs-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs-helpers.d.ts","sourceRoot":"","sources":["../../src/tools/fs-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAkBH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,oEAAoE;AACpE,wBAAsB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAO7D;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhE;AAED,2BAA2B;AAC3B,wBAAsB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAE5D;AAED,oEAAoE;AACpE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,4DAA4D;AAC5D,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAG1E;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAe9F;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS/F;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAChD,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAChD,OAAO,CAAC,IAAI,CAAC,CASf;AAED,+EAA+E;AAC/E,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,OAAO,EAClB,OAAO,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAChD,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED,0DAA0D;AAC1D,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,OAAO,EAClB,KAAK,EAAE,UAAU,GAAG,MAAM,GACzB,OAAO,CAAC,IAAI,CAAC,CAEf;AAgDD;;;;;;;;;GASG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAUjF;AAeD,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAOxF;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE;IAAE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAAE,EACxC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAYjB"}
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Filesystem helpers shared by the write tools.
3
+ *
4
+ * Centralised because every tool that edits a user's file needs the same three
5
+ * behaviours, and six divergent copies is six chances to get one of them wrong:
6
+ * do not clobber silently, do not lose the original, and never leave a
7
+ * half-written file behind.
8
+ */
9
+ import { constants } from "node:fs";
10
+ import { chmod, copyFile, link, mkdir, mkdtemp, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
11
+ import path from "node:path";
12
+ import { McpToolError } from "../errors.js";
13
+ /** True when a path exists. A missing file is not an error here. */
14
+ export async function exists(target) {
15
+ try {
16
+ await stat(target);
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ /**
24
+ * Create a write target's parent directory.
25
+ *
26
+ * Needed because a model naturally writes `out/report.xlsx` without first
27
+ * checking that `out` exists, and none of the underlying writers create it.
28
+ */
29
+ export async function ensureParent(target) {
30
+ await mkdir(path.dirname(target), { recursive: true });
31
+ }
32
+ /** Byte size of a file. */
33
+ export async function sizeOf(target) {
34
+ return (await stat(target)).size;
35
+ }
36
+ /** Capture the version a tool is about to read and edit. */
37
+ export async function fingerprint(target) {
38
+ const current = await stat(target);
39
+ return { size: current.size, mtimeMs: current.mtimeMs, ino: current.ino };
40
+ }
41
+ /**
42
+ * Refuse to overwrite a file another process changed while this tool worked.
43
+ *
44
+ * This closes the common lost-update race: read A, somebody saves B, then this
45
+ * tool writes an output derived from A over B. Node cannot bind every later
46
+ * operation to an `openat` directory descriptor cross-platform, but a version
47
+ * check immediately before backup/install prevents accidental concurrent edits
48
+ * from being silently discarded.
49
+ */
50
+ export async function assertUnchanged(target, expected) {
51
+ const current = await fingerprint(target);
52
+ if (current.size !== expected.size ||
53
+ current.mtimeMs !== expected.mtimeMs ||
54
+ current.ino !== expected.ino) {
55
+ throw new McpToolError("invalid_input", "the input file changed while this operation was running", {
56
+ hint: "Nothing was written. Read the latest file and retry so another process's changes are not lost."
57
+ });
58
+ }
59
+ }
60
+ /**
61
+ * Write bytes to `target` with atomic POSIX replacement / Windows rollback.
62
+ *
63
+ * The naive `writeFile(target, bytes)` truncates the destination before the new
64
+ * content is complete, so a serialization error, a full disk or a killed
65
+ * process destroys the user's file — which is precisely the outcome the
66
+ * "atomic" claim in the edit tools is supposed to rule out. Writing a sibling
67
+ * temporary file and renaming it makes the replacement a single filesystem
68
+ * operation on POSIX: either the old file or the new one, never a truncated
69
+ * hybrid. Windows uses the rollback-safe two-rename path described below.
70
+ *
71
+ * The temporary file is a sibling rather than in the OS temp directory so the
72
+ * rename stays within one filesystem; `rename` across devices fails with
73
+ * `EXDEV`.
74
+ */
75
+ export async function writeFileAtomic(target, bytes) {
76
+ const temporary = await createPrivateTemporary(target);
77
+ try {
78
+ await writeFile(temporary, bytes);
79
+ await preserveMode(target, temporary);
80
+ await replaceInstalledFile(temporary, target);
81
+ }
82
+ finally {
83
+ await rm(path.dirname(temporary), { recursive: true, force: true });
84
+ }
85
+ }
86
+ /**
87
+ * Run `produce` to build a file's new bytes, then replace `target` atomically.
88
+ *
89
+ * Used where a library insists on writing to a path itself: it writes to the
90
+ * temporary sibling and the rename happens only after it returns successfully.
91
+ */
92
+ export async function replaceAtomically(target, produce) {
93
+ const temporary = await createPrivateTemporary(target);
94
+ try {
95
+ await produce(temporary);
96
+ await preserveMode(target, temporary);
97
+ await replaceInstalledFile(temporary, target);
98
+ }
99
+ finally {
100
+ await rm(path.dirname(temporary), { recursive: true, force: true });
101
+ }
102
+ }
103
+ /**
104
+ * Install a complete file only when `target` does not already exist.
105
+ *
106
+ * A pre-check followed by rename has a race: another process can create the
107
+ * target in between and POSIX rename will silently replace it. `link` is an
108
+ * atomic no-clobber operation on the same filesystem: it either installs the
109
+ * already-complete inode or fails with EEXIST.
110
+ */
111
+ export async function writeFileNoClobber(target, produce) {
112
+ const temporary = await createPrivateTemporary(target);
113
+ try {
114
+ await produce(temporary);
115
+ await link(temporary, target);
116
+ await unlink(temporary);
117
+ }
118
+ finally {
119
+ await rm(path.dirname(temporary), { recursive: true, force: true });
120
+ }
121
+ }
122
+ /** Select atomic replace or atomic no-clobber from a public overwrite flag. */
123
+ export async function writeWithPolicy(target, overwrite, produce) {
124
+ if (overwrite) {
125
+ await replaceAtomically(target, produce);
126
+ }
127
+ else {
128
+ try {
129
+ await writeFileNoClobber(target, produce);
130
+ }
131
+ catch (cause) {
132
+ if (cause.code === "EEXIST") {
133
+ throw new McpToolError("invalid_input", `${target} already exists`, {
134
+ hint: "Pass overwrite: true to replace it, or choose a different path.",
135
+ cause
136
+ });
137
+ }
138
+ throw cause;
139
+ }
140
+ }
141
+ }
142
+ /** Byte/string counterpart of {@link writeWithPolicy}. */
143
+ export async function writeBytesWithPolicy(target, overwrite, bytes) {
144
+ await writeWithPolicy(target, overwrite, temporary => writeFile(temporary, bytes));
145
+ }
146
+ /** A random, private sibling temporary path on the target filesystem. */
147
+ async function createPrivateTemporary(target) {
148
+ await ensureParent(target);
149
+ const directory = await mkdtemp(path.join(path.dirname(target), ".documonster-"));
150
+ // mkdtemp normally uses 0700; enforce it so a permissive umask or platform
151
+ // implementation cannot expose a document while it is being written.
152
+ await chmod(directory, 0o700);
153
+ return path.join(directory, path.basename(target));
154
+ }
155
+ /** Carry the original file's Unix permissions onto its replacement. */
156
+ async function preserveMode(target, temporary) {
157
+ try {
158
+ const current = await stat(target);
159
+ await chmod(temporary, current.mode & 0o777);
160
+ }
161
+ catch (cause) {
162
+ if (cause.code !== "ENOENT") {
163
+ throw cause;
164
+ }
165
+ }
166
+ }
167
+ /**
168
+ * Replace an installed file.
169
+ *
170
+ * POSIX rename atomically replaces an existing file. Windows commonly refuses
171
+ * that operation, so use a rollback rename there: the target is never deleted,
172
+ * and a failure to install the new file restores the old one.
173
+ */
174
+ async function replaceInstalledFile(temporary, target) {
175
+ if (process.platform !== "win32" || !(await exists(target))) {
176
+ await rename(temporary, target);
177
+ return;
178
+ }
179
+ const rollback = `${target}.rollback-${process.pid.toString(36)}-${Date.now().toString(36)}`;
180
+ await rename(target, rollback);
181
+ try {
182
+ await rename(temporary, target);
183
+ await rm(rollback, { force: true });
184
+ }
185
+ catch (cause) {
186
+ await rename(rollback, target).catch(() => undefined);
187
+ throw cause;
188
+ }
189
+ }
190
+ /**
191
+ * Copy the original aside before an in-place edit.
192
+ *
193
+ * Never overwrites an existing backup. A second edit would otherwise replace the
194
+ * only copy of the user's original with the output of the first edit, which
195
+ * defeats the purpose — the `.bak` a user reaches for is the pristine one, not
196
+ * the previous intermediate state. Later backups get a numbered suffix.
197
+ *
198
+ * @returns The path written, or `undefined` when the source does not exist.
199
+ */
200
+ export async function backupOnce(target) {
201
+ if (!(await exists(target.path))) {
202
+ return undefined;
203
+ }
204
+ const chosen = await firstFreeBackupPath(target.backupPath);
205
+ // COPYFILE_EXCL: fail rather than follow or overwrite anything that appeared
206
+ // between the check above and this call.
207
+ await copyFile(target.path, chosen, constants.COPYFILE_EXCL);
208
+ return chosen;
209
+ }
210
+ async function firstFreeBackupPath(base) {
211
+ if (!(await exists(base))) {
212
+ return base;
213
+ }
214
+ for (let index = 2; index < 1000; index += 1) {
215
+ const candidate = `${base}.${index}`;
216
+ if (!(await exists(candidate))) {
217
+ return candidate;
218
+ }
219
+ }
220
+ throw new Error(`could not find a free backup filename beside ${base}`);
221
+ }
222
+ /** Describe a backup for a tool's report, relative to the caller's own path. */
223
+ export function describeBackup(display, backupPath) {
224
+ if (backupPath === undefined) {
225
+ return [];
226
+ }
227
+ return [
228
+ `- original copied to ${display}${backupPath.endsWith(".bak") ? ".bak" : `.bak (${path.basename(backupPath)})`}`
229
+ ];
230
+ }
231
+ /**
232
+ * True when two resolved paths denote the same file.
233
+ *
234
+ * `out: "./report.xlsx"` resolves to the same file as `path: "report.xlsx"`, and
235
+ * a tool that decides "in place or not" by asking whether `out` was supplied
236
+ * would then skip the backup while still overwriting the input.
237
+ */
238
+ export function isSameFile(a, b) {
239
+ return path.resolve(a) === path.resolve(b);
240
+ }
241
+ /**
242
+ * Refuse an input document larger than the configured ceiling.
243
+ *
244
+ * `doc_inspect` tells the model that an oversized file "will be rejected", so
245
+ * every tool that opens a document has to honour that — otherwise the limit is
246
+ * advice, and a single huge file can exhaust the server's memory.
247
+ */
248
+ export async function assertReadableSize(config, target, display) {
249
+ const size = await sizeOf(target);
250
+ if (size > config.maxFileSize) {
251
+ throw new McpToolError("too_large", `${display} is ${size} bytes, over the ${config.maxFileSize} byte limit`, {
252
+ hint: "Ask the user to raise --max-file-size, or work with a smaller extract of the document."
253
+ });
254
+ }
255
+ return size;
256
+ }
257
+ //# sourceMappingURL=fs-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs-helpers.js","sourceRoot":"","sources":["../../src/tools/fs-helpers.ts"],"names":[],"mappings":"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"}