@wrongstack/tools 0.299.0 → 0.301.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.
Files changed (53) hide show
  1. package/dist/audit.js +6 -2
  2. package/dist/bash.js +6 -2
  3. package/dist/batch-tool-use.js +3 -1
  4. package/dist/browser/index.js +1 -1
  5. package/dist/builtin.d.ts +3 -2
  6. package/dist/builtin.js +1781 -376
  7. package/dist/codebase-index/bm25.d.ts +7 -1
  8. package/dist/codebase-index/import-extractor.d.ts +39 -0
  9. package/dist/codebase-index/index.js +1418 -267
  10. package/dist/codebase-index/languages.d.ts +24 -0
  11. package/dist/codebase-index/module-resolver.d.ts +78 -0
  12. package/dist/codebase-index/module-roots.d.ts +81 -0
  13. package/dist/codebase-index/parser-output.d.ts +29 -0
  14. package/dist/codebase-index/project-server.js +1402 -249
  15. package/dist/codebase-index/rs-parser.d.ts +22 -0
  16. package/dist/codebase-index/schema.d.ts +24 -1
  17. package/dist/codebase-index/worker.js +1401 -250
  18. package/dist/codebase-index/writer-graph-helpers.d.ts +17 -5
  19. package/dist/codebase-index/writer-ref-mapper.d.ts +3 -0
  20. package/dist/codebase-index/writer-schema.d.ts +15 -3
  21. package/dist/codebase-index/writer.d.ts +76 -3
  22. package/dist/exec.js +35 -2
  23. package/dist/format.js +6 -2
  24. package/dist/git.js +2 -5
  25. package/dist/glob.js +2 -2
  26. package/dist/grep.js +118 -3
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +1925 -415
  29. package/dist/install.js +6 -2
  30. package/dist/json.js +132 -2
  31. package/dist/languages/index.js +6 -2
  32. package/dist/lint.js +6 -2
  33. package/dist/logs.js +81 -0
  34. package/dist/next-steps-tool.d.ts +26 -0
  35. package/dist/outdated.js +6 -2
  36. package/dist/pack.js +1781 -376
  37. package/dist/patch.js +206 -45
  38. package/dist/process-registry.d.ts +6 -0
  39. package/dist/process-registry.js +6 -2
  40. package/dist/ps-slash.js +6 -2
  41. package/dist/read.js +1410 -257
  42. package/dist/replace.js +81 -0
  43. package/dist/skill.js +51 -2
  44. package/dist/test.js +6 -2
  45. package/dist/tool-help.js +2 -2
  46. package/dist/tool-search.js +1 -1
  47. package/dist/tool-tier.d.ts +1 -1
  48. package/dist/tool-tier.js +1786 -397
  49. package/dist/tool-use.js +1 -1
  50. package/dist/tree.js +13 -3
  51. package/dist/typecheck.js +6 -2
  52. package/package.json +3 -3
  53. package/dist/codebase-index/refs-extractor.d.ts +0 -11
package/dist/patch.js CHANGED
@@ -3,10 +3,11 @@ import { spawn } from "node:child_process";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as os from "node:os";
5
5
  import * as path2 from "node:path";
6
- import { buildChildEnv } from "@wrongstack/core/utils";
6
+ import { buildChildEnv, toErrorMessage } from "@wrongstack/core/utils";
7
7
 
8
8
  // src/_util.ts
9
9
  import { createHash } from "node:crypto";
10
+ import * as fsp from "node:fs/promises";
10
11
  import * as path from "node:path";
11
12
  import * as Core from "@wrongstack/core/utils";
12
13
  function sha256hex(content) {
@@ -33,13 +34,46 @@ function ensureInsideRoot(absPath, ctx) {
33
34
  function safeResolve(input, ctx) {
34
35
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
35
36
  }
37
+ async function resolveRealInsideRoot(absPath, ctx) {
38
+ if (ctx.allowOutsideProjectRoot) return absPath;
39
+ const realRoots = await Promise.all(
40
+ allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r)))
41
+ );
42
+ let probe = absPath;
43
+ const pendingTail = [];
44
+ for (; ; ) {
45
+ let real;
46
+ try {
47
+ real = await fsp.realpath(probe);
48
+ } catch (err) {
49
+ if (err.code === "ENOENT") {
50
+ const parent = path.dirname(probe);
51
+ if (parent === probe) return absPath;
52
+ pendingTail.unshift(path.basename(probe));
53
+ probe = parent;
54
+ continue;
55
+ }
56
+ throw err;
57
+ }
58
+ if (isInsideAny(real, realRoots)) {
59
+ return pendingTail.length > 0 ? path.join(real, ...pendingTail) : real;
60
+ }
61
+ throw new Error(
62
+ `Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
63
+ );
64
+ }
65
+ }
66
+ async function safeResolveReal(input, ctx) {
67
+ const abs = safeResolve(input, ctx);
68
+ return await resolveRealInsideRoot(abs, ctx);
69
+ }
36
70
 
37
71
  // src/patch.ts
38
72
  var patchTool = {
39
73
  name: "patch",
40
74
  category: "Filesystem",
41
75
  description: "Apply a unified diff (patch) to the project. This is the correct tool when you have a diff that needs to be applied precisely, including handling of rejects.",
42
- usageHint: "Best used when you already have a diff (from generation, external source, or previous step).\n- Use `dry_run: true` to see what would happen without modifying files.\n- On failure it creates .rej and .orig files for manual review.\nOften cleaner than many small `edit` operations for larger changes.",
76
+ usageHint: "Best used when you already have a diff (from generation, external source, or previous step).\n- Use `dry_run: true` to see what would happen without modifying files.\n- Applied with `--merge`: a conflicting hunk writes git-style conflict\n markers (<<<<<<< / ======= / >>>>>>>) INTO the file and reports failure.\n It does NOT create .rej/.orig files. `files` lists what changed on disk\n even when the patch failed, so read those back before retrying.\nOften cleaner than many small `edit` operations for larger changes.",
43
77
  selection: {
44
78
  doNotUseWhen: "you do not already have a unified diff or only need one precise replacement.",
45
79
  useInstead: ["edit"]
@@ -65,31 +99,50 @@ var patchTool = {
65
99
  },
66
100
  async execute(input, ctx, opts) {
67
101
  if (!input?.patch) throw new Error("patch: patch content is required");
68
- const dir = input.directory ? safeResolve(input.directory, ctx) : ctx.cwd;
69
102
  const strip = Math.max(1, input.strip ?? 1);
70
103
  const dryRun = input.dry_run ?? false;
104
+ const refuse = (message) => ({
105
+ applied: 0,
106
+ rejected: 1,
107
+ files: [],
108
+ dry_run: dryRun,
109
+ message
110
+ });
111
+ let dir;
112
+ try {
113
+ dir = input.directory ? await safeResolveReal(input.directory, ctx) : ctx.cwd;
114
+ } catch (err) {
115
+ return refuse(`patch refused: ${toErrorMessage(err)}`);
116
+ }
117
+ const realRoot = await fs.realpath(ctx.projectRoot).catch(() => path2.resolve(ctx.projectRoot));
71
118
  const targets = extractDiffTargets(input.patch);
72
119
  const resolvedTargets = [];
73
120
  for (const t of targets) {
74
- const stripped = stripPathComponents(t, strip);
121
+ const stripped = stripPathComponents(t.raw, strip);
75
122
  if (!stripped) continue;
123
+ if (path2.isAbsolute(stripped)) {
124
+ return refuse(`patch refused: target "${t.raw}" strips to absolute path`);
125
+ }
76
126
  const candidate = path2.resolve(dir, stripped);
77
- const rel = path2.relative(ctx.projectRoot, candidate);
127
+ let real;
128
+ try {
129
+ real = await resolveRealInsideRoot(candidate, ctx);
130
+ } catch (err) {
131
+ return refuse(`patch refused: target "${t.raw}" ${toErrorMessage(err)}`);
132
+ }
133
+ const rel = path2.relative(realRoot, real);
78
134
  if (rel.startsWith("..") || path2.isAbsolute(rel)) {
79
- return {
80
- applied: 0,
81
- rejected: 1,
82
- files: [],
83
- dry_run: dryRun,
84
- message: `patch refused: target "${t}" resolves outside project root`
85
- };
135
+ return refuse(`patch refused: target "${t.raw}" resolves outside project root`);
86
136
  }
87
- resolvedTargets.push(candidate);
137
+ resolvedTargets.push({ raw: t.raw, deleted: t.deleted, abs: real });
88
138
  }
89
139
  const beforeContents = /* @__PURE__ */ new Map();
140
+ const beforeExisted = /* @__PURE__ */ new Set();
90
141
  if (!dryRun) {
91
142
  for (const target of resolvedTargets) {
92
- beforeContents.set(target, await readTextForTracking(target));
143
+ const existed = (await fs.stat(target.abs).catch(() => null))?.isFile() ?? false;
144
+ if (existed) beforeExisted.add(target.abs);
145
+ beforeContents.set(target.abs, await readTextForTracking(target.abs));
93
146
  }
94
147
  }
95
148
  const tmpDir = await fs.mkdtemp(path2.join(os.tmpdir(), ".wstack_patch_"));
@@ -99,32 +152,70 @@ var patchTool = {
99
152
  const patchFile = path2.join(tmpDir, "in.diff");
100
153
  await fs.writeFile(patchFile, input.patch, { mode: 384 });
101
154
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
102
- const result = await runPatch(args, dir, opts.signal);
103
- if (result.exitCode !== 0 && !dryRun) {
104
- return {
105
- applied: 0,
106
- rejected: 1,
107
- files: [],
108
- dry_run: dryRun,
109
- message: `patch failed: ${result.stderr || result.stdout}`
110
- };
111
- }
112
- const patched = extractPatchedFiles(result.stdout);
155
+ const result = await runPatch(args, dir, opts.signal, {
156
+ patchFile,
157
+ strip,
158
+ dryRun
159
+ });
160
+ const touched = [];
113
161
  if (!dryRun) {
114
162
  for (const target of resolvedTargets) {
115
- const before = beforeContents.get(target) ?? null;
116
- const after = await readTextForTracking(target);
163
+ const abs = target.abs;
164
+ const before = beforeContents.get(abs) ?? null;
165
+ const stat2 = await fs.stat(abs).catch(() => null);
166
+ if (!stat2?.isFile()) {
167
+ if (beforeExisted.has(abs)) {
168
+ touched.push(abs);
169
+ ctx.session?.recordFileChange?.({
170
+ path: abs,
171
+ action: "deleted",
172
+ before,
173
+ after: null
174
+ });
175
+ }
176
+ continue;
177
+ }
178
+ const after = await readTextForTracking(abs);
117
179
  if (after === null || after === before) continue;
118
- const stat2 = await fs.stat(target).catch(() => null);
119
- if (stat2) ctx.recordRead?.(target, stat2.mtimeMs, "write", sha256hex(after));
180
+ touched.push(abs);
181
+ ctx.recordRead?.(abs, stat2.mtimeMs, "write", sha256hex(after));
120
182
  ctx.session?.recordFileChange?.({
121
- path: target,
183
+ path: abs,
122
184
  action: before === null ? "created" : "modified",
123
185
  before,
124
186
  after
125
187
  });
126
188
  }
127
189
  }
190
+ if (result.exitCode !== 0) {
191
+ if (!dryRun) {
192
+ const partial = touched.length > 0 ? ` ${touched.length} file(s) were still modified on disk and have been recorded for rewind: ${touched.map((p) => path2.relative(realRoot, p) || p).join(", ")}.` : "";
193
+ return {
194
+ applied: touched.length,
195
+ rejected: 1,
196
+ // Normalize to relative-to-realRoot for API consistency with the
197
+ // success path (which returns GNU patch's dir-relative names).
198
+ // `touched` entries are realpaths from resolveRealInsideRoot, and
199
+ // realRoot is also a realpath, so path.relative is like-for-like.
200
+ files: touched.map((p) => path2.relative(realRoot, p) || p),
201
+ dry_run: dryRun,
202
+ message: `patch failed: ${result.stderr || result.stdout}${partial}`
203
+ };
204
+ }
205
+ const wouldPatch = extractPatchedFiles(result.stdout);
206
+ return {
207
+ applied: wouldPatch.length,
208
+ rejected: 1,
209
+ files: wouldPatch,
210
+ dry_run: dryRun,
211
+ message: `patch preview: would conflict \u2014 ${result.stderr || result.stdout}`
212
+ };
213
+ }
214
+ const patched = result.engine === "git" ? [
215
+ ...new Set(
216
+ resolvedTargets.map((target) => path2.relative(dir, target.abs) || target.abs)
217
+ )
218
+ ] : extractPatchedFiles(result.stdout);
128
219
  return {
129
220
  applied: patched.length,
130
221
  rejected: 0,
@@ -152,27 +243,86 @@ async function readTextForTracking(absPath) {
152
243
  }
153
244
  function extractDiffTargets(patch) {
154
245
  const out = [];
155
- const re = /^\+\+\+\s+([^\t\r\n]+)/gm;
156
- for (const m of patch.matchAll(re)) {
157
- const raw = m[1];
158
- if (!raw) continue;
159
- const target = raw.length > 4096 ? raw.slice(0, 4096).trim() : raw.trim();
160
- if (!target || target === "/dev/null") continue;
161
- out.push(target);
246
+ const clean = (raw) => {
247
+ if (!raw) return "";
248
+ return (raw.length > 4096 ? raw.slice(0, 4096) : raw).trim();
249
+ };
250
+ let lastOld;
251
+ let inHunk = false;
252
+ let oldLinesLeft = 0;
253
+ let newLinesLeft = 0;
254
+ for (const line of patch.split(/\r?\n/)) {
255
+ const hunkMatch = /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/.exec(line);
256
+ if (hunkMatch) {
257
+ inHunk = true;
258
+ oldLinesLeft = hunkMatch[1] ? Number(hunkMatch[1]) : 1;
259
+ newLinesLeft = hunkMatch[2] ? Number(hunkMatch[2]) : 1;
260
+ lastOld = void 0;
261
+ continue;
262
+ }
263
+ if (inHunk) {
264
+ const ch = line[0];
265
+ if (ch === "-") oldLinesLeft--;
266
+ else if (ch === "+") newLinesLeft--;
267
+ else if (ch === " " || ch === void 0) {
268
+ oldLinesLeft--;
269
+ newLinesLeft--;
270
+ }
271
+ if (oldLinesLeft <= 0 && newLinesLeft <= 0) inHunk = false;
272
+ continue;
273
+ }
274
+ const oldMatch = /^---\s+([^\t\r\n]+)/.exec(line);
275
+ if (oldMatch) {
276
+ lastOld = clean(oldMatch[1]);
277
+ continue;
278
+ }
279
+ const newMatch = /^\+\+\+\s+([^\t\r\n]+)/.exec(line);
280
+ if (!newMatch) continue;
281
+ const newTarget = clean(newMatch[1]);
282
+ if (newTarget && newTarget !== "/dev/null") {
283
+ out.push({ raw: newTarget, deleted: false });
284
+ } else if (lastOld && lastOld !== "/dev/null") {
285
+ out.push({ raw: lastOld, deleted: true });
286
+ }
287
+ lastOld = void 0;
162
288
  }
163
289
  return out;
164
290
  }
165
291
  function stripPathComponents(p, strip) {
166
- const parts = p.replace(/\\/g, "/").split("/").filter((s) => s !== "" && s !== ".");
167
- if (parts.length <= strip) return void 0;
168
- return parts.slice(strip).join("/");
292
+ const s = p.replace(/\\/g, "/");
293
+ let idx = 0;
294
+ for (let i = 0; i < strip; i++) {
295
+ while (idx < s.length && s[idx] !== "/") idx++;
296
+ let hadSlash = false;
297
+ while (idx < s.length && s[idx] === "/") {
298
+ idx++;
299
+ hadSlash = true;
300
+ }
301
+ if (!hadSlash) return void 0;
302
+ }
303
+ return s.slice(idx) || void 0;
304
+ }
305
+ function runPatch(args, cwd, signal, fallback) {
306
+ return runPatchProcess("patch", args, cwd, signal).then(async (result) => {
307
+ if (!result.unavailable) return { ...result, engine: "patch" };
308
+ const gitArgs = [
309
+ "apply",
310
+ "--unsafe-paths",
311
+ `-p${fallback.strip}`,
312
+ "--verbose",
313
+ ...fallback.dryRun ? ["--check"] : [],
314
+ fallback.patchFile
315
+ ];
316
+ const gitResult = await runPatchProcess("git", gitArgs, cwd, signal);
317
+ return { ...gitResult, engine: "git" };
318
+ });
169
319
  }
170
- function runPatch(args, cwd, signal) {
320
+ function runPatchProcess(command, args, cwd, signal) {
171
321
  return new Promise((resolve3) => {
172
322
  let stdout = "";
173
323
  let stderr = "";
174
324
  const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
175
- const child = spawn("patch", args, {
325
+ const child = spawn(command, args, {
176
326
  cwd,
177
327
  signal,
178
328
  env,
@@ -185,13 +335,24 @@ function runPatch(args, cwd, signal) {
185
335
  child.stderr?.on("data", (c) => {
186
336
  stderr += c.toString();
187
337
  });
188
- child.on("close", (code) => resolve3({ exitCode: code ?? 1, stdout, stderr }));
189
- child.on("error", (e) => resolve3({ exitCode: 1, stdout: "", stderr: e.message }));
338
+ child.on(
339
+ "close",
340
+ (code) => resolve3({ exitCode: code ?? 1, stdout, stderr, unavailable: false })
341
+ );
342
+ child.on(
343
+ "error",
344
+ (e) => resolve3({
345
+ exitCode: 1,
346
+ stdout: "",
347
+ stderr: e.message,
348
+ unavailable: e.code === "ENOENT"
349
+ })
350
+ );
190
351
  });
191
352
  }
192
353
  function extractPatchedFiles(output) {
193
354
  const files = [];
194
- const re = /patching file (.+)/gi;
355
+ const re = /(?:patching|checking) file (.+)/gi;
195
356
  for (const m of output.matchAll(re)) {
196
357
  if (m[1]) files.push(m[1]);
197
358
  }
@@ -37,6 +37,12 @@ interface KillOpts {
37
37
  graceMs?: number | undefined;
38
38
  /** Leave explicitly backgrounded jobs alive. Default false. */
39
39
  preserveBackground?: boolean | undefined;
40
+ /**
41
+ * Also kill processes marked `protected` (browser open, etc.).
42
+ * Default false. Host-process shutdown should pass true so protected
43
+ * children cannot strand the parent event loop (issue #322).
44
+ */
45
+ includeProtected?: boolean | undefined;
40
46
  }
41
47
  /**
42
48
  * Snapshot of the armed auto kill/reset countdown, or null when nothing is
@@ -503,7 +503,7 @@ var ProcessRegistryImpl = class {
503
503
  const p = this.processes.get(pid);
504
504
  if (!p) return false;
505
505
  if (p.killed) return true;
506
- if (p.protected) return false;
506
+ if (p.protected && opts.includeProtected !== true) return false;
507
507
  if (opts.preserveBackground && p.background) return false;
508
508
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
509
509
  const isWin = os.platform() === "win32";
@@ -554,9 +554,13 @@ var ProcessRegistryImpl = class {
554
554
  killAll(opts = {}) {
555
555
  const pids = Array.from(this.processes.keys());
556
556
  const killed = [];
557
+ const includeProtected = opts.includeProtected === true;
557
558
  for (const pid of pids) {
558
559
  const p = this.processes.get(pid);
559
- if (p && !p.protected && this.kill(pid, opts)) killed.push(pid);
560
+ if (!p) continue;
561
+ if (p.protected && !includeProtected) continue;
562
+ if (opts.preserveBackground && p.background) continue;
563
+ if (this.kill(pid, opts)) killed.push(pid);
560
564
  }
561
565
  return killed;
562
566
  }
package/dist/ps-slash.js CHANGED
@@ -473,7 +473,7 @@ var ProcessRegistryImpl = class {
473
473
  const p = this.processes.get(pid);
474
474
  if (!p) return false;
475
475
  if (p.killed) return true;
476
- if (p.protected) return false;
476
+ if (p.protected && opts.includeProtected !== true) return false;
477
477
  if (opts.preserveBackground && p.background) return false;
478
478
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
479
479
  const isWin = os.platform() === "win32";
@@ -524,9 +524,13 @@ var ProcessRegistryImpl = class {
524
524
  killAll(opts = {}) {
525
525
  const pids = Array.from(this.processes.keys());
526
526
  const killed = [];
527
+ const includeProtected = opts.includeProtected === true;
527
528
  for (const pid of pids) {
528
529
  const p = this.processes.get(pid);
529
- if (p && !p.protected && this.kill(pid, opts)) killed.push(pid);
530
+ if (!p) continue;
531
+ if (p.protected && !includeProtected) continue;
532
+ if (opts.preserveBackground && p.background) continue;
533
+ if (this.kill(pid, opts)) killed.push(pid);
530
534
  }
531
535
  return killed;
532
536
  }