@pi-unipi/cocoindex 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/commands.ts ADDED
@@ -0,0 +1,175 @@
1
+ /**
2
+ * commands.ts — CocoIndex command registration
3
+ *
4
+ * Exposes cocoindex operations as Pi commands:
5
+ * - /unipi:cocoindex-update — Run indexing
6
+ * - /unipi:cocoindex-status — Show status
7
+ * - /unipi:cocoindex-init — Scaffold pipeline
8
+ * - /unipi:cocoindex-settings — TUI settings
9
+ *
10
+ * Commands are registered at extension load time (synchronous).
11
+ * Project directory is resolved from ctx.cwd at handler invocation time.
12
+ */
13
+
14
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
15
+ import { COCOINDEX_COMMANDS, COCOINDEX_PACKAGE_SPEC } from "@pi-unipi/core";
16
+ import * as bridge from "./bridge.js";
17
+ import { ensureCocoindex } from "./installer.js";
18
+
19
+ export function registerCocoindexCommands(pi: ExtensionAPI): void {
20
+ // ── /unipi:cocoindex-update ────────────────────────────
21
+ pi.registerCommand(`unipi:${COCOINDEX_COMMANDS.UPDATE}`, {
22
+ description: "Run CocoIndex update to index the current project",
23
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
24
+ const projectDir = (ctx as any).cwd ?? process.cwd();
25
+ const ensured = await ensureCocoindex(ctx);
26
+ if (!ensured.ok) return;
27
+
28
+ const pipelineDir = bridge.getPipelineDir(projectDir);
29
+ const initialized = await bridge.isPipelineInitialized(pipelineDir);
30
+ if (!initialized) {
31
+ ctx.ui.notify("⚠️ Pipeline not initialized. Run /unipi:cocoindex-init first.", "warning");
32
+ return;
33
+ }
34
+
35
+ ctx.ui.notify("🔄 Running CocoIndex update...", "info");
36
+
37
+ const result = await bridge.indexProject(projectDir);
38
+ if (result.success) {
39
+ ctx.ui.notify(
40
+ `✅ CocoIndex update complete: ${result.chunksProcessed} chunks in ${(result.durationMs / 1000).toFixed(1)}s`,
41
+ "info",
42
+ );
43
+ } else {
44
+ ctx.ui.notify(`❌ CocoIndex update failed: ${result.error}`, "error");
45
+ }
46
+ },
47
+ });
48
+
49
+ // ── /unipi:cocoindex-status ────────────────────────────
50
+ pi.registerCommand(`unipi:${COCOINDEX_COMMANDS.STATUS}`, {
51
+ description: "Show CocoIndex indexing status",
52
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
53
+ const projectDir = (ctx as any).cwd ?? process.cwd();
54
+ const info = await bridge.status(projectDir);
55
+ const lines = [
56
+ "📦 CocoIndex Status",
57
+ `CLI: ${info.cliAvailable ? "✅ installed" : "❌ not found"}`,
58
+ `Pipeline: ${info.pipelineConfigured ? "✅ configured" : "❌ not initialized"}`,
59
+ `Target: ${info.targetStore}`,
60
+ `Data: ${info.indexed ? `✅ ${info.docCount} documents` : "— no data"}`,
61
+ `Last run: ${info.lastRun ?? "never"}`,
62
+ ];
63
+
64
+ if (!info.cliAvailable) {
65
+ lines.push(
66
+ "",
67
+ "Setup: run /unipi:cocoindex-init for guided install.",
68
+ `Manual: uv tool install '${COCOINDEX_PACKAGE_SPEC}'`,
69
+ "Fallback: mise use -g uv@latest",
70
+ );
71
+ }
72
+ if (!info.pipelineConfigured) {
73
+ lines.push("", "Initialize: /unipi:cocoindex-init");
74
+ }
75
+
76
+ ctx.ui.notify(lines.join("\n"), "info");
77
+ },
78
+ });
79
+
80
+ // ── /unipi:cocoindex-init ──────────────────────────────
81
+ pi.registerCommand(`unipi:${COCOINDEX_COMMANDS.INIT}`, {
82
+ description: "Initialize CocoIndex pipeline for the current project",
83
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
84
+ const projectDir = (ctx as any).cwd ?? process.cwd();
85
+ const ensured = await ensureCocoindex(ctx);
86
+ if (!ensured.ok) return;
87
+
88
+ const result = await bridge.initPipeline(projectDir);
89
+ if (result.success) {
90
+ ctx.ui.notify(
91
+ "✅ CocoIndex pipeline initialized at .unipi/cocoindex/main.py\n" +
92
+ "Run /unipi:cocoindex-update to start indexing.",
93
+ "info",
94
+ );
95
+ } else {
96
+ ctx.ui.notify(`❌ Init failed: ${result.error}`, "error");
97
+ }
98
+ },
99
+ });
100
+
101
+ // ── /unipi:cocoindex-search ──────────────────────────
102
+ pi.registerCommand(`unipi:${COCOINDEX_COMMANDS.SEARCH}`, {
103
+ description: "Search indexed codebase with semantic/vector, full-text, or lexical search",
104
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
105
+ const projectDir = (ctx as any).cwd ?? process.cwd();
106
+ const query = args.trim();
107
+
108
+ if (!query) {
109
+ ctx.ui.notify("Usage: /unipi:cocoindex-search <query>", "warning");
110
+ return;
111
+ }
112
+
113
+ const pipelineDir = bridge.getPipelineDir(projectDir);
114
+ const lancedbPath = bridge.getPipelineDir(projectDir) + "/.lancedb";
115
+ const { existsSync } = await import("fs");
116
+ if (!existsSync(lancedbPath)) {
117
+ ctx.ui.notify("❌ No index found. Run /unipi:cocoindex-update first.", "error");
118
+ return;
119
+ }
120
+
121
+ ctx.ui.notify(`🔍 Searching: "${query}"...`, "info");
122
+
123
+ try {
124
+ const results = await bridge.search(projectDir, query, { limit: 10 });
125
+
126
+ if (results.length === 0) {
127
+ ctx.ui.notify(
128
+ `No results for "${query}" in the current CocoIndex data. Run /unipi:cocoindex-update if the index may be stale.`,
129
+ "info",
130
+ );
131
+ return;
132
+ }
133
+
134
+ const lines = results.map((r, i) => {
135
+ const dist = r.rank != null ? ` (${r.rank.toFixed(3)})` : "";
136
+ const file = r.source || r.title;
137
+ const snippet = r.content.slice(0, 150).replace(/\n/g, " ");
138
+ return `${i + 1}. ${file}${dist}\n ${snippet}...`;
139
+ });
140
+ ctx.ui.notify(
141
+ `🔍 ${results.length} results for "${query}":\n\n${lines.join("\n\n")}`,
142
+ "info",
143
+ );
144
+ } catch (err: any) {
145
+ ctx.ui.notify(`❌ Search failed: ${err.message}`, "error");
146
+ }
147
+ },
148
+ });
149
+
150
+ // ── /unipi:cocoindex-settings ──────────────────────────
151
+ pi.registerCommand(`unipi:${COCOINDEX_COMMANDS.SETTINGS}`, {
152
+ description: "Show CocoIndex configuration and settings",
153
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
154
+ const projectDir = (ctx as any).cwd ?? process.cwd();
155
+ const pipelineDir = bridge.getPipelineDir(projectDir);
156
+ const info = await bridge.status(projectDir);
157
+ const lines = [
158
+ "⚙️ CocoIndex Settings",
159
+ "",
160
+ `Pipeline: ${pipelineDir}/main.py`,
161
+ `Target store: ${info.targetStore}`,
162
+ `Embedding model: (from memory config)`,
163
+ "",
164
+ "Configuration:",
165
+ " • Pipeline file: .unipi/cocoindex/main.py",
166
+ " • Data store: .unipi/cocoindex/.lancedb/",
167
+ " • Embedding config: ~/.unipi/memory/config.json",
168
+ "",
169
+ "To customize, edit .unipi/cocoindex/main.py directly.",
170
+ ];
171
+
172
+ ctx.ui.notify(lines.join("\n"), "info");
173
+ },
174
+ });
175
+ }
package/index.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @pi-unipi/cocoindex — CocoIndex integration for Pi
3
+ *
4
+ * Bridges Pi to CocoIndex CLI for AST-aware content indexing,
5
+ * semantic vector search, and incremental pipeline management.
6
+ *
7
+ * Default target store: LanceDB (zero-config, local file-based).
8
+ * Embedding model: reuses memory package settings (OpenRouter API key + model).
9
+ */
10
+
11
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
12
+ import { MODULES, UNIPI_EVENTS, COCOINDEX_TOOLS, COCOINDEX_COMMANDS, COCOINDEX_PACKAGE_SPEC, emitEvent } from "@pi-unipi/core";
13
+ import { registerCocoindexTools } from "./tools.js";
14
+ import { registerCocoindexCommands } from "./commands.js";
15
+ import * as bridge from "./bridge.js";
16
+
17
+ export default function cocoindexExtension(pi: ExtensionAPI): void {
18
+ // Register commands at extension load time (synchronous).
19
+ // Commands resolve projectDir from ctx.cwd at handler invocation time.
20
+ registerCocoindexCommands(pi);
21
+
22
+ pi.on("session_start", async (_event, ctx) => {
23
+ const projectDir = (ctx as any).cwd ?? process.cwd();
24
+
25
+ // Register tools — these need projectDir for search context
26
+ const pipelineDir = bridge.getPipelineDir(projectDir);
27
+ const initialized = await bridge.isPipelineInitialized(pipelineDir);
28
+
29
+ registerCocoindexTools(pi, {
30
+ projectDir,
31
+ pipelineDir,
32
+ initialized,
33
+ });
34
+
35
+ emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
36
+ name: MODULES.COCOINDEX,
37
+ version: "0.1.0",
38
+ commands: Object.values(COCOINDEX_COMMANDS),
39
+ tools: Object.values(COCOINDEX_TOOLS),
40
+ });
41
+
42
+ const available = await bridge.isAvailable();
43
+ if (available) {
44
+ ctx.ui.notify("📦 CocoIndex ready", "info");
45
+ } else {
46
+ ctx.ui.notify(
47
+ `📦 CocoIndex: CLI not found — run /unipi:cocoindex-init for guided install (manual: uv tool install '${COCOINDEX_PACKAGE_SPEC}').`,
48
+ "info",
49
+ );
50
+ }
51
+ });
52
+ }
53
+
54
+ export { bridge };
55
+ export * as installer from "./installer.js";
package/installer.ts ADDED
@@ -0,0 +1,397 @@
1
+ /**
2
+ * installer.ts — Consent-based CocoIndex CLI installer helpers.
3
+ *
4
+ * Pure planning helpers are separated from side-effecting execution so command
5
+ * handlers can show the exact install plan before running anything.
6
+ */
7
+
8
+ import { execFileSync, spawn } from "node:child_process";
9
+ import { homedir } from "node:os";
10
+ import { delimiter, join } from "node:path";
11
+ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
12
+ import { COCOINDEX_MIN_VERSION, COCOINDEX_PACKAGE_SPEC } from "@pi-unipi/core";
13
+ import * as bridge from "./bridge.js";
14
+
15
+ export type SupportedShell = "bash" | "zsh" | "fish" | "unknown";
16
+ export type InstallPlanKind = "auto" | "manual";
17
+
18
+ export interface InstallStep {
19
+ /** Executable name/path. */
20
+ command: string;
21
+ /** Argv passed to the executable. Prefer this over shell strings. */
22
+ args?: string[];
23
+ /** User-facing command display for consent/errors. */
24
+ displayCommand: string;
25
+ /** User-facing description of the step. */
26
+ description: string;
27
+ /** Optional steps may fail without aborting the whole plan. */
28
+ optional?: boolean;
29
+ /** Timeout for this step. Defaults to 10 minutes. */
30
+ timeoutMs?: number;
31
+ }
32
+
33
+ export interface InstallPlan {
34
+ kind: InstallPlanKind;
35
+ steps: InstallStep[];
36
+ summary: string;
37
+ shell: SupportedShell;
38
+ manualInstructions?: string[];
39
+ }
40
+
41
+ export interface InstallResult {
42
+ ok: boolean;
43
+ binPath?: string;
44
+ version?: string;
45
+ error?: string;
46
+ skipped?: boolean;
47
+ stdout?: string;
48
+ stderr?: string;
49
+ failedStep?: InstallStep;
50
+ manualInstructions?: string[];
51
+ }
52
+
53
+ export type InstallProgress = (message: string, step?: InstallStep) => void;
54
+
55
+ const DEFAULT_STEP_TIMEOUT_MS = 10 * 60 * 1000;
56
+
57
+ /** Detect the user's login shell for shell-aware manual instructions. */
58
+ export function detectShell(): SupportedShell {
59
+ const shell = process.env.SHELL ?? "";
60
+ if (shell.includes("zsh")) return "zsh";
61
+ if (shell.includes("bash")) return "bash";
62
+ if (shell.includes("fish")) return "fish";
63
+ return "unknown";
64
+ }
65
+
66
+ /** Safely check whether a command is on PATH. */
67
+ export function hasTool(name: string): boolean {
68
+ // Defend against accidental shell metacharacters even though the name is
69
+ // supplied internally today.
70
+ if (!/^[A-Za-z0-9._+-]+$/.test(name)) return false;
71
+
72
+ try {
73
+ const result = execFileSync("sh", ["-c", "command -v -- \"$1\"", "sh", name], {
74
+ encoding: "utf-8",
75
+ timeout: 3000,
76
+ stdio: ["pipe", "pipe", "pipe"],
77
+ env: installerEnv(),
78
+ });
79
+ return result.trim().length > 0;
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ /** Compute an install plan without executing any commands. */
86
+ export function dryRun(): InstallPlan {
87
+ const shell = detectShell();
88
+
89
+ if (hasTool("uv")) {
90
+ return automaticPlan(shell, [uvInstallStep()]);
91
+ }
92
+
93
+ if (hasTool("mise")) {
94
+ return automaticPlan(shell, [
95
+ {
96
+ command: "mise",
97
+ args: ["use", "-g", "uv@latest"],
98
+ displayCommand: "mise use -g uv@latest",
99
+ description: "Install and activate uv with mise",
100
+ },
101
+ uvInstallStep(),
102
+ ]);
103
+ }
104
+
105
+ const manualInstructions = buildManualInstructions(shell);
106
+ return {
107
+ kind: "manual",
108
+ steps: [],
109
+ shell,
110
+ manualInstructions,
111
+ summary: [
112
+ "CocoIndex CLI is not installed, and neither uv nor mise was found on PATH.",
113
+ "",
114
+ ...manualInstructions,
115
+ ].join("\n"),
116
+ };
117
+ }
118
+
119
+ /** Execute an automatic install plan sequentially. */
120
+ export async function execute(plan: InstallPlan, onProgress?: InstallProgress): Promise<InstallResult> {
121
+ if (plan.kind === "manual") {
122
+ return {
123
+ ok: false,
124
+ error: "Manual CocoIndex installation is required.",
125
+ manualInstructions: plan.manualInstructions,
126
+ };
127
+ }
128
+
129
+ for (const step of plan.steps) {
130
+ onProgress?.(step.description, step);
131
+ const result = await runStep(step);
132
+
133
+ if (result.ok) continue;
134
+ if (step.optional) continue;
135
+
136
+ const isMiseStep = step.command === "mise";
137
+ const manualInstructions = isMiseStep ? buildUvInstallerFallback(plan.shell) : undefined;
138
+ const fallback = manualInstructions
139
+ ? `\n\nFallback uv installer:\n${manualInstructions.join("\n")}`
140
+ : "";
141
+
142
+ return {
143
+ ok: false,
144
+ error: `Install step failed: ${step.displayCommand}\n${summarizeCommandOutput(result)}${fallback}`,
145
+ stdout: result.stdout,
146
+ stderr: result.stderr,
147
+ failedStep: step,
148
+ manualInstructions,
149
+ };
150
+ }
151
+
152
+ return { ok: true };
153
+ }
154
+
155
+ /**
156
+ * Ensure CocoIndex v1.0+ exists, prompting the user before any install.
157
+ * Tools should not call this; it is intended for interactive commands only.
158
+ */
159
+ export async function ensureCocoindex(ctx: ExtensionCommandContext): Promise<InstallResult> {
160
+ const existing = await getExistingInstall();
161
+ if (existing.ok) return existing;
162
+ if (existing.error === "upgrade-needed") {
163
+ const message = upgradeMessage(existing.version);
164
+ notify(ctx, message, "warning");
165
+ return { ok: false, error: message, version: existing.version };
166
+ }
167
+
168
+ const plan = dryRun();
169
+ if (plan.kind === "manual") {
170
+ const message = plan.summary;
171
+ notify(ctx, message, "warning");
172
+ return { ok: false, error: "Manual CocoIndex installation is required.", manualInstructions: plan.manualInstructions };
173
+ }
174
+
175
+ if (!hasConfirm(ctx)) {
176
+ const message = [
177
+ "CocoIndex CLI is not installed and this command is running without an interactive confirmation UI.",
178
+ "Please install manually, then re-run /unipi:cocoindex-init:",
179
+ ` uv tool install '${COCOINDEX_PACKAGE_SPEC}'`,
180
+ ].join("\n");
181
+ notify(ctx, message, "warning");
182
+ return { ok: false, error: message };
183
+ }
184
+
185
+ const confirmed = await ctx.ui.confirm("Install CocoIndex?", plan.summary);
186
+ if (!confirmed) {
187
+ notify(ctx, "CocoIndex install skipped. /unipi:cocoindex-init can try again later.", "info");
188
+ return { ok: false, skipped: true };
189
+ }
190
+
191
+ try {
192
+ const result = await execute(plan, (message) => {
193
+ setStatus(ctx, message);
194
+ });
195
+
196
+ if (!result.ok) {
197
+ notify(ctx, `❌ CocoIndex install failed:\n${result.error ?? "Unknown error"}`, "error");
198
+ return result;
199
+ }
200
+
201
+ bridge.resetAvailabilityCache();
202
+ const verified = await getExistingInstall();
203
+ if (verified.ok) {
204
+ notify(ctx, `✅ CocoIndex ${verified.version ?? ""} installed and ready.`, "info");
205
+ return verified;
206
+ }
207
+
208
+ const message = verified.error === "upgrade-needed"
209
+ ? upgradeMessage(verified.version)
210
+ : "CocoIndex installation finished, but the CLI could not be verified. Make sure ~/.local/bin is on PATH.";
211
+ notify(ctx, message, verified.error === "upgrade-needed" ? "warning" : "error");
212
+ return { ok: false, error: message, version: verified.version };
213
+ } finally {
214
+ setStatus(ctx, undefined);
215
+ }
216
+ }
217
+
218
+ async function getExistingInstall(): Promise<InstallResult & { error?: string }> {
219
+ const available = await bridge.isAvailable({ useCache: false });
220
+ if (!available) return { ok: false, error: "missing" };
221
+
222
+ const versionOutput = await bridge.getVersion();
223
+ const version = versionOutput ? bridge.parseVersion(versionOutput) : null;
224
+ if (!bridge.isVersionAtLeast(version, COCOINDEX_MIN_VERSION)) {
225
+ return { ok: false, error: "upgrade-needed", version: version ?? versionOutput ?? undefined };
226
+ }
227
+
228
+ return {
229
+ ok: true,
230
+ binPath: bridge.getCocoindexBinPath(),
231
+ version: version ?? versionOutput ?? undefined,
232
+ };
233
+ }
234
+
235
+ function upgradeMessage(version: string | undefined): string {
236
+ return [
237
+ `CocoIndex ${COCOINDEX_MIN_VERSION}+ is required${version ? ` (found ${version})` : ""}.`,
238
+ "Please upgrade before continuing:",
239
+ " uv tool upgrade cocoindex",
240
+ `or reinstall with: uv tool install '${COCOINDEX_PACKAGE_SPEC}' --force`,
241
+ ].join("\n");
242
+ }
243
+
244
+ function hasConfirm(ctx: ExtensionCommandContext): boolean {
245
+ const anyCtx = ctx as any;
246
+ return anyCtx.hasUI === true && typeof anyCtx.ui?.confirm === "function";
247
+ }
248
+
249
+ function notify(ctx: ExtensionCommandContext, message: string, type: "info" | "warning" | "error"): void {
250
+ const anyCtx = ctx as any;
251
+ if (typeof anyCtx.ui?.notify === "function") anyCtx.ui.notify(message, type);
252
+ }
253
+
254
+ function setStatus(ctx: ExtensionCommandContext, message: string | undefined): void {
255
+ const anyCtx = ctx as any;
256
+ if (anyCtx.hasUI === true && typeof anyCtx.ui?.setStatus === "function") {
257
+ anyCtx.ui.setStatus("cocoindex-installer", message);
258
+ }
259
+ }
260
+
261
+ function automaticPlan(shell: SupportedShell, steps: InstallStep[]): InstallPlan {
262
+ const commandLines = steps.map((step) => ` • ${step.displayCommand}`);
263
+ return {
264
+ kind: "auto",
265
+ steps,
266
+ shell,
267
+ summary: [
268
+ "Pi can install CocoIndex CLI with LanceDB support for this user account.",
269
+ "",
270
+ `Package: ${COCOINDEX_PACKAGE_SPEC}`,
271
+ "Commands to run:",
272
+ ...commandLines,
273
+ "",
274
+ "This uses isolated uv tool environments and exposes the `cocoindex` binary under ~/.local/bin/.",
275
+ ].join("\n"),
276
+ };
277
+ }
278
+
279
+ function uvInstallStep(): InstallStep {
280
+ return {
281
+ command: "uv",
282
+ args: ["tool", "install", COCOINDEX_PACKAGE_SPEC],
283
+ displayCommand: `uv tool install '${COCOINDEX_PACKAGE_SPEC}'`,
284
+ description: "Install CocoIndex CLI with LanceDB support using uv",
285
+ };
286
+ }
287
+
288
+ function buildManualInstructions(shell: SupportedShell): string[] {
289
+ const uvInstructions = buildUvInstallerFallback(shell);
290
+ return [
291
+ "Manual installation required:",
292
+ "1. Install uv:",
293
+ ...uvInstructions.map((line) => ` ${line}`),
294
+ "2. Restart your shell if instructed by the installer.",
295
+ `3. Run: uv tool install '${COCOINDEX_PACKAGE_SPEC}'`,
296
+ "4. Re-run /unipi:cocoindex-init.",
297
+ "",
298
+ "If you prefer mise, install mise from https://mise.jdx.dev/getting-started.html, then run:",
299
+ " mise use -g uv@latest",
300
+ ` uv tool install '${COCOINDEX_PACKAGE_SPEC}'`,
301
+ ];
302
+ }
303
+
304
+ function buildUvInstallerFallback(shell: SupportedShell): string[] {
305
+ if (shell === "fish") {
306
+ return [
307
+ "curl -LsSf https://astral.sh/uv/install.sh | sh",
308
+ "fish_add_path ~/.local/bin",
309
+ ];
310
+ }
311
+
312
+ if (shell === "bash" || shell === "zsh") {
313
+ return [
314
+ "curl -LsSf https://astral.sh/uv/install.sh | sh",
315
+ "export PATH=\"$HOME/.local/bin:$PATH\"",
316
+ ];
317
+ }
318
+
319
+ return [
320
+ "curl -LsSf https://astral.sh/uv/install.sh | sh",
321
+ "Add ~/.local/bin to PATH for your shell.",
322
+ ];
323
+ }
324
+
325
+ interface StepRunResult {
326
+ ok: boolean;
327
+ stdout: string;
328
+ stderr: string;
329
+ exitCode?: number | null;
330
+ signal?: NodeJS.Signals | null;
331
+ }
332
+
333
+ function runStep(step: InstallStep): Promise<StepRunResult> {
334
+ return new Promise((resolve) => {
335
+ const proc = spawn(step.command, step.args ?? [], {
336
+ stdio: ["ignore", "pipe", "pipe"],
337
+ env: installerEnv(),
338
+ });
339
+
340
+ let stdout = "";
341
+ let stderr = "";
342
+ let settled = false;
343
+
344
+ const timeout = setTimeout(() => {
345
+ if (settled) return;
346
+ proc.kill("SIGTERM");
347
+ stderr += `\nTimed out after ${step.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS}ms.`;
348
+ }, step.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS);
349
+
350
+ proc.stdout.on("data", (data: Buffer) => {
351
+ stdout += data.toString();
352
+ });
353
+
354
+ proc.stderr.on("data", (data: Buffer) => {
355
+ stderr += data.toString();
356
+ });
357
+
358
+ proc.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
359
+ settled = true;
360
+ clearTimeout(timeout);
361
+ resolve({ ok: code === 0, stdout, stderr, exitCode: code, signal });
362
+ });
363
+
364
+ proc.on("error", (err: Error) => {
365
+ settled = true;
366
+ clearTimeout(timeout);
367
+ resolve({ ok: false, stdout, stderr: stderr || err.message });
368
+ });
369
+ });
370
+ }
371
+
372
+ function installerEnv(): NodeJS.ProcessEnv {
373
+ const home = homedir();
374
+ const extraPaths = [
375
+ join(home, ".local", "bin"),
376
+ join(home, ".local", "share", "mise", "shims"),
377
+ ];
378
+ const currentPath = process.env.PATH ?? "";
379
+ return {
380
+ ...process.env,
381
+ PATH: [...extraPaths, currentPath].filter(Boolean).join(delimiter),
382
+ };
383
+ }
384
+
385
+ function summarizeCommandOutput(result: StepRunResult): string {
386
+ const parts: string[] = [];
387
+ if (result.exitCode !== undefined) parts.push(`exit code: ${result.exitCode}`);
388
+ if (result.signal) parts.push(`signal: ${result.signal}`);
389
+ if (result.stderr.trim()) parts.push(`stderr:\n${tail(result.stderr.trim())}`);
390
+ if (result.stdout.trim()) parts.push(`stdout:\n${tail(result.stdout.trim())}`);
391
+ return parts.join("\n") || "No command output was captured.";
392
+ }
393
+
394
+ function tail(value: string, maxChars = 4000): string {
395
+ if (value.length <= maxChars) return value;
396
+ return `…${value.slice(value.length - maxChars)}`;
397
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@pi-unipi/cocoindex",
3
+ "version": "2.0.0",
4
+ "description": "CocoIndex integration for Pi — AST-aware content indexing, semantic vector search, and incremental pipeline management",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "types": "index.ts",
8
+ "license": "MIT",
9
+ "author": "Neuron Mr White",
10
+ "keywords": [
11
+ "pi-package",
12
+ "pi-extension",
13
+ "pi-coding-agent",
14
+ "cocoindex",
15
+ "content-indexing",
16
+ "vector-search"
17
+ ],
18
+ "scripts": {
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "peerDependencies": {
22
+ "@mariozechner/pi-coding-agent": "*",
23
+ "@mariozechner/pi-ai": "*",
24
+ "@mariozechner/pi-tui": "*",
25
+ "@sinclair/typebox": "*"
26
+ },
27
+ "dependencies": {
28
+ "@pi-unipi/core": "*"
29
+ },
30
+ "optionalDependencies": {
31
+ "@lancedb/lancedb": "^0.21.0"
32
+ },
33
+ "files": [
34
+ "index.ts",
35
+ "bridge.ts",
36
+ "installer.ts",
37
+ "tools.ts",
38
+ "commands.ts",
39
+ "skills/**/*",
40
+ "README.md"
41
+ ]
42
+ }