@krosskinetic/pi-zg 0.1.1 → 0.1.2
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 +41 -43
- package/extensions/pi-zg.ts +58 -222
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
# @krosskinetic/pi-zg
|
|
4
4
|
|
|
5
5
|
A [Pi](https://github.com/earendil-works/pi-mono) package that natively integrates
|
|
6
|
-
[zvec-grep](https://github.com/) (`zg`) semantic code search into Pi: it
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
(`/zg-settings`, `/zg-status`, `/zg-index
|
|
10
|
-
it does not install or bundle zvec-grep itself.
|
|
6
|
+
[zvec-grep](https://github.com/) (`zg`) semantic code search into Pi: it offers
|
|
7
|
+
to build a missing index interactively, and gives the agent four tools
|
|
8
|
+
(`zg_search`, `zg_rg`, `zg_index`, `zg_status`) plus three commands
|
|
9
|
+
(`/zg-settings`, `/zg-status`, `/zg-index`). It calls an existing local `zg` CLI;
|
|
11
10
|
|
|
12
11
|
## Prerequisites
|
|
13
12
|
|
|
@@ -32,9 +31,9 @@ pi -e .
|
|
|
32
31
|
1. Install and configure `zg`, then verify that `zg version` works from your
|
|
33
32
|
project directory.
|
|
34
33
|
2. Install this package and start Pi in the project you want to search.
|
|
35
|
-
3. At session start, pi-zg checks whether `zg` is available
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
3. At session start, pi-zg checks whether `zg` is available and whether the
|
|
35
|
+
project has an index. Each `zg_search` refreshes the index first, so
|
|
36
|
+
results are never stale — no server daemon is used.
|
|
38
37
|
4. Ask Pi a natural-language code-search question. Its `zg_search` tool
|
|
39
38
|
searches the index. If the project has not been indexed, pi-zg offers to
|
|
40
39
|
build one interactively.
|
|
@@ -44,22 +43,16 @@ exact identifier, literal, or regex search, Pi can use `zg_rg` instead.
|
|
|
44
43
|
|
|
45
44
|
## What it does
|
|
46
45
|
|
|
47
|
-
###
|
|
46
|
+
### Always-fresh direct searches
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
This extension runs every search in **direct mode and refreshes the index before
|
|
49
|
+
answering** (`zg query --mode direct --refresh wait`). It does **not** use or manage
|
|
50
|
+
zg's shared server daemon — there is no background process to keep alive, start,
|
|
51
|
+
or shut down, so nothing can linger as an orphan between sessions.
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
- **Stops it at Pi session shutdown only when Pi started it.** Pi records the
|
|
56
|
-
daemon PID and rechecks it before stopping the server, so a server that was
|
|
57
|
-
already running—or has since been replaced—is left alone. Use
|
|
58
|
-
`/zg-server off` to stop a shared server explicitly.
|
|
59
|
-
|
|
60
|
-
With the server running, `zg query` refreshes the index in the background
|
|
61
|
-
automatically after file changes, so `zg_index` is rarely needed once a
|
|
62
|
-
project has an initial index.
|
|
53
|
+
Because each semantic search rebuilds any stale parts of the index first, results
|
|
54
|
+
are never stale: after editing a file, the next `zg_search` reflects the change.
|
|
55
|
+
This also means no index auto-refresh daemon needs to run at all.
|
|
63
56
|
|
|
64
57
|
### Tools (LLM-callable)
|
|
65
58
|
|
|
@@ -73,39 +66,48 @@ project has an initial index.
|
|
|
73
66
|
does not require an index.
|
|
74
67
|
- **`zg_index`** — build, rebuild, or drop the persistent index. Gated by
|
|
75
68
|
prompt guidelines so the agent only uses it when the user explicitly asks.
|
|
69
|
+
- **`zg_status`** — report zg version and the current project's index status.
|
|
70
|
+
Reads pi's cached zg state, so no extra `zg` subprocess is spawned for it.
|
|
76
71
|
|
|
77
72
|
`zg_search` accepts a natural-language `query` and an optional `limit` of
|
|
78
73
|
1–100 results (zg defaults to 7). `zg_rg` accepts a regex `pattern`, optional
|
|
79
74
|
paths, `fixedString` for literal matching, and one `glob` filter. Its output
|
|
80
75
|
and the output of the other tools are capped at 2,000 lines or 50 KB.
|
|
81
76
|
|
|
77
|
+
|
|
78
|
+
#### Choosing a search tool
|
|
79
|
+
|
|
80
|
+
Use the right tool for the kind of query, and don't reach for grep when semantic
|
|
81
|
+
search is the better fit:
|
|
82
|
+
|
|
83
|
+
- **`zg_search`** — find code by **meaning**. Use when you don't know the exact
|
|
84
|
+
identifiers or wording (“where is token refresh handled”, “how does the cache
|
|
85
|
+
layer work”). Keyword grep would miss these.
|
|
86
|
+
- **`zg_rg`** — find code by **exact text**: a known identifier, string literal, or
|
|
87
|
+
regex, honoring the project's ignore/glob rules.
|
|
88
|
+
- **Pi's built-in `grep`** — a quick literal scan when you don't need zg's
|
|
89
|
+
ignore rules or rg features.
|
|
90
|
+
|
|
91
|
+
In short: concept → `zg_search`; exact token/regex → `zg_rg`; everything is
|
|
92
|
+
refreshed before answering, so you always search current code.
|
|
82
93
|
### Commands (human-invoked)
|
|
83
94
|
|
|
84
|
-
- **`/zg-settings`** — interactive configuration for
|
|
85
|
-
|
|
86
|
-
- **`/zg-status`** — zg version
|
|
95
|
+
- **`/zg-settings`** — interactive configuration for zg's default embedding
|
|
96
|
+
model, embedding device, and provider API key.
|
|
97
|
+
- **`/zg-status`** — zg version and index status/coverage.
|
|
87
98
|
- **`/zg-index [--rebuild|--drop]`** — build, rebuild, or drop the index
|
|
88
99
|
directly, without going through the LLM. Confirms before `--drop`.
|
|
89
|
-
- **`/zg-server <on|off|status>`** — explicit manual control of the shared
|
|
90
|
-
daemon. Confirms before `off`, since other tools may depend on it.
|
|
91
100
|
|
|
92
101
|
### Settings (`/zg-settings`)
|
|
93
102
|
|
|
94
103
|
`/zg-settings` exposes the configuration that most directly affects Pi search:
|
|
95
104
|
|
|
96
|
-
- **Search and refresh mode** — choose between a shared server with background
|
|
97
|
-
refresh, direct queries that refresh before every semantic search, or direct
|
|
98
|
-
queries that use the current index and can be stale. The choice applies to
|
|
99
|
-
the current Pi session. Switching to either direct mode stops a running
|
|
100
|
-
shared server only after confirmation, because the daemon owns index writes
|
|
101
|
-
and may be used by other tools.
|
|
102
105
|
- **Default embedding model** — sets zg's persistent default for newly built
|
|
103
106
|
indexes, for example `local/potion-code-16m-v2` or
|
|
104
107
|
`qwen/text-embedding-v4`. Existing indexes keep their recorded embedding
|
|
105
108
|
schema.
|
|
106
109
|
- **Embedding device** — configures `auto`, `cpu`, `metal`, `vulkan`, or
|
|
107
|
-
`cuda` for a specified local model.
|
|
108
|
-
server after confirmation so the changed runtime takes effect.
|
|
110
|
+
`cuda` for a specified local model.
|
|
109
111
|
- **Provider API key** — saves credentials for a named embedding provider via
|
|
110
112
|
`zg config provider set`. The value is passed directly to zg and is not
|
|
111
113
|
shown in Pi notifications.
|
|
@@ -115,13 +117,11 @@ other less-common settings, use the underlying `zg` CLI directly.
|
|
|
115
117
|
|
|
116
118
|
### Status
|
|
117
119
|
|
|
118
|
-
The footer shows `
|
|
119
|
-
|
|
120
|
+
The footer shows `index ✓/✗` (colored via the active theme), refreshed at
|
|
121
|
+
session start and at the start of every turn.
|
|
120
122
|
|
|
121
123
|
### Flags
|
|
122
124
|
|
|
123
|
-
- `--no-zg-autostart` — disable automatically starting the shared server at
|
|
124
|
-
Pi session start.
|
|
125
125
|
- `--no-zg-onboard` — disable the interactive "build an index?" offer;
|
|
126
126
|
`zg_search` fails with a manual-fix message instead (useful for
|
|
127
127
|
non-interactive/scripted `pi -p` runs).
|
|
@@ -129,7 +129,6 @@ theme), refreshed at session start and at the start of every turn.
|
|
|
129
129
|
Pass these when launching Pi, for example:
|
|
130
130
|
|
|
131
131
|
```bash
|
|
132
|
-
pi --no-zg-autostart
|
|
133
132
|
pi --no-zg-onboard
|
|
134
133
|
```
|
|
135
134
|
|
|
@@ -137,8 +136,7 @@ pi --no-zg-onboard
|
|
|
137
136
|
|
|
138
137
|
- Does not register zg's MCP server as an actual MCP tool source inside Pi
|
|
139
138
|
— Pi extensions have no MCP-client API, so integration stays CLI-based
|
|
140
|
-
(`pi.exec`),
|
|
141
|
-
|
|
139
|
+
(`pi.exec`), stateful rather than re-deriving status via subprocess spawns
|
|
140
|
+
before every call.
|
|
142
141
|
- Does not override Pi's built-in `grep` tool. `zg_search`/`zg_rg` are
|
|
143
142
|
purely additive.
|
|
144
|
-
- Does not stop a shared `zg` server that Pi did not start.
|
package/extensions/pi-zg.ts
CHANGED
|
@@ -9,10 +9,9 @@ import { Type } from "typebox";
|
|
|
9
9
|
* pi-zg: a native Pi integration for the zvec-grep (`zg`) CLI.
|
|
10
10
|
*
|
|
11
11
|
* Beyond wrapping `zg query`/`zg index`, this extension:
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* - Caches zg's availability/server/index state per session (refreshed at
|
|
12
|
+
* - Runs every search in direct mode, refreshing the index before answering so
|
|
13
|
+
* results are never stale (no background server daemon involved).
|
|
14
|
+
* - Caches zg's availability and index state per session (refreshed at
|
|
16
15
|
* session_start and each turn_start) instead of re-deriving it with
|
|
17
16
|
* extra `zg` subprocess spawns before every tool call.
|
|
18
17
|
* - Offers to build a missing index interactively instead of just failing.
|
|
@@ -25,40 +24,24 @@ import { Type } from "typebox";
|
|
|
25
24
|
|
|
26
25
|
const STATUS_KEY = "pi-zg";
|
|
27
26
|
const DEFAULT_LOCAL_MODEL = "local/potion-code-16m-v2";
|
|
28
|
-
type ZgQueryMode = "server" | "direct";
|
|
29
|
-
type ZgRefreshPolicy = "background" | "wait" | "off";
|
|
30
|
-
|
|
31
27
|
interface ZgState {
|
|
32
28
|
/** Whether refreshZgState has run at least once this session. */
|
|
33
29
|
checked: boolean;
|
|
34
30
|
/** Whether `zg` is on PATH. */
|
|
35
31
|
available: boolean;
|
|
36
32
|
version?: string;
|
|
37
|
-
/** Whether the shared zg server daemon is up and ready. */
|
|
38
|
-
serverRunning: boolean;
|
|
39
|
-
/** PID reported by the ready shared server, when available. */
|
|
40
|
-
serverPid?: string;
|
|
41
|
-
/** PID of the server Pi started and therefore owns for this session. */
|
|
42
|
-
ownedServerPid?: string;
|
|
43
33
|
/** Whether the current project has a ready index. */
|
|
44
34
|
indexed: boolean;
|
|
45
35
|
/** Whether the user already declined the "build an index?" offer this session. */
|
|
46
36
|
declinedIndexOffer: boolean;
|
|
47
|
-
/** Pi's active indexed-search transport for this session. */
|
|
48
|
-
queryMode: ZgQueryMode;
|
|
49
|
-
/** Pi's active freshness policy for this session. */
|
|
50
|
-
refreshPolicy: ZgRefreshPolicy;
|
|
51
37
|
}
|
|
52
38
|
|
|
53
39
|
function createZgState(): ZgState {
|
|
54
40
|
return {
|
|
55
41
|
checked: false,
|
|
56
42
|
available: false,
|
|
57
|
-
serverRunning: false,
|
|
58
43
|
indexed: false,
|
|
59
44
|
declinedIndexOffer: false,
|
|
60
|
-
queryMode: "server",
|
|
61
|
-
refreshPolicy: "background",
|
|
62
45
|
};
|
|
63
46
|
}
|
|
64
47
|
|
|
@@ -77,21 +60,12 @@ function renderStatus(ctx: ExtensionContext, state: ZgState) {
|
|
|
77
60
|
ctx.ui.setStatus(STATUS_KEY, theme.fg("dim", "zg: not found"));
|
|
78
61
|
return;
|
|
79
62
|
}
|
|
80
|
-
const server = state.serverRunning ? theme.fg("success", "server\u25cf") : theme.fg("dim", "server\u25cb");
|
|
81
63
|
const index = state.indexed ? theme.fg("success", "index\u2713") : theme.fg("warning", "index\u2717");
|
|
82
|
-
ctx.ui.setStatus(STATUS_KEY,
|
|
64
|
+
ctx.ui.setStatus(STATUS_KEY, index);
|
|
83
65
|
}
|
|
84
66
|
|
|
85
|
-
function parseServerPid(output: string): string | undefined {
|
|
86
|
-
return output.match(/^PID:\s*(\d+)$/m)?.[1];
|
|
87
|
-
}
|
|
88
67
|
|
|
89
|
-
|
|
90
|
-
// Do not risk stopping a daemon if zg did not report a stable PID for it.
|
|
91
|
-
if (state.serverRunning && state.serverPid) state.ownedServerPid = state.serverPid;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Refresh cached zg availability/server/index state and update the footer. */
|
|
68
|
+
/** Refresh cached zg availability and index state, then update the footer. */
|
|
95
69
|
async function refreshZgState(
|
|
96
70
|
pi: ExtensionAPI,
|
|
97
71
|
ctx: ExtensionContext,
|
|
@@ -104,19 +78,12 @@ async function refreshZgState(
|
|
|
104
78
|
state.version = state.available ? version.stdout.trim() : undefined;
|
|
105
79
|
|
|
106
80
|
if (!state.available) {
|
|
107
|
-
state.serverRunning = false;
|
|
108
|
-
state.serverPid = undefined;
|
|
109
81
|
state.indexed = false;
|
|
110
82
|
renderStatus(ctx, state);
|
|
111
83
|
return state;
|
|
112
84
|
}
|
|
113
85
|
|
|
114
|
-
const
|
|
115
|
-
execZg(pi, ["server", "status", "--check-ready"], ctx, { signal, timeout: 5_000 }),
|
|
116
|
-
execZg(pi, ["status", "--check-ready"], ctx, { signal, timeout: 5_000 }),
|
|
117
|
-
]);
|
|
118
|
-
state.serverRunning = server.code === 0;
|
|
119
|
-
state.serverPid = state.serverRunning ? parseServerPid(server.stdout) : undefined;
|
|
86
|
+
const status = await execZg(pi, ["status", "--check-ready"], ctx, { signal, timeout: 5_000 });
|
|
120
87
|
state.indexed = status.code === 0;
|
|
121
88
|
|
|
122
89
|
renderStatus(ctx, state);
|
|
@@ -137,97 +104,9 @@ function parseHitCount(output: string): number | undefined {
|
|
|
137
104
|
return match ? Number(match[1]) : undefined;
|
|
138
105
|
}
|
|
139
106
|
|
|
140
|
-
function searchModeLabel(state: ZgState): string {
|
|
141
|
-
if (state.queryMode === "server") return "server / background refresh";
|
|
142
|
-
return state.refreshPolicy === "wait" ? "direct / refresh before search" : "direct / current index";
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function ensureServerRunning(pi: ExtensionAPI, ctx: ExtensionContext, state: ZgState): Promise<boolean> {
|
|
146
|
-
if (state.serverRunning) return true;
|
|
147
|
-
ctx.ui.setWorkingMessage("Starting zg server...");
|
|
148
|
-
const result = await execZg(pi, ["server", "on"], ctx, { timeout: 20_000 });
|
|
149
|
-
ctx.ui.setWorkingMessage();
|
|
150
|
-
await refreshZgState(pi, ctx, state);
|
|
151
|
-
if (result.code === 0) markServerOwnedByPi(state);
|
|
152
|
-
if (result.code !== 0 || !state.serverRunning) {
|
|
153
|
-
ctx.ui.notify("zg server could not be started. Choose a direct search mode in /zg-settings or inspect /zg-server status.", "error");
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* A daemon has an exclusive lease on index writes, so direct refresh must stop
|
|
161
|
-
* it first. Ask because that daemon can be shared with other agent clients.
|
|
162
|
-
*/
|
|
163
|
-
async function stopServerForDirectMode(pi: ExtensionAPI, ctx: ExtensionContext, state: ZgState): Promise<boolean> {
|
|
164
|
-
if (!state.serverRunning) return true;
|
|
165
|
-
const confirmed = await ctx.ui.confirm(
|
|
166
|
-
"Switch to direct searches?",
|
|
167
|
-
"Direct refresh writes to the index itself and requires stopping the shared zg server. This may affect other tools using it.",
|
|
168
|
-
);
|
|
169
|
-
if (!confirmed) return false;
|
|
170
|
-
|
|
171
|
-
ctx.ui.setWorkingMessage("Stopping zg server...");
|
|
172
|
-
const result = await execZg(pi, ["server", "off"], ctx, { timeout: 20_000 });
|
|
173
|
-
ctx.ui.setWorkingMessage();
|
|
174
|
-
await refreshZgState(pi, ctx, state);
|
|
175
|
-
if (result.code !== 0 || state.serverRunning) {
|
|
176
|
-
ctx.ui.notify("zg server could not be stopped; direct refresh was not enabled.", "error");
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
state.ownedServerPid = undefined;
|
|
180
|
-
return true;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async function offerServerRestart(pi: ExtensionAPI, ctx: ExtensionContext, state: ZgState): Promise<void> {
|
|
184
|
-
if (!state.serverRunning) return;
|
|
185
|
-
const restart = await ctx.ui.confirm(
|
|
186
|
-
"Restart zg server?",
|
|
187
|
-
"Restarting applies the changed embedding runtime to the shared server. Other tools using it will briefly lose access.",
|
|
188
|
-
);
|
|
189
|
-
if (!restart) return;
|
|
190
|
-
ctx.ui.setWorkingMessage("Restarting zg server...");
|
|
191
|
-
const stopped = await execZg(pi, ["server", "off"], ctx, { timeout: 20_000 });
|
|
192
|
-
if (stopped.code === 0) state.ownedServerPid = undefined;
|
|
193
|
-
const started = stopped.code === 0 ? await execZg(pi, ["server", "on"], ctx, { timeout: 20_000 }) : stopped;
|
|
194
|
-
ctx.ui.setWorkingMessage();
|
|
195
|
-
await refreshZgState(pi, ctx, state);
|
|
196
|
-
if (started.code === 0) markServerOwnedByPi(state);
|
|
197
|
-
ctx.ui.notify(
|
|
198
|
-
started.code === 0 && state.serverRunning ? "zg server restarted." : "zg server restart failed; inspect /zg-server status.",
|
|
199
|
-
started.code === 0 && state.serverRunning ? "info" : "error",
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async function configureSearchMode(pi: ExtensionAPI, ctx: ExtensionContext, state: ZgState): Promise<void> {
|
|
204
|
-
const choice = await ctx.ui.select("Search and refresh mode", [
|
|
205
|
-
"Server — background refresh (fast repeated searches)",
|
|
206
|
-
"Direct — refresh before each search (no daemon)",
|
|
207
|
-
"Direct — use current index (fast, may be stale)",
|
|
208
|
-
"Cancel",
|
|
209
|
-
]);
|
|
210
|
-
if (!choice || choice === "Cancel") return;
|
|
211
|
-
|
|
212
|
-
if (choice.startsWith("Server")) {
|
|
213
|
-
state.queryMode = "server";
|
|
214
|
-
state.refreshPolicy = "background";
|
|
215
|
-
if (await ensureServerRunning(pi, ctx, state)) ctx.ui.notify("zg searches use the shared server with background refresh.", "info");
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
107
|
|
|
219
|
-
if (!(await stopServerForDirectMode(pi, ctx, state))) return;
|
|
220
|
-
state.queryMode = "direct";
|
|
221
|
-
state.refreshPolicy = choice.startsWith("Direct — refresh") ? "wait" : "off";
|
|
222
|
-
ctx.ui.notify(
|
|
223
|
-
state.refreshPolicy === "wait"
|
|
224
|
-
? "zg searches refresh the index before each semantic query."
|
|
225
|
-
: "zg searches use the current index and may be stale after edits.",
|
|
226
|
-
"info",
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
108
|
|
|
230
|
-
async function configureDefaultModel(pi: ExtensionAPI, ctx: ExtensionContext
|
|
109
|
+
async function configureDefaultModel(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
231
110
|
const model = await ctx.ui.input("Default embedding model", "e.g. local/potion-code-16m-v2 or qwen/text-embedding-v4");
|
|
232
111
|
if (!model?.trim()) return;
|
|
233
112
|
const result = await execZg(pi, ["config", "model", "set", model.trim(), "--default"], ctx);
|
|
@@ -236,10 +115,9 @@ async function configureDefaultModel(pi: ExtensionAPI, ctx: ExtensionContext, st
|
|
|
236
115
|
return;
|
|
237
116
|
}
|
|
238
117
|
ctx.ui.notify(`Default embedding model set to ${model.trim()}. Existing indexes keep their current schema.`, "info");
|
|
239
|
-
await offerServerRestart(pi, ctx, state);
|
|
240
118
|
}
|
|
241
119
|
|
|
242
|
-
async function configureEmbeddingDevice(pi: ExtensionAPI, ctx: ExtensionContext
|
|
120
|
+
async function configureEmbeddingDevice(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
243
121
|
const model = await ctx.ui.input("Model to configure", "e.g. local/potion-code-16m-v2");
|
|
244
122
|
if (!model?.trim()) return;
|
|
245
123
|
const device = await ctx.ui.select("Embedding device", ["auto", "cpu", "metal", "vulkan", "cuda", "Cancel"]);
|
|
@@ -250,7 +128,6 @@ async function configureEmbeddingDevice(pi: ExtensionAPI, ctx: ExtensionContext,
|
|
|
250
128
|
return;
|
|
251
129
|
}
|
|
252
130
|
ctx.ui.notify(`Embedding device for ${model.trim()} set to ${device}.`, "info");
|
|
253
|
-
await offerServerRestart(pi, ctx, state);
|
|
254
131
|
}
|
|
255
132
|
|
|
256
133
|
async function configureProviderKey(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
@@ -270,8 +147,7 @@ async function openZgSettings(pi: ExtensionAPI, ctx: ExtensionContext, state: Zg
|
|
|
270
147
|
if (!state.checked) await refreshZgState(pi, ctx, state);
|
|
271
148
|
|
|
272
149
|
while (true) {
|
|
273
|
-
const choice = await ctx.ui.select(
|
|
274
|
-
"Search and refresh mode",
|
|
150
|
+
const choice = await ctx.ui.select("zg settings", [
|
|
275
151
|
"Default embedding model",
|
|
276
152
|
"Embedding device",
|
|
277
153
|
"Provider API key",
|
|
@@ -281,14 +157,11 @@ async function openZgSettings(pi: ExtensionAPI, ctx: ExtensionContext, state: Zg
|
|
|
281
157
|
if (!choice || choice === "Done") return;
|
|
282
158
|
|
|
283
159
|
switch (choice) {
|
|
284
|
-
case "Search and refresh mode":
|
|
285
|
-
await configureSearchMode(pi, ctx, state);
|
|
286
|
-
break;
|
|
287
160
|
case "Default embedding model":
|
|
288
|
-
await configureDefaultModel(pi, ctx
|
|
161
|
+
await configureDefaultModel(pi, ctx);
|
|
289
162
|
break;
|
|
290
163
|
case "Embedding device":
|
|
291
|
-
await configureEmbeddingDevice(pi, ctx
|
|
164
|
+
await configureEmbeddingDevice(pi, ctx);
|
|
292
165
|
break;
|
|
293
166
|
case "Provider API key":
|
|
294
167
|
await configureProviderKey(pi, ctx);
|
|
@@ -298,9 +171,6 @@ async function openZgSettings(pi: ExtensionAPI, ctx: ExtensionContext, state: Zg
|
|
|
298
171
|
const detail = await execZg(pi, ["status"], ctx);
|
|
299
172
|
ctx.ui.notify(
|
|
300
173
|
[
|
|
301
|
-
`Pi search mode: ${searchModeLabel(state)} (session setting)`,
|
|
302
|
-
`zg server: ${state.serverRunning ? "running" : "stopped"}`,
|
|
303
|
-
"",
|
|
304
174
|
(detail.stdout || detail.stderr).trim(),
|
|
305
175
|
].join("\n"),
|
|
306
176
|
state.indexed ? "info" : "warning",
|
|
@@ -337,7 +207,7 @@ async function offerToBuildIndex(
|
|
|
337
207
|
}
|
|
338
208
|
|
|
339
209
|
ctx.ui.setWorkingMessage("Building zg index...");
|
|
340
|
-
let result = await execZg(pi, ["index", "--mode",
|
|
210
|
+
let result = await execZg(pi, ["index", "--mode", "direct"], ctx, { signal, timeout: 300_000 });
|
|
341
211
|
|
|
342
212
|
if (result.code !== 0 && /embedding/i.test(result.stderr)) {
|
|
343
213
|
// No default embedding model configured yet -- offer to set one and retry.
|
|
@@ -354,7 +224,7 @@ async function offerToBuildIndex(
|
|
|
354
224
|
}
|
|
355
225
|
if (model) {
|
|
356
226
|
await execZg(pi, ["config", "model", "set", model, "--default"], ctx, { signal });
|
|
357
|
-
result = await execZg(pi, ["index", "--mode",
|
|
227
|
+
result = await execZg(pi, ["index", "--mode", "direct", "--embedding", model], ctx, {
|
|
358
228
|
signal,
|
|
359
229
|
timeout: 300_000,
|
|
360
230
|
});
|
|
@@ -376,46 +246,16 @@ async function offerToBuildIndex(
|
|
|
376
246
|
export default function (pi: ExtensionAPI) {
|
|
377
247
|
const state = createZgState();
|
|
378
248
|
|
|
379
|
-
pi.registerFlag("no-zg-autostart", {
|
|
380
|
-
description: "Disable automatically starting the shared zg server at session start",
|
|
381
|
-
type: "boolean",
|
|
382
|
-
default: false,
|
|
383
|
-
});
|
|
384
249
|
pi.registerFlag("no-zg-onboard", {
|
|
385
250
|
description: "Disable the interactive offer to build a missing zg index; fail with a manual-fix message instead",
|
|
386
251
|
type: "boolean",
|
|
387
252
|
default: false,
|
|
388
253
|
});
|
|
389
254
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (!pi.getFlag("no-zg-autostart") && state.queryMode === "server" && state.available && !state.serverRunning) {
|
|
394
|
-
// Fire-and-forget: don't block startup on daemon warmup. `zg server on`
|
|
395
|
-
// is idempotent, so this is safe even if something else started it
|
|
396
|
-
// in the meantime.
|
|
397
|
-
execZg(pi, ["server", "on"], ctx, { timeout: 20_000 })
|
|
398
|
-
.then(async (result) => {
|
|
399
|
-
await refreshZgState(pi, ctx, state);
|
|
400
|
-
if (result.code === 0) markServerOwnedByPi(state);
|
|
401
|
-
})
|
|
402
|
-
.catch(() => {});
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
pi.on("session_shutdown", async (_event, ctx) => {
|
|
407
|
-
if (!state.ownedServerPid) return;
|
|
255
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
256
|
+
await refreshZgState(pi, ctx, state);
|
|
257
|
+
});
|
|
408
258
|
|
|
409
|
-
// Recheck the PID before stopping anything. If another process replaced the
|
|
410
|
-
// daemon, it is no longer the server Pi started and must be left alone.
|
|
411
|
-
const server = await execZg(pi, ["server", "status", "--check-ready"], ctx, { timeout: 5_000 });
|
|
412
|
-
if (server.code !== 0 || parseServerPid(server.stdout) !== state.ownedServerPid) return;
|
|
413
|
-
|
|
414
|
-
await execZg(pi, ["server", "off"], ctx, { timeout: 20_000 });
|
|
415
|
-
state.serverRunning = false;
|
|
416
|
-
state.serverPid = undefined;
|
|
417
|
-
state.ownedServerPid = undefined;
|
|
418
|
-
});
|
|
419
259
|
|
|
420
260
|
pi.on("turn_start", async (_event, ctx) => {
|
|
421
261
|
if (state.available) {
|
|
@@ -427,7 +267,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
427
267
|
name: "zg_search",
|
|
428
268
|
label: "zg search",
|
|
429
269
|
description:
|
|
430
|
-
|
|
270
|
+
"Semantic (meaning-based) code search over the current project's zvec-grep index. The go-to when you want to find code by CONCEPT rather than exact text — e.g. where authentication is handled, or how a feature works — without knowing the precise identifiers. Each call refreshes the index first, so results are never stale; it offers to build the index interactively if none exists. Prefer over keyword grep when an exact-token search would miss relevant code. For precise identifiers, string literals, or regex, use zg_rg or the built-in grep tool instead. Returns at most 2,000 lines or 50 KB of CLI output.",
|
|
431
271
|
promptSnippet: "Semantic search in the current project's zg index",
|
|
432
272
|
parameters: Type.Object({
|
|
433
273
|
query: Type.String({ minLength: 1, description: "Semantic code-search query" }),
|
|
@@ -448,16 +288,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
448
288
|
);
|
|
449
289
|
}
|
|
450
290
|
|
|
451
|
-
if (state.queryMode === "server" && !(await ensureServerRunning(pi, ctx, state))) {
|
|
452
|
-
throw new Error("zg server is unavailable. Use /zg-settings to choose a direct search mode or /zg-server to inspect it.");
|
|
453
|
-
}
|
|
454
291
|
if (!state.indexed && !(await offerToBuildIndex(pi, ctx, state, signal))) {
|
|
455
292
|
throw new Error(
|
|
456
293
|
`This project does not appear to be indexed. Run \`zg index\` manually in ${ctx.cwd}, then retry.`,
|
|
457
294
|
);
|
|
458
295
|
}
|
|
459
296
|
|
|
460
|
-
const args = ["query", "--mode",
|
|
297
|
+
const args = ["query", "--mode", "direct", "--refresh", "wait"];
|
|
461
298
|
if (params.limit !== undefined) args.push("--limit", String(params.limit));
|
|
462
299
|
args.push(params.query);
|
|
463
300
|
|
|
@@ -480,7 +317,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
480
317
|
name: "zg_rg",
|
|
481
318
|
label: "zg managed ripgrep",
|
|
482
319
|
description:
|
|
483
|
-
"Exhaustive exact-match search via zvec-grep's managed ripgrep (`zg query --rg`).
|
|
320
|
+
"Exhaustive exact-match search via zvec-grep's managed ripgrep (`zg query --rg`). Use when you know the precise identifier, string literal, or regex to locate and want results that honor the project's configured ignore/glob rules (e.g. excluding node_modules). Prefer it over zg_search when you need literal/regex matches rather than meaning; prefer it over the built-in grep tool when you want rg's powers (globs, -F literal) with the project's ignore rules applied. Does not require an index.",
|
|
484
321
|
promptSnippet: "Exhaustive managed ripgrep search via zg (respects project ignore rules)",
|
|
485
322
|
parameters: Type.Object({
|
|
486
323
|
pattern: Type.String({ minLength: 1, description: "Pattern to search for" }),
|
|
@@ -522,7 +359,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
522
359
|
name: "zg_index",
|
|
523
360
|
label: "zg index",
|
|
524
361
|
description:
|
|
525
|
-
"Build, rebuild, or drop the current project's persistent zvec-grep index. Use only when the user explicitly asks:
|
|
362
|
+
"Build, rebuild, or drop the current project's persistent zvec-grep index. Use only when the user explicitly asks: searches already refresh a stale index before answering, so this is mainly for the first-time build, an explicit rebuild, or dropping the index.",
|
|
526
363
|
promptSnippet: "Explicitly build, rebuild, or drop the current project's zg index",
|
|
527
364
|
promptGuidelines: [
|
|
528
365
|
"Use zg_index only when the user explicitly requests indexing, rebuilding, or dropping the zg index; do not call it merely because zg_search reports a missing index -- that flow already offers to build it interactively.",
|
|
@@ -543,7 +380,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
543
380
|
throw new Error("`zg` is not available on PATH. Install @zvec/zvec-grep separately before indexing.");
|
|
544
381
|
}
|
|
545
382
|
|
|
546
|
-
const args = ["index", "--mode",
|
|
383
|
+
const args = ["index", "--mode", "direct"];
|
|
547
384
|
if (params.drop) args.push("--drop", "--yes");
|
|
548
385
|
else if (params.rebuild) args.push("--rebuild");
|
|
549
386
|
|
|
@@ -561,15 +398,49 @@ export default function (pi: ExtensionAPI) {
|
|
|
561
398
|
},
|
|
562
399
|
});
|
|
563
400
|
|
|
401
|
+
pi.registerTool({
|
|
402
|
+
name: "zg_status",
|
|
403
|
+
label: "zg status",
|
|
404
|
+
description:
|
|
405
|
+
"Report zg availability and the current project's index status (ready or not). Use to confirm zg is set up and the index is current before relying on semantic search, or to diagnose why zg_search/zg_rg return nothing. Reads pi's cached state (refreshed for this call) and does not spawn a separate `zg` subprocess. For full index detail/coverage, humans can use `/zg-status`.",
|
|
406
|
+
promptSnippet: "Check zg availability and index status",
|
|
407
|
+
parameters: Type.Object({}),
|
|
408
|
+
async execute(_toolCallId, _params, signal, _onUpdate, ctx) {
|
|
409
|
+
if (!state.checked) await refreshZgState(pi, ctx, state, signal);
|
|
410
|
+
if (!state.available) {
|
|
411
|
+
throw new Error("`zg` is not available on PATH. Install @zvec/zvec-grep separately before checking status.");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
await refreshZgState(pi, ctx, state, signal);
|
|
415
|
+
const index = state.indexed ? "index: ready" : "index: not ready";
|
|
416
|
+
return {
|
|
417
|
+
content: [
|
|
418
|
+
{
|
|
419
|
+
type: "text",
|
|
420
|
+
text: [
|
|
421
|
+
`zg: ${state.version || "available"}`,
|
|
422
|
+
index,
|
|
423
|
+
].join("\n"),
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
details: {
|
|
427
|
+
available: true,
|
|
428
|
+
version: state.version,
|
|
429
|
+
indexed: state.indexed,
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
|
|
564
435
|
pi.registerCommand("zg-settings", {
|
|
565
|
-
description: "Configure
|
|
436
|
+
description: "Configure zg embedding model, device, and provider credentials",
|
|
566
437
|
handler: async (_args, ctx) => {
|
|
567
438
|
await openZgSettings(pi, ctx, state);
|
|
568
439
|
},
|
|
569
440
|
});
|
|
570
441
|
|
|
571
442
|
pi.registerCommand("zg-status", {
|
|
572
|
-
description: "Report zg version
|
|
443
|
+
description: "Report zg version and index status for this project",
|
|
573
444
|
handler: async (_args, ctx) => {
|
|
574
445
|
await refreshZgState(pi, ctx, state);
|
|
575
446
|
if (!state.available) {
|
|
@@ -580,8 +451,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
580
451
|
const detail = await execZg(pi, ["status"], ctx);
|
|
581
452
|
const lines = [
|
|
582
453
|
`zg: ${state.version || "available"}`,
|
|
583
|
-
`server: ${state.serverRunning ? "running" : "stopped"}`,
|
|
584
|
-
`Pi search mode: ${searchModeLabel(state)} (session setting)`,
|
|
585
454
|
"",
|
|
586
455
|
(detail.stdout || detail.stderr).trim(),
|
|
587
456
|
];
|
|
@@ -606,7 +475,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
606
475
|
if (!confirmed) return;
|
|
607
476
|
}
|
|
608
477
|
|
|
609
|
-
const cmdArgs = ["index", "--mode",
|
|
478
|
+
const cmdArgs = ["index", "--mode", "direct"];
|
|
610
479
|
if (drop) cmdArgs.push("--drop", "--yes");
|
|
611
480
|
else if (rebuild) cmdArgs.push("--rebuild");
|
|
612
481
|
|
|
@@ -623,37 +492,4 @@ export default function (pi: ExtensionAPI) {
|
|
|
623
492
|
},
|
|
624
493
|
});
|
|
625
494
|
|
|
626
|
-
pi.registerCommand("zg-server", {
|
|
627
|
-
description: "Control the shared zg server: /zg-server <on|off|status>",
|
|
628
|
-
handler: async (args, ctx) => {
|
|
629
|
-
if (!state.available) {
|
|
630
|
-
ctx.ui.notify("zg: not found on PATH", "error");
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const action = args.trim().toLowerCase() || "status";
|
|
635
|
-
if (action !== "on" && action !== "off" && action !== "status") {
|
|
636
|
-
ctx.ui.notify("Usage: /zg-server <on|off|status>", "warning");
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
if (action === "off") {
|
|
641
|
-
const confirmed = await ctx.ui.confirm(
|
|
642
|
-
"Stop the shared zg server?",
|
|
643
|
-
"This daemon may be used by other agents/tools (Claude, Cursor, etc.) configured via `zg install`. Stopping it affects all of them, not just this session.",
|
|
644
|
-
);
|
|
645
|
-
if (!confirmed) return;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
const wasServerRunning = state.serverRunning;
|
|
649
|
-
const result = await execZg(pi, ["server", action], ctx, { timeout: 20_000 });
|
|
650
|
-
await refreshZgState(pi, ctx, state);
|
|
651
|
-
if (action === "on" && !wasServerRunning && result.code === 0) markServerOwnedByPi(state);
|
|
652
|
-
if (action === "off" && result.code === 0) state.ownedServerPid = undefined;
|
|
653
|
-
ctx.ui.notify(
|
|
654
|
-
(result.stdout || result.stderr).trim() || `zg server ${action} done.`,
|
|
655
|
-
result.code === 0 ? "info" : "error",
|
|
656
|
-
);
|
|
657
|
-
},
|
|
658
|
-
});
|
|
659
495
|
}
|
package/package.json
CHANGED