@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 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 three commands
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:pi-zg
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
- - **Never auto-stops it** since it's shared, stopping it could break other
39
- tools relying on it. Use `/zg-server off` to stop it explicitly.
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 auto-stop the shared `zg` server.
144
+ - Does not stop a shared `zg` server that Pi did not start.
@@ -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 (never auto-stops it, since
14
- * it's a daemon other agents/tools may also depend on).
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, { signal, timeout: 300_000 });
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(() => refreshZgState(pi, ctx, state))
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.0",
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": ["pi-package", "pi", "pi-coding-agent", "zvec-grep", "semantic-search", "code-search"],
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": ["./extensions"]
23
+ "extensions": [
24
+ "./extensions"
25
+ ]
17
26
  },
18
- "files": ["extensions", "README.md", "LICENSE"],
27
+ "files": [
28
+ "extensions",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
19
32
  "peerDependencies": {
20
33
  "@earendil-works/pi-coding-agent": "*",
21
34
  "typebox": "*"