@theokit/sdk-tools 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -2
- package/README.md +1 -1
- package/dist/index.cjs +119 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -3
- package/dist/index.d.ts +42 -3
- package/dist/index.js +117 -15
- package/dist/index.js.map +1 -1
- package/package.json +14 -14
package/CHANGELOG.md
CHANGED
|
@@ -40,13 +40,13 @@
|
|
|
40
40
|
|
|
41
41
|
A pluggable filesystem _storage_ provider, the storage-side twin of `@theokit/sdk/sandbox`. `FilesystemBackend` is an abstract class with four methods (`readFile` / `writeFile` / `stat` / `list`), an `exists()` derived on the base, a boundary `basePath`, a `readOnly` flag, structured `stat().mtimeMs` (the read-before-write oracle for SE32), and typed errors (`FileNotFoundError` / `FilesystemSecurityError` / `FilesystemReadOnlyError` / `StaleFileError`). `LocalFilesystem` is the local-process implementation, boundary-enforced by reusing the core path-guard (traversal + symlink escape → `FilesystemSecurityError`). `FilesystemProvider` + `resolveFilesystem` support a per-request resolver `(ctx) => FilesystemBackend` for multi-tenant roots.
|
|
42
42
|
|
|
43
|
-
Unlike `SandboxBackend` (whose file ops shell out via `execute`, require command execution, and give no structured `stat`), a `FilesystemBackend` serves a filesystem-only workspace with no sandbox — see ADR 0011 for why file ops are NOT routed through `SandboxBackend`. `@theokit/sdk-tools`' `createWriteFileTool` now accepts an optional `filesystem` backend (writes route through it; omitted ⇒ identical local-`projectRoot` behavior). This is the backend seam, NOT a bundled `Workspace` and NOT a new toolset — bring-your-own-tools stands; `mounts`/FUSE, S3/GCS, and LSP remain out of core.
|
|
43
|
+
Unlike `SandboxBackend` (whose file ops shell out via `execute`, require command execution, and give no structured `stat`), a `FilesystemBackend` serves a filesystem-only workspace with no sandbox — see ADR 0011 for why file ops are NOT routed through `SandboxBackend`. `@theokit/sdk-tools`' `createWriteFileTool` now accepts an optional `filesystem` backend (writes route through it; omitted ⇒ identical local-`projectRoot` behavior). This is the backend seam, NOT a bundled `Workspace` and NOT a new toolset — bring-your-own-tools stands; `mounts`/FUSE, S3/GCS, and LSP remain out of core. (SDK Evolution roadmap SE31.)
|
|
44
44
|
|
|
45
45
|
- 84df83a: **SE32 — read-before-write safety (`requireReadBeforeWrite` + `ReadTracker`).**
|
|
46
46
|
|
|
47
47
|
An opt-in guard on `createWriteFileTool` that refuses to blindly overwrite a file the agent has not seen. A per-run `ReadTracker` (exported from `@theokit/sdk-tools`) records each file's mtime when `createReadFileTool` reads it; when `createWriteFileTool` is created with `{ requireReadBeforeWrite: true, readTracker }`, a write is refused with `read_required` if the existing file was never read, or `stale_file` if it changed on disk since it was read. A NEW file writes freely (nothing to clobber). Default OFF — omitting the flag preserves current behavior exactly.
|
|
48
48
|
|
|
49
|
-
Works on both the local `projectRoot` path and the SE31 `filesystem` backend path (the backend also gets `expectedMtime` forwarded so it re-checks at write time — TOCTOU defense). The tracker is deliberately per-instance, not a global singleton, so state never leaks across runs. `edit_file` already has implicit read-before-write safety via `old_string` content matching, so the guard targets the blind-overwrite path (`write_file`).
|
|
49
|
+
Works on both the local `projectRoot` path and the SE31 `filesystem` backend path (the backend also gets `expectedMtime` forwarded so it re-checks at write time — TOCTOU defense). The tracker is deliberately per-instance, not a global singleton, so state never leaks across runs. `edit_file` already has implicit read-before-write safety via `old_string` content matching, so the guard targets the blind-overwrite path (`write_file`). Refusals surface as `FileReadRequiredError` / `StaleFileError`. (SDK Evolution roadmap SE32.)
|
|
50
50
|
|
|
51
51
|
## 0.8.0
|
|
52
52
|
|
package/README.md
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -265,6 +265,90 @@ function createSessionArtifactStore(options) {
|
|
|
265
265
|
}
|
|
266
266
|
return { write, read, has, list, path };
|
|
267
267
|
}
|
|
268
|
+
|
|
269
|
+
// src/internal/context-match.ts
|
|
270
|
+
var ContextMatchError = class extends Error {
|
|
271
|
+
reason;
|
|
272
|
+
constructor(reason, message) {
|
|
273
|
+
super(message);
|
|
274
|
+
this.name = "ContextMatchError";
|
|
275
|
+
this.reason = reason;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
function normalizeUnicode(s) {
|
|
279
|
+
return s.replace(/[‐-―−]/g, "-").replace(/[‘-‛]/g, "'").replace(/[“-‟]/g, '"').replace(/[ - ]/g, " ");
|
|
280
|
+
}
|
|
281
|
+
var LINE_MATCH_LADDER = [
|
|
282
|
+
(s) => s,
|
|
283
|
+
// exact
|
|
284
|
+
(s) => s.replace(/\s+$/, ""),
|
|
285
|
+
// rstrip (ignore trailing whitespace)
|
|
286
|
+
(s) => s.trim(),
|
|
287
|
+
// trim (ignore leading + trailing)
|
|
288
|
+
(s) => normalizeUnicode(s).trim()
|
|
289
|
+
// unicode + trim (loosest)
|
|
290
|
+
];
|
|
291
|
+
function matchesAt(lines, pattern, i, norm) {
|
|
292
|
+
for (let p = 0; p < pattern.length; p++) {
|
|
293
|
+
const lineAt = lines[i + p];
|
|
294
|
+
const patAt = pattern[p];
|
|
295
|
+
if (lineAt === void 0 || patAt === void 0 || norm(lineAt) !== norm(patAt)) return false;
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
function findHits(lines, pattern, norm) {
|
|
300
|
+
const hits = [];
|
|
301
|
+
for (let i = 0; i + pattern.length <= lines.length; i++) {
|
|
302
|
+
if (matchesAt(lines, pattern, i, norm)) hits.push(i);
|
|
303
|
+
}
|
|
304
|
+
return hits;
|
|
305
|
+
}
|
|
306
|
+
function seekUniqueLineMatch(lines, pattern, find) {
|
|
307
|
+
if (pattern.length === 0 || pattern.length > lines.length) return null;
|
|
308
|
+
for (const norm of LINE_MATCH_LADDER) {
|
|
309
|
+
const hits = findHits(lines, pattern, norm);
|
|
310
|
+
if (hits.length === 1) return hits[0] ?? null;
|
|
311
|
+
if (hits.length > 1) {
|
|
312
|
+
throw new ContextMatchError(
|
|
313
|
+
"ambiguous",
|
|
314
|
+
`target text is ambiguous (multiple matches); include more context:
|
|
315
|
+
${find}`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
function replaceUnique(content, find, replace) {
|
|
322
|
+
if (find === "") {
|
|
323
|
+
throw new ContextMatchError(
|
|
324
|
+
"empty",
|
|
325
|
+
"empty find is not a valid target; provide the text to replace"
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const first = content.indexOf(find);
|
|
329
|
+
if (first !== -1) {
|
|
330
|
+
if (content.indexOf(find, first + 1) !== -1) {
|
|
331
|
+
throw new ContextMatchError(
|
|
332
|
+
"ambiguous",
|
|
333
|
+
`target text is ambiguous (multiple matches); include more context:
|
|
334
|
+
${find}`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return content.slice(0, first) + replace + content.slice(first + find.length);
|
|
338
|
+
}
|
|
339
|
+
const lines = content.split("\n");
|
|
340
|
+
const at = seekUniqueLineMatch(lines, find.split("\n"), find);
|
|
341
|
+
if (at === null) {
|
|
342
|
+
throw new ContextMatchError("not_found", `target text not found:
|
|
343
|
+
${find}`);
|
|
344
|
+
}
|
|
345
|
+
const patternLen = find.split("\n").length;
|
|
346
|
+
const matchedCrlf = patternLen > 0 && lines.slice(at, at + patternLen).every((l) => l.endsWith("\r"));
|
|
347
|
+
const replaceLines = matchedCrlf ? replace.split("\n").map((l) => l.endsWith("\r") ? l : `${l}\r`) : replace.split("\n");
|
|
348
|
+
return [...lines.slice(0, at), ...replaceLines, ...lines.slice(at + patternLen)].join("\n");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/edit-file.ts
|
|
268
352
|
function createEditFileTool(opts) {
|
|
269
353
|
const { projectRoot } = opts;
|
|
270
354
|
return sdk.Tool.create({
|
|
@@ -306,26 +390,36 @@ function createEditFileTool(opts) {
|
|
|
306
390
|
const exactIdx = content.indexOf(old_string);
|
|
307
391
|
if (exactIdx !== -1) {
|
|
308
392
|
await promises.copyFile(absolutePath, `${absolutePath}.bak`);
|
|
309
|
-
const
|
|
310
|
-
await promises.writeFile(absolutePath,
|
|
393
|
+
const result = content.slice(0, exactIdx) + new_string + content.slice(exactIdx + old_string.length);
|
|
394
|
+
await promises.writeFile(absolutePath, result, "utf-8");
|
|
311
395
|
return JSON.stringify({ ok: true, replacements: 1 });
|
|
312
396
|
}
|
|
313
397
|
const normalizedContent = normalizeWhitespace(content);
|
|
314
398
|
const normalizedOld = normalizeWhitespace(old_string);
|
|
315
399
|
const normalizedIdx = normalizedContent.indexOf(normalizedOld);
|
|
316
|
-
if (normalizedIdx
|
|
317
|
-
|
|
400
|
+
if (normalizedIdx !== -1) {
|
|
401
|
+
const span = findOriginalSpan(
|
|
402
|
+
content,
|
|
403
|
+
normalizedContent,
|
|
404
|
+
normalizedIdx,
|
|
405
|
+
normalizedOld.length
|
|
406
|
+
);
|
|
407
|
+
await promises.copyFile(absolutePath, `${absolutePath}.bak`);
|
|
408
|
+
const result = content.slice(0, span.start) + new_string + content.slice(span.end);
|
|
409
|
+
await promises.writeFile(absolutePath, result, "utf-8");
|
|
410
|
+
return JSON.stringify({ ok: true, replacements: 1 });
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
const result = replaceUnique(content, old_string, new_string);
|
|
414
|
+
await promises.copyFile(absolutePath, `${absolutePath}.bak`);
|
|
415
|
+
await promises.writeFile(absolutePath, result, "utf-8");
|
|
416
|
+
return JSON.stringify({ ok: true, replacements: 1 });
|
|
417
|
+
} catch (err) {
|
|
418
|
+
if (err instanceof ContextMatchError) {
|
|
419
|
+
return JSON.stringify({ ok: false, error: "no_match", path });
|
|
420
|
+
}
|
|
421
|
+
throw err;
|
|
318
422
|
}
|
|
319
|
-
const span = findOriginalSpan(
|
|
320
|
-
content,
|
|
321
|
-
normalizedContent,
|
|
322
|
-
normalizedIdx,
|
|
323
|
-
normalizedOld.length
|
|
324
|
-
);
|
|
325
|
-
await promises.copyFile(absolutePath, `${absolutePath}.bak`);
|
|
326
|
-
const result = content.slice(0, span.start) + new_string + content.slice(span.end);
|
|
327
|
-
await promises.writeFile(absolutePath, result, "utf-8");
|
|
328
|
-
return JSON.stringify({ ok: true, replacements: 1 });
|
|
329
423
|
}
|
|
330
424
|
});
|
|
331
425
|
}
|
|
@@ -990,6 +1084,14 @@ function withDescription(tool, description) {
|
|
|
990
1084
|
handler: tool.handler
|
|
991
1085
|
};
|
|
992
1086
|
}
|
|
1087
|
+
function withName(tool, name) {
|
|
1088
|
+
return {
|
|
1089
|
+
name,
|
|
1090
|
+
description: tool.description,
|
|
1091
|
+
inputSchema: tool.inputSchema,
|
|
1092
|
+
handler: tool.handler
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
993
1095
|
function esc(s) {
|
|
994
1096
|
return String(s).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
995
1097
|
}
|
|
@@ -2242,6 +2344,7 @@ async function isBinaryFile(absolutePath) {
|
|
|
2242
2344
|
}
|
|
2243
2345
|
|
|
2244
2346
|
exports.CatastrophicCommandError = CatastrophicCommandError;
|
|
2347
|
+
exports.ContextMatchError = ContextMatchError;
|
|
2245
2348
|
exports.DEFAULT_TOOL_GUIDANCE = DEFAULT_TOOL_GUIDANCE;
|
|
2246
2349
|
exports.ReadTracker = ReadTracker;
|
|
2247
2350
|
exports.ReasoningTools = ReasoningTools;
|
|
@@ -2278,12 +2381,14 @@ exports.injectGuidance = injectGuidance;
|
|
|
2278
2381
|
exports.isBlockedIp = isBlockedIp;
|
|
2279
2382
|
exports.isCommandAllowed = isCommandAllowed;
|
|
2280
2383
|
exports.renderToolList = renderToolList;
|
|
2384
|
+
exports.replaceUnique = replaceUnique;
|
|
2281
2385
|
exports.resolveAndScreen = resolveAndScreen;
|
|
2282
2386
|
exports.screenedFetch = screenedFetch;
|
|
2283
2387
|
exports.todoItemsToPlanNodes = todoItemsToPlanNodes;
|
|
2284
2388
|
exports.truncateOutput = truncateOutput;
|
|
2285
2389
|
exports.withDefaultGuidance = withDefaultGuidance;
|
|
2286
2390
|
exports.withDescription = withDescription;
|
|
2391
|
+
exports.withName = withName;
|
|
2287
2392
|
exports.withShellExitGuidance = withShellExitGuidance;
|
|
2288
2393
|
exports.withToolResultGuidance = withToolResultGuidance;
|
|
2289
2394
|
//# sourceMappingURL=index.cjs.map
|