codiedev 0.9.70 → 0.9.72

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.
@@ -0,0 +1,25 @@
1
+ export declare const PI_PACKAGE = "@mariozechner/pi-coding-agent";
2
+ export declare const PI_VERSION = "0.73.1";
3
+ export declare const PI_RUNTIME_SPEC = "@mariozechner/pi-coding-agent@0.73.1";
4
+ export declare function piRuntimeDir(stateDir: string): string;
5
+ /** The installed pi's CLI entry, when it is the pinned version. */
6
+ export declare function installedPiEntry(installDir: string, readFile?: (file: string) => string): string | null;
7
+ export interface PiRuntimeDeps {
8
+ stateDir: string;
9
+ exists?: (file: string) => boolean;
10
+ readFile?: (file: string) => string;
11
+ makeStagingDir?: () => string;
12
+ run?: (args: string[]) => {
13
+ status: number | null;
14
+ stderr: string;
15
+ };
16
+ rename?: (from: string, to: string) => void;
17
+ remove?: (target: string) => void;
18
+ log?: (message: string) => void;
19
+ }
20
+ /**
21
+ * The pi CLI entry to run, installing the pinned version first if it is
22
+ * missing or a different version. Staged then renamed, so an interrupted
23
+ * download never leaves a half-installed pi that looks ready.
24
+ */
25
+ export declare function ensurePiRuntime(deps: PiRuntimeDeps): string;
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PI_RUNTIME_SPEC = exports.PI_VERSION = exports.PI_PACKAGE = void 0;
37
+ exports.piRuntimeDir = piRuntimeDir;
38
+ exports.installedPiEntry = installedPiEntry;
39
+ exports.ensurePiRuntime = ensurePiRuntime;
40
+ // pi for `codie --pi`, installed on first use like the Codie engine
41
+ // (codieRuntimeInstall.ts): developers never run a second npm install, and
42
+ // every machine runs the version Codiedev tested its capture against.
43
+ //
44
+ // pi is plain JavaScript, so it runs on the same Node as `codie` from its
45
+ // package entry: no PATH lookup, no Windows .cmd shim, and no dependency on
46
+ // whatever `pi` (if any) the developer installed globally.
47
+ const fs = __importStar(require("fs"));
48
+ const path = __importStar(require("path"));
49
+ const child_process_1 = require("child_process");
50
+ const codieRuntimeInstall_1 = require("./codieRuntimeInstall");
51
+ exports.PI_PACKAGE = "@mariozechner/pi-coding-agent";
52
+ exports.PI_VERSION = "0.73.1";
53
+ exports.PI_RUNTIME_SPEC = `${exports.PI_PACKAGE}@${exports.PI_VERSION}`;
54
+ function piRuntimeDir(stateDir) {
55
+ return path.join(stateDir, "runtime-pi");
56
+ }
57
+ function packageDir(installDir) {
58
+ return path.join(installDir, "node_modules", ...exports.PI_PACKAGE.split("/"));
59
+ }
60
+ /** The installed pi's CLI entry, when it is the pinned version. */
61
+ function installedPiEntry(installDir, readFile = (f) => fs.readFileSync(f, "utf8")) {
62
+ try {
63
+ const pkg = JSON.parse(readFile(path.join(packageDir(installDir), "package.json")));
64
+ if (pkg.version !== exports.PI_VERSION)
65
+ return null;
66
+ const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.pi;
67
+ return typeof bin === "string" ? path.join(packageDir(installDir), bin) : null;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ /**
74
+ * The pi CLI entry to run, installing the pinned version first if it is
75
+ * missing or a different version. Staged then renamed, so an interrupted
76
+ * download never leaves a half-installed pi that looks ready.
77
+ */
78
+ function ensurePiRuntime(deps) {
79
+ const installDir = piRuntimeDir(deps.stateDir);
80
+ const exists = deps.exists ?? fs.existsSync;
81
+ const readFile = deps.readFile ?? ((f) => fs.readFileSync(f, "utf8"));
82
+ const ready = installedPiEntry(installDir, readFile);
83
+ if (ready && exists(ready))
84
+ return ready;
85
+ const remove = deps.remove ?? ((target) => fs.rmSync(target, { recursive: true, force: true }));
86
+ const rename = deps.rename ?? fs.renameSync;
87
+ const log = deps.log ?? ((message) => console.error(message));
88
+ const staging = (deps.makeStagingDir ?? (() => {
89
+ fs.mkdirSync(deps.stateDir, { recursive: true });
90
+ return fs.mkdtempSync(path.join(deps.stateDir, ".runtime-pi-"));
91
+ }))();
92
+ const run = deps.run ?? ((args) => {
93
+ const result = (0, child_process_1.spawnSync)(process.platform === "win32" ? "npm.cmd" : "npm", args, {
94
+ encoding: "utf8", stdio: ["ignore", "ignore", "pipe"], shell: process.platform === "win32",
95
+ });
96
+ if (result.error)
97
+ throw result.error;
98
+ return { status: result.status, stderr: result.stderr ?? "" };
99
+ });
100
+ log(`Installing pi (${exports.PI_RUNTIME_SPEC}). This runs once, and only for \`codie --pi\`.`);
101
+ try {
102
+ const result = run((0, codieRuntimeInstall_1.buildRuntimeInstallArgs)(staging, exports.PI_RUNTIME_SPEC));
103
+ if (result.status !== 0)
104
+ throw new Error(result.stderr?.trim() || `npm exited with ${result.status}`);
105
+ // Trust the artifact, not the exit code.
106
+ const staged = installedPiEntry(staging, readFile);
107
+ if (!staged || !exists(staged)) {
108
+ throw new Error(`the install did not produce pi. Check that ${exports.PI_RUNTIME_SPEC} is available from your npm registry.`);
109
+ }
110
+ remove(installDir);
111
+ rename(staging, installDir);
112
+ }
113
+ catch (error) {
114
+ throw new Error(`Couldn't install pi: ${error instanceof Error ? error.message : String(error)}`);
115
+ }
116
+ finally {
117
+ // No-op after a successful promote; otherwise it keeps a ~200MB staging
118
+ // dir from being orphaned.
119
+ remove(staging);
120
+ }
121
+ const entry = installedPiEntry(installDir, readFile);
122
+ if (!entry)
123
+ throw new Error("Couldn't install pi: the installed package has no CLI entry.");
124
+ return entry;
125
+ }
package/dist/utils.d.ts CHANGED
@@ -4,6 +4,7 @@ import { type TranscriptModelUsage } from "./transcriptModels";
4
4
  import { spawn as nodeSpawn } from "child_process";
5
5
  import type { CaptureLogEntry } from "./captureLog";
6
6
  import { codiedevStateDir } from "./configDir";
7
+ export { inspectPiExtension, piExtensionPath } from "./piHooks";
7
8
  export { codiedevStateDir };
8
9
  export interface CodiedevConfig {
9
10
  token: string;
@@ -108,6 +109,22 @@ export declare function openCodeInstalled(): boolean;
108
109
  * keeps `codiedev connect` as the only setup step even when Codie/OpenCode is
109
110
  * installed later. OpenCode loads this file from its documented plugin dir. */
110
111
  export declare function installOpenCodeCapturePlugin(): void;
112
+ export declare function piInstalled(): boolean;
113
+ /** Subcommands the pi extension calls, in the order inspectPiExtension checks. */
114
+ export declare const PI_EXTENSION_SUBCOMMANDS: readonly ["session-start-pi", "backup-pi", "capture-pi-final", "discover-pi", "security-pre-pi", "security-post-pi", "security-failure-pi"];
115
+ /**
116
+ * pi has no hook config: it auto-loads TypeScript extensions from
117
+ * <agentDir>/extensions/. This one forwards pi's events to the same hook
118
+ * subcommands as every other harness. pi already writes the session JSONL, so
119
+ * capture just hands its path over (no transcript dump like OpenCode's).
120
+ */
121
+ export declare function piExtensionSource(agentSecurityEnabled: boolean): string;
122
+ /** Install the pi extension. Unconditional like OpenCode's: pi installed
123
+ * later still gets captured without re-running `codiedev connect`. This
124
+ * release line has no Agent Security hooks, so security stays off. */
125
+ export declare function installPiExtension(agentSecurityEnabled?: boolean): void;
126
+ /** Self-heal after an upgrade or drift, like Copilot's hook file. */
127
+ export declare function ensurePiExtensionInstalled(agentSecurityEnabled?: boolean): void;
111
128
  /**
112
129
  * VS Code with the GitHub Copilot Chat extension installed. The extension
113
130
  * directory pattern is `~/.vscode/extensions/github.copilot-chat-*`. We treat
@@ -127,6 +144,11 @@ export declare function ensureSessionStartHookInstalled(): void;
127
144
  export declare function installClaudeCodeInstructions(): void;
128
145
  export declare function installCodexInstructions(): void;
129
146
  export declare function copilotInstructionsPath(): string;
147
+ /** pi's global context file, loaded into every session's system prompt. */
148
+ export declare function piInstructionsPath(): string;
149
+ /** pi has no session-start output channel, so team skill context rides in
150
+ * AGENTS.md the way it does for Copilot. */
151
+ export declare function installPiInstructions(): void;
130
152
  export declare function installCopilotInstructions(): void;
131
153
  export declare function installCursorInstructions(): void;
132
154
  export interface InstructionsTarget {
@@ -212,7 +234,7 @@ export interface SteeringStats {
212
234
  }
213
235
  export declare function parseClaudeSteering(content: string): SteeringStats;
214
236
  export declare function parseCodexSteering(content: string): SteeringStats;
215
- export declare function parseSteering(content: string, mode: "claude-code" | "codex" | "cursor" | "opencode" | "copilot"): SteeringStats;
237
+ export declare function parseSteering(content: string, mode: "claude-code" | "codex" | "cursor" | "opencode" | "copilot" | "pi"): SteeringStats;
216
238
  export interface UsageStats {
217
239
  inputTokens: number;
218
240
  outputTokens: number;
@@ -224,7 +246,7 @@ export interface UsageStats {
224
246
  /** Engine-reported spend (opencode SDK `info.cost`). Desktop mode only. */
225
247
  costUsd?: number;
226
248
  }
227
- export declare function parseUsage(content: string, mode: "claude-code" | "codex" | "cursor" | "opencode" | "desktop" | "copilot"): UsageStats;
249
+ export declare function parseUsage(content: string, mode: "claude-code" | "codex" | "cursor" | "opencode" | "desktop" | "copilot" | "pi"): UsageStats;
228
250
  export declare function parseSessionStart(content: string): number | null;
229
251
  export declare function parseClaudeCodeStats(content: string): ParsedStats;
230
252
  /** Convert OpenCode's SDK message array (`{info, parts}[]`) into the same
package/dist/utils.js CHANGED
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.DIRECT_UPLOAD_THRESHOLD_CHARS = exports.SWEEP_MIN_AGE_MS = exports.CAPTURE_THROTTLE_MS = exports.FINISH_ORPHAN_AGE_MS = exports.CLAIM_STALE_MS = exports.UPLOAD_TTL_MS = exports.MAX_UPLOAD_ATTEMPTS = exports.CONFIG_PATH = exports.codiedevStateDir = void 0;
36
+ exports.DIRECT_UPLOAD_THRESHOLD_CHARS = exports.SWEEP_MIN_AGE_MS = exports.CAPTURE_THROTTLE_MS = exports.FINISH_ORPHAN_AGE_MS = exports.CLAIM_STALE_MS = exports.UPLOAD_TTL_MS = exports.MAX_UPLOAD_ATTEMPTS = exports.PI_EXTENSION_SUBCOMMANDS = exports.CONFIG_PATH = exports.codiedevStateDir = exports.piExtensionPath = exports.inspectPiExtension = void 0;
37
37
  exports.readConfig = readConfig;
38
38
  exports.writeConfig = writeConfig;
39
39
  exports.writeConfigAtPath = writeConfigAtPath;
@@ -64,6 +64,10 @@ exports.cursorInstalled = cursorInstalled;
64
64
  exports.copilotInstalled = copilotInstalled;
65
65
  exports.openCodeInstalled = openCodeInstalled;
66
66
  exports.installOpenCodeCapturePlugin = installOpenCodeCapturePlugin;
67
+ exports.piInstalled = piInstalled;
68
+ exports.piExtensionSource = piExtensionSource;
69
+ exports.installPiExtension = installPiExtension;
70
+ exports.ensurePiExtensionInstalled = ensurePiExtensionInstalled;
67
71
  exports.vscodeCopilotInstalled = vscodeCopilotInstalled;
68
72
  exports.upsertCodiedevHook = upsertCodiedevHook;
69
73
  exports.installHook = installHook;
@@ -77,6 +81,8 @@ exports.ensureSessionStartHookInstalled = ensureSessionStartHookInstalled;
77
81
  exports.installClaudeCodeInstructions = installClaudeCodeInstructions;
78
82
  exports.installCodexInstructions = installCodexInstructions;
79
83
  exports.copilotInstructionsPath = copilotInstructionsPath;
84
+ exports.piInstructionsPath = piInstructionsPath;
85
+ exports.installPiInstructions = installPiInstructions;
80
86
  exports.installCopilotInstructions = installCopilotInstructions;
81
87
  exports.installCursorInstructions = installCursorInstructions;
82
88
  exports.installedInstructionsTargets = installedInstructionsTargets;
@@ -140,6 +146,10 @@ const teamSkillContext_1 = require("./teamSkillContext");
140
146
  const nativeSteering_1 = require("./nativeSteering");
141
147
  const copilotHooks_1 = require("./copilotHooks");
142
148
  const harnessDirs_1 = require("./harnessDirs");
149
+ const piHooks_1 = require("./piHooks");
150
+ var piHooks_2 = require("./piHooks");
151
+ Object.defineProperty(exports, "inspectPiExtension", { enumerable: true, get: function () { return piHooks_2.inspectPiExtension; } });
152
+ Object.defineProperty(exports, "piExtensionPath", { enumerable: true, get: function () { return piHooks_2.piExtensionPath; } });
143
153
  const CONFIG_DIR = (0, configDir_1.codiedevStateDir)();
144
154
  exports.CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
145
155
  function readConfig() {
@@ -1111,7 +1121,6 @@ function installOpenCodeCapturePlugin() {
1111
1121
  `import os from "node:os";\n` +
1112
1122
  `import path from "node:path";\n\n` +
1113
1123
  `const START = ${JSON.stringify(start)};\n` +
1114
- `const CAPTURE = ${JSON.stringify(capture)};\n` +
1115
1124
  `const FINISH = ${JSON.stringify(finish)};\n` +
1116
1125
  `const PROMPT = ${JSON.stringify(prompt)};\n` +
1117
1126
  `// How this Codie process was started (run / serve / tui). OpenCode records\n` +
@@ -1203,6 +1212,167 @@ function installOpenCodeCapturePlugin() {
1203
1212
  fs.writeFileSync(OPENCODE_PLUGIN_PATH, source, { encoding: "utf8", mode: 0o600 });
1204
1213
  fs.chmodSync(OPENCODE_PLUGIN_PATH, 0o600);
1205
1214
  }
1215
+ function piInstalled() {
1216
+ if (fs.existsSync((0, harnessDirs_1.piAgentDir)()))
1217
+ return true;
1218
+ try {
1219
+ (0, child_process_1.execSync)(process.platform === "win32" ? "where pi" : "command -v pi", { stdio: ["pipe", "pipe", "pipe"] });
1220
+ return true;
1221
+ }
1222
+ catch {
1223
+ return false;
1224
+ }
1225
+ }
1226
+ /** Subcommands the pi extension calls, in the order inspectPiExtension checks. */
1227
+ exports.PI_EXTENSION_SUBCOMMANDS = [
1228
+ "session-start-pi", "backup-pi", "capture-pi-final", "discover-pi",
1229
+ "security-pre-pi", "security-post-pi", "security-failure-pi",
1230
+ ];
1231
+ /**
1232
+ * pi has no hook config: it auto-loads TypeScript extensions from
1233
+ * <agentDir>/extensions/. This one forwards pi's events to the same hook
1234
+ * subcommands as every other harness. pi already writes the session JSONL, so
1235
+ * capture just hands its path over (no transcript dump like OpenCode's).
1236
+ */
1237
+ function piExtensionSource(agentSecurityEnabled) {
1238
+ const [start, backup, finish, prompt, securityPre, securityPost, securityFailure] = exports.PI_EXTENSION_SUBCOMMANDS.map((subcommand) => resolveHookInvocation(subcommand));
1239
+ return `// Managed by codiedev connect. Safe to remove; \`codiedev connect\` reinstalls it.\n` +
1240
+ `import { spawn, spawnSync } from "node:child_process";\n\n` +
1241
+ `const START = ${JSON.stringify(start)};\n` +
1242
+ `const FINISH = ${JSON.stringify(finish)};\n` +
1243
+ `const BACKUP = ${JSON.stringify(backup)};\n` +
1244
+ `const PROMPT = ${JSON.stringify(prompt)};\n` +
1245
+ `const SECURITY_PRE = ${JSON.stringify(securityPre)};\n` +
1246
+ `const SECURITY_POST = ${JSON.stringify(securityPost)};\n` +
1247
+ `const SECURITY_FAILURE = ${JSON.stringify(securityFailure)};\n` +
1248
+ `const AGENT_SECURITY_ENABLED = ${JSON.stringify(agentSecurityEnabled)};\n` +
1249
+ `const CONTEXT_TYPE = ${JSON.stringify(piHooks_1.PI_CONTEXT_CUSTOM_TYPE)};\n` +
1250
+ `const piLaunchFromArgv = ${piHooks_1.piLaunchFromArgv.toString()};\n` +
1251
+ `const LAUNCH = piLaunchFromArgv(process.argv.slice(2));\n\n` +
1252
+ `function dispatch(invocation, payload) {\n` +
1253
+ ` try {\n` +
1254
+ ` const child = spawn(invocation.command, invocation.args, { detached: true, stdio: ["pipe", "ignore", "ignore"] });\n` +
1255
+ ` child.stdin.end(JSON.stringify(payload));\n` +
1256
+ ` child.unref();\n` +
1257
+ ` } catch {}\n` +
1258
+ `}\n\n` +
1259
+ `function dispatchSync(invocation, payload) {\n` +
1260
+ ` try { spawnSync(invocation.command, invocation.args, { input: JSON.stringify(payload), stdio: ["pipe", "ignore", "ignore"], timeout: 1500 }); } catch {}\n` +
1261
+ `}\n\n` +
1262
+ `function invokeSync(invocation, payload) {\n` +
1263
+ ` try {\n` +
1264
+ ` const result = spawnSync(invocation.command, invocation.args, { input: JSON.stringify(payload), encoding: "utf8", stdio: ["pipe", "pipe", "ignore"], timeout: 1500 });\n` +
1265
+ ` if (result.error) return null;\n` +
1266
+ ` return JSON.parse(result.stdout || "{}");\n` +
1267
+ ` } catch { return null; }\n` +
1268
+ `}\n\n` +
1269
+ `function blockReason(decision) {\n` +
1270
+ ` const denied = decision?.decision === "block" || decision?.hookSpecificOutput?.permissionDecision === "deny";\n` +
1271
+ ` return denied ? (decision?.reason || decision?.hookSpecificOutput?.permissionDecisionReason || "Codie security policy blocked this action") : null;\n` +
1272
+ `}\n\n` +
1273
+ `function warn(ctx, reason) {\n` +
1274
+ ` if (ctx?.hasUI) { try { ctx.ui.notify("Codie Security: " + reason, "error"); return; } catch {} }\n` +
1275
+ ` process.stderr.write("\\nCodie Security blocked this action:\\n" + reason + "\\n");\n` +
1276
+ `}\n\n` +
1277
+ `export default function codiedev(pi) {\n` +
1278
+ ` let session = null;\n` +
1279
+ ` let pendingContext = null;\n` +
1280
+ ` let finalized = false;\n` +
1281
+ ` const current = (ctx) => {\n` +
1282
+ ` const sm = ctx?.sessionManager;\n` +
1283
+ ` const id = sm?.getSessionId?.() ?? session?.id;\n` +
1284
+ ` const file = sm?.getSessionFile?.() ?? session?.file;\n` +
1285
+ ` const cwd = ctx?.cwd || session?.cwd || process.cwd();\n` +
1286
+ ` session = { id, file, cwd };\n` +
1287
+ ` return session;\n` +
1288
+ ` };\n` +
1289
+ ` const payload = (s, extra) => ({ session_id: s.id, transcript_path: s.file, cwd: s.cwd, workspace_roots: [s.cwd], launch: LAUNCH, ...extra });\n` +
1290
+ ` const finalize = () => {\n` +
1291
+ ` // No file (pi --no-session) still finalizes: the hook reports a skip, so\n` +
1292
+ ` // the start ping is not left open.\n` +
1293
+ ` if (finalized || !session?.id) return;\n` +
1294
+ ` finalized = true;\n` +
1295
+ ` dispatchSync(FINISH, payload(session));\n` +
1296
+ ` };\n` +
1297
+ ` // Print mode can exit without a shutdown event; the session is still over.\n` +
1298
+ ` // pi rebuilds extensions on every /new, /resume and /fork, so register one\n` +
1299
+ ` // exit hook per process and point it at the newest instance.\n` +
1300
+ ` globalThis.__codiedevPiFinalize = finalize;\n` +
1301
+ ` if (!globalThis.__codiedevPiExitHook) {\n` +
1302
+ ` globalThis.__codiedevPiExitHook = true;\n` +
1303
+ ` process.once("exit", () => globalThis.__codiedevPiFinalize?.());\n` +
1304
+ ` }\n\n` +
1305
+ ` pi.on("session_start", async (event, ctx) => {\n` +
1306
+ ` finalized = false;\n` +
1307
+ ` if (event?.reason === "reload") { current(ctx); return; }\n` +
1308
+ ` const s = current(ctx);\n` +
1309
+ ` // pi can announce one new session twice across an extension rebind.\n` +
1310
+ ` if (!s.id || globalThis.__codiedevPiStarted === s.id) return;\n` +
1311
+ ` globalThis.__codiedevPiStarted = s.id;\n` +
1312
+ ` dispatch(START, payload(s));\n` +
1313
+ ` });\n\n` +
1314
+ ` pi.on("input", async (event, ctx) => {\n` +
1315
+ ` if (event?.source === "extension") return { action: "continue" };\n` +
1316
+ ` const s = current(ctx);\n` +
1317
+ ` const decision = invokeSync(PROMPT, { session_id: s.id, cwd: s.cwd, prompt: event?.text ?? "" });\n` +
1318
+ ` const reason = blockReason(decision);\n` +
1319
+ ` if (reason) { warn(ctx, reason); return { action: "handled" }; }\n` +
1320
+ ` pendingContext = decision?.hookSpecificOutput?.additionalContext || null;\n` +
1321
+ ` return { action: "continue" };\n` +
1322
+ ` });\n\n` +
1323
+ ` pi.on("before_agent_start", async () => {\n` +
1324
+ ` if (!pendingContext) return;\n` +
1325
+ ` const content = pendingContext;\n` +
1326
+ ` pendingContext = null;\n` +
1327
+ ` return { message: { customType: CONTEXT_TYPE, content, display: false } };\n` +
1328
+ ` });\n\n` +
1329
+ ` pi.on("tool_call", async (event, ctx) => {\n` +
1330
+ ` if (!AGENT_SECURITY_ENABLED) return;\n` +
1331
+ ` const s = current(ctx);\n` +
1332
+ ` const reason = blockReason(invokeSync(SECURITY_PRE, { sessionId: s.id, cwd: s.cwd, toolName: event.toolName, toolUseId: event.toolCallId, toolInput: event.input }));\n` +
1333
+ ` if (reason) { warn(ctx, reason); return { block: true, reason }; }\n` +
1334
+ ` });\n\n` +
1335
+ ` pi.on("tool_result", async (event, ctx) => {\n` +
1336
+ ` if (!AGENT_SECURITY_ENABLED) return;\n` +
1337
+ ` const s = current(ctx);\n` +
1338
+ ` dispatch(event.isError ? SECURITY_FAILURE : SECURITY_POST, { sessionId: s.id, cwd: s.cwd, toolName: event.toolName, toolUseId: event.toolCallId, toolInput: event.input, toolResponse: { content: event.content, isError: event.isError } });\n` +
1339
+ ` });\n\n` +
1340
+ ` pi.on("agent_end", async (_event, ctx) => {\n` +
1341
+ ` const s = current(ctx);\n` +
1342
+ ` if (!s.id || !s.file) return;\n` +
1343
+ ` // Interim capture + artifact backup, like every harness's Stop hook. pi\n` +
1344
+ ` // retries provider errors itself; agent_end comes after the last try, so\n` +
1345
+ ` // the capture reports a hit limit once per run, not once per retry.\n` +
1346
+ ` dispatch(BACKUP, payload(s));\n` +
1347
+ ` });\n\n` +
1348
+ ` pi.on("session_shutdown", async (event, ctx) => {\n` +
1349
+ ` // A reload keeps the same session running.\n` +
1350
+ ` if (event?.reason === "reload") return;\n` +
1351
+ ` current(ctx);\n` +
1352
+ ` finalize();\n` +
1353
+ ` });\n` +
1354
+ `}\n`;
1355
+ }
1356
+ /** Install the pi extension. Unconditional like OpenCode's: pi installed
1357
+ * later still gets captured without re-running `codiedev connect`. This
1358
+ * release line has no Agent Security hooks, so security stays off. */
1359
+ function installPiExtension(agentSecurityEnabled = false) {
1360
+ const target = (0, piHooks_1.piExtensionPath)();
1361
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
1362
+ const tmp = `${target}.${process.pid}.tmp`;
1363
+ fs.writeFileSync(tmp, piExtensionSource(agentSecurityEnabled), { encoding: "utf8", mode: 0o600 });
1364
+ fs.renameSync(tmp, target);
1365
+ }
1366
+ /** Self-heal after an upgrade or drift, like Copilot's hook file. */
1367
+ function ensurePiExtensionInstalled(agentSecurityEnabled = false) {
1368
+ try {
1369
+ const file = (0, piHooks_1.piExtensionPath)();
1370
+ if (fs.existsSync(file) && fs.readFileSync(file, "utf8") === piExtensionSource(agentSecurityEnabled))
1371
+ return;
1372
+ installPiExtension(agentSecurityEnabled);
1373
+ }
1374
+ catch { /* best-effort */ }
1375
+ }
1206
1376
  /**
1207
1377
  * VS Code with the GitHub Copilot Chat extension installed. The extension
1208
1378
  * directory pattern is `~/.vscode/extensions/github.copilot-chat-*`. We treat
@@ -1424,6 +1594,7 @@ function migrateHooksToLauncher() {
1424
1594
  ["cursor", CURSOR_HOOKS_PATH, installCursorHook],
1425
1595
  ["copilot", copilotHooksPath(), installCopilotHook],
1426
1596
  ["opencode", OPENCODE_PLUGIN_PATH, installOpenCodeCapturePlugin],
1597
+ ["pi", (0, piHooks_1.piExtensionPath)(), installPiExtension],
1427
1598
  ];
1428
1599
  const migrated = [];
1429
1600
  for (const [name, file, install] of targets) {
@@ -1601,6 +1772,15 @@ function installCodexInstructions() {
1601
1772
  function copilotInstructionsPath() {
1602
1773
  return path.join((0, copilotHooks_1.copilotHome)(), "copilot-instructions.md");
1603
1774
  }
1775
+ /** pi's global context file, loaded into every session's system prompt. */
1776
+ function piInstructionsPath() {
1777
+ return path.join((0, harnessDirs_1.piAgentDir)(), "AGENTS.md");
1778
+ }
1779
+ /** pi has no session-start output channel, so team skill context rides in
1780
+ * AGENTS.md the way it does for Copilot. */
1781
+ function installPiInstructions() {
1782
+ writeInstructionsBlock(piInstructionsPath(), `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${CODIEDEV_INSTRUCTIONS_BODY}`);
1783
+ }
1604
1784
  function installCopilotInstructions() {
1605
1785
  writeInstructionsBlock(copilotInstructionsPath(), `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${CODIEDEV_INSTRUCTIONS_BODY}`);
1606
1786
  }
@@ -1654,6 +1834,7 @@ function installedInstructionsTargets() {
1654
1834
  { path: CODEX_USER_INSTRUCTIONS_PATH, body: CODIEDEV_INSTRUCTIONS_BODY },
1655
1835
  { path: copilotInstructionsPath(), body: withTeamContext },
1656
1836
  { path: CURSOR_RULES_PATH, body: withTeamContext },
1837
+ { path: piInstructionsPath(), body: withTeamContext },
1657
1838
  ];
1658
1839
  }
1659
1840
  /**
@@ -2798,9 +2979,9 @@ exports.SWEEP_MIN_AGE_MS = exports.CAPTURE_THROTTLE_MS + 2 * 60 * 1000;
2798
2979
  // to true would silently re-introduce the per-turn lease-close bug for every
2799
2980
  // new agent.
2800
2981
  function resolveCaptureFinal(argv2) {
2801
- if (argv2 === "capture-opencode" || argv2 === "capture-desktop")
2982
+ if (argv2 === "capture-opencode" || argv2 === "capture-desktop" || argv2 === "capture-pi")
2802
2983
  return false;
2803
- if (argv2 === "capture-opencode-final" || argv2 === "capture-desktop-final")
2984
+ if (argv2 === "capture-opencode-final" || argv2 === "capture-desktop-final" || argv2 === "capture-pi-final")
2804
2985
  return true;
2805
2986
  return argv2 === "capture" || argv2 === "capture-cursor" || argv2 === "capture-copilot" || argv2 === undefined
2806
2987
  ? true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.70",
3
+ "version": "0.9.72",
4
4
  "description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration — and run the governed Codie harness",
5
5
  "bin": {
6
6
  "codiedev": "dist/cli.js",
@@ -12,13 +12,13 @@
12
12
  "dev": "tsc --watch",
13
13
  "prepublishOnly": "tsc",
14
14
  "test": "vitest run",
15
- "test:functional": "vitest run src/__tests__/limitEvent.functional.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/copilotCapture.functional.test.ts",
16
- "test:harness-parity": "vitest run src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/copilotCapture.functional.test.ts",
15
+ "test:functional": "vitest run src/__tests__/limitEvent.functional.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/copilotCapture.functional.test.ts src/__tests__/piCapture.functional.test.ts",
16
+ "test:harness-parity": "vitest run src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/copilotCapture.functional.test.ts src/__tests__/piCapture.functional.test.ts",
17
17
  "test:harness-binaries": "node scripts/checkHarnessBinaries.mjs",
18
18
  "test:watch": "vitest",
19
19
  "test:live-sources": "npm run build && node scripts/liveSourceParity.mjs",
20
20
  "test:ide-sources": "npm run build && node scripts/liveSourceParity.mjs --ide",
21
- "test:metadata-parity": "vitest run src/__tests__/sourceMetadataMatrix.test.ts src/__tests__/copilotCapture.functional.test.ts src/__tests__/copilotCapture.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/nativeSourceParity.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/subagents.test.ts src/__tests__/transcriptModels.test.ts src/__tests__/captureRetry.test.ts src/__tests__/limitEvent.functional.test.ts src/__tests__/captureExclusions.test.ts"
21
+ "test:metadata-parity": "vitest run src/__tests__/sourceMetadataMatrix.test.ts src/__tests__/copilotCapture.functional.test.ts src/__tests__/copilotCapture.test.ts src/__tests__/piCapture.functional.test.ts src/__tests__/piHooks.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/nativeSourceParity.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/subagents.test.ts src/__tests__/transcriptModels.test.ts src/__tests__/captureRetry.test.ts src/__tests__/limitEvent.functional.test.ts src/__tests__/captureExclusions.test.ts"
22
22
  },
23
23
  "files": [
24
24
  "dist",