@wrongstack/tools 0.68.0 → 0.77.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/{codebase-stats-tool-C8ApERbn.d.ts → background-indexer-C70RD7LU.d.ts} +81 -1
- package/dist/builtin.js +216 -56
- package/dist/builtin.js.map +1 -1
- package/dist/codebase-index/index.d.ts +35 -4
- package/dist/codebase-index/index.js +267 -13
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +274 -15
- package/dist/index.js.map +1 -1
- package/dist/pack.js +216 -56
- package/dist/pack.js.map +1 -1
- package/dist/todo.js +2 -1
- package/dist/todo.js.map +1 -1
- package/dist/tool-search.js +5 -1
- package/dist/tool-search.js.map +1 -1
- package/package.json +2 -2
package/dist/builtin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn, execFileSync, spawnSync } from 'node:child_process';
|
|
2
2
|
import * as Core from '@wrongstack/core';
|
|
3
3
|
import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, loadPlan, emptyPlan, clearPlan, savePlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, formatPlan, resolveWstackPaths } from '@wrongstack/core';
|
|
4
|
-
import * as
|
|
4
|
+
import * as fs12 from 'node:fs/promises';
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import { resolve, sep, dirname } from 'node:path';
|
|
7
7
|
import * as os from 'node:os';
|
|
@@ -127,12 +127,12 @@ function safeResolve(input, ctx) {
|
|
|
127
127
|
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
128
128
|
}
|
|
129
129
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
130
|
-
const realRoot = await
|
|
130
|
+
const realRoot = await fs12.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));
|
|
131
131
|
let probe = absPath;
|
|
132
132
|
for (; ; ) {
|
|
133
133
|
let real;
|
|
134
134
|
try {
|
|
135
|
-
real = await
|
|
135
|
+
real = await fs12.realpath(probe);
|
|
136
136
|
} catch (err) {
|
|
137
137
|
if (err.code === "ENOENT") {
|
|
138
138
|
const parent = path.dirname(probe);
|
|
@@ -1296,6 +1296,17 @@ var IndexStore = class {
|
|
|
1296
1296
|
({ id, text }) => ({ id, text })
|
|
1297
1297
|
);
|
|
1298
1298
|
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Largest symbol id currently in the table (0 when empty). New ids must be
|
|
1301
|
+
* allocated from this, NOT from `COUNT(*)`: incremental reindexes delete a
|
|
1302
|
+
* changed file's rows, so the row count drops below the max id and a
|
|
1303
|
+
* count-based id would collide with a surviving row (UNIQUE constraint on
|
|
1304
|
+
* `symbols.id`). Ids may have gaps — that is fine.
|
|
1305
|
+
*/
|
|
1306
|
+
getMaxSymbolId() {
|
|
1307
|
+
const rows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
|
|
1308
|
+
return rows[0]?.m ?? 0;
|
|
1309
|
+
}
|
|
1299
1310
|
// ─── Stats ───────────────────────────────────────────────────────────────────
|
|
1300
1311
|
getStats() {
|
|
1301
1312
|
const sizeBytes = this.sizeBytes();
|
|
@@ -2614,8 +2625,92 @@ function makeSymbol2(opts) {
|
|
|
2614
2625
|
text: `${opts.name} ${opts.signature}`.trim()
|
|
2615
2626
|
};
|
|
2616
2627
|
}
|
|
2628
|
+
function globBody(glob) {
|
|
2629
|
+
return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
|
|
2630
|
+
}
|
|
2631
|
+
function compileGitignore(lines) {
|
|
2632
|
+
const rules = [];
|
|
2633
|
+
for (const raw of lines) {
|
|
2634
|
+
let line = raw.replace(/\r$/, "");
|
|
2635
|
+
if (!line.trim() || line.trimStart().startsWith("#")) continue;
|
|
2636
|
+
line = line.trim();
|
|
2637
|
+
let negated = false;
|
|
2638
|
+
if (line.startsWith("!")) {
|
|
2639
|
+
negated = true;
|
|
2640
|
+
line = line.slice(1);
|
|
2641
|
+
}
|
|
2642
|
+
let dirOnly = false;
|
|
2643
|
+
if (line.endsWith("/")) {
|
|
2644
|
+
dirOnly = true;
|
|
2645
|
+
line = line.slice(0, -1);
|
|
2646
|
+
}
|
|
2647
|
+
if (!line) continue;
|
|
2648
|
+
const anchored = line.startsWith("/") || line.includes("/");
|
|
2649
|
+
if (line.startsWith("/")) line = line.slice(1);
|
|
2650
|
+
const body = globBody(line);
|
|
2651
|
+
const prefix = anchored ? "^" : "(?:^|.*/)";
|
|
2652
|
+
rules.push({
|
|
2653
|
+
eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),
|
|
2654
|
+
under: new RegExp(`${prefix}${body}/.*$`),
|
|
2655
|
+
negated,
|
|
2656
|
+
dirOnly
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
return (relPath, isDir) => {
|
|
2660
|
+
const p = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
2661
|
+
let ignored = false;
|
|
2662
|
+
for (const r of rules) {
|
|
2663
|
+
const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;
|
|
2664
|
+
if (re.test(p)) ignored = !r.negated;
|
|
2665
|
+
}
|
|
2666
|
+
return ignored;
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
async function loadGitignoreMatcher(projectRoot) {
|
|
2670
|
+
let lines = [];
|
|
2671
|
+
try {
|
|
2672
|
+
const raw = await fs12.readFile(path.join(projectRoot, ".gitignore"), "utf8");
|
|
2673
|
+
lines = raw.split("\n");
|
|
2674
|
+
} catch {
|
|
2675
|
+
}
|
|
2676
|
+
return compileGitignore(lines);
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
// src/codebase-index/background-indexer.ts
|
|
2680
|
+
var _ready = false;
|
|
2681
|
+
var _indexing = false;
|
|
2682
|
+
var _currentFile = 0;
|
|
2683
|
+
var _totalFiles = 0;
|
|
2684
|
+
var _lastError = null;
|
|
2685
|
+
function setIndexReady() {
|
|
2686
|
+
_ready = true;
|
|
2687
|
+
}
|
|
2688
|
+
function getIndexState() {
|
|
2689
|
+
return {
|
|
2690
|
+
ready: _ready,
|
|
2691
|
+
indexing: _indexing,
|
|
2692
|
+
currentFile: _currentFile,
|
|
2693
|
+
totalFiles: _totalFiles,
|
|
2694
|
+
lastError: _lastError
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2697
|
+
var _listeners = [];
|
|
2698
|
+
function emitState() {
|
|
2699
|
+
const state = getIndexState();
|
|
2700
|
+
for (const l of _listeners) l(state);
|
|
2701
|
+
}
|
|
2702
|
+
function _setIndexProgress(current, total) {
|
|
2703
|
+
_currentFile = current;
|
|
2704
|
+
_totalFiles = total;
|
|
2705
|
+
emitState();
|
|
2706
|
+
}
|
|
2707
|
+
Promise.resolve();
|
|
2617
2708
|
|
|
2618
2709
|
// src/codebase-index/indexer.ts
|
|
2710
|
+
var YIELD_EVERY_N = 50;
|
|
2711
|
+
function yieldEventLoop() {
|
|
2712
|
+
return new Promise((resolve7) => setImmediate(resolve7));
|
|
2713
|
+
}
|
|
2619
2714
|
var DEFAULT_IGNORE = [
|
|
2620
2715
|
"node_modules",
|
|
2621
2716
|
".git",
|
|
@@ -2627,7 +2722,7 @@ var DEFAULT_IGNORE = [
|
|
|
2627
2722
|
"__snapshots__",
|
|
2628
2723
|
".nyc_output"
|
|
2629
2724
|
];
|
|
2630
|
-
async function findSourceFiles(projectRoot, ignore) {
|
|
2725
|
+
async function findSourceFiles(projectRoot, ignore, isGitIgnored) {
|
|
2631
2726
|
const results = [];
|
|
2632
2727
|
const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
|
|
2633
2728
|
const globs = [
|
|
@@ -2645,17 +2740,19 @@ async function findSourceFiles(projectRoot, ignore) {
|
|
|
2645
2740
|
const walk = async (dir) => {
|
|
2646
2741
|
let entries;
|
|
2647
2742
|
try {
|
|
2648
|
-
entries = await
|
|
2743
|
+
entries = await fs12.readdir(dir, { withFileTypes: true });
|
|
2649
2744
|
} catch {
|
|
2650
2745
|
return;
|
|
2651
2746
|
}
|
|
2652
2747
|
for (const e of entries) {
|
|
2653
2748
|
if (ignoreSet.has(e.name)) continue;
|
|
2654
2749
|
const full = path.join(dir, e.name);
|
|
2750
|
+
const rel = path.relative(projectRoot, full).replace(/\\/g, "/");
|
|
2655
2751
|
if (e.isDirectory()) {
|
|
2752
|
+
if (isGitIgnored(rel, true)) continue;
|
|
2656
2753
|
await walk(full);
|
|
2657
2754
|
} else if (e.isFile()) {
|
|
2658
|
-
|
|
2755
|
+
if (isGitIgnored(rel, false)) continue;
|
|
2659
2756
|
const ext = path.extname(e.name);
|
|
2660
2757
|
for (const { ext: extName, pat } of globs) {
|
|
2661
2758
|
if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
|
|
@@ -2698,11 +2795,12 @@ async function runIndexer(_ctx, opts) {
|
|
|
2698
2795
|
const langStats = {};
|
|
2699
2796
|
let filesIndexed = 0;
|
|
2700
2797
|
let symbolsIndexed = 0;
|
|
2798
|
+
const isGitIgnored = await loadGitignoreMatcher(projectRoot);
|
|
2701
2799
|
let files;
|
|
2702
2800
|
if (opts.files && opts.files.length > 0) {
|
|
2703
|
-
files = opts.files.map((f) => path.resolve(projectRoot, f));
|
|
2801
|
+
files = opts.files.map((f) => path.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path.relative(projectRoot, f).replace(/\\/g, "/"), false));
|
|
2704
2802
|
} else {
|
|
2705
|
-
files = await findSourceFiles(projectRoot, ignore);
|
|
2803
|
+
files = await findSourceFiles(projectRoot, ignore, isGitIgnored);
|
|
2706
2804
|
}
|
|
2707
2805
|
if (langs && langs.length > 0) {
|
|
2708
2806
|
const langSet = new Set(langs);
|
|
@@ -2716,10 +2814,15 @@ async function runIndexer(_ctx, opts) {
|
|
|
2716
2814
|
if (!force) {
|
|
2717
2815
|
for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
|
|
2718
2816
|
}
|
|
2719
|
-
for (
|
|
2817
|
+
for (let fi = 0; fi < files.length; fi++) {
|
|
2818
|
+
const file = files[fi];
|
|
2819
|
+
_setIndexProgress(fi + 1, files.length);
|
|
2820
|
+
if (fi > 0 && fi % YIELD_EVERY_N === 0) {
|
|
2821
|
+
await yieldEventLoop();
|
|
2822
|
+
}
|
|
2720
2823
|
let stat10;
|
|
2721
2824
|
try {
|
|
2722
|
-
stat10 = await
|
|
2825
|
+
stat10 = await fs12.stat(file);
|
|
2723
2826
|
} catch {
|
|
2724
2827
|
store.deleteFile(file);
|
|
2725
2828
|
continue;
|
|
@@ -2734,11 +2837,11 @@ async function runIndexer(_ctx, opts) {
|
|
|
2734
2837
|
filesIndexed++;
|
|
2735
2838
|
continue;
|
|
2736
2839
|
}
|
|
2737
|
-
store.deleteSymbolsForFile(file);
|
|
2738
2840
|
store.deleteRefsForFile(file);
|
|
2841
|
+
store.deleteSymbolsForFile(file);
|
|
2739
2842
|
let content;
|
|
2740
2843
|
try {
|
|
2741
|
-
content = await
|
|
2844
|
+
content = await fs12.readFile(file, "utf8");
|
|
2742
2845
|
} catch (e) {
|
|
2743
2846
|
errors.push(`read error: ${file}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2744
2847
|
continue;
|
|
@@ -2761,7 +2864,7 @@ async function runIndexer(_ctx, opts) {
|
|
|
2761
2864
|
filesIndexed++;
|
|
2762
2865
|
continue;
|
|
2763
2866
|
}
|
|
2764
|
-
const nextId = store.
|
|
2867
|
+
const nextId = store.getMaxSymbolId() + 1;
|
|
2765
2868
|
const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
|
|
2766
2869
|
store.insertSymbols(symbolsWithIds, nextId);
|
|
2767
2870
|
const count = symbolsWithIds.length;
|
|
@@ -2788,7 +2891,7 @@ async function runIndexer(_ctx, opts) {
|
|
|
2788
2891
|
}
|
|
2789
2892
|
for (const [file_] of existingMeta) {
|
|
2790
2893
|
try {
|
|
2791
|
-
await
|
|
2894
|
+
await fs12.stat(file_);
|
|
2792
2895
|
} catch {
|
|
2793
2896
|
store.deleteFile(file_);
|
|
2794
2897
|
}
|
|
@@ -2836,6 +2939,7 @@ var codebaseIndexTool = {
|
|
|
2836
2939
|
langs: input.langs,
|
|
2837
2940
|
indexDir: codebaseIndexDirOverride(ctx)
|
|
2838
2941
|
});
|
|
2942
|
+
setIndexReady();
|
|
2839
2943
|
return result;
|
|
2840
2944
|
}
|
|
2841
2945
|
};
|
|
@@ -2971,6 +3075,31 @@ var codebaseSearchTool = {
|
|
|
2971
3075
|
required: ["query"]
|
|
2972
3076
|
},
|
|
2973
3077
|
async execute(input, ctx) {
|
|
3078
|
+
const state = getIndexState();
|
|
3079
|
+
if (!state.ready) {
|
|
3080
|
+
return {
|
|
3081
|
+
results: [],
|
|
3082
|
+
total: 0,
|
|
3083
|
+
query: input.query,
|
|
3084
|
+
indexStatus: state.indexing ? `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.` : "Index not yet built. The codebase is being indexed at startup \u2014 search will be available shortly."
|
|
3085
|
+
};
|
|
3086
|
+
}
|
|
3087
|
+
if (state.indexing) {
|
|
3088
|
+
return {
|
|
3089
|
+
results: [],
|
|
3090
|
+
total: 0,
|
|
3091
|
+
query: input.query,
|
|
3092
|
+
indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files). Results may be incomplete.`
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
if (state.lastError) {
|
|
3096
|
+
return {
|
|
3097
|
+
results: [],
|
|
3098
|
+
total: 0,
|
|
3099
|
+
query: input.query,
|
|
3100
|
+
indexStatus: `Index build failed: ${state.lastError}. Try /codebase-reindex.`
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
2974
3103
|
const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
|
|
2975
3104
|
try {
|
|
2976
3105
|
const limit = Math.min(input.limit ?? 20, 100);
|
|
@@ -3028,6 +3157,32 @@ var codebaseStatsTool = {
|
|
|
3028
3157
|
additionalProperties: false
|
|
3029
3158
|
},
|
|
3030
3159
|
async execute(_input, ctx) {
|
|
3160
|
+
const idxState = getIndexState();
|
|
3161
|
+
if (!idxState.ready) {
|
|
3162
|
+
return {
|
|
3163
|
+
totalSymbols: 0,
|
|
3164
|
+
totalFiles: 0,
|
|
3165
|
+
byLang: {},
|
|
3166
|
+
byKind: {},
|
|
3167
|
+
lastIndexed: null,
|
|
3168
|
+
sizeBytes: 0,
|
|
3169
|
+
indexPath: "",
|
|
3170
|
+
version: SCHEMA_VERSION,
|
|
3171
|
+
indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
|
|
3172
|
+
};
|
|
3173
|
+
}
|
|
3174
|
+
if (idxState.indexing) {
|
|
3175
|
+
const store2 = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
|
|
3176
|
+
try {
|
|
3177
|
+
const stats = store2.getStats();
|
|
3178
|
+
return {
|
|
3179
|
+
...stats,
|
|
3180
|
+
indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
|
|
3181
|
+
};
|
|
3182
|
+
} finally {
|
|
3183
|
+
store2.close();
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3031
3186
|
const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
|
|
3032
3187
|
try {
|
|
3033
3188
|
const stats = store.getStats();
|
|
@@ -3171,9 +3326,9 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
3171
3326
|
const results = [];
|
|
3172
3327
|
for (const file of files) {
|
|
3173
3328
|
const absPath = safeResolve(file, ctx);
|
|
3174
|
-
const stat10 = await
|
|
3329
|
+
const stat10 = await fs12.stat(absPath).catch(() => null);
|
|
3175
3330
|
if (!stat10?.isFile()) continue;
|
|
3176
|
-
const content = await
|
|
3331
|
+
const content = await fs12.readFile(absPath, "utf8");
|
|
3177
3332
|
const lines = content.split(/\r?\n/);
|
|
3178
3333
|
results.push(formatWithLineNumbers(file, lines));
|
|
3179
3334
|
}
|
|
@@ -3235,7 +3390,7 @@ var documentTool = {
|
|
|
3235
3390
|
const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
|
|
3236
3391
|
for (const absPath of fileList) {
|
|
3237
3392
|
try {
|
|
3238
|
-
const content = await
|
|
3393
|
+
const content = await fs12.readFile(absPath, "utf8");
|
|
3239
3394
|
filesProcessed++;
|
|
3240
3395
|
const processed = processFile(
|
|
3241
3396
|
content,
|
|
@@ -3271,7 +3426,7 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
3271
3426
|
for (const f of files) {
|
|
3272
3427
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
3273
3428
|
try {
|
|
3274
|
-
const stat10 = await
|
|
3429
|
+
const stat10 = await fs12.stat(absPath);
|
|
3275
3430
|
if (stat10.isFile()) resolved.push(absPath);
|
|
3276
3431
|
} catch {
|
|
3277
3432
|
}
|
|
@@ -3363,7 +3518,7 @@ var editTool = {
|
|
|
3363
3518
|
if (input.new_string === void 0) throw new Error("edit: new_string is required");
|
|
3364
3519
|
if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
|
|
3365
3520
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
3366
|
-
const stat10 = await
|
|
3521
|
+
const stat10 = await fs12.stat(absPath).catch((err) => {
|
|
3367
3522
|
if (err.code === "ENOENT") {
|
|
3368
3523
|
throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
|
|
3369
3524
|
}
|
|
@@ -3373,8 +3528,8 @@ var editTool = {
|
|
|
3373
3528
|
if (!ctx.hasRead(absPath)) {
|
|
3374
3529
|
throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
|
|
3375
3530
|
}
|
|
3376
|
-
const original = await
|
|
3377
|
-
const updated = await
|
|
3531
|
+
const original = await fs12.readFile(absPath, "utf8");
|
|
3532
|
+
const updated = await fs12.stat(absPath);
|
|
3378
3533
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
3379
3534
|
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
3380
3535
|
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
@@ -3414,7 +3569,7 @@ var editTool = {
|
|
|
3414
3569
|
const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
|
|
3415
3570
|
const newFile = toStyle(newFileLf, style);
|
|
3416
3571
|
await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
|
|
3417
|
-
const written = await
|
|
3572
|
+
const written = await fs12.stat(absPath);
|
|
3418
3573
|
ctx.recordRead(absPath, written.mtimeMs);
|
|
3419
3574
|
ctx.session.recordFileChange({
|
|
3420
3575
|
path: absPath,
|
|
@@ -4446,7 +4601,7 @@ var globTool = {
|
|
|
4446
4601
|
}
|
|
4447
4602
|
let entries;
|
|
4448
4603
|
try {
|
|
4449
|
-
entries = await
|
|
4604
|
+
entries = await fs12.readdir(dir, { withFileTypes: true });
|
|
4450
4605
|
} catch {
|
|
4451
4606
|
return;
|
|
4452
4607
|
}
|
|
@@ -4462,7 +4617,7 @@ var globTool = {
|
|
|
4462
4617
|
} else if (e.isFile()) {
|
|
4463
4618
|
if (re.test(rel) || re.test(name)) {
|
|
4464
4619
|
try {
|
|
4465
|
-
const st = await
|
|
4620
|
+
const st = await fs12.stat(full);
|
|
4466
4621
|
results.push({ rel: full, mtime: st.mtimeMs });
|
|
4467
4622
|
if (results.length >= limit) {
|
|
4468
4623
|
truncated = true;
|
|
@@ -4481,7 +4636,7 @@ var globTool = {
|
|
|
4481
4636
|
};
|
|
4482
4637
|
async function readGitignore(dir) {
|
|
4483
4638
|
try {
|
|
4484
|
-
const raw = await
|
|
4639
|
+
const raw = await fs12.readFile(path.join(dir, ".gitignore"), "utf8");
|
|
4485
4640
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
4486
4641
|
} catch {
|
|
4487
4642
|
return [];
|
|
@@ -4763,7 +4918,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
4763
4918
|
if (stopped || signal.aborted) return;
|
|
4764
4919
|
let entries;
|
|
4765
4920
|
try {
|
|
4766
|
-
entries = await
|
|
4921
|
+
entries = await fs12.readdir(dir, { withFileTypes: true });
|
|
4767
4922
|
} catch {
|
|
4768
4923
|
return;
|
|
4769
4924
|
}
|
|
@@ -4778,9 +4933,9 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
4778
4933
|
if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
|
|
4779
4934
|
if (globRe) globRe.lastIndex = 0;
|
|
4780
4935
|
try {
|
|
4781
|
-
const stat10 = await
|
|
4936
|
+
const stat10 = await fs12.stat(full);
|
|
4782
4937
|
if (stat10.size > 1e6) continue;
|
|
4783
|
-
const head = await
|
|
4938
|
+
const head = await fs12.readFile(full);
|
|
4784
4939
|
if (isBinaryBuffer(head)) continue;
|
|
4785
4940
|
const text = head.toString("utf8");
|
|
4786
4941
|
const lines = text.split(/\r?\n/);
|
|
@@ -4959,7 +5114,7 @@ var jsonTool = {
|
|
|
4959
5114
|
let raw;
|
|
4960
5115
|
if (input.file) {
|
|
4961
5116
|
try {
|
|
4962
|
-
raw = await
|
|
5117
|
+
raw = await fs12.readFile(input.file, "utf8");
|
|
4963
5118
|
} catch {
|
|
4964
5119
|
return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
|
|
4965
5120
|
}
|
|
@@ -4997,8 +5152,8 @@ var jsonTool = {
|
|
|
4997
5152
|
};
|
|
4998
5153
|
}
|
|
4999
5154
|
};
|
|
5000
|
-
function query(data,
|
|
5001
|
-
const parts =
|
|
5155
|
+
function query(data, path19) {
|
|
5156
|
+
const parts = path19.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
5002
5157
|
let current = data;
|
|
5003
5158
|
for (const part of parts) {
|
|
5004
5159
|
if (current === null || current === void 0) return void 0;
|
|
@@ -5267,7 +5422,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
5267
5422
|
}
|
|
5268
5423
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
5269
5424
|
var MAX_TAIL_LINES = 1e5;
|
|
5270
|
-
async function fileLogs(
|
|
5425
|
+
async function fileLogs(path19, lines, filterRe, stream) {
|
|
5271
5426
|
const { createInterface } = await import('node:readline');
|
|
5272
5427
|
const { createReadStream } = await import('node:fs');
|
|
5273
5428
|
const entries = [];
|
|
@@ -5276,7 +5431,7 @@ async function fileLogs(path18, lines, filterRe, stream) {
|
|
|
5276
5431
|
let writeIdx = 0;
|
|
5277
5432
|
let totalLines = 0;
|
|
5278
5433
|
const rl = createInterface({
|
|
5279
|
-
input: createReadStream(
|
|
5434
|
+
input: createReadStream(path19),
|
|
5280
5435
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
5281
5436
|
});
|
|
5282
5437
|
for await (const line of rl) {
|
|
@@ -5297,7 +5452,7 @@ async function fileLogs(path18, lines, filterRe, stream) {
|
|
|
5297
5452
|
if (parsed) entries.push(parsed);
|
|
5298
5453
|
}
|
|
5299
5454
|
return {
|
|
5300
|
-
source:
|
|
5455
|
+
source: path19,
|
|
5301
5456
|
entries,
|
|
5302
5457
|
total: entries.length,
|
|
5303
5458
|
truncated: totalLines > effLines,
|
|
@@ -5476,12 +5631,12 @@ var patchTool = {
|
|
|
5476
5631
|
};
|
|
5477
5632
|
}
|
|
5478
5633
|
}
|
|
5479
|
-
const tmpDir = await
|
|
5634
|
+
const tmpDir = await fs12.mkdtemp(path.join(os.tmpdir(), ".wstack_patch_"));
|
|
5480
5635
|
try {
|
|
5481
|
-
await
|
|
5636
|
+
await fs12.chmod(tmpDir, 448).catch(() => {
|
|
5482
5637
|
});
|
|
5483
5638
|
const patchFile = path.join(tmpDir, "in.diff");
|
|
5484
|
-
await
|
|
5639
|
+
await fs12.writeFile(patchFile, input.patch, { mode: 384 });
|
|
5485
5640
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
5486
5641
|
const result = await runPatch(args, dir, opts.signal);
|
|
5487
5642
|
if (result.exitCode !== 0 && !dryRun) {
|
|
@@ -5502,7 +5657,7 @@ var patchTool = {
|
|
|
5502
5657
|
message: result.stdout || "patch applied"
|
|
5503
5658
|
};
|
|
5504
5659
|
} finally {
|
|
5505
|
-
await
|
|
5660
|
+
await fs12.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
5506
5661
|
});
|
|
5507
5662
|
}
|
|
5508
5663
|
}
|
|
@@ -5744,7 +5899,7 @@ var readTool = {
|
|
|
5744
5899
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
5745
5900
|
let stat10;
|
|
5746
5901
|
try {
|
|
5747
|
-
stat10 = await
|
|
5902
|
+
stat10 = await fs12.stat(absPath);
|
|
5748
5903
|
} catch (err) {
|
|
5749
5904
|
const code = err.code;
|
|
5750
5905
|
if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
|
|
@@ -5756,7 +5911,7 @@ var readTool = {
|
|
|
5756
5911
|
if (stat10.size > MAX_BYTES2) {
|
|
5757
5912
|
throw new Error(`read: file too large (${stat10.size} bytes, limit ${MAX_BYTES2})`);
|
|
5758
5913
|
}
|
|
5759
|
-
const buf = await
|
|
5914
|
+
const buf = await fs12.readFile(absPath);
|
|
5760
5915
|
if (isBinaryBuffer(buf)) {
|
|
5761
5916
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
5762
5917
|
}
|
|
@@ -5824,11 +5979,11 @@ var replaceTool = {
|
|
|
5824
5979
|
const dryRun = input.dry_run ?? false;
|
|
5825
5980
|
const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
|
|
5826
5981
|
const fileList = await resolveFiles2(filesInput, ctx, globRe);
|
|
5827
|
-
const realRoot = await
|
|
5982
|
+
const realRoot = await fs12.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
5828
5983
|
const results = [];
|
|
5829
5984
|
let totalReplacements = 0;
|
|
5830
5985
|
for (const absPath of fileList) {
|
|
5831
|
-
const lstat2 = await
|
|
5986
|
+
const lstat2 = await fs12.lstat(absPath).catch((err) => {
|
|
5832
5987
|
if (err.code === "ENOENT") return null;
|
|
5833
5988
|
throw err;
|
|
5834
5989
|
});
|
|
@@ -5836,17 +5991,17 @@ var replaceTool = {
|
|
|
5836
5991
|
if (lstat2.isSymbolicLink()) continue;
|
|
5837
5992
|
let realPath;
|
|
5838
5993
|
try {
|
|
5839
|
-
realPath = await
|
|
5994
|
+
realPath = await fs12.realpath(absPath);
|
|
5840
5995
|
} catch {
|
|
5841
5996
|
continue;
|
|
5842
5997
|
}
|
|
5843
5998
|
const rel = path.relative(realRoot, realPath);
|
|
5844
5999
|
if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
|
|
5845
|
-
const stat10 = await
|
|
6000
|
+
const stat10 = await fs12.stat(realPath).catch(() => null);
|
|
5846
6001
|
if (!stat10 || !stat10.isFile()) continue;
|
|
5847
6002
|
let content;
|
|
5848
6003
|
try {
|
|
5849
|
-
const buf = await
|
|
6004
|
+
const buf = await fs12.readFile(realPath);
|
|
5850
6005
|
if (isBinaryBuffer(buf)) continue;
|
|
5851
6006
|
content = buf.toString("utf8");
|
|
5852
6007
|
} catch {
|
|
@@ -5898,7 +6053,7 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
5898
6053
|
const resolved = [];
|
|
5899
6054
|
for (const p of parts) {
|
|
5900
6055
|
const absPath = safeResolve(p, ctx);
|
|
5901
|
-
const stat10 = await
|
|
6056
|
+
const stat10 = await fs12.stat(absPath).catch(() => null);
|
|
5902
6057
|
if (stat10?.isFile()) {
|
|
5903
6058
|
resolved.push(absPath);
|
|
5904
6059
|
}
|
|
@@ -5949,7 +6104,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
5949
6104
|
const walk = async (dir) => {
|
|
5950
6105
|
let entries;
|
|
5951
6106
|
try {
|
|
5952
|
-
entries = await
|
|
6107
|
+
entries = await fs12.readdir(dir, { withFileTypes: true });
|
|
5953
6108
|
} catch {
|
|
5954
6109
|
return;
|
|
5955
6110
|
}
|
|
@@ -5957,7 +6112,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
5957
6112
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
5958
6113
|
const full = path.join(dir, e.name);
|
|
5959
6114
|
try {
|
|
5960
|
-
const stat10 = await
|
|
6115
|
+
const stat10 = await fs12.lstat(full);
|
|
5961
6116
|
if (stat10.isSymbolicLink()) continue;
|
|
5962
6117
|
} catch {
|
|
5963
6118
|
continue;
|
|
@@ -6134,7 +6289,7 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
6134
6289
|
}
|
|
6135
6290
|
const fullPath = target;
|
|
6136
6291
|
if (!dryRun) {
|
|
6137
|
-
await
|
|
6292
|
+
await fs12.mkdir(path.dirname(fullPath), { recursive: true });
|
|
6138
6293
|
await atomicWrite(fullPath, substituteVars(content, name, vars));
|
|
6139
6294
|
}
|
|
6140
6295
|
files.push(resolvedPath);
|
|
@@ -6525,9 +6680,10 @@ var todoTool = {
|
|
|
6525
6680
|
name: "todo",
|
|
6526
6681
|
category: "Session",
|
|
6527
6682
|
description: "Manage the session-level todo list. This is the primary mechanism for tracking multi-step work. The list is fully replaced on every call (not appended).",
|
|
6528
|
-
usageHint: "BEST PRACTICE for complex tasks:\n- At the beginning of a non-trivial task, create a clear todo list with specific, actionable items.\n- Only **one** item should be `in_progress` at any time.\n- Update the list frequently as work progresses (mark items done, add new ones, change status).\n- The system and user can see this list, so keep it honest and up-to-date.\nThis tool is extremely valuable for maintaining focus and giving the user visibility into your plan.",
|
|
6683
|
+
usageHint: "BEST PRACTICE for complex tasks:\n- At the beginning of a non-trivial task, create a clear todo list with specific, actionable items.\n- Only **one** item should be `in_progress` at any time.\n- Update the list frequently as work progresses (mark items done, add new ones, change status).\n- **Re-order items** to reflect current priorities \u2014 the full list is replaced each call, so item order is entirely under your control.\n- When all items are completed the board auto-clears \u2014 you do NOT need to send an empty list.\n- The system and user can see this list, so keep it honest and up-to-date.\nThis tool is extremely valuable for maintaining focus and giving the user visibility into your plan.",
|
|
6529
6684
|
permission: "auto",
|
|
6530
6685
|
mutating: false,
|
|
6686
|
+
// mutates only conversation state (ctx.todos), not external state — no confirmation needed
|
|
6531
6687
|
timeoutMs: 1e3,
|
|
6532
6688
|
inputSchema: {
|
|
6533
6689
|
type: "object",
|
|
@@ -6773,10 +6929,14 @@ var toolSearchTool = {
|
|
|
6773
6929
|
permission: t.permission,
|
|
6774
6930
|
mutating: t.mutating
|
|
6775
6931
|
}));
|
|
6932
|
+
const totalAvailable = tools.length;
|
|
6933
|
+
const hint = results.length === 0 && query2 ? `No tools matched "${input.query}". Use tool-help (without arguments) to see all ${totalAvailable} available tools.` : void 0;
|
|
6776
6934
|
return {
|
|
6777
6935
|
tools: results,
|
|
6778
6936
|
total: filtered.length,
|
|
6779
|
-
truncated: filtered.length > limit
|
|
6937
|
+
truncated: filtered.length > limit,
|
|
6938
|
+
...hint ? { hint } : {},
|
|
6939
|
+
_available: totalAvailable
|
|
6780
6940
|
};
|
|
6781
6941
|
}
|
|
6782
6942
|
};
|
|
@@ -6986,7 +7146,7 @@ var treeTool = {
|
|
|
6986
7146
|
}
|
|
6987
7147
|
};
|
|
6988
7148
|
async function walkDir(dir, depth, opts) {
|
|
6989
|
-
const entries = await
|
|
7149
|
+
const entries = await fs12.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
6990
7150
|
const filtered = entries.filter((e) => {
|
|
6991
7151
|
if (!opts.showHidden && e.name.startsWith(".")) return false;
|
|
6992
7152
|
if (opts.exclude.has(e.name)) return false;
|
|
@@ -7134,14 +7294,14 @@ var writeTool = {
|
|
|
7134
7294
|
let existed = false;
|
|
7135
7295
|
let prev = "";
|
|
7136
7296
|
try {
|
|
7137
|
-
const stat11 = await
|
|
7297
|
+
const stat11 = await fs12.stat(absPath);
|
|
7138
7298
|
existed = stat11.isFile();
|
|
7139
7299
|
if (existed) {
|
|
7140
7300
|
if (!ctx.hasRead(absPath)) {
|
|
7141
|
-
prev = await
|
|
7301
|
+
prev = await fs12.readFile(absPath, "utf8");
|
|
7142
7302
|
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
7143
7303
|
} else {
|
|
7144
|
-
prev = await
|
|
7304
|
+
prev = await fs12.readFile(absPath, "utf8");
|
|
7145
7305
|
}
|
|
7146
7306
|
}
|
|
7147
7307
|
} catch (err) {
|
|
@@ -7152,7 +7312,7 @@ var writeTool = {
|
|
|
7152
7312
|
await atomicWrite(absPath, input.content);
|
|
7153
7313
|
const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
7154
7314
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
7155
|
-
const stat10 = await
|
|
7315
|
+
const stat10 = await fs12.stat(absPath);
|
|
7156
7316
|
ctx.recordRead(absPath, stat10.mtimeMs);
|
|
7157
7317
|
ctx.session.recordFileChange({
|
|
7158
7318
|
path: absPath,
|