codiedev 0.9.63 → 0.9.65
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/captureLog.d.ts +1 -1
- package/dist/codexLimitScan.js +2 -2
- package/dist/codieLauncher.js +2 -1
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.js +32 -11
- package/dist/connect.js +7 -5
- package/dist/harnessDirs.d.ts +12 -0
- package/dist/harnessDirs.js +90 -0
- package/dist/hook.js +16 -1
- package/dist/launcher.d.ts +20 -0
- package/dist/launcher.js +167 -0
- package/dist/selfUpdate.d.ts +49 -0
- package/dist/selfUpdate.js +390 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +94 -11
- package/package.json +1 -1
package/dist/captureLog.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "fs";
|
|
|
2
2
|
export declare const CAPTURE_LOG_MAX_BYTES: number;
|
|
3
3
|
export declare const CAPTURE_LOG_PATH: string;
|
|
4
4
|
export interface CaptureLogEntry {
|
|
5
|
-
stage: "capture-finish" | "upload" | "upload-deferred" | "stderr" | "limit";
|
|
5
|
+
stage: "capture-finish" | "upload" | "upload-deferred" | "stderr" | "limit" | "self-update";
|
|
6
6
|
sessionId: string | undefined;
|
|
7
7
|
outcome: string;
|
|
8
8
|
detail?: string;
|
package/dist/codexLimitScan.js
CHANGED
|
@@ -64,10 +64,10 @@ exports.sweepCodexLimits = sweepCodexLimits;
|
|
|
64
64
|
// after the block: in one real 44 MB rollout the marker sat 1.13 MB from the
|
|
65
65
|
// end, outside the window, and the parse returned null.
|
|
66
66
|
const fs = __importStar(require("fs"));
|
|
67
|
-
const os = __importStar(require("os"));
|
|
68
67
|
const path = __importStar(require("path"));
|
|
69
68
|
const limitEvent_1 = require("./limitEvent");
|
|
70
69
|
const configDir_1 = require("./configDir");
|
|
70
|
+
const harnessDirs_1 = require("./harnessDirs");
|
|
71
71
|
const MARKER = Buffer.from("rate_limit_reached_type");
|
|
72
72
|
const SPEND_MARKER = Buffer.from("spend_control_reached");
|
|
73
73
|
const NEWLINE = 0x0a;
|
|
@@ -269,7 +269,7 @@ function sessionIdFromRolloutPath(filePath) {
|
|
|
269
269
|
}
|
|
270
270
|
/** Where Codex writes its rollout JSONL files. */
|
|
271
271
|
function codexSessionsDir() {
|
|
272
|
-
return path.join(
|
|
272
|
+
return path.join((0, harnessDirs_1.codexHome)(), "sessions");
|
|
273
273
|
}
|
|
274
274
|
exports.CODEX_LIMIT_STATE_PATH = path.join((0, configDir_1.codiedevStateDir)(), "codex-limit-state.json");
|
|
275
275
|
// Bounds the state file on a machine with a long Codex history.
|
package/dist/codieLauncher.js
CHANGED
|
@@ -57,6 +57,7 @@ exports.parseCodieArgs = parseCodieArgs;
|
|
|
57
57
|
exports.validateCodieArgs = validateCodieArgs;
|
|
58
58
|
const path = __importStar(require("path"));
|
|
59
59
|
const url_1 = require("url");
|
|
60
|
+
const harnessDirs_1 = require("./harnessDirs");
|
|
60
61
|
const DEFAULT_NAMES = {
|
|
61
62
|
"openai/gpt-5.3-codex": "GPT-5.3 Codex",
|
|
62
63
|
"openai/gpt-5.2-codex": "GPT-5.2 Codex",
|
|
@@ -342,7 +343,7 @@ function patchBrandWordmark(binary) {
|
|
|
342
343
|
return { changed: true, offset: matches[0], stock, replacement: Buffer.from(exports.HIDDEN_WORDMARK) };
|
|
343
344
|
}
|
|
344
345
|
function resolveCapturePlugin({ homeDir, existsSync, }) {
|
|
345
|
-
const capturePath = path.join(
|
|
346
|
+
const capturePath = path.join((0, harnessDirs_1.openCodeConfigDir)(process.env, homeDir), "plugins", "codiedev.js");
|
|
346
347
|
if (!existsSync(capturePath)) {
|
|
347
348
|
throw new Error("Codiedev session capture is not installed. Run `codiedev connect` first");
|
|
348
349
|
}
|
|
@@ -1 +1,9 @@
|
|
|
1
|
+
type CheckStatus = "pass" | "fail" | "warn";
|
|
2
|
+
interface CheckResult {
|
|
3
|
+
name: string;
|
|
4
|
+
status: CheckStatus;
|
|
5
|
+
detail?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function configDirInUseCheck(agent: string, dir: string, inUse: boolean, envVar: string): CheckResult;
|
|
1
8
|
export declare function runDoctor(_args: string[]): Promise<void>;
|
|
9
|
+
export {};
|
package/dist/commands/doctor.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.configDirInUseCheck = configDirInUseCheck;
|
|
36
37
|
exports.runDoctor = runDoctor;
|
|
37
38
|
const fs = __importStar(require("fs"));
|
|
38
39
|
const path = __importStar(require("path"));
|
|
@@ -46,12 +47,26 @@ const upgradeNudge_1 = require("../upgradeNudge");
|
|
|
46
47
|
const captureReadiness_1 = require("../captureReadiness");
|
|
47
48
|
const captureLog_1 = require("../captureLog");
|
|
48
49
|
const copilotInstallation_1 = require("../copilotInstallation");
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
50
|
+
const harnessDirs_1 = require("../harnessDirs");
|
|
51
|
+
const CLAUDE_DIR = (0, harnessDirs_1.claudeConfigDir)();
|
|
52
|
+
const CLAUDE_SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
|
|
53
|
+
const CLAUDE_INSTRUCTIONS_PATH = path.join(CLAUDE_DIR, "CLAUDE.md");
|
|
54
|
+
const CODEX_DIR = (0, harnessDirs_1.codexHome)();
|
|
55
|
+
const CODEX_HOOKS_PATH = path.join(CODEX_DIR, "hooks.json");
|
|
56
|
+
const CODEX_INSTRUCTIONS_PATH = path.join(CODEX_DIR, "AGENTS.md");
|
|
53
57
|
const CURSOR_HOOKS_PATH = path.join(os.homedir(), ".cursor", "hooks.json");
|
|
54
58
|
const CURSOR_INSTRUCTIONS_PATH = path.join(os.homedir(), ".cursor", "rules", "codiedev.mdc");
|
|
59
|
+
function configDirInUseCheck(agent, dir, inUse, envVar) {
|
|
60
|
+
return {
|
|
61
|
+
name: `${agent} reads this config dir`,
|
|
62
|
+
status: inUse ? "pass" : "warn",
|
|
63
|
+
detail: inUse
|
|
64
|
+
? (0, harnessDirs_1.displayPath)(dir)
|
|
65
|
+
: `no ${agent} activity in ${(0, harnessDirs_1.displayPath)(dir)} — if you've run ${agent} here, it reads its config from ` +
|
|
66
|
+
`somewhere else, so the hooks below never fire. Set ${envVar} to that dir (or run as the user that runs ` +
|
|
67
|
+
`${agent}) and re-run \`codiedev connect\``,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
55
70
|
function symbol(status) {
|
|
56
71
|
if (status === "pass")
|
|
57
72
|
return "✓";
|
|
@@ -60,7 +75,7 @@ function symbol(status) {
|
|
|
60
75
|
return "✗";
|
|
61
76
|
}
|
|
62
77
|
function codexInstalled() {
|
|
63
|
-
return fs.existsSync(
|
|
78
|
+
return fs.existsSync(CODEX_DIR);
|
|
64
79
|
}
|
|
65
80
|
function cursorInstalled() {
|
|
66
81
|
return fs.existsSync(path.join(os.homedir(), ".cursor"));
|
|
@@ -401,20 +416,25 @@ async function runDoctor(_args) {
|
|
|
401
416
|
checks.push({
|
|
402
417
|
name: "Claude Code detected",
|
|
403
418
|
status: "pass",
|
|
404
|
-
detail:
|
|
419
|
+
detail: (0, harnessDirs_1.displayPath)(CLAUDE_DIR),
|
|
405
420
|
});
|
|
421
|
+
// A hook in settings.json only fires if Claude Code actually reads this
|
|
422
|
+
// dir. If it holds nothing Claude Code writes on use, it's running from
|
|
423
|
+
// somewhere else (CLAUDE_CONFIG_DIR unset in this shell, another user,
|
|
424
|
+
// the host vs. a container) and the ✓s below are hollow.
|
|
425
|
+
checks.push(configDirInUseCheck("Claude Code", CLAUDE_DIR, (0, harnessDirs_1.claudeConfigDirInUse)(CLAUDE_DIR), "CLAUDE_CONFIG_DIR"));
|
|
406
426
|
checks.push({
|
|
407
427
|
name: "Claude Code SessionEnd hook",
|
|
408
428
|
status: hasCodiedevHook(CLAUDE_SETTINGS_PATH, "SessionEnd") ? "pass" : "fail",
|
|
409
429
|
detail: hasCodiedevHook(CLAUDE_SETTINGS_PATH, "SessionEnd")
|
|
410
|
-
?
|
|
430
|
+
? (0, harnessDirs_1.displayPath)(CLAUDE_SETTINGS_PATH)
|
|
411
431
|
: "missing — re-run `codiedev connect`",
|
|
412
432
|
});
|
|
413
433
|
checks.push({
|
|
414
434
|
name: "Claude Code agent instructions",
|
|
415
435
|
status: hasCodiedevInstructions(CLAUDE_INSTRUCTIONS_PATH) ? "pass" : "fail",
|
|
416
436
|
detail: hasCodiedevInstructions(CLAUDE_INSTRUCTIONS_PATH)
|
|
417
|
-
?
|
|
437
|
+
? (0, harnessDirs_1.displayPath)(CLAUDE_INSTRUCTIONS_PATH)
|
|
418
438
|
: "missing — re-run `codiedev connect`",
|
|
419
439
|
});
|
|
420
440
|
}
|
|
@@ -430,20 +450,21 @@ async function runDoctor(_args) {
|
|
|
430
450
|
checks.push({
|
|
431
451
|
name: "Codex detected",
|
|
432
452
|
status: "pass",
|
|
433
|
-
detail:
|
|
453
|
+
detail: (0, harnessDirs_1.displayPath)(CODEX_DIR),
|
|
434
454
|
});
|
|
455
|
+
checks.push(configDirInUseCheck("Codex", CODEX_DIR, (0, harnessDirs_1.codexHomeInUse)(CODEX_DIR), "CODEX_HOME"));
|
|
435
456
|
checks.push({
|
|
436
457
|
name: "Codex Stop hook",
|
|
437
458
|
status: hasCodiedevHook(CODEX_HOOKS_PATH, "Stop") ? "pass" : "fail",
|
|
438
459
|
detail: hasCodiedevHook(CODEX_HOOKS_PATH, "Stop")
|
|
439
|
-
?
|
|
460
|
+
? (0, harnessDirs_1.displayPath)(CODEX_HOOKS_PATH)
|
|
440
461
|
: "missing — re-run `codiedev connect`",
|
|
441
462
|
});
|
|
442
463
|
checks.push({
|
|
443
464
|
name: "Codex agent instructions",
|
|
444
465
|
status: hasCodiedevInstructions(CODEX_INSTRUCTIONS_PATH) ? "pass" : "fail",
|
|
445
466
|
detail: hasCodiedevInstructions(CODEX_INSTRUCTIONS_PATH)
|
|
446
|
-
?
|
|
467
|
+
? (0, harnessDirs_1.displayPath)(CODEX_INSTRUCTIONS_PATH)
|
|
447
468
|
: "missing — re-run `codiedev connect`",
|
|
448
469
|
});
|
|
449
470
|
}
|
package/dist/connect.js
CHANGED
|
@@ -39,11 +39,13 @@ const https = __importStar(require("https"));
|
|
|
39
39
|
const http = __importStar(require("http"));
|
|
40
40
|
const crypto = __importStar(require("crypto"));
|
|
41
41
|
const os = __importStar(require("os"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
42
43
|
const utils_1 = require("./utils");
|
|
43
44
|
const connectFlow_1 = require("./connectFlow");
|
|
44
45
|
const version_1 = require("./version");
|
|
45
46
|
const upgradeNudge_1 = require("./upgradeNudge");
|
|
46
47
|
const shared_1 = require("./commands/shared");
|
|
48
|
+
const harnessDirs_1 = require("./harnessDirs");
|
|
47
49
|
// cli.codiedev.com is a Convex custom domain on the prod deployment; the
|
|
48
50
|
// underlying judicious-falcon-861.convex.site URL keeps serving, so configs
|
|
49
51
|
// written by older CLI versions stay valid without a re-connect.
|
|
@@ -304,12 +306,12 @@ async function runConnect(args = []) {
|
|
|
304
306
|
}
|
|
305
307
|
};
|
|
306
308
|
if (hasClaude) {
|
|
307
|
-
runInstall("claude-code-hook", utils_1.installHook,
|
|
308
|
-
runInstall("claude-code-instructions", utils_1.installClaudeCodeInstructions,
|
|
309
|
+
runInstall("claude-code-hook", utils_1.installHook, `Claude Code SessionEnd (capture) + UserPromptSubmit (skill discovery) + Stop (auto-backup) hooks (${(0, harnessDirs_1.displayPath)(path.join((0, harnessDirs_1.claudeConfigDir)(), "settings.json"))})`, "claudeCode-hook");
|
|
310
|
+
runInstall("claude-code-instructions", utils_1.installClaudeCodeInstructions, `Claude Code agent instructions (${(0, harnessDirs_1.displayPath)(path.join((0, harnessDirs_1.claudeConfigDir)(), "CLAUDE.md"))})`, "claudeCode-instructions");
|
|
309
311
|
}
|
|
310
312
|
if (hasCodex) {
|
|
311
|
-
runInstall("codex-hook", utils_1.installCodexHook,
|
|
312
|
-
runInstall("codex-instructions", utils_1.installCodexInstructions,
|
|
313
|
+
runInstall("codex-hook", utils_1.installCodexHook, `Codex Stop (capture + auto-backup) hooks (${(0, harnessDirs_1.displayPath)(path.join((0, harnessDirs_1.codexHome)(), "hooks.json"))})`, "codex-hook");
|
|
314
|
+
runInstall("codex-instructions", utils_1.installCodexInstructions, `Codex agent instructions (${(0, harnessDirs_1.displayPath)(path.join((0, harnessDirs_1.codexHome)(), "AGENTS.md"))})`, "codex-instructions");
|
|
313
315
|
}
|
|
314
316
|
if (hasCursor) {
|
|
315
317
|
runInstall("cursor-hook", utils_1.installCursorHook, "Cursor sessionStart + stop backup + sessionEnd capture hooks (~/.cursor/hooks.json)", "cursor-hook");
|
|
@@ -324,7 +326,7 @@ async function runConnect(args = []) {
|
|
|
324
326
|
// Always stage the adapter. It is dormant until OpenCode starts, which lets
|
|
325
327
|
// a user connect Claude Code today and install the Codie harness tomorrow
|
|
326
328
|
// without needing a second setup ritual.
|
|
327
|
-
runInstall("opencode-capture-adapter", utils_1.installOpenCodeCapturePlugin,
|
|
329
|
+
runInstall("opencode-capture-adapter", utils_1.installOpenCodeCapturePlugin, `OpenCode session capture adapter (${(0, harnessDirs_1.displayPath)(path.join((0, harnessDirs_1.openCodeConfigDir)(), "plugins", "codiedev.js"))})`, "opencode-capture");
|
|
328
330
|
// Tell the backend which targets we wired up so the portal's onboarding
|
|
329
331
|
// card flips to "Connected" only when something actually installed —
|
|
330
332
|
// not on bare token validation. Best-effort: a network failure here
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Claude Code: `$CLAUDE_CONFIG_DIR` or ~/.claude. */
|
|
2
|
+
export declare function claudeConfigDir(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
3
|
+
/** Claude Code's user state file. Lives inside `$CLAUDE_CONFIG_DIR` when set, else ~/.claude.json. */
|
|
4
|
+
export declare function claudeUserConfigPath(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
5
|
+
/** Codex: `$CODEX_HOME` or ~/.codex. */
|
|
6
|
+
export declare function codexHome(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
7
|
+
/** OpenCode: `$XDG_CONFIG_HOME/opencode` or ~/.config/opencode. */
|
|
8
|
+
export declare function openCodeConfigDir(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
9
|
+
/** `~`-abbreviated path for user-facing output. */
|
|
10
|
+
export declare function displayPath(p: string, home?: string): string;
|
|
11
|
+
export declare function claudeConfigDirInUse(dir?: string, exists?: (p: string) => boolean): boolean;
|
|
12
|
+
export declare function codexHomeInUse(dir?: string, exists?: (p: string) => boolean): boolean;
|
|
@@ -0,0 +1,90 @@
|
|
|
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.claudeConfigDir = claudeConfigDir;
|
|
37
|
+
exports.claudeUserConfigPath = claudeUserConfigPath;
|
|
38
|
+
exports.codexHome = codexHome;
|
|
39
|
+
exports.openCodeConfigDir = openCodeConfigDir;
|
|
40
|
+
exports.displayPath = displayPath;
|
|
41
|
+
exports.claudeConfigDirInUse = claudeConfigDirInUse;
|
|
42
|
+
exports.codexHomeInUse = codexHomeInUse;
|
|
43
|
+
// Where each coding agent reads its user config from. Every harness lets the
|
|
44
|
+
// user relocate it with an env var; installing into the default dir when the
|
|
45
|
+
// var is set writes hooks the agent never loads (a container with
|
|
46
|
+
// CLAUDE_CONFIG_DIR=/home/node/.claude while HOME=/root captured nothing for
|
|
47
|
+
// two weeks while doctor reported ✓). Same shape as copilotHome().
|
|
48
|
+
//
|
|
49
|
+
// Imports only node built-ins so utils.ts, doctor.ts and the hook path
|
|
50
|
+
// can all import it without a cycle.
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const os = __importStar(require("os"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
function override(value) {
|
|
55
|
+
const trimmed = value?.trim();
|
|
56
|
+
return trimmed ? path.resolve(trimmed) : undefined;
|
|
57
|
+
}
|
|
58
|
+
/** Claude Code: `$CLAUDE_CONFIG_DIR` or ~/.claude. */
|
|
59
|
+
function claudeConfigDir(env = process.env, home = os.homedir()) {
|
|
60
|
+
return override(env.CLAUDE_CONFIG_DIR) ?? path.join(home, ".claude");
|
|
61
|
+
}
|
|
62
|
+
/** Claude Code's user state file. Lives inside `$CLAUDE_CONFIG_DIR` when set, else ~/.claude.json. */
|
|
63
|
+
function claudeUserConfigPath(env = process.env, home = os.homedir()) {
|
|
64
|
+
const dir = override(env.CLAUDE_CONFIG_DIR);
|
|
65
|
+
return dir ? path.join(dir, ".claude.json") : path.join(home, ".claude.json");
|
|
66
|
+
}
|
|
67
|
+
/** Codex: `$CODEX_HOME` or ~/.codex. */
|
|
68
|
+
function codexHome(env = process.env, home = os.homedir()) {
|
|
69
|
+
return override(env.CODEX_HOME) ?? path.join(home, ".codex");
|
|
70
|
+
}
|
|
71
|
+
/** OpenCode: `$XDG_CONFIG_HOME/opencode` or ~/.config/opencode. */
|
|
72
|
+
function openCodeConfigDir(env = process.env, home = os.homedir()) {
|
|
73
|
+
return path.join(override(env.XDG_CONFIG_HOME) ?? path.join(home, ".config"), "opencode");
|
|
74
|
+
}
|
|
75
|
+
/** `~`-abbreviated path for user-facing output. */
|
|
76
|
+
function displayPath(p, home = os.homedir()) {
|
|
77
|
+
return p === home || p.startsWith(home + path.sep) ? "~" + p.slice(home.length) : p;
|
|
78
|
+
}
|
|
79
|
+
// Files/dirs Claude Code writes on first real use. Our installer only writes
|
|
80
|
+
// settings.json + CLAUDE.md, so a dir holding nothing but those is one Claude
|
|
81
|
+
// Code has never run against — the hooks in it will never fire.
|
|
82
|
+
const CLAUDE_USAGE_MARKERS = ["projects", "history.jsonl", "sessions", "statsig", "todos", "shell-snapshots"];
|
|
83
|
+
function claudeConfigDirInUse(dir = claudeConfigDir(), exists = fs.existsSync) {
|
|
84
|
+
return CLAUDE_USAGE_MARKERS.some((m) => exists(path.join(dir, m)));
|
|
85
|
+
}
|
|
86
|
+
// Codex writes rollouts under sessions/ and keeps history.jsonl alongside.
|
|
87
|
+
const CODEX_USAGE_MARKERS = ["sessions", "history.jsonl", "auth.json"];
|
|
88
|
+
function codexHomeInUse(dir = codexHome(), exists = fs.existsSync) {
|
|
89
|
+
return CODEX_USAGE_MARKERS.some((m) => exists(path.join(dir, m)));
|
|
90
|
+
}
|
package/dist/hook.js
CHANGED
|
@@ -48,6 +48,7 @@ const limitEvent_1 = require("./limitEvent");
|
|
|
48
48
|
const cursorLifecycle_1 = require("./cursorLifecycle");
|
|
49
49
|
const codexLimitScan_1 = require("./codexLimitScan");
|
|
50
50
|
const launchContext_1 = require("./launchContext");
|
|
51
|
+
const selfUpdate_1 = require("./selfUpdate");
|
|
51
52
|
const version_1 = require("./version");
|
|
52
53
|
const captureLog_1 = require("./captureLog");
|
|
53
54
|
const subagents_1 = require("./subagents");
|
|
@@ -527,6 +528,20 @@ async function main() {
|
|
|
527
528
|
}
|
|
528
529
|
process.exit(0);
|
|
529
530
|
}
|
|
531
|
+
// Detached daily self-upgrade of this install (see selfUpdate.ts).
|
|
532
|
+
if (process.argv[2] === "self-update") {
|
|
533
|
+
await (0, selfUpdate_1.runSelfUpdate)(process.argv[1]);
|
|
534
|
+
process.exit(0);
|
|
535
|
+
}
|
|
536
|
+
// A hook reached directly (not via the stable launcher) means some harness
|
|
537
|
+
// config predates the launcher: move them all onto it once. Steady state is
|
|
538
|
+
// every call arriving via the launcher, which skips this entirely.
|
|
539
|
+
const migrated = process.env.CODIEDEV_VIA_LAUNCHER ? [] : (0, utils_1.migrateHooksToLauncher)();
|
|
540
|
+
// Every harness reaches this point, so one check covers all of them. Right
|
|
541
|
+
// after a migration, wait for the next call: it arrives via the launcher,
|
|
542
|
+
// which is what makes a pnpm install safe to upgrade.
|
|
543
|
+
if (migrated.length === 0)
|
|
544
|
+
(0, selfUpdate_1.maybeSpawnSelfUpdate)(process.argv[1]);
|
|
530
545
|
let hookInput = {};
|
|
531
546
|
try {
|
|
532
547
|
const raw = await readStdin();
|
|
@@ -651,7 +666,7 @@ async function main() {
|
|
|
651
666
|
// Same detached capture-finish child as SessionEnd, marked non-final so the
|
|
652
667
|
// server renews the session lease instead of summarizing immediately.
|
|
653
668
|
if (process.argv[2] === "backup" || process.argv[2] === "backup-cursor" || process.argv[2] === "backup-copilot") {
|
|
654
|
-
if (transcript_path) {
|
|
669
|
+
if (transcript_path && !(0, utils_1.isCodexRolloutPath)(transcript_path)) {
|
|
655
670
|
try {
|
|
656
671
|
(0, utils_1.dispatchCaptureFinish)({
|
|
657
672
|
mode: process.argv[2] === "backup-cursor"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const LAUNCHER_FILE = "codiedev-hook-launcher.cjs";
|
|
2
|
+
export interface InstallRecord {
|
|
3
|
+
hookScript: string;
|
|
4
|
+
version?: string;
|
|
5
|
+
recordedAt: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function launcherPath(): string;
|
|
8
|
+
/** True iff `cmd` invokes the stable launcher (from any state dir). */
|
|
9
|
+
export declare function usesLauncher(cmd: string | undefined): boolean;
|
|
10
|
+
export declare function readInstallRecord(): InstallRecord | null;
|
|
11
|
+
/** Point the launcher at `hookScript`. Atomic, so a hook mid-launch never reads half a file. */
|
|
12
|
+
export declare function recordCurrentInstall(hookScript: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Make sure the launcher exists and forwards to `hookScript`, returning the
|
|
15
|
+
* launcher path to bake into hook configs — or null if the state dir is not
|
|
16
|
+
* writable, in which case callers keep baking the direct path. Memoized:
|
|
17
|
+
* one connect writes ~20 hook entries.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ensureLauncher(hookScript: string): string | null;
|
|
20
|
+
export declare function resetLauncherCacheForTests(): void;
|
package/dist/launcher.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Stable hook launcher.
|
|
3
|
+
//
|
|
4
|
+
// Harness configs used to bake the absolute path of whichever install ran
|
|
5
|
+
// `connect` (…/node_modules/codiedev/dist/hook.js). That path is only stable
|
|
6
|
+
// for npm/bun globals: pnpm puts every global install in a fresh hashed dir,
|
|
7
|
+
// and repo checkouts, Homebrew cellar moves, etc. can strand it too. Instead,
|
|
8
|
+
// every hook config points at one file we own —
|
|
9
|
+
// <codiedevStateDir()>/bin/codiedev-hook-launcher.cjs — which forwards to the
|
|
10
|
+
// install recorded in <codiedevStateDir()>/install.json. Moving to a new
|
|
11
|
+
// install (self-update, re-connect) rewrites one JSON file, never the
|
|
12
|
+
// harness configs, so Codex hook trust and IT-managed dotfiles stay put.
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.LAUNCHER_FILE = void 0;
|
|
48
|
+
exports.launcherPath = launcherPath;
|
|
49
|
+
exports.usesLauncher = usesLauncher;
|
|
50
|
+
exports.readInstallRecord = readInstallRecord;
|
|
51
|
+
exports.recordCurrentInstall = recordCurrentInstall;
|
|
52
|
+
exports.ensureLauncher = ensureLauncher;
|
|
53
|
+
exports.resetLauncherCacheForTests = resetLauncherCacheForTests;
|
|
54
|
+
const fs = __importStar(require("fs"));
|
|
55
|
+
const path = __importStar(require("path"));
|
|
56
|
+
const configDir_1 = require("./configDir");
|
|
57
|
+
exports.LAUNCHER_FILE = "codiedev-hook-launcher.cjs";
|
|
58
|
+
// Bump when LAUNCHER_SOURCE changes so existing launchers get rewritten.
|
|
59
|
+
const LAUNCHER_REVISION = 1;
|
|
60
|
+
function launcherPath() {
|
|
61
|
+
return path.join((0, configDir_1.codiedevStateDir)(), "bin", exports.LAUNCHER_FILE);
|
|
62
|
+
}
|
|
63
|
+
function installRecordPath() {
|
|
64
|
+
return path.join((0, configDir_1.codiedevStateDir)(), "install.json");
|
|
65
|
+
}
|
|
66
|
+
/** True iff `cmd` invokes the stable launcher (from any state dir). */
|
|
67
|
+
function usesLauncher(cmd) {
|
|
68
|
+
return typeof cmd === "string" && cmd.includes(exports.LAUNCHER_FILE);
|
|
69
|
+
}
|
|
70
|
+
// Plain CommonJS with no dependencies: it runs under whatever node the harness
|
|
71
|
+
// config names, before any of our modules load. `fallback` is the install the
|
|
72
|
+
// launcher was written for, used if install.json is missing or points at a
|
|
73
|
+
// directory that no longer exists. If neither exists it exits 0 so a stranded
|
|
74
|
+
// hook can never fail the developer's session.
|
|
75
|
+
function launcherSource(fallback) {
|
|
76
|
+
return `// codiedev-hook-launcher revision ${LAUNCHER_REVISION}. Managed by codiedev; safe to delete (re-run \`codiedev connect\`).
|
|
77
|
+
"use strict";
|
|
78
|
+
const fs = require("fs");
|
|
79
|
+
const path = require("path");
|
|
80
|
+
const FALLBACK = ${JSON.stringify(fallback)};
|
|
81
|
+
let target = null;
|
|
82
|
+
try {
|
|
83
|
+
const rec = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "install.json"), "utf8"));
|
|
84
|
+
if (rec && typeof rec.hookScript === "string" && fs.existsSync(rec.hookScript)) target = rec.hookScript;
|
|
85
|
+
} catch {}
|
|
86
|
+
if (!target && fs.existsSync(FALLBACK)) target = FALLBACK;
|
|
87
|
+
if (!target) process.exit(0);
|
|
88
|
+
process.env.CODIEDEV_VIA_LAUNCHER = "1";
|
|
89
|
+
process.argv[1] = target;
|
|
90
|
+
require(target);
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
function writeAtomic(target, contents) {
|
|
94
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
95
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
96
|
+
fs.writeFileSync(tmp, contents, "utf8");
|
|
97
|
+
fs.renameSync(tmp, target);
|
|
98
|
+
}
|
|
99
|
+
function readInstallRecord() {
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(fs.readFileSync(installRecordPath(), "utf8"));
|
|
102
|
+
return typeof parsed.hookScript === "string" ? parsed : null;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function packageVersionFor(hookScript) {
|
|
109
|
+
try {
|
|
110
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(path.dirname(path.dirname(hookScript)), "package.json"), "utf8"));
|
|
111
|
+
return typeof pkg.version === "string" ? pkg.version : undefined;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Point the launcher at `hookScript`. Atomic, so a hook mid-launch never reads half a file. */
|
|
118
|
+
function recordCurrentInstall(hookScript) {
|
|
119
|
+
try {
|
|
120
|
+
const record = {
|
|
121
|
+
hookScript,
|
|
122
|
+
version: packageVersionFor(hookScript),
|
|
123
|
+
recordedAt: Date.now(),
|
|
124
|
+
};
|
|
125
|
+
writeAtomic(installRecordPath(), JSON.stringify(record, null, 2));
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
let ensuredFor;
|
|
133
|
+
/**
|
|
134
|
+
* Make sure the launcher exists and forwards to `hookScript`, returning the
|
|
135
|
+
* launcher path to bake into hook configs — or null if the state dir is not
|
|
136
|
+
* writable, in which case callers keep baking the direct path. Memoized:
|
|
137
|
+
* one connect writes ~20 hook entries.
|
|
138
|
+
*/
|
|
139
|
+
function ensureLauncher(hookScript) {
|
|
140
|
+
if (ensuredFor !== undefined && ensuredFor === hookScript)
|
|
141
|
+
return launcherPath();
|
|
142
|
+
try {
|
|
143
|
+
const target = launcherPath();
|
|
144
|
+
const source = launcherSource(hookScript);
|
|
145
|
+
let current = null;
|
|
146
|
+
try {
|
|
147
|
+
current = fs.readFileSync(target, "utf8");
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// first install
|
|
151
|
+
}
|
|
152
|
+
if (current !== source)
|
|
153
|
+
writeAtomic(target, source);
|
|
154
|
+
if (readInstallRecord()?.hookScript !== hookScript && !recordCurrentInstall(hookScript)) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
ensuredFor = hookScript;
|
|
158
|
+
return target;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Test-only: the memo lives for the process lifetime.
|
|
165
|
+
function resetLauncherCacheForTests() {
|
|
166
|
+
ensuredFor = undefined;
|
|
167
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type SelfUpdatePlan = {
|
|
2
|
+
kind: "npm";
|
|
3
|
+
prefix: string;
|
|
4
|
+
} | {
|
|
5
|
+
kind: "pnpm";
|
|
6
|
+
} | {
|
|
7
|
+
kind: "bun";
|
|
8
|
+
} | {
|
|
9
|
+
kind: "skip";
|
|
10
|
+
reason: string;
|
|
11
|
+
};
|
|
12
|
+
export interface PlanDeps {
|
|
13
|
+
hookScriptReal: string;
|
|
14
|
+
env: NodeJS.ProcessEnv;
|
|
15
|
+
platform: NodeJS.Platform;
|
|
16
|
+
isContainer: boolean;
|
|
17
|
+
isWritable: (dir: string) => boolean;
|
|
18
|
+
viaLauncher?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Decide which package manager owns this install, from where hook.js really
|
|
22
|
+
* lives. We only ever upgrade the install we are running from: running
|
|
23
|
+
* whatever `npm` is first on PATH would, under nvm/Homebrew mixes, upgrade a
|
|
24
|
+
* different global tree than the one the harness config points at.
|
|
25
|
+
*/
|
|
26
|
+
export declare function planSelfUpdate(deps: PlanDeps): SelfUpdatePlan;
|
|
27
|
+
export interface SelfUpdateState {
|
|
28
|
+
lastAttemptAt: number;
|
|
29
|
+
lastResult?: string;
|
|
30
|
+
fromVersion?: string;
|
|
31
|
+
toVersion?: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function isSelfUpdateDue(state: SelfUpdateState | null, nowMs: number): boolean;
|
|
34
|
+
export declare function readSelfUpdateState(): SelfUpdateState | null;
|
|
35
|
+
/**
|
|
36
|
+
* Called on every hook invocation, so the not-due path is one small file read.
|
|
37
|
+
* Stamps the attempt before spawning so a burst of parallel hooks spawns one
|
|
38
|
+
* child, not one each; the child's lock covers whatever still slips through.
|
|
39
|
+
*/
|
|
40
|
+
export declare function maybeSpawnSelfUpdate(hookEntry: string | undefined): void;
|
|
41
|
+
/**
|
|
42
|
+
* Where the just-installed copy lives. npm/bun reinstall in place. pnpm's
|
|
43
|
+
* `root -g` is <root>/node_modules up to v10 but the bare global/v11 parent
|
|
44
|
+
* from v11 on, with each install in a hashed child — so check both shapes and
|
|
45
|
+
* take the copy that is actually at the version we just installed.
|
|
46
|
+
*/
|
|
47
|
+
export declare function findInstalledHookScript(plan: SelfUpdatePlan, hookEntry: string, version: string, pnpmRoot?: () => string | null): string | null;
|
|
48
|
+
/** Body of the detached `codiedev-hook self-update` child. Never throws. */
|
|
49
|
+
export declare function runSelfUpdate(hookEntry: string): Promise<SelfUpdateState>;
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Background self-update for the hook path.
|
|
3
|
+
//
|
|
4
|
+
// Onboarded developers never run `connect`/`doctor`/`codie` again — only the
|
|
5
|
+
// harness hooks fire — so the interactive nudges in upgradeNudge.ts never
|
|
6
|
+
// reach them. This module lets any hook invocation spawn a detached child
|
|
7
|
+
// (at most once per day) that upgrades the exact install the hook is running
|
|
8
|
+
// from. Harness configs bake the absolute path to dist/hook.js, so the next
|
|
9
|
+
// hook call picks up the new code with no config change.
|
|
10
|
+
//
|
|
11
|
+
// Split like versionCheck.ts: `planSelfUpdate` is pure and unit-tested; the IO
|
|
12
|
+
// (state file, lock, registry fetch, package-manager spawn) is below it.
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.planSelfUpdate = planSelfUpdate;
|
|
48
|
+
exports.isSelfUpdateDue = isSelfUpdateDue;
|
|
49
|
+
exports.readSelfUpdateState = readSelfUpdateState;
|
|
50
|
+
exports.maybeSpawnSelfUpdate = maybeSpawnSelfUpdate;
|
|
51
|
+
exports.findInstalledHookScript = findInstalledHookScript;
|
|
52
|
+
exports.runSelfUpdate = runSelfUpdate;
|
|
53
|
+
const fs = __importStar(require("fs"));
|
|
54
|
+
const path = __importStar(require("path"));
|
|
55
|
+
const child_process_1 = require("child_process");
|
|
56
|
+
const version_1 = require("./version");
|
|
57
|
+
const versionCheck_1 = require("./versionCheck");
|
|
58
|
+
const configDir_1 = require("./configDir");
|
|
59
|
+
const captureLog_1 = require("./captureLog");
|
|
60
|
+
const launcher_1 = require("./launcher");
|
|
61
|
+
const PKG = "codiedev";
|
|
62
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
63
|
+
const LOCK_STALE_MS = 10 * 60 * 1000;
|
|
64
|
+
const INSTALL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
65
|
+
const FETCH_TIMEOUT_MS = 5_000;
|
|
66
|
+
/**
|
|
67
|
+
* Decide which package manager owns this install, from where hook.js really
|
|
68
|
+
* lives. We only ever upgrade the install we are running from: running
|
|
69
|
+
* whatever `npm` is first on PATH would, under nvm/Homebrew mixes, upgrade a
|
|
70
|
+
* different global tree than the one the harness config points at.
|
|
71
|
+
*/
|
|
72
|
+
function planSelfUpdate(deps) {
|
|
73
|
+
const { env } = deps;
|
|
74
|
+
if (env.CODIEDEV_NO_UPDATE_CHECK || env.CODIEDEV_NO_AUTO_UPDATE) {
|
|
75
|
+
return { kind: "skip", reason: "disabled" };
|
|
76
|
+
}
|
|
77
|
+
if (env.CI || env.CONTINUOUS_INTEGRATION || env.GITHUB_ACTIONS || env.BUILD_NUMBER) {
|
|
78
|
+
return { kind: "skip", reason: "ci" };
|
|
79
|
+
}
|
|
80
|
+
if (deps.isContainer)
|
|
81
|
+
return { kind: "skip", reason: "container" };
|
|
82
|
+
const p = deps.hookScriptReal.replace(/\\/g, "/");
|
|
83
|
+
if (/\/(?:_npx|\.npm\/_npx)\//.test(p))
|
|
84
|
+
return { kind: "skip", reason: "npx" };
|
|
85
|
+
const pkgDir = path.dirname(path.dirname(deps.hookScriptReal));
|
|
86
|
+
if (!deps.isWritable(pkgDir))
|
|
87
|
+
return { kind: "skip", reason: "readonly" };
|
|
88
|
+
// pnpm puts every global install in a fresh hashed dir
|
|
89
|
+
// (global/v11/<hash>/node_modules/codiedev). That is only safe to upgrade
|
|
90
|
+
// when the hook runs through the stable launcher, which runSelfUpdate
|
|
91
|
+
// re-points at the new dir; a direct-path config would stay on the old copy.
|
|
92
|
+
if (/\/\.pnpm\/|\/pnpm\/global\/|\/global\/v\d+\/[^/]+\/node_modules\/codiedev\/|\/store\/v\d+\/links\//.test(p)) {
|
|
93
|
+
return deps.viaLauncher ? { kind: "pnpm" } : { kind: "skip", reason: "pnpm-direct" };
|
|
94
|
+
}
|
|
95
|
+
// $BUN_INSTALL/install/global — ~/.bun by default, but BUN_INSTALL is often
|
|
96
|
+
// customized, so match the layout rather than the directory name.
|
|
97
|
+
if (/\/install\/global\/node_modules\/codiedev\/dist\/hook\.js$/.test(p))
|
|
98
|
+
return { kind: "bun" };
|
|
99
|
+
// npm global: <prefix>/lib/node_modules/codiedev/dist/hook.js on unix,
|
|
100
|
+
// <prefix>/node_modules/codiedev/dist/hook.js on Windows.
|
|
101
|
+
const unix = p.match(/^(.*)\/lib\/node_modules\/codiedev\/dist\/hook\.js$/);
|
|
102
|
+
if (unix && deps.platform !== "win32")
|
|
103
|
+
return { kind: "npm", prefix: unix[1] };
|
|
104
|
+
const win = p.match(/^(.*)\/node_modules\/codiedev\/dist\/hook\.js$/);
|
|
105
|
+
if (win && deps.platform === "win32")
|
|
106
|
+
return { kind: "npm", prefix: win[1] };
|
|
107
|
+
// A repo checkout, a project-local install, or a layout we don't know.
|
|
108
|
+
return { kind: "skip", reason: "unmanaged" };
|
|
109
|
+
}
|
|
110
|
+
function isSelfUpdateDue(state, nowMs) {
|
|
111
|
+
return !state || nowMs - state.lastAttemptAt > DAY_MS;
|
|
112
|
+
}
|
|
113
|
+
function statePath() {
|
|
114
|
+
return path.join((0, configDir_1.codiedevStateDir)(), ".self-update.json");
|
|
115
|
+
}
|
|
116
|
+
function lockPath() {
|
|
117
|
+
return path.join((0, configDir_1.codiedevStateDir)(), ".self-update.lock");
|
|
118
|
+
}
|
|
119
|
+
function readSelfUpdateState() {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(fs.readFileSync(statePath(), "utf8"));
|
|
122
|
+
return typeof parsed.lastAttemptAt === "number" ? parsed : null;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function writeState(state) {
|
|
129
|
+
try {
|
|
130
|
+
fs.mkdirSync((0, configDir_1.codiedevStateDir)(), { recursive: true });
|
|
131
|
+
fs.writeFileSync(statePath(), JSON.stringify(state), "utf8");
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Called on every hook invocation, so the not-due path is one small file read.
|
|
140
|
+
* Stamps the attempt before spawning so a burst of parallel hooks spawns one
|
|
141
|
+
* child, not one each; the child's lock covers whatever still slips through.
|
|
142
|
+
*/
|
|
143
|
+
function maybeSpawnSelfUpdate(hookEntry) {
|
|
144
|
+
try {
|
|
145
|
+
if (!hookEntry || process.env.CODIEDEV_NO_AUTO_UPDATE || process.env.CODIEDEV_NO_UPDATE_CHECK)
|
|
146
|
+
return;
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
if (!isSelfUpdateDue(readSelfUpdateState(), now))
|
|
149
|
+
return;
|
|
150
|
+
if (!writeState({ lastAttemptAt: now, lastResult: "spawned" }))
|
|
151
|
+
return;
|
|
152
|
+
const child = (0, child_process_1.spawn)(process.execPath, [hookEntry, "self-update"], {
|
|
153
|
+
detached: true,
|
|
154
|
+
stdio: "ignore",
|
|
155
|
+
// detached on Windows opens a console window unless hidden.
|
|
156
|
+
windowsHide: true,
|
|
157
|
+
});
|
|
158
|
+
child.unref();
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Best-effort; a failed spawn retries tomorrow.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function acquireLock() {
|
|
165
|
+
const lock = lockPath();
|
|
166
|
+
try {
|
|
167
|
+
fs.mkdirSync(lock);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
try {
|
|
172
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) {
|
|
173
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
174
|
+
fs.mkdirSync(lock);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Lost the race to another child.
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function detectContainer(env) {
|
|
185
|
+
if (env.KUBERNETES_SERVICE_HOST)
|
|
186
|
+
return true;
|
|
187
|
+
// /run/.containerenv is podman's marker (e.g. Sonifi's dev containers);
|
|
188
|
+
// cgroup v2 hosts report a bare "0::/" there, so the cgroup probe misses it.
|
|
189
|
+
if (fs.existsSync("/.dockerenv") || fs.existsSync("/run/.containerenv"))
|
|
190
|
+
return true;
|
|
191
|
+
try {
|
|
192
|
+
return /docker|containerd|kubepods|lxc/.test(fs.readFileSync("/proc/1/cgroup", "utf8"));
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function registryBase(env) {
|
|
199
|
+
const configured = env.npm_config_registry || env.NPM_CONFIG_REGISTRY;
|
|
200
|
+
return (configured || "https://registry.npmjs.org").replace(/\/+$/, "");
|
|
201
|
+
}
|
|
202
|
+
async function fetchLatest(env) {
|
|
203
|
+
const controller = new AbortController();
|
|
204
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
205
|
+
try {
|
|
206
|
+
const res = await fetch(`${registryBase(env)}/${PKG}/latest`, {
|
|
207
|
+
headers: { Accept: "application/json" },
|
|
208
|
+
signal: controller.signal,
|
|
209
|
+
});
|
|
210
|
+
if (!res.ok)
|
|
211
|
+
return null;
|
|
212
|
+
const json = (await res.json());
|
|
213
|
+
return typeof json.version === "string" ? json.version : null;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function installCommand(plan, version, env) {
|
|
223
|
+
const spec = `${PKG}@${version}`;
|
|
224
|
+
// Pass an env-level registry override explicitly so the install always
|
|
225
|
+
// hits the registry fetchLatest just read from (not every manager honors
|
|
226
|
+
// npm_config_registry for global installs).
|
|
227
|
+
const configured = env.npm_config_registry || env.NPM_CONFIG_REGISTRY;
|
|
228
|
+
const registry = configured ? ["--registry", registryBase(env)] : [];
|
|
229
|
+
switch (plan.kind) {
|
|
230
|
+
case "npm": {
|
|
231
|
+
const args = ["i", "-g", "--prefix", plan.prefix, "--no-fund", "--no-audit", ...registry, spec];
|
|
232
|
+
// Run the npm that ships beside the node running us (it owns this
|
|
233
|
+
// prefix even when PATH resolves a different node), through its JS
|
|
234
|
+
// entry. That avoids spawning npm.cmd, which on Windows needs a shell
|
|
235
|
+
// and would split a prefix like C:\Users\First Last\… on the space.
|
|
236
|
+
const execDir = path.dirname(process.execPath);
|
|
237
|
+
const npmCli = [
|
|
238
|
+
path.join(execDir, "node_modules", "npm", "bin", "npm-cli.js"),
|
|
239
|
+
path.join(execDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
240
|
+
].find((p) => fs.existsSync(p));
|
|
241
|
+
if (npmCli)
|
|
242
|
+
return { cmd: process.execPath, args: [npmCli, ...args] };
|
|
243
|
+
return { cmd: process.platform === "win32" ? "npm.cmd" : "npm", args, shell: process.platform === "win32" };
|
|
244
|
+
}
|
|
245
|
+
case "pnpm":
|
|
246
|
+
// pnpm on Windows is usually a .cmd shim; these args carry no paths.
|
|
247
|
+
return { cmd: "pnpm", args: ["add", "-g", ...registry, spec], shell: process.platform === "win32" };
|
|
248
|
+
case "bun":
|
|
249
|
+
return { cmd: "bun", args: ["add", "-g", ...registry, spec] };
|
|
250
|
+
default:
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function readVersionAt(hookScript) {
|
|
255
|
+
try {
|
|
256
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(path.dirname(path.dirname(hookScript)), "package.json"), "utf8"));
|
|
257
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Where the just-installed copy lives. npm/bun reinstall in place. pnpm's
|
|
265
|
+
* `root -g` is <root>/node_modules up to v10 but the bare global/v11 parent
|
|
266
|
+
* from v11 on, with each install in a hashed child — so check both shapes and
|
|
267
|
+
* take the copy that is actually at the version we just installed.
|
|
268
|
+
*/
|
|
269
|
+
function findInstalledHookScript(plan, hookEntry, version, pnpmRoot = pnpmGlobalRoot) {
|
|
270
|
+
if (plan.kind !== "pnpm")
|
|
271
|
+
return readVersionAt(hookEntry) === version ? hookEntry : null;
|
|
272
|
+
const root = pnpmRoot();
|
|
273
|
+
if (!root)
|
|
274
|
+
return null;
|
|
275
|
+
const candidates = [path.join(root, PKG, "dist", "hook.js")];
|
|
276
|
+
try {
|
|
277
|
+
for (const entry of fs.readdirSync(root)) {
|
|
278
|
+
candidates.push(path.join(root, entry, "node_modules", PKG, "dist", "hook.js"));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
// root vanished between calls
|
|
283
|
+
}
|
|
284
|
+
for (const candidate of candidates) {
|
|
285
|
+
if (readVersionAt(candidate) !== version)
|
|
286
|
+
continue;
|
|
287
|
+
try {
|
|
288
|
+
return fs.realpathSync(candidate);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// dangling link; keep looking
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
function pnpmGlobalRoot() {
|
|
297
|
+
const root = (0, child_process_1.spawnSync)("pnpm", ["root", "-g"], {
|
|
298
|
+
encoding: "utf8",
|
|
299
|
+
timeout: 30_000,
|
|
300
|
+
shell: process.platform === "win32",
|
|
301
|
+
windowsHide: true,
|
|
302
|
+
});
|
|
303
|
+
const dir = root.status === 0 ? root.stdout.trim() : "";
|
|
304
|
+
return dir || null;
|
|
305
|
+
}
|
|
306
|
+
/** Body of the detached `codiedev-hook self-update` child. Never throws. */
|
|
307
|
+
async function runSelfUpdate(hookEntry) {
|
|
308
|
+
const env = process.env;
|
|
309
|
+
const now = Date.now();
|
|
310
|
+
const finish = (lastResult, extra = {}) => {
|
|
311
|
+
const state = { lastAttemptAt: now, lastResult, fromVersion: version_1.CLI_VERSION, ...extra };
|
|
312
|
+
writeState(state);
|
|
313
|
+
(0, captureLog_1.appendCaptureLog)({
|
|
314
|
+
stage: "self-update",
|
|
315
|
+
sessionId: undefined,
|
|
316
|
+
outcome: lastResult.startsWith("error") ? "ERROR" : "ok",
|
|
317
|
+
detail: `result=${lastResult} from=${version_1.CLI_VERSION} to=${extra.toVersion ?? "-"} elapsedMs=${Date.now() - now}`,
|
|
318
|
+
});
|
|
319
|
+
return state;
|
|
320
|
+
};
|
|
321
|
+
let hookScriptReal;
|
|
322
|
+
try {
|
|
323
|
+
hookScriptReal = fs.realpathSync(hookEntry);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return finish("skip:unresolved");
|
|
327
|
+
}
|
|
328
|
+
const plan = planSelfUpdate({
|
|
329
|
+
hookScriptReal,
|
|
330
|
+
env,
|
|
331
|
+
platform: process.platform,
|
|
332
|
+
isContainer: detectContainer(env),
|
|
333
|
+
viaLauncher: Boolean(env.CODIEDEV_VIA_LAUNCHER),
|
|
334
|
+
isWritable: (dir) => {
|
|
335
|
+
try {
|
|
336
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
if (plan.kind === "skip")
|
|
345
|
+
return finish(`skip:${plan.reason}`);
|
|
346
|
+
if (!acquireLock()) {
|
|
347
|
+
// Another child is mid-install and owns the state file; overwriting it
|
|
348
|
+
// here would report "skip:locked" while that install is still running.
|
|
349
|
+
(0, captureLog_1.appendCaptureLog)({ stage: "self-update", sessionId: undefined, outcome: "ok", detail: "result=skip:locked" });
|
|
350
|
+
return { lastAttemptAt: now, lastResult: "skip:locked" };
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
const latest = await fetchLatest(env);
|
|
354
|
+
if (!latest)
|
|
355
|
+
return finish("error:registry");
|
|
356
|
+
if ((0, versionCheck_1.compareSemver)(version_1.CLI_VERSION, latest) !== -1)
|
|
357
|
+
return finish("current", { toVersion: latest });
|
|
358
|
+
const install = installCommand(plan, latest, env);
|
|
359
|
+
if (!install)
|
|
360
|
+
return finish("skip:no-installer");
|
|
361
|
+
const result = (0, child_process_1.spawnSync)(install.cmd, install.args, {
|
|
362
|
+
stdio: "ignore",
|
|
363
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
364
|
+
env: { ...env, CODIEDEV_NO_AUTO_UPDATE: "1" },
|
|
365
|
+
shell: install.shell ?? false,
|
|
366
|
+
windowsHide: true,
|
|
367
|
+
});
|
|
368
|
+
if (result.error || result.status !== 0) {
|
|
369
|
+
return finish(`error:install-exit-${result.status ?? "spawn"}`, { toVersion: latest });
|
|
370
|
+
}
|
|
371
|
+
// Re-point the launcher. npm/bun reinstall in place; pnpm lands in a new
|
|
372
|
+
// hashed dir. Only switch once the new copy is on disk at the new version,
|
|
373
|
+
// so a half-finished install can never strand the hooks.
|
|
374
|
+
const installed = findInstalledHookScript(plan, hookScriptReal, latest);
|
|
375
|
+
if (!installed) {
|
|
376
|
+
return finish("error:not-found-after-install", { toVersion: latest });
|
|
377
|
+
}
|
|
378
|
+
if (!(0, launcher_1.recordCurrentInstall)(installed))
|
|
379
|
+
return finish("error:record", { toVersion: latest });
|
|
380
|
+
return finish("updated", { toVersion: latest });
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
try {
|
|
384
|
+
fs.rmSync(lockPath(), { recursive: true, force: true });
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// A stale lock self-expires after LOCK_STALE_MS.
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -120,6 +120,9 @@ export declare function ensureDiscoveryHookInstalled(): void;
|
|
|
120
120
|
export declare function ensureBackupHookInstalled(): void;
|
|
121
121
|
export declare function ensureLimitHookInstalled(): void;
|
|
122
122
|
export declare function ensureAllCodiedevHooksInstalled(): void;
|
|
123
|
+
/** Exported for unit testing. */
|
|
124
|
+
export declare function hasDirectHookPath(configText: string): boolean;
|
|
125
|
+
export declare function migrateHooksToLauncher(): string[];
|
|
123
126
|
export declare function ensureSessionStartHookInstalled(): void;
|
|
124
127
|
export declare function installClaudeCodeInstructions(): void;
|
|
125
128
|
export declare function installCodexInstructions(): void;
|
|
@@ -356,4 +359,5 @@ interface DeferredUploadDeps extends UploadDeps {
|
|
|
356
359
|
}
|
|
357
360
|
export declare function runDeferredUpload(jobFile: string, delayMs: number, deps: DeferredUploadDeps): Promise<void>;
|
|
358
361
|
export declare function transcriptReadSkipReason(err: unknown): string;
|
|
362
|
+
export declare function isCodexRolloutPath(transcriptPath: string | undefined): boolean;
|
|
359
363
|
export declare function shouldReportCaptureSkip(final: boolean | undefined): boolean;
|
package/dist/utils.js
CHANGED
|
@@ -71,6 +71,8 @@ exports.ensureDiscoveryHookInstalled = ensureDiscoveryHookInstalled;
|
|
|
71
71
|
exports.ensureBackupHookInstalled = ensureBackupHookInstalled;
|
|
72
72
|
exports.ensureLimitHookInstalled = ensureLimitHookInstalled;
|
|
73
73
|
exports.ensureAllCodiedevHooksInstalled = ensureAllCodiedevHooksInstalled;
|
|
74
|
+
exports.hasDirectHookPath = hasDirectHookPath;
|
|
75
|
+
exports.migrateHooksToLauncher = migrateHooksToLauncher;
|
|
74
76
|
exports.ensureSessionStartHookInstalled = ensureSessionStartHookInstalled;
|
|
75
77
|
exports.installClaudeCodeInstructions = installClaudeCodeInstructions;
|
|
76
78
|
exports.installCodexInstructions = installCodexInstructions;
|
|
@@ -119,6 +121,7 @@ exports.readLastCapture = readLastCapture;
|
|
|
119
121
|
exports.runCaptureUpload = runCaptureUpload;
|
|
120
122
|
exports.runDeferredUpload = runDeferredUpload;
|
|
121
123
|
exports.transcriptReadSkipReason = transcriptReadSkipReason;
|
|
124
|
+
exports.isCodexRolloutPath = isCodexRolloutPath;
|
|
122
125
|
exports.shouldReportCaptureSkip = shouldReportCaptureSkip;
|
|
123
126
|
const fs = __importStar(require("fs"));
|
|
124
127
|
const transcriptModels_1 = require("./transcriptModels");
|
|
@@ -130,9 +133,11 @@ const captureLog_1 = require("./captureLog");
|
|
|
130
133
|
const configDir_1 = require("./configDir");
|
|
131
134
|
Object.defineProperty(exports, "codiedevStateDir", { enumerable: true, get: function () { return configDir_1.codiedevStateDir; } });
|
|
132
135
|
const codexHookTrust_1 = require("./codexHookTrust");
|
|
136
|
+
const launcher_1 = require("./launcher");
|
|
133
137
|
const teamSkillContext_1 = require("./teamSkillContext");
|
|
134
138
|
const nativeSteering_1 = require("./nativeSteering");
|
|
135
139
|
const copilotHooks_1 = require("./copilotHooks");
|
|
140
|
+
const harnessDirs_1 = require("./harnessDirs");
|
|
136
141
|
const CONFIG_DIR = (0, configDir_1.codiedevStateDir)();
|
|
137
142
|
exports.CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
138
143
|
function readConfig() {
|
|
@@ -886,12 +891,12 @@ function hashToken(token) {
|
|
|
886
891
|
}
|
|
887
892
|
return hash.toString(36);
|
|
888
893
|
}
|
|
889
|
-
const CLAUDE_SETTINGS_PATH = path.join(
|
|
890
|
-
const CLAUDE_DIR =
|
|
891
|
-
const CODEX_HOOKS_PATH = path.join(
|
|
892
|
-
const CODEX_DIR =
|
|
894
|
+
const CLAUDE_SETTINGS_PATH = path.join((0, harnessDirs_1.claudeConfigDir)(), "settings.json");
|
|
895
|
+
const CLAUDE_DIR = (0, harnessDirs_1.claudeConfigDir)();
|
|
896
|
+
const CODEX_HOOKS_PATH = path.join((0, harnessDirs_1.codexHome)(), "hooks.json");
|
|
897
|
+
const CODEX_DIR = (0, harnessDirs_1.codexHome)();
|
|
893
898
|
const CURSOR_DIR = path.join(os.homedir(), ".cursor");
|
|
894
|
-
const OPENCODE_DIR =
|
|
899
|
+
const OPENCODE_DIR = (0, harnessDirs_1.openCodeConfigDir)();
|
|
895
900
|
const OPENCODE_PLUGIN_DIR = path.join(OPENCODE_DIR, "plugins");
|
|
896
901
|
const OPENCODE_PLUGIN_PATH = path.join(OPENCODE_PLUGIN_DIR, "codiedev.js");
|
|
897
902
|
const CURSOR_HOOKS_PATH = path.join(CURSOR_DIR, "hooks.json");
|
|
@@ -1018,7 +1023,10 @@ function resolveHookInvocation(subcommand, preferredNodePath) {
|
|
|
1018
1023
|
const hookScript = path.join(distDir, "hook.js");
|
|
1019
1024
|
if (fs.existsSync(hookScript)) {
|
|
1020
1025
|
const nodePath = resolveHookNodePath(preferredNodePath);
|
|
1021
|
-
|
|
1026
|
+
// Bake the stable launcher, not this install's path, so later
|
|
1027
|
+
// upgrades (including pnpm's hashed dirs) never strand the hook.
|
|
1028
|
+
const launcher = (0, launcher_1.ensureLauncher)(hookScript);
|
|
1029
|
+
return { command: nodePath, args: [launcher ?? hookScript, subcommand] };
|
|
1022
1030
|
}
|
|
1023
1031
|
}
|
|
1024
1032
|
}
|
|
@@ -1391,6 +1399,44 @@ function ensureAllCodiedevHooksInstalled() {
|
|
|
1391
1399
|
// Best-effort — never block a session on self-heal.
|
|
1392
1400
|
}
|
|
1393
1401
|
}
|
|
1402
|
+
// Matches a hook config that bakes an install's own dist/hook.js rather than
|
|
1403
|
+
// the stable launcher (see launcher.ts). Runs on raw file text, where JSON
|
|
1404
|
+
// doubles Windows separators (codiedev\\dist\\hook.js), hence `+`.
|
|
1405
|
+
const DIRECT_HOOK_PATH = /codiedev[^"'\s/\\]*[\\/]+dist[\\/]+hook\.js/;
|
|
1406
|
+
/** Exported for unit testing. */
|
|
1407
|
+
function hasDirectHookPath(configText) {
|
|
1408
|
+
return DIRECT_HOOK_PATH.test(configText);
|
|
1409
|
+
}
|
|
1410
|
+
// Move configs written before the launcher existed onto it, by re-running each
|
|
1411
|
+
// harness's own installer (which, for Codex, also re-trusts the new commands).
|
|
1412
|
+
// Only harnesses that already carry a direct-path codiedev hook are touched,
|
|
1413
|
+
// each in isolation. Bails when the launcher can't be written, so a read-only
|
|
1414
|
+
// state dir doesn't rewrite every config on every hook call.
|
|
1415
|
+
function migrateHooksToLauncher() {
|
|
1416
|
+
const probe = resolveHookInvocation("session-start");
|
|
1417
|
+
if (!(0, launcher_1.usesLauncher)(probe.args[0]))
|
|
1418
|
+
return [];
|
|
1419
|
+
const targets = [
|
|
1420
|
+
["claude-code", CLAUDE_SETTINGS_PATH, installHook],
|
|
1421
|
+
["codex", CODEX_HOOKS_PATH, installCodexHook],
|
|
1422
|
+
["cursor", CURSOR_HOOKS_PATH, installCursorHook],
|
|
1423
|
+
["copilot", copilotHooksPath(), installCopilotHook],
|
|
1424
|
+
["opencode", OPENCODE_PLUGIN_PATH, installOpenCodeCapturePlugin],
|
|
1425
|
+
];
|
|
1426
|
+
const migrated = [];
|
|
1427
|
+
for (const [name, file, install] of targets) {
|
|
1428
|
+
try {
|
|
1429
|
+
if (!fs.existsSync(file) || !hasDirectHookPath(fs.readFileSync(file, "utf8")))
|
|
1430
|
+
continue;
|
|
1431
|
+
install();
|
|
1432
|
+
migrated.push(name);
|
|
1433
|
+
}
|
|
1434
|
+
catch {
|
|
1435
|
+
// Best-effort per harness; the next direct-path hook call retries.
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return migrated;
|
|
1439
|
+
}
|
|
1394
1440
|
function hasCodiedevHook(hooks, eventName) {
|
|
1395
1441
|
const entries = hooks[eventName] ?? [];
|
|
1396
1442
|
// A malformed (non-array) event value would make `.some` throw and — under
|
|
@@ -1433,10 +1479,10 @@ function ensureSessionStartHookInstalled() {
|
|
|
1433
1479
|
// MCP server installation — writes the codiedev MCP server config into each
|
|
1434
1480
|
// agent CLI's expected location so "npx codiedev connect" wires it up once.
|
|
1435
1481
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1436
|
-
const CLAUDE_USER_CONFIG_PATH =
|
|
1437
|
-
const CLAUDE_USER_INSTRUCTIONS_PATH = path.join(
|
|
1438
|
-
const CODEX_USER_INSTRUCTIONS_PATH = path.join(
|
|
1439
|
-
const CODEX_CONFIG_PATH = path.join(
|
|
1482
|
+
const CLAUDE_USER_CONFIG_PATH = (0, harnessDirs_1.claudeUserConfigPath)();
|
|
1483
|
+
const CLAUDE_USER_INSTRUCTIONS_PATH = path.join((0, harnessDirs_1.claudeConfigDir)(), "CLAUDE.md");
|
|
1484
|
+
const CODEX_USER_INSTRUCTIONS_PATH = path.join((0, harnessDirs_1.codexHome)(), "AGENTS.md");
|
|
1485
|
+
const CODEX_CONFIG_PATH = path.join((0, harnessDirs_1.codexHome)(), "config.toml");
|
|
1440
1486
|
const CODIEDEV_INSTRUCTIONS_BEGIN = "<!-- codiedev-cli:begin -->";
|
|
1441
1487
|
const CODIEDEV_INSTRUCTIONS_END = "<!-- codiedev-cli:end -->";
|
|
1442
1488
|
const CODIEDEV_INSTRUCTIONS_BODY = `## Codiedev — team artifact layer
|
|
@@ -2131,7 +2177,37 @@ function parseClaudeUsage(content) {
|
|
|
2131
2177
|
}
|
|
2132
2178
|
const sum = (field) => models.reduce((total, usage) => total + usage[field], 0);
|
|
2133
2179
|
const cacheReadTokens = sum("cacheReadTokens"), cacheCreationTokens = sum("cacheCreationTokens");
|
|
2134
|
-
|
|
2180
|
+
const stats = { inputTokens: sum("inputTokens"), outputTokens: sum("outputTokens"),
|
|
2181
|
+
cacheTokens: cacheReadTokens + cacheCreationTokens, cacheReadTokens, cacheCreationTokens, model };
|
|
2182
|
+
return model ? stats : fromClaudeCostState(content, stats);
|
|
2183
|
+
}
|
|
2184
|
+
// Some Claude Code transcripts carry no assistant records, only a closing
|
|
2185
|
+
// `cost-state` whose `modelUsage` is keyed by model id. Without it these
|
|
2186
|
+
// sessions upload with no model and trip harness health model_missing.
|
|
2187
|
+
function fromClaudeCostState(content, stats) {
|
|
2188
|
+
let usage;
|
|
2189
|
+
for (const line of content.split("\n")) {
|
|
2190
|
+
if (!line.includes('"cost-state"'))
|
|
2191
|
+
continue;
|
|
2192
|
+
try {
|
|
2193
|
+
const e = JSON.parse(line);
|
|
2194
|
+
if (e?.type === "cost-state" && e.modelUsage && typeof e.modelUsage === "object")
|
|
2195
|
+
usage = e.modelUsage;
|
|
2196
|
+
}
|
|
2197
|
+
catch { /* partial records */ }
|
|
2198
|
+
}
|
|
2199
|
+
const n = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
|
|
2200
|
+
const entries = Object.entries(usage ?? {}).filter(([id, u]) => id.trim() && !id.startsWith("<") && u && typeof u === "object");
|
|
2201
|
+
if (!entries.length)
|
|
2202
|
+
return stats;
|
|
2203
|
+
// A session that switched models names the one that did most of the work.
|
|
2204
|
+
const [model] = entries.reduce((best, entry) => n(entry[1].outputTokens) > n(best[1].outputTokens) ? entry : best);
|
|
2205
|
+
// Assistant records, when present, already counted tokens; never add twice.
|
|
2206
|
+
if (stats.inputTokens || stats.outputTokens || stats.cacheTokens)
|
|
2207
|
+
return { ...stats, model };
|
|
2208
|
+
const total = (field) => entries.reduce((sum, [, u]) => sum + n(u[field]), 0);
|
|
2209
|
+
const cacheReadTokens = total("cacheReadInputTokens"), cacheCreationTokens = total("cacheCreationInputTokens");
|
|
2210
|
+
return { inputTokens: total("inputTokens"), outputTokens: total("outputTokens"),
|
|
2135
2211
|
cacheTokens: cacheReadTokens + cacheCreationTokens, cacheReadTokens, cacheCreationTokens, model };
|
|
2136
2212
|
}
|
|
2137
2213
|
function parseCodexUsage(content) {
|
|
@@ -3045,6 +3121,13 @@ function transcriptReadSkipReason(err) {
|
|
|
3045
3121
|
return "no-transcript";
|
|
3046
3122
|
return `transcript-unreadable:${code || "unknown"}`;
|
|
3047
3123
|
}
|
|
3124
|
+
// Codex's Stop hook also runs the plain `backup` command for artifact backup.
|
|
3125
|
+
// Its transcript is a Codex rollout, already captured per turn by
|
|
3126
|
+
// capture-codex; parsing it as Claude Code would upload an empty duplicate
|
|
3127
|
+
// session with no model under source claude-code.
|
|
3128
|
+
function isCodexRolloutPath(transcriptPath) {
|
|
3129
|
+
return Boolean(transcriptPath && /^rollout-.*\.jsonl$/.test(path.basename(transcriptPath)));
|
|
3130
|
+
}
|
|
3048
3131
|
// Per-turn snapshots are backups, not session endings. Reporting their local
|
|
3049
3132
|
// read/empty condition as a session skip would hide or close coverage while the
|
|
3050
3133
|
// agent is still running; only final/legacy capture paths emit the skip ping.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codiedev",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.65",
|
|
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",
|