codiedev 0.9.67 → 0.9.69

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/dist/cli.js CHANGED
@@ -29,6 +29,11 @@ const library_1 = require("./commands/library");
29
29
  const share_1 = require("./commands/share");
30
30
  const send_1 = require("./commands/send");
31
31
  const capture_1 = require("./commands/capture");
32
+ const pack_1 = require("./commands/pack");
33
+ // --- starter packs phase 2 ---
34
+ const hookGate_1 = require("./packs/hookGate");
35
+ const sync_1 = require("./packs/sync");
36
+ // --- end starter packs ---
32
37
  const version_1 = require("./version");
33
38
  const HELP = `
34
39
  Codiedev CLI
@@ -78,6 +83,14 @@ Messaging:
78
83
  codiedev inbox [--unread] [--limit N] Show messages
79
84
  codiedev read <ping-id> Mark read + show full message
80
85
 
86
+ Starter packs:
87
+ codiedev pack list Packs assigned to your team
88
+ codiedev pack install [slug] [--harness claude,codex,copilot,cursor,codie]
89
+ [--dry-run] [--json]
90
+ Install a pack (or all) into every
91
+ detected tool
92
+ codiedev pack status Installed packs, up to date or not
93
+
81
94
  Discovery:
82
95
  codiedev search "<query>" Semantic search across the team's
83
96
  captured sessions and artifacts
@@ -138,6 +151,7 @@ locally before any capture leaves your machine, so an offline or failed portal
138
151
  status sync never weakens it.
139
152
  `.trim();
140
153
  const COMMAND_HELP = {
154
+ pack: pack_1.PACK_HELP,
141
155
  connect: `
142
156
  codiedev connect — link your agent CLI to Codiedev
143
157
 
@@ -394,6 +408,20 @@ function removedCommand(command) {
394
408
  }
395
409
  async function main() {
396
410
  const [, , command, ...rest] = process.argv;
411
+ // --- starter packs: hook-gate runs inside agent hooks; stdout/stderr and
412
+ // the exit code belong to the wrapped command, so nothing else may run. ---
413
+ if (command === "hook-gate") {
414
+ (0, hookGate_1.runHookGateCli)(rest);
415
+ return;
416
+ }
417
+ // Daily "pack has updates" nudge from the cached status (local read only).
418
+ // stderr, so piped/--json output stays clean.
419
+ if (command && !["pack", "packs", "help", "docs", "version", "--version", "-v", "--help", "-h"].includes(command) && !rest.includes("--json")) {
420
+ const nag = (0, sync_1.packNagLine)();
421
+ if (nag)
422
+ console.error(`\n${nag}\n`);
423
+ }
424
+ // --- end starter packs ---
397
425
  if (!command || command === "--help" || command === "-h") {
398
426
  console.log(HELP);
399
427
  return;
@@ -496,6 +524,10 @@ async function main() {
496
524
  case "ignored":
497
525
  await (0, capture_1.runCaptureCommand)([command, ...rest]);
498
526
  return;
527
+ case "pack":
528
+ case "packs":
529
+ await (0, pack_1.runPack)(rest);
530
+ return;
499
531
  case "capture":
500
532
  // Back-compat alias for the earlier `codiedev capture <sub>` form.
501
533
  await (0, capture_1.runCaptureCommand)(rest);
@@ -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;
@@ -31,4 +37,13 @@ export declare function inspectCodexHookTrust(configPath: string, hooksPath: str
31
37
  total: number;
32
38
  trusted: number;
33
39
  };
40
+ /** Trust entries for every command handler in a pack's hooks.json. */
41
+ export declare function packCodexHookTrustEntries(hooksPath: string, hooksFile: unknown, plugin?: {
42
+ id: string;
43
+ root: string;
44
+ }): CodexHookTrustEntry[];
45
+ /** Regenerate the pack trust block from scratch with exactly `entries`.
46
+ * Entries for packs that were removed or whose hooks changed disappear; the
47
+ * `codiedev-hook-trust` block and all other config are left untouched. */
48
+ export declare function writeCodexPackHookTrust(configPath: string, entries: CodexHookTrustEntry[]): CodexHookTrustEntry[];
34
49
  export {};
@@ -33,10 +33,13 @@ 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;
39
40
  exports.inspectCodexHookTrust = inspectCodexHookTrust;
41
+ exports.packCodexHookTrustEntries = packCodexHookTrustEntries;
42
+ exports.writeCodexPackHookTrust = writeCodexPackHookTrust;
40
43
  const crypto = __importStar(require("crypto"));
41
44
  const fs = __importStar(require("fs"));
42
45
  const path = __importStar(require("path"));
@@ -67,6 +70,46 @@ const MATCHER_EVENTS = new Set([
67
70
  "SubagentStart",
68
71
  "SubagentStop",
69
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
+ }
70
113
  function canonicalize(value) {
71
114
  if (Array.isArray(value))
72
115
  return value.map(canonicalize);
@@ -155,6 +198,32 @@ function codiedevCodexHookTrustEntries(hooksPath, hooksFile) {
155
198
  }
156
199
  return entries;
157
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
+ }
158
227
  function tableHeader(key) {
159
228
  return `[hooks.state.${JSON.stringify(key)}]`;
160
229
  }
@@ -179,7 +248,7 @@ function removeTable(lines, header) {
179
248
  function writeCodexHookTrust(configPath, hooksPath, hooksFile) {
180
249
  const entries = codiedevCodexHookTrustEntries(hooksPath, hooksFile);
181
250
  let existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
182
- existing = existing.replace(new RegExp(`\\n?${TRUST_BEGIN}\\n[\\s\\S]*?${TRUST_END}\\n?`, "g"), "\n");
251
+ existing = stripBlockKeepingForeign(existing, TRUST_BEGIN, TRUST_END);
183
252
  let lines = existing.split("\n");
184
253
  for (const entry of entries)
185
254
  lines = removeTable(lines, tableHeader(entry.key));
@@ -226,3 +295,81 @@ function inspectCodexHookTrust(configPath, hooksPath, hooksFile, requiredSubcomm
226
295
  }).length;
227
296
  return { ok: entries.length > 0 && trusted === entries.length, total: entries.length, trusted };
228
297
  }
298
+ // ---------------------------------------------------------------------------
299
+ // Starter-pack hook trust (SIG-113). `codiedev pack install` is an explicit
300
+ // install action for admin-approved pack hooks, so it trusts every command
301
+ // handler in the installed pack's hooks.json (not only Codiedev's own). These
302
+ // live in their own managed block so `codiedev connect` and `pack install`
303
+ // never clobber each other's entries.
304
+ const PACK_TRUST_BEGIN = "# codiedev-pack-trust:begin";
305
+ const PACK_TRUST_END = "# codiedev-pack-trust:end";
306
+ /** Trust entries for every command handler in a pack's hooks.json. */
307
+ function packCodexHookTrustEntries(hooksPath, hooksFile, plugin) {
308
+ const events = hooksFile?.hooks;
309
+ if (!events || typeof events !== "object")
310
+ return [];
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;
317
+ const entries = [];
318
+ for (const [eventName, groupsValue] of Object.entries(events)) {
319
+ const eventLabel = EVENT_LABELS[eventName];
320
+ if (!eventLabel || !Array.isArray(groupsValue))
321
+ continue;
322
+ groupsValue.forEach((groupValue, groupIndex) => {
323
+ const group = groupValue;
324
+ if (!group || !Array.isArray(group.hooks))
325
+ return;
326
+ group.hooks.forEach((handlerValue, handlerIndex) => {
327
+ const handler = handlerValue;
328
+ if (!handler || handler.type !== "command" || typeof handler.command !== "string")
329
+ return;
330
+ entries.push({
331
+ key: `${source}:${eventLabel}:${groupIndex}:${handlerIndex}`,
332
+ eventName,
333
+ command: handler.command,
334
+ currentHash: codexCommandHookHash(eventName, group, handler),
335
+ ...(contentHash ? { contentHash } : {}),
336
+ });
337
+ });
338
+ });
339
+ }
340
+ return entries;
341
+ }
342
+ /** Regenerate the pack trust block from scratch with exactly `entries`.
343
+ * Entries for packs that were removed or whose hooks changed disappear; the
344
+ * `codiedev-hook-trust` block and all other config are left untouched. */
345
+ function writeCodexPackHookTrust(configPath, entries) {
346
+ let existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
347
+ existing = stripBlockKeepingForeign(existing, PACK_TRUST_BEGIN, PACK_TRUST_END);
348
+ let lines = existing.split("\n");
349
+ // A stray table for the same key outside our block would make the TOML
350
+ // invalid (duplicate table) — drop it so ours is authoritative.
351
+ for (const entry of entries)
352
+ lines = removeTable(lines, tableHeader(entry.key));
353
+ const base = lines.join("\n").trimEnd();
354
+ if (entries.length === 0) {
355
+ const next = base ? `${base}\n` : "";
356
+ if (!fs.existsSync(configPath) && !next)
357
+ return entries;
358
+ fs.mkdirSync(path.dirname(configPath), { recursive: true, mode: 0o700 });
359
+ fs.writeFileSync(configPath, next, { encoding: "utf8", mode: 0o600 });
360
+ fs.chmodSync(configPath, 0o600);
361
+ return entries;
362
+ }
363
+ const trustBlock = entries.flatMap((entry) => [
364
+ tableHeader(entry.key),
365
+ "enabled = true",
366
+ `trusted_hash = ${JSON.stringify(entry.currentHash)}`,
367
+ ...(entry.contentHash ? [`trusted_content_hash = ${JSON.stringify(entry.contentHash)}`] : []),
368
+ "",
369
+ ]).join("\n");
370
+ const next = `${base}${base ? "\n\n" : ""}${PACK_TRUST_BEGIN}\n${trustBlock}${PACK_TRUST_END}\n`;
371
+ fs.mkdirSync(path.dirname(configPath), { recursive: true, mode: 0o700 });
372
+ fs.writeFileSync(configPath, next, { encoding: "utf8", mode: 0o600 });
373
+ fs.chmodSync(configPath, 0o600);
374
+ return entries;
375
+ }
package/dist/codie.js CHANGED
@@ -56,6 +56,8 @@ const codieUsage_1 = require("./codieUsage");
56
56
  const codieRuntime_1 = require("./codieRuntime");
57
57
  const codieRuntimeInstall_1 = require("./codieRuntimeInstall");
58
58
  const upgradeNudge_1 = require("./upgradeNudge");
59
+ const codie_1 = require("./packs/codie");
60
+ const paths_1 = require("./packs/paths");
59
61
  // How long to let the gateway finish booking the last request before asking
60
62
  // what the session cost.
61
63
  const USAGE_SETTLE_MS = 1200;
@@ -97,6 +99,24 @@ function connectedCodiedevConfig() {
97
99
  return {};
98
100
  }
99
101
  }
102
+ // Starter packs (`codiedev pack install`) reach Codie here: skills, agents and
103
+ // the hook shim are merged into the inline config at launch. Best-effort — a
104
+ // broken pack must never stop the harness from starting.
105
+ function withStarterPacks(existing, approved, catalog) {
106
+ try {
107
+ // Agent levels (opus/sonnet/haiku) resolve against this account's catalog.
108
+ const packs = (0, codie_1.codiePackConfig)((0, paths_1.packsRoot)(), { approvedModels: approved, catalog });
109
+ if (process.env.CODIEDEV_DEBUG) {
110
+ for (const note of packs.notes)
111
+ console.error(`codie: starter pack ${note}`);
112
+ }
113
+ return (0, codie_1.mergeCodiePackConfig)(existing, packs);
114
+ }
115
+ catch (error) {
116
+ console.error(`codie: skipping starter packs (${error instanceof Error ? error.message : error})`);
117
+ return existing;
118
+ }
119
+ }
100
120
  function assetPath(name) {
101
121
  // dist/codie.js → ../assets/<name>; src/codie.ts → ../assets/<name>.
102
122
  return path.join(__dirname, "..", "assets", name);
@@ -210,7 +230,7 @@ async function main() {
210
230
  (0, codieBrandInstall_1.installTuiPlugin)();
211
231
  installBrandWordmark(runtime);
212
232
  const config = (0, codieLauncher_1.buildCodieConfig)({
213
- existing: existingInlineConfig(),
233
+ existing: withStarterPacks(existingInlineConfig(), new Set(catalog.map((model) => (0, codieLauncher_1.bareModel)(model.id)).filter(Boolean)), catalog),
214
234
  catalog,
215
235
  env: { ...process.env, ...parsed.overrides, CODIEDEV_GATEWAY_URL: gatewayUrl },
216
236
  plugins: [capturePlugin, metadataPlugin],
@@ -0,0 +1,54 @@
1
+ import { CodiedevConfig } from "../utils";
2
+ import { InstallOptions, PackInstallResult, PackUninstallResult } from "../packs/install";
3
+ import { HarnessName } from "../packs/harnesses";
4
+ import { PackManifest } from "../packs/manifest";
5
+ export declare const DEFAULT_BACKEND_URL = "https://cli.codiedev.com";
6
+ /** CODIEDEV_TOKEN alone is enough: no ~/.codiedev/config.json needed (CI).
7
+ * CODIEDEV_URL defaults to production. Otherwise the connected config. */
8
+ export declare function resolvePackConfig(env?: NodeJS.ProcessEnv): CodiedevConfig | null;
9
+ /** In CI, `--harness` defaults to claude when it's on PATH; anything else is
10
+ * ambiguous on a build box, so say what was found. */
11
+ export declare function ciHarnesses(args: PackArgs, env: NodeJS.ProcessEnv, home?: string): HarnessName[];
12
+ export interface PackArgs {
13
+ sub?: string;
14
+ slug?: string;
15
+ harnesses?: HarnessName[];
16
+ dryRun: boolean;
17
+ json: boolean;
18
+ quiet?: boolean;
19
+ noPrune?: boolean;
20
+ out?: string;
21
+ git?: boolean;
22
+ manifest?: boolean;
23
+ }
24
+ export declare function parsePackArgs(args: string[]): PackArgs;
25
+ export declare function runList(config: CodiedevConfig, json: boolean, overrides?: Partial<InstallOptions>): Promise<void>;
26
+ export interface InstallRun {
27
+ installed: PackInstallResult[];
28
+ removed: PackUninstallResult[];
29
+ /** Installed slugs the server no longer serves (only populated by --dry-run). */
30
+ wouldRemove: string[];
31
+ }
32
+ export declare function runInstall(config: CodiedevConfig, args: PackArgs, overrides?: Partial<InstallOptions>): Promise<InstallRun>;
33
+ export declare function runStatus(config: CodiedevConfig, json: boolean, overrides?: Partial<InstallOptions>, options?: {
34
+ prune?: boolean;
35
+ }): Promise<{
36
+ installed: Array<Record<string, unknown>>;
37
+ removed: PackUninstallResult[];
38
+ notInstalled: string[];
39
+ offline: boolean;
40
+ }>;
41
+ export declare function runPack(args: string[]): Promise<void>;
42
+ /** Init (if needed) and commit everything under `dir`. Uses the user's git
43
+ * identity when configured, a neutral Codiedev one otherwise (fresh CI). */
44
+ export declare function commitExport(dir: string, message: string): string;
45
+ /** Write a CI-committable copy of the pack: `<out>/plugins/<slug>/…` plus
46
+ * `<out>/.claude-plugin/marketplace.json` (existing entries for other packs
47
+ * in the same folder are kept). */
48
+ export declare function exportPluginFolder(out: string, manifest: PackManifest): string;
49
+ export declare function runExport(config: CodiedevConfig, args: PackArgs): Promise<{
50
+ written: string;
51
+ commit?: string;
52
+ }>;
53
+ export declare function runApply(config: CodiedevConfig, args: PackArgs): Promise<Record<string, unknown>>;
54
+ export declare const PACK_HELP = "Usage:\n codiedev pack list Packs assigned to your team\n codiedev pack install [slug] [--harness claude,codex,copilot,cursor,codie] [--dry-run] [--json] [--quiet]\n Install one pack, or every pack that applies\n codiedev pack status [--json] Installed packs and whether they're up to date\n codiedev pack export <slug> --out <dir> [--git] [--manifest]\n Write the pack as a Claude-format plugin folder plus\n .claude-plugin/marketplace.json (commit it for CI).\n --git inits the folder and commits; --manifest writes\n an editable <slug>.pack.md instead\n codiedev pack apply <pack.md> Create or update a pack from a pack.md\n\nCursor: packs are copied to ~/.cursor/plugins/local/codiedev-<slug>/. A Cursor\nteam admin can disable \"user local plugin imports\"; then Cursor ignores them.\n\nAuto-update: session starts run `codiedev pack install --quiet` in the background\nabout once an hour. Opt out with CODIEDEV_PACK_AUTOSYNC=0.\n\nHooks for specific skills only: codiedev hook-gate --skills a,b --cmd-b64 <base64url of the command>\n(the pack editor writes this; the command runs via /bin/sh, or cmd.exe on Windows)\n\nCI: set CODIEDEV_TOKEN to a pack CI key (cdvci_\u2026, from the pack's page in the portal).\nNo `codiedev connect` or config file is needed. CI mode (a cdvci_ key, or CI=true)\nnever auto-syncs, reports or prunes, installs the one pack the key can see, defaults\n--harness to claude when it's on PATH, and exits non-zero unless every requested tool\ngot the pack. Skills from a pack are /<pack-slug>:<skill> in Claude Code.\n\n GitLab CI (.gitlab-ci.yml; add CODIEDEV_TOKEN and ANTHROPIC_API_KEY as masked\n CI/CD variables):\n pack-scan:\n image: node:22\n script:\n - npm i -g codiedev @anthropic-ai/claude-code\n - codiedev pack install --harness claude\n - claude -p \"/platform-starter:healthcare-scan\" --output-format text\n\n GitHub Actions (repository secrets CODIEDEV_TOKEN and ANTHROPIC_API_KEY):\n - uses: actions/setup-node@v4\n with: { node-version: 22 }\n - run: npm i -g codiedev @anthropic-ai/claude-code\n - run: codiedev pack install --harness claude\n env:\n CODIEDEV_TOKEN: ${{ secrets.CODIEDEV_TOKEN }}\n - run: claude -p \"/platform-starter:healthcare-scan\" --output-format text\n env:\n ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n\n Or vendor the plugin into the repo instead of installing at build time:\n codiedev pack export <slug> --out .codiedev/packs";