@wrongstack/tools 0.300.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.
- package/dist/batch-tool-use.js +3 -1
- package/dist/browser/index.js +1 -1
- package/dist/builtin.d.ts +3 -2
- package/dist/builtin.js +310 -83
- package/dist/codebase-index/bm25.d.ts +7 -1
- package/dist/codebase-index/index.js +23 -13
- package/dist/codebase-index/project-server.js +23 -13
- package/dist/codebase-index/worker.js +20 -12
- package/dist/git.js +2 -5
- package/dist/glob.js +2 -2
- package/dist/grep.js +118 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +422 -90
- package/dist/json.js +81 -0
- package/dist/logs.js +81 -0
- package/dist/next-steps-tool.d.ts +26 -0
- package/dist/pack.js +310 -83
- package/dist/patch.js +206 -45
- package/dist/read.js +23 -13
- package/dist/replace.js +81 -0
- package/dist/skill.js +51 -2
- package/dist/tool-help.js +2 -2
- package/dist/tool-search.js +1 -1
- package/dist/tool-tier.d.ts +1 -1
- package/dist/tool-tier.js +311 -100
- package/dist/tool-use.js +1 -1
- package/dist/tree.js +13 -3
- package/package.json +3 -3
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-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
116
|
-
const
|
|
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
|
-
|
|
119
|
-
|
|
180
|
+
touched.push(abs);
|
|
181
|
+
ctx.recordRead?.(abs, stat2.mtimeMs, "write", sha256hex(after));
|
|
120
182
|
ctx.session?.recordFileChange?.({
|
|
121
|
-
path:
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
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(
|
|
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(
|
|
189
|
-
|
|
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
|
}
|
package/dist/read.js
CHANGED
|
@@ -992,16 +992,23 @@ function looksBinary(content) {
|
|
|
992
992
|
}
|
|
993
993
|
return bad / sample.length > 0.1;
|
|
994
994
|
}
|
|
995
|
-
function
|
|
996
|
-
|
|
997
|
-
let
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
995
|
+
function newlineOffsets2(content) {
|
|
996
|
+
const offsets = [];
|
|
997
|
+
for (let i = 0; i < content.length; i++) {
|
|
998
|
+
if (content.charCodeAt(i) === 10) offsets.push(i);
|
|
999
|
+
}
|
|
1000
|
+
return offsets;
|
|
1001
|
+
}
|
|
1002
|
+
function lineColAt(offsets, index) {
|
|
1003
|
+
let low = 0;
|
|
1004
|
+
let high = offsets.length;
|
|
1005
|
+
while (low < high) {
|
|
1006
|
+
const mid = low + high >>> 1;
|
|
1007
|
+
if ((offsets[mid] ?? 0) < index) low = mid + 1;
|
|
1008
|
+
else high = mid;
|
|
1003
1009
|
}
|
|
1004
|
-
|
|
1010
|
+
const lastNl = low > 0 ? offsets[low - 1] : -1;
|
|
1011
|
+
return { line: low + 1, col: index - lastNl };
|
|
1005
1012
|
}
|
|
1006
1013
|
function parseGeneric(opts) {
|
|
1007
1014
|
const { file, lang } = opts;
|
|
@@ -1014,6 +1021,7 @@ function parseGeneric(opts) {
|
|
|
1014
1021
|
const patterns = patternsFor(lang);
|
|
1015
1022
|
const symbols = [];
|
|
1016
1023
|
const seen = /* @__PURE__ */ new Set();
|
|
1024
|
+
const nlOffsets = newlineOffsets2(content);
|
|
1017
1025
|
for (const pattern of patterns) {
|
|
1018
1026
|
const re = new RegExp(pattern.re.source, pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`);
|
|
1019
1027
|
re.lastIndex = 0;
|
|
@@ -1027,7 +1035,7 @@ function parseGeneric(opts) {
|
|
|
1027
1035
|
if (!/^[A-Za-z_#.@/\w][\w.\-:/#!?]*$/.test(name) && lang !== "md" && lang !== "toml") {
|
|
1028
1036
|
continue;
|
|
1029
1037
|
}
|
|
1030
|
-
const { line, col } = lineColAt(
|
|
1038
|
+
const { line, col } = lineColAt(nlOffsets, match.index ?? 0);
|
|
1031
1039
|
const key = `${name}\0${line}\0${pattern.kind}`;
|
|
1032
1040
|
if (seen.has(key)) continue;
|
|
1033
1041
|
seen.add(key);
|
|
@@ -3128,9 +3136,9 @@ import * as path11 from "node:path";
|
|
|
3128
3136
|
// src/codebase-index/bm25.ts
|
|
3129
3137
|
var K1 = 1.5;
|
|
3130
3138
|
var B = 0.75;
|
|
3139
|
+
var TOKENISE_RE = new RegExp("[^\\p{L}\\p{N}$']", "gu");
|
|
3131
3140
|
function tokenise(text) {
|
|
3132
|
-
|
|
3133
|
-
return sanitised.toLowerCase().split(" ").filter(Boolean);
|
|
3141
|
+
return text.replace(TOKENISE_RE, " ").toLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
3134
3142
|
}
|
|
3135
3143
|
function splitName(name) {
|
|
3136
3144
|
return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([\p{L}])(\d)/gu, "$1 $2").replace(/(\d)([\p{L}])/gu, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
@@ -5878,7 +5886,9 @@ var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
|
|
|
5878
5886
|
var PROJECT_INDEX_SERVER_SOCKET_DIR = `wsci-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`;
|
|
5879
5887
|
var buildIdCache;
|
|
5880
5888
|
function projectIndexServerBuildId(entrypoint) {
|
|
5881
|
-
const
|
|
5889
|
+
const href = entrypoint instanceof URL ? entrypoint.href : entrypoint;
|
|
5890
|
+
const cleanHref = href.split(/[?#]/, 1)[0] ?? href;
|
|
5891
|
+
const file = cleanHref.startsWith("file:") ? fileURLToPath(cleanHref) : path13.resolve(cleanHref);
|
|
5882
5892
|
try {
|
|
5883
5893
|
const stat3 = fs9.statSync(file);
|
|
5884
5894
|
if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat3.mtimeMs && buildIdCache.size === stat3.size) {
|
package/dist/replace.js
CHANGED
|
@@ -27,6 +27,81 @@ var DANGEROUS_PATTERNS = [
|
|
|
27
27
|
// Greedy quantifier inside lookahead/lookbehind — (?!.*a+)
|
|
28
28
|
/[([][^)\]]*[+*][^)\]]*[)\]][^)]*\?\??/
|
|
29
29
|
];
|
|
30
|
+
function hasAmbiguousQuantifiedAlternation(pattern) {
|
|
31
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
32
|
+
if (pattern[i] !== "(") continue;
|
|
33
|
+
if (i > 0 && pattern[i - 1] === "\\") continue;
|
|
34
|
+
let depth = 0;
|
|
35
|
+
let inClass = false;
|
|
36
|
+
let j = i;
|
|
37
|
+
for (; j < pattern.length; j++) {
|
|
38
|
+
const ch = pattern[j];
|
|
39
|
+
if (ch === "\\") {
|
|
40
|
+
j++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (inClass) {
|
|
44
|
+
if (ch === "]") inClass = false;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (ch === "[") {
|
|
48
|
+
inClass = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (ch === "(") depth++;
|
|
52
|
+
else if (ch === ")") {
|
|
53
|
+
depth--;
|
|
54
|
+
if (depth === 0) break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (j >= pattern.length) return false;
|
|
58
|
+
const next = pattern[j + 1];
|
|
59
|
+
if (next !== "+" && next !== "*" && next !== "{") continue;
|
|
60
|
+
let inner = pattern.slice(i + 1, j);
|
|
61
|
+
inner = inner.replace(/^\?(?::|<?[=!])/u, "");
|
|
62
|
+
const branches = [];
|
|
63
|
+
let current = "";
|
|
64
|
+
let d = 0;
|
|
65
|
+
let cls = false;
|
|
66
|
+
for (let k = 0; k < inner.length; k++) {
|
|
67
|
+
const ch = inner[k];
|
|
68
|
+
if (ch === "\\") {
|
|
69
|
+
current += ch + (inner[k + 1] ?? "");
|
|
70
|
+
k++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (cls) {
|
|
74
|
+
if (ch === "]") cls = false;
|
|
75
|
+
current += ch;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (ch === "[") {
|
|
79
|
+
cls = true;
|
|
80
|
+
current += ch;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (ch === "(") d++;
|
|
84
|
+
if (ch === ")") d--;
|
|
85
|
+
if (ch === "|" && d === 0) {
|
|
86
|
+
branches.push(current);
|
|
87
|
+
current = "";
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
current += ch;
|
|
91
|
+
}
|
|
92
|
+
branches.push(current);
|
|
93
|
+
if (branches.length < 2) continue;
|
|
94
|
+
for (let a = 0; a < branches.length; a++) {
|
|
95
|
+
for (let b = a + 1; b < branches.length; b++) {
|
|
96
|
+
const x = branches[a];
|
|
97
|
+
const y = branches[b];
|
|
98
|
+
if (x === "" || y === "") return true;
|
|
99
|
+
if (x === y || x.startsWith(y) || y.startsWith(x)) return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
30
105
|
function compileUserRegex(pattern, flags) {
|
|
31
106
|
if (typeof pattern !== "string") {
|
|
32
107
|
return { ok: false, reason: "pattern must be a string" };
|
|
@@ -45,6 +120,12 @@ function compileUserRegex(pattern, flags) {
|
|
|
45
120
|
};
|
|
46
121
|
}
|
|
47
122
|
}
|
|
123
|
+
if (hasAmbiguousQuantifiedAlternation(pattern)) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
reason: "pattern quantifies an alternation with overlapping branches \u2014 rewrite so no two branches can match the same text"
|
|
127
|
+
};
|
|
128
|
+
}
|
|
48
129
|
try {
|
|
49
130
|
return { ok: true, regex: new RegExp(pattern, flags) };
|
|
50
131
|
} catch (err) {
|
package/dist/skill.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// src/skill.ts
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
missingRequiredRuntimeTools,
|
|
6
|
+
missingRuntimeCapabilities,
|
|
7
|
+
runtimeToolReferencesFromText
|
|
8
|
+
} from "@wrongstack/core/agent-catalog";
|
|
4
9
|
import { SKILL_LIMITS, stripFrontmatter } from "@wrongstack/core/skills";
|
|
5
10
|
import { ToolValidationError } from "@wrongstack/core/types";
|
|
6
11
|
var MAX_BODY_CHARS = SKILL_LIMITS.MAX_SKILL_BODY_CHARS;
|
|
@@ -44,12 +49,37 @@ function makeSkillTool(skillLoader) {
|
|
|
44
49
|
field: "name"
|
|
45
50
|
});
|
|
46
51
|
}
|
|
52
|
+
const availableToolNames = (ctx?.catalogTools ?? ctx?.tools ?? []).map((tool) => tool.name);
|
|
53
|
+
const missingCapabilities = missingRuntimeCapabilities(
|
|
54
|
+
manifest.requiredCapabilities,
|
|
55
|
+
availableToolNames
|
|
56
|
+
);
|
|
57
|
+
const missingTools = missingRequiredRuntimeTools(manifest.requiredTools, availableToolNames);
|
|
58
|
+
if (missingCapabilities.length > 0 || missingTools.length > 0) {
|
|
59
|
+
throw new ToolValidationError({
|
|
60
|
+
message: `skill "${name}" is unavailable in this runtime; ` + [
|
|
61
|
+
missingCapabilities.length > 0 ? `missing capabilities: ${missingCapabilities.join(", ")}` : "",
|
|
62
|
+
missingTools.length > 0 ? `missing tools: ${missingTools.join(", ")}` : ""
|
|
63
|
+
].filter(Boolean).join("; "),
|
|
64
|
+
field: "name"
|
|
65
|
+
});
|
|
66
|
+
}
|
|
47
67
|
const dir = path.dirname(manifest.path);
|
|
48
68
|
let loadedResource;
|
|
49
69
|
if (input.resource?.trim()) {
|
|
50
70
|
loadedResource = await loadResource(dir, input.resource.trim());
|
|
51
71
|
}
|
|
52
72
|
const raw = await skillLoader.readBody(name);
|
|
73
|
+
const missingBodyTools = missingRequiredRuntimeTools(
|
|
74
|
+
runtimeToolReferencesFromText(raw),
|
|
75
|
+
availableToolNames
|
|
76
|
+
);
|
|
77
|
+
if (missingBodyTools.length > 0) {
|
|
78
|
+
throw new ToolValidationError({
|
|
79
|
+
message: `skill "${name}" references unregistered tools: ${missingBodyTools.join(", ")}`,
|
|
80
|
+
field: "name"
|
|
81
|
+
});
|
|
82
|
+
}
|
|
53
83
|
const body = stripFrontmatter(raw).trim().slice(0, MAX_BODY_CHARS);
|
|
54
84
|
const resources = loadedResource ? [] : await listResources(dir);
|
|
55
85
|
try {
|
|
@@ -108,9 +138,26 @@ async function loadResource(skillDir, rel) {
|
|
|
108
138
|
field: "resource"
|
|
109
139
|
});
|
|
110
140
|
}
|
|
141
|
+
let realPath;
|
|
142
|
+
let realRoot;
|
|
143
|
+
try {
|
|
144
|
+
realRoot = await fs.realpath(root);
|
|
145
|
+
realPath = await fs.realpath(absPath);
|
|
146
|
+
} catch {
|
|
147
|
+
throw new ToolValidationError({
|
|
148
|
+
message: `skill: resource "${rel}" not readable`,
|
|
149
|
+
field: "resource"
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (realPath !== realRoot && !realPath.startsWith(realRoot + path.sep)) {
|
|
153
|
+
throw new ToolValidationError({
|
|
154
|
+
message: `skill: resource "${rel}" resolves outside the skill directory`,
|
|
155
|
+
field: "resource"
|
|
156
|
+
});
|
|
157
|
+
}
|
|
111
158
|
let buf;
|
|
112
159
|
try {
|
|
113
|
-
buf = await fs.readFile(
|
|
160
|
+
buf = await fs.readFile(realPath);
|
|
114
161
|
} catch {
|
|
115
162
|
throw new ToolValidationError({
|
|
116
163
|
message: `skill: resource "${rel}" not readable`,
|
|
@@ -121,7 +168,9 @@ async function loadResource(skillDir, rel) {
|
|
|
121
168
|
const truncated = raw.length > MAX_RESOURCE_CHARS;
|
|
122
169
|
return {
|
|
123
170
|
rel: norm,
|
|
124
|
-
|
|
171
|
+
// The canonical path — the one actually opened, and the one a follow-up
|
|
172
|
+
// `bash` invocation should use.
|
|
173
|
+
absPath: realPath,
|
|
125
174
|
content: truncated ? raw.slice(0, MAX_RESOURCE_CHARS) : raw,
|
|
126
175
|
bytes: buf.length,
|
|
127
176
|
truncated
|
package/dist/tool-help.js
CHANGED
|
@@ -31,7 +31,7 @@ var toolHelpTool = {
|
|
|
31
31
|
const format = input.format ?? "short";
|
|
32
32
|
const includeExamples = input.include_examples ?? false;
|
|
33
33
|
if (input.tool) {
|
|
34
|
-
const tool = ctx.tools.find((t) => t.name === input.tool);
|
|
34
|
+
const tool = (ctx.catalogTools ?? ctx.tools).find((t) => t.name === input.tool);
|
|
35
35
|
if (!tool) {
|
|
36
36
|
return {
|
|
37
37
|
tool: input.tool,
|
|
@@ -56,7 +56,7 @@ var toolHelpTool = {
|
|
|
56
56
|
total: 1
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
|
-
const allTools = ctx.tools.map((t) => ({
|
|
59
|
+
const allTools = (ctx.catalogTools ?? ctx.tools).map((t) => ({
|
|
60
60
|
name: t.name,
|
|
61
61
|
description: t.description,
|
|
62
62
|
usageHint: t.usageHint ?? "",
|
package/dist/tool-search.js
CHANGED
|
@@ -40,7 +40,7 @@ var toolSearchTool = {
|
|
|
40
40
|
},
|
|
41
41
|
async execute(input, ctx) {
|
|
42
42
|
const limit = Math.min(input.limit ?? 20, 100);
|
|
43
|
-
const tools = ctx.tools;
|
|
43
|
+
const tools = ctx.catalogTools ?? ctx.tools;
|
|
44
44
|
const query = input.query?.toLowerCase() ?? "";
|
|
45
45
|
const filtered = tools.filter((t) => {
|
|
46
46
|
if (query && !t.name.toLowerCase().includes(query) && !t.description.toLowerCase().includes(query)) {
|
package/dist/tool-tier.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ConcreteTokenSavingTier, Tool } from '@wrongstack/core/types';
|
|
2
1
|
import type { ToolRegistry } from '@wrongstack/core/registry';
|
|
2
|
+
import type { ConcreteTokenSavingTier, Tool } from '@wrongstack/core/types';
|
|
3
3
|
/**
|
|
4
4
|
* Select built-in tools for a concrete token-saving tier while preserving the
|
|
5
5
|
* order and instances supplied by the caller.
|