@wrongstack/tools 0.9.7 → 0.9.20

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 (53) hide show
  1. package/dist/audit.js.map +1 -1
  2. package/dist/bash.js +0 -3
  3. package/dist/bash.js.map +1 -1
  4. package/dist/builtin.js +67 -30
  5. package/dist/builtin.js.map +1 -1
  6. package/dist/circuit-breaker.d.ts +0 -2
  7. package/dist/circuit-breaker.js +0 -3
  8. package/dist/circuit-breaker.js.map +1 -1
  9. package/dist/codebase-index/index.d.ts +2 -2
  10. package/dist/codebase-index/index.js +2 -2
  11. package/dist/codebase-index/index.js.map +1 -1
  12. package/dist/{codebase-stats-tool-BLhQmPNc.d.ts → codebase-stats-tool-C8ApERbn.d.ts} +0 -11
  13. package/dist/diff.js +15 -5
  14. package/dist/diff.js.map +1 -1
  15. package/dist/document.js +1 -2
  16. package/dist/document.js.map +1 -1
  17. package/dist/edit.js +31 -1
  18. package/dist/edit.js.map +1 -1
  19. package/dist/exec.js +0 -3
  20. package/dist/exec.js.map +1 -1
  21. package/dist/fetch.d.ts +10 -1
  22. package/dist/fetch.js +6 -7
  23. package/dist/fetch.js.map +1 -1
  24. package/dist/format.js.map +1 -1
  25. package/dist/glob.js.map +1 -1
  26. package/dist/grep.js.map +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +65 -28
  29. package/dist/index.js.map +1 -1
  30. package/dist/install.js.map +1 -1
  31. package/dist/json.js.map +1 -1
  32. package/dist/lint.js.map +1 -1
  33. package/dist/logs.js +4 -0
  34. package/dist/logs.js.map +1 -1
  35. package/dist/outdated.js.map +1 -1
  36. package/dist/pack.js +67 -30
  37. package/dist/pack.js.map +1 -1
  38. package/dist/patch.js.map +1 -1
  39. package/dist/process-registry.js +0 -3
  40. package/dist/process-registry.js.map +1 -1
  41. package/dist/read.js +37 -5
  42. package/dist/read.js.map +1 -1
  43. package/dist/replace.js +0 -1
  44. package/dist/replace.js.map +1 -1
  45. package/dist/scaffold.js.map +1 -1
  46. package/dist/search.js +177 -5
  47. package/dist/search.js.map +1 -1
  48. package/dist/test.js.map +1 -1
  49. package/dist/tree.js.map +1 -1
  50. package/dist/typecheck.js.map +1 -1
  51. package/dist/write.js +31 -1
  52. package/dist/write.js.map +1 -1
  53. package/package.json +2 -2
package/dist/write.js CHANGED
@@ -18,6 +18,36 @@ function ensureInsideRoot(absPath, ctx) {
18
18
  function safeResolve(input, ctx) {
19
19
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
20
20
  }
21
+ async function assertRealInsideRoot(absPath, ctx) {
22
+ const realRoot = await fs.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));
23
+ let probe = absPath;
24
+ for (; ; ) {
25
+ let real;
26
+ try {
27
+ real = await fs.realpath(probe);
28
+ } catch (err) {
29
+ if (err.code === "ENOENT") {
30
+ const parent = path.dirname(probe);
31
+ if (parent === probe) return;
32
+ probe = parent;
33
+ continue;
34
+ }
35
+ throw err;
36
+ }
37
+ const rel = path.relative(realRoot, real);
38
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
39
+ throw new Error(
40
+ `Path "${absPath}" resolves through a symlink outside project root "${realRoot}"`
41
+ );
42
+ }
43
+ return;
44
+ }
45
+ }
46
+ async function safeResolveReal(input, ctx) {
47
+ const abs = safeResolve(input, ctx);
48
+ await assertRealInsideRoot(abs, ctx);
49
+ return abs;
50
+ }
21
51
 
22
52
  // src/write.ts
23
53
  var writeTool = {
@@ -39,7 +69,7 @@ var writeTool = {
39
69
  async execute(input, ctx) {
40
70
  if (!input?.path) throw new Error("write: path is required");
41
71
  if (input.content === void 0) throw new Error("write: content is required");
42
- const absPath = safeResolve(input.path, ctx);
72
+ const absPath = await safeResolveReal(input.path, ctx);
43
73
  let existed = false;
44
74
  let prev = "";
45
75
  try {
package/dist/write.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_util.ts","../src/write.ts"],"names":["stat"],"mappings":"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","file":"write.js","sourcesContent":["import * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n","import * as fs from 'node:fs/promises';\nimport { atomicWrite, unifiedDiff } from '@wrongstack/core';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolve } from './_util.js';\n\ninterface WriteInput {\n path: string;\n content: string;\n}\n\ninterface WriteOutput {\n path: string;\n bytes_written: number;\n created: boolean;\n diff?: string;\n}\n\nexport const writeTool: Tool<WriteInput, WriteOutput> = {\n name: 'write',\n category: 'Filesystem',\n description: 'Write or overwrite a file. For existing files, prefer `edit` over `write`.',\n usageHint:\n 'Use `write` for new files or full replacements. For partial edits use `edit`. Existing files must have been `read` first in this session.',\n permission: 'confirm',\n mutating: true,\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: { type: 'string' },\n content: { type: 'string' },\n },\n required: ['path', 'content'],\n },\n async execute(input, ctx) {\n if (!input?.path) throw new Error('write: path is required');\n if (input.content === undefined) throw new Error('write: content is required');\n const absPath = safeResolve(input.path, ctx);\n\n let existed = false;\n let prev = '';\n try {\n const stat = await fs.stat(absPath);\n existed = stat.isFile();\n if (existed) {\n if (!ctx.hasRead(absPath)) {\n // User approved this write (confirm → yes/always) but ctx has no\n // read record. The model may call write without a prior explicit\n // read. Read the file now so we can compute the diff and honor\n // the user's intent to overwrite.\n prev = await fs.readFile(absPath, 'utf8');\n ctx.recordRead(absPath, stat.mtimeMs);\n } else {\n prev = await fs.readFile(absPath, 'utf8');\n }\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw err;\n }\n }\n\n await atomicWrite(absPath, input.content);\n\n const diff = existed\n ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path })\n : `+++ ${input.path}\\n+ (new file, ${input.content.split('\\n').length} lines)`;\n\n const stat = await fs.stat(absPath);\n ctx.recordRead(absPath, stat.mtimeMs);\n\n // Record for session rewind\n ctx.session.recordFileChange({\n path: absPath,\n action: existed ? 'modified' : 'created',\n before: existed ? prev : null,\n after: input.content,\n });\n\n return {\n path: absPath,\n bytes_written: Buffer.byteLength(input.content, 'utf8'),\n created: !existed,\n diff,\n };\n },\n};\n"]}
1
+ {"version":3,"sources":["../src/_util.ts","../src/write.ts"],"names":["fsp","stat"],"mappings":"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","file":"write.js","sourcesContent":["import * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root→out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink — macOS `/var`→`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n const realRoot = await fsp.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n const rel = path.relative(realRoot, real);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoot}\"`,\n );\n }\n return;\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n","import * as fs from 'node:fs/promises';\nimport { atomicWrite, unifiedDiff } from '@wrongstack/core';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolveReal } from './_util.js';\n\ninterface WriteInput {\n path: string;\n content: string;\n}\n\ninterface WriteOutput {\n path: string;\n bytes_written: number;\n created: boolean;\n diff?: string;\n}\n\nexport const writeTool: Tool<WriteInput, WriteOutput> = {\n name: 'write',\n category: 'Filesystem',\n description: 'Write or overwrite a file. For existing files, prefer `edit` over `write`.',\n usageHint:\n 'Use `write` for new files or full replacements. For partial edits use `edit`. Existing files must have been `read` first in this session.',\n permission: 'confirm',\n mutating: true,\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: { type: 'string' },\n content: { type: 'string' },\n },\n required: ['path', 'content'],\n },\n async execute(input, ctx) {\n if (!input?.path) throw new Error('write: path is required');\n if (input.content === undefined) throw new Error('write: content is required');\n const absPath = await safeResolveReal(input.path, ctx);\n\n let existed = false;\n let prev = '';\n try {\n const stat = await fs.stat(absPath);\n existed = stat.isFile();\n if (existed) {\n if (!ctx.hasRead(absPath)) {\n // User approved this write (confirm → yes/always) but ctx has no\n // read record. The model may call write without a prior explicit\n // read. Read the file now so we can compute the diff and honor\n // the user's intent to overwrite.\n prev = await fs.readFile(absPath, 'utf8');\n ctx.recordRead(absPath, stat.mtimeMs);\n } else {\n prev = await fs.readFile(absPath, 'utf8');\n }\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw err;\n }\n }\n\n await atomicWrite(absPath, input.content);\n\n const diff = existed\n ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path })\n : `+++ ${input.path}\\n+ (new file, ${input.content.split('\\n').length} lines)`;\n\n const stat = await fs.stat(absPath);\n ctx.recordRead(absPath, stat.mtimeMs);\n\n // Record for session rewind\n ctx.session.recordFileChange({\n path: absPath,\n action: existed ? 'modified' : 'created',\n before: existed ? prev : null,\n after: input.content,\n });\n\n return {\n path: absPath,\n bytes_written: Buffer.byteLength(input.content, 'utf8'),\n created: !existed,\n diff,\n };\n },\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/tools",
3
- "version": "0.9.7",
3
+ "version": "0.9.20",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack built-in tools: read/write/edit, bash/exec, grep/glob, git, fetch, test, lint, and more.",
6
6
  "repository": {
@@ -175,7 +175,7 @@
175
175
  "dependencies": {
176
176
  "typescript": "^5.9.3",
177
177
  "undici": "^7.25.0",
178
- "@wrongstack/core": "0.9.7"
178
+ "@wrongstack/core": "0.9.20"
179
179
  },
180
180
  "devDependencies": {
181
181
  "@types/node": "^22.19.19",