@krosskinetic/pi-zg 0.1.1 → 0.1.3

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,13 +1,10 @@
1
- **WORK IN PROGRESS / ALPHA**
2
-
3
1
  # @krosskinetic/pi-zg
4
2
 
5
3
  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 manages
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 four commands
9
- (`/zg-settings`, `/zg-status`, `/zg-index`, `/zg-server`). It calls an existing local `zg` CLI;
10
- it does not install or bundle zvec-grep itself.
4
+ [zvec-grep](https://github.com/) (`zg`) semantic code search into Pi: it offers
5
+ to build a missing index interactively, and gives the agent four tools
6
+ (`zg_search`, `zg_rg`, `zg_index`, `zg_status`) plus three commands
7
+ (`/zg-settings`, `/zg-status`, `/zg-index`). It calls an existing local `zg` CLI;
11
8
 
12
9
  ## Prerequisites
13
10
 
@@ -32,9 +29,9 @@ pi -e .
32
29
  1. Install and configure `zg`, then verify that `zg version` works from your
33
30
  project directory.
34
31
  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.
32
+ 3. At session start, pi-zg checks whether `zg` is available and whether the
33
+ project has an index. Each `zg_search` refreshes the index first, so
34
+ results are never stale no server daemon is used.
38
35
  4. Ask Pi a natural-language code-search question. Its `zg_search` tool
39
36
  searches the index. If the project has not been indexed, pi-zg offers to
40
37
  build one interactively.
@@ -44,22 +41,16 @@ exact identifier, literal, or regex search, Pi can use `zg_rg` instead.
44
41
 
45
42
  ## What it does
46
43
 
47
- ### Shared server (`zg server`)
48
-
49
- `zg` runs a shared, loopback daemon that other tools (Claude, Cursor, etc.,
50
- configured via `zg install`) may also use. This extension:
44
+ ### Always-fresh direct searches
51
45
 
52
- - **Auto-starts** it at session start if it isn't already running
53
- (`--no-zg-autostart` disables this). `zg server on` is idempotent, so this
54
- is safe to run every session.
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.
46
+ This extension runs every search in **direct mode and refreshes the index before
47
+ answering** (`zg query --mode direct --refresh wait`). It does **not** use or manage
48
+ zg's shared server daemon — there is no background process to keep alive, start,
49
+ or shut down, so nothing can linger as an orphan between sessions.
59
50
 
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.
51
+ Because each semantic search rebuilds any stale parts of the index first, results
52
+ are never stale: after editing a file, the next `zg_search` reflects the change.
53
+ This also means no index auto-refresh daemon needs to run at all.
63
54
 
64
55
  ### Tools (LLM-callable)
65
56
 
@@ -73,39 +64,48 @@ project has an initial index.
73
64
  does not require an index.
74
65
  - **`zg_index`** — build, rebuild, or drop the persistent index. Gated by
75
66
  prompt guidelines so the agent only uses it when the user explicitly asks.
67
+ - **`zg_status`** — report zg version and the current project's index status.
68
+ Reads pi's cached zg state, so no extra `zg` subprocess is spawned for it.
76
69
 
77
70
  `zg_search` accepts a natural-language `query` and an optional `limit` of
78
71
  1–100 results (zg defaults to 7). `zg_rg` accepts a regex `pattern`, optional
79
72
  paths, `fixedString` for literal matching, and one `glob` filter. Its output
80
73
  and the output of the other tools are capped at 2,000 lines or 50 KB.
81
74
 
75
+
76
+ #### Choosing a search tool
77
+
78
+ Use the right tool for the kind of query, and don't reach for grep when semantic
79
+ search is the better fit:
80
+
81
+ - **`zg_search`** — find code by **meaning**. Use when you don't know the exact
82
+ identifiers or wording (“where is token refresh handled”, “how does the cache
83
+ layer work”). Keyword grep would miss these.
84
+ - **`zg_rg`** — find code by **exact text**: a known identifier, string literal, or
85
+ regex, honoring the project's ignore/glob rules.
86
+ - **Pi's built-in `grep`** — a quick literal scan when you don't need zg's
87
+ ignore rules or rg features.
88
+
89
+ In short: concept → `zg_search`; exact token/regex → `zg_rg`; everything is
90
+ refreshed before answering, so you always search current code.
82
91
  ### Commands (human-invoked)
83
92
 
84
- - **`/zg-settings`** — interactive configuration for Pi's search and refresh
85
- mode, default embedding model, embedding device, and provider API key.
86
- - **`/zg-status`** — zg version, server state, and index status/coverage.
93
+ - **`/zg-settings`** — interactive configuration for zg's default embedding
94
+ model, embedding device, and provider API key.
95
+ - **`/zg-status`** — zg version and index status/coverage.
87
96
  - **`/zg-index [--rebuild|--drop]`** — build, rebuild, or drop the index
88
97
  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
98
 
92
99
  ### Settings (`/zg-settings`)
93
100
 
94
101
  `/zg-settings` exposes the configuration that most directly affects Pi search:
95
102
 
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
103
  - **Default embedding model** — sets zg's persistent default for newly built
103
104
  indexes, for example `local/potion-code-16m-v2` or
104
105
  `qwen/text-embedding-v4`. Existing indexes keep their recorded embedding
105
106
  schema.
106
107
  - **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.
108
+ `cuda` for a specified local model.
109
109
  - **Provider API key** — saves credentials for a named embedding provider via
110
110
  `zg config provider set`. The value is passed directly to zg and is not
111
111
  shown in Pi notifications.
@@ -115,13 +115,11 @@ other less-common settings, use the underlying `zg` CLI directly.
115
115
 
116
116
  ### Status
117
117
 
118
- The footer shows `server ●/○` and `index ✓/✗` (colored via the active
119
- theme), refreshed at session start and at the start of every turn.
118
+ The footer shows `index ✓/✗` (colored via the active theme), refreshed at
119
+ session start and at the start of every turn.
120
120
 
121
121
  ### Flags
122
122
 
123
- - `--no-zg-autostart` — disable automatically starting the shared server at
124
- Pi session start.
125
123
  - `--no-zg-onboard` — disable the interactive "build an index?" offer;
126
124
  `zg_search` fails with a manual-fix message instead (useful for
127
125
  non-interactive/scripted `pi -p` runs).
@@ -129,7 +127,6 @@ theme), refreshed at session start and at the start of every turn.
129
127
  Pass these when launching Pi, for example:
130
128
 
131
129
  ```bash
132
- pi --no-zg-autostart
133
130
  pi --no-zg-onboard
134
131
  ```
135
132
 
@@ -137,8 +134,7 @@ pi --no-zg-onboard
137
134
 
138
135
  - Does not register zg's MCP server as an actual MCP tool source inside Pi
139
136
  — Pi extensions have no MCP-client API, so integration stays CLI-based
140
- (`pi.exec`), just daemon-aware and stateful rather than re-deriving status
141
- via subprocess spawns before every call.
137
+ (`pi.exec`), stateful rather than re-deriving status via subprocess spawns
138
+ before every call.
142
139
  - Does not override Pi's built-in `grep` tool. `zg_search`/`zg_rg` are
143
140
  purely additive.
144
- - Does not stop a shared `zg` server that Pi did not start.
@@ -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
- * - Auto-starts zg's shared server at session start so queries get
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
- * - 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, `${server} ${index}`);
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
- 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
-
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 [server, status] = await Promise.all([
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, state: ZgState): Promise<void> {
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, state: ZgState): Promise<void> {
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(`zg settings — ${searchModeLabel(state)}`, [
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, state);
161
+ await configureDefaultModel(pi, ctx);
289
162
  break;
290
163
  case "Embedding device":
291
- await configureEmbeddingDevice(pi, ctx, state);
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", state.queryMode], ctx, { signal, timeout: 300_000 });
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", state.queryMode, "--embedding", model], ctx, {
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
- pi.on("session_start", async (_event, ctx) => {
391
- await refreshZgState(pi, ctx, state);
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
- "Semantic code search over the current project's zvec-grep index. Requires zg on PATH; offers to build a missing index interactively when possible. With the zg server running, the index refreshes in the background automatically. Returns at most 2,000 lines or 50 KB of CLI output.",
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", state.queryMode, "--refresh", state.refreshPolicy];
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`). Respects this project's configured ignore/glob rules. Complements zg_search (semantic) and the built-in grep tool; does not require an index.",
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: with the zg server running, an existing index already refreshes in the background after edits, so this is mainly for the first-time build, an explicit rebuild, or dropping the index.",
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", state.queryMode];
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 Pi search mode plus zg embedding model, device, and provider credentials",
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, server, and index status for this project",
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", state.queryMode];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krosskinetic/pi-zg",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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
6
  "keywords": [