@khanhicetea/pi-better-tool 0.2.4 → 0.2.5

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  Context-aware tools for the [Pi coding agent](https://github.com/earendil-works/pi-mono):
4
4
 
5
5
  - **`edit`** replaces the built-in edit tool. Failures explain what failed, what was not written, and what to do next.
6
- - **`read_symbol`** reads a whole function, method, class, or type by containing line or exact name. With only a path, it gets a symbol outline.
6
+ - **`read_symbol`** reads a whole function, method, class, or type by containing line or exact name. Its required `mode` makes symbol reads and outline listings explicit.
7
7
  - **`read_code_imports`** reads the first dependency import zone as exact raw source lines, including comments and blank lines between adjacent imports.
8
8
  - **`read` stays unchanged** for ordinary text, images, and explicit line ranges.
9
9
 
@@ -26,25 +26,25 @@ The root `package.json` also registers the extension. After local changes, use `
26
26
  After grep identifies a location:
27
27
 
28
28
  ```json
29
- {"path":"src/server.ts","line":142}
29
+ {"path":"src/server.ts","mode":"symbol","line":142}
30
30
  ```
31
31
 
32
32
  Call `read_symbol` with an exact name instead:
33
33
 
34
34
  ```json
35
- {"path":"src/server.ts","symbol":"Server.handleRequest"}
35
+ {"path":"src/server.ts","mode":"symbol","symbol":"Server.handleRequest"}
36
36
  ```
37
37
 
38
38
  Get the enclosing class or function:
39
39
 
40
40
  ```json
41
- {"path":"src/server.ts","line":142,"parent":1}
41
+ {"path":"src/server.ts","mode":"symbol","line":142,"parent":1}
42
42
  ```
43
43
 
44
44
  Discover names and ranges without reading every body:
45
45
 
46
46
  ```json
47
- {"path":"src/server.ts"}
47
+ {"path":"src/server.ts","mode":"outline"}
48
48
  ```
49
49
 
50
50
  ### Arguments
@@ -52,6 +52,7 @@ Discover names and ranges without reading every body:
52
52
  | Argument | Meaning |
53
53
  | --- | --- |
54
54
  | `path` | Local relative/absolute path; supports `@path`, `~/path`, and file URLs. |
55
+ | `mode` | Required: `symbol` reads one declaration and requires `symbol` or `line`; `outline` only lists names and ranges. |
55
56
  | `line` | 1-based file line. Select the innermost declaration containing it. |
56
57
  | `column` | Optional 1-based UTF-16 column with `line`, to distinguish same-line symbols. |
57
58
  | `symbol` | Exact, case-sensitive name or qualified name such as `Server.run`. Combine with `line` for duplicate names. |
@@ -60,7 +61,7 @@ Discover names and ranges without reading every body:
60
61
  | `offset` | 1-based position **within the selection**, not a file line. For an outline, the entry position. |
61
62
  | `limit` | Maximum source lines (default 1000) or outline entries (default 50); maximum 1800. |
62
63
 
63
- Omit both `line` and `symbol` for an outline. Named selection never silently chooses the first duplicate. Line selection never silently chooses between same-line siblings. Candidate lists include concrete calls with names and positions.
64
+ Set `mode` to `outline` for an outline. Named selection never silently chooses the first duplicate. Line selection never silently chooses between same-line siblings. Candidate lists include concrete calls with names and positions.
64
65
 
65
66
  ### Languages and boundaries
66
67
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanhicetea/pi-better-tool",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Context-aware tools for pi: safe edits, syntax-aware symbol reads, and raw source import-zone reads",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -147,7 +147,7 @@ export function formatEditFailure(opts: FormatFailureOptions): string {
147
147
  function nextContextCall(path: string, start: number, end = start): string {
148
148
  const read = `read ${JSON.stringify({ path, offset: Math.max(1, start - 3), limit: Math.min(1800, end - start + 7) })}`;
149
149
  return languageForPath(path)
150
- ? `Next context call: read_symbol ${JSON.stringify({ path, line: start })} for the enclosing symbol; or ${read} for exact line context.`
150
+ ? `Next context call: read_symbol ${JSON.stringify({ path, mode: "symbol", line: start })} for the enclosing symbol; or ${read} for exact line context.`
151
151
  : `Next context call: ${read}.`;
152
152
  }
153
153
 
@@ -456,7 +456,7 @@ function formatNotFound(opts: FormatFailureOptions, oldText: string): string {
456
456
  } else {
457
457
  lines.push("No reliable similar region was found within the bounded diagnostic search.");
458
458
  lines.push("If you expected this text to exist, read the file around the expected location and retry.");
459
- lines.push(languageForPath(opts.path) ? `Next context call: read_symbol ${JSON.stringify({ path: opts.path })} to locate the intended symbol without guessing line ranges.` : nextContextCall(opts.path, 1));
459
+ lines.push(languageForPath(opts.path) ? `Next context call: read_symbol ${JSON.stringify({ path: opts.path, mode: "outline" })} to locate the intended symbol without guessing line ranges.` : nextContextCall(opts.path, 1));
460
460
  }
461
461
 
462
462
  if (causes.length > 0) {
@@ -9,15 +9,18 @@ import { getLineSpans } from "./text.ts";
9
9
 
10
10
  export const readSymbolSchema = Type.Object({
11
11
  path: Type.String({ minLength: 1, maxLength: 4096, description: "Local source file path (relative, absolute, @path, ~/path, or file URL)." }),
12
- line: Type.Optional(Type.Integer({ minimum: 1, description: "Read the innermost symbol containing this 1-based line. Combine with symbol to disambiguate duplicate names." })),
12
+ mode: Type.Union([Type.Literal("symbol"), Type.Literal("outline")], { description: 'Required operation. Use "symbol" to read one declaration (and provide symbol or line); use "outline" only to list declaration names and ranges.' }),
13
+ line: Type.Optional(Type.Integer({ minimum: 1, description: 'With mode="symbol", read the innermost symbol containing this 1-based line. Combine with symbol to disambiguate duplicate names.' })),
13
14
  column: Type.Optional(Type.Integer({ minimum: 1, description: "Optional 1-based UTF-16 column with line, to distinguish symbols on the same line." })),
14
- symbol: Type.Optional(Type.String({ minLength: 1, maxLength: 256, description: "Exact symbol name or qualified name, for example Server.run. Omit both symbol and line to list a symbol outline." })),
15
+ symbol: Type.Optional(Type.String({ minLength: 1, maxLength: 256, description: 'With mode="symbol", the exact symbol name or qualified name, for example Server.run. A symbol or line selector is required in symbol mode.' })),
15
16
  parent: Type.Optional(Type.Integer({ minimum: 0, maximum: 20, description: "Move outward this many enclosing symbols after selection (default 0)." })),
16
- context: Type.Optional(Type.Integer({ minimum: 0, maximum: 20, description: "Extra whole lines before and after the selected symbol (default 0)." })),
17
+ context: Type.Optional(Type.Integer({ minimum: 0, maximum: 20, description: "Extra whole lines before and after the selected symbol (default 0, max 20)." })),
17
18
  offset: Type.Optional(Type.Integer({ minimum: 1, description: "1-based position within the selected symbol/context or outline, not a file line. Use the exact continuation call from a partial result." })),
18
19
  limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 1800, description: "Maximum source lines (default 1000) or outline entries (default 50). Output also has a 48 KiB hard limit." })),
19
20
  });
20
- export type ReadSymbolInput = Static<typeof readSymbolSchema>;
21
+ type RegisteredReadSymbolInput = Static<typeof readSymbolSchema>;
22
+ /** mode stays optional here so stored calls from releases before explicit modes remain verifiable. */
23
+ export type ReadSymbolInput = Omit<RegisteredReadSymbolInput, "mode"> & { mode?: RegisteredReadSymbolInput["mode"] };
21
24
 
22
25
  export interface VisibleSource {
23
26
  startLine: number;
@@ -48,12 +51,15 @@ export function validateReadSymbolInput(input: ReadSymbolInput): void {
48
51
  const value = input[key];
49
52
  if (value !== undefined && (!Number.isSafeInteger(value) || value < min || value > max)) throw new Error(`read_symbol ${key} must be an integer from ${min} to ${max}.`);
50
53
  }
54
+ if (input.mode !== undefined && input.mode !== "symbol" && input.mode !== "outline") throw new Error('read_symbol mode must be "symbol" or "outline".');
51
55
  if (input.column !== undefined && input.line === undefined) throw new Error("read_symbol column requires line.");
52
- if ((input.parent || input.context) && input.line === undefined && input.symbol === undefined) throw new Error("read_symbol parent/context requires a line or symbol selector; omit them to get an outline.");
56
+ if (input.mode === "symbol" && input.line === undefined && input.symbol === undefined) throw new Error('read_symbol mode="symbol" requires a symbol name or containing line.');
57
+ if (input.mode === "outline" && (input.line !== undefined || input.symbol !== undefined || input.parent !== undefined || input.context !== undefined)) throw new Error('read_symbol mode="outline" does not accept symbol, line, parent, or context.');
58
+ if ((input.parent || input.context) && input.line === undefined && input.symbol === undefined) throw new Error("read_symbol parent/context requires a line or symbol selector.");
53
59
  }
54
60
 
55
61
  function candidateLine(path: string, symbol: SourceSymbol): string {
56
- return `- ${label(symbol.qualifiedName)} (${symbol.kind}), lines ${symbol.startLine}-${symbol.endLine}. ${callText({ path, line: symbol.startLine, column: symbol.startColumn, symbol: symbol.qualifiedName.length <= 256 ? symbol.qualifiedName : undefined })}`;
62
+ return `- ${label(symbol.qualifiedName)} (${symbol.kind}), lines ${symbol.startLine}-${symbol.endLine}. ${callText({ path, mode: "symbol", line: symbol.startLine, column: symbol.startColumn, symbol: symbol.qualifiedName.length <= 256 ? symbol.qualifiedName : undefined })}`;
57
63
  }
58
64
 
59
65
  /** Non-copyable preview for failed selection; never presented as a complete symbol. */
@@ -99,7 +105,7 @@ function selectSymbol(input: ReadSymbolInput, symbols: SourceSymbol[]): SourceSy
99
105
  matches.length ? `Ambiguous symbol selection: ${matches.length} candidates. No symbol was selected.` : "No matching symbol. Names are exact and case-sensitive; no nearby symbol was selected automatically.",
100
106
  ...candidates.slice(0, 8).map((symbol) => candidateLine(input.path, symbol)),
101
107
  ...(candidates.length > 8 ? [`${candidates.length - 8} more candidates omitted.`] : []),
102
- `Next: choose a candidate call above, or list the outline with ${callText({ path: input.path })}.`,
108
+ `Next: choose a candidate call above, or list the outline with ${callText({ path: input.path, mode: "outline" })}.`,
103
109
  ].join("\n"));
104
110
  }
105
111
  let selected = matches[0];
@@ -133,7 +139,7 @@ export async function buildSymbolRead(input: ReadSymbolInput, content: string, s
133
139
  const snapshot = createHash("sha256").update(content).digest("hex");
134
140
  const header = `Source ${label(input.path)} (${lines.length} lines). Snapshot sha256:${snapshot}`;
135
141
  const offset = input.offset ?? 1;
136
- if (input.line === undefined && input.symbol === undefined) {
142
+ if (input.mode === "outline" || (input.mode === undefined && input.line === undefined && input.symbol === undefined)) {
137
143
  const total = index.symbols.length;
138
144
  if (offset > Math.max(1, total)) throw new Error(`Outline offset=${offset} is beyond ${total} entries. Next: ${callText({ ...input, offset: Math.max(1, total - 49) })}`);
139
145
  const entries: string[] = [];
@@ -201,11 +207,12 @@ export async function executeReadSymbol(input: ReadSymbolInput, signal: AbortSig
201
207
  export function registerReadSymbolTool(pi: ExtensionAPI): void {
202
208
  pi.registerTool({
203
209
  name: "read_symbol", label: "read_symbol",
204
- description: `Read a complete function, method, class, or type from a local source file by containing line or exact symbol name. Supports ${SUPPORTED_LANGUAGES}. With path only, lists a symbol outline. Includes enclosing names, exact source ranges, and actionable failure context. Output is bounded to 48 KiB/1950 lines; partial results include an exact continuation call. Does not replace read for ordinary text or images.`,
205
- promptSnippet: "Read whole source symbols by line/name, or list a file's symbol outline",
210
+ description: `Read a complete function, method, class, or type from a local source file. Always set mode: use mode="symbol" with an exact symbol name or containing line; use mode="outline" only to list names and ranges. Supports ${SUPPORTED_LANGUAGES}. Includes enclosing names, exact source ranges, and actionable failure context. Output is bounded to 48 KiB/1950 lines; partial results include an exact continuation call. Does not replace read for ordinary text or images.`,
211
+ promptSnippet: "Read one whole source symbol by line/name; set mode=outline only to list declarations",
206
212
  promptGuidelines: [
207
- "Use read_symbol with path and line after grep/edit identifies a code location, instead of guessing successive read offsets to find the function boundary.",
208
- "Use read_symbol with symbol for an exact name or qualified name; use path alone for an outline. Use parent to include an enclosing function or class.",
213
+ "Always set read_symbol mode. Use mode=\"symbol\" with a symbol name or containing line to read source; mode=\"outline\" only lists declarations and does not read their bodies.",
214
+ "Use read_symbol with mode=\"symbol\", path, and line after grep/edit identifies a code location, instead of guessing successive read offsets to find the function boundary.",
215
+ "Use read_symbol with mode=\"symbol\" and symbol for an exact name or qualified name. Use parent to include an enclosing function or class.",
209
216
  "For read_symbol partial output, use the supplied continuation arguments. offset is relative to the selection, not a file line. Only displayed source is evidence for edit.",
210
217
  ],
211
218
  parameters: readSymbolSchema,