@krosskinetic/pi-zg 0.1.0 → 0.1.1
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 +66 -8
- package/extensions/pi-zg.ts +240 -9
- package/package.json +17 -4
package/README.md
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
**WORK IN PROGRESS / ALPHA**
|
|
2
2
|
|
|
3
|
-
# pi-zg
|
|
3
|
+
# @krosskinetic/pi-zg
|
|
4
4
|
|
|
5
5
|
A [Pi](https://github.com/earendil-works/pi-mono) package that natively integrates
|
|
6
6
|
[zvec-grep](https://github.com/) (`zg`) semantic code search into Pi: it manages
|
|
7
7
|
`zg`'s shared server, offers to build a missing index interactively, and gives
|
|
8
|
-
the agent three tools (`zg_search`, `zg_rg`, `zg_index`) plus
|
|
9
|
-
(`/zg-status`, `/zg-index`, `/zg-server`). It calls an existing local `zg` CLI;
|
|
8
|
+
the agent three tools (`zg_search`, `zg_rg`, `zg_index`) plus four commands
|
|
9
|
+
(`/zg-settings`, `/zg-status`, `/zg-index`, `/zg-server`). It calls an existing local `zg` CLI;
|
|
10
10
|
it does not install or bundle zvec-grep itself.
|
|
11
11
|
|
|
12
12
|
## Prerequisites
|
|
13
13
|
|
|
14
14
|
Install `@zvec/zvec-grep` separately and ensure `zg` is on `PATH`.
|
|
15
|
+
This extension invokes that local CLI; it does not install, bundle, or
|
|
16
|
+
configure zvec-grep for you.
|
|
15
17
|
|
|
16
18
|
## Install into Pi
|
|
17
19
|
|
|
18
20
|
```bash
|
|
19
|
-
pi install npm
|
|
21
|
+
pi install npm:@krosskinetic/pi-zg
|
|
20
22
|
```
|
|
21
23
|
|
|
22
24
|
For local development, from a checkout of this repo:
|
|
@@ -25,6 +27,21 @@ For local development, from a checkout of this repo:
|
|
|
25
27
|
pi -e .
|
|
26
28
|
```
|
|
27
29
|
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
1. Install and configure `zg`, then verify that `zg version` works from your
|
|
33
|
+
project directory.
|
|
34
|
+
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, whether its
|
|
36
|
+
shared server is ready, and whether the project has an index. Unless
|
|
37
|
+
disabled, it starts a stopped shared server.
|
|
38
|
+
4. Ask Pi a natural-language code-search question. Its `zg_search` tool
|
|
39
|
+
searches the index. If the project has not been indexed, pi-zg offers to
|
|
40
|
+
build one interactively.
|
|
41
|
+
|
|
42
|
+
For example, ask: “Where is authentication token refresh implemented?” For an
|
|
43
|
+
exact identifier, literal, or regex search, Pi can use `zg_rg` instead.
|
|
44
|
+
|
|
28
45
|
## What it does
|
|
29
46
|
|
|
30
47
|
### Shared server (`zg server`)
|
|
@@ -35,8 +52,10 @@ configured via `zg install`) may also use. This extension:
|
|
|
35
52
|
- **Auto-starts** it at session start if it isn't already running
|
|
36
53
|
(`--no-zg-autostart` disables this). `zg server on` is idempotent, so this
|
|
37
54
|
is safe to run every session.
|
|
38
|
-
- **
|
|
39
|
-
|
|
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.
|
|
40
59
|
|
|
41
60
|
With the server running, `zg query` refreshes the index in the background
|
|
42
61
|
automatically after file changes, so `zg_index` is rarely needed once a
|
|
@@ -55,14 +74,45 @@ project has an initial index.
|
|
|
55
74
|
- **`zg_index`** — build, rebuild, or drop the persistent index. Gated by
|
|
56
75
|
prompt guidelines so the agent only uses it when the user explicitly asks.
|
|
57
76
|
|
|
77
|
+
`zg_search` accepts a natural-language `query` and an optional `limit` of
|
|
78
|
+
1–100 results (zg defaults to 7). `zg_rg` accepts a regex `pattern`, optional
|
|
79
|
+
paths, `fixedString` for literal matching, and one `glob` filter. Its output
|
|
80
|
+
and the output of the other tools are capped at 2,000 lines or 50 KB.
|
|
81
|
+
|
|
58
82
|
### Commands (human-invoked)
|
|
59
83
|
|
|
84
|
+
- **`/zg-settings`** — interactive configuration for Pi's search and refresh
|
|
85
|
+
mode, default embedding model, embedding device, and provider API key.
|
|
60
86
|
- **`/zg-status`** — zg version, server state, and index status/coverage.
|
|
61
87
|
- **`/zg-index [--rebuild|--drop]`** — build, rebuild, or drop the index
|
|
62
88
|
directly, without going through the LLM. Confirms before `--drop`.
|
|
63
89
|
- **`/zg-server <on|off|status>`** — explicit manual control of the shared
|
|
64
90
|
daemon. Confirms before `off`, since other tools may depend on it.
|
|
65
91
|
|
|
92
|
+
### Settings (`/zg-settings`)
|
|
93
|
+
|
|
94
|
+
`/zg-settings` exposes the configuration that most directly affects Pi search:
|
|
95
|
+
|
|
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
|
+
- **Default embedding model** — sets zg's persistent default for newly built
|
|
103
|
+
indexes, for example `local/potion-code-16m-v2` or
|
|
104
|
+
`qwen/text-embedding-v4`. Existing indexes keep their recorded embedding
|
|
105
|
+
schema.
|
|
106
|
+
- **Embedding device** — configures `auto`, `cpu`, `metal`, `vulkan`, or
|
|
107
|
+
`cuda` for a specified local model. The command can restart a running shared
|
|
108
|
+
server after confirmation so the changed runtime takes effect.
|
|
109
|
+
- **Provider API key** — saves credentials for a named embedding provider via
|
|
110
|
+
`zg config provider set`. The value is passed directly to zg and is not
|
|
111
|
+
shown in Pi notifications.
|
|
112
|
+
|
|
113
|
+
For advanced index-selection rules, remote endpoints, authentication, and
|
|
114
|
+
other less-common settings, use the underlying `zg` CLI directly.
|
|
115
|
+
|
|
66
116
|
### Status
|
|
67
117
|
|
|
68
118
|
The footer shows `server ●/○` and `index ✓/✗` (colored via the active
|
|
@@ -70,11 +120,19 @@ theme), refreshed at session start and at the start of every turn.
|
|
|
70
120
|
|
|
71
121
|
### Flags
|
|
72
122
|
|
|
73
|
-
- `--no-zg-autostart` — disable automatically starting the shared server
|
|
123
|
+
- `--no-zg-autostart` — disable automatically starting the shared server at
|
|
124
|
+
Pi session start.
|
|
74
125
|
- `--no-zg-onboard` — disable the interactive "build an index?" offer;
|
|
75
126
|
`zg_search` fails with a manual-fix message instead (useful for
|
|
76
127
|
non-interactive/scripted `pi -p` runs).
|
|
77
128
|
|
|
129
|
+
Pass these when launching Pi, for example:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pi --no-zg-autostart
|
|
133
|
+
pi --no-zg-onboard
|
|
134
|
+
```
|
|
135
|
+
|
|
78
136
|
## Non-goals
|
|
79
137
|
|
|
80
138
|
- Does not register zg's MCP server as an actual MCP tool source inside Pi
|
|
@@ -83,4 +141,4 @@ theme), refreshed at session start and at the start of every turn.
|
|
|
83
141
|
via subprocess spawns before every call.
|
|
84
142
|
- Does not override Pi's built-in `grep` tool. `zg_search`/`zg_rg` are
|
|
85
143
|
purely additive.
|
|
86
|
-
- Does not
|
|
144
|
+
- Does not stop a shared `zg` server that Pi did not start.
|
package/extensions/pi-zg.ts
CHANGED
|
@@ -10,8 +10,8 @@ import { Type } from "typebox";
|
|
|
10
10
|
*
|
|
11
11
|
* Beyond wrapping `zg query`/`zg index`, this extension:
|
|
12
12
|
* - Auto-starts zg's shared server at session start so queries get
|
|
13
|
-
* background index auto-refresh for free
|
|
14
|
-
*
|
|
13
|
+
* background index auto-refresh for free, then shuts it down at Pi session
|
|
14
|
+
* end only when Pi owns that exact daemon PID.
|
|
15
15
|
* - Caches zg's availability/server/index state per session (refreshed at
|
|
16
16
|
* session_start and each turn_start) instead of re-deriving it with
|
|
17
17
|
* extra `zg` subprocess spawns before every tool call.
|
|
@@ -25,6 +25,8 @@ import { Type } from "typebox";
|
|
|
25
25
|
|
|
26
26
|
const STATUS_KEY = "pi-zg";
|
|
27
27
|
const DEFAULT_LOCAL_MODEL = "local/potion-code-16m-v2";
|
|
28
|
+
type ZgQueryMode = "server" | "direct";
|
|
29
|
+
type ZgRefreshPolicy = "background" | "wait" | "off";
|
|
28
30
|
|
|
29
31
|
interface ZgState {
|
|
30
32
|
/** Whether refreshZgState has run at least once this session. */
|
|
@@ -34,10 +36,18 @@ interface ZgState {
|
|
|
34
36
|
version?: string;
|
|
35
37
|
/** Whether the shared zg server daemon is up and ready. */
|
|
36
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;
|
|
37
43
|
/** Whether the current project has a ready index. */
|
|
38
44
|
indexed: boolean;
|
|
39
45
|
/** Whether the user already declined the "build an index?" offer this session. */
|
|
40
46
|
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;
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
function createZgState(): ZgState {
|
|
@@ -47,6 +57,8 @@ function createZgState(): ZgState {
|
|
|
47
57
|
serverRunning: false,
|
|
48
58
|
indexed: false,
|
|
49
59
|
declinedIndexOffer: false,
|
|
60
|
+
queryMode: "server",
|
|
61
|
+
refreshPolicy: "background",
|
|
50
62
|
};
|
|
51
63
|
}
|
|
52
64
|
|
|
@@ -70,6 +82,15 @@ function renderStatus(ctx: ExtensionContext, state: ZgState) {
|
|
|
70
82
|
ctx.ui.setStatus(STATUS_KEY, `${server} ${index}`);
|
|
71
83
|
}
|
|
72
84
|
|
|
85
|
+
function parseServerPid(output: string): string | undefined {
|
|
86
|
+
return output.match(/^PID:\s*(\d+)$/m)?.[1];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function markServerOwnedByPi(state: ZgState) {
|
|
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
|
+
|
|
73
94
|
/** Refresh cached zg availability/server/index state and update the footer. */
|
|
74
95
|
async function refreshZgState(
|
|
75
96
|
pi: ExtensionAPI,
|
|
@@ -84,6 +105,7 @@ async function refreshZgState(
|
|
|
84
105
|
|
|
85
106
|
if (!state.available) {
|
|
86
107
|
state.serverRunning = false;
|
|
108
|
+
state.serverPid = undefined;
|
|
87
109
|
state.indexed = false;
|
|
88
110
|
renderStatus(ctx, state);
|
|
89
111
|
return state;
|
|
@@ -94,6 +116,7 @@ async function refreshZgState(
|
|
|
94
116
|
execZg(pi, ["status", "--check-ready"], ctx, { signal, timeout: 5_000 }),
|
|
95
117
|
]);
|
|
96
118
|
state.serverRunning = server.code === 0;
|
|
119
|
+
state.serverPid = state.serverRunning ? parseServerPid(server.stdout) : undefined;
|
|
97
120
|
state.indexed = status.code === 0;
|
|
98
121
|
|
|
99
122
|
renderStatus(ctx, state);
|
|
@@ -114,6 +137,180 @@ function parseHitCount(output: string): number | undefined {
|
|
|
114
137
|
return match ? Number(match[1]) : undefined;
|
|
115
138
|
}
|
|
116
139
|
|
|
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
|
+
|
|
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
|
+
|
|
230
|
+
async function configureDefaultModel(pi: ExtensionAPI, ctx: ExtensionContext, state: ZgState): Promise<void> {
|
|
231
|
+
const model = await ctx.ui.input("Default embedding model", "e.g. local/potion-code-16m-v2 or qwen/text-embedding-v4");
|
|
232
|
+
if (!model?.trim()) return;
|
|
233
|
+
const result = await execZg(pi, ["config", "model", "set", model.trim(), "--default"], ctx);
|
|
234
|
+
if (result.code !== 0) {
|
|
235
|
+
ctx.ui.notify("Could not set the embedding model. Inspect zg configuration in a terminal for details.", "error");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
ctx.ui.notify(`Default embedding model set to ${model.trim()}. Existing indexes keep their current schema.`, "info");
|
|
239
|
+
await offerServerRestart(pi, ctx, state);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function configureEmbeddingDevice(pi: ExtensionAPI, ctx: ExtensionContext, state: ZgState): Promise<void> {
|
|
243
|
+
const model = await ctx.ui.input("Model to configure", "e.g. local/potion-code-16m-v2");
|
|
244
|
+
if (!model?.trim()) return;
|
|
245
|
+
const device = await ctx.ui.select("Embedding device", ["auto", "cpu", "metal", "vulkan", "cuda", "Cancel"]);
|
|
246
|
+
if (!device || device === "Cancel") return;
|
|
247
|
+
const result = await execZg(pi, ["config", "model", "set", model.trim(), "--device", device], ctx);
|
|
248
|
+
if (result.code !== 0) {
|
|
249
|
+
ctx.ui.notify("Could not set the embedding device. Inspect zg configuration in a terminal for details.", "error");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
ctx.ui.notify(`Embedding device for ${model.trim()} set to ${device}.`, "info");
|
|
253
|
+
await offerServerRestart(pi, ctx, state);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function configureProviderKey(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
257
|
+
const provider = await ctx.ui.input("Embedding provider", "e.g. qwen");
|
|
258
|
+
if (!provider?.trim()) return;
|
|
259
|
+
const apiKey = await ctx.ui.input("API key", "Paste the provider API key");
|
|
260
|
+
if (!apiKey?.trim()) return;
|
|
261
|
+
const result = await execZg(pi, ["config", "provider", "set", provider.trim(), "--api-key", apiKey.trim()], ctx);
|
|
262
|
+
if (result.code !== 0) {
|
|
263
|
+
ctx.ui.notify("Could not save the provider API key. Inspect zg configuration in a terminal for details.", "error");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
ctx.ui.notify(`Credentials saved for ${provider.trim()}.`, "info");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function openZgSettings(pi: ExtensionAPI, ctx: ExtensionContext, state: ZgState): Promise<void> {
|
|
270
|
+
if (!state.checked) await refreshZgState(pi, ctx, state);
|
|
271
|
+
|
|
272
|
+
while (true) {
|
|
273
|
+
const choice = await ctx.ui.select(`zg settings — ${searchModeLabel(state)}`, [
|
|
274
|
+
"Search and refresh mode",
|
|
275
|
+
"Default embedding model",
|
|
276
|
+
"Embedding device",
|
|
277
|
+
"Provider API key",
|
|
278
|
+
"View active settings",
|
|
279
|
+
"Done",
|
|
280
|
+
]);
|
|
281
|
+
if (!choice || choice === "Done") return;
|
|
282
|
+
|
|
283
|
+
switch (choice) {
|
|
284
|
+
case "Search and refresh mode":
|
|
285
|
+
await configureSearchMode(pi, ctx, state);
|
|
286
|
+
break;
|
|
287
|
+
case "Default embedding model":
|
|
288
|
+
await configureDefaultModel(pi, ctx, state);
|
|
289
|
+
break;
|
|
290
|
+
case "Embedding device":
|
|
291
|
+
await configureEmbeddingDevice(pi, ctx, state);
|
|
292
|
+
break;
|
|
293
|
+
case "Provider API key":
|
|
294
|
+
await configureProviderKey(pi, ctx);
|
|
295
|
+
break;
|
|
296
|
+
case "View active settings": {
|
|
297
|
+
await refreshZgState(pi, ctx, state);
|
|
298
|
+
const detail = await execZg(pi, ["status"], ctx);
|
|
299
|
+
ctx.ui.notify(
|
|
300
|
+
[
|
|
301
|
+
`Pi search mode: ${searchModeLabel(state)} (session setting)`,
|
|
302
|
+
`zg server: ${state.serverRunning ? "running" : "stopped"}`,
|
|
303
|
+
"",
|
|
304
|
+
(detail.stdout || detail.stderr).trim(),
|
|
305
|
+
].join("\n"),
|
|
306
|
+
state.indexed ? "info" : "warning",
|
|
307
|
+
);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
117
314
|
/**
|
|
118
315
|
* Lazily offer to build a missing index right where the LLM discovered it's
|
|
119
316
|
* missing, instead of dead-ending with a "go run this command yourself" error.
|
|
@@ -140,7 +337,7 @@ async function offerToBuildIndex(
|
|
|
140
337
|
}
|
|
141
338
|
|
|
142
339
|
ctx.ui.setWorkingMessage("Building zg index...");
|
|
143
|
-
let result = await execZg(pi, ["index"], ctx, { signal, timeout: 300_000 });
|
|
340
|
+
let result = await execZg(pi, ["index", "--mode", state.queryMode], ctx, { signal, timeout: 300_000 });
|
|
144
341
|
|
|
145
342
|
if (result.code !== 0 && /embedding/i.test(result.stderr)) {
|
|
146
343
|
// No default embedding model configured yet -- offer to set one and retry.
|
|
@@ -157,7 +354,10 @@ async function offerToBuildIndex(
|
|
|
157
354
|
}
|
|
158
355
|
if (model) {
|
|
159
356
|
await execZg(pi, ["config", "model", "set", model, "--default"], ctx, { signal });
|
|
160
|
-
result = await execZg(pi, ["index", "--embedding", model], ctx, {
|
|
357
|
+
result = await execZg(pi, ["index", "--mode", state.queryMode, "--embedding", model], ctx, {
|
|
358
|
+
signal,
|
|
359
|
+
timeout: 300_000,
|
|
360
|
+
});
|
|
161
361
|
}
|
|
162
362
|
}
|
|
163
363
|
|
|
@@ -190,16 +390,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
190
390
|
pi.on("session_start", async (_event, ctx) => {
|
|
191
391
|
await refreshZgState(pi, ctx, state);
|
|
192
392
|
|
|
193
|
-
if (!pi.getFlag("no-zg-autostart") && state.available && !state.serverRunning) {
|
|
393
|
+
if (!pi.getFlag("no-zg-autostart") && state.queryMode === "server" && state.available && !state.serverRunning) {
|
|
194
394
|
// Fire-and-forget: don't block startup on daemon warmup. `zg server on`
|
|
195
395
|
// is idempotent, so this is safe even if something else started it
|
|
196
396
|
// in the meantime.
|
|
197
397
|
execZg(pi, ["server", "on"], ctx, { timeout: 20_000 })
|
|
198
|
-
.then(() =>
|
|
398
|
+
.then(async (result) => {
|
|
399
|
+
await refreshZgState(pi, ctx, state);
|
|
400
|
+
if (result.code === 0) markServerOwnedByPi(state);
|
|
401
|
+
})
|
|
199
402
|
.catch(() => {});
|
|
200
403
|
}
|
|
201
404
|
});
|
|
202
405
|
|
|
406
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
407
|
+
if (!state.ownedServerPid) return;
|
|
408
|
+
|
|
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
|
+
|
|
203
420
|
pi.on("turn_start", async (_event, ctx) => {
|
|
204
421
|
if (state.available) {
|
|
205
422
|
await refreshZgState(pi, ctx, state);
|
|
@@ -231,13 +448,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
231
448
|
);
|
|
232
449
|
}
|
|
233
450
|
|
|
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
|
+
}
|
|
234
454
|
if (!state.indexed && !(await offerToBuildIndex(pi, ctx, state, signal))) {
|
|
235
455
|
throw new Error(
|
|
236
456
|
`This project does not appear to be indexed. Run \`zg index\` manually in ${ctx.cwd}, then retry.`,
|
|
237
457
|
);
|
|
238
458
|
}
|
|
239
459
|
|
|
240
|
-
const args = ["query"];
|
|
460
|
+
const args = ["query", "--mode", state.queryMode, "--refresh", state.refreshPolicy];
|
|
241
461
|
if (params.limit !== undefined) args.push("--limit", String(params.limit));
|
|
242
462
|
args.push(params.query);
|
|
243
463
|
|
|
@@ -323,7 +543,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
323
543
|
throw new Error("`zg` is not available on PATH. Install @zvec/zvec-grep separately before indexing.");
|
|
324
544
|
}
|
|
325
545
|
|
|
326
|
-
const args = ["index"];
|
|
546
|
+
const args = ["index", "--mode", state.queryMode];
|
|
327
547
|
if (params.drop) args.push("--drop", "--yes");
|
|
328
548
|
else if (params.rebuild) args.push("--rebuild");
|
|
329
549
|
|
|
@@ -341,6 +561,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
341
561
|
},
|
|
342
562
|
});
|
|
343
563
|
|
|
564
|
+
pi.registerCommand("zg-settings", {
|
|
565
|
+
description: "Configure Pi search mode plus zg embedding model, device, and provider credentials",
|
|
566
|
+
handler: async (_args, ctx) => {
|
|
567
|
+
await openZgSettings(pi, ctx, state);
|
|
568
|
+
},
|
|
569
|
+
});
|
|
570
|
+
|
|
344
571
|
pi.registerCommand("zg-status", {
|
|
345
572
|
description: "Report zg version, server, and index status for this project",
|
|
346
573
|
handler: async (_args, ctx) => {
|
|
@@ -354,6 +581,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
354
581
|
const lines = [
|
|
355
582
|
`zg: ${state.version || "available"}`,
|
|
356
583
|
`server: ${state.serverRunning ? "running" : "stopped"}`,
|
|
584
|
+
`Pi search mode: ${searchModeLabel(state)} (session setting)`,
|
|
357
585
|
"",
|
|
358
586
|
(detail.stdout || detail.stderr).trim(),
|
|
359
587
|
];
|
|
@@ -378,7 +606,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
378
606
|
if (!confirmed) return;
|
|
379
607
|
}
|
|
380
608
|
|
|
381
|
-
const cmdArgs = ["index"];
|
|
609
|
+
const cmdArgs = ["index", "--mode", state.queryMode];
|
|
382
610
|
if (drop) cmdArgs.push("--drop", "--yes");
|
|
383
611
|
else if (rebuild) cmdArgs.push("--rebuild");
|
|
384
612
|
|
|
@@ -417,8 +645,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
417
645
|
if (!confirmed) return;
|
|
418
646
|
}
|
|
419
647
|
|
|
648
|
+
const wasServerRunning = state.serverRunning;
|
|
420
649
|
const result = await execZg(pi, ["server", action], ctx, { timeout: 20_000 });
|
|
421
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;
|
|
422
653
|
ctx.ui.notify(
|
|
423
654
|
(result.stdout || result.stderr).trim() || `zg server ${action} done.`,
|
|
424
655
|
result.code === 0 ? "info" : "error",
|
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krosskinetic/pi-zg",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Pi extension for zvec-grep (zg): semantic code search, managed ripgrep, and shared-server-aware index management",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"keywords": [
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi",
|
|
9
|
+
"pi-coding-agent",
|
|
10
|
+
"zvec-grep",
|
|
11
|
+
"semantic-search",
|
|
12
|
+
"code-search"
|
|
13
|
+
],
|
|
7
14
|
"license": "MIT",
|
|
8
15
|
"author": "KrossKinetic",
|
|
9
16
|
"repository": {
|
|
@@ -13,9 +20,15 @@
|
|
|
13
20
|
"homepage": "https://github.com/KrossKinetic/pi-zg#readme",
|
|
14
21
|
"bugs": "https://github.com/KrossKinetic/pi-zg/issues",
|
|
15
22
|
"pi": {
|
|
16
|
-
"extensions": [
|
|
23
|
+
"extensions": [
|
|
24
|
+
"./extensions"
|
|
25
|
+
]
|
|
17
26
|
},
|
|
18
|
-
"files": [
|
|
27
|
+
"files": [
|
|
28
|
+
"extensions",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
19
32
|
"peerDependencies": {
|
|
20
33
|
"@earendil-works/pi-coding-agent": "*",
|
|
21
34
|
"typebox": "*"
|