codiedev 0.9.69 → 0.9.71

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.
@@ -48,6 +48,7 @@ exports.planSelfUpdate = planSelfUpdate;
48
48
  exports.isSelfUpdateDue = isSelfUpdateDue;
49
49
  exports.readSelfUpdateState = readSelfUpdateState;
50
50
  exports.maybeSpawnSelfUpdate = maybeSpawnSelfUpdate;
51
+ exports.maybeRefreshInstructions = maybeRefreshInstructions;
51
52
  exports.findInstalledHookScript = findInstalledHookScript;
52
53
  exports.runSelfUpdate = runSelfUpdate;
53
54
  const fs = __importStar(require("fs"));
@@ -58,6 +59,7 @@ const versionCheck_1 = require("./versionCheck");
58
59
  const configDir_1 = require("./configDir");
59
60
  const captureLog_1 = require("./captureLog");
60
61
  const launcher_1 = require("./launcher");
62
+ const utils_1 = require("./utils");
61
63
  const PKG = "codiedev";
62
64
  const DAY_MS = 24 * 60 * 60 * 1000;
63
65
  const LOCK_STALE_MS = 10 * 60 * 1000;
@@ -161,6 +163,43 @@ function maybeSpawnSelfUpdate(hookEntry) {
161
163
  // Best-effort; a failed spawn retries tomorrow.
162
164
  }
163
165
  }
166
+ function instructionsVersionPath() {
167
+ return path.join((0, configDir_1.codiedevStateDir)(), ".instructions-version");
168
+ }
169
+ /**
170
+ * First hook call on a new CLI version (self-updated or installed by hand)
171
+ * re-renders the codiedev block in the harness instruction files, so they
172
+ * stop listing commands this version no longer has. The steady-state cost is
173
+ * one small file read. Honors the same opt-outs as the self-update.
174
+ */
175
+ function maybeRefreshInstructions(refresh = utils_1.refreshInstalledInstructions) {
176
+ if (process.env.CODIEDEV_NO_AUTO_UPDATE || process.env.CODIEDEV_NO_UPDATE_CHECK)
177
+ return;
178
+ try {
179
+ const marker = instructionsVersionPath();
180
+ try {
181
+ if (fs.readFileSync(marker, "utf8").trim() === version_1.CLI_VERSION)
182
+ return;
183
+ }
184
+ catch {
185
+ // No marker yet: first run on a version that ships this check.
186
+ }
187
+ const refreshed = refresh();
188
+ fs.mkdirSync((0, configDir_1.codiedevStateDir)(), { recursive: true });
189
+ fs.writeFileSync(marker, version_1.CLI_VERSION, "utf8");
190
+ if (refreshed.length > 0) {
191
+ (0, captureLog_1.appendCaptureLog)({
192
+ stage: "self-update",
193
+ sessionId: undefined,
194
+ outcome: "ok",
195
+ detail: `result=instructions-refreshed version=${version_1.CLI_VERSION} files=${refreshed.length}`,
196
+ });
197
+ }
198
+ }
199
+ catch {
200
+ // Best-effort; retried on the next hook call.
201
+ }
202
+ }
164
203
  function acquireLock() {
165
204
  const lock = lockPath();
166
205
  try {
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,8 +144,26 @@ 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;
154
+ export interface InstructionsTarget {
155
+ path: string;
156
+ body: string;
157
+ }
158
+ export declare function installedInstructionsTargets(): InstructionsTarget[];
159
+ /**
160
+ * Re-render the codiedev block in every instructions file that already has
161
+ * one, so an upgrade (background self-update or a manual `npm i`) stops
162
+ * teaching retired commands without the user re-running `connect`. Never
163
+ * creates a file or adds a block: a user who deleted it, or never connected
164
+ * that harness, is left alone. Returns the paths it rewrote.
165
+ */
166
+ export declare function refreshInstalledInstructions(targets?: InstructionsTarget[]): string[];
132
167
  /**
133
168
  * Install the Codiedev MCP server into Claude Code's user-scope config.
134
169
  * Safe to call multiple times — updates the existing entry if present.
@@ -199,7 +234,7 @@ export interface SteeringStats {
199
234
  }
200
235
  export declare function parseClaudeSteering(content: string): SteeringStats;
201
236
  export declare function parseCodexSteering(content: string): SteeringStats;
202
- 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;
203
238
  export interface UsageStats {
204
239
  inputTokens: number;
205
240
  outputTokens: number;
@@ -211,7 +246,7 @@ export interface UsageStats {
211
246
  /** Engine-reported spend (opencode SDK `info.cost`). Desktop mode only. */
212
247
  costUsd?: number;
213
248
  }
214
- 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;
215
250
  export declare function parseSessionStart(content: string): number | null;
216
251
  export declare function parseClaudeCodeStats(content: string): ParsedStats;
217
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,8 +81,12 @@ 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;
88
+ exports.installedInstructionsTargets = installedInstructionsTargets;
89
+ exports.refreshInstalledInstructions = refreshInstalledInstructions;
82
90
  exports.installClaudeCodeMcp = installClaudeCodeMcp;
83
91
  exports.installCursorMcp = installCursorMcp;
84
92
  exports.installVSCodeMcp = installVSCodeMcp;
@@ -138,6 +146,10 @@ const teamSkillContext_1 = require("./teamSkillContext");
138
146
  const nativeSteering_1 = require("./nativeSteering");
139
147
  const copilotHooks_1 = require("./copilotHooks");
140
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; } });
141
153
  const CONFIG_DIR = (0, configDir_1.codiedevStateDir)();
142
154
  exports.CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
143
155
  function readConfig() {
@@ -1109,7 +1121,6 @@ function installOpenCodeCapturePlugin() {
1109
1121
  `import os from "node:os";\n` +
1110
1122
  `import path from "node:path";\n\n` +
1111
1123
  `const START = ${JSON.stringify(start)};\n` +
1112
- `const CAPTURE = ${JSON.stringify(capture)};\n` +
1113
1124
  `const FINISH = ${JSON.stringify(finish)};\n` +
1114
1125
  `const PROMPT = ${JSON.stringify(prompt)};\n` +
1115
1126
  `// How this Codie process was started (run / serve / tui). OpenCode records\n` +
@@ -1201,6 +1212,167 @@ function installOpenCodeCapturePlugin() {
1201
1212
  fs.writeFileSync(OPENCODE_PLUGIN_PATH, source, { encoding: "utf8", mode: 0o600 });
1202
1213
  fs.chmodSync(OPENCODE_PLUGIN_PATH, 0o600);
1203
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
+ }
1204
1376
  /**
1205
1377
  * VS Code with the GitHub Copilot Chat extension installed. The extension
1206
1378
  * directory pattern is `~/.vscode/extensions/github.copilot-chat-*`. We treat
@@ -1422,6 +1594,7 @@ function migrateHooksToLauncher() {
1422
1594
  ["cursor", CURSOR_HOOKS_PATH, installCursorHook],
1423
1595
  ["copilot", copilotHooksPath(), installCopilotHook],
1424
1596
  ["opencode", OPENCODE_PLUGIN_PATH, installOpenCodeCapturePlugin],
1597
+ ["pi", (0, piHooks_1.piExtensionPath)(), installPiExtension],
1425
1598
  ];
1426
1599
  const migrated = [];
1427
1600
  for (const [name, file, install] of targets) {
@@ -1599,6 +1772,15 @@ function installCodexInstructions() {
1599
1772
  function copilotInstructionsPath() {
1600
1773
  return path.join((0, copilotHooks_1.copilotHome)(), "copilot-instructions.md");
1601
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
+ }
1602
1784
  function installCopilotInstructions() {
1603
1785
  writeInstructionsBlock(copilotInstructionsPath(), `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${CODIEDEV_INSTRUCTIONS_BODY}`);
1604
1786
  }
@@ -1645,6 +1827,46 @@ function installCursorInstructions() {
1645
1827
  }
1646
1828
  fs.writeFileSync(CURSOR_RULES_PATH, next, "utf8");
1647
1829
  }
1830
+ function installedInstructionsTargets() {
1831
+ const withTeamContext = `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${CODIEDEV_INSTRUCTIONS_BODY}`;
1832
+ return [
1833
+ { path: CLAUDE_USER_INSTRUCTIONS_PATH, body: CODIEDEV_INSTRUCTIONS_BODY },
1834
+ { path: CODEX_USER_INSTRUCTIONS_PATH, body: CODIEDEV_INSTRUCTIONS_BODY },
1835
+ { path: copilotInstructionsPath(), body: withTeamContext },
1836
+ { path: CURSOR_RULES_PATH, body: withTeamContext },
1837
+ { path: piInstructionsPath(), body: withTeamContext },
1838
+ ];
1839
+ }
1840
+ /**
1841
+ * Re-render the codiedev block in every instructions file that already has
1842
+ * one, so an upgrade (background self-update or a manual `npm i`) stops
1843
+ * teaching retired commands without the user re-running `connect`. Never
1844
+ * creates a file or adds a block: a user who deleted it, or never connected
1845
+ * that harness, is left alone. Returns the paths it rewrote.
1846
+ */
1847
+ function refreshInstalledInstructions(targets = installedInstructionsTargets()) {
1848
+ const refreshed = [];
1849
+ for (const target of targets) {
1850
+ try {
1851
+ const existing = fs.readFileSync(target.path, "utf8");
1852
+ const beginIdx = existing.indexOf(CODIEDEV_INSTRUCTIONS_BEGIN);
1853
+ const endIdx = existing.indexOf(CODIEDEV_INSTRUCTIONS_END);
1854
+ if (beginIdx === -1 || endIdx === -1 || endIdx <= beginIdx)
1855
+ continue;
1856
+ const next = existing.slice(0, beginIdx) +
1857
+ `${CODIEDEV_INSTRUCTIONS_BEGIN}\n${target.body}\n${CODIEDEV_INSTRUCTIONS_END}` +
1858
+ existing.slice(endIdx + CODIEDEV_INSTRUCTIONS_END.length);
1859
+ if (next === existing)
1860
+ continue;
1861
+ fs.writeFileSync(target.path, next, "utf8");
1862
+ refreshed.push(target.path);
1863
+ }
1864
+ catch {
1865
+ // Missing or unreadable file: nothing of ours to refresh.
1866
+ }
1867
+ }
1868
+ return refreshed;
1869
+ }
1648
1870
  function readMcpConfigSafely(path) {
1649
1871
  if (!fs.existsSync(path))
1650
1872
  return {};
@@ -2757,9 +2979,9 @@ exports.SWEEP_MIN_AGE_MS = exports.CAPTURE_THROTTLE_MS + 2 * 60 * 1000;
2757
2979
  // to true would silently re-introduce the per-turn lease-close bug for every
2758
2980
  // new agent.
2759
2981
  function resolveCaptureFinal(argv2) {
2760
- if (argv2 === "capture-opencode" || argv2 === "capture-desktop")
2982
+ if (argv2 === "capture-opencode" || argv2 === "capture-desktop" || argv2 === "capture-pi")
2761
2983
  return false;
2762
- if (argv2 === "capture-opencode-final" || argv2 === "capture-desktop-final")
2984
+ if (argv2 === "capture-opencode-final" || argv2 === "capture-desktop-final" || argv2 === "capture-pi-final")
2763
2985
  return true;
2764
2986
  return argv2 === "capture" || argv2 === "capture-cursor" || argv2 === "capture-copilot" || argv2 === undefined
2765
2987
  ? true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.69",
3
+ "version": "0.9.71",
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",