codiedev 0.9.50 → 0.9.52
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/cli.js +1 -11
- package/dist/codie.js +0 -121
- package/dist/codieLauncher.d.ts +0 -5
- package/dist/codieLauncher.js +1 -22
- package/dist/commands/doctor.js +0 -8
- package/dist/connect.js +3 -52
- package/dist/hook.js +44 -215
- package/dist/limitEvent.d.ts +4 -1
- package/dist/limitEvent.js +54 -0
- package/dist/limitEventReporter.d.ts +21 -0
- package/dist/limitEventReporter.js +91 -0
- package/dist/utils.d.ts +5 -7
- package/dist/utils.js +48 -145
- package/package.json +3 -10
- package/dist/commands/security.d.ts +0 -1
- package/dist/commands/security.js +0 -156
- package/dist/security.d.ts +0 -97
- package/dist/security.js +0 -422
- package/dist/securityDeepScan.d.ts +0 -41
- package/dist/securityDeepScan.js +0 -248
- package/dist/securityDoctor.d.ts +0 -32
- package/dist/securityDoctor.js +0 -63
- package/dist/securityFeature.d.ts +0 -5
- package/dist/securityFeature.js +0 -7
- package/dist/securityInstall.d.ts +0 -62
- package/dist/securityInstall.js +0 -227
- package/dist/securityInventory.d.ts +0 -54
- package/dist/securityInventory.js +0 -556
- package/dist/securityPilot.d.ts +0 -19
- package/dist/securityPilot.js +0 -61
- package/dist/securityPolicy.d.ts +0 -32
- package/dist/securityPolicy.js +0 -179
- package/dist/securitySession.d.ts +0 -6
- package/dist/securitySession.js +0 -72
package/dist/hook.js
CHANGED
|
@@ -42,14 +42,9 @@ const https = __importStar(require("https"));
|
|
|
42
42
|
const http = __importStar(require("http"));
|
|
43
43
|
const child_process_1 = require("child_process");
|
|
44
44
|
const utils_1 = require("./utils");
|
|
45
|
-
const
|
|
45
|
+
const limitEventReporter_1 = require("./limitEventReporter");
|
|
46
46
|
const version_1 = require("./version");
|
|
47
47
|
const captureLog_1 = require("./captureLog");
|
|
48
|
-
const security_1 = require("./security");
|
|
49
|
-
const securitySession_1 = require("./securitySession");
|
|
50
|
-
const securityPolicy_1 = require("./securityPolicy");
|
|
51
|
-
const securityFeature_1 = require("./securityFeature");
|
|
52
|
-
const securityInventory_1 = require("./securityInventory");
|
|
53
48
|
const captureExclusions_1 = require("./captureExclusions");
|
|
54
49
|
// Pure builder for the discovery additionalContext. Returns null when there are
|
|
55
50
|
// no matches. The suggested `codiedev pull` command carries the EXACT top key
|
|
@@ -98,43 +93,56 @@ async function runDiscover(prompt) {
|
|
|
98
93
|
}
|
|
99
94
|
process.exit(0);
|
|
100
95
|
}
|
|
96
|
+
function readTranscriptTailFile(transcriptPath, maxBytes = 512 * 1024) {
|
|
97
|
+
const size = fs.statSync(transcriptPath).size;
|
|
98
|
+
if (size <= maxBytes)
|
|
99
|
+
return fs.readFileSync(transcriptPath, "utf8");
|
|
100
|
+
const handle = fs.openSync(transcriptPath, "r");
|
|
101
|
+
try {
|
|
102
|
+
const buffer = Buffer.allocUnsafe(maxBytes);
|
|
103
|
+
const bytesRead = fs.readSync(handle, buffer, 0, maxBytes, size - maxBytes);
|
|
104
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
fs.closeSync(handle);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
101
110
|
// StopFailure (matcher rate_limit): the dev just got blocked. Read the local
|
|
102
111
|
// transcript tail for the ground-truth rate_limit line and POST a tiny event.
|
|
103
112
|
// Best-effort + silent — StopFailure output is ignored by Claude Code anyway.
|
|
104
|
-
async function
|
|
113
|
+
async function readAndPostLimitEvent(hookInput, source, emitMinimalOnMiss) {
|
|
105
114
|
const config = (0, utils_1.readConfig)();
|
|
106
115
|
if (!config)
|
|
107
116
|
process.exit(0);
|
|
108
|
-
let
|
|
109
|
-
let transcriptTail;
|
|
117
|
+
let transcript = "";
|
|
110
118
|
const tp = hookInput.transcript_path;
|
|
111
119
|
if (tp) {
|
|
112
120
|
try {
|
|
113
|
-
|
|
114
|
-
event = (0, limitEvent_1.parseLimitEventFromTranscript)(transcript);
|
|
115
|
-
// Ship the tail with the event so the server can summarize "what they
|
|
116
|
-
// were working on" right now — no waiting for the SessionEnd capture.
|
|
117
|
-
transcriptTail = (0, limitEvent_1.tailForLimitEvent)(transcript) || undefined;
|
|
121
|
+
transcript = readTranscriptTailFile(tp);
|
|
118
122
|
}
|
|
119
123
|
catch {
|
|
120
124
|
/* unreadable transcript — fall through to minimal event */
|
|
121
125
|
}
|
|
122
126
|
}
|
|
123
|
-
// The hook is registered with matcher "rate_limit", so a miss still means a
|
|
124
|
-
// real limit — emit a minimal event rather than dropping it.
|
|
125
|
-
const payload = event ?? { kind: "other", at: Date.now(), resetText: null };
|
|
126
127
|
try {
|
|
127
|
-
await
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
resetText: payload.resetText,
|
|
128
|
+
await (0, limitEventReporter_1.reportTranscriptLimitEvent)({
|
|
129
|
+
config,
|
|
130
|
+
source,
|
|
131
131
|
sessionId: hookInput.session_id,
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
transcript,
|
|
133
|
+
emitMinimalOnMiss,
|
|
134
|
+
});
|
|
134
135
|
}
|
|
135
136
|
catch {
|
|
136
137
|
/* best-effort */
|
|
137
138
|
}
|
|
139
|
+
}
|
|
140
|
+
async function runStopFailure(hookInput) {
|
|
141
|
+
await readAndPostLimitEvent(hookInput, "claude-code", true);
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
async function runCodexLimit(hookInput) {
|
|
145
|
+
await readAndPostLimitEvent(hookInput, "codex", false);
|
|
138
146
|
process.exit(0);
|
|
139
147
|
}
|
|
140
148
|
// Skip-reason ping: this session is ending WITHOUT a capture, and here's why.
|
|
@@ -223,7 +231,7 @@ timeoutMs) {
|
|
|
223
231
|
// postJson variant that returns the parsed response body — the upload-URL
|
|
224
232
|
// handshake needs the { uploadUrl } the server hands back. No timeout: runs
|
|
225
233
|
// only in detached uploader children, never on the shutdown clock.
|
|
226
|
-
function postJsonForResponse(url, body, bearerToken
|
|
234
|
+
function postJsonForResponse(url, body, bearerToken) {
|
|
227
235
|
return new Promise((resolve, reject) => {
|
|
228
236
|
const parsed = new URL(url);
|
|
229
237
|
const data = JSON.stringify(body);
|
|
@@ -257,139 +265,10 @@ function postJsonForResponse(url, body, bearerToken, timeoutMs) {
|
|
|
257
265
|
});
|
|
258
266
|
});
|
|
259
267
|
req.on("error", reject);
|
|
260
|
-
if (timeoutMs) {
|
|
261
|
-
req.setTimeout(timeoutMs, () => {
|
|
262
|
-
req.destroy(new Error(`timeout after ${timeoutMs}ms`));
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
268
|
req.write(data);
|
|
266
269
|
req.end();
|
|
267
270
|
});
|
|
268
271
|
}
|
|
269
|
-
// Local policy runs synchronously before a tool executes. Workflow telemetry
|
|
270
|
-
// is dispatched separately and fail-open, so a detector outage or timeout can
|
|
271
|
-
// never delay or override the deterministic endpoint decision.
|
|
272
|
-
async function runSecurityHook(hookInput, phase, source, uploadTelemetry = true) {
|
|
273
|
-
const config = (0, utils_1.readConfig)();
|
|
274
|
-
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
275
|
-
return;
|
|
276
|
-
const policy = (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId);
|
|
277
|
-
const sessionId = hookInput.session_id ?? hookInput.sessionId;
|
|
278
|
-
const sessionSignal = phase === "pre" ? (0, securitySession_1.readSessionSecuritySignal)(sessionId) : undefined;
|
|
279
|
-
const localDecision = (0, security_1.evaluateToolSecurity)(hookInput, phase, policy, source, sessionSignal);
|
|
280
|
-
if (phase === "post" && (0, security_1.securityInputHasSensitivePath)(hookInput)) {
|
|
281
|
-
(0, securitySession_1.recordSensitiveReadSignal)(sessionId);
|
|
282
|
-
}
|
|
283
|
-
const event = (0, security_1.buildSecurityWorkflowEvent)(hookInput, phase, source);
|
|
284
|
-
if (event) {
|
|
285
|
-
event.localDecision = localDecision.action;
|
|
286
|
-
event.localCategory = localDecision.category;
|
|
287
|
-
}
|
|
288
|
-
if (localDecision.action === "block" && phase === "pre") {
|
|
289
|
-
(0, captureLog_1.appendCaptureLog)({
|
|
290
|
-
stage: "security",
|
|
291
|
-
sessionId: hookInput.session_id ?? hookInput.sessionId ?? "unknown",
|
|
292
|
-
outcome: "ok",
|
|
293
|
-
detail: `phase=${phase} localDecision=block category=${localDecision.category}`,
|
|
294
|
-
});
|
|
295
|
-
if (uploadTelemetry)
|
|
296
|
-
dispatchSecurityEventUpload(event);
|
|
297
|
-
process.stdout.write(JSON.stringify({
|
|
298
|
-
hookSpecificOutput: {
|
|
299
|
-
hookEventName: "PreToolUse",
|
|
300
|
-
permissionDecision: "deny",
|
|
301
|
-
permissionDecisionReason: localDecision.reason,
|
|
302
|
-
},
|
|
303
|
-
}));
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
if (!event)
|
|
307
|
-
return;
|
|
308
|
-
if (uploadTelemetry)
|
|
309
|
-
dispatchSecurityEventUpload(event);
|
|
310
|
-
}
|
|
311
|
-
function runPromptSecurityHook(hookInput, source, uploadTelemetry = true) {
|
|
312
|
-
const config = (0, utils_1.readConfig)();
|
|
313
|
-
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
314
|
-
return false;
|
|
315
|
-
const decision = (0, security_1.evaluatePromptSecurity)(hookInput.prompt, (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId), source);
|
|
316
|
-
if (decision.action !== "block")
|
|
317
|
-
return false;
|
|
318
|
-
(0, captureLog_1.appendCaptureLog)({
|
|
319
|
-
stage: "security",
|
|
320
|
-
sessionId: hookInput.session_id ?? hookInput.sessionId ?? "unknown",
|
|
321
|
-
outcome: "ok",
|
|
322
|
-
detail: `phase=prompt localDecision=block category=${decision.category} telemetry=${uploadTelemetry ? "enabled" : "suppressed"} cwd=${hookInput.cwd ?? "unknown"}`,
|
|
323
|
-
});
|
|
324
|
-
if (uploadTelemetry) {
|
|
325
|
-
dispatchSecurityEventUpload((0, security_1.buildBlockedSecurityPromptEvent)(hookInput, source, decision.category ?? "secret_in_prompt"));
|
|
326
|
-
}
|
|
327
|
-
process.stdout.write(JSON.stringify({
|
|
328
|
-
decision: "block",
|
|
329
|
-
reason: decision.reason,
|
|
330
|
-
}));
|
|
331
|
-
return true;
|
|
332
|
-
}
|
|
333
|
-
function dispatchAllowedPromptSecurityEvent(hookInput, source) {
|
|
334
|
-
const config = (0, utils_1.readConfig)();
|
|
335
|
-
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
336
|
-
return;
|
|
337
|
-
const event = (0, security_1.buildSecurityPromptEvent)(hookInput, source, Date.now, (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId));
|
|
338
|
-
if (event)
|
|
339
|
-
dispatchSecurityEventUpload(event);
|
|
340
|
-
}
|
|
341
|
-
function dispatchSecurityEventUpload(event) {
|
|
342
|
-
if (!event || !(0, securityFeature_1.isAgentSecurityEnabled)((0, utils_1.readConfig)()))
|
|
343
|
-
return;
|
|
344
|
-
try {
|
|
345
|
-
const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1], "security-upload"], {
|
|
346
|
-
detached: true,
|
|
347
|
-
stdio: ["pipe", "ignore", "ignore"],
|
|
348
|
-
});
|
|
349
|
-
child.stdin?.end(JSON.stringify(event));
|
|
350
|
-
child.unref();
|
|
351
|
-
}
|
|
352
|
-
catch {
|
|
353
|
-
// Telemetry is best-effort and must never delay local enforcement.
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
async function runSecurityEventUpload() {
|
|
357
|
-
const config = (0, utils_1.readConfig)();
|
|
358
|
-
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
359
|
-
return;
|
|
360
|
-
const startedAt = Date.now();
|
|
361
|
-
try {
|
|
362
|
-
const raw = await readStdin();
|
|
363
|
-
const event = JSON.parse(raw);
|
|
364
|
-
const eventWithEndpoint = (0, security_1.withEndpointIdentity)(event, {
|
|
365
|
-
endpointId: config.deviceId,
|
|
366
|
-
endpointName: config.deviceName,
|
|
367
|
-
cliVersion: version_1.CLI_VERSION,
|
|
368
|
-
});
|
|
369
|
-
const result = await postJsonForResponse(`${config.backendUrl}/api/cli/securityEvent`, { ...eventWithEndpoint }, config.token,
|
|
370
|
-
// Leave room for a cold backend and durable scheduling. This uploader is
|
|
371
|
-
// detached, so the network budget never delays the developer's prompt or
|
|
372
|
-
// tool call. Policy refreshes use the separately signed sync endpoint.
|
|
373
|
-
5_000);
|
|
374
|
-
if (result?.policy?.schemaVersion === 1) {
|
|
375
|
-
(0, securityPolicy_1.writeEndpointSecurityPolicy)(result.policy, undefined, undefined, config.companyId);
|
|
376
|
-
}
|
|
377
|
-
(0, captureLog_1.appendCaptureLog)({
|
|
378
|
-
stage: "security",
|
|
379
|
-
sessionId: event.sessionId,
|
|
380
|
-
outcome: "ok",
|
|
381
|
-
detail: `phase=${event.phase} elapsedMs=${Date.now() - startedAt} verdict=${String(result?.verdict ?? "unknown")}`,
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
catch (err) {
|
|
385
|
-
(0, captureLog_1.appendCaptureLog)({
|
|
386
|
-
stage: "security",
|
|
387
|
-
sessionId: "upload",
|
|
388
|
-
outcome: "ERROR",
|
|
389
|
-
detail: `elapsedMs=${Date.now() - startedAt} ${err.message}`,
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
272
|
// Raw-bytes POST to a Convex storage upload URL (no auth header — the URL
|
|
394
273
|
// itself is the one-time credential). Returns the parsed { storageId }.
|
|
395
274
|
function postBytes(url, bytes, contentType) {
|
|
@@ -513,20 +392,6 @@ async function main() {
|
|
|
513
392
|
}
|
|
514
393
|
process.exit(0);
|
|
515
394
|
}
|
|
516
|
-
// Daily component drift scan runs in a detached hook process. The parent
|
|
517
|
-
// SessionStart path only performs a timestamp read, so agent startup never
|
|
518
|
-
// waits for filesystem traversal or an inventory upload.
|
|
519
|
-
if (process.argv[2] === "security-inventory-upload") {
|
|
520
|
-
const config = (0, utils_1.readConfig)();
|
|
521
|
-
if (config && (0, securityFeature_1.isAgentSecurityEnabled)(config)) {
|
|
522
|
-
const inventory = (0, securityInventory_1.buildSecurityInventory)({ workspace: process.argv[3] || process.cwd() });
|
|
523
|
-
try {
|
|
524
|
-
await (0, securityInventory_1.uploadSecurityInventory)(inventory, config);
|
|
525
|
-
}
|
|
526
|
-
catch { /* retry after the daily inventory window */ }
|
|
527
|
-
}
|
|
528
|
-
process.exit(0);
|
|
529
|
-
}
|
|
530
395
|
// Detached backup uploader child (spawned by runBackup below). The job file
|
|
531
396
|
// carries only { root, files }; the child re-reads config from disk. No stdin
|
|
532
397
|
// needed, so handle it before readStdin() like the capture uploader above.
|
|
@@ -563,13 +428,6 @@ async function main() {
|
|
|
563
428
|
}
|
|
564
429
|
process.exit(0);
|
|
565
430
|
}
|
|
566
|
-
// Detached, best-effort security telemetry. Local policy decisions have
|
|
567
|
-
// already completed in the parent hook; this child can never delay or alter
|
|
568
|
-
// the developer's action.
|
|
569
|
-
if (process.argv[2] === "security-upload") {
|
|
570
|
-
await runSecurityEventUpload();
|
|
571
|
-
process.exit(0);
|
|
572
|
-
}
|
|
573
431
|
let hookInput = {};
|
|
574
432
|
try {
|
|
575
433
|
const raw = await readStdin();
|
|
@@ -580,60 +438,29 @@ async function main() {
|
|
|
580
438
|
catch {
|
|
581
439
|
process.exit(0);
|
|
582
440
|
}
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const securityMatch = securityCommand.match(/^security-(pre|post|failure)(?:-(codex|codie))?$/);
|
|
589
|
-
if (securityMatch) {
|
|
590
|
-
const source = securityMatch[2] === "codex"
|
|
591
|
-
? "codex"
|
|
592
|
-
: securityMatch[2] === "codie"
|
|
593
|
-
? "codie-harness"
|
|
594
|
-
: "claude-code";
|
|
595
|
-
await runSecurityHook(hookInput, securityMatch[1], source, !captureIgnored);
|
|
441
|
+
// Local privacy boundary: every stdin-driven hook mode passes through this
|
|
442
|
+
// check BEFORE prompt discovery, limit-event tails, SessionStart pings,
|
|
443
|
+
// backup, self-heal, transcript reads, or detached dispatch. Ignored means
|
|
444
|
+
// no Codiedev request at all for this session.
|
|
445
|
+
if ((0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput)) {
|
|
596
446
|
process.exit(0);
|
|
597
447
|
}
|
|
598
448
|
// UserPromptSubmit discovery path — separate from session capture.
|
|
599
|
-
|
|
600
|
-
if (process.argv[2] === "discover" ||
|
|
601
|
-
process.argv[2] === "discover-codex" ||
|
|
602
|
-
process.argv[2] === "discover-codie") {
|
|
603
|
-
const source = promptCommand === "discover-codex"
|
|
604
|
-
? "codex"
|
|
605
|
-
: promptCommand === "discover-codie"
|
|
606
|
-
? "codie-harness"
|
|
607
|
-
: "claude-code";
|
|
608
|
-
// Security runs first and locally. A blocked prompt is erased before it
|
|
609
|
-
// reaches the model, so discovery must not send it to the backend.
|
|
610
|
-
if (runPromptSecurityHook(hookInput, source, !captureIgnored))
|
|
611
|
-
return;
|
|
612
|
-
if (captureIgnored)
|
|
613
|
-
return;
|
|
614
|
-
dispatchAllowedPromptSecurityEvent(hookInput, source);
|
|
449
|
+
if (process.argv[2] === "discover") {
|
|
615
450
|
await runDiscover(hookInput.prompt);
|
|
616
451
|
return;
|
|
617
452
|
}
|
|
618
|
-
if (captureIgnored)
|
|
619
|
-
process.exit(0);
|
|
620
453
|
// StopFailure (matcher rate_limit): post the rate-limit event.
|
|
621
454
|
if (process.argv[2] === "limit") {
|
|
622
455
|
await runStopFailure(hookInput);
|
|
623
456
|
return;
|
|
624
457
|
}
|
|
458
|
+
if (process.argv[2] === "limit-codex") {
|
|
459
|
+
await runCodexLimit(hookInput);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
625
462
|
// SessionStart: post the capture-coverage denominator ping. Tiny + fast.
|
|
626
463
|
if (process.argv[2] === "session-start" || process.argv[2] === "session-start-opencode") {
|
|
627
|
-
if ((0, securityFeature_1.isAgentSecurityEnabled)((0, utils_1.readConfig)()) && (0, securityInventory_1.securityInventoryDue)()) {
|
|
628
|
-
try {
|
|
629
|
-
const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1], "security-inventory-upload", hookInput.cwd || process.cwd()], {
|
|
630
|
-
detached: true,
|
|
631
|
-
stdio: "ignore",
|
|
632
|
-
});
|
|
633
|
-
child.unref();
|
|
634
|
-
}
|
|
635
|
-
catch { /* daily inventory is best-effort */ }
|
|
636
|
-
}
|
|
637
464
|
await runSessionStart(hookInput, process.argv[2] === "session-start-opencode" ? "opencode" : "claude-code");
|
|
638
465
|
return;
|
|
639
466
|
}
|
|
@@ -643,6 +470,8 @@ async function main() {
|
|
|
643
470
|
// pinging (the Sonifi "starts arrive, captures never" loss) — re-wired on
|
|
644
471
|
// their next Stop/SessionEnd. One read + one write, only when missing.
|
|
645
472
|
(0, utils_1.ensureAllCodiedevHooksInstalled)();
|
|
473
|
+
if (process.argv[2] === "capture-codex")
|
|
474
|
+
(0, utils_1.ensureCodexLimitHookInstalled)();
|
|
646
475
|
const { session_id, transcript_path, cwd, workspace_roots } = hookInput;
|
|
647
476
|
const workspaceRoot = workspace_roots && workspace_roots.length > 0
|
|
648
477
|
? workspace_roots[0]
|
package/dist/limitEvent.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
export type LimitKind = "session" | "org_spend" | "other";
|
|
1
|
+
export type LimitKind = "session" | "user_spend" | "org_spend" | "other";
|
|
2
2
|
export interface LimitEvent {
|
|
3
3
|
kind: LimitKind;
|
|
4
4
|
at: number;
|
|
5
5
|
resetText: string | null;
|
|
6
|
+
reason?: string;
|
|
6
7
|
}
|
|
7
8
|
export declare function classifyLimitKind(text: string): LimitKind;
|
|
8
9
|
export declare function parseLimitEventFromTranscript(transcript: string): LimitEvent | null;
|
|
10
|
+
/** Parse the structured rate-limit snapshot written to Codex rollout JSONL. */
|
|
11
|
+
export declare function parseCodexLimitEventFromTranscript(transcript: string): LimitEvent | null;
|
|
9
12
|
export declare function tailForLimitEvent(transcript: string, maxChars?: number): string;
|
package/dist/limitEvent.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.classifyLimitKind = classifyLimitKind;
|
|
4
4
|
exports.parseLimitEventFromTranscript = parseLimitEventFromTranscript;
|
|
5
|
+
exports.parseCodexLimitEventFromTranscript = parseCodexLimitEventFromTranscript;
|
|
5
6
|
exports.tailForLimitEvent = tailForLimitEvent;
|
|
6
7
|
function classifyLimitKind(text) {
|
|
7
8
|
const t = text.toLowerCase();
|
|
@@ -50,6 +51,59 @@ function parseLimitEventFromTranscript(transcript) {
|
|
|
50
51
|
}
|
|
51
52
|
return found;
|
|
52
53
|
}
|
|
54
|
+
function codexLimitKind(reason, spendControlReached) {
|
|
55
|
+
if (spendControlReached || reason.startsWith("workspace_owner_"))
|
|
56
|
+
return "org_spend";
|
|
57
|
+
if (reason.startsWith("workspace_member_"))
|
|
58
|
+
return "user_spend";
|
|
59
|
+
return reason === "rate_limit_reached" ? "session" : "other";
|
|
60
|
+
}
|
|
61
|
+
function codexResetText(rateLimits) {
|
|
62
|
+
const windows = [rateLimits?.primary, rateLimits?.secondary]
|
|
63
|
+
.filter((window) => window && typeof window === "object");
|
|
64
|
+
const reached = windows.filter((window) => Number(window.used_percent) >= 100);
|
|
65
|
+
const candidates = reached.length > 0 ? reached : windows;
|
|
66
|
+
const resetAt = candidates
|
|
67
|
+
.map((window) => Number(window.resets_at))
|
|
68
|
+
.filter((value) => Number.isFinite(value) && value > 0)
|
|
69
|
+
.sort((a, b) => a - b)[0];
|
|
70
|
+
return resetAt ? new Date(resetAt * 1000).toISOString() : null;
|
|
71
|
+
}
|
|
72
|
+
/** Parse the structured rate-limit snapshot written to Codex rollout JSONL. */
|
|
73
|
+
function parseCodexLimitEventFromTranscript(transcript) {
|
|
74
|
+
let found = null;
|
|
75
|
+
for (const line of transcript.split("\n")) {
|
|
76
|
+
if (!line.includes('"rate_limit_reached_type"') && !line.includes('"spend_control_reached"'))
|
|
77
|
+
continue;
|
|
78
|
+
let entry;
|
|
79
|
+
try {
|
|
80
|
+
entry = JSON.parse(line);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (entry?.type !== "event_msg" || entry?.payload?.type !== "token_count")
|
|
86
|
+
continue;
|
|
87
|
+
const rateLimits = entry.payload.rate_limits;
|
|
88
|
+
if (!rateLimits || typeof rateLimits !== "object")
|
|
89
|
+
continue;
|
|
90
|
+
const reachedType = typeof rateLimits.rate_limit_reached_type === "string"
|
|
91
|
+
? rateLimits.rate_limit_reached_type
|
|
92
|
+
: null;
|
|
93
|
+
const spendControlReached = rateLimits.spend_control_reached === true;
|
|
94
|
+
if (!reachedType && !spendControlReached)
|
|
95
|
+
continue;
|
|
96
|
+
const reason = reachedType ?? "spend_control_reached";
|
|
97
|
+
const at = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : NaN;
|
|
98
|
+
found = {
|
|
99
|
+
kind: codexLimitKind(reason, spendControlReached),
|
|
100
|
+
at: Number.isNaN(at) ? Date.now() : at,
|
|
101
|
+
resetText: codexResetText(rateLimits),
|
|
102
|
+
reason,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return found;
|
|
106
|
+
}
|
|
53
107
|
// Tail of the raw transcript to ship with the limit event so the server can
|
|
54
108
|
// summarize "what they were working on" immediately — without waiting for the
|
|
55
109
|
// SessionEnd capture (which can lag hours, or drop entirely on Windows).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type LimitEvent } from "./limitEvent";
|
|
2
|
+
export type LimitEventSource = "claude-code" | "codex";
|
|
3
|
+
export type LimitEventPayload = LimitEvent & {
|
|
4
|
+
source: LimitEventSource;
|
|
5
|
+
sessionId?: string;
|
|
6
|
+
transcriptTail?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function postLimitEvent(config: {
|
|
9
|
+
backendUrl: string;
|
|
10
|
+
token: string;
|
|
11
|
+
}, payload: LimitEventPayload, timeoutMs?: number): Promise<void>;
|
|
12
|
+
export declare function reportTranscriptLimitEvent(args: {
|
|
13
|
+
config: {
|
|
14
|
+
backendUrl: string;
|
|
15
|
+
token: string;
|
|
16
|
+
};
|
|
17
|
+
source: LimitEventSource;
|
|
18
|
+
sessionId?: string;
|
|
19
|
+
transcript: string;
|
|
20
|
+
emitMinimalOnMiss?: boolean;
|
|
21
|
+
}): Promise<boolean>;
|
|
@@ -0,0 +1,91 @@
|
|
|
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.postLimitEvent = postLimitEvent;
|
|
37
|
+
exports.reportTranscriptLimitEvent = reportTranscriptLimitEvent;
|
|
38
|
+
const http = __importStar(require("node:http"));
|
|
39
|
+
const https = __importStar(require("node:https"));
|
|
40
|
+
const version_1 = require("./version");
|
|
41
|
+
const limitEvent_1 = require("./limitEvent");
|
|
42
|
+
function postLimitEvent(config, payload, timeoutMs = 5_000) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const target = new URL("/api/cli/limitEvent", config.backendUrl);
|
|
45
|
+
const data = JSON.stringify(payload);
|
|
46
|
+
const transport = target.protocol === "https:" ? https : http;
|
|
47
|
+
const request = transport.request({
|
|
48
|
+
hostname: target.hostname,
|
|
49
|
+
port: target.port || (target.protocol === "https:" ? 443 : 80),
|
|
50
|
+
path: target.pathname + target.search,
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
"Content-Length": Buffer.byteLength(data),
|
|
55
|
+
Authorization: `Bearer ${config.token}`,
|
|
56
|
+
"X-Codiedev-Cli-Version": version_1.CLI_VERSION,
|
|
57
|
+
},
|
|
58
|
+
}, (response) => {
|
|
59
|
+
let responseBody = "";
|
|
60
|
+
response.on("data", (chunk) => { responseBody += chunk; });
|
|
61
|
+
response.on("end", () => {
|
|
62
|
+
if (response.statusCode && response.statusCode >= 200 && response.statusCode < 300) {
|
|
63
|
+
resolve();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
reject(new Error(`HTTP ${response.statusCode}: ${responseBody}`));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
request.on("error", reject);
|
|
71
|
+
request.setTimeout(timeoutMs, () => request.destroy(new Error(`timeout after ${timeoutMs}ms`)));
|
|
72
|
+
request.write(data);
|
|
73
|
+
request.end();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async function reportTranscriptLimitEvent(args) {
|
|
77
|
+
const parse = args.source === "codex"
|
|
78
|
+
? limitEvent_1.parseCodexLimitEventFromTranscript
|
|
79
|
+
: limitEvent_1.parseLimitEventFromTranscript;
|
|
80
|
+
const parsed = parse(args.transcript);
|
|
81
|
+
if (!parsed && !args.emitMinimalOnMiss)
|
|
82
|
+
return false;
|
|
83
|
+
const event = parsed ?? { kind: "other", at: Date.now(), resetText: null };
|
|
84
|
+
await postLimitEvent(args.config, {
|
|
85
|
+
source: args.source,
|
|
86
|
+
...event,
|
|
87
|
+
sessionId: args.sessionId,
|
|
88
|
+
transcriptTail: args.transcript ? (0, limitEvent_1.tailForLimitEvent)(args.transcript) : undefined,
|
|
89
|
+
});
|
|
90
|
+
return true;
|
|
91
|
+
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -14,7 +14,6 @@ export interface CodiedevConfig {
|
|
|
14
14
|
}>;
|
|
15
15
|
lastSynced: string;
|
|
16
16
|
backendUrl: string;
|
|
17
|
-
agentSecurityEnabled?: boolean;
|
|
18
17
|
deviceId?: string;
|
|
19
18
|
deviceName?: string;
|
|
20
19
|
captureIgnoredPaths?: string[];
|
|
@@ -97,7 +96,7 @@ export declare function openCodeInstalled(): boolean;
|
|
|
97
96
|
/** Install a dormant global OpenCode adapter. Installing it unconditionally
|
|
98
97
|
* keeps `codiedev connect` as the only setup step even when Codie/OpenCode is
|
|
99
98
|
* installed later. OpenCode loads this file from its documented plugin dir. */
|
|
100
|
-
export declare function installOpenCodeCapturePlugin(
|
|
99
|
+
export declare function installOpenCodeCapturePlugin(): void;
|
|
101
100
|
/**
|
|
102
101
|
* VS Code with the GitHub Copilot Chat extension installed. The extension
|
|
103
102
|
* directory pattern is `~/.vscode/extensions/github.copilot-chat-*`. We treat
|
|
@@ -105,13 +104,11 @@ export declare function installOpenCodeCapturePlugin(agentSecurityEnabled?: bool
|
|
|
105
104
|
*/
|
|
106
105
|
export declare function vscodeCopilotInstalled(): boolean;
|
|
107
106
|
export declare function upsertCodiedevHook(hooks: Record<string, unknown>, eventName: string, subcommand: string, matcher?: string): void;
|
|
108
|
-
|
|
109
|
-
export declare function removeCodiedevHook(hooks: Record<string, unknown>, eventName: string): boolean;
|
|
110
|
-
export declare function installHook(agentSecurityEnabled?: boolean): void;
|
|
107
|
+
export declare function installHook(): void;
|
|
111
108
|
export declare function ensureDiscoveryHookInstalled(): void;
|
|
112
109
|
export declare function ensureBackupHookInstalled(): void;
|
|
113
110
|
export declare function ensureLimitHookInstalled(): void;
|
|
114
|
-
export declare function ensureAllCodiedevHooksInstalled(
|
|
111
|
+
export declare function ensureAllCodiedevHooksInstalled(): void;
|
|
115
112
|
export declare function ensureSessionStartHookInstalled(): void;
|
|
116
113
|
export declare function installClaudeCodeInstructions(): void;
|
|
117
114
|
export declare function installCodexInstructions(): void;
|
|
@@ -169,7 +166,8 @@ export declare function cleanupCodexMcp(): void;
|
|
|
169
166
|
* so we append a well-marked block if one isn't already present. Idempotent.
|
|
170
167
|
*/
|
|
171
168
|
export declare function installCodexMcp(): boolean;
|
|
172
|
-
export declare function installCodexHook(
|
|
169
|
+
export declare function installCodexHook(): void;
|
|
170
|
+
export declare function ensureCodexLimitHookInstalled(): void;
|
|
173
171
|
export declare function installCursorHook(): void;
|
|
174
172
|
export interface ParsedStats {
|
|
175
173
|
messageCount: number;
|