@wrongstack/tools 0.5.3 → 0.5.6

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/dist/edit.js CHANGED
@@ -54,12 +54,13 @@ var editTool = {
54
54
  if (!ctx.hasRead(absPath)) {
55
55
  throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
56
56
  }
57
- const lastReadMtime = ctx.lastReadMtime(absPath);
57
+ const original = await fs.readFile(absPath, "utf8");
58
+ const updated = await fs.stat(absPath);
58
59
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
59
- if (lastReadMtime !== void 0 && stat2.mtimeMs > lastReadMtime + mtimeTolerance) {
60
+ const lastReadMtime = ctx.lastReadMtime(absPath);
61
+ if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
60
62
  throw new Error(`edit: file "${input.path}" was modified externally. Re-read it first.`);
61
63
  }
62
- const original = await fs.readFile(absPath, "utf8");
63
64
  const style = detectNewlineStyle(original);
64
65
  const fileLf = normalizeToLf(original);
65
66
  const oldLf = normalizeToLf(input.old_string);
@@ -93,8 +94,7 @@ var editTool = {
93
94
  }
94
95
  const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
95
96
  const newFile = toStyle(newFileLf, style);
96
- await atomicWrite(absPath, newFile, { mode: stat2.mode & 511 });
97
- const updated = await fs.stat(absPath);
97
+ await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
98
98
  ctx.recordRead(absPath, updated.mtimeMs);
99
99
  ctx.session.recordFileChange({
100
100
  path: absPath,
package/dist/edit.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_util.ts","../src/edit.ts"],"names":["stat"],"mappings":"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","file":"edit.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 {\n atomicWrite,\n detectNewlineStyle,\n normalizeToLf,\n toStyle,\n unifiedDiff,\n} from '@wrongstack/core';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolve } from './_util.js';\n\ninterface EditInput {\n path: string;\n old_string: string;\n new_string: string;\n /**\n * When true, replaces all occurrences of `old_string`.\n * When false (default), replaces only the first occurrence and errors\n * if more than one match exists — use this to ensure you target the\n * right location.\n */\n replace_all?: boolean;\n}\n\ninterface EditOutput {\n path: string;\n replacements: number;\n diff: string;\n}\n\nexport const editTool: Tool<EditInput, EditOutput> = {\n name: 'edit',\n category: 'Filesystem',\n description:\n 'Make a surgical edit by replacing exact text. Fails if `old_string` is not unique unless `replace_all` is true.',\n usageHint:\n 'Always `read` the file first. `old_string` must be an EXACT match (whitespace included). If multiple matches exist, either narrow `old_string` with more context or set `replace_all: true`.',\n permission: 'confirm',\n mutating: true,\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: { type: 'string' },\n old_string: { type: 'string' },\n new_string: { type: 'string' },\n replace_all: { type: 'boolean' },\n },\n required: ['path', 'old_string', 'new_string'],\n },\n async execute(input, ctx) {\n if (!input?.path) throw new Error('edit: path is required');\n if (input.old_string === undefined) throw new Error('edit: old_string is required');\n if (input.new_string === undefined) throw new Error('edit: new_string is required');\n if (input.old_string === '') throw new Error('edit: old_string cannot be empty');\n\n const absPath = safeResolve(input.path, ctx);\n const stat = await fs.stat(absPath).catch((err) => {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n throw new Error(`edit: file \"${input.path}\" does not exist. Use \\`write\\` instead.`);\n }\n throw err;\n });\n if (!stat.isFile()) throw new Error(`edit: \"${input.path}\" is not a regular file`);\n\n // Read-before-write invariant\n if (!ctx.hasRead(absPath)) {\n throw new Error(`edit: file \"${input.path}\" was not read in this session. Read it first.`);\n }\n // Stale-read detection. Tolerance accounts for filesystem mtime\n // resolution: ext4/APFS report ms, but Windows FAT and some network\n // filesystems quantize to 2 s. A too-tight tolerance triggers false\n // \"modified externally\" failures when a tool writes and re-reads the\n // same file in quick succession.\n const lastReadMtime = ctx.lastReadMtime(absPath);\n const mtimeTolerance = process.platform === 'win32' ? 2000 : 1;\n if (lastReadMtime !== undefined && stat.mtimeMs > lastReadMtime + mtimeTolerance) {\n throw new Error(`edit: file \"${input.path}\" was modified externally. Re-read it first.`);\n }\n\n const original = await fs.readFile(absPath, 'utf8');\n const style = detectNewlineStyle(original);\n const fileLf = normalizeToLf(original);\n const oldLf = normalizeToLf(input.old_string);\n const newLf = normalizeToLf(input.new_string);\n\n if (oldLf === newLf) {\n return {\n path: absPath,\n replacements: 0,\n diff: '(no-op: old and new are identical)',\n };\n }\n\n let count = 0;\n let idx = fileLf.indexOf(oldLf);\n const matches: number[] = [];\n while (idx !== -1) {\n matches.push(idx);\n count++;\n idx = fileLf.indexOf(oldLf, idx + 1);\n }\n\n if (count === 0) {\n const hint = findSimilarity(fileLf, oldLf);\n throw new Error(\n `edit: no match for old_string in \"${input.path}\".${\n hint ? ` Nearest match near line ${hint}.` : ''\n }`,\n );\n }\n\n if (count > 1 && !input.replace_all) {\n const lines = lineNumbersFor(fileLf, matches);\n throw new Error(\n `edit: old_string matched ${count} times in \"${input.path}\" (lines: ${lines.join(', ')}). ` +\n `Add more context to make it unique, or set replace_all: true.`,\n );\n }\n\n const newFileLf = input.replace_all\n ? fileLf.split(oldLf).join(newLf)\n : fileLf.replace(oldLf, newLf);\n const newFile = toStyle(newFileLf, style);\n\n await atomicWrite(absPath, newFile, { mode: stat.mode & 0o777 });\n const updated = await fs.stat(absPath);\n ctx.recordRead(absPath, updated.mtimeMs);\n\n // Record for session rewind\n ctx.session.recordFileChange({\n path: absPath,\n action: 'modified',\n before: original,\n after: newFile,\n });\n\n const diff = unifiedDiff(original, newFile, {\n fromFile: input.path,\n toFile: input.path,\n });\n\n return {\n path: absPath,\n replacements: input.replace_all ? count : 1,\n diff,\n };\n },\n};\n\nfunction lineNumbersFor(text: string, indices: number[]): number[] {\n const out: number[] = [];\n let pos = 0;\n let line = 1;\n for (const target of indices) {\n while (pos < target) {\n if (text.charCodeAt(pos) === 0x0a) line++;\n pos++;\n }\n out.push(line);\n }\n return out;\n}\n\nfunction findSimilarity(haystack: string, needle: string): number | undefined {\n if (needle.length < 20) return undefined;\n const probe = needle.slice(0, Math.min(40, needle.length));\n const idx = haystack.indexOf(probe);\n if (idx === -1) return undefined;\n let line = 1;\n for (let i = 0; i < idx; i++) {\n if (haystack.charCodeAt(i) === 0x0a) line++;\n }\n return line;\n}\n"]}
1
+ {"version":3,"sources":["../src/_util.ts","../src/edit.ts"],"names":["stat"],"mappings":"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","file":"edit.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 {\n atomicWrite,\n detectNewlineStyle,\n normalizeToLf,\n toStyle,\n unifiedDiff,\n} from '@wrongstack/core';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolve } from './_util.js';\n\ninterface EditInput {\n path: string;\n old_string: string;\n new_string: string;\n /**\n * When true, replaces all occurrences of `old_string`.\n * When false (default), replaces only the first occurrence and errors\n * if more than one match exists — use this to ensure you target the\n * right location.\n */\n replace_all?: boolean;\n}\n\ninterface EditOutput {\n path: string;\n replacements: number;\n diff: string;\n}\n\nexport const editTool: Tool<EditInput, EditOutput> = {\n name: 'edit',\n category: 'Filesystem',\n description:\n 'Make a surgical edit by replacing exact text. Fails if `old_string` is not unique unless `replace_all` is true.',\n usageHint:\n 'Always `read` the file first. `old_string` must be an EXACT match (whitespace included). If multiple matches exist, either narrow `old_string` with more context or set `replace_all: true`.',\n permission: 'confirm',\n mutating: true,\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: { type: 'string' },\n old_string: { type: 'string' },\n new_string: { type: 'string' },\n replace_all: { type: 'boolean' },\n },\n required: ['path', 'old_string', 'new_string'],\n },\n async execute(input, ctx) {\n if (!input?.path) throw new Error('edit: path is required');\n if (input.old_string === undefined) throw new Error('edit: old_string is required');\n if (input.new_string === undefined) throw new Error('edit: new_string is required');\n if (input.old_string === '') throw new Error('edit: old_string cannot be empty');\n\n const absPath = safeResolve(input.path, ctx);\n const stat = await fs.stat(absPath).catch((err) => {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n throw new Error(`edit: file \"${input.path}\" does not exist. Use \\`write\\` instead.`);\n }\n throw err;\n });\n if (!stat.isFile()) throw new Error(`edit: \"${input.path}\" is not a regular file`);\n\n // Read-before-write invariant\n if (!ctx.hasRead(absPath)) {\n throw new Error(`edit: file \"${input.path}\" was not read in this session. Read it first.`);\n }\n // Read BEFORE mtime check to eliminate TOCTOU window.\n // The sequence must be: read content → check mtime → apply edit.\n // If we check mtime first, a concurrent modification between the\n // stat call and the read gives us stale content to search/replace.\n const original = await fs.readFile(absPath, 'utf8');\n const updated = await fs.stat(absPath);\n const mtimeTolerance = process.platform === 'win32' ? 2000 : 1;\n const lastReadMtime = ctx.lastReadMtime(absPath);\n if (lastReadMtime !== undefined && updated.mtimeMs > lastReadMtime + mtimeTolerance) {\n throw new Error(`edit: file \"${input.path}\" was modified externally. Re-read it first.`);\n }\n const style = detectNewlineStyle(original);\n const fileLf = normalizeToLf(original);\n const oldLf = normalizeToLf(input.old_string);\n const newLf = normalizeToLf(input.new_string);\n\n if (oldLf === newLf) {\n return {\n path: absPath,\n replacements: 0,\n diff: '(no-op: old and new are identical)',\n };\n }\n\n let count = 0;\n let idx = fileLf.indexOf(oldLf);\n const matches: number[] = [];\n while (idx !== -1) {\n matches.push(idx);\n count++;\n idx = fileLf.indexOf(oldLf, idx + 1);\n }\n\n if (count === 0) {\n const hint = findSimilarity(fileLf, oldLf);\n throw new Error(\n `edit: no match for old_string in \"${input.path}\".${\n hint ? ` Nearest match near line ${hint}.` : ''\n }`,\n );\n }\n\n if (count > 1 && !input.replace_all) {\n const lines = lineNumbersFor(fileLf, matches);\n throw new Error(\n `edit: old_string matched ${count} times in \"${input.path}\" (lines: ${lines.join(', ')}). ` +\n `Add more context to make it unique, or set replace_all: true.`,\n );\n }\n\n const newFileLf = input.replace_all\n ? fileLf.split(oldLf).join(newLf)\n : fileLf.replace(oldLf, newLf);\n const newFile = toStyle(newFileLf, style);\n\n await atomicWrite(absPath, newFile, { mode: updated.mode & 0o777 });\n ctx.recordRead(absPath, updated.mtimeMs);\n\n // Record for session rewind\n ctx.session.recordFileChange({\n path: absPath,\n action: 'modified',\n before: original,\n after: newFile,\n });\n\n const diff = unifiedDiff(original, newFile, {\n fromFile: input.path,\n toFile: input.path,\n });\n\n return {\n path: absPath,\n replacements: input.replace_all ? count : 1,\n diff,\n };\n },\n};\n\nfunction lineNumbersFor(text: string, indices: number[]): number[] {\n const out: number[] = [];\n let pos = 0;\n let line = 1;\n for (const target of indices) {\n while (pos < target) {\n if (text.charCodeAt(pos) === 0x0a) line++;\n pos++;\n }\n out.push(line);\n }\n return out;\n}\n\nfunction findSimilarity(haystack: string, needle: string): number | undefined {\n if (needle.length < 20) return undefined;\n const probe = needle.slice(0, Math.min(40, needle.length));\n const idx = haystack.indexOf(probe);\n if (idx === -1) return undefined;\n let line = 1;\n for (let i = 0; i < idx; i++) {\n if (haystack.charCodeAt(i) === 0x0a) line++;\n }\n return line;\n}\n"]}
package/dist/exec.js CHANGED
@@ -7,8 +7,8 @@ import { buildChildEnv } from '@wrongstack/core';
7
7
  // src/exec.ts
8
8
  var ALLOWED_COMMANDS = {
9
9
  node: ["--version", "-r", "--input-type=module"],
10
- npm: ["--version", "init", "install", "test", "list", "pkg", "doctor"],
11
- pnpm: ["--version", "init", "install", "add", "remove", "list"],
10
+ npm: ["--version", "list", "pkg", "doctor", "view", "outdated", "audit"],
11
+ pnpm: ["--version", "remove", "list", "view", "outdated", "audit"],
12
12
  npx: ["--version"],
13
13
  git: [
14
14
  "--version",
@@ -36,7 +36,7 @@ var ALLOWED_COMMANDS = {
36
36
  mv: [],
37
37
  rm: ["-rf"],
38
38
  touch: [],
39
- bun: ["--version", "add", "init"],
39
+ bun: ["--version"],
40
40
  tsc: ["--version", "--noEmit", "--project"],
41
41
  vitest: ["--version", "run", "--coverage"],
42
42
  biome: ["--version", "lint", "format", "check"],
@@ -44,7 +44,7 @@ var ALLOWED_COMMANDS = {
44
44
  rustc: ["--version"],
45
45
  go: ["version", "run", "build", "test"],
46
46
  python: ["--version"],
47
- pip: ["--version", "install", "list"],
47
+ pip: ["--version", "list"],
48
48
  docker: ["--version", "ps", "images"],
49
49
  kubectl: ["version", "get", "describe", "logs"]
50
50
  };
package/dist/exec.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/exec.ts"],"names":["resolve"],"mappings":"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","file":"exec.js","sourcesContent":["import { spawn } from 'node:child_process';\r\nimport * as path from 'node:path';\r\nimport type { Tool } from '@wrongstack/core';\r\nimport { buildChildEnv } from './_env.js';\r\n\r\nconst ALLOWED_COMMANDS: Record<string, string[]> = {\r\n node: ['--version', '-r', '--input-type=module'],\r\n npm: ['--version', 'init', 'install', 'test', 'list', 'pkg', 'doctor'],\r\n pnpm: ['--version', 'init', 'install', 'add', 'remove', 'list'],\r\n npx: ['--version'],\r\n git: [\r\n '--version',\r\n 'status',\r\n 'log',\r\n 'diff',\r\n 'branch',\r\n 'checkout',\r\n 'stash',\r\n 'add',\r\n 'commit',\r\n 'push',\r\n 'pull',\r\n ],\r\n ls: ['-la', '-l', '-a'],\r\n cat: [],\r\n head: ['-n'],\r\n tail: ['-n'],\r\n wc: ['-l', '-w', '-c'],\r\n grep: [],\r\n find: [],\r\n echo: [],\r\n mkdir: ['-p'],\r\n cp: ['-r'],\r\n mv: [],\r\n rm: ['-rf'],\r\n touch: [],\r\n bun: ['--version', 'add', 'init'],\r\n tsc: ['--version', '--noEmit', '--project'],\r\n vitest: ['--version', 'run', '--coverage'],\r\n biome: ['--version', 'lint', 'format', 'check'],\r\n cargo: ['--version', 'build', 'test', 'check'],\r\n rustc: ['--version'],\r\n go: ['version', 'run', 'build', 'test'],\r\n python: ['--version'],\r\n pip: ['--version', 'install', 'list'],\r\n docker: ['--version', 'ps', 'images'],\r\n kubectl: ['version', 'get', 'describe', 'logs'],\r\n};\r\n\r\nconst MAX_ARGS = 20;\r\nconst MAX_OUTPUT = 200_000;\r\nconst TIMEOUT_MS = 30_000;\r\n\r\n// Per-command argument validation. Each entry is a list of regex patterns\r\n// that, if matched against any argument, will reject the invocation.\r\n// This blocks common injection vectors through allowlisted commands.\r\nconst BLOCKED_ARG_PATTERNS: Record<string, RegExp[]> = {\r\n // python -c/--command executes arbitrary code; python -m runs modules\r\n python: [/-c$/, /^--command$/, /^-m$/, /^--module$/],\r\n // git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;\r\n // -C <dir> changes working directory, bypassing cwd sandbox\r\n git: [/^--exec=/, /^--upload-pack=/, /^--receive-pack=/, /^-C$/],\r\n // node -r/--require preloads arbitrary modules; --eval executes code\r\n node: [/^-r$/, /^--require$/, /^-e$/, /^--eval$/, /^--prof-process$/],\r\n // go run could execute arbitrary .go files; -ldflags could inject build-time code\r\n go: [/^-ldflags$/],\r\n // bun --preload is similar to node --require\r\n bun: [/^--preload$/],\r\n // docker build/run can create containers with host access;\r\n // only allow read-only commands (ps, images, version)\r\n docker: [/^build$/, /^run$/, /^exec$/, /^push$/, /^pull$/],\r\n // find -exec/-ok/-execdir execute arbitrary commands\r\n find: [/^-exec$/, /^-exec;$/, /^-ok$/, /^-ok;$/, /^-execdir$/, /^-execdir;$/, /^-exec=/, /^-ok=/, /^-execdir=/],\r\n // rm -rf / is catastrophic — block root and home targets\r\n rm: [/^\\/$/, /^\\/\\*$/, /^~$/],\r\n};\r\n\r\nfunction validateArgs(cmd: string, args: string[]): string | null {\r\n const blocked = BLOCKED_ARG_PATTERNS[cmd];\r\n if (!blocked) return null;\r\n\r\n for (const arg of args) {\r\n for (const pattern of blocked) {\r\n if (pattern.test(arg)) {\r\n return `Blocked argument \"${arg}\" for command \"${cmd}\" (matches security pattern ${pattern})`;\r\n }\r\n }\r\n }\r\n return null;\r\n}\r\n\r\ninterface ExecInput {\r\n command: string;\r\n args?: string[];\r\n cwd?: string;\r\n timeout?: number;\r\n}\r\n\r\ninterface ExecOutput {\r\n command: string;\r\n args: string[];\r\n stdout: string;\r\n stderr: string;\r\n exitCode: number;\r\n truncated: boolean;\r\n allowed: boolean;\r\n}\r\n\r\nexport const execTool: Tool<ExecInput, ExecOutput> = {\r\n name: 'exec',\r\n category: 'Shell',\r\n description:\r\n 'Restricted shell that only runs pre-approved commands with constrained arguments. Safer alternative to `bash`.',\r\n usageHint:\r\n 'Set `command` (must be in allowlist). `args` passed through. For arbitrary shell access use the `bash` tool instead.',\r\n permission: 'confirm',\r\n mutating: true,\r\n timeoutMs: TIMEOUT_MS,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n command: { type: 'string', description: 'Command to run (must be in allowlist)' },\r\n args: { type: 'array', items: { type: 'string' }, description: 'Arguments' },\r\n cwd: { type: 'string', description: 'Working directory (must resolve inside project root)' },\r\n timeout: { type: 'integer', description: 'Timeout in ms (default: 30000)' },\r\n },\r\n required: ['command'],\r\n },\r\n async execute(input, ctx, opts) {\r\n const cmd = input.command.trim();\r\n if (!cmd)\r\n return {\r\n command: cmd,\r\n args: [],\r\n stdout: '',\r\n stderr: 'Empty command',\r\n exitCode: 1,\r\n truncated: false,\r\n allowed: false,\r\n };\r\n\r\n if (!(cmd in ALLOWED_COMMANDS)) {\r\n return {\r\n command: cmd,\r\n args: input.args ?? [],\r\n stdout: '',\r\n stderr: `Command \"${cmd}\" not in allowlist. Use the bash tool for arbitrary commands.`,\r\n exitCode: 1,\r\n truncated: false,\r\n allowed: false,\r\n };\r\n }\r\n\r\n const args = (input.args ?? []).slice(0, MAX_ARGS);\r\n const timeout = Math.max(1, Math.min(input.timeout ?? TIMEOUT_MS, TIMEOUT_MS));\r\n\r\n // Validate args against per-command security patterns\r\n const argError = validateArgs(cmd, args);\r\n if (argError) {\r\n return {\r\n command: cmd,\r\n args,\r\n stdout: '',\r\n stderr: argError,\r\n exitCode: 1,\r\n truncated: false,\r\n allowed: false,\r\n };\r\n }\r\n\r\n // Resolve cwd inside the project root. Model-supplied paths like '/etc'\r\n // would otherwise let allowlisted commands operate anywhere on disk.\r\n const requestedCwd = input.cwd ? path.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;\r\n const rel = path.relative(ctx.projectRoot, requestedCwd);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n return {\r\n command: cmd,\r\n args,\r\n stdout: '',\r\n stderr: `cwd \"${input.cwd}\" resolves outside project root`,\r\n exitCode: 1,\r\n truncated: false,\r\n allowed: false,\r\n };\r\n }\r\n const cwd = requestedCwd;\r\n const signal = opts.signal;\r\n\r\n return runCommand(cmd, args, cwd, timeout, signal, ctx.session?.id);\r\n },\r\n};\r\n\r\nfunction runCommand(\r\n cmd: string,\r\n args: string[],\r\n cwd: string,\r\n timeout: number,\r\n signal: AbortSignal,\r\n sessionId: string | undefined,\r\n): Promise<ExecOutput> {\r\n return new Promise((resolve) => {\r\n let stdout = '';\r\n let stderr = '';\r\n let killed = false;\r\n\r\n const child = spawn(cmd, args, {\r\n cwd,\r\n signal,\r\n env: buildChildEnv(sessionId),\r\n stdio: ['ignore', 'pipe', 'pipe'],\r\n });\r\n const timer = setTimeout(() => {\r\n killed = true;\r\n child.kill('SIGTERM');\r\n }, timeout);\r\n\r\n child.stdout?.on('data', (chunk: Buffer) => {\r\n if (stdout.length < MAX_OUTPUT) stdout += chunk.toString();\r\n });\r\n\r\n child.stderr?.on('data', (chunk: Buffer) => {\r\n if (stderr.length < MAX_OUTPUT) stderr += chunk.toString();\r\n });\r\n\r\n child.on('close', (code) => {\r\n clearTimeout(timer);\r\n resolve({\r\n command: cmd,\r\n args,\r\n stdout: stdout.slice(0, MAX_OUTPUT),\r\n stderr: stderr.slice(0, MAX_OUTPUT),\r\n exitCode: killed ? 124 : (code ?? 1),\r\n truncated: stdout.length >= MAX_OUTPUT || stderr.length >= MAX_OUTPUT,\r\n allowed: true,\r\n });\r\n });\r\n\r\n child.on('error', (err) => {\r\n clearTimeout(timer);\r\n resolve({\r\n command: cmd,\r\n args,\r\n stdout: stdout.slice(0, MAX_OUTPUT),\r\n stderr: err.message,\r\n exitCode: 1,\r\n truncated: false,\r\n allowed: true,\r\n });\r\n });\r\n });\r\n}\r\n"]}
1
+ {"version":3,"sources":["../src/exec.ts"],"names":["resolve"],"mappings":"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","file":"exec.js","sourcesContent":["import { spawn } from 'node:child_process';\r\nimport * as path from 'node:path';\r\nimport type { Tool } from '@wrongstack/core';\r\nimport { buildChildEnv } from './_env.js';\r\n\r\nconst ALLOWED_COMMANDS: Record<string, string[]> = {\r\n node: ['--version', '-r', '--input-type=module'],\r\n npm: ['--version', 'list', 'pkg', 'doctor', 'view', 'outdated', 'audit'],\r\n pnpm: ['--version', 'remove', 'list', 'view', 'outdated', 'audit'],\r\n npx: ['--version'],\r\n git: [\r\n '--version',\r\n 'status',\r\n 'log',\r\n 'diff',\r\n 'branch',\r\n 'checkout',\r\n 'stash',\r\n 'add',\r\n 'commit',\r\n 'push',\r\n 'pull',\r\n ],\r\n ls: ['-la', '-l', '-a'],\r\n cat: [],\r\n head: ['-n'],\r\n tail: ['-n'],\r\n wc: ['-l', '-w', '-c'],\r\n grep: [],\r\n find: [],\r\n echo: [],\r\n mkdir: ['-p'],\r\n cp: ['-r'],\r\n mv: [],\r\n rm: ['-rf'],\r\n touch: [],\r\n bun: ['--version'],\r\n tsc: ['--version', '--noEmit', '--project'],\r\n vitest: ['--version', 'run', '--coverage'],\r\n biome: ['--version', 'lint', 'format', 'check'],\r\n cargo: ['--version', 'build', 'test', 'check'],\r\n rustc: ['--version'],\r\n go: ['version', 'run', 'build', 'test'],\r\n python: ['--version'],\r\n pip: ['--version', 'list'],\r\n docker: ['--version', 'ps', 'images'],\r\n kubectl: ['version', 'get', 'describe', 'logs'],\r\n};\r\n\r\nconst MAX_ARGS = 20;\r\nconst MAX_OUTPUT = 200_000;\r\nconst TIMEOUT_MS = 30_000;\r\n\r\n// Per-command argument validation. Each entry is a list of regex patterns\r\n// that, if matched against any argument, will reject the invocation.\r\n// This blocks common injection vectors through allowlisted commands.\r\nconst BLOCKED_ARG_PATTERNS: Record<string, RegExp[]> = {\r\n // python -c/--command executes arbitrary code; python -m runs modules\r\n python: [/-c$/, /^--command$/, /^-m$/, /^--module$/],\r\n // git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;\r\n // -C <dir> changes working directory, bypassing cwd sandbox\r\n git: [/^--exec=/, /^--upload-pack=/, /^--receive-pack=/, /^-C$/],\r\n // node -r/--require preloads arbitrary modules; --eval executes code\r\n node: [/^-r$/, /^--require$/, /^-e$/, /^--eval$/, /^--prof-process$/],\r\n // go run could execute arbitrary .go files; -ldflags could inject build-time code\r\n go: [/^-ldflags$/],\r\n // bun --preload is similar to node --require\r\n bun: [/^--preload$/],\r\n // docker build/run can create containers with host access;\r\n // only allow read-only commands (ps, images, version)\r\n docker: [/^build$/, /^run$/, /^exec$/, /^push$/, /^pull$/],\r\n // find -exec/-ok/-execdir execute arbitrary commands\r\n find: [/^-exec$/, /^-exec;$/, /^-ok$/, /^-ok;$/, /^-execdir$/, /^-execdir;$/, /^-exec=/, /^-ok=/, /^-execdir=/],\r\n // rm -rf / is catastrophic — block root and home targets\r\n rm: [/^\\/$/, /^\\/\\*$/, /^~$/],\r\n};\r\n\r\nfunction validateArgs(cmd: string, args: string[]): string | null {\r\n const blocked = BLOCKED_ARG_PATTERNS[cmd];\r\n if (!blocked) return null;\r\n\r\n for (const arg of args) {\r\n for (const pattern of blocked) {\r\n if (pattern.test(arg)) {\r\n return `Blocked argument \"${arg}\" for command \"${cmd}\" (matches security pattern ${pattern})`;\r\n }\r\n }\r\n }\r\n return null;\r\n}\r\n\r\ninterface ExecInput {\r\n command: string;\r\n args?: string[];\r\n cwd?: string;\r\n timeout?: number;\r\n}\r\n\r\ninterface ExecOutput {\r\n command: string;\r\n args: string[];\r\n stdout: string;\r\n stderr: string;\r\n exitCode: number;\r\n truncated: boolean;\r\n allowed: boolean;\r\n}\r\n\r\nexport const execTool: Tool<ExecInput, ExecOutput> = {\r\n name: 'exec',\r\n category: 'Shell',\r\n description:\r\n 'Restricted shell that only runs pre-approved commands with constrained arguments. Safer alternative to `bash`.',\r\n usageHint:\r\n 'Set `command` (must be in allowlist). `args` passed through. For arbitrary shell access use the `bash` tool instead.',\r\n permission: 'confirm',\r\n mutating: true,\r\n timeoutMs: TIMEOUT_MS,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n command: { type: 'string', description: 'Command to run (must be in allowlist)' },\r\n args: { type: 'array', items: { type: 'string' }, description: 'Arguments' },\r\n cwd: { type: 'string', description: 'Working directory (must resolve inside project root)' },\r\n timeout: { type: 'integer', description: 'Timeout in ms (default: 30000)' },\r\n },\r\n required: ['command'],\r\n },\r\n async execute(input, ctx, opts) {\r\n const cmd = input.command.trim();\r\n if (!cmd)\r\n return {\r\n command: cmd,\r\n args: [],\r\n stdout: '',\r\n stderr: 'Empty command',\r\n exitCode: 1,\r\n truncated: false,\r\n allowed: false,\r\n };\r\n\r\n if (!(cmd in ALLOWED_COMMANDS)) {\r\n return {\r\n command: cmd,\r\n args: input.args ?? [],\r\n stdout: '',\r\n stderr: `Command \"${cmd}\" not in allowlist. Use the bash tool for arbitrary commands.`,\r\n exitCode: 1,\r\n truncated: false,\r\n allowed: false,\r\n };\r\n }\r\n\r\n const args = (input.args ?? []).slice(0, MAX_ARGS);\r\n const timeout = Math.max(1, Math.min(input.timeout ?? TIMEOUT_MS, TIMEOUT_MS));\r\n\r\n // Validate args against per-command security patterns\r\n const argError = validateArgs(cmd, args);\r\n if (argError) {\r\n return {\r\n command: cmd,\r\n args,\r\n stdout: '',\r\n stderr: argError,\r\n exitCode: 1,\r\n truncated: false,\r\n allowed: false,\r\n };\r\n }\r\n\r\n // Resolve cwd inside the project root. Model-supplied paths like '/etc'\r\n // would otherwise let allowlisted commands operate anywhere on disk.\r\n const requestedCwd = input.cwd ? path.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;\r\n const rel = path.relative(ctx.projectRoot, requestedCwd);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n return {\r\n command: cmd,\r\n args,\r\n stdout: '',\r\n stderr: `cwd \"${input.cwd}\" resolves outside project root`,\r\n exitCode: 1,\r\n truncated: false,\r\n allowed: false,\r\n };\r\n }\r\n const cwd = requestedCwd;\r\n const signal = opts.signal;\r\n\r\n return runCommand(cmd, args, cwd, timeout, signal, ctx.session?.id);\r\n },\r\n};\r\n\r\nfunction runCommand(\r\n cmd: string,\r\n args: string[],\r\n cwd: string,\r\n timeout: number,\r\n signal: AbortSignal,\r\n sessionId: string | undefined,\r\n): Promise<ExecOutput> {\r\n return new Promise((resolve) => {\r\n let stdout = '';\r\n let stderr = '';\r\n let killed = false;\r\n\r\n const child = spawn(cmd, args, {\r\n cwd,\r\n signal,\r\n env: buildChildEnv(sessionId),\r\n stdio: ['ignore', 'pipe', 'pipe'],\r\n });\r\n const timer = setTimeout(() => {\r\n killed = true;\r\n child.kill('SIGTERM');\r\n }, timeout);\r\n\r\n child.stdout?.on('data', (chunk: Buffer) => {\r\n if (stdout.length < MAX_OUTPUT) stdout += chunk.toString();\r\n });\r\n\r\n child.stderr?.on('data', (chunk: Buffer) => {\r\n if (stderr.length < MAX_OUTPUT) stderr += chunk.toString();\r\n });\r\n\r\n child.on('close', (code) => {\r\n clearTimeout(timer);\r\n resolve({\r\n command: cmd,\r\n args,\r\n stdout: stdout.slice(0, MAX_OUTPUT),\r\n stderr: stderr.slice(0, MAX_OUTPUT),\r\n exitCode: killed ? 124 : (code ?? 1),\r\n truncated: stdout.length >= MAX_OUTPUT || stderr.length >= MAX_OUTPUT,\r\n allowed: true,\r\n });\r\n });\r\n\r\n child.on('error', (err) => {\r\n clearTimeout(timer);\r\n resolve({\r\n command: cmd,\r\n args,\r\n stdout: stdout.slice(0, MAX_OUTPUT),\r\n stderr: err.message,\r\n exitCode: 1,\r\n truncated: false,\r\n allowed: true,\r\n });\r\n });\r\n });\r\n}\r\n"]}
package/dist/index.d.ts CHANGED
@@ -43,18 +43,22 @@ export { builtinToolsPack } from './pack.js';
43
43
  * during startup so the tool always knows where to read/write.
44
44
  *
45
45
  * One tool, multiple actions, JSON in/out. The action discriminates the
46
- * operation so the LLM can do show / add / start / done / remove / clear
47
- * via a single tool registration instead of bloating the surface with
48
- * six near-identical tools.
46
+ * operation so the LLM can do show / add / start / done / remove / promote /
47
+ * derive / template_use / clear via a single tool registration instead of
48
+ * bloating the surface with nine near-identical tools.
49
49
  */
50
50
  interface PlanInput {
51
- action: 'show' | 'add' | 'start' | 'done' | 'remove' | 'clear';
51
+ action: 'show' | 'add' | 'start' | 'done' | 'remove' | 'promote' | 'derive' | 'template_use' | 'clear';
52
52
  /** Required for add. */
53
53
  title?: string;
54
54
  /** Optional detail line for add. */
55
55
  details?: string;
56
- /** Required for start/done/remove — accepts plan item id OR 1-based index OR title substring. */
56
+ /** Required for start/done/remove/promote/derive — accepts plan item id OR 1-based index OR title substring. */
57
57
  target?: string;
58
+ /** Optional subtasks for promote/derive. If omitted, a single todo is created from the plan item title. */
59
+ subtasks?: string[];
60
+ /** Required for template_use — the template name (e.g. "new-feature", "bug-fix"). */
61
+ template?: string;
58
62
  }
59
63
  interface PlanOutput {
60
64
  ok: boolean;
@@ -65,6 +69,13 @@ interface PlanOutput {
65
69
  count: number;
66
70
  /** Number of items not in 'done' status. */
67
71
  open: number;
72
+ /** When promote/derive succeed, the generated todo items so the caller can inspect them. */
73
+ todos?: Array<{
74
+ id: string;
75
+ content: string;
76
+ status: string;
77
+ activeForm?: string;
78
+ }>;
68
79
  }
69
80
  declare const planTool: Tool<PlanInput, PlanOutput>;
70
81
 
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as fs4 from 'fs/promises';
2
2
  import * as path from 'path';
3
3
  import { dirname } from 'path';
4
- import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, buildChildEnv, stripAnsi, loadPlan, emptyPlan, clearPlan, savePlan, removePlanItem, setPlanItemStatus, addPlanItem, formatPlan } from '@wrongstack/core';
4
+ import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, buildChildEnv, stripAnsi, loadPlan, emptyPlan, clearPlan, savePlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, formatPlan } from '@wrongstack/core';
5
5
  import { spawn } from 'child_process';
6
6
  import * as os from 'os';
7
7
  import * as dns from 'dns/promises';
@@ -196,12 +196,13 @@ var editTool = {
196
196
  if (!ctx.hasRead(absPath)) {
197
197
  throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
198
198
  }
199
- const lastReadMtime = ctx.lastReadMtime(absPath);
199
+ const original = await fs4.readFile(absPath, "utf8");
200
+ const updated = await fs4.stat(absPath);
200
201
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
201
- if (lastReadMtime !== void 0 && stat9.mtimeMs > lastReadMtime + mtimeTolerance) {
202
+ const lastReadMtime = ctx.lastReadMtime(absPath);
203
+ if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
202
204
  throw new Error(`edit: file "${input.path}" was modified externally. Re-read it first.`);
203
205
  }
204
- const original = await fs4.readFile(absPath, "utf8");
205
206
  const style = detectNewlineStyle(original);
206
207
  const fileLf = normalizeToLf(original);
207
208
  const oldLf = normalizeToLf(input.old_string);
@@ -235,8 +236,7 @@ var editTool = {
235
236
  }
236
237
  const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
237
238
  const newFile = toStyle(newFileLf, style);
238
- await atomicWrite(absPath, newFile, { mode: stat9.mode & 511 });
239
- const updated = await fs4.stat(absPath);
239
+ await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
240
240
  ctx.recordRead(absPath, updated.mtimeMs);
241
241
  ctx.session.recordFileChange({
242
242
  path: absPath,
@@ -494,7 +494,12 @@ async function globNative(pattern, base, extraGlob) {
494
494
  for (const e of entries) {
495
495
  if (DEFAULT_IGNORE.includes(e.name)) continue;
496
496
  const full = path.join(dir, e.name);
497
- if (e.isSymbolicLink()) continue;
497
+ try {
498
+ const stat9 = await fs4.lstat(full);
499
+ if (stat9.isSymbolicLink()) continue;
500
+ } catch {
501
+ continue;
502
+ }
498
503
  if (e.isDirectory()) {
499
504
  await walk(full);
500
505
  } else if (e.isFile()) {
@@ -1064,8 +1069,8 @@ var bashTool = {
1064
1069
  };
1065
1070
  var ALLOWED_COMMANDS = {
1066
1071
  node: ["--version", "-r", "--input-type=module"],
1067
- npm: ["--version", "init", "install", "test", "list", "pkg", "doctor"],
1068
- pnpm: ["--version", "init", "install", "add", "remove", "list"],
1072
+ npm: ["--version", "list", "pkg", "doctor", "view", "outdated", "audit"],
1073
+ pnpm: ["--version", "remove", "list", "view", "outdated", "audit"],
1069
1074
  npx: ["--version"],
1070
1075
  git: [
1071
1076
  "--version",
@@ -1093,7 +1098,7 @@ var ALLOWED_COMMANDS = {
1093
1098
  mv: [],
1094
1099
  rm: ["-rf"],
1095
1100
  touch: [],
1096
- bun: ["--version", "add", "init"],
1101
+ bun: ["--version"],
1097
1102
  tsc: ["--version", "--noEmit", "--project"],
1098
1103
  vitest: ["--version", "run", "--coverage"],
1099
1104
  biome: ["--version", "lint", "format", "check"],
@@ -1101,7 +1106,7 @@ var ALLOWED_COMMANDS = {
1101
1106
  rustc: ["--version"],
1102
1107
  go: ["version", "run", "build", "test"],
1103
1108
  python: ["--version"],
1104
- pip: ["--version", "install", "list"],
1109
+ pip: ["--version", "list"],
1105
1110
  docker: ["--version", "ps", "images"],
1106
1111
  kubectl: ["version", "get", "describe", "logs"]
1107
1112
  };
@@ -1816,8 +1821,8 @@ var todoTool = {
1816
1821
  var planTool = {
1817
1822
  name: "plan",
1818
1823
  category: "Session",
1819
- description: "Inspect or edit the strategic plan board for this session. Plans persist across resume (unlike todos). Use this to lay out the multi-step approach before diving in, then mark steps in_progress/done as the work proceeds.",
1820
- usageHint: "Set action to one of: show | add | start | done | remove | clear. Pass `title` for add. Pass `target` (item id, 1-based index, or title substring) for start/done/remove. Always returns the formatted plan plus open/total counts.",
1824
+ description: "Inspect or edit the strategic plan board for this session. Plans persist across resume (unlike todos). Use this to lay out the multi-step approach before diving in, then mark steps in_progress/done as the work proceeds. Promote a plan item to todos to start working on it. Apply templates for common workflows.",
1825
+ usageHint: 'Set action to one of: show | add | start | done | remove | promote | derive | template_use | clear. Pass `title` for add. Pass `target` (item id, 1-based index, or title substring) for start/done/remove/promote/derive. Pass `subtasks` for promote/derive to break the plan item into multiple todos. Pass `template` (e.g. "new-feature", "bug-fix", "refactor", "release") for template_use. Always returns the formatted plan plus open/total counts.',
1821
1826
  permission: "auto",
1822
1827
  mutating: false,
1823
1828
  timeoutMs: 2e3,
@@ -1826,13 +1831,22 @@ var planTool = {
1826
1831
  properties: {
1827
1832
  action: {
1828
1833
  type: "string",
1829
- enum: ["show", "add", "start", "done", "remove", "clear"]
1834
+ enum: ["show", "add", "start", "done", "remove", "promote", "derive", "template_use", "clear"]
1830
1835
  },
1831
1836
  title: { type: "string", description: "Required when action = add." },
1832
1837
  details: { type: "string", description: "Optional extra context for add." },
1833
1838
  target: {
1834
1839
  type: "string",
1835
- description: "Plan item id, 1-based index, or title substring. Required for start/done/remove."
1840
+ description: "Plan item id, 1-based index, or title substring. Required for start/done/remove/promote/derive."
1841
+ },
1842
+ subtasks: {
1843
+ type: "array",
1844
+ items: { type: "string" },
1845
+ description: "Optional subtasks for promote/derive. If omitted, a single todo is created from the plan item title."
1846
+ },
1847
+ template: {
1848
+ type: "string",
1849
+ description: "Template name for template_use action. Available: new-feature, bug-fix, refactor, release, security-audit, onboarding."
1836
1850
  }
1837
1851
  },
1838
1852
  required: ["action"]
@@ -1891,6 +1905,35 @@ var planTool = {
1891
1905
  await savePlan(planPath, plan);
1892
1906
  break;
1893
1907
  }
1908
+ case "promote":
1909
+ case "derive": {
1910
+ if (!input.target) {
1911
+ return mkResult(plan, false, `${input.action} requires \`target\` (id|index|substring).`);
1912
+ }
1913
+ const derived = deriveTodosFromPlanItem(plan, input.target, input.subtasks);
1914
+ if (!derived) {
1915
+ return mkResult(plan, false, `No plan item matched "${input.target}".`);
1916
+ }
1917
+ plan = derived.plan;
1918
+ await savePlan(planPath, plan);
1919
+ ctx.state.replaceTodos(derived.todos);
1920
+ return mkResult(plan, true, `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`, derived.todos);
1921
+ }
1922
+ case "template_use": {
1923
+ const templateName = input.template?.trim();
1924
+ if (!templateName) {
1925
+ return mkResult(plan, false, "template_use requires `template` name.");
1926
+ }
1927
+ const template = getPlanTemplate(templateName);
1928
+ if (!template) {
1929
+ return mkResult(plan, false, `Unknown template "${templateName}".`);
1930
+ }
1931
+ for (const item of template.items) {
1932
+ ({ plan } = addPlanItem(plan, item.title, item.details));
1933
+ }
1934
+ await savePlan(planPath, plan);
1935
+ return mkResult(plan, true, `Applied template "${template.name}" \u2014 ${template.items.length} items added.`);
1936
+ }
1894
1937
  case "clear":
1895
1938
  plan = clearPlan(plan);
1896
1939
  await savePlan(planPath, plan);
@@ -1901,14 +1944,15 @@ var planTool = {
1901
1944
  return mkResult(plan, true, `Plan ${input.action} ok.`);
1902
1945
  }
1903
1946
  };
1904
- function mkResult(plan, ok, message) {
1947
+ function mkResult(plan, ok, message, todos) {
1905
1948
  const open = plan.items.filter((i) => i.status !== "done").length;
1906
1949
  return {
1907
1950
  ok,
1908
1951
  message,
1909
1952
  plan: formatPlan(plan),
1910
1953
  count: plan.items.length,
1911
- open
1954
+ open,
1955
+ todos
1912
1956
  };
1913
1957
  }
1914
1958
  var TIMEOUT_MS4 = 3e4;
@@ -2432,7 +2476,11 @@ var DEFAULT_IGNORE4 = [
2432
2476
  "build",
2433
2477
  ".next",
2434
2478
  "coverage",
2435
- "__pycache__"
2479
+ "__pycache__",
2480
+ ".wrongstack",
2481
+ ".ssh",
2482
+ ".gnupg",
2483
+ ".aws"
2436
2484
  ];
2437
2485
  var treeTool = {
2438
2486
  name: "tree",