@gotgenes/pi-colgrep 1.0.0 → 1.1.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/CHANGELOG.md +23 -0
- package/package.json +10 -4
- package/src/extension.ts +21 -2
- package/src/lib/args.ts +41 -0
- package/src/lib/availability.ts +44 -0
- package/src/lib/exec.ts +9 -0
- package/src/lib/format.ts +71 -0
- package/src/lib/search.ts +37 -0
- package/src/tool-result.ts +31 -0
- package/src/tools/colgrep.ts +211 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.0](https://github.com/gotgenes/pi-packages/compare/pi-colgrep-v1.0.0...pi-colgrep-v1.1.0) (2026-05-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add colgrep availability check ([#90](https://github.com/gotgenes/pi-packages/issues/90)) ([4bf9893](https://github.com/gotgenes/pi-packages/commit/4bf989340bfae97c1aa3bca7bd246768222a4fad))
|
|
9
|
+
* add colgrep CLI argument builder ([#90](https://github.com/gotgenes/pi-packages/issues/90)) ([80ba0db](https://github.com/gotgenes/pi-packages/commit/80ba0dba499b37116c98694ea0933b3a624cb85f))
|
|
10
|
+
* add colgrep result formatting ([#90](https://github.com/gotgenes/pi-packages/issues/90)) ([efad4e7](https://github.com/gotgenes/pi-packages/commit/efad4e778c06afadfeba0bb0e965bde9b0db247a))
|
|
11
|
+
* add colgrep search execution ([#90](https://github.com/gotgenes/pi-packages/issues/90)) ([d896766](https://github.com/gotgenes/pi-packages/commit/d8967660175c525e4234a2170d79fb853ecc6a1e))
|
|
12
|
+
* add exec type and tool-result helpers ([#90](https://github.com/gotgenes/pi-packages/issues/90)) ([3142a2b](https://github.com/gotgenes/pi-packages/commit/3142a2b2d0aa5577d3883f5d58bc7674711fe324))
|
|
13
|
+
* register colgrep search tool ([#90](https://github.com/gotgenes/pi-packages/issues/90)) ([0ad2bbd](https://github.com/gotgenes/pi-packages/commit/0ad2bbdc7a8926d461f5c8b37786e12940ae65f5))
|
|
14
|
+
* wire colgrep tool and availability check into extension ([#90](https://github.com/gotgenes/pi-packages/issues/90)) ([738bbdb](https://github.com/gotgenes/pi-packages/commit/738bbdb3208f6bc37e802725cea2093ed94f8e9c))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Documentation
|
|
18
|
+
|
|
19
|
+
* plan colgrep search tool registration ([#90](https://github.com/gotgenes/pi-packages/issues/90)) ([30a723d](https://github.com/gotgenes/pi-packages/commit/30a723df4b31d3ceb3b609fe89562d0ba4e43f05))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Miscellaneous Chores
|
|
23
|
+
|
|
24
|
+
* add vitest test infrastructure for pi-colgrep ([#90](https://github.com/gotgenes/pi-packages/issues/90)) ([eba526d](https://github.com/gotgenes/pi-packages/commit/eba526d7dc0f962ad06b9d733613fd0a3568465d))
|
|
25
|
+
|
|
3
26
|
## 1.0.0 (2026-05-23)
|
|
4
27
|
|
|
5
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotgenes/pi-colgrep",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Pi extension that integrates ColGrep semantic code search as an agent tool.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -43,18 +43,24 @@
|
|
|
43
43
|
]
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@earendil-works/pi-coding-agent": ">=0.75.0"
|
|
46
|
+
"@earendil-works/pi-coding-agent": ">=0.75.0",
|
|
47
|
+
"@earendil-works/pi-tui": ">=0.75.0"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@biomejs/biome": "^2.4.14",
|
|
50
51
|
"@earendil-works/pi-coding-agent": "0.75.4",
|
|
52
|
+
"@earendil-works/pi-tui": "0.75.4",
|
|
51
53
|
"@types/node": "^22.15.3",
|
|
52
54
|
"rumdl": "^0.1.93",
|
|
53
|
-
"
|
|
55
|
+
"typebox": "^1.1.38",
|
|
56
|
+
"typescript": "^6.0.3",
|
|
57
|
+
"vitest": "^4.1.5"
|
|
54
58
|
},
|
|
55
59
|
"scripts": {
|
|
56
60
|
"check": "tsc --noEmit",
|
|
57
61
|
"lint:md": "rumdl check *.md docs/**/*.md",
|
|
58
|
-
"lint": "biome check . && pnpm run lint:md"
|
|
62
|
+
"lint": "biome check . && pnpm run lint:md",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"test:watch": "vitest"
|
|
59
65
|
}
|
|
60
66
|
}
|
package/src/extension.ts
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { createAvailabilityState } from "./lib/availability.js";
|
|
3
|
+
import { registerColGrep } from "./tools/colgrep.js";
|
|
2
4
|
|
|
3
|
-
export default function piColGrepExtension(
|
|
4
|
-
|
|
5
|
+
export default function piColGrepExtension(pi: ExtensionAPI): void {
|
|
6
|
+
const availability = createAvailabilityState();
|
|
7
|
+
|
|
8
|
+
registerColGrep(pi, {
|
|
9
|
+
exec: (cmd, args, opts) => pi.exec(cmd, args, opts),
|
|
10
|
+
availability,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
14
|
+
await availability.refresh((cmd, args, opts) => pi.exec(cmd, args, opts));
|
|
15
|
+
|
|
16
|
+
if (!availability.available) {
|
|
17
|
+
ctx.ui.notify(
|
|
18
|
+
"colgrep is not installed. Semantic code search will not be available.\n" +
|
|
19
|
+
"Install from: https://github.com/lightonai/next-plaid#installation",
|
|
20
|
+
"warning",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
5
24
|
}
|
package/src/lib/args.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface SearchParams {
|
|
2
|
+
query?: string;
|
|
3
|
+
regex?: string;
|
|
4
|
+
path?: string;
|
|
5
|
+
glob?: string;
|
|
6
|
+
limit?: number;
|
|
7
|
+
context?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build the CLI argument list for a colgrep search invocation.
|
|
12
|
+
*
|
|
13
|
+
* Always includes `--json`. Positional arguments (query, path) come last
|
|
14
|
+
* so flags are unambiguously parsed by the CLI.
|
|
15
|
+
*/
|
|
16
|
+
export function buildSearchArgs(params: SearchParams): string[] {
|
|
17
|
+
const args: string[] = ["--json"];
|
|
18
|
+
|
|
19
|
+
if (params.regex !== undefined) {
|
|
20
|
+
args.push("-e", params.regex);
|
|
21
|
+
}
|
|
22
|
+
if (params.glob !== undefined) {
|
|
23
|
+
args.push("--include", params.glob);
|
|
24
|
+
}
|
|
25
|
+
if (params.limit !== undefined) {
|
|
26
|
+
args.push("-k", String(params.limit));
|
|
27
|
+
}
|
|
28
|
+
if (params.context !== undefined) {
|
|
29
|
+
args.push("-n", String(params.context));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Positional: query comes before path
|
|
33
|
+
if (params.query !== undefined) {
|
|
34
|
+
args.push(params.query);
|
|
35
|
+
}
|
|
36
|
+
if (params.path !== undefined) {
|
|
37
|
+
args.push(params.path);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return args;
|
|
41
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Exec } from "./exec.js";
|
|
2
|
+
|
|
3
|
+
export interface AvailabilityResult {
|
|
4
|
+
available: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check whether the `colgrep` binary is present and responsive by running
|
|
9
|
+
* `colgrep --version`. Returns `{ available: false }` on any failure so the
|
|
10
|
+
* caller can degrade gracefully without catching.
|
|
11
|
+
*/
|
|
12
|
+
export async function checkAvailability(
|
|
13
|
+
exec: Exec,
|
|
14
|
+
): Promise<AvailabilityResult> {
|
|
15
|
+
try {
|
|
16
|
+
const result = await exec("colgrep", ["--version"], { timeout: 5000 });
|
|
17
|
+
return { available: result.code === 0 };
|
|
18
|
+
} catch {
|
|
19
|
+
return { available: false };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AvailabilityState {
|
|
24
|
+
/** `undefined` before the first `refresh()` call. */
|
|
25
|
+
available: boolean | undefined;
|
|
26
|
+
refresh(exec: Exec): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a mutable availability state object that caches the result of the
|
|
31
|
+
* most recent `checkAvailability()` call.
|
|
32
|
+
*
|
|
33
|
+
* The `session_start` handler calls `refresh()` once per session and stores
|
|
34
|
+
* the result here. The tool's `execute()` reads `available` synchronously.
|
|
35
|
+
*/
|
|
36
|
+
export function createAvailabilityState(): AvailabilityState {
|
|
37
|
+
return {
|
|
38
|
+
available: undefined,
|
|
39
|
+
async refresh(exec: Exec): Promise<void> {
|
|
40
|
+
const result = await checkAvailability(exec);
|
|
41
|
+
this.available = result.available;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
package/src/lib/exec.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow exec interface matching `pi.exec()` — injected into library modules
|
|
3
|
+
* so they stay free of Pi SDK imports and remain directly testable.
|
|
4
|
+
*/
|
|
5
|
+
export type Exec = (
|
|
6
|
+
command: string,
|
|
7
|
+
args: string[],
|
|
8
|
+
options?: { cwd?: string; timeout?: number; signal?: AbortSignal },
|
|
9
|
+
) => Promise<{ stdout: string; stderr: string; code: number }>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { isAbsolute, relative } from "node:path";
|
|
2
|
+
|
|
3
|
+
export interface ColGrepJsonHit {
|
|
4
|
+
unit: {
|
|
5
|
+
file: string;
|
|
6
|
+
line: number;
|
|
7
|
+
end_line: number;
|
|
8
|
+
};
|
|
9
|
+
score: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Format a single colgrep JSON hit into the concise text representation:
|
|
14
|
+
* `relative/path.ts:startLine-endLine [score=0.xxx]`
|
|
15
|
+
*
|
|
16
|
+
* Paths are made relative to `searchDir`. If the file is outside
|
|
17
|
+
* `searchDir`, the absolute path is used unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export function formatHit(hit: ColGrepJsonHit, searchDir: string): string {
|
|
20
|
+
const rel = relative(searchDir, hit.unit.file);
|
|
21
|
+
// relative() produces a path starting with ".." when outside searchDir.
|
|
22
|
+
// In that case keep the absolute path for clarity.
|
|
23
|
+
const displayPath =
|
|
24
|
+
isAbsolute(rel) || rel.startsWith("..") ? hit.unit.file : rel;
|
|
25
|
+
const score = hit.score.toFixed(3);
|
|
26
|
+
return `${displayPath}:${hit.unit.line}-${hit.unit.end_line} [score=${score}]`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse the colgrep `--json` stdout into agent-friendly text.
|
|
31
|
+
*
|
|
32
|
+
* Returns `"No matches found"` for an empty result set.
|
|
33
|
+
* Falls back to returning `rawOutput` unchanged when the output is not a
|
|
34
|
+
* valid JSON array — defensive against unexpected colgrep output.
|
|
35
|
+
* Skips individual hits that are missing required fields rather than
|
|
36
|
+
* throwing, so a single malformed entry doesn't suppress all results.
|
|
37
|
+
*/
|
|
38
|
+
export function formatResults(rawOutput: string, searchDir: string): string {
|
|
39
|
+
let parsed: unknown;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(rawOutput);
|
|
42
|
+
} catch {
|
|
43
|
+
return rawOutput;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!Array.isArray(parsed)) {
|
|
47
|
+
return rawOutput;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const lines: string[] = [];
|
|
51
|
+
for (const item of parsed) {
|
|
52
|
+
if (!isValidHit(item)) continue;
|
|
53
|
+
lines.push(formatHit(item, searchDir));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return lines.length === 0 ? "No matches found" : lines.join("\n");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isValidHit(item: unknown): item is ColGrepJsonHit {
|
|
60
|
+
if (typeof item !== "object" || item === null) return false;
|
|
61
|
+
const obj = item as Record<string, unknown>;
|
|
62
|
+
if (typeof obj.score !== "number") return false;
|
|
63
|
+
const unit = obj.unit;
|
|
64
|
+
if (typeof unit !== "object" || unit === null) return false;
|
|
65
|
+
const u = unit as Record<string, unknown>;
|
|
66
|
+
return (
|
|
67
|
+
typeof u.file === "string" &&
|
|
68
|
+
typeof u.line === "number" &&
|
|
69
|
+
typeof u.end_line === "number"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { buildSearchArgs, type SearchParams } from "./args.js";
|
|
2
|
+
import type { Exec } from "./exec.js";
|
|
3
|
+
import { formatResults } from "./format.js";
|
|
4
|
+
|
|
5
|
+
export interface SearchResult {
|
|
6
|
+
/** Formatted hit lines, present on success. */
|
|
7
|
+
output?: string;
|
|
8
|
+
/** Error message, present on failure. */
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Run a colgrep search and return formatted results.
|
|
14
|
+
*
|
|
15
|
+
* Paths in the output are relativized against `params.path` when provided,
|
|
16
|
+
* otherwise against `cwd`.
|
|
17
|
+
*/
|
|
18
|
+
export async function runSearch(
|
|
19
|
+
exec: Exec,
|
|
20
|
+
params: SearchParams,
|
|
21
|
+
cwd: string,
|
|
22
|
+
signal?: AbortSignal,
|
|
23
|
+
): Promise<SearchResult> {
|
|
24
|
+
const args = buildSearchArgs(params);
|
|
25
|
+
const result = await exec("colgrep", args, { cwd, signal });
|
|
26
|
+
|
|
27
|
+
if (result.code !== 0) {
|
|
28
|
+
const detail = result.stderr.trim();
|
|
29
|
+
const message = detail
|
|
30
|
+
? detail
|
|
31
|
+
: `colgrep exited with exit code ${result.code}`;
|
|
32
|
+
return { error: message };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const searchDir = params.path ?? cwd;
|
|
36
|
+
return { output: formatResults(result.stdout, searchDir) };
|
|
37
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers to create AgentToolResult-compatible objects.
|
|
3
|
+
*
|
|
4
|
+
* Pi's AgentToolResult expects `content` as an array of TextContent objects
|
|
5
|
+
* and a `details` field. These helpers wrap a plain string into that shape.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface ToolResult<TDetails = undefined> {
|
|
9
|
+
content: { type: "text"; text: string }[];
|
|
10
|
+
details: TDetails;
|
|
11
|
+
isError: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ok<TDetails = undefined>(
|
|
15
|
+
text: string,
|
|
16
|
+
details?: TDetails,
|
|
17
|
+
): ToolResult<TDetails> {
|
|
18
|
+
return {
|
|
19
|
+
content: [{ type: "text", text }],
|
|
20
|
+
details: details as TDetails,
|
|
21
|
+
isError: false,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function err(text: string): ToolResult<undefined> {
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: "text", text }],
|
|
28
|
+
details: undefined,
|
|
29
|
+
isError: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type {
|
|
5
|
+
ExtensionAPI,
|
|
6
|
+
ExtensionContext,
|
|
7
|
+
Theme,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_MAX_BYTES,
|
|
11
|
+
DEFAULT_MAX_LINES,
|
|
12
|
+
truncateHead,
|
|
13
|
+
} from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
15
|
+
import { Type } from "typebox";
|
|
16
|
+
import type { SearchParams } from "../lib/args.js";
|
|
17
|
+
import type { AvailabilityState } from "../lib/availability.js";
|
|
18
|
+
import type { Exec } from "../lib/exec.js";
|
|
19
|
+
import { runSearch } from "../lib/search.js";
|
|
20
|
+
import { err, ok } from "../tool-result.js";
|
|
21
|
+
|
|
22
|
+
export interface ColGrepToolDetails {
|
|
23
|
+
hitCount: number;
|
|
24
|
+
truncated: boolean;
|
|
25
|
+
fullOutputPath?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ColGrepExecuteDeps {
|
|
29
|
+
exec: Exec;
|
|
30
|
+
availability: AvailabilityState;
|
|
31
|
+
/** Override default truncation line limit. Used in tests. */
|
|
32
|
+
maxLines?: number;
|
|
33
|
+
/** Override default truncation byte limit. Used in tests. */
|
|
34
|
+
maxBytes?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Core execute logic, extracted for testability.
|
|
39
|
+
* Called by the tool's `execute()` callback with `ctx.cwd` as cwd.
|
|
40
|
+
*/
|
|
41
|
+
export async function executeColGrepSearch(
|
|
42
|
+
params: SearchParams,
|
|
43
|
+
deps: ColGrepExecuteDeps,
|
|
44
|
+
cwd: string,
|
|
45
|
+
signal: AbortSignal | undefined,
|
|
46
|
+
) {
|
|
47
|
+
if (!deps.availability.available) {
|
|
48
|
+
return err(
|
|
49
|
+
"colgrep is not installed or not available.\n" +
|
|
50
|
+
"Install it from: https://github.com/lightonai/next-plaid#installation",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!params.query && !params.regex) {
|
|
55
|
+
return err("At least one of query or regex is required.");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const searchResult = await runSearch(deps.exec, params, cwd, signal);
|
|
59
|
+
if (searchResult.error) {
|
|
60
|
+
return err(searchResult.error);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const output = searchResult.output ?? "No matches found";
|
|
64
|
+
const maxLines = deps.maxLines ?? DEFAULT_MAX_LINES;
|
|
65
|
+
const maxBytes = deps.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
66
|
+
const truncation = truncateHead(output, { maxLines, maxBytes });
|
|
67
|
+
|
|
68
|
+
if (truncation.truncated) {
|
|
69
|
+
const tempPath = join(tmpdir(), `colgrep-${Date.now()}.txt`);
|
|
70
|
+
await writeFile(tempPath, output);
|
|
71
|
+
const hitCount = countHits(output);
|
|
72
|
+
return ok(`${truncation.content}\n[Truncated. Full output: ${tempPath}]`, {
|
|
73
|
+
hitCount,
|
|
74
|
+
truncated: true,
|
|
75
|
+
fullOutputPath: tempPath,
|
|
76
|
+
} satisfies ColGrepToolDetails);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const hitCount = countHits(output);
|
|
80
|
+
return ok(output, {
|
|
81
|
+
hitCount,
|
|
82
|
+
truncated: false,
|
|
83
|
+
} satisfies ColGrepToolDetails);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface RegisterColGrepDeps {
|
|
87
|
+
exec: Exec;
|
|
88
|
+
availability: AvailabilityState;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function registerColGrep(
|
|
92
|
+
pi: ExtensionAPI,
|
|
93
|
+
deps: RegisterColGrepDeps,
|
|
94
|
+
): void {
|
|
95
|
+
pi.registerTool({
|
|
96
|
+
name: "colgrep",
|
|
97
|
+
label: "ColGrep",
|
|
98
|
+
description:
|
|
99
|
+
"Search for code by meaning using semantic / hybrid search (ColBERT embeddings + tree-sitter). " +
|
|
100
|
+
"Complements the built-in grep: use colgrep for intent-based exploration, grep for exact pattern matching. " +
|
|
101
|
+
"At least one of query or regex is required.",
|
|
102
|
+
promptSnippet:
|
|
103
|
+
"colgrep: Semantic and hybrid code search — find code by intent, not just text.",
|
|
104
|
+
promptGuidelines: [
|
|
105
|
+
"Prefer colgrep for intent-based searches and exploration (e.g. 'error handling for database connections').",
|
|
106
|
+
"Use grep for exact pattern or symbol matching; use colgrep when keywords may not match exactly.",
|
|
107
|
+
"Increase limit (default 15) when exploring a large codebase — try limit=30 for broader coverage.",
|
|
108
|
+
],
|
|
109
|
+
parameters: Type.Object({
|
|
110
|
+
query: Type.Optional(
|
|
111
|
+
Type.String({
|
|
112
|
+
description: "Semantic search query (natural language intent)",
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
regex: Type.Optional(
|
|
116
|
+
Type.String({
|
|
117
|
+
description:
|
|
118
|
+
"Regex pre-filter applied before semantic ranking (-e flag)",
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
path: Type.Optional(
|
|
122
|
+
Type.String({
|
|
123
|
+
description: "Directory or file to search (defaults to cwd)",
|
|
124
|
+
}),
|
|
125
|
+
),
|
|
126
|
+
glob: Type.Optional(
|
|
127
|
+
Type.String({ description: "Include glob pattern (--include flag)" }),
|
|
128
|
+
),
|
|
129
|
+
limit: Type.Optional(
|
|
130
|
+
Type.Number({ description: "Max results (-k flag, default: 15)" }),
|
|
131
|
+
),
|
|
132
|
+
context: Type.Optional(
|
|
133
|
+
Type.Number({ description: "Context lines (-n flag)" }),
|
|
134
|
+
),
|
|
135
|
+
}),
|
|
136
|
+
renderCall(args, theme, context) {
|
|
137
|
+
const text =
|
|
138
|
+
(context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
139
|
+
text.setText(formatCall(args, theme));
|
|
140
|
+
return text;
|
|
141
|
+
},
|
|
142
|
+
renderResult(result, options, theme, context) {
|
|
143
|
+
const text =
|
|
144
|
+
(context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
145
|
+
text.setText(formatResult(result, options, theme));
|
|
146
|
+
return text;
|
|
147
|
+
},
|
|
148
|
+
async execute(
|
|
149
|
+
_toolCallId,
|
|
150
|
+
params,
|
|
151
|
+
signal,
|
|
152
|
+
_onUpdate,
|
|
153
|
+
ctx: ExtensionContext,
|
|
154
|
+
) {
|
|
155
|
+
return executeColGrepSearch(params, deps, ctx.cwd, signal);
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---- rendering helpers ----
|
|
161
|
+
|
|
162
|
+
function formatCall(args: Record<string, unknown>, theme: Theme): string {
|
|
163
|
+
const query = typeof args.query === "string" ? args.query : undefined;
|
|
164
|
+
const regex = typeof args.regex === "string" ? args.regex : undefined;
|
|
165
|
+
const path = typeof args.path === "string" ? args.path : ".";
|
|
166
|
+
const limit = typeof args.limit === "number" ? args.limit : 15;
|
|
167
|
+
|
|
168
|
+
const parts: string[] = [theme.fg("toolTitle", theme.bold("colgrep"))];
|
|
169
|
+
if (query) parts.push(theme.fg("accent", `"${query}"`));
|
|
170
|
+
if (regex) parts.push(theme.fg("accent", `-e /${regex}/`));
|
|
171
|
+
parts.push(theme.fg("toolOutput", `in ${path} (k=${limit})`));
|
|
172
|
+
return parts.join(" ");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
interface AnyToolResult {
|
|
176
|
+
content: Array<{ type: string; text?: string }>;
|
|
177
|
+
details?: ColGrepToolDetails;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function formatResult(
|
|
181
|
+
result: unknown,
|
|
182
|
+
options: { expanded: boolean },
|
|
183
|
+
theme: Theme,
|
|
184
|
+
): string {
|
|
185
|
+
const r = result as AnyToolResult;
|
|
186
|
+
const details = r.details;
|
|
187
|
+
const hitCount = details?.hitCount ?? 0;
|
|
188
|
+
const outputText =
|
|
189
|
+
r.content[0]?.type === "text" ? (r.content[0].text ?? "") : "";
|
|
190
|
+
|
|
191
|
+
if (!options.expanded) {
|
|
192
|
+
const icon = theme.fg("success", "✓");
|
|
193
|
+
const count =
|
|
194
|
+
hitCount === 0
|
|
195
|
+
? theme.fg("muted", "no matches")
|
|
196
|
+
: theme.fg("muted", `${hitCount} hit${hitCount === 1 ? "" : "s"}`);
|
|
197
|
+
return `${icon} ${count}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const lines = outputText
|
|
201
|
+
.split("\n")
|
|
202
|
+
.map((l: string) => theme.fg("toolOutput", ` ${l}`));
|
|
203
|
+
return lines.join("\n");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---- private helpers ----
|
|
207
|
+
|
|
208
|
+
function countHits(output: string): number {
|
|
209
|
+
if (output === "No matches found") return 0;
|
|
210
|
+
return output.split("\n").length;
|
|
211
|
+
}
|