pi-readseek 0.3.8 → 0.3.10

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/src/read.ts CHANGED
@@ -11,8 +11,9 @@ import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
11
11
  import { readFile as fsReadFile } from "fs/promises";
12
12
  import { normalizeToLF, stripBom, hasBareCarriageReturn } from "./edit-diff.js";
13
13
  import { ensureHashInit, escapeControlCharsForDisplay } from "./hashline.js";
14
- import { buildReadseekError, buildReadseekWarning, renderReadseekLines, type ReadseekLine, type ReadseekWarning } from "./readseek-value.js";
14
+ import { buildReadseekWarning, buildToolErrorResult, renderReadseekLines, type ReadseekLine, type ReadseekWarning } from "./readseek-value.js";
15
15
  import { looksLikeBinary } from "./binary-detect.js";
16
+ import { isSupportedImageBuffer } from "./image-detect.js";
16
17
  import { resolveToCwd } from "./path-utils.js";
17
18
  import { throwIfAborted } from "./runtime.js";
18
19
  import { getOrGenerateMap } from "./map-cache.js";
@@ -26,17 +27,11 @@ import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
26
27
  import { readseekRead } from "./readseek-client.js";
27
28
  import { Text } from "@earendil-works/pi-tui";
28
29
  import { formatReadCallText, formatReadResultText } from "./read-render-helpers.js";
29
- import { clampLineToWidth, clampLinesToWidth, isRendererExpanded, linkToolPath, renderToolLabel, summaryLine, wrapReadHashlinesForWidth } from "./tui-render-utils.js";
30
+ import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderToolLabel, resolveRenderResultContext, summaryLine, wrapReadHashlinesForWidth } from "./tui-render-utils.js";
30
31
 
31
32
  const READ_PROMPT_METADATA = defineToolPromptMetadata({
32
33
  promptUrl: new URL("../prompts/read.md", import.meta.url),
33
34
  promptSnippet: "Read text files or images; text reads include hashline anchors and optional maps/symbol lookup",
34
- promptGuidelines: [
35
- "Use read instead of bash cat/head/tail/sed for file inspection.",
36
- "Use read for images/screenshots; supported images return attachments like stock pi read.",
37
- "Use read offset/limit, symbol, or map to keep large files focused.",
38
- "Use read anchors as fresh inputs for edit.",
39
- ],
40
35
  });
41
36
 
42
37
  interface ReadParams {
@@ -58,55 +53,6 @@ function splitReadseekLines(text: string): string[] {
58
53
  return withoutTrailingTerminator.split("\n");
59
54
  }
60
55
 
61
- const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
62
-
63
- function startsWithBytes(buffer: Buffer, bytes: number[]): boolean {
64
- return buffer.length >= bytes.length && bytes.every((byte, index) => buffer[index] === byte);
65
- }
66
-
67
- function startsWithAscii(buffer: Buffer, offset: number, text: string): boolean {
68
- if (buffer.length < offset + text.length) return false;
69
- for (let index = 0; index < text.length; index++) {
70
- if (buffer[offset + index] !== text.charCodeAt(index)) return false;
71
- }
72
- return true;
73
- }
74
-
75
- function readUint32BE(buffer: Buffer, offset: number): number {
76
- return (
77
- ((buffer[offset] ?? 0) * 0x1000000) +
78
- ((buffer[offset + 1] ?? 0) << 16) +
79
- ((buffer[offset + 2] ?? 0) << 8) +
80
- (buffer[offset + 3] ?? 0)
81
- );
82
- }
83
-
84
- function isPng(buffer: Buffer): boolean {
85
- return buffer.length >= 16 && readUint32BE(buffer, PNG_SIGNATURE.length) === 13 && startsWithAscii(buffer, 12, "IHDR");
86
- }
87
-
88
- function isAnimatedPng(buffer: Buffer): boolean {
89
- let offset = PNG_SIGNATURE.length;
90
- while (offset + 8 <= buffer.length) {
91
- const chunkLength = readUint32BE(buffer, offset);
92
- const chunkTypeOffset = offset + 4;
93
- if (startsWithAscii(buffer, chunkTypeOffset, "acTL")) return true;
94
- if (startsWithAscii(buffer, chunkTypeOffset, "IDAT")) return false;
95
- const nextOffset = offset + 8 + chunkLength + 4;
96
- if (nextOffset <= offset || nextOffset > buffer.length) return false;
97
- offset = nextOffset;
98
- }
99
- return false;
100
- }
101
-
102
- function isSupportedImageBuffer(buffer: Buffer): boolean {
103
- if (startsWithBytes(buffer, [0xff, 0xd8, 0xff])) return buffer[3] !== 0xf7;
104
- if (startsWithBytes(buffer, PNG_SIGNATURE)) return isPng(buffer) && !isAnimatedPng(buffer);
105
- if (startsWithAscii(buffer, 0, "GIF")) return true;
106
- return startsWithAscii(buffer, 0, "RIFF") && startsWithAscii(buffer, 8, "WEBP");
107
- }
108
-
109
-
110
56
  export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}) {
111
57
  const toolConfig = {
112
58
  callable: true,
@@ -151,63 +97,19 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
151
97
  const rawParams = params as ReadParams;
152
98
  const offset = coerceObviousBase10Int(rawParams.offset, "offset");
153
99
  if (!offset.ok) {
154
- return {
155
- content: [{ type: "text", text: offset.message }],
156
- isError: true,
157
- details: {
158
- readseekValue: {
159
- tool: "read",
160
- ok: false,
161
- path: rawParams.path,
162
- error: buildReadseekError("invalid-offset", offset.message),
163
- },
164
- },
165
- };
100
+ return buildToolErrorResult("read", "invalid-offset", offset.message, { path: rawParams.path });
166
101
  }
167
102
  const limit = coerceObviousBase10Int(rawParams.limit, "limit");
168
103
  if (!limit.ok) {
169
- return {
170
- content: [{ type: "text", text: limit.message }],
171
- isError: true,
172
- details: {
173
- readseekValue: {
174
- tool: "read",
175
- ok: false,
176
- path: rawParams.path,
177
- error: buildReadseekError("invalid-limit", limit.message),
178
- },
179
- },
180
- };
104
+ return buildToolErrorResult("read", "invalid-limit", limit.message, { path: rawParams.path });
181
105
  }
182
106
  if (limit.value !== undefined && limit.value < 1) {
183
107
  const message = `Invalid limit: expected a positive integer, received ${limit.value}.`;
184
- return {
185
- content: [{ type: "text", text: message }],
186
- isError: true,
187
- details: {
188
- readseekValue: {
189
- tool: "read",
190
- ok: false,
191
- path: rawParams.path,
192
- error: buildReadseekError("invalid-limit", message),
193
- },
194
- },
195
- };
108
+ return buildToolErrorResult("read", "invalid-limit", message, { path: rawParams.path });
196
109
  }
197
110
  if (offset.value !== undefined && offset.value < 1) {
198
111
  const message = `Invalid offset: expected a positive integer, received ${offset.value}.`;
199
- return {
200
- content: [{ type: "text", text: message }],
201
- isError: true,
202
- details: {
203
- readseekValue: {
204
- tool: "read",
205
- ok: false,
206
- path: rawParams.path,
207
- error: buildReadseekError("invalid-offset", message),
208
- },
209
- },
210
- };
112
+ return buildToolErrorResult("read", "invalid-offset", message, { path: rawParams.path });
211
113
  }
212
114
  const p = {
213
115
  ...rawParams,
@@ -218,18 +120,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
218
120
  const trimmedSymbol = typeof rawParams.symbol === "string" ? rawParams.symbol.trim() : "";
219
121
  if (trimmedSymbol.length === 0) {
220
122
  const message = "Invalid symbol: expected a non-empty string.";
221
- return {
222
- content: [{ type: "text", text: message }],
223
- isError: true,
224
- details: {
225
- readseekValue: {
226
- tool: "read",
227
- ok: false,
228
- path: rawParams.path,
229
- error: buildReadseekError("invalid-params-combo", message),
230
- },
231
- },
232
- };
123
+ return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
233
124
  }
234
125
  p.symbol = trimmedSymbol;
235
126
  }
@@ -246,63 +137,19 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
246
137
  throwIfAborted(signal);
247
138
  if (p.symbol && (p.offset !== undefined || p.limit !== undefined)) {
248
139
  const message = "Cannot combine symbol with offset/limit. Use one or the other.";
249
- return {
250
- content: [{ type: "text", text: message }],
251
- isError: true,
252
- details: {
253
- readseekValue: {
254
- tool: "read",
255
- ok: false,
256
- path: rawParams.path,
257
- error: buildReadseekError("invalid-params-combo", message),
258
- },
259
- },
260
- };
140
+ return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
261
141
  }
262
142
  if (p.bundle && !p.symbol) {
263
143
  const message = 'Cannot use bundle without symbol. Use read({ path, symbol, bundle: "local" }).';
264
- return {
265
- content: [{ type: "text", text: message }],
266
- isError: true,
267
- details: {
268
- readseekValue: {
269
- tool: "read",
270
- ok: false,
271
- path: rawParams.path,
272
- error: buildReadseekError("invalid-params-combo", message),
273
- },
274
- },
275
- };
144
+ return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
276
145
  }
277
146
  if (p.bundle && p.map) {
278
147
  const message = "Cannot combine bundle with map. Use one or the other.";
279
- return {
280
- content: [{ type: "text", text: message }],
281
- isError: true,
282
- details: {
283
- readseekValue: {
284
- tool: "read",
285
- ok: false,
286
- path: rawParams.path,
287
- error: buildReadseekError("invalid-params-combo", message),
288
- },
289
- },
290
- };
148
+ return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
291
149
  }
292
150
  if (p.map && p.symbol) {
293
151
  const message = "Cannot combine map with symbol. Use one or the other.";
294
- return {
295
- content: [{ type: "text", text: message }],
296
- isError: true,
297
- details: {
298
- readseekValue: {
299
- tool: "read",
300
- ok: false,
301
- path: rawParams.path,
302
- error: buildReadseekError("invalid-params-combo", message),
303
- },
304
- },
305
- };
152
+ return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
306
153
  }
307
154
  // Delegate images to the built-in read tool
308
155
  throwIfAborted(signal);
@@ -320,69 +167,18 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
320
167
  const code = err?.code;
321
168
  if (code === "EISDIR") {
322
169
  const message = `Path is a directory: ${rawPath}. Use ls to inspect directories.`;
323
- return {
324
- content: [{ type: "text", text: message }],
325
- isError: true,
326
- details: {
327
- readseekValue: {
328
- tool: "read",
329
- ok: false,
330
- path: rawParams.path,
331
- error: buildReadseekError(
332
- "path-is-directory",
333
- message,
334
- `Use ls(${JSON.stringify(rawPath)}) to inspect directories.`,
335
- ),
336
- },
337
- },
338
- };
170
+ return buildToolErrorResult("read", "path-is-directory", message, { path: rawParams.path, hint: `Use ls(${JSON.stringify(rawPath)}) to inspect directories.` });
339
171
  }
340
172
  if (code === "EACCES" || code === "EPERM") {
341
173
  const message = `Permission denied — cannot access: ${rawPath}`;
342
- return {
343
- content: [{ type: "text", text: message }],
344
- isError: true,
345
- details: {
346
- readseekValue: {
347
- tool: "read",
348
- ok: false,
349
- path: rawParams.path,
350
- error: buildReadseekError("permission-denied", message),
351
- },
352
- },
353
- };
174
+ return buildToolErrorResult("read", "permission-denied", message, { path: rawParams.path });
354
175
  }
355
176
  if (code === "ENOENT") {
356
177
  const message = `File not found: ${rawPath}`;
357
- return {
358
- content: [{ type: "text", text: message }],
359
- isError: true,
360
- details: {
361
- readseekValue: {
362
- tool: "read",
363
- ok: false,
364
- path: rawParams.path,
365
- error: buildReadseekError("file-not-found", message),
366
- },
367
- },
368
- };
178
+ return buildToolErrorResult("read", "file-not-found", message, { path: rawParams.path });
369
179
  }
370
180
  const message = `File not readable: ${rawPath}${err?.message ? ` — ${err.message}` : ""}`;
371
- return {
372
- content: [{ type: "text", text: message }],
373
- isError: true,
374
- details: {
375
- readseekValue: {
376
- tool: "read",
377
- ok: false,
378
- path: rawParams.path,
379
- error: buildReadseekError("fs-error", message, undefined, {
380
- fsCode: code,
381
- fsMessage: err?.message,
382
- }),
383
- },
384
- },
385
- };
181
+ return buildToolErrorResult("read", "fs-error", message, { path: rawParams.path, details: { fsCode: code, fsMessage: err?.message } });
386
182
  }
387
183
 
388
184
  if (isSupportedImageBuffer(rawBuffer)) {
@@ -399,18 +195,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
399
195
  let endIdx = p.limit !== undefined ? Math.min(startLine - 1 + p.limit, total) : total;
400
196
  if (p.offset !== undefined && startLine > total) {
401
197
  const message = `[offset ${p.offset} is past end of file (${total} lines)]`;
402
- return {
403
- content: [{ type: "text", text: message }],
404
- isError: true,
405
- details: {
406
- readseekValue: {
407
- tool: "read",
408
- ok: false,
409
- path: rawParams.path,
410
- error: buildReadseekError("offset-past-end", message),
411
- },
412
- },
413
- };
198
+ return buildToolErrorResult("read", "offset-past-end", message, { path: rawParams.path });
414
199
  }
415
200
  let symbolMatch: SymbolMatch | undefined;
416
201
  let symbolFileMap: Awaited<ReturnType<typeof getOrGenerateMap>> | null = null;
@@ -550,23 +335,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
550
335
  } catch (err: any) {
551
336
  const detail = err?.message ? ` — ${err.message}` : "";
552
337
  const message = `readseek failed while reading ${rawPath}${detail}`;
553
- return {
554
- content: [{ type: "text", text: message }],
555
- isError: true,
556
- details: {
557
- readseekValue: {
558
- tool: "read",
559
- ok: false,
560
- path: rawParams.path,
561
- error: buildReadseekError(
562
- "readseek-error",
563
- message,
564
- "Ensure @jarkkojs/readseek and its npm platform package are installed.",
565
- { message: err?.message },
566
- ),
567
- },
568
- },
569
- };
338
+ return buildToolErrorResult("read", "readseek-error", message, { path: rawParams.path, hint: "Ensure @jarkkojs/readseek and its npm platform package are installed.", details: { message: err?.message } });
570
339
  }
571
340
  const expectedLineCount = Math.max(0, endIdx - startLine + 1);
572
341
  const invalidLine = readseekOutput.hashlines.find((line, index) => line.line !== startLine + index);
@@ -574,18 +343,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
574
343
  const message = invalidLine
575
344
  ? `readseek returned non-sequential line ${invalidLine.line} for requested range ${startLine}-${endIdx}`
576
345
  : `readseek returned ${readseekOutput.hashlines.length} lines for requested range ${startLine}-${endIdx} (${expectedLineCount} expected)`;
577
- return {
578
- content: [{ type: "text", text: message }],
579
- isError: true,
580
- details: {
581
- readseekValue: {
582
- tool: "read",
583
- ok: false,
584
- path: rawParams.path,
585
- error: buildReadseekError("readseek-output-mismatch", message),
586
- },
587
- },
588
- };
346
+ return buildToolErrorResult("read", "readseek-output-mismatch", message, { path: rawParams.path });
589
347
  }
590
348
  const readseekLines: ReadseekLine[] = readseekOutput.hashlines.map((line) => ({
591
349
  line: line.line,
@@ -708,11 +466,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
708
466
  return new Text(clampLineToWidth(text, context.width), 0, 0);
709
467
  },
710
468
  renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
711
- const context: { isPartial?: boolean; isError?: boolean; expanded?: boolean; cwd?: string; width?: number } = rest[0] ?? options ?? {};
712
- const isPartial = context.isPartial ?? (options as any)?.isPartial ?? false;
713
- const isError = context.isError ?? false;
714
- const expanded = isRendererExpanded(options as any, context as any);
715
- const width = context.width ?? (options as any)?.width;
469
+ const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
716
470
  if (isPartial) return new Text(clampLinesToWidth([summaryLine("pending read")], width).join("\n"), 0, 0);
717
471
 
718
472
  const content = result.content?.[0];
@@ -4,49 +4,25 @@ import { readseekMap, readseekMapContent } from "../readseek-client.js";
4
4
  import { throwIfAborted } from "../runtime.js";
5
5
  import type { FileMap, MapOptions } from "./types.js";
6
6
 
7
- export const READSEEK_MAPPER_NAME = "readseek";
8
- export const READSEEK_MAPPER_VERSION = 2;
9
-
10
- export interface MapperIdentity {
11
- mapperName: string;
12
- mapperVersion: number;
13
- }
14
-
15
- export interface MapResultWithIdentity extends MapperIdentity {
16
- map: FileMap | null;
17
- }
18
-
19
- export const READSEEK_MAPPER_IDENTITY: MapperIdentity = {
20
- mapperName: READSEEK_MAPPER_NAME,
21
- mapperVersion: READSEEK_MAPPER_VERSION,
22
- };
23
- export async function generateMapWithIdentity(
24
- filePath: string,
25
- options: MapOptions = {},
26
- ): Promise<MapResultWithIdentity> {
27
- throwIfAborted(options.signal);
28
- const fileStat = await stat(filePath);
29
- throwIfAborted(options.signal);
30
- const map = await readseekMap(filePath, fileStat.size, { signal: options.signal });
31
- throwIfAborted(options.signal);
32
- return { map, ...READSEEK_MAPPER_IDENTITY };
33
- }
34
-
35
7
  export async function generateMap(
36
- filePath: string,
37
- options: MapOptions = {},
8
+ filePath: string,
9
+ options: MapOptions = {},
38
10
  ): Promise<FileMap | null> {
39
- return (await generateMapWithIdentity(filePath, options)).map;
11
+ throwIfAborted(options.signal);
12
+ const fileStat = await stat(filePath);
13
+ throwIfAborted(options.signal);
14
+ const map = await readseekMap(filePath, fileStat.size, { signal: options.signal });
15
+ throwIfAborted(options.signal);
16
+ return map;
40
17
  }
41
18
 
42
19
  export async function generateMapFromContent(
43
- filePath: string,
44
- content: string,
45
- options: MapOptions = {},
20
+ filePath: string,
21
+ content: string,
22
+ options: MapOptions = {},
46
23
  ): Promise<FileMap | null> {
47
- throwIfAborted(options.signal);
48
- const map = await readseekMapContent(filePath, content, { signal: options.signal });
49
- throwIfAborted(options.signal);
50
- return map;
24
+ throwIfAborted(options.signal);
25
+ const map = await readseekMapContent(filePath, content, { signal: options.signal });
26
+ throwIfAborted(options.signal);
27
+ return map;
51
28
  }
52
-
@@ -4,7 +4,6 @@ import { join } from "node:path";
4
4
 
5
5
  export interface ReadseekJsonSettings {
6
6
  grep?: { maxLines?: number; maxBytes?: number };
7
- mapCache?: { dir?: string; enabled?: boolean };
8
7
  edit?: { diffDisplay?: "collapsed" | "expanded" };
9
8
  }
10
9
 
@@ -77,16 +76,6 @@ function validateSettings(raw: unknown, source: string): ReadseekSettingsResult
77
76
  if (Object.keys(grep).length > 0) settings.grep = grep;
78
77
  }
79
78
 
80
- if (isRecord(raw.mapCache)) {
81
- const mapCache: NonNullable<ReadseekJsonSettings["mapCache"]> = {};
82
- if ("dir" in raw.mapCache) {
83
- if (typeof raw.mapCache.dir === "string" && raw.mapCache.dir.length > 0) mapCache.dir = raw.mapCache.dir;
84
- else warnings.push(invalid(source, "mapCache.dir"));
85
- }
86
- const enabled = readBoolean(raw.mapCache, "enabled", "mapCache.enabled", source, warnings);
87
- if (enabled !== undefined) mapCache.enabled = enabled;
88
- if (Object.keys(mapCache).length > 0) settings.mapCache = mapCache;
89
- }
90
79
 
91
80
  if (isRecord(raw.edit)) {
92
81
  const edit: NonNullable<ReadseekJsonSettings["edit"]> = {};
@@ -117,8 +106,6 @@ function mergeSettings(base: ReadseekJsonSettings, override: ReadseekJsonSetting
117
106
  const merged: ReadseekJsonSettings = {};
118
107
  const grep = { ...(base.grep ?? {}), ...(override.grep ?? {}) };
119
108
  if (Object.keys(grep).length > 0) merged.grep = grep;
120
- const mapCache = { ...(base.mapCache ?? {}), ...(override.mapCache ?? {}) };
121
- if (Object.keys(mapCache).length > 0) merged.mapCache = mapCache;
122
109
  const edit = { ...(base.edit ?? {}), ...(override.edit ?? {}) };
123
110
  if (Object.keys(edit).length > 0) merged.edit = edit;
124
111
  return merged;
@@ -100,6 +100,42 @@ export function buildReadseekError(
100
100
  };
101
101
  }
102
102
 
103
+ export interface ToolErrorResult {
104
+ content: [{ type: "text"; text: string }];
105
+ isError: true;
106
+ details: { readseekValue: Record<string, unknown> };
107
+ }
108
+
109
+ /**
110
+ * Build the standard failure envelope shared by every read-family tool: a text
111
+ * content block plus a `readseekValue` carrying `ok: false` and a
112
+ * {@link buildReadseekError} payload.
113
+ *
114
+ * `path` is included only when provided, and `extra` is merged into
115
+ * `readseekValue` so callers can attach tool-specific fields (e.g. write's
116
+ * `lines`/`warnings`).
117
+ */
118
+ export function buildToolErrorResult(
119
+ tool: string,
120
+ code: string,
121
+ message: string,
122
+ opts: { path?: string; hint?: string; details?: unknown; extra?: Record<string, unknown> } = {},
123
+ ): ToolErrorResult {
124
+ return {
125
+ content: [{ type: "text", text: message }],
126
+ isError: true,
127
+ details: {
128
+ readseekValue: {
129
+ tool,
130
+ ...(opts.extra ?? {}),
131
+ ok: false,
132
+ ...(opts.path !== undefined ? { path: opts.path } : {}),
133
+ error: buildReadseekError(code, message, opts.hint, opts.details),
134
+ },
135
+ },
136
+ };
137
+ }
138
+
103
139
  export function buildReadseekEditResult(input: {
104
140
  ok?: boolean;
105
141
  path: string;
package/src/sg.ts CHANGED
@@ -5,12 +5,12 @@ import path from "node:path";
5
5
  import { stat as fsStat } from "node:fs/promises";
6
6
  import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
7
7
  import { escapeControlCharsForDisplay } from "./hashline.js";
8
- import { buildReadseekError, type ReadseekLine } from "./readseek-value.js";
8
+ import { buildToolErrorResult, type ReadseekLine } from "./readseek-value.js";
9
9
  import { resolveToCwd } from "./path-utils.js";
10
10
  import { isReadseekAvailable, readseekSearch, type ReadseekHashline, type ReadseekSearchFileOutput } from "./readseek-client.js";
11
11
  import { buildSgOutput } from "./sg-output.js";
12
12
 
13
- import { clampLineToWidth, clampLinesToWidth, isRendererExpanded, renderToolLabel, summaryLine } from "./tui-render-utils.js";
13
+ import { clampLineToWidth, clampLinesToWidth, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
14
14
 
15
15
  type SgParams = { pattern: string; lang?: string; path?: string; cached?: boolean; others?: boolean; ignored?: boolean };
16
16
 
@@ -47,11 +47,6 @@ export function mergeRanges(ranges: SgRange[]): SgRange[] {
47
47
  const SG_PROMPT_METADATA = defineToolPromptMetadata({
48
48
  promptUrl: new URL("../prompts/sg.md", import.meta.url),
49
49
  promptSnippet: "Search code structurally with readseek and return edit-ready anchors",
50
- promptGuidelines: [
51
- "Use search when text search is too broad or brittle and the query depends on code shape.",
52
- "Use search for calls, imports, declarations, JSX, and similar syntax patterns.",
53
- "Use grep instead of search for plain text search.",
54
- ],
55
50
  });
56
51
 
57
52
  export function isSgAvailable(): boolean {
@@ -128,17 +123,7 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
128
123
  const p = params as SgParams;
129
124
  if (p.ignored && !p.others) {
130
125
  const message = "Error: search parameter 'ignored' requires 'others'";
131
- return {
132
- content: [{ type: "text", text: message }],
133
- isError: true,
134
- details: {
135
- readseekValue: {
136
- tool: "search",
137
- ok: false,
138
- error: buildReadseekError("invalid-parameter", message),
139
- },
140
- },
141
- };
126
+ return buildToolErrorResult("search", "invalid-parameter", message);
142
127
  }
143
128
  const searchPath = resolveToCwd(p.path ?? ".", ctx.cwd);
144
129
  let searchPathIsFile = false;
@@ -149,47 +134,14 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
149
134
  } catch (err: any) {
150
135
  if (err?.code === "ENOENT") {
151
136
  const message = `Error: path '${p.path ?? "."}' does not exist`;
152
- return {
153
- content: [{ type: "text", text: message }],
154
- isError: true,
155
- details: {
156
- readseekValue: {
157
- tool: "search",
158
- ok: false,
159
- path: p.path ?? searchPath,
160
- error: buildReadseekError("path-not-found", message),
161
- },
162
- },
163
- };
137
+ return buildToolErrorResult("search", "path-not-found", message, { path: p.path ?? searchPath });
164
138
  }
165
139
  if (err?.code === "EACCES" || err?.code === "EPERM") {
166
140
  const message = `Error: permission denied for path '${p.path ?? "."}'`;
167
- return {
168
- content: [{ type: "text", text: message }],
169
- isError: true,
170
- details: {
171
- readseekValue: {
172
- tool: "search",
173
- ok: false,
174
- path: p.path ?? searchPath,
175
- error: buildReadseekError("permission-denied", message),
176
- },
177
- },
178
- };
141
+ return buildToolErrorResult("search", "permission-denied", message, { path: p.path ?? searchPath });
179
142
  }
180
143
  const message = `Error: could not access path '${p.path ?? "."}': ${err?.message ?? String(err)}`;
181
- return {
182
- content: [{ type: "text", text: message }],
183
- isError: true,
184
- details: {
185
- readseekValue: {
186
- tool: "search",
187
- ok: false,
188
- path: p.path ?? searchPath,
189
- error: buildReadseekError("fs-error", message, undefined, { fsCode: err?.code, fsMessage: err?.message }),
190
- },
191
- },
192
- };
144
+ return buildToolErrorResult("search", "fs-error", message, { path: p.path ?? searchPath, details: { fsCode: err?.code, fsMessage: err?.message } });
193
145
  }
194
146
 
195
147
  try {
@@ -260,19 +212,12 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
260
212
  } catch (err: any) {
261
213
  const message = String(err?.message || err);
262
214
  const missingReadseek = err?.code === "ENOENT" || /Cannot find package|Cannot find module|no such file/i.test(message);
263
- return {
264
- content: [{ type: "text", text: message }],
265
- isError: true,
266
- details: {
267
- readseekValue: {
268
- tool: "search",
269
- ok: false,
270
- error: missingReadseek
271
- ? buildReadseekError("readseek-not-installed", message, "Run npm install to install @jarkkojs/readseek.")
272
- : buildReadseekError("readseek-execution-error", message),
273
- },
274
- },
275
- };
215
+ return buildToolErrorResult(
216
+ "search",
217
+ missingReadseek ? "readseek-not-installed" : "readseek-execution-error",
218
+ message,
219
+ missingReadseek ? { hint: "Run npm install to install @jarkkojs/readseek." } : {},
220
+ );
276
221
  }
277
222
  },
278
223
  renderCall(args: any, theme: any, ...rest: any[]) {
@@ -285,13 +230,7 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
285
230
  return new Text(clampLineToWidth(text, context.width), 0, 0);
286
231
  },
287
232
  renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
288
- const context: { isPartial?: boolean; isError?: boolean; expanded?: boolean; cwd?: string; width?: number } =
289
- rest[0] ?? options ?? {};
290
- const isPartial = context.isPartial ?? (options as any)?.isPartial ?? false;
291
- const isError = context.isError ?? false;
292
- const expanded = isRendererExpanded(options as any, context as any);
293
- const cwd = context.cwd ?? process.cwd();
294
- const width = (context as any).width ?? (options as any)?.width;
233
+ const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
295
234
 
296
235
  if (isPartial) return new Text(clampLinesToWidth([summaryLine("pending search")], width).join("\n"), 0, 0);
297
236
 
@@ -59,7 +59,6 @@ function promptFileName(promptUrl: URL): string {
59
59
  export function defineToolPromptMetadata(options: {
60
60
  promptUrl: URL;
61
61
  promptSnippet: string;
62
- promptGuidelines: string[];
63
62
  }): ToolPromptMetadata {
64
63
  const prompt = loadPrompt(options.promptUrl);
65
64
  const fileName = promptFileName(options.promptUrl);
@@ -67,6 +66,6 @@ export function defineToolPromptMetadata(options: {
67
66
  return {
68
67
  description: compactDescription ?? firstPromptParagraph(prompt),
69
68
  promptSnippet: options.promptSnippet,
70
- promptGuidelines: COMPACT_GUIDELINES[fileName] ?? options.promptGuidelines,
69
+ promptGuidelines: COMPACT_GUIDELINES[fileName] ?? [],
71
70
  };
72
71
  }