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.
- package/dist/codexHookTrust.d.ts +10 -1
- package/dist/codexHookTrust.js +78 -4
- package/dist/hook.js +1 -0
- package/dist/packs/install.js +3 -1
- package/dist/selfUpdate.d.ts +7 -0
- package/dist/selfUpdate.js +39 -0
- package/dist/utils.d.ts +13 -0
- package/dist/utils.js +41 -0
- package/package.json +1 -1
package/dist/codexHookTrust.d.ts
CHANGED
|
@@ -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
|
|
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. */
|
package/dist/codexHookTrust.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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();
|
package/dist/packs/install.js
CHANGED
|
@@ -94,7 +94,9 @@ function codexPackTrustEntries(registry, codexDir) {
|
|
|
94
94
|
if (!hooksPath)
|
|
95
95
|
continue;
|
|
96
96
|
try {
|
|
97
|
-
|
|
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
|
package/dist/selfUpdate.d.ts
CHANGED
|
@@ -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
|
package/dist/selfUpdate.js
CHANGED
|
@@ -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.
|
|
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",
|