pi-readseek 0.4.25 → 0.4.27
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/README.md +35 -2
- package/index.ts +6 -0
- package/package.json +1 -1
- package/prompts/def.md +24 -0
- package/prompts/hover.md +19 -0
- package/prompts/read.md +1 -1
- package/prompts/rename.md +31 -0
- package/src/def.ts +148 -0
- package/src/edit.ts +4 -14
- package/src/fs-error.ts +27 -24
- package/src/hover.ts +133 -0
- package/src/read.ts +51 -17
- package/src/readseek-client.ts +303 -2
- package/src/readseek-settings.ts +24 -0
- package/src/rename.ts +167 -0
- package/src/stat-search-path.ts +3 -8
- package/src/write.ts +10 -66
package/README.md
CHANGED
|
@@ -24,8 +24,9 @@ npm install --save-dev @jarkkojs/readseek
|
|
|
24
24
|
## Tools
|
|
25
25
|
|
|
26
26
|
- **read** — reads text files with `LINE:HASH` anchors for later `edit` calls;
|
|
27
|
-
images are returned as attachments
|
|
28
|
-
options powered by
|
|
27
|
+
images are returned as attachments and may include local OCR, caption, and
|
|
28
|
+
object text. Supports `symbol`, `map`, and `bundle` options powered by
|
|
29
|
+
`@jarkkojs/readseek`.
|
|
29
30
|
- **edit** — changes existing text files using fresh anchors from `read`,
|
|
30
31
|
`grep`, `search`, or `write`. Variants: `set_line`, `replace_lines`,
|
|
31
32
|
`insert_after`, `replace_symbol`, `replace`. Set `new_text` to `""` to
|
|
@@ -40,6 +41,38 @@ npm install --save-dev @jarkkojs/readseek
|
|
|
40
41
|
- **write** — creates or overwrites whole files and returns anchors for
|
|
41
42
|
immediate follow-up edits.
|
|
42
43
|
|
|
44
|
+
## Settings
|
|
45
|
+
|
|
46
|
+
`pi-readseek` reads optional JSON settings from:
|
|
47
|
+
|
|
48
|
+
| Location | Scope |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `~/.pi/agent/readseek/settings.json` | Global |
|
|
51
|
+
| `.pi/readseek/settings.json` | Project |
|
|
52
|
+
|
|
53
|
+
Project settings override global settings. Image OCR behavior is controlled by
|
|
54
|
+
`read.ocrMode`:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"read": {
|
|
59
|
+
"ocrMode": "on"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Modes:
|
|
65
|
+
|
|
66
|
+
- `"on"` — always run local image OCR/caption/object analysis. This is the
|
|
67
|
+
default.
|
|
68
|
+
- `"off"` — return only the image attachment. Use this as a workaround if the
|
|
69
|
+
local readseek image-analysis path crashes.
|
|
70
|
+
- `"auto"` — run local image analysis only when the active model does not
|
|
71
|
+
support native image input.
|
|
72
|
+
|
|
73
|
+
`READSEEK_READ_OCR_MODE=on|off|auto` overrides the JSON setting for one
|
|
74
|
+
process.
|
|
75
|
+
|
|
43
76
|
## Related
|
|
44
77
|
|
|
45
78
|
- [readseek.vim](https://github.com/jarkkojs/readseek.vim) — Vim 9 plugin
|
package/index.ts
CHANGED
|
@@ -4,7 +4,10 @@ import { registerEditTool } from "./src/edit.js";
|
|
|
4
4
|
import { registerGrepTool } from "./src/grep.js";
|
|
5
5
|
import { registerSgTool } from "./src/sg.js";
|
|
6
6
|
import { registerRefsTool } from "./src/refs.js";
|
|
7
|
+
import { registerRenameTool } from "./src/rename.js";
|
|
8
|
+
import { registerHoverTool } from "./src/hover.js";
|
|
7
9
|
import { registerWriteTool } from "./src/write.js";
|
|
10
|
+
import { registerDefTool } from "./src/def.js";
|
|
8
11
|
import { SessionAnchors } from "./src/session-anchors.js";
|
|
9
12
|
import { isReadSeekAvailable } from "./src/readseek-client.js";
|
|
10
13
|
|
|
@@ -23,5 +26,8 @@ export default function piReadSeekExtension(pi: ExtensionAPI): void {
|
|
|
23
26
|
registerGrepTool(pi, { searchGuideline, onFileAnchored: markAnchored });
|
|
24
27
|
registerSgTool(pi, { onFileAnchored: markAnchored });
|
|
25
28
|
registerRefsTool(pi, { onFileAnchored: markAnchored });
|
|
29
|
+
registerRenameTool(pi);
|
|
30
|
+
registerHoverTool(pi);
|
|
31
|
+
registerDefTool(pi, { onFileAnchored: markAnchored });
|
|
26
32
|
registerWriteTool(pi, { onFileAnchored: markAnchored });
|
|
27
33
|
}
|
package/package.json
CHANGED
package/prompts/def.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
tools: def
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Find structural symbol definitions. Calls `readseek def` which searches
|
|
6
|
+
for the definition of a named symbol across a file or directory.
|
|
7
|
+
|
|
8
|
+
## Parameters
|
|
9
|
+
|
|
10
|
+
- `path` (optional): File or directory to search (default: ".").
|
|
11
|
+
- `name` (optional): Qualified or unqualified symbol name. Required unless
|
|
12
|
+
`fromIdentify` is true.
|
|
13
|
+
- `lang` (optional): Language override.
|
|
14
|
+
- `fromIdentify` (optional): When true, reads identify output from a
|
|
15
|
+
previous identify call to extract the symbol name automatically.
|
|
16
|
+
- `cached` (optional): Search tracked/indexed files in a Git repository.
|
|
17
|
+
- `others` (optional): Search untracked files.
|
|
18
|
+
- `ignored` (optional): Include ignored untracked files.
|
|
19
|
+
|
|
20
|
+
## When to use
|
|
21
|
+
|
|
22
|
+
- After a hover call, to jump to where a symbol is defined.
|
|
23
|
+
- When the user asks "where is X defined?".
|
|
24
|
+
- To find a function/class/type definition by its qualified name.
|
package/prompts/hover.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
tools: hover
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Use hover to identify the identifier and enclosing symbol at a cursor
|
|
6
|
+
position. Calls `readseek identify` with the file content sent via stdin
|
|
7
|
+
so unsaved editor content is included.
|
|
8
|
+
|
|
9
|
+
## Parameters
|
|
10
|
+
|
|
11
|
+
- `path` (required): File path.
|
|
12
|
+
- `line` (required): One-based cursor line.
|
|
13
|
+
- `column` (optional): One-based cursor byte column.
|
|
14
|
+
|
|
15
|
+
## When to use
|
|
16
|
+
|
|
17
|
+
- Before a rename, to confirm the identifier under the cursor.
|
|
18
|
+
- Before a go-to-definition, to get the qualified symbol name.
|
|
19
|
+
- To inspect what symbol a specific line belongs to.
|
package/prompts/read.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Read files through readseek. Text output uses `LINE:HASH|content` anchors that can be copied directly into `edit`; supported images return attachments
|
|
1
|
+
Read files through readseek. Text output uses `LINE:HASH|content` anchors that can be copied directly into `edit`; supported images return attachments and may append OCR/caption/object text depending on `read.ocrMode`. Default cap: {{DEFAULT_MAX_LINES}} lines or {{DEFAULT_MAX_BYTES}}.
|
|
2
2
|
|
|
3
3
|
## Choose the right read
|
|
4
4
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
tools: rename
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Use rename to rename an identifier (variable, function, type, etc.) with
|
|
6
|
+
binding accuracy. The tool calls `readseek rename` which resolves the
|
|
7
|
+
lexical binding under the cursor and renames every occurrence that binds
|
|
8
|
+
to the same declaration.
|
|
9
|
+
|
|
10
|
+
## Parameters
|
|
11
|
+
|
|
12
|
+
- `path` (required): File holding the binding to rename (a single regular file).
|
|
13
|
+
- `line` (required): One-based cursor line of the binding to rename.
|
|
14
|
+
- `column` (optional): One-based cursor byte column of the binding.
|
|
15
|
+
- `to` (required): New name for the binding. Must be a plain identifier.
|
|
16
|
+
- `workspace` (optional): When true, expands the rename across the project
|
|
17
|
+
root. The cursor file remains binding-accurate; other files are matched
|
|
18
|
+
by name (free uses only, local shadows excluded).
|
|
19
|
+
- `apply` (optional, default true): When true, writes the edits to disk
|
|
20
|
+
after verifying line hashes. When false, returns the plan only.
|
|
21
|
+
|
|
22
|
+
## When to use
|
|
23
|
+
|
|
24
|
+
- The user asks to rename a symbol (function, variable, class, etc.).
|
|
25
|
+
- The cursor position (line, optionally column) is known from an identify
|
|
26
|
+
call or from the user's editor context.
|
|
27
|
+
|
|
28
|
+
## When not to use
|
|
29
|
+
|
|
30
|
+
- For search-and-replace across files, use grep or sg.
|
|
31
|
+
- For refactoring that requires adding/removing parameters, use edit.
|
package/src/def.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { ExtensionAPI, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
6
|
+
import { buildReadSeekLineWithHash, buildToolErrorResult } from "./readseek-value.js";
|
|
7
|
+
import { resolveToCwd } from "./path-utils.js";
|
|
8
|
+
import { statSearchPathOrError } from "./stat-search-path.js";
|
|
9
|
+
import { classifyReadSeekFailure, readseekDef } from "./readseek-client.js";
|
|
10
|
+
import { searchPathParam, langParam, readseekGitSearchParams } from "./readseek-params.js";
|
|
11
|
+
import { registerReadSeekTool } from "./register-tool.js";
|
|
12
|
+
|
|
13
|
+
import { renderAnchoredFilesResult, renderReadSeekSearchCall } from "./tui-render-utils.js";
|
|
14
|
+
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
15
|
+
|
|
16
|
+
const DEF_PROMPT_METADATA = defineToolPromptMetadata({
|
|
17
|
+
promptUrl: new URL("../prompts/def.md", import.meta.url),
|
|
18
|
+
promptSnippet: "Find structural symbol definitions with readseek",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
type DefParams = {
|
|
22
|
+
name?: string;
|
|
23
|
+
path?: string;
|
|
24
|
+
lang?: string;
|
|
25
|
+
fromIdentify?: boolean;
|
|
26
|
+
cached?: boolean;
|
|
27
|
+
others?: boolean;
|
|
28
|
+
ignored?: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
interface DefToolOptions {
|
|
32
|
+
onFileAnchored?: FileAnchoredCallback;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ExecuteDefOptions {
|
|
36
|
+
params: unknown;
|
|
37
|
+
signal: AbortSignal | undefined;
|
|
38
|
+
cwd: string;
|
|
39
|
+
onFileAnchored?: FileAnchoredCallback;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function executeDef(opts: ExecuteDefOptions): Promise<any> {
|
|
43
|
+
const { params, signal, cwd, onFileAnchored } = opts;
|
|
44
|
+
const p = params as DefParams;
|
|
45
|
+
|
|
46
|
+
if (!p.fromIdentify && (!p.name || !p.name.trim())) {
|
|
47
|
+
return buildToolErrorResult("def", "invalid-parameter", "def requires 'name' or 'fromIdentify'");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const searchPath = resolveToCwd(p.path ?? ".", cwd);
|
|
51
|
+
|
|
52
|
+
const statResult = await statSearchPathOrError("def", p.path, searchPath);
|
|
53
|
+
if (!statResult.ok) return statResult.error;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const definitions = await readseekDef(searchPath, {
|
|
57
|
+
name: p.name,
|
|
58
|
+
fromIdentify: p.fromIdentify,
|
|
59
|
+
language: p.lang,
|
|
60
|
+
cached: p.cached,
|
|
61
|
+
others: p.others,
|
|
62
|
+
ignored: p.ignored,
|
|
63
|
+
signal,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (definitions.length === 0) {
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text", text: "no definitions found" }],
|
|
69
|
+
details: {
|
|
70
|
+
readseekValue: { tool: "def", ok: true, path: searchPath, definitions: [] },
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const files = new Map<string, { displayPath: string; path: string; lines: ReturnType<typeof buildReadSeekLineWithHash>[] }>();
|
|
76
|
+
for (const def of definitions) {
|
|
77
|
+
const abs = path.isAbsolute(def.file) ? def.file : path.resolve(cwd, def.file);
|
|
78
|
+
let file = files.get(abs);
|
|
79
|
+
if (!file) {
|
|
80
|
+
file = { displayPath: path.relative(cwd, abs) || abs, path: abs, lines: [] };
|
|
81
|
+
files.set(abs, file);
|
|
82
|
+
}
|
|
83
|
+
file.lines.push(buildReadSeekLineWithHash(def.line, def.line_hash, def.text));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const fileList = [...files.values()];
|
|
87
|
+
for (const file of fileList) {
|
|
88
|
+
onFileAnchored?.(file.path);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const textParts: string[] = [];
|
|
92
|
+
for (const file of fileList) {
|
|
93
|
+
textParts.push(file.displayPath);
|
|
94
|
+
for (const line of file.lines) {
|
|
95
|
+
textParts.push(` ${line.line}:${line.hash} ${line.display}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: "text", text: textParts.join("\n") }],
|
|
101
|
+
details: {
|
|
102
|
+
readseekValue: { tool: "def", ok: true, path: searchPath, definitions },
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
} catch (err: any) {
|
|
106
|
+
const failure = classifyReadSeekFailure(err);
|
|
107
|
+
return buildToolErrorResult("def", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function registerDefTool(pi: ExtensionAPI, options: DefToolOptions = {}) {
|
|
112
|
+
registerReadSeekTool(pi, {
|
|
113
|
+
policy: "read-only",
|
|
114
|
+
pythonName: "def",
|
|
115
|
+
defaultExposure: "opt-in",
|
|
116
|
+
}, {
|
|
117
|
+
name: "def",
|
|
118
|
+
label: "Definition",
|
|
119
|
+
description: DEF_PROMPT_METADATA.description,
|
|
120
|
+
promptSnippet: DEF_PROMPT_METADATA.promptSnippet,
|
|
121
|
+
promptGuidelines: DEF_PROMPT_METADATA.promptGuidelines,
|
|
122
|
+
parameters: Type.Object({
|
|
123
|
+
name: Type.Optional(Type.String({ description: "Qualified or unqualified symbol name" })),
|
|
124
|
+
path: searchPathParam(),
|
|
125
|
+
lang: langParam(),
|
|
126
|
+
fromIdentify: Type.Optional(Type.Boolean({ description: "Read identify output from stdin to choose the symbol name" })),
|
|
127
|
+
...readseekGitSearchParams(),
|
|
128
|
+
}),
|
|
129
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
130
|
+
return executeDef({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
|
|
131
|
+
},
|
|
132
|
+
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
133
|
+
return renderReadSeekSearchCall(args, theme, rest, {
|
|
134
|
+
label: "def",
|
|
135
|
+
accent: args.name,
|
|
136
|
+
flags: [args.fromIdentify && "from-identify", args.cached && "cached", args.others && "others", args.ignored && "ignored"],
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
140
|
+
return renderAnchoredFilesResult(result, options, theme, rest, {
|
|
141
|
+
pendingLabel: "pending def",
|
|
142
|
+
emptyLabel: "no definitions",
|
|
143
|
+
unitSingular: "definition",
|
|
144
|
+
unitPlural: "definitions",
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
package/src/edit.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { HashlineMismatchError, applyHashlineEdits, computeLineHash, ensureHashI
|
|
|
12
12
|
import { resolveToCwd } from "./path-utils.js";
|
|
13
13
|
import { looksLikeBinary } from "./binary-detect.js";
|
|
14
14
|
import { throwIfAborted } from "./runtime.js";
|
|
15
|
-
import {
|
|
15
|
+
import { formatFsError } from "./fs-error.js";
|
|
16
16
|
import { buildEditOutput } from "./edit-output.js";
|
|
17
17
|
import { classifyEdit, isDifftAvailable, runDifftastic } from "./edit-classify.js";
|
|
18
18
|
import type { SemanticSummary } from "./readseek-value.js";
|
|
@@ -104,19 +104,9 @@ function buildEditError(
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
function mapEditFileError(err: any, filePath: string,
|
|
108
|
-
const code = err
|
|
109
|
-
|
|
110
|
-
return buildEditError(filePath, "file-not-found", `Failed to write file: ${displayPath}`);
|
|
111
|
-
}
|
|
112
|
-
const fsError = classifyFsError(err, displayPath);
|
|
113
|
-
if (fsError) {
|
|
114
|
-
return buildEditError(filePath, fsError.code, fsError.message, fsError.hint);
|
|
115
|
-
}
|
|
116
|
-
const prefix = phase === "write" ? "Failed to write file" : "File not readable";
|
|
117
|
-
const message = `${prefix}: ${displayPath}${err?.message ? ` — ${err.message}` : ""}`;
|
|
118
|
-
return buildEditError(filePath, "fs-error", message, undefined,
|
|
119
|
-
{ fsCode: code, fsMessage: err?.message });
|
|
107
|
+
function mapEditFileError(err: any, filePath: string, _displayPath: string, _phase: "read" | "write"): ReturnType<typeof buildEditError> {
|
|
108
|
+
const { code, message } = formatFsError(err, "edit-error");
|
|
109
|
+
return buildEditError(filePath, code, message);
|
|
120
110
|
}
|
|
121
111
|
|
|
122
112
|
export interface EditToolOptions {
|
package/src/fs-error.ts
CHANGED
|
@@ -1,30 +1,33 @@
|
|
|
1
|
-
|
|
1
|
+
const SYSLOG_PATH_SUFFIX = /,\s+[a-z]+(\s+'.*')?$/;
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Derive the canonical OS strerror from a Node {@link err.message} by
|
|
5
|
+
* stripping the trailing syscall and optional path suffix so the result
|
|
6
|
+
* is portable and never duplicates information already carried in the
|
|
7
|
+
* error-envelope `path` field.
|
|
8
|
+
*/
|
|
9
|
+
function strerror(err: { message?: unknown }): string {
|
|
10
|
+
const raw = String(err?.message ?? String(err));
|
|
11
|
+
return raw.replace(SYSLOG_PATH_SUFFIX, "");
|
|
7
12
|
}
|
|
8
13
|
|
|
9
14
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
15
|
+
* Build a readseek error from a Node `fs` errno exception.
|
|
16
|
+
*
|
|
17
|
+
* Every errno maps to its own canonical code automatically — no
|
|
18
|
+
* hand-curated switch, no `"fs-error"` catch-all.
|
|
19
|
+
*
|
|
20
|
+
* @param err The caught exception (carries `err.code` and `err.message`).
|
|
21
|
+
* @param domain Tool-level prefix for the message: `"read-error"`,
|
|
22
|
+
* `"edit-error"`, `"write-error"`, `"stat-error"`.
|
|
13
23
|
*/
|
|
14
|
-
export function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
case "EPERM":
|
|
24
|
-
return { code: "permission-denied", message: `Permission denied: ${path}` };
|
|
25
|
-
case "ENOENT":
|
|
26
|
-
return { code: "file-not-found", message: `File not found: ${path}` };
|
|
27
|
-
default:
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
24
|
+
export function formatFsError(
|
|
25
|
+
err: { code?: unknown; message?: string },
|
|
26
|
+
domain: string,
|
|
27
|
+
): { code: string; message: string } {
|
|
28
|
+
const errno = typeof err.code === "string" ? err.code : "EIO";
|
|
29
|
+
return {
|
|
30
|
+
code: errno,
|
|
31
|
+
message: `${domain}: ${errno}: ${strerror(err)}`,
|
|
32
|
+
};
|
|
30
33
|
}
|
package/src/hover.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import type { ExtensionAPI, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
6
|
+
|
|
7
|
+
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
8
|
+
import { buildToolErrorResult } from "./readseek-value.js";
|
|
9
|
+
import { resolveToCwd } from "./path-utils.js";
|
|
10
|
+
import { formatFsError } from "./fs-error.js";
|
|
11
|
+
import { classifyReadSeekFailure, readseekIdentify } from "./readseek-client.js";
|
|
12
|
+
import { filePathParam, registerReadSeekTool } from "./register-tool.js";
|
|
13
|
+
|
|
14
|
+
import { clampLinesToWidth, linkToolPath, renderPendingResult, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
15
|
+
|
|
16
|
+
const HOVER_PROMPT_METADATA = defineToolPromptMetadata({
|
|
17
|
+
promptUrl: new URL("../prompts/hover.md", import.meta.url),
|
|
18
|
+
promptSnippet: "Identify the identifier and enclosing symbol at a cursor position",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const hoverSchema = Type.Object({
|
|
22
|
+
path: filePathParam(),
|
|
23
|
+
line: Type.Number({ description: "One-based cursor line" }),
|
|
24
|
+
column: Type.Optional(Type.Number({ description: "One-based cursor byte column" })),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
interface HoverParams {
|
|
28
|
+
path: string;
|
|
29
|
+
line: number;
|
|
30
|
+
column?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ExecuteHoverOptions {
|
|
34
|
+
params: unknown;
|
|
35
|
+
signal: AbortSignal | undefined;
|
|
36
|
+
cwd: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function executeHover(opts: ExecuteHoverOptions): Promise<any> {
|
|
40
|
+
const { params, signal, cwd } = opts;
|
|
41
|
+
const p = params as HoverParams;
|
|
42
|
+
|
|
43
|
+
if (!Number.isSafeInteger(p.line) || p.line < 1) {
|
|
44
|
+
return buildToolErrorResult("hover", "invalid-parameter", "hover parameter 'line' must be a positive integer");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const filePath = resolveToCwd(p.path, cwd);
|
|
48
|
+
|
|
49
|
+
let content: string;
|
|
50
|
+
try {
|
|
51
|
+
content = await readFile(filePath, "utf-8");
|
|
52
|
+
} catch (err: any) {
|
|
53
|
+
const { code, message } = formatFsError(err, "hover-error");
|
|
54
|
+
return buildToolErrorResult("hover", code, message, { path: p.path });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const output = await readseekIdentify(filePath, content, {
|
|
59
|
+
line: p.line,
|
|
60
|
+
column: p.column,
|
|
61
|
+
signal,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const lines: string[] = [];
|
|
65
|
+
if (output.identifier) {
|
|
66
|
+
lines.push(`identifier: ${output.identifier.text}`);
|
|
67
|
+
}
|
|
68
|
+
if (output.symbol) {
|
|
69
|
+
lines.push(`symbol: ${output.symbol.name}`);
|
|
70
|
+
lines.push(`kind: ${output.symbol.kind}`);
|
|
71
|
+
lines.push(`qualified: ${output.symbol.qualified_name}`);
|
|
72
|
+
}
|
|
73
|
+
lines.push(`file: ${output.file}`);
|
|
74
|
+
lines.push(`language: ${output.language}`);
|
|
75
|
+
lines.push(`location: ${output.line}:${output.column}`);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
79
|
+
details: {
|
|
80
|
+
readseekValue: {
|
|
81
|
+
tool: "hover",
|
|
82
|
+
ok: true,
|
|
83
|
+
path: filePath,
|
|
84
|
+
output,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
} catch (err: any) {
|
|
89
|
+
const failure = classifyReadSeekFailure(err);
|
|
90
|
+
return buildToolErrorResult("hover", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function registerHoverTool(pi: ExtensionAPI) {
|
|
95
|
+
registerReadSeekTool(pi, {
|
|
96
|
+
policy: "read-only",
|
|
97
|
+
pythonName: "hover",
|
|
98
|
+
defaultExposure: "opt-in",
|
|
99
|
+
}, {
|
|
100
|
+
name: "hover",
|
|
101
|
+
label: "Hover",
|
|
102
|
+
description: HOVER_PROMPT_METADATA.description,
|
|
103
|
+
promptSnippet: HOVER_PROMPT_METADATA.promptSnippet,
|
|
104
|
+
promptGuidelines: HOVER_PROMPT_METADATA.promptGuidelines,
|
|
105
|
+
parameters: hoverSchema,
|
|
106
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
107
|
+
return executeHover({ params, signal, cwd: ctx.cwd });
|
|
108
|
+
},
|
|
109
|
+
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
110
|
+
const context = rest[0] ?? {};
|
|
111
|
+
const cwd = context.cwd ?? process.cwd();
|
|
112
|
+
const displayPath = typeof args?.path === "string" ? args.path : "?";
|
|
113
|
+
let text = theme.fg("toolTitle", theme.bold("hover"));
|
|
114
|
+
text += ` ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
|
|
115
|
+
if (args?.line) text += theme.fg("dim", `:${args.line}`);
|
|
116
|
+
return text;
|
|
117
|
+
},
|
|
118
|
+
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
119
|
+
const { isPartial, isError, width } = resolveRenderResultContext(options, rest);
|
|
120
|
+
|
|
121
|
+
if (isPartial) return renderPendingResult("pending hover", width);
|
|
122
|
+
|
|
123
|
+
const content = result.content?.[0];
|
|
124
|
+
const textContent = content?.type === "text" ? content.text : "";
|
|
125
|
+
|
|
126
|
+
if (isError || result.isError) {
|
|
127
|
+
return new Text(textContent || "hover failed", 0, 0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return new Text(textContent.split("\n")[0] || "", 0, 0);
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
package/src/read.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { buildReadSeekWarning, buildToolErrorResult, renderReadSeekLines, type R
|
|
|
17
17
|
import { looksLikeBinary } from "./binary-detect.js";
|
|
18
18
|
import { resolveToCwd } from "./path-utils.js";
|
|
19
19
|
import { throwIfAborted } from "./runtime.js";
|
|
20
|
-
import {
|
|
20
|
+
import { formatFsError } from "./fs-error.js";
|
|
21
21
|
import { getOrGenerateMap } from "./file-map.js";
|
|
22
22
|
import { formatFileMapWithBudget } from "./readseek/formatter.js";
|
|
23
23
|
import { findSymbol, type SymbolMatch } from "./readseek/symbol-lookup.js";
|
|
@@ -28,6 +28,7 @@ import { buildLocalBundle } from "./read-local-bundle.js";
|
|
|
28
28
|
import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
|
|
29
29
|
import { readseekRead, readseekDetect, type ReadSeekDetection } from "./readseek-client.js";
|
|
30
30
|
import { formatReadCallText, formatReadResultText } from "./read-render-helpers.js";
|
|
31
|
+
import { resolveReadSeekOcrMode } from "./readseek-settings.js";
|
|
31
32
|
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine, wrapReadHashlinesForWidth } from "./tui-render-utils.js";
|
|
32
33
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
33
34
|
import { filePathParam, mapParam, optionalIntOrString, registerReadSeekTool } from "./register-tool.js";
|
|
@@ -57,6 +58,8 @@ export interface ExecuteReadOptions {
|
|
|
57
58
|
onUpdate: any;
|
|
58
59
|
cwd: string;
|
|
59
60
|
onSuccessfulRead?: FileAnchoredCallback;
|
|
61
|
+
/** Whether the active model accepts image input natively. Used when OCR mode is auto. */
|
|
62
|
+
modelSupportsImages?: boolean;
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
function hasReadAnchors(result: AgentToolResult<any>): boolean {
|
|
@@ -68,6 +71,20 @@ function hasReadAnchors(result: AgentToolResult<any>): boolean {
|
|
|
68
71
|
return Array.isArray(lines) && lines.length > 0;
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
function formatImageAnalysis(detection: ReadSeekDetection): string | undefined {
|
|
75
|
+
if (detection.kind !== "image") return undefined;
|
|
76
|
+
const sections: string[] = [];
|
|
77
|
+
const transcript = detection.transcribe?.text?.trim();
|
|
78
|
+
if (transcript) sections.push(`Transcribed text from image:\n${transcript}`);
|
|
79
|
+
const caption = detection.caption?.trim();
|
|
80
|
+
if (caption) sections.push(`Image caption:\n${caption}`);
|
|
81
|
+
if (detection.objects?.length) {
|
|
82
|
+
const lines = detection.objects.map((object) => `- ${object.label} [${object.bbox.join(", ")}]`);
|
|
83
|
+
sections.push(`Detected objects:\n${lines.join("\n")}`);
|
|
84
|
+
}
|
|
85
|
+
return sections.length > 0 ? sections.join("\n\n") : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
71
88
|
export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolResult<any>> {
|
|
72
89
|
const { toolCallId, params, signal, onUpdate, cwd, onSuccessfulRead } = opts;
|
|
73
90
|
await ensureHashInit();
|
|
@@ -144,37 +161,53 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
144
161
|
try {
|
|
145
162
|
rawBuffer = await fsReadFile(absolutePath);
|
|
146
163
|
} catch (err: any) {
|
|
147
|
-
const code = err
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
path: rawParams.path,
|
|
152
|
-
...(fsError.hint ? { hint: fsError.hint } : {}),
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
const message = `File not readable: ${rawPath}${err?.message ? ` — ${err.message}` : ""}`;
|
|
156
|
-
return buildToolErrorResult("read", "fs-error", message, { path: rawParams.path, details: { fsCode: code, fsMessage: err?.message } });
|
|
164
|
+
const { code, message } = formatFsError(err, "read-error");
|
|
165
|
+
return buildToolErrorResult("read", code, message, {
|
|
166
|
+
path: rawParams.path,
|
|
167
|
+
});
|
|
157
168
|
}
|
|
158
169
|
|
|
159
170
|
const hasBinaryContent = looksLikeBinary(rawBuffer);
|
|
160
171
|
if (hasBinaryContent) {
|
|
161
|
-
// Images are always binary; classify and transcribe in a single readseek detect call.
|
|
162
172
|
let detection: ReadSeekDetection | undefined;
|
|
163
173
|
try {
|
|
164
|
-
detection = await readseekDetect(absolutePath, {
|
|
174
|
+
detection = await readseekDetect(absolutePath, { signal });
|
|
165
175
|
} catch {
|
|
166
|
-
// detect unavailable — fall through to binary-as-text handling below
|
|
167
176
|
}
|
|
168
177
|
if (detection?.kind === "image") {
|
|
169
178
|
const builtinRead = createReadTool(cwd);
|
|
170
179
|
const builtinResult = await builtinRead.execute(toolCallId, p, signal, onUpdate);
|
|
171
|
-
const
|
|
172
|
-
|
|
180
|
+
const ocrMode = resolveReadSeekOcrMode();
|
|
181
|
+
const shouldTranscribe = ocrMode === "on" || (ocrMode === "auto" && !opts.modelSupportsImages);
|
|
182
|
+
if (!shouldTranscribe) return succeed(builtinResult);
|
|
183
|
+
|
|
184
|
+
let ocrFailed = false;
|
|
185
|
+
try {
|
|
186
|
+
const ocrDetection = await readseekDetect(absolutePath, {
|
|
187
|
+
transcribe: true,
|
|
188
|
+
caption: true,
|
|
189
|
+
objects: true,
|
|
190
|
+
signal,
|
|
191
|
+
});
|
|
192
|
+
const imageAnalysis = formatImageAnalysis(ocrDetection);
|
|
193
|
+
if (imageAnalysis) {
|
|
194
|
+
return succeed({
|
|
195
|
+
...builtinResult,
|
|
196
|
+
content: [
|
|
197
|
+
...(builtinResult.content ?? []),
|
|
198
|
+
{ type: "text" as const, text: imageAnalysis },
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
ocrFailed = true;
|
|
204
|
+
}
|
|
205
|
+
if (ocrFailed) {
|
|
173
206
|
return succeed({
|
|
174
207
|
...builtinResult,
|
|
175
208
|
content: [
|
|
176
209
|
...(builtinResult.content ?? []),
|
|
177
|
-
{ type: "text" as const, text:
|
|
210
|
+
{ type: "text" as const, text: "[Warning: local image analysis (OCR) unavailable — showing image attachment only]" },
|
|
178
211
|
],
|
|
179
212
|
});
|
|
180
213
|
}
|
|
@@ -465,6 +498,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
465
498
|
onUpdate,
|
|
466
499
|
cwd: ctx.cwd,
|
|
467
500
|
onSuccessfulRead: options.onSuccessfulRead,
|
|
501
|
+
modelSupportsImages: ctx.model?.input.includes("image") ?? false,
|
|
468
502
|
});
|
|
469
503
|
},
|
|
470
504
|
renderCall(args: any, theme: any, ...rest: any[]) {
|