@wrongstack/tools 0.265.1 → 0.267.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/builtin.js +136 -16
- package/dist/builtin.js.map +1 -1
- package/dist/edit.d.ts +1 -0
- package/dist/edit.js +12 -7
- package/dist/edit.js.map +1 -1
- package/dist/git.d.ts +7 -0
- package/dist/git.js +19 -2
- package/dist/git.js.map +1 -1
- package/dist/index.js +141 -19
- package/dist/index.js.map +1 -1
- package/dist/outdated.js +5 -2
- package/dist/outdated.js.map +1 -1
- package/dist/pack.js +136 -16
- package/dist/pack.js.map +1 -1
- package/dist/read.d.ts +3 -0
- package/dist/read.js +103 -6
- package/dist/read.js.map +1 -1
- package/dist/tool-icons.js +2 -2
- package/dist/tool-icons.js.map +1 -1
- package/package.json +2 -2
package/dist/read.d.ts
CHANGED
|
@@ -4,12 +4,15 @@ interface ReadInput {
|
|
|
4
4
|
path: string;
|
|
5
5
|
offset?: number | undefined;
|
|
6
6
|
limit?: number | undefined;
|
|
7
|
+
mode?: 'content' | 'summary' | undefined;
|
|
7
8
|
}
|
|
8
9
|
interface ReadOutput {
|
|
9
10
|
text: string;
|
|
10
11
|
total_lines: number;
|
|
11
12
|
encoding: string;
|
|
12
13
|
truncated: boolean;
|
|
14
|
+
cached?: boolean | undefined;
|
|
15
|
+
note?: string | undefined;
|
|
13
16
|
}
|
|
14
17
|
declare const readTool: Tool<ReadInput, ReadOutput>;
|
|
15
18
|
|
package/dist/read.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fsp from 'node:fs/promises';
|
|
2
|
+
import { toErrorMessage } from '@wrongstack/core/utils';
|
|
2
3
|
import * as path from 'node:path';
|
|
3
4
|
import * as Core from '@wrongstack/core';
|
|
4
|
-
import { toErrorMessage } from '@wrongstack/core/utils';
|
|
5
5
|
|
|
6
6
|
// src/read.ts
|
|
7
7
|
function resolvePath(input, ctx) {
|
|
@@ -62,6 +62,8 @@ function isBinaryBuffer(buf) {
|
|
|
62
62
|
}
|
|
63
63
|
return false;
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
// src/read.ts
|
|
65
67
|
var MAX_BYTES = 5 * 1024 * 1024;
|
|
66
68
|
var readTool = {
|
|
67
69
|
name: "read",
|
|
@@ -88,6 +90,11 @@ var readTool = {
|
|
|
88
90
|
limit: {
|
|
89
91
|
type: "integer",
|
|
90
92
|
description: "Maximum number of lines to return (default is 2000)."
|
|
93
|
+
},
|
|
94
|
+
mode: {
|
|
95
|
+
type: "string",
|
|
96
|
+
enum: ["content", "summary"],
|
|
97
|
+
description: "Return full line-numbered content (default) or a compact file summary with imports/exports/symbols."
|
|
91
98
|
}
|
|
92
99
|
},
|
|
93
100
|
required: ["path"]
|
|
@@ -101,14 +108,27 @@ var readTool = {
|
|
|
101
108
|
} catch (err) {
|
|
102
109
|
const code = err.code;
|
|
103
110
|
if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
|
|
104
|
-
throw new Error(
|
|
105
|
-
`read: failed to stat "${input.path}": ${toErrorMessage(err)}`
|
|
106
|
-
);
|
|
111
|
+
throw new Error(`read: failed to stat "${input.path}": ${toErrorMessage(err)}`);
|
|
107
112
|
}
|
|
108
113
|
if (!stat2.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
|
|
109
114
|
if (stat2.size > MAX_BYTES) {
|
|
110
115
|
throw new Error(`read: file too large (${stat2.size} bytes, limit ${MAX_BYTES})`);
|
|
111
116
|
}
|
|
117
|
+
const offset = Math.max(1, input.offset ?? 1);
|
|
118
|
+
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
119
|
+
const prior = getReadRangeRecord(ctx, absPath);
|
|
120
|
+
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
121
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat2.mtimeMs, offset, requestedEnd)) {
|
|
122
|
+
ctx.recordRead(absPath, stat2.mtimeMs);
|
|
123
|
+
return {
|
|
124
|
+
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat2.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
|
|
125
|
+
total_lines: prior.totalLines,
|
|
126
|
+
encoding: "utf8",
|
|
127
|
+
truncated: requestedEnd < prior.totalLines,
|
|
128
|
+
cached: true,
|
|
129
|
+
note: "Repeated read suppressed to save tokens."
|
|
130
|
+
};
|
|
131
|
+
}
|
|
112
132
|
const buf = await fsp.readFile(absPath);
|
|
113
133
|
if (isBinaryBuffer(buf)) {
|
|
114
134
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
@@ -116,17 +136,38 @@ var readTool = {
|
|
|
116
136
|
const text = buf.toString("utf8");
|
|
117
137
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
118
138
|
const total = allLines.length;
|
|
119
|
-
|
|
120
|
-
|
|
139
|
+
if (input.mode === "summary") {
|
|
140
|
+
ctx.recordRead(absPath, stat2.mtimeMs);
|
|
141
|
+
rememberReadRange(ctx, absPath, stat2.mtimeMs, total, 1, Math.min(total, 200));
|
|
142
|
+
return {
|
|
143
|
+
text: summarizeFile(input.path, stat2.size, allLines),
|
|
144
|
+
total_lines: total,
|
|
145
|
+
encoding: "utf8",
|
|
146
|
+
truncated: total > 200,
|
|
147
|
+
note: "Summary mode returned compact structure instead of full file content."
|
|
148
|
+
};
|
|
149
|
+
}
|
|
121
150
|
if (limit === 0) {
|
|
122
151
|
ctx.recordRead(absPath, stat2.mtimeMs);
|
|
152
|
+
rememberReadRange(ctx, absPath, stat2.mtimeMs, total, 1, 0);
|
|
123
153
|
return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
|
|
124
154
|
}
|
|
155
|
+
if (offset > total) {
|
|
156
|
+
ctx.recordRead(absPath, stat2.mtimeMs);
|
|
157
|
+
rememberReadRange(ctx, absPath, stat2.mtimeMs, total, total + 1, total + 1);
|
|
158
|
+
return {
|
|
159
|
+
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
160
|
+
total_lines: total,
|
|
161
|
+
encoding: "utf8",
|
|
162
|
+
truncated: false
|
|
163
|
+
};
|
|
164
|
+
}
|
|
125
165
|
const slice = allLines.slice(offset - 1, offset - 1 + limit);
|
|
126
166
|
const truncated = offset - 1 + slice.length < total;
|
|
127
167
|
const width = String(offset + slice.length - 1).length;
|
|
128
168
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
129
169
|
ctx.recordRead(absPath, stat2.mtimeMs);
|
|
170
|
+
rememberReadRange(ctx, absPath, stat2.mtimeMs, total, offset, offset + slice.length - 1);
|
|
130
171
|
return {
|
|
131
172
|
text: numbered,
|
|
132
173
|
total_lines: total,
|
|
@@ -135,6 +176,62 @@ var readTool = {
|
|
|
135
176
|
};
|
|
136
177
|
}
|
|
137
178
|
};
|
|
179
|
+
var READ_RANGES_META_KEY = "tools.read.ranges.v1";
|
|
180
|
+
function getReadRanges(ctx) {
|
|
181
|
+
const existing = ctx.meta[READ_RANGES_META_KEY];
|
|
182
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
183
|
+
return existing;
|
|
184
|
+
}
|
|
185
|
+
const next = {};
|
|
186
|
+
ctx.meta[READ_RANGES_META_KEY] = next;
|
|
187
|
+
return next;
|
|
188
|
+
}
|
|
189
|
+
function getReadRangeRecord(ctx, absPath) {
|
|
190
|
+
return getReadRanges(ctx)[absPath];
|
|
191
|
+
}
|
|
192
|
+
function rememberReadRange(ctx, absPath, mtimeMs, totalLines, start, end) {
|
|
193
|
+
if (end < start) return;
|
|
194
|
+
const ranges = getReadRanges(ctx);
|
|
195
|
+
const prior = ranges[absPath];
|
|
196
|
+
const nextRanges = prior && Math.abs(prior.mtimeMs - mtimeMs) <= 1 ? prior.ranges.slice() : [];
|
|
197
|
+
nextRanges.push({ start, end });
|
|
198
|
+
ranges[absPath] = {
|
|
199
|
+
mtimeMs,
|
|
200
|
+
totalLines,
|
|
201
|
+
ranges: mergeRanges(nextRanges)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function coversRange(record, mtimeMs, start, end) {
|
|
205
|
+
if (Math.abs(record.mtimeMs - mtimeMs) > 1) return false;
|
|
206
|
+
return record.ranges.some((range) => range.start <= start && range.end >= end);
|
|
207
|
+
}
|
|
208
|
+
function mergeRanges(ranges) {
|
|
209
|
+
const sorted = ranges.slice().sort((a, b) => a.start - b.start);
|
|
210
|
+
const merged = [];
|
|
211
|
+
for (const range of sorted) {
|
|
212
|
+
const last = merged[merged.length - 1];
|
|
213
|
+
if (!last || range.start > last.end + 1) {
|
|
214
|
+
merged.push({ ...range });
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
last.end = Math.max(last.end, range.end);
|
|
218
|
+
}
|
|
219
|
+
return merged;
|
|
220
|
+
}
|
|
221
|
+
function summarizeFile(filePath, bytes, lines) {
|
|
222
|
+
const interesting = lines.map((line, index) => ({ line: line.trim(), number: index + 1 })).filter(
|
|
223
|
+
({ line }) => /^(import\s|export\s|class\s|interface\s|type\s|function\s|const\s+\w+\s*=|let\s+\w+\s*=|var\s+\w+\s*=|def\s+|async\s+function\s)/.test(
|
|
224
|
+
line
|
|
225
|
+
)
|
|
226
|
+
).slice(0, 80).map(({ line, number }) => `${number}: ${line}`);
|
|
227
|
+
return [
|
|
228
|
+
`summary: ${filePath}`,
|
|
229
|
+
`bytes=${bytes}`,
|
|
230
|
+
`total_lines=${lines.length}`,
|
|
231
|
+
interesting.length > 0 ? `symbols/imports:
|
|
232
|
+
${interesting.join("\n")}` : "symbols/imports: (none detected)"
|
|
233
|
+
].join("\n");
|
|
234
|
+
}
|
|
138
235
|
|
|
139
236
|
export { readTool };
|
|
140
237
|
//# sourceMappingURL=read.js.map
|
package/dist/read.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/_util.ts","../src/read.ts"],"names":["stat","fs"],"mappings":";;;;;;AA8BO,SAAS,WAAA,CAAY,OAAe,GAAA,EAAsB;AAC/D,EAAA,OAAY,IAAA,CAAA,UAAA,CAAW,KAAK,CAAA,GAAS,IAAA,CAAA,SAAA,CAAU,KAAK,CAAA,GAAS,IAAA,CAAA,OAAA,CAAQ,GAAA,CAAI,UAAA,IAAc,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AACvG;AAOA,SAAS,aAAa,GAAA,EAAwB;AAC5C,EAAA,OAAO,CAAM,aAAQ,GAAA,CAAI,WAAW,GAAQ,IAAA,CAAA,OAAA,CAAa,IAAA,CAAA,gBAAA,EAAkB,CAAC,CAAA;AAC9E;AAGA,SAAS,WAAA,CAAY,QAAgB,KAAA,EAA0B;AAC7D,EAAA,OAAO,KAAA,CAAM,IAAA,CAAK,CAAC,IAAA,KAAS;AAC1B,IAAA,MAAM,GAAA,GAAW,IAAA,CAAA,QAAA,CAAS,IAAA,EAAM,MAAM,CAAA;AACtC,IAAA,OAAO,GAAA,KAAQ,MAAO,CAAC,GAAA,CAAI,WAAW,IAAI,CAAA,IAAK,CAAM,IAAA,CAAA,UAAA,CAAW,GAAG,CAAA;AAAA,EACrE,CAAC,CAAA;AACH;AAEO,SAAS,gBAAA,CAAiB,SAAiB,GAAA,EAAsB;AACtE,EAAA,MAAM,MAAA,GAAc,aAAQ,OAAO,CAAA;AAEnC,EAAA,IAAI,GAAA,CAAI,yBAAyB,OAAO,MAAA;AACxC,EAAA,IAAI,YAAY,MAAA,EAAQ,YAAA,CAAa,GAAG,CAAC,GAAG,OAAO,MAAA;AACnD,EAAA,MAAM,IAAI,MAAM,CAAA,MAAA,EAAS,OAAO,8BAAmC,IAAA,CAAA,OAAA,CAAQ,GAAA,CAAI,WAAW,CAAC,CAAA,CAAA,CAAG,CAAA;AAChG;AAEO,SAAS,WAAA,CAAY,OAAe,GAAA,EAAsB;AAC/D,EAAA,OAAO,gBAAA,CAAiB,WAAA,CAAY,KAAA,EAAO,GAAG,GAAG,GAAG,CAAA;AACtD;AAgBA,eAAsB,oBAAA,CAAqB,SAAiB,GAAA,EAA6B;AAEvF,EAAA,IAAI,IAAI,uBAAA,EAAyB;AAGjC,EAAA,MAAM,SAAA,GAAY,MAAM,OAAA,CAAQ,GAAA;AAAA,IAC9B,YAAA,CAAa,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,KAAU,GAAA,CAAA,QAAA,CAAS,CAAC,CAAA,CAAE,KAAA,CAAM,MAAW,IAAA,CAAA,OAAA,CAAQ,CAAC,CAAC,CAAC;AAAA,GAC3E;AACA,EAAA,IAAI,KAAA,GAAQ,OAAA;AACZ,EAAA,WAAS;AACP,IAAA,IAAI,IAAA;AACJ,IAAA,IAAI;AACF,MAAA,IAAA,GAAO,MAAU,aAAS,KAAK,CAAA;AAAA,IACjC,SAAS,GAAA,EAAK;AACZ,MAAA,IAAK,GAAA,CAA8B,SAAS,QAAA,EAAU;AACpD,QAAA,MAAM,MAAA,GAAc,aAAQ,KAAK,CAAA;AACjC,QAAA,IAAI,WAAW,KAAA,EAAO;AACtB,QAAA,KAAA,GAAQ,MAAA;AACR,QAAA;AAAA,MACF;AACA,MAAA,MAAM,GAAA;AAAA,IACR;AACA,IAAA,IAAI,WAAA,CAAY,IAAA,EAAM,SAAS,CAAA,EAAG;AAClC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,MAAA,EAAS,OAAO,CAAA,mDAAA,EAAsD,SAAA,CAAU,CAAC,CAAC,CAAA,CAAA;AAAA,KACpF;AAAA,EACF;AACF;AAGA,eAAsB,eAAA,CAAgB,OAAe,GAAA,EAA+B;AAClF,EAAA,MAAM,GAAA,GAAM,WAAA,CAAY,KAAA,EAAO,GAAG,CAAA;AAClC,EAAA,MAAM,oBAAA,CAAqB,KAAK,GAAG,CAAA;AACnC,EAAA,OAAO,GAAA;AACT;AAYO,SAAS,eAAe,GAAA,EAAsB;AACnD,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,QAAQ,IAAI,CAAA;AACrC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,GAAA,EAAK,CAAA,EAAA,EAAK;AAC5B,IAAA,IAAI,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,EAAG,OAAO,IAAA;AAAA,EAC3B;AACA,EAAA,OAAO,KAAA;AACT;AC/GA,IAAM,SAAA,GAAY,IAAI,IAAA,GAAO,IAAA;AAEtB,IAAM,QAAA,GAAwC;AAAA,EACnD,IAAA,EAAM,MAAA;AAAA,EACN,QAAA,EAAU,YAAA;AAAA,EACV,WAAA,EACE,wOAAA;AAAA,EAEF,SAAA,EACE,0bAAA;AAAA,EAMF,UAAA,EAAY,MAAA;AAAA,EACZ,QAAA,EAAU,KAAA;AAAA,EACV,YAAA,EAAc,CAAC,SAAS,CAAA;AAAA,EACxB,IAAA,EAAM,MAAA;AAAA,EACN,cAAA,EAAgB,MAAA;AAAA,EAChB,SAAA,EAAW,GAAA;AAAA,EACX,WAAA,EAAa;AAAA,IACX,IAAA,EAAM,QAAA;AAAA,IACN,UAAA,EAAY;AAAA,MACV,IAAA,EAAM;AAAA,QACJ,IAAA,EAAM,QAAA;AAAA,QACN,WAAA,EAAa;AAAA,OACf;AAAA,MACA,MAAA,EAAQ;AAAA,QACN,IAAA,EAAM,SAAA;AAAA,QACN,WAAA,EAAa;AAAA,OACf;AAAA,MACA,KAAA,EAAO;AAAA,QACL,IAAA,EAAM,SAAA;AAAA,QACN,WAAA,EAAa;AAAA;AACf,KACF;AAAA,IACA,QAAA,EAAU,CAAC,MAAM;AAAA,GACnB;AAAA,EACA,MAAM,OAAA,CAAQ,KAAA,EAAO,GAAA,EAAK;AACxB,IAAA,IAAI,CAAC,KAAA,EAAO,IAAA,EAAM,MAAM,IAAI,MAAM,wBAAwB,CAAA;AAC1D,IAAA,MAAM,OAAA,GAAU,MAAM,eAAA,CAAgB,KAAA,CAAM,MAAM,GAAG,CAAA;AAErD,IAAA,IAAIA,KAAAA;AACJ,IAAA,IAAI;AACF,MAAAA,KAAAA,GAAO,MAASC,GAAA,CAAA,IAAA,CAAK,OAAO,CAAA;AAAA,IAC9B,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,OAAQ,GAAA,CAA8B,IAAA;AAC5C,MAAA,IAAI,IAAA,KAAS,UAAU,MAAM,IAAI,MAAM,CAAA,sBAAA,EAAyB,KAAA,CAAM,IAAI,CAAA,CAAA,CAAG,CAAA;AAC7E,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,yBAAyB,KAAA,CAAM,IAAI,CAAA,GAAA,EAAM,cAAA,CAAe,GAAG,CAAC,CAAA;AAAA,OAC9D;AAAA,IACF;AACA,IAAA,IAAI,CAACD,KAAAA,CAAK,MAAA,EAAO,EAAG,MAAM,IAAI,KAAA,CAAM,CAAA,OAAA,EAAU,KAAA,CAAM,IAAI,CAAA,uBAAA,CAAyB,CAAA;AACjF,IAAA,IAAIA,KAAAA,CAAK,OAAO,SAAA,EAAW;AACzB,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,sBAAA,EAAyBA,MAAK,IAAI,CAAA,cAAA,EAAiB,SAAS,CAAA,CAAA,CAAG,CAAA;AAAA,IACjF;AAEA,IAAA,MAAM,GAAA,GAAM,MAASC,GAAA,CAAA,QAAA,CAAS,OAAO,CAAA;AACrC,IAAA,IAAI,cAAA,CAAe,GAAG,CAAA,EAAG;AACvB,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,OAAA,EAAU,KAAA,CAAM,IAAI,CAAA,sBAAA,CAAwB,CAAA;AAAA,IAC9D;AAEA,IAAA,MAAM,IAAA,GAAO,GAAA,CAAI,QAAA,CAAS,MAAM,CAAA;AAChC,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,YAAY,CAAA;AACxC,IAAA,MAAM,QAAQ,QAAA,CAAS,MAAA;AACvB,IAAA,MAAM,SAAS,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,KAAA,CAAM,UAAU,CAAC,CAAA;AAC5C,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,IAAI,KAAA,CAAM,KAAA,IAAS,GAAA,EAAM,GAAI,CAAC,CAAA;AAC7D,IAAA,IAAI,UAAU,CAAA,EAAG;AACf,MAAA,GAAA,CAAI,UAAA,CAAW,OAAA,EAASD,KAAAA,CAAK,OAAO,CAAA;AACpC,MAAA,OAAO,EAAE,MAAM,EAAA,EAAI,WAAA,EAAa,OAAO,QAAA,EAAU,MAAA,EAAQ,SAAA,EAAW,KAAA,GAAQ,CAAA,EAAE;AAAA,IAChF;AACA,IAAA,MAAM,QAAQ,QAAA,CAAS,KAAA,CAAM,SAAS,CAAA,EAAG,MAAA,GAAS,IAAI,KAAK,CAAA;AAC3D,IAAA,MAAM,SAAA,GAAY,MAAA,GAAS,CAAA,GAAI,KAAA,CAAM,MAAA,GAAS,KAAA;AAE9C,IAAA,MAAM,QAAQ,MAAA,CAAO,MAAA,GAAS,KAAA,CAAM,MAAA,GAAS,CAAC,CAAA,CAAE,MAAA;AAChD,IAAA,MAAM,QAAA,GAAW,MACd,GAAA,CAAI,CAAC,MAAM,CAAA,KAAM,CAAA,EAAG,OAAO,MAAA,GAAS,CAAC,EAAE,QAAA,CAAS,KAAA,EAAO,GAAG,CAAC,CAAA,MAAA,EAAI,IAAI,CAAA,CAAE,CAAA,CACrE,KAAK,IAAI,CAAA;AAEZ,IAAA,GAAA,CAAI,UAAA,CAAW,OAAA,EAASA,KAAAA,CAAK,OAAO,CAAA;AAEpC,IAAA,OAAO;AAAA,MACL,IAAA,EAAM,QAAA;AAAA,MACN,WAAA,EAAa,KAAA;AAAA,MACb,QAAA,EAAU,MAAA;AAAA,MACV;AAAA,KACF;AAAA,EACF;AACF","file":"read.js","sourcesContent":["import * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm → yarn → npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root→out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink — macOS `/var`→`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// ─── Command-output normalization (token-saving) ────────────────────────────\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) — never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only — it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends — the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n…[truncated ${total - kept} bytes]…\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI → collapse\n * carriage-return progress → trim trailing whitespace → collapse identical\n * consecutive lines → squeeze blank-line runs → head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines → 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n","import * as fs from 'node:fs/promises';\nimport type { Tool } from '@wrongstack/core';\nimport { isBinaryBuffer, safeResolveReal } from './_util.js';\nimport { toErrorMessage } from '@wrongstack/core/utils';\n\ninterface ReadInput {\n path: string;\n offset?: number | undefined;\n limit?: number | undefined;\n}\n\ninterface ReadOutput {\n text: string;\n total_lines: number;\n encoding: string;\n truncated: boolean;\n}\n\nconst MAX_BYTES = 5 * 1024 * 1024;\n\nexport const readTool: Tool<ReadInput, ReadOutput> = {\n name: 'read',\n category: 'Filesystem',\n description:\n 'Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. ' +\n 'Lines are returned 1-indexed with a ` N| ` prefix for easy reference in edits.',\n usageHint:\n 'FOUNDATIONAL TOOL — call this before almost any edit operation.\\n\\n' +\n 'Best practices:\\n' +\n '- Always read a file before using `edit`, `replace`, or `write` on it (the system often requires it for safety).\\n' +\n '- Use `offset` + `limit` for very large files instead of reading everything at once.\\n' +\n '- Default limit is generous (2000 lines) but can be increased.\\n' +\n '- The output format is designed to be directly usable as context for `edit` operations.',\n permission: 'auto',\n mutating: false,\n capabilities: ['fs.read'],\n icon: 'file',\n maxOutputBytes: 262_144,\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Path to the file (relative to project root or absolute within project).',\n },\n offset: {\n type: 'integer',\n description: '1-based starting line number. Use together with `limit` for large files.',\n },\n limit: {\n type: 'integer',\n description: 'Maximum number of lines to return (default is 2000).',\n },\n },\n required: ['path'],\n },\n async execute(input, ctx) {\n if (!input?.path) throw new Error('read: path is required');\n const absPath = await safeResolveReal(input.path, ctx);\n\n let stat: Awaited<ReturnType<typeof fs.stat>>;\n try {\n stat = await fs.stat(absPath);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') throw new Error(`read: file not found \"${input.path}\"`);\n throw new Error(\n `read: failed to stat \"${input.path}\": ${toErrorMessage(err)}`,\n );\n }\n if (!stat.isFile()) throw new Error(`read: \"${input.path}\" is not a regular file`);\n if (stat.size > MAX_BYTES) {\n throw new Error(`read: file too large (${stat.size} bytes, limit ${MAX_BYTES})`);\n }\n\n const buf = await fs.readFile(absPath);\n if (isBinaryBuffer(buf)) {\n throw new Error(`read: \"${input.path}\" appears to be binary`);\n }\n\n const text = buf.toString('utf8');\n const allLines = text.split(/\\r\\n|\\r|\\n/);\n const total = allLines.length;\n const offset = Math.max(1, input.offset ?? 1);\n const limit = Math.max(0, Math.min(input.limit ?? 2000, 5000));\n if (limit === 0) {\n ctx.recordRead(absPath, stat.mtimeMs);\n return { text: '', total_lines: total, encoding: 'utf8', truncated: total > 0 };\n }\n const slice = allLines.slice(offset - 1, offset - 1 + limit);\n const truncated = offset - 1 + slice.length < total;\n\n const width = String(offset + slice.length - 1).length;\n const numbered = slice\n .map((line, i) => `${String(offset + i).padStart(width, ' ')}→${line}`)\n .join('\\n');\n\n ctx.recordRead(absPath, stat.mtimeMs);\n\n return {\n text: numbered,\n total_lines: total,\n encoding: 'utf8',\n truncated,\n };\n },\n};\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/_util.ts","../src/read.ts"],"names":["stat","fs"],"mappings":"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","file":"read.js","sourcesContent":["import * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm → yarn → npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root→out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink — macOS `/var`→`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// ─── Command-output normalization (token-saving) ────────────────────────────\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) — never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only — it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends — the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n…[truncated ${total - kept} bytes]…\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI → collapse\n * carriage-return progress → trim trailing whitespace → collapse identical\n * consecutive lines → squeeze blank-line runs → head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines → 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n","import * as fs from 'node:fs/promises';\nimport type { Tool } from '@wrongstack/core';\nimport { toErrorMessage } from '@wrongstack/core/utils';\nimport { isBinaryBuffer, safeResolveReal } from './_util.js';\n\ninterface ReadInput {\n path: string;\n offset?: number | undefined;\n limit?: number | undefined;\n mode?: 'content' | 'summary' | undefined;\n}\n\ninterface ReadOutput {\n text: string;\n total_lines: number;\n encoding: string;\n truncated: boolean;\n cached?: boolean | undefined;\n note?: string | undefined;\n}\n\nconst MAX_BYTES = 5 * 1024 * 1024;\n\nexport const readTool: Tool<ReadInput, ReadOutput> = {\n name: 'read',\n category: 'Filesystem',\n description:\n 'Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. ' +\n 'Lines are returned 1-indexed with a ` N| ` prefix for easy reference in edits.',\n usageHint:\n 'FOUNDATIONAL TOOL — call this before almost any edit operation.\\n\\n' +\n 'Best practices:\\n' +\n '- Always read a file before using `edit`, `replace`, or `write` on it (the system often requires it for safety).\\n' +\n '- Use `offset` + `limit` for very large files instead of reading everything at once.\\n' +\n '- Default limit is generous (2000 lines) but can be increased.\\n' +\n '- The output format is designed to be directly usable as context for `edit` operations.',\n permission: 'auto',\n mutating: false,\n capabilities: ['fs.read'],\n icon: 'file',\n maxOutputBytes: 262_144,\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Path to the file (relative to project root or absolute within project).',\n },\n offset: {\n type: 'integer',\n description: '1-based starting line number. Use together with `limit` for large files.',\n },\n limit: {\n type: 'integer',\n description: 'Maximum number of lines to return (default is 2000).',\n },\n mode: {\n type: 'string',\n enum: ['content', 'summary'],\n description:\n 'Return full line-numbered content (default) or a compact file summary with imports/exports/symbols.',\n },\n },\n required: ['path'],\n },\n async execute(input, ctx) {\n if (!input?.path) throw new Error('read: path is required');\n const absPath = await safeResolveReal(input.path, ctx);\n\n let stat: Awaited<ReturnType<typeof fs.stat>>;\n try {\n stat = await fs.stat(absPath);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') throw new Error(`read: file not found \"${input.path}\"`);\n throw new Error(`read: failed to stat \"${input.path}\": ${toErrorMessage(err)}`);\n }\n if (!stat.isFile()) throw new Error(`read: \"${input.path}\" is not a regular file`);\n if (stat.size > MAX_BYTES) {\n throw new Error(`read: file too large (${stat.size} bytes, limit ${MAX_BYTES})`);\n }\n\n const offset = Math.max(1, input.offset ?? 1);\n const limit = Math.max(0, Math.min(input.limit ?? 2000, 5000));\n const prior = getReadRangeRecord(ctx, absPath);\n const requestedEnd = prior\n ? Math.min(offset + limit - 1, prior.totalLines)\n : offset + limit - 1;\n if (\n input.mode !== 'summary' &&\n limit > 0 &&\n prior &&\n coversRange(prior, stat.mtimeMs, offset, requestedEnd)\n ) {\n ctx.recordRead(absPath, stat.mtimeMs);\n return {\n text:\n `[unchanged since previous read: \"${input.path}\" mtime=${Math.round(stat.mtimeMs)}; ` +\n `requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,\n total_lines: prior.totalLines,\n encoding: 'utf8',\n truncated: requestedEnd < prior.totalLines,\n cached: true,\n note: 'Repeated read suppressed to save tokens.',\n };\n }\n\n const buf = await fs.readFile(absPath);\n if (isBinaryBuffer(buf)) {\n throw new Error(`read: \"${input.path}\" appears to be binary`);\n }\n\n const text = buf.toString('utf8');\n const allLines = text.split(/\\r\\n|\\r|\\n/);\n const total = allLines.length;\n if (input.mode === 'summary') {\n ctx.recordRead(absPath, stat.mtimeMs);\n rememberReadRange(ctx, absPath, stat.mtimeMs, total, 1, Math.min(total, 200));\n return {\n text: summarizeFile(input.path, stat.size, allLines),\n total_lines: total,\n encoding: 'utf8',\n truncated: total > 200,\n note: 'Summary mode returned compact structure instead of full file content.',\n };\n }\n if (limit === 0) {\n ctx.recordRead(absPath, stat.mtimeMs);\n rememberReadRange(ctx, absPath, stat.mtimeMs, total, 1, 0);\n return { text: '', total_lines: total, encoding: 'utf8', truncated: total > 0 };\n }\n // Offset past EOF: return an explicit message instead of an empty string.\n // Without this, models with weak instruction-following (e.g. k2p7) see an\n // empty result, assume the read failed transiently, and retry the exact\n // same offset indefinitely — a tight tool-use loop that burns iterations\n // and context without making progress.\n if (offset > total) {\n ctx.recordRead(absPath, stat.mtimeMs);\n rememberReadRange(ctx, absPath, stat.mtimeMs, total, total + 1, total + 1);\n return {\n text: `[offset ${offset} is past end of file \"${input.path}\" — file has ${total} line(s). Do not retry this offset.]`,\n total_lines: total,\n encoding: 'utf8',\n truncated: false,\n };\n }\n\n const slice = allLines.slice(offset - 1, offset - 1 + limit);\n const truncated = offset - 1 + slice.length < total;\n\n const width = String(offset + slice.length - 1).length;\n const numbered = slice\n .map((line, i) => `${String(offset + i).padStart(width, ' ')}→${line}`)\n .join('\\n');\n\n ctx.recordRead(absPath, stat.mtimeMs);\n rememberReadRange(ctx, absPath, stat.mtimeMs, total, offset, offset + slice.length - 1);\n\n return {\n text: numbered,\n total_lines: total,\n encoding: 'utf8',\n truncated,\n };\n },\n};\n\ninterface ReadRangeRecord {\n mtimeMs: number;\n totalLines: number;\n ranges: Array<{ start: number; end: number }>;\n}\n\nconst READ_RANGES_META_KEY = 'tools.read.ranges.v1';\n\nfunction getReadRanges(ctx: import('@wrongstack/core').Context): Record<string, ReadRangeRecord> {\n const existing = ctx.meta[READ_RANGES_META_KEY];\n if (existing && typeof existing === 'object' && !Array.isArray(existing)) {\n return existing as Record<string, ReadRangeRecord>;\n }\n const next: Record<string, ReadRangeRecord> = {};\n ctx.meta[READ_RANGES_META_KEY] = next;\n return next;\n}\n\nfunction getReadRangeRecord(\n ctx: import('@wrongstack/core').Context,\n absPath: string,\n): ReadRangeRecord | undefined {\n return getReadRanges(ctx)[absPath];\n}\n\nfunction rememberReadRange(\n ctx: import('@wrongstack/core').Context,\n absPath: string,\n mtimeMs: number,\n totalLines: number,\n start: number,\n end: number,\n): void {\n if (end < start) return;\n const ranges = getReadRanges(ctx);\n const prior = ranges[absPath];\n const nextRanges = prior && Math.abs(prior.mtimeMs - mtimeMs) <= 1 ? prior.ranges.slice() : [];\n nextRanges.push({ start, end });\n ranges[absPath] = {\n mtimeMs,\n totalLines,\n ranges: mergeRanges(nextRanges),\n };\n}\n\nfunction coversRange(\n record: ReadRangeRecord,\n mtimeMs: number,\n start: number,\n end: number,\n): boolean {\n if (Math.abs(record.mtimeMs - mtimeMs) > 1) return false;\n return record.ranges.some((range) => range.start <= start && range.end >= end);\n}\n\nfunction mergeRanges(\n ranges: Array<{ start: number; end: number }>,\n): Array<{ start: number; end: number }> {\n const sorted = ranges.slice().sort((a, b) => a.start - b.start);\n const merged: Array<{ start: number; end: number }> = [];\n for (const range of sorted) {\n const last = merged[merged.length - 1];\n if (!last || range.start > last.end + 1) {\n merged.push({ ...range });\n continue;\n }\n last.end = Math.max(last.end, range.end);\n }\n return merged;\n}\n\nfunction summarizeFile(filePath: string, bytes: number, lines: string[]): string {\n const interesting = lines\n .map((line, index) => ({ line: line.trim(), number: index + 1 }))\n .filter(({ line }) =>\n /^(import\\s|export\\s|class\\s|interface\\s|type\\s|function\\s|const\\s+\\w+\\s*=|let\\s+\\w+\\s*=|var\\s+\\w+\\s*=|def\\s+|async\\s+function\\s)/.test(\n line,\n ),\n )\n .slice(0, 80)\n .map(({ line, number }) => `${number}: ${line}`);\n return [\n `summary: ${filePath}`,\n `bytes=${bytes}`,\n `total_lines=${lines.length}`,\n interesting.length > 0\n ? `symbols/imports:\\n${interesting.join('\\n')}`\n : 'symbols/imports: (none detected)',\n ].join('\\n');\n}\n"]}
|
package/dist/tool-icons.js
CHANGED
package/dist/tool-icons.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tool-icons.ts"],"names":[],"mappings":"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","file":"tool-icons.js","sourcesContent":["// Canonical, surface-agnostic per-tool icon identity + color.\n//\n// This is PURE DATA (no node imports) so it is safe to import from the browser\n// (WebUI) as well as the TUI. Each surface maps a `ToolIconId` to its own\n// rendering primitive — the WebUI to a lucide-react component (`tool-icon.ts`),\n// the TUI to a unicode glyph (`tool-glyph.ts`) — and pulls the shared color\n// from `TOOL_ICON_CONFIG` so both stay in lockstep.\n//\n// Add a new tool by giving it an entry in `TOOL_ICON_MAP` (name → id). The id\n// must be one of the `ToolIconId` union members below; every id already has a\n// color, a lucide component, and a glyph.\n\n/** The closed set of visual identities a tool can map to. */\nexport type ToolIconId =\n | 'file'\n | 'edit'\n | 'search'\n | 'folder'\n | 'terminal'\n | 'web'\n | 'git'\n | 'tree'\n | 'code'\n | 'test'\n | 'package'\n | 'document'\n | 'scaffold'\n | 'todo'\n | 'plan'\n | 'task'\n | 'meta'\n | 'index'\n | 'json'\n | 'diff'\n | 'logs'\n | 'settings'\n | 'brain'\n | 'fallback';\n\n/** Canonical hex color per icon id — the single source both surfaces read. */\nexport const TOOL_ICON_CONFIG: Record<ToolIconId, { color: string }> = {\n file: { color: '#60a5fa' }, // blue\n edit: { color: '#fbbf24' }, // amber\n search: { color: '#a78bfa' }, // violet\n folder: { color: '#38bdf8' }, // sky\n terminal: { color: '#
|
|
1
|
+
{"version":3,"sources":["../src/tool-icons.ts"],"names":[],"mappings":"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","file":"tool-icons.js","sourcesContent":["// Canonical, surface-agnostic per-tool icon identity + color.\n//\n// This is PURE DATA (no node imports) so it is safe to import from the browser\n// (WebUI) as well as the TUI. Each surface maps a `ToolIconId` to its own\n// rendering primitive — the WebUI to a lucide-react component (`tool-icon.ts`),\n// the TUI to a unicode glyph (`tool-glyph.ts`) — and pulls the shared color\n// from `TOOL_ICON_CONFIG` so both stay in lockstep.\n//\n// Add a new tool by giving it an entry in `TOOL_ICON_MAP` (name → id). The id\n// must be one of the `ToolIconId` union members below; every id already has a\n// color, a lucide component, and a glyph.\n\n/** The closed set of visual identities a tool can map to. */\nexport type ToolIconId =\n | 'file'\n | 'edit'\n | 'search'\n | 'folder'\n | 'terminal'\n | 'web'\n | 'git'\n | 'tree'\n | 'code'\n | 'test'\n | 'package'\n | 'document'\n | 'scaffold'\n | 'todo'\n | 'plan'\n | 'task'\n | 'meta'\n | 'index'\n | 'json'\n | 'diff'\n | 'logs'\n | 'settings'\n | 'brain'\n | 'fallback';\n\n/** Canonical hex color per icon id — the single source both surfaces read. */\nexport const TOOL_ICON_CONFIG: Record<ToolIconId, { color: string }> = {\n file: { color: '#60a5fa' }, // blue\n edit: { color: '#fbbf24' }, // amber\n search: { color: '#a78bfa' }, // violet\n folder: { color: '#38bdf8' }, // sky\n terminal: { color: '#fb923c' }, // orange\n web: { color: '#34d399' }, // emerald\n git: { color: '#fb923c' }, // orange\n tree: { color: '#22d3ee' }, // cyan\n code: { color: '#818cf8' }, // indigo\n test: { color: '#4ade80' }, // green\n package: { color: '#f472b6' }, // pink\n document: { color: '#94a3b8' }, // slate\n scaffold: { color: '#c084fc' }, // purple\n todo: { color: '#facc15' }, // yellow\n plan: { color: '#2dd4bf' }, // teal\n task: { color: '#5eead4' }, // teal-light\n meta: { color: '#cbd5e1' }, // slate-light\n index: { color: '#06b6d4' }, // cyan-dark\n json: { color: '#eab308' }, // yellow-dark\n diff: { color: '#f97316' }, // orange-dark\n logs: { color: '#a3a3a3' }, // neutral\n settings: { color: '#9ca3af' }, // gray\n brain: { color: '#e879f9' }, // fuchsia\n fallback: { color: '#9ca3af' }, // gray\n};\n\n/**\n * Tool name (and common alias) → icon id. Keys are lowercase; `getToolIcon`\n * lowercases the query, so lookups are case-insensitive. Covers every builtin\n * tool plus the aliases models commonly emit.\n */\nexport const TOOL_ICON_MAP: Record<string, ToolIconId> = {\n // ── file IO ──\n read: 'file',\n cat: 'file',\n view: 'file',\n write: 'file',\n create: 'file',\n edit: 'edit',\n replace: 'edit',\n str_replace: 'edit',\n multi_edit: 'edit',\n patch: 'diff',\n // ── search ──\n grep: 'search',\n search: 'search',\n rg: 'search',\n ripgrep: 'search',\n glob: 'search',\n find: 'search',\n // ── navigation ──\n folder: 'folder',\n ls: 'folder',\n list: 'folder',\n set_working_dir: 'folder',\n tree: 'tree',\n // ── shell ──\n bash: 'terminal',\n shell: 'terminal',\n sh: 'terminal',\n exec: 'terminal',\n run: 'terminal',\n command: 'terminal',\n // ── web ──\n fetch: 'web',\n web_fetch: 'web',\n web_search: 'web',\n // ── vcs ──\n git: 'git',\n diff: 'diff',\n // ── code quality ──\n lint: 'code',\n format: 'settings',\n typecheck: 'code',\n test: 'test',\n // ── packages ──\n install: 'package',\n outdated: 'package',\n audit: 'package',\n // ── docs / scaffold ──\n document: 'document',\n scaffold: 'scaffold',\n // ── planning / work tracking ──\n todo: 'todo',\n plan: 'plan',\n task: 'task',\n // ── data ──\n json: 'json',\n index: 'index',\n logs: 'logs',\n // ── meta / tooling ──\n mode: 'meta',\n tool_search: 'meta',\n tool_use: 'meta',\n batch_tool_use: 'meta',\n tool_help: 'meta',\n // ── memory ──\n remember: 'brain',\n forget: 'brain',\n search_memory: 'brain',\n find_related_memories: 'brain',\n};\n\n/**\n * Resolve a tool name to its canonical `ToolIconId`. Case-insensitive.\n * Unknown names — including `mcp__*` server tools and plugin tools — fall back\n * to `'fallback'`.\n */\nexport function getToolIcon(name: string): ToolIconId {\n if (!name) return 'fallback';\n return TOOL_ICON_MAP[name.toLowerCase()] ?? 'fallback';\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.267.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "WrongStack built-in tools: read/write/edit, bash/exec, grep/glob, git, fetch, test, lint, and more.",
|
|
6
6
|
"repository": {
|
|
@@ -180,7 +180,7 @@
|
|
|
180
180
|
"turndown": "^7.2.0",
|
|
181
181
|
"typescript": "^6.0.3",
|
|
182
182
|
"undici": "^8.4.1",
|
|
183
|
-
"@wrongstack/core": "0.
|
|
183
|
+
"@wrongstack/core": "0.267.0"
|
|
184
184
|
},
|
|
185
185
|
"devDependencies": {
|
|
186
186
|
"@types/node": "^25.9.3",
|