codiedev 0.9.80 → 0.10.0-beta.10
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/README.md +12 -5
- package/dist/backup/cache.js +1 -5
- package/dist/backup/match.js +0 -6
- package/dist/backup/run.js +3 -35
- package/dist/captureExclusions.js +0 -12
- package/dist/captureLog.js +0 -28
- package/dist/captureReadiness.js +0 -58
- package/dist/captureSignals.js +0 -11
- package/dist/cli.js +11 -33
- package/dist/cliSession.js +3 -60
- package/dist/codexHookTrust.js +0 -28
- package/dist/codexLimitScan.js +0 -107
- package/dist/codie.js +64 -57
- package/dist/codieBrandInstall.js +0 -18
- package/dist/codieLauncher.js +1 -72
- package/dist/codieRuntime.js +27 -32
- package/dist/codieRuntimeInstall.js +11 -43
- package/dist/codieUsage.js +0 -49
- package/dist/commands/agentTemplates.generated.js +136 -0
- package/dist/commands/agents.js +1088 -0
- package/dist/commands/agentsSdk.generated.js +7 -0
- package/dist/commands/ask.js +0 -3
- package/dist/commands/capture.js +0 -2
- package/dist/commands/createTicket.js +0 -10
- package/dist/commands/doctor.js +2 -72
- package/dist/commands/evals.js +3 -9
- package/dist/commands/inbox.js +0 -2
- package/dist/commands/library.js +0 -3
- package/dist/commands/pack.js +0 -29
- package/dist/commands/ping.js +0 -2
- package/dist/commands/pull.js +0 -9
- package/dist/commands/push.js +0 -12
- package/dist/commands/search.js +0 -4
- package/dist/commands/send.js +0 -5
- package/dist/commands/share.js +0 -4
- package/dist/commands/shared.js +0 -35
- package/dist/commands/traces.js +0 -13
- package/dist/commands/tracesLabel.js +0 -8
- package/dist/configDir.js +1 -18
- package/dist/connect.js +3 -75
- package/dist/connectFlow.js +0 -15
- package/dist/copilotHooks.js +0 -67
- package/dist/copilotInstallation.js +0 -1
- package/dist/cursorHooks.js +1 -11
- package/dist/cursorLifecycle.js +1 -7
- package/dist/detection.js +0 -30
- package/dist/harnessDirs.js +0 -19
- package/dist/hook.js +43 -293
- package/dist/inboxFormat.js +0 -24
- package/dist/launchContext.js +0 -50
- package/dist/launcher.js +0 -33
- package/dist/limitEvent.js +2 -34
- package/dist/managedPi.js +2 -50
- package/dist/mcp.js +0 -48
- package/dist/nativeSteering.js +1 -4
- package/dist/packs/codie.js +0 -15
- package/dist/packs/harnesses.js +1 -38
- package/dist/packs/hookGate.js +0 -33
- package/dist/packs/install.js +0 -20
- package/dist/packs/manifest.js +1 -15
- package/dist/packs/paths.js +0 -8
- package/dist/packs/shim.js +0 -8
- package/dist/packs/store.js +0 -12
- package/dist/packs/sync.js +0 -20
- package/dist/piHooks.js +4 -45
- package/dist/piRuntime.js +0 -18
- package/dist/repoResolver.js +0 -19
- package/dist/secret.js +0 -3
- package/dist/selfUpdate.js +2 -77
- package/dist/skillMarker.js +0 -14
- package/dist/subagents.js +0 -18
- package/dist/teamSkillContext.js +0 -1
- package/dist/transcriptModels.js +0 -8
- package/dist/upgradeNudge.js +1 -30
- package/dist/utils.js +165 -768
- package/dist/version.js +0 -10
- package/dist/versionCheck.js +0 -33
- package/package.json +13 -8
- package/dist/backup/cache.d.ts +0 -14
- package/dist/backup/match.d.ts +0 -2
- package/dist/backup/run.d.ts +0 -25
- package/dist/captureExclusions.d.ts +0 -30
- package/dist/captureLog.d.ts +0 -20
- package/dist/captureReadiness.d.ts +0 -65
- package/dist/captureSignals.d.ts +0 -47
- package/dist/cli.d.ts +0 -15
- package/dist/cliSession.d.ts +0 -108
- package/dist/codexHookTrust.d.ts +0 -49
- package/dist/codexLimitScan.d.ts +0 -64
- package/dist/codie.d.ts +0 -2
- package/dist/codieBrandInstall.d.ts +0 -11
- package/dist/codieLauncher.d.ts +0 -87
- package/dist/codieRuntime.d.ts +0 -35
- package/dist/codieRuntimeInstall.d.ts +0 -56
- package/dist/codieUsage.d.ts +0 -64
- package/dist/commands/ask.d.ts +0 -1
- package/dist/commands/capture.d.ts +0 -14
- package/dist/commands/createTicket.d.ts +0 -27
- package/dist/commands/delete.d.ts +0 -1
- package/dist/commands/doctor.d.ts +0 -9
- package/dist/commands/evals.d.ts +0 -3
- package/dist/commands/inbox.d.ts +0 -3
- package/dist/commands/library.d.ts +0 -2
- package/dist/commands/note.d.ts +0 -1
- package/dist/commands/pack.d.ts +0 -54
- package/dist/commands/ping.d.ts +0 -1
- package/dist/commands/pull.d.ts +0 -2
- package/dist/commands/push.d.ts +0 -33
- package/dist/commands/search.d.ts +0 -2
- package/dist/commands/send.d.ts +0 -2
- package/dist/commands/share.d.ts +0 -2
- package/dist/commands/shared.d.ts +0 -64
- package/dist/commands/traces.d.ts +0 -72
- package/dist/commands/tracesLabel.d.ts +0 -24
- package/dist/configDir.d.ts +0 -10
- package/dist/connect.d.ts +0 -1
- package/dist/connectFlow.d.ts +0 -34
- package/dist/copilotHooks.d.ts +0 -85
- package/dist/copilotInstallation.d.ts +0 -10
- package/dist/cursorHooks.d.ts +0 -13
- package/dist/cursorLifecycle.d.ts +0 -2
- package/dist/detection.d.ts +0 -30
- package/dist/harnessDirs.d.ts +0 -15
- package/dist/hook.d.ts +0 -6
- package/dist/inboxFormat.d.ts +0 -38
- package/dist/launchContext.d.ts +0 -46
- package/dist/launcher.d.ts +0 -25
- package/dist/limitEvent.d.ts +0 -15
- package/dist/limitEventReporter.d.ts +0 -21
- package/dist/managedPi.d.ts +0 -48
- package/dist/mcp.d.ts +0 -12
- package/dist/nativeSteering.d.ts +0 -5
- package/dist/packs/codie.d.ts +0 -45
- package/dist/packs/harnesses.d.ts +0 -74
- package/dist/packs/hookGate.d.ts +0 -53
- package/dist/packs/install.d.ts +0 -46
- package/dist/packs/manifest.d.ts +0 -52
- package/dist/packs/paths.d.ts +0 -17
- package/dist/packs/shim.d.ts +0 -26
- package/dist/packs/store.d.ts +0 -42
- package/dist/packs/sync.d.ts +0 -40
- package/dist/piHooks.d.ts +0 -22
- package/dist/piRuntime.d.ts +0 -26
- package/dist/repoResolver.d.ts +0 -17
- package/dist/secret.d.ts +0 -1
- package/dist/selfUpdate.d.ts +0 -64
- package/dist/skillMarker.d.ts +0 -6
- package/dist/subagents.d.ts +0 -36
- package/dist/teamSkillContext.d.ts +0 -9
- package/dist/transcriptModels.d.ts +0 -20
- package/dist/upgradeNudge.d.ts +0 -24
- package/dist/utils.d.ts +0 -396
- package/dist/version.d.ts +0 -1
- package/dist/versionCheck.d.ts +0 -68
package/dist/cliSession.js
CHANGED
|
@@ -1,27 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// Browser sign-in for `codiedev connect`, and keeping its access token fresh.
|
|
3
|
-
//
|
|
4
|
-
// Sign-in is OAuth 2.1 authorization code + PKCE against the Codiedev
|
|
5
|
-
// backend with a loopback redirect (RFC 8252): the CLI listens on
|
|
6
|
-
// 127.0.0.1:<random port>, opens the portal's consent page, and exchanges the
|
|
7
|
-
// returned code for a 24h access token (cdv_at_) and a 30-day refresh token
|
|
8
|
-
// (cdv_rt_). No long-lived API key is saved.
|
|
9
|
-
//
|
|
10
|
-
// config.json keeps `token` = the current access token, so every existing
|
|
11
|
-
// reader keeps working. Each process entry point (codiedev, codiedev-hook,
|
|
12
|
-
// codie, the MCP server) calls ensureFreshToken() first; it refreshes when the
|
|
13
|
-
// token is about to expire.
|
|
14
|
-
//
|
|
15
|
-
// Refresh tokens are single-use and the server treats reuse of a spent one as
|
|
16
|
-
// theft (it revokes the whole sign-in). Many hooks run in parallel and share
|
|
17
|
-
// config.json, so a refresh goes:
|
|
18
|
-
// 1. read config, note the stale access token ("seen")
|
|
19
|
-
// 2. take ~/.codiedev/refresh.lock
|
|
20
|
-
// 3. re-read config; if the token is no longer "seen", another process
|
|
21
|
-
// already refreshed: use that, no network call
|
|
22
|
-
// 4. else refresh, atomically replace config.json, release
|
|
23
|
-
// specs/tla/CliSession.tla model-checks this: without step 2–3, two hooks can
|
|
24
|
-
// both present the same refresh token and sign the user out.
|
|
25
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
26
3
|
if (k2 === undefined) k2 = k;
|
|
27
4
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -74,15 +51,11 @@ const http = __importStar(require("http"));
|
|
|
74
51
|
const os = __importStar(require("os"));
|
|
75
52
|
const path = __importStar(require("path"));
|
|
76
53
|
const child_process_1 = require("child_process");
|
|
77
|
-
/** Default: refresh when less than 10 minutes remain. */
|
|
78
54
|
exports.DEFAULT_MIN_VALIDITY_MS = 10 * 60_000;
|
|
79
|
-
/** A lock older than this was left by a crashed process. Refresh calls time
|
|
80
|
-
* out well before it (REFRESH_TIMEOUT_MS), so a live holder never looks stale. */
|
|
81
55
|
exports.LOCK_STALE_MS = 30_000;
|
|
82
56
|
const LOCK_WAIT_MS = 20_000;
|
|
83
57
|
const REFRESH_TIMEOUT_MS = 10_000;
|
|
84
58
|
const LOGIN_TIMEOUT_MS = 10 * 60_000;
|
|
85
|
-
// ─── config IO (atomic) ────────────────────────────────────────────────
|
|
86
59
|
function readConfigAt(configPath) {
|
|
87
60
|
try {
|
|
88
61
|
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
@@ -91,15 +64,11 @@ function readConfigAt(configPath) {
|
|
|
91
64
|
return null;
|
|
92
65
|
}
|
|
93
66
|
}
|
|
94
|
-
// Windows refuses to replace a file another process holds without share-delete
|
|
95
|
-
// (antivirus, the search indexer; Node's own readers do share). Those holds
|
|
96
|
-
// last milliseconds, so retry like graceful-fs/write-file-atomic do.
|
|
97
67
|
const RENAME_RETRY_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
98
68
|
const RENAME_RETRY_MS = [10, 25, 50, 100, 200, 400, 800];
|
|
99
69
|
function sleepSync(ms) {
|
|
100
70
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
101
71
|
}
|
|
102
|
-
/** rename(), retrying the transient Windows sharing errors. Exported for tests. */
|
|
103
72
|
function renameWithRetry(from, to, rename = fs.renameSync, sleep = sleepSync) {
|
|
104
73
|
for (let attempt = 0;; attempt++) {
|
|
105
74
|
try {
|
|
@@ -114,11 +83,6 @@ function renameWithRetry(from, to, rename = fs.renameSync, sleep = sleepSync) {
|
|
|
114
83
|
}
|
|
115
84
|
}
|
|
116
85
|
}
|
|
117
|
-
/**
|
|
118
|
-
* Write via temp file + rename so a concurrent reader never sees a torn file.
|
|
119
|
-
* If the rename still fails after ~1.6s of retries (a handle held that long),
|
|
120
|
-
* fall back to writing in place: a rare torn read beats failing `connect`.
|
|
121
|
-
*/
|
|
122
86
|
function writeConfigAtomic(configPath, config) {
|
|
123
87
|
const dir = path.dirname(configPath);
|
|
124
88
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
@@ -134,14 +98,13 @@ function writeConfigAtomic(configPath, config) {
|
|
|
134
98
|
try {
|
|
135
99
|
fs.unlinkSync(tmp);
|
|
136
100
|
}
|
|
137
|
-
catch {
|
|
101
|
+
catch { }
|
|
138
102
|
if (!RENAME_RETRY_CODES.has(err?.code ?? ""))
|
|
139
103
|
throw err;
|
|
140
104
|
}
|
|
141
105
|
fs.writeFileSync(configPath, data, { encoding: "utf8", mode: 0o600 });
|
|
142
106
|
fs.chmodSync(configPath, 0o600);
|
|
143
107
|
}
|
|
144
|
-
// ─── refresh lock ──────────────────────────────────────────────────────
|
|
145
108
|
function lockPathFor(configPath) {
|
|
146
109
|
return path.join(path.dirname(configPath), "refresh.lock");
|
|
147
110
|
}
|
|
@@ -155,14 +118,12 @@ function tryLock(lockPath, now) {
|
|
|
155
118
|
catch (err) {
|
|
156
119
|
if (err?.code !== "EEXIST")
|
|
157
120
|
throw err;
|
|
158
|
-
// Break a lock left by a crashed process.
|
|
159
121
|
try {
|
|
160
122
|
const age = now - fs.statSync(lockPath).mtimeMs;
|
|
161
123
|
if (age > exports.LOCK_STALE_MS)
|
|
162
124
|
fs.unlinkSync(lockPath);
|
|
163
125
|
}
|
|
164
126
|
catch {
|
|
165
|
-
/* raced with its owner releasing it: retry */
|
|
166
127
|
}
|
|
167
128
|
return false;
|
|
168
129
|
}
|
|
@@ -171,7 +132,7 @@ function unlock(lockPath) {
|
|
|
171
132
|
try {
|
|
172
133
|
fs.unlinkSync(lockPath);
|
|
173
134
|
}
|
|
174
|
-
catch {
|
|
135
|
+
catch { }
|
|
175
136
|
}
|
|
176
137
|
function needsRefresh(config, now, minValidityMs) {
|
|
177
138
|
const auth = config.auth;
|
|
@@ -193,11 +154,6 @@ async function postForm(fetchImpl, url, form, timeoutMs) {
|
|
|
193
154
|
clearTimeout(timer);
|
|
194
155
|
}
|
|
195
156
|
}
|
|
196
|
-
/**
|
|
197
|
-
* Make sure config.token is a usable access token for at least
|
|
198
|
-
* `minValidityMs`. Never throws: auth problems surface as the next request's
|
|
199
|
-
* 401, which every command already explains.
|
|
200
|
-
*/
|
|
201
157
|
async function ensureFreshToken(deps, minValidityMs = exports.DEFAULT_MIN_VALIDITY_MS) {
|
|
202
158
|
const now = deps.now ?? Date.now;
|
|
203
159
|
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
@@ -217,7 +173,6 @@ async function ensureFreshToken(deps, minValidityMs = exports.DEFAULT_MIN_VALIDI
|
|
|
217
173
|
return { status: "unavailable", config: first };
|
|
218
174
|
await sleep(50 + Math.floor(Math.random() * 100));
|
|
219
175
|
}
|
|
220
|
-
// Step 3: re-read under the lock.
|
|
221
176
|
const current = readConfigAt(deps.configPath);
|
|
222
177
|
if (!current?.auth)
|
|
223
178
|
return { status: "none", config: current };
|
|
@@ -248,8 +203,6 @@ async function ensureFreshToken(deps, minValidityMs = exports.DEFAULT_MIN_VALIDI
|
|
|
248
203
|
return { status: "refreshed", config: next };
|
|
249
204
|
}
|
|
250
205
|
if (res.status === 400 && res.body?.error === "invalid_grant") {
|
|
251
|
-
// Revoked in the portal, signed out elsewhere, expired, or reuse was
|
|
252
|
-
// detected. Forget the credentials but keep device settings.
|
|
253
206
|
const out = { ...current, token: "" };
|
|
254
207
|
delete out.auth;
|
|
255
208
|
writeConfigAtomic(deps.configPath, out);
|
|
@@ -262,7 +215,6 @@ async function ensureFreshToken(deps, minValidityMs = exports.DEFAULT_MIN_VALIDI
|
|
|
262
215
|
unlock(lockPath);
|
|
263
216
|
}
|
|
264
217
|
}
|
|
265
|
-
// ─── browser sign-in ───────────────────────────────────────────────────
|
|
266
218
|
function base64Url(buf) {
|
|
267
219
|
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
268
220
|
}
|
|
@@ -292,7 +244,6 @@ const DONE_PAGE = (ok, msg) => `<!doctype html><html><head><meta charset="utf-8"
|
|
|
292
244
|
height:100vh;margin:0;background:#fbfbfa;color:#1f1f1f}main{max-width:28rem;text-align:center}
|
|
293
245
|
h1{font-size:1.25rem}</style></head><body><main><h1>${ok ? "You're signed in" : "Sign-in didn't finish"}</h1>
|
|
294
246
|
<p>${msg}</p></main></body></html>`;
|
|
295
|
-
/** Parse `code`/`state` (or `error`) out of a loopback callback URL. */
|
|
296
247
|
function parseCallback(input) {
|
|
297
248
|
try {
|
|
298
249
|
const u = new URL(input.trim());
|
|
@@ -399,13 +350,7 @@ async function browserLogin(opts) {
|
|
|
399
350
|
server.close();
|
|
400
351
|
}
|
|
401
352
|
}
|
|
402
|
-
// ─── device sign-in (SSH / headless) ──────────────────────────────────
|
|
403
353
|
exports.DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
404
|
-
/**
|
|
405
|
-
* True when a browser opened here can't reach the user: an SSH session, or a
|
|
406
|
-
* Linux box with no display. Then `connect` uses the device flow (a code the
|
|
407
|
-
* user approves on any device) instead of a loopback redirect, with no flag.
|
|
408
|
-
*/
|
|
409
354
|
function isHeadless(env = process.env, platform = process.platform) {
|
|
410
355
|
if (env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY)
|
|
411
356
|
return true;
|
|
@@ -458,7 +403,7 @@ async function deviceLogin(opts) {
|
|
|
458
403
|
res = await postForm(fetchImpl, `${opts.backendUrl}/oauth/token`, { grant_type: exports.DEVICE_CODE_GRANT, device_code: auth.device_code, client_id: clientId }, REFRESH_TIMEOUT_MS);
|
|
459
404
|
}
|
|
460
405
|
catch {
|
|
461
|
-
continue;
|
|
406
|
+
continue;
|
|
462
407
|
}
|
|
463
408
|
const body = res.body;
|
|
464
409
|
if (res.status === 200 && typeof body?.access_token === "string" && typeof body.refresh_token === "string") {
|
|
@@ -486,7 +431,6 @@ async function deviceLogin(opts) {
|
|
|
486
431
|
}
|
|
487
432
|
}
|
|
488
433
|
}
|
|
489
|
-
/** `codiedev logout`: end a browser sign-in server-side, forget it locally. */
|
|
490
434
|
async function logout(deps) {
|
|
491
435
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
492
436
|
const config = readConfigAt(deps.configPath);
|
|
@@ -503,7 +447,6 @@ async function logout(deps) {
|
|
|
503
447
|
serverRevoked = res.ok && (await res.json().catch(() => null))?.revoked === true;
|
|
504
448
|
}
|
|
505
449
|
catch {
|
|
506
|
-
/* offline: still forget locally; the portal can revoke it */
|
|
507
450
|
}
|
|
508
451
|
}
|
|
509
452
|
const out = { ...config, token: "" };
|
package/dist/codexHookTrust.js
CHANGED
|
@@ -70,9 +70,6 @@ const MATCHER_EVENTS = new Set([
|
|
|
70
70
|
"SubagentStart",
|
|
71
71
|
"SubagentStop",
|
|
72
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
73
|
function codexPluginContentHash(root) {
|
|
77
74
|
const hash = crypto.createHash("sha256");
|
|
78
75
|
hash.update("codex-plugin-bundle-v1");
|
|
@@ -94,7 +91,6 @@ function codexPluginContentHash(root) {
|
|
|
94
91
|
const stat = fs.lstatSync(current);
|
|
95
92
|
if (stat.isDirectory()) {
|
|
96
93
|
hash.update("directory");
|
|
97
|
-
// Rust sorts by OsString, i.e. bytewise; push reversed so the smallest pops first.
|
|
98
94
|
const entries = fs.readdirSync(current).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)));
|
|
99
95
|
for (const entry of entries.reverse())
|
|
100
96
|
pending.push(path.join(current, entry));
|
|
@@ -128,8 +124,6 @@ function normalizedTimeout(eventName, value) {
|
|
|
128
124
|
return Math.min(3, Math.max(1, configured ?? 1));
|
|
129
125
|
return Math.max(1, configured ?? 600);
|
|
130
126
|
}
|
|
131
|
-
/** Codex hashes a canonical, normalized hook identity before it will execute a
|
|
132
|
-
* user hook. Keep this compatible with codex_config::version_for_toml. */
|
|
133
127
|
function codexCommandHookHash(eventName, group, handler) {
|
|
134
128
|
const normalizedHandler = {
|
|
135
129
|
type: "command",
|
|
@@ -198,11 +192,6 @@ function codiedevCodexHookTrustEntries(hooksPath, hooksFile) {
|
|
|
198
192
|
}
|
|
199
193
|
return entries;
|
|
200
194
|
}
|
|
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
195
|
function stripBlockKeepingForeign(text, begin, end) {
|
|
207
196
|
const foreign = [];
|
|
208
197
|
const stripped = text.replace(new RegExp(`\\n?${begin}\\n([\\s\\S]*?)${end}\\n?`, "g"), (_match, body) => {
|
|
@@ -242,9 +231,6 @@ function removeTable(lines, header) {
|
|
|
242
231
|
}
|
|
243
232
|
return output;
|
|
244
233
|
}
|
|
245
|
-
/** `codiedev connect` is an explicit install action, so trust only the exact
|
|
246
|
-
* Codiedev commands it just wrote. Any later command change produces a new
|
|
247
|
-
* hash and Codex disables that hook until connect is run again. */
|
|
248
234
|
function writeCodexHookTrust(configPath, hooksPath, hooksFile) {
|
|
249
235
|
const entries = codiedevCodexHookTrustEntries(hooksPath, hooksFile);
|
|
250
236
|
let existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
|
|
@@ -295,21 +281,12 @@ function inspectCodexHookTrust(configPath, hooksPath, hooksFile, requiredSubcomm
|
|
|
295
281
|
}).length;
|
|
296
282
|
return { ok: entries.length > 0 && trusted === entries.length, total: entries.length, trusted };
|
|
297
283
|
}
|
|
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
284
|
const PACK_TRUST_BEGIN = "# codiedev-pack-trust:begin";
|
|
305
285
|
const PACK_TRUST_END = "# codiedev-pack-trust:end";
|
|
306
|
-
/** Trust entries for every command handler in a pack's hooks.json. */
|
|
307
286
|
function packCodexHookTrustEntries(hooksPath, hooksFile, plugin) {
|
|
308
287
|
const events = hooksFile?.hooks;
|
|
309
288
|
if (!events || typeof events !== "object")
|
|
310
289
|
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
290
|
const source = plugin
|
|
314
291
|
? `${plugin.id}:${path.relative(plugin.root, hooksPath).split(path.sep).join("/")}`
|
|
315
292
|
: normalizedHooksPath(hooksPath);
|
|
@@ -339,15 +316,10 @@ function packCodexHookTrustEntries(hooksPath, hooksFile, plugin) {
|
|
|
339
316
|
}
|
|
340
317
|
return entries;
|
|
341
318
|
}
|
|
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
319
|
function writeCodexPackHookTrust(configPath, entries) {
|
|
346
320
|
let existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
|
|
347
321
|
existing = stripBlockKeepingForeign(existing, PACK_TRUST_BEGIN, PACK_TRUST_END);
|
|
348
322
|
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
323
|
for (const entry of entries)
|
|
352
324
|
lines = removeTable(lines, tableHeader(entry.key));
|
|
353
325
|
const base = lines.join("\n").trimEnd();
|
package/dist/codexLimitScan.js
CHANGED
|
@@ -41,28 +41,6 @@ exports.sessionIdFromRolloutPath = sessionIdFromRolloutPath;
|
|
|
41
41
|
exports.codexSessionsDir = codexSessionsDir;
|
|
42
42
|
exports.recordCodexLimitReported = recordCodexLimitReported;
|
|
43
43
|
exports.sweepCodexLimits = sweepCodexLimits;
|
|
44
|
-
// Reporting a Codex credit/rate-limit block.
|
|
45
|
-
//
|
|
46
|
-
// WHY THIS EXISTS: the `limit-codex` reporter is a Codex `Stop` hook, and
|
|
47
|
-
// Codex does NOT run Stop hooks on the turn a block kills. Measured on real
|
|
48
|
-
// rollouts: every normal `task_complete` pairs with a Stop-hook run within
|
|
49
|
-
// ~200ms, and the blocked one never does (21 turns → 20 runs; 11 → 10, short
|
|
50
|
-
// by exactly the blocked turn each time). Claude Code has a dedicated
|
|
51
|
-
// StopFailure event with a `rate_limit` matcher, which is why it reports
|
|
52
|
-
// correctly; Codex's HookEventName enum has no equivalent — there is no event
|
|
53
|
-
// at all that fires at the block.
|
|
54
|
-
//
|
|
55
|
-
// So the block can only be reported LATER, from a hook that does run. This
|
|
56
|
-
// module scans the rollout files Codex has already written and reports any
|
|
57
|
-
// block that has not been reported yet. It is driven from UserPromptSubmit
|
|
58
|
-
// (`discover-codex`), which fires on the developer's next prompt — in the
|
|
59
|
-
// blocked session or in a new one — and from Stop, which still catches the
|
|
60
|
-
// (rarer) case of a block followed by more turns.
|
|
61
|
-
//
|
|
62
|
-
// The scan reads BACKWARDS from end of file. The previous reader took a fixed
|
|
63
|
-
// 512 KB tail, which silently lost the event whenever the session kept writing
|
|
64
|
-
// after the block: in one real 44 MB rollout the marker sat 1.13 MB from the
|
|
65
|
-
// end, outside the window, and the parse returned null.
|
|
66
44
|
const fs = __importStar(require("fs"));
|
|
67
45
|
const path = __importStar(require("path"));
|
|
68
46
|
const limitEvent_1 = require("./limitEvent");
|
|
@@ -71,19 +49,9 @@ const harnessDirs_1 = require("./harnessDirs");
|
|
|
71
49
|
const MARKER = Buffer.from("rate_limit_reached_type");
|
|
72
50
|
const SPEND_MARKER = Buffer.from("spend_control_reached");
|
|
73
51
|
const NEWLINE = 0x0a;
|
|
74
|
-
// Read backwards in 256 KB chunks, and give up after 16 MB. Codex rollouts run
|
|
75
|
-
// to tens of megabytes, but a block is always in the session's LAST turn — the
|
|
76
|
-
// bytes after it are whatever the session did once the window reset. 16 MB
|
|
77
|
-
// covers that comfortably without ever loading a whole rollout.
|
|
78
52
|
const DEFAULT_CHUNK_SIZE = 256 * 1024;
|
|
79
53
|
const DEFAULT_MAX_SCAN_BYTES = 16 * 1024 * 1024;
|
|
80
|
-
// Tail shipped to the server so it can summarize what the developer was doing
|
|
81
|
-
// when they got blocked. Matches the server-side cap in http.ts.
|
|
82
54
|
const DEFAULT_TAIL_BYTES = 64 * 1024;
|
|
83
|
-
/**
|
|
84
|
-
* Find the LAST rate-limit/credit block recorded in a Codex rollout file.
|
|
85
|
-
* Returns null when the session was never blocked.
|
|
86
|
-
*/
|
|
87
55
|
function scanCodexRolloutForLimit(filePath, options = {}) {
|
|
88
56
|
const chunkSize = options.chunkSize ?? DEFAULT_CHUNK_SIZE;
|
|
89
57
|
const maxScanBytes = options.maxScanBytes ?? DEFAULT_MAX_SCAN_BYTES;
|
|
@@ -105,8 +73,6 @@ function scanCodexRolloutForLimit(filePath, options = {}) {
|
|
|
105
73
|
}
|
|
106
74
|
try {
|
|
107
75
|
let pos = size;
|
|
108
|
-
// Bytes of the (earlier-read) chunk that belong to a line whose start we
|
|
109
|
-
// have not reached yet. Carried into the next, earlier chunk.
|
|
110
76
|
let carry = Buffer.alloc(0);
|
|
111
77
|
let scanned = 0;
|
|
112
78
|
while (pos > 0 && scanned < maxScanBytes) {
|
|
@@ -118,15 +84,10 @@ function scanCodexRolloutForLimit(filePath, options = {}) {
|
|
|
118
84
|
const read = fs.readSync(fd, chunk, 0, length, pos);
|
|
119
85
|
scanned += read;
|
|
120
86
|
const window = Buffer.concat([chunk.subarray(0, read), carry]);
|
|
121
|
-
// At pos > 0 the first line in the window is partial — its start lies in
|
|
122
|
-
// an earlier chunk — so hold it back rather than parse a torn line.
|
|
123
87
|
let completeStart = 0;
|
|
124
88
|
if (pos > 0) {
|
|
125
89
|
const firstNewline = window.indexOf(NEWLINE);
|
|
126
90
|
if (firstNewline < 0) {
|
|
127
|
-
// The whole window is one unterminated line. A token_count record is
|
|
128
|
-
// ~1 KB, so a line this long is never the one we want: drop it rather
|
|
129
|
-
// than grow the carry without bound.
|
|
130
91
|
carry = window.length > maxScanBytes ? Buffer.alloc(0) : window;
|
|
131
92
|
continue;
|
|
132
93
|
}
|
|
@@ -139,8 +100,6 @@ function scanCodexRolloutForLimit(filePath, options = {}) {
|
|
|
139
100
|
const region = window.subarray(completeStart);
|
|
140
101
|
if (region.indexOf(MARKER) < 0 && region.indexOf(SPEND_MARKER) < 0)
|
|
141
102
|
continue;
|
|
142
|
-
// Walk this window's complete lines from the end backwards; the first
|
|
143
|
-
// match going backwards is the latest one in the file.
|
|
144
103
|
const bounds = [];
|
|
145
104
|
let lineStart = 0;
|
|
146
105
|
for (let index = 0; index < region.length; index += 1) {
|
|
@@ -168,18 +127,10 @@ function scanCodexRolloutForLimit(filePath, options = {}) {
|
|
|
168
127
|
fs.closeSync(fd);
|
|
169
128
|
}
|
|
170
129
|
catch {
|
|
171
|
-
/* best-effort */
|
|
172
130
|
}
|
|
173
131
|
}
|
|
174
132
|
}
|
|
175
|
-
/**
|
|
176
|
-
* Read up to `maxBytes` of the rollout ENDING at `lineEnd` — the work the
|
|
177
|
-
* developer was doing when the block hit, not whatever the session did after
|
|
178
|
-
* the window reset. Resumes at a line boundary so every JSONL line parses.
|
|
179
|
-
*/
|
|
180
133
|
function readTailEndingAt(filePath, lineEnd, maxBytes = DEFAULT_TAIL_BYTES) {
|
|
181
|
-
// Clamp to the file as it is NOW: the offset came from an earlier scan, and
|
|
182
|
-
// reading past EOF would return a short, garbled tail.
|
|
183
134
|
let end = lineEnd;
|
|
184
135
|
try {
|
|
185
136
|
end = Math.min(lineEnd, fs.statSync(filePath).size);
|
|
@@ -215,13 +166,11 @@ function readTailEndingAt(filePath, lineEnd, maxBytes = DEFAULT_TAIL_BYTES) {
|
|
|
215
166
|
fs.closeSync(fd);
|
|
216
167
|
}
|
|
217
168
|
catch {
|
|
218
|
-
/* best-effort */
|
|
219
169
|
}
|
|
220
170
|
}
|
|
221
171
|
}
|
|
222
172
|
const DEFAULT_WITHIN_MS = 24 * 3600_000;
|
|
223
173
|
const DEFAULT_MAX_FILES = 12;
|
|
224
|
-
/** Codex rollout files, newest first, limited by age and count. */
|
|
225
174
|
function findRecentCodexRollouts(sessionsDir, options = {}) {
|
|
226
175
|
const withinMs = options.withinMs ?? DEFAULT_WITHIN_MS;
|
|
227
176
|
const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
@@ -252,7 +201,6 @@ function findRecentCodexRollouts(sessionsDir, options = {}) {
|
|
|
252
201
|
found.push({ file: full, mtime });
|
|
253
202
|
}
|
|
254
203
|
catch {
|
|
255
|
-
/* unreadable — skip */
|
|
256
204
|
}
|
|
257
205
|
}
|
|
258
206
|
};
|
|
@@ -262,17 +210,14 @@ function findRecentCodexRollouts(sessionsDir, options = {}) {
|
|
|
262
210
|
.slice(0, maxFiles)
|
|
263
211
|
.map((entry) => entry.file);
|
|
264
212
|
}
|
|
265
|
-
/** Codex names rollouts `rollout-<ISO local time>-<session id>.jsonl`. */
|
|
266
213
|
function sessionIdFromRolloutPath(filePath) {
|
|
267
214
|
const match = path.basename(filePath).match(/^rollout-.*?T[\d-]+-(.+)\.jsonl$/);
|
|
268
215
|
return match ? match[1] : undefined;
|
|
269
216
|
}
|
|
270
|
-
/** Where Codex writes its rollout JSONL files. */
|
|
271
217
|
function codexSessionsDir() {
|
|
272
218
|
return path.join((0, harnessDirs_1.codexHome)(), "sessions");
|
|
273
219
|
}
|
|
274
220
|
exports.CODEX_LIMIT_STATE_PATH = path.join((0, configDir_1.codiedevStateDir)(), "codex-limit-state.json");
|
|
275
|
-
// Bounds the state file on a machine with a long Codex history.
|
|
276
221
|
const MAX_STATE_ENTRIES = 500;
|
|
277
222
|
function readSweepState(statePath) {
|
|
278
223
|
let parsed;
|
|
@@ -297,10 +242,6 @@ function readSweepState(statePath) {
|
|
|
297
242
|
};
|
|
298
243
|
}
|
|
299
244
|
}
|
|
300
|
-
// Migrate the 0.10.0-beta shape ({ reportedThrough, clean }). The old global
|
|
301
|
-
// watermark is DELIBERATELY dropped rather than carried over: it is exactly
|
|
302
|
-
// what could suppress an older block forever. Re-reporting is safe — the
|
|
303
|
-
// server deduplicates on (companyId, userId, at).
|
|
304
245
|
const legacyClean = parsed.clean;
|
|
305
246
|
if (legacyClean && typeof legacyClean === "object") {
|
|
306
247
|
for (const [file, size] of Object.entries(legacyClean)) {
|
|
@@ -310,9 +251,6 @@ function readSweepState(statePath) {
|
|
|
310
251
|
}
|
|
311
252
|
return { seen };
|
|
312
253
|
}
|
|
313
|
-
// A record is dropped only once its file has been gone for a while. Pruning
|
|
314
|
-
// the moment a path is missing would re-report an already-sent block after any
|
|
315
|
-
// transient rename or move.
|
|
316
254
|
const PRUNE_AFTER_MS = 7 * 24 * 3600_000;
|
|
317
255
|
function pruneSweepState(state, now) {
|
|
318
256
|
for (const [file, record] of Object.entries(state.seen)) {
|
|
@@ -329,11 +267,6 @@ function pruneSweepState(state, now) {
|
|
|
329
267
|
.slice(MAX_STATE_ENTRIES)
|
|
330
268
|
.forEach((file) => delete state.seen[file]);
|
|
331
269
|
}
|
|
332
|
-
/**
|
|
333
|
-
* Merge our records into whatever is on disk NOW and write the result. Two
|
|
334
|
-
* Codex sessions can submit a prompt at the same time; a plain
|
|
335
|
-
* read-modify-write would silently drop the other process's records.
|
|
336
|
-
*/
|
|
337
270
|
function commitSweepState(statePath, ours, now) {
|
|
338
271
|
const current = readSweepState(statePath);
|
|
339
272
|
for (const [file, record] of Object.entries(ours))
|
|
@@ -342,9 +275,6 @@ function commitSweepState(statePath, ours, now) {
|
|
|
342
275
|
writeSweepState(statePath, current);
|
|
343
276
|
}
|
|
344
277
|
function writeSweepState(statePath, state) {
|
|
345
|
-
// Written through a temp file and renamed: Codex kills a hook that overruns
|
|
346
|
-
// its timeout, and a half-written state file would read as "nothing seen"
|
|
347
|
-
// and re-report every block in the window.
|
|
348
278
|
const temp = `${statePath}.${process.pid}.tmp`;
|
|
349
279
|
try {
|
|
350
280
|
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
@@ -356,7 +286,6 @@ function writeSweepState(statePath, state) {
|
|
|
356
286
|
fs.unlinkSync(temp);
|
|
357
287
|
}
|
|
358
288
|
catch {
|
|
359
|
-
/* best-effort */
|
|
360
289
|
}
|
|
361
290
|
}
|
|
362
291
|
}
|
|
@@ -369,46 +298,15 @@ function fingerprint(filePath) {
|
|
|
369
298
|
return null;
|
|
370
299
|
}
|
|
371
300
|
}
|
|
372
|
-
/**
|
|
373
|
-
* Record a block reported outside the sweep (the Stop path), so the next
|
|
374
|
-
* prompt sweep neither re-scans the rollout nor re-posts the event.
|
|
375
|
-
*/
|
|
376
301
|
function recordCodexLimitReported(filePath, at, statePath = exports.CODEX_LIMIT_STATE_PATH) {
|
|
377
302
|
const print = fingerprint(filePath);
|
|
378
303
|
if (!print)
|
|
379
304
|
return;
|
|
380
305
|
commitSweepState(statePath, { [filePath]: { ...print, at, t: Date.now() } }, Date.now());
|
|
381
306
|
}
|
|
382
|
-
// Candidates are listed generously and settled by one statSync each; only
|
|
383
|
-
// files that actually changed cost a read. The budget therefore bounds work
|
|
384
|
-
// without letting idle sessions bury an unreported block.
|
|
385
|
-
//
|
|
386
|
-
// The budget binds only on a COLD START, where nothing has been seen before,
|
|
387
|
-
// so it must be big enough to finish that first pass. A budget of 8 silently
|
|
388
|
-
// starved the older blocked rollouts on a real machine: 49 candidates in the
|
|
389
|
-
// window, the newest 8 all unblocked, nothing reported. A full pass over
|
|
390
|
-
// those 49 costs ~205ms — the backward scan stops at the marker rather than
|
|
391
|
-
// reading each file to its cap — so the budget can afford to cover them.
|
|
392
307
|
const DEFAULT_CANDIDATES = 200;
|
|
393
308
|
const DEFAULT_MAX_SCANS = 64;
|
|
394
|
-
// The budget bounds how MANY files are read; this bounds how LONG. A count is
|
|
395
|
-
// not enough on a slow or network-mounted disk, and this runs on the user's
|
|
396
|
-
// prompt clock. Whatever is not reached stays unrecorded, so the next prompt
|
|
397
|
-
// picks it up rather than skipping it.
|
|
398
309
|
const DEFAULT_DEADLINE_MS = 1_500;
|
|
399
|
-
/**
|
|
400
|
-
* Report every Codex block this machine has not reported yet. Returns how many
|
|
401
|
-
* events were posted. Never throws — it runs inside a hook.
|
|
402
|
-
*
|
|
403
|
-
* State is kept PER FILE, not as one newest-wins timestamp. A single global
|
|
404
|
-
* watermark loses data: rollouts are capped per sweep, so a block in a file
|
|
405
|
-
* outside the cap can be older than one already reported and would then be
|
|
406
|
-
* suppressed forever. A per-file record can only ever cause a re-report, which
|
|
407
|
-
* the server deduplicates on (companyId, userId, at).
|
|
408
|
-
*
|
|
409
|
-
* A file is recorded only after its POST succeeds, so a failed send is retried
|
|
410
|
-
* on the next prompt.
|
|
411
|
-
*/
|
|
412
310
|
async function sweepCodexLimits(args) {
|
|
413
311
|
const statePath = args.statePath ?? exports.CODEX_LIMIT_STATE_PATH;
|
|
414
312
|
const state = readSweepState(statePath);
|
|
@@ -426,8 +324,6 @@ async function sweepCodexLimits(args) {
|
|
|
426
324
|
if (!print)
|
|
427
325
|
continue;
|
|
428
326
|
const record = state.seen[file];
|
|
429
|
-
// Settled by the stat alone — it has not changed since the last scan, so
|
|
430
|
-
// it holds nothing new and does not spend the read budget.
|
|
431
327
|
if (record && record.size === print.size && record.mtime === print.mtime)
|
|
432
328
|
continue;
|
|
433
329
|
if (budget <= 0 || Date.now() >= deadline)
|
|
@@ -439,14 +335,11 @@ async function sweepCodexLimits(args) {
|
|
|
439
335
|
continue;
|
|
440
336
|
}
|
|
441
337
|
if (record && record.at === hit.event.at) {
|
|
442
|
-
// Same block, already reported; the file just grew.
|
|
443
338
|
touched[file] = { ...print, at: record.at, t: now };
|
|
444
339
|
continue;
|
|
445
340
|
}
|
|
446
341
|
pending.push({ file, print, hit });
|
|
447
342
|
}
|
|
448
|
-
// Posted concurrently: each file is independent, and this runs on the
|
|
449
|
-
// prompt path where a serial chain of network timeouts would be felt.
|
|
450
343
|
const results = await Promise.allSettled(pending.map(({ file, hit }) => args.post({
|
|
451
344
|
source: "codex",
|
|
452
345
|
...hit.event,
|