codiedev 0.9.68 → 0.9.70

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.
@@ -17,7 +17,13 @@ export interface CodexHookTrustEntry {
17
17
  eventName: string;
18
18
  command: string;
19
19
  currentHash: string;
20
+ /** Plugin hooks only: Codex also pins approval to the bundle's contents. */
21
+ contentHash?: string;
20
22
  }
23
+ /** Codex's fingerprint of an installed plugin bundle, byte-for-byte the
24
+ * algorithm in codex-rs hooks/src/engine/plugin_fingerprint.rs. Codex skips a
25
+ * plugin hook unless `trusted_content_hash` equals this (Codex 0.163+). */
26
+ export declare function codexPluginContentHash(root: string): string;
21
27
  /** Codex hashes a canonical, normalized hook identity before it will execute a
22
28
  * user hook. Keep this compatible with codex_config::version_for_toml. */
23
29
  export declare function codexCommandHookHash(eventName: string, group: MatcherGroup, handler: CommandHandler): string;
@@ -32,7 +38,10 @@ export declare function inspectCodexHookTrust(configPath: string, hooksPath: str
32
38
  trusted: number;
33
39
  };
34
40
  /** Trust entries for every command handler in a pack's hooks.json. */
35
- export declare function packCodexHookTrustEntries(hooksPath: string, hooksFile: unknown): CodexHookTrustEntry[];
41
+ export declare function packCodexHookTrustEntries(hooksPath: string, hooksFile: unknown, plugin?: {
42
+ id: string;
43
+ root: string;
44
+ }): CodexHookTrustEntry[];
36
45
  /** Regenerate the pack trust block from scratch with exactly `entries`.
37
46
  * Entries for packs that were removed or whose hooks changed disappear; the
38
47
  * `codiedev-hook-trust` block and all other config are left untouched. */
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.codexPluginContentHash = codexPluginContentHash;
36
37
  exports.codexCommandHookHash = codexCommandHookHash;
37
38
  exports.codiedevCodexHookTrustEntries = codiedevCodexHookTrustEntries;
38
39
  exports.writeCodexHookTrust = writeCodexHookTrust;
@@ -69,6 +70,46 @@ const MATCHER_EVENTS = new Set([
69
70
  "SubagentStart",
70
71
  "SubagentStop",
71
72
  ]);
73
+ /** Codex's fingerprint of an installed plugin bundle, byte-for-byte the
74
+ * algorithm in codex-rs hooks/src/engine/plugin_fingerprint.rs. Codex skips a
75
+ * plugin hook unless `trusted_content_hash` equals this (Codex 0.163+). */
76
+ function codexPluginContentHash(root) {
77
+ const hash = crypto.createHash("sha256");
78
+ hash.update("codex-plugin-bundle-v1");
79
+ const u64 = (n) => {
80
+ const b = Buffer.alloc(8);
81
+ b.writeBigUInt64LE(BigInt(n));
82
+ return b;
83
+ };
84
+ const pending = [root];
85
+ while (pending.length) {
86
+ const current = pending.pop();
87
+ const relative = path.relative(root, current).split(path.sep).join("/");
88
+ const name = path.posix.basename(relative);
89
+ if (name === "__pycache__" || name === ".venv" || name.endsWith(".pyc") || name.endsWith(".egg-info"))
90
+ continue;
91
+ const nameBytes = Buffer.from(relative, "utf8");
92
+ hash.update(u64(nameBytes.length));
93
+ hash.update(nameBytes);
94
+ const stat = fs.lstatSync(current);
95
+ if (stat.isDirectory()) {
96
+ hash.update("directory");
97
+ // Rust sorts by OsString, i.e. bytewise; push reversed so the smallest pops first.
98
+ const entries = fs.readdirSync(current).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)));
99
+ for (const entry of entries.reverse())
100
+ pending.push(path.join(current, entry));
101
+ }
102
+ else if (stat.isFile()) {
103
+ hash.update("file");
104
+ hash.update(u64(stat.size));
105
+ hash.update(fs.readFileSync(current));
106
+ }
107
+ else {
108
+ throw new Error(`unsupported plugin entry type: ${current}`);
109
+ }
110
+ }
111
+ return `sha256:${hash.digest("hex")}`;
112
+ }
72
113
  function canonicalize(value) {
73
114
  if (Array.isArray(value))
74
115
  return value.map(canonicalize);
@@ -157,6 +198,32 @@ function codiedevCodexHookTrustEntries(hooksPath, hooksFile) {
157
198
  }
158
199
  return entries;
159
200
  }
201
+ /** Remove our marker block, keeping any table that isn't a hook-trust entry.
202
+ * Codex appends new tables (e.g. `[marketplaces.codiedev-packs]`,
203
+ * `[plugins."<id>"]`) at the end of config.toml, which lands them before our
204
+ * closing marker when our block is last. Dropping them with the block silently
205
+ * unregistered the pack from Codex on every reinstall. */
206
+ function stripBlockKeepingForeign(text, begin, end) {
207
+ const foreign = [];
208
+ const stripped = text.replace(new RegExp(`\\n?${begin}\\n([\\s\\S]*?)${end}\\n?`, "g"), (_match, body) => {
209
+ let table = null;
210
+ const flush = () => {
211
+ if (table && table.join("").trim())
212
+ foreign.push(table.join("\n").trimEnd());
213
+ };
214
+ for (const line of body.split("\n")) {
215
+ if (/^\s*\[/.test(line)) {
216
+ flush();
217
+ table = /^\s*\[hooks\.state\./.test(line) ? null : [];
218
+ }
219
+ if (table)
220
+ table.push(line);
221
+ }
222
+ flush();
223
+ return "\n";
224
+ });
225
+ return foreign.length ? `${stripped.trimEnd()}\n\n${foreign.join("\n\n")}\n` : stripped;
226
+ }
160
227
  function tableHeader(key) {
161
228
  return `[hooks.state.${JSON.stringify(key)}]`;
162
229
  }
@@ -181,7 +248,7 @@ function removeTable(lines, header) {
181
248
  function writeCodexHookTrust(configPath, hooksPath, hooksFile) {
182
249
  const entries = codiedevCodexHookTrustEntries(hooksPath, hooksFile);
183
250
  let existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
184
- existing = existing.replace(new RegExp(`\\n?${TRUST_BEGIN}\\n[\\s\\S]*?${TRUST_END}\\n?`, "g"), "\n");
251
+ existing = stripBlockKeepingForeign(existing, TRUST_BEGIN, TRUST_END);
185
252
  let lines = existing.split("\n");
186
253
  for (const entry of entries)
187
254
  lines = removeTable(lines, tableHeader(entry.key));
@@ -237,11 +304,16 @@ function inspectCodexHookTrust(configPath, hooksPath, hooksFile, requiredSubcomm
237
304
  const PACK_TRUST_BEGIN = "# codiedev-pack-trust:begin";
238
305
  const PACK_TRUST_END = "# codiedev-pack-trust:end";
239
306
  /** Trust entries for every command handler in a pack's hooks.json. */
240
- function packCodexHookTrustEntries(hooksPath, hooksFile) {
307
+ function packCodexHookTrustEntries(hooksPath, hooksFile, plugin) {
241
308
  const events = hooksFile?.hooks;
242
309
  if (!events || typeof events !== "object")
243
310
  return [];
244
- const source = normalizedHooksPath(hooksPath);
311
+ // Codex keys a plugin's hooks by "<plugin id>:<path inside the plugin>",
312
+ // not by the file's absolute path (codex-rs declarations::plugin_hook_key_source).
313
+ const source = plugin
314
+ ? `${plugin.id}:${path.relative(plugin.root, hooksPath).split(path.sep).join("/")}`
315
+ : normalizedHooksPath(hooksPath);
316
+ const contentHash = plugin ? codexPluginContentHash(plugin.root) : undefined;
245
317
  const entries = [];
246
318
  for (const [eventName, groupsValue] of Object.entries(events)) {
247
319
  const eventLabel = EVENT_LABELS[eventName];
@@ -260,6 +332,7 @@ function packCodexHookTrustEntries(hooksPath, hooksFile) {
260
332
  eventName,
261
333
  command: handler.command,
262
334
  currentHash: codexCommandHookHash(eventName, group, handler),
335
+ ...(contentHash ? { contentHash } : {}),
263
336
  });
264
337
  });
265
338
  });
@@ -271,7 +344,7 @@ function packCodexHookTrustEntries(hooksPath, hooksFile) {
271
344
  * `codiedev-hook-trust` block and all other config are left untouched. */
272
345
  function writeCodexPackHookTrust(configPath, entries) {
273
346
  let existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
274
- existing = existing.replace(new RegExp(`\\n?${PACK_TRUST_BEGIN}\\n[\\s\\S]*?${PACK_TRUST_END}\\n?`, "g"), "\n");
347
+ existing = stripBlockKeepingForeign(existing, PACK_TRUST_BEGIN, PACK_TRUST_END);
275
348
  let lines = existing.split("\n");
276
349
  // A stray table for the same key outside our block would make the TOML
277
350
  // invalid (duplicate table) — drop it so ours is authoritative.
@@ -291,6 +364,7 @@ function writeCodexPackHookTrust(configPath, entries) {
291
364
  tableHeader(entry.key),
292
365
  "enabled = true",
293
366
  `trusted_hash = ${JSON.stringify(entry.currentHash)}`,
367
+ ...(entry.contentHash ? [`trusted_content_hash = ${JSON.stringify(entry.contentHash)}`] : []),
294
368
  "",
295
369
  ]).join("\n");
296
370
  const next = `${base}${base ? "\n\n" : ""}${PACK_TRUST_BEGIN}\n${trustBlock}${PACK_TRUST_END}\n`;
package/dist/hook.js CHANGED
@@ -546,6 +546,7 @@ async function main() {
546
546
  // which is what makes a pnpm install safe to upgrade.
547
547
  if (migrated.length === 0)
548
548
  (0, selfUpdate_1.maybeSpawnSelfUpdate)(process.argv[1]);
549
+ (0, selfUpdate_1.maybeRefreshInstructions)();
549
550
  let hookInput = {};
550
551
  try {
551
552
  const raw = await readStdin();
@@ -94,7 +94,9 @@ function codexPackTrustEntries(registry, codexDir) {
94
94
  if (!hooksPath)
95
95
  continue;
96
96
  try {
97
- bySlug.set(slug, (0, codexHookTrust_1.packCodexHookTrustEntries)(hooksPath, JSON.parse(fs.readFileSync(hooksPath, "utf8"))));
97
+ // hooks/hooks.json sits at the root of the installed plugin bundle.
98
+ const plugin = { id: `${slug}@${paths_1.MARKETPLACE_NAME}`, root: path.dirname(path.dirname(hooksPath)) };
99
+ bySlug.set(slug, (0, codexHookTrust_1.packCodexHookTrustEntries)(hooksPath, JSON.parse(fs.readFileSync(hooksPath, "utf8")), plugin));
98
100
  }
99
101
  catch {
100
102
  // unreadable hooks → not trusted
@@ -38,6 +38,13 @@ export declare function readSelfUpdateState(): SelfUpdateState | null;
38
38
  * child, not one each; the child's lock covers whatever still slips through.
39
39
  */
40
40
  export declare function maybeSpawnSelfUpdate(hookEntry: string | undefined): void;
41
+ /**
42
+ * First hook call on a new CLI version (self-updated or installed by hand)
43
+ * re-renders the codiedev block in the harness instruction files, so they
44
+ * stop listing commands this version no longer has. The steady-state cost is
45
+ * one small file read. Honors the same opt-outs as the self-update.
46
+ */
47
+ export declare function maybeRefreshInstructions(refresh?: () => string[]): void;
41
48
  /**
42
49
  * Where the just-installed copy lives. npm/bun reinstall in place. pnpm's
43
50
  * `root -g` is <root>/node_modules up to v10 but the bare global/v11 parent
@@ -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
@@ -129,6 +129,19 @@ export declare function installCodexInstructions(): void;
129
129
  export declare function copilotInstructionsPath(): string;
130
130
  export declare function installCopilotInstructions(): void;
131
131
  export declare function installCursorInstructions(): void;
132
+ export interface InstructionsTarget {
133
+ path: string;
134
+ body: string;
135
+ }
136
+ export declare function installedInstructionsTargets(): InstructionsTarget[];
137
+ /**
138
+ * Re-render the codiedev block in every instructions file that already has
139
+ * one, so an upgrade (background self-update or a manual `npm i`) stops
140
+ * teaching retired commands without the user re-running `connect`. Never
141
+ * creates a file or adds a block: a user who deleted it, or never connected
142
+ * that harness, is left alone. Returns the paths it rewrote.
143
+ */
144
+ export declare function refreshInstalledInstructions(targets?: InstructionsTarget[]): string[];
132
145
  /**
133
146
  * Install the Codiedev MCP server into Claude Code's user-scope config.
134
147
  * Safe to call multiple times — updates the existing entry if present.
package/dist/utils.js CHANGED
@@ -79,6 +79,8 @@ exports.installCodexInstructions = installCodexInstructions;
79
79
  exports.copilotInstructionsPath = copilotInstructionsPath;
80
80
  exports.installCopilotInstructions = installCopilotInstructions;
81
81
  exports.installCursorInstructions = installCursorInstructions;
82
+ exports.installedInstructionsTargets = installedInstructionsTargets;
83
+ exports.refreshInstalledInstructions = refreshInstalledInstructions;
82
84
  exports.installClaudeCodeMcp = installClaudeCodeMcp;
83
85
  exports.installCursorMcp = installCursorMcp;
84
86
  exports.installVSCodeMcp = installVSCodeMcp;
@@ -1645,6 +1647,45 @@ function installCursorInstructions() {
1645
1647
  }
1646
1648
  fs.writeFileSync(CURSOR_RULES_PATH, next, "utf8");
1647
1649
  }
1650
+ function installedInstructionsTargets() {
1651
+ const withTeamContext = `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${CODIEDEV_INSTRUCTIONS_BODY}`;
1652
+ return [
1653
+ { path: CLAUDE_USER_INSTRUCTIONS_PATH, body: CODIEDEV_INSTRUCTIONS_BODY },
1654
+ { path: CODEX_USER_INSTRUCTIONS_PATH, body: CODIEDEV_INSTRUCTIONS_BODY },
1655
+ { path: copilotInstructionsPath(), body: withTeamContext },
1656
+ { path: CURSOR_RULES_PATH, body: withTeamContext },
1657
+ ];
1658
+ }
1659
+ /**
1660
+ * Re-render the codiedev block in every instructions file that already has
1661
+ * one, so an upgrade (background self-update or a manual `npm i`) stops
1662
+ * teaching retired commands without the user re-running `connect`. Never
1663
+ * creates a file or adds a block: a user who deleted it, or never connected
1664
+ * that harness, is left alone. Returns the paths it rewrote.
1665
+ */
1666
+ function refreshInstalledInstructions(targets = installedInstructionsTargets()) {
1667
+ const refreshed = [];
1668
+ for (const target of targets) {
1669
+ try {
1670
+ const existing = fs.readFileSync(target.path, "utf8");
1671
+ const beginIdx = existing.indexOf(CODIEDEV_INSTRUCTIONS_BEGIN);
1672
+ const endIdx = existing.indexOf(CODIEDEV_INSTRUCTIONS_END);
1673
+ if (beginIdx === -1 || endIdx === -1 || endIdx <= beginIdx)
1674
+ continue;
1675
+ const next = existing.slice(0, beginIdx) +
1676
+ `${CODIEDEV_INSTRUCTIONS_BEGIN}\n${target.body}\n${CODIEDEV_INSTRUCTIONS_END}` +
1677
+ existing.slice(endIdx + CODIEDEV_INSTRUCTIONS_END.length);
1678
+ if (next === existing)
1679
+ continue;
1680
+ fs.writeFileSync(target.path, next, "utf8");
1681
+ refreshed.push(target.path);
1682
+ }
1683
+ catch {
1684
+ // Missing or unreadable file: nothing of ours to refresh.
1685
+ }
1686
+ }
1687
+ return refreshed;
1688
+ }
1648
1689
  function readMcpConfigSafely(path) {
1649
1690
  if (!fs.existsSync(path))
1650
1691
  return {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.68",
3
+ "version": "0.9.70",
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",