codiedev 0.9.52 → 0.10.0-beta.1
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 +9 -1
- package/dist/captureLog.d.ts +1 -1
- package/dist/cli.js +17 -12
- package/dist/codie.js +146 -1
- package/dist/codieLauncher.d.ts +5 -0
- package/dist/codieLauncher.js +22 -1
- package/dist/codieRuntime.d.ts +11 -1
- package/dist/codieRuntime.js +20 -3
- package/dist/codieRuntimeInstall.d.ts +12 -11
- package/dist/codieRuntimeInstall.js +3 -1
- package/dist/codieStartup.d.ts +26 -0
- package/dist/codieStartup.js +30 -0
- package/dist/commands/doctor.js +8 -0
- package/dist/commands/security.d.ts +1 -0
- package/dist/commands/security.js +156 -0
- package/dist/connect.js +56 -4
- package/dist/cursorHooks.d.ts +23 -0
- package/dist/cursorHooks.js +81 -0
- package/dist/hook.js +272 -26
- package/dist/limitEvent.d.ts +5 -1
- package/dist/limitEvent.js +5 -1
- package/dist/mcp.js +1 -93
- package/dist/repoResolver.js +10 -4
- package/dist/security.d.ts +102 -0
- package/dist/security.js +437 -0
- package/dist/securityDeepScan.d.ts +41 -0
- package/dist/securityDeepScan.js +248 -0
- package/dist/securityDoctor.d.ts +32 -0
- package/dist/securityDoctor.js +63 -0
- package/dist/securityFeature.d.ts +5 -0
- package/dist/securityFeature.js +7 -0
- package/dist/securityInstall.d.ts +63 -0
- package/dist/securityInstall.js +239 -0
- package/dist/securityInventory.d.ts +54 -0
- package/dist/securityInventory.js +556 -0
- package/dist/securityPilot.d.ts +19 -0
- package/dist/securityPilot.js +61 -0
- package/dist/securityPolicy.d.ts +33 -0
- package/dist/securityPolicy.js +188 -0
- package/dist/securitySession.d.ts +6 -0
- package/dist/securitySession.js +72 -0
- package/dist/upgradeNudge.d.ts +1 -0
- package/dist/upgradeNudge.js +1 -0
- package/dist/utils.d.ts +16 -6
- package/dist/utils.js +311 -70
- package/package.json +14 -3
- package/dist/commands/post.d.ts +0 -2
- package/dist/commands/post.js +0 -96
package/dist/hook.js
CHANGED
|
@@ -37,7 +37,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
37
37
|
exports.buildDiscoveryContext = buildDiscoveryContext;
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
const os = __importStar(require("os"));
|
|
40
|
-
const path = __importStar(require("path"));
|
|
41
40
|
const https = __importStar(require("https"));
|
|
42
41
|
const http = __importStar(require("http"));
|
|
43
42
|
const child_process_1 = require("child_process");
|
|
@@ -45,7 +44,13 @@ const utils_1 = require("./utils");
|
|
|
45
44
|
const limitEventReporter_1 = require("./limitEventReporter");
|
|
46
45
|
const version_1 = require("./version");
|
|
47
46
|
const captureLog_1 = require("./captureLog");
|
|
47
|
+
const security_1 = require("./security");
|
|
48
|
+
const securitySession_1 = require("./securitySession");
|
|
49
|
+
const securityPolicy_1 = require("./securityPolicy");
|
|
50
|
+
const securityFeature_1 = require("./securityFeature");
|
|
51
|
+
const securityInventory_1 = require("./securityInventory");
|
|
48
52
|
const captureExclusions_1 = require("./captureExclusions");
|
|
53
|
+
const cursorHooks_1 = require("./cursorHooks");
|
|
49
54
|
// Pure builder for the discovery additionalContext. Returns null when there are
|
|
50
55
|
// no matches. The suggested `codiedev pull` command carries the EXACT top key
|
|
51
56
|
// (e.g. "pr-push.md") rather than a "<key>" placeholder — otherwise agents
|
|
@@ -118,6 +123,8 @@ async function readAndPostLimitEvent(hookInput, source, emitMinimalOnMiss) {
|
|
|
118
123
|
const tp = hookInput.transcript_path;
|
|
119
124
|
if (tp) {
|
|
120
125
|
try {
|
|
126
|
+
// Codex invokes Stop after every turn. Bound this read so long-running
|
|
127
|
+
// sessions do not repeatedly load their entire rollout into memory.
|
|
121
128
|
transcript = readTranscriptTailFile(tp);
|
|
122
129
|
}
|
|
123
130
|
catch {
|
|
@@ -231,7 +238,7 @@ timeoutMs) {
|
|
|
231
238
|
// postJson variant that returns the parsed response body — the upload-URL
|
|
232
239
|
// handshake needs the { uploadUrl } the server hands back. No timeout: runs
|
|
233
240
|
// only in detached uploader children, never on the shutdown clock.
|
|
234
|
-
function postJsonForResponse(url, body, bearerToken) {
|
|
241
|
+
function postJsonForResponse(url, body, bearerToken, timeoutMs) {
|
|
235
242
|
return new Promise((resolve, reject) => {
|
|
236
243
|
const parsed = new URL(url);
|
|
237
244
|
const data = JSON.stringify(body);
|
|
@@ -265,10 +272,145 @@ function postJsonForResponse(url, body, bearerToken) {
|
|
|
265
272
|
});
|
|
266
273
|
});
|
|
267
274
|
req.on("error", reject);
|
|
275
|
+
if (timeoutMs) {
|
|
276
|
+
req.setTimeout(timeoutMs, () => {
|
|
277
|
+
req.destroy(new Error(`timeout after ${timeoutMs}ms`));
|
|
278
|
+
});
|
|
279
|
+
}
|
|
268
280
|
req.write(data);
|
|
269
281
|
req.end();
|
|
270
282
|
});
|
|
271
283
|
}
|
|
284
|
+
// Local policy runs synchronously before a tool executes. Workflow telemetry
|
|
285
|
+
// is dispatched separately and fail-open, so a detector outage or timeout can
|
|
286
|
+
// never delay or override the deterministic endpoint decision.
|
|
287
|
+
async function runSecurityHook(hookInput, phase, source, uploadTelemetry = true) {
|
|
288
|
+
const config = (0, utils_1.readConfig)();
|
|
289
|
+
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
290
|
+
return;
|
|
291
|
+
const policy = (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId);
|
|
292
|
+
const sessionId = hookInput.session_id ?? hookInput.sessionId;
|
|
293
|
+
const sessionSignal = phase === "pre" ? (0, securitySession_1.readSessionSecuritySignal)(sessionId) : undefined;
|
|
294
|
+
const localDecision = (0, security_1.evaluateToolSecurity)(hookInput, phase, policy, source, sessionSignal);
|
|
295
|
+
if (phase === "post" && (0, security_1.securityInputHasSensitivePath)(hookInput)) {
|
|
296
|
+
(0, securitySession_1.recordSensitiveReadSignal)(sessionId);
|
|
297
|
+
}
|
|
298
|
+
const event = (0, security_1.buildSecurityWorkflowEvent)(hookInput, phase, source);
|
|
299
|
+
if (event) {
|
|
300
|
+
event.localDecision = localDecision.action;
|
|
301
|
+
event.localCategory = localDecision.category;
|
|
302
|
+
}
|
|
303
|
+
if (localDecision.action === "block" && phase === "pre") {
|
|
304
|
+
(0, captureLog_1.appendCaptureLog)({
|
|
305
|
+
stage: "security",
|
|
306
|
+
sessionId: hookInput.session_id ?? hookInput.sessionId ?? "unknown",
|
|
307
|
+
outcome: "ok",
|
|
308
|
+
detail: `phase=${phase} localDecision=block category=${localDecision.category}`,
|
|
309
|
+
});
|
|
310
|
+
if (uploadTelemetry)
|
|
311
|
+
dispatchSecurityEventUpload(event);
|
|
312
|
+
const response = source === "cursor"
|
|
313
|
+
? (0, cursorHooks_1.cursorPreToolBlockResponse)(localDecision.reason ?? "Codie blocked this action")
|
|
314
|
+
: {
|
|
315
|
+
hookSpecificOutput: {
|
|
316
|
+
hookEventName: "PreToolUse",
|
|
317
|
+
permissionDecision: "deny",
|
|
318
|
+
permissionDecisionReason: localDecision.reason,
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
process.stdout.write(JSON.stringify(response));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (!event)
|
|
325
|
+
return;
|
|
326
|
+
if (uploadTelemetry)
|
|
327
|
+
dispatchSecurityEventUpload(event);
|
|
328
|
+
}
|
|
329
|
+
function runPromptSecurityHook(hookInput, source, uploadTelemetry = true) {
|
|
330
|
+
const config = (0, utils_1.readConfig)();
|
|
331
|
+
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
332
|
+
return false;
|
|
333
|
+
const decision = (0, security_1.evaluatePromptSecurity)(hookInput.prompt, (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId), source);
|
|
334
|
+
if (decision.action !== "block")
|
|
335
|
+
return false;
|
|
336
|
+
(0, captureLog_1.appendCaptureLog)({
|
|
337
|
+
stage: "security",
|
|
338
|
+
sessionId: hookInput.session_id ?? hookInput.sessionId ?? "unknown",
|
|
339
|
+
outcome: "ok",
|
|
340
|
+
detail: `phase=prompt localDecision=block category=${decision.category} telemetry=${uploadTelemetry ? "enabled" : "suppressed"} cwd=${hookInput.cwd ?? "unknown"}`,
|
|
341
|
+
});
|
|
342
|
+
if (uploadTelemetry) {
|
|
343
|
+
dispatchSecurityEventUpload((0, security_1.buildBlockedSecurityPromptEvent)(hookInput, source, decision.category ?? "secret_in_prompt"));
|
|
344
|
+
}
|
|
345
|
+
const response = source === "cursor"
|
|
346
|
+
? (0, cursorHooks_1.cursorPromptBlockResponse)(decision.reason ?? "Codie blocked this prompt")
|
|
347
|
+
: { decision: "block", reason: decision.reason };
|
|
348
|
+
process.stdout.write(JSON.stringify(response));
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
function dispatchAllowedPromptSecurityEvent(hookInput, source) {
|
|
352
|
+
const config = (0, utils_1.readConfig)();
|
|
353
|
+
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
354
|
+
return;
|
|
355
|
+
const event = (0, security_1.buildSecurityPromptEvent)(hookInput, source, Date.now, (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId));
|
|
356
|
+
if (event)
|
|
357
|
+
dispatchSecurityEventUpload(event);
|
|
358
|
+
}
|
|
359
|
+
function dispatchSecurityEventUpload(event) {
|
|
360
|
+
if (!event || !(0, securityFeature_1.isAgentSecurityEnabled)((0, utils_1.readConfig)()))
|
|
361
|
+
return;
|
|
362
|
+
try {
|
|
363
|
+
const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1], "security-upload"], {
|
|
364
|
+
detached: true,
|
|
365
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
366
|
+
});
|
|
367
|
+
child.stdin?.end(JSON.stringify(event));
|
|
368
|
+
child.unref();
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// Telemetry is best-effort and must never delay local enforcement.
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async function runSecurityEventUpload() {
|
|
375
|
+
const config = (0, utils_1.readConfig)();
|
|
376
|
+
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
377
|
+
return;
|
|
378
|
+
const startedAt = Date.now();
|
|
379
|
+
try {
|
|
380
|
+
const raw = await readStdin();
|
|
381
|
+
const event = JSON.parse(raw);
|
|
382
|
+
const cachedPolicy = (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId);
|
|
383
|
+
const eventWithEndpoint = (0, security_1.withEndpointIdentity)(event, {
|
|
384
|
+
endpointId: config.deviceId,
|
|
385
|
+
endpointName: config.deviceName,
|
|
386
|
+
cliVersion: version_1.CLI_VERSION,
|
|
387
|
+
securityRuntimeVersion: security_1.SECURITY_RUNTIME_VERSION,
|
|
388
|
+
securityPolicyVersion: cachedPolicy?.policyVersion,
|
|
389
|
+
});
|
|
390
|
+
const result = await postJsonForResponse(`${config.backendUrl}/api/cli/securityEvent`, { ...eventWithEndpoint }, config.token,
|
|
391
|
+
// Leave room for a cold backend and durable scheduling. This uploader is
|
|
392
|
+
// detached, so the network budget never delays the developer's prompt or
|
|
393
|
+
// tool call. Policy refreshes use the separately signed sync endpoint.
|
|
394
|
+
5_000);
|
|
395
|
+
if (result?.policy?.schemaVersion === 1) {
|
|
396
|
+
(0, securityPolicy_1.writeEndpointSecurityPolicy)(result.policy, undefined, undefined, config.companyId);
|
|
397
|
+
}
|
|
398
|
+
(0, captureLog_1.appendCaptureLog)({
|
|
399
|
+
stage: "security",
|
|
400
|
+
sessionId: event.sessionId,
|
|
401
|
+
outcome: "ok",
|
|
402
|
+
detail: `phase=${event.phase} elapsedMs=${Date.now() - startedAt} verdict=${String(result?.verdict ?? "unknown")}`,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
catch (err) {
|
|
406
|
+
(0, captureLog_1.appendCaptureLog)({
|
|
407
|
+
stage: "security",
|
|
408
|
+
sessionId: "upload",
|
|
409
|
+
outcome: "ERROR",
|
|
410
|
+
detail: `elapsedMs=${Date.now() - startedAt} ${err.message}`,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
272
414
|
// Raw-bytes POST to a Convex storage upload URL (no auth header — the URL
|
|
273
415
|
// itself is the one-time credential). Returns the parsed { storageId }.
|
|
274
416
|
function postBytes(url, bytes, contentType) {
|
|
@@ -324,7 +466,7 @@ async function readStdin() {
|
|
|
324
466
|
function resolveMode(arg) {
|
|
325
467
|
if (arg === "capture-codex")
|
|
326
468
|
return "codex";
|
|
327
|
-
if (arg === "capture-cursor")
|
|
469
|
+
if (arg === "capture-cursor" || arg === "backup-cursor")
|
|
328
470
|
return "cursor";
|
|
329
471
|
if (arg === "capture-opencode" || arg === "capture-opencode-final")
|
|
330
472
|
return "opencode";
|
|
@@ -392,6 +534,20 @@ async function main() {
|
|
|
392
534
|
}
|
|
393
535
|
process.exit(0);
|
|
394
536
|
}
|
|
537
|
+
// Daily component drift scan runs in a detached hook process. The parent
|
|
538
|
+
// SessionStart path only performs a timestamp read, so agent startup never
|
|
539
|
+
// waits for filesystem traversal or an inventory upload.
|
|
540
|
+
if (process.argv[2] === "security-inventory-upload") {
|
|
541
|
+
const config = (0, utils_1.readConfig)();
|
|
542
|
+
if (config && (0, securityFeature_1.isAgentSecurityEnabled)(config)) {
|
|
543
|
+
const inventory = (0, securityInventory_1.buildSecurityInventory)({ workspace: process.argv[3] || process.cwd() });
|
|
544
|
+
try {
|
|
545
|
+
await (0, securityInventory_1.uploadSecurityInventory)(inventory, config);
|
|
546
|
+
}
|
|
547
|
+
catch { /* retry after the daily inventory window */ }
|
|
548
|
+
}
|
|
549
|
+
process.exit(0);
|
|
550
|
+
}
|
|
395
551
|
// Detached backup uploader child (spawned by runBackup below). The job file
|
|
396
552
|
// carries only { root, files }; the child re-reads config from disk. No stdin
|
|
397
553
|
// needed, so handle it before readStdin() like the capture uploader above.
|
|
@@ -428,28 +584,73 @@ async function main() {
|
|
|
428
584
|
}
|
|
429
585
|
process.exit(0);
|
|
430
586
|
}
|
|
587
|
+
// Detached, best-effort security telemetry. Local policy decisions have
|
|
588
|
+
// already completed in the parent hook; this child can never delay or alter
|
|
589
|
+
// the developer's action.
|
|
590
|
+
if (process.argv[2] === "security-upload") {
|
|
591
|
+
await runSecurityEventUpload();
|
|
592
|
+
process.exit(0);
|
|
593
|
+
}
|
|
431
594
|
let hookInput = {};
|
|
432
595
|
try {
|
|
433
596
|
const raw = await readStdin();
|
|
434
597
|
if (raw.trim()) {
|
|
435
|
-
|
|
598
|
+
const parsed = JSON.parse(raw);
|
|
599
|
+
hookInput = (process.argv[2]?.endsWith("-cursor")
|
|
600
|
+
? (0, cursorHooks_1.normalizeCursorHookInput)(parsed)
|
|
601
|
+
: parsed);
|
|
436
602
|
}
|
|
437
603
|
}
|
|
438
604
|
catch {
|
|
439
605
|
process.exit(0);
|
|
440
606
|
}
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
|
|
607
|
+
// Privacy exclusions suppress every Codiedev upload, but they must never
|
|
608
|
+
// disable local security enforcement. Security hooks run first with telemetry
|
|
609
|
+
// disabled; all other hook modes still exit before making a network request.
|
|
610
|
+
const captureIgnored = (0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput);
|
|
611
|
+
const securityCommand = process.argv[2] ?? "";
|
|
612
|
+
const securityMatch = securityCommand.match(/^security-(pre|post|failure)(?:-(codex|cursor|codie))?$/);
|
|
613
|
+
if (securityMatch) {
|
|
614
|
+
const source = securityMatch[2] === "codex"
|
|
615
|
+
? "codex"
|
|
616
|
+
: securityMatch[2] === "cursor"
|
|
617
|
+
? "cursor"
|
|
618
|
+
: securityMatch[2] === "codie"
|
|
619
|
+
? "codie-harness"
|
|
620
|
+
: "claude-code";
|
|
621
|
+
await runSecurityHook(hookInput, securityMatch[1], source, !captureIgnored);
|
|
446
622
|
process.exit(0);
|
|
447
623
|
}
|
|
448
624
|
// UserPromptSubmit discovery path — separate from session capture.
|
|
449
|
-
|
|
625
|
+
const promptCommand = process.argv[2] ?? "";
|
|
626
|
+
if (promptCommand === "security-prompt-cursor") {
|
|
627
|
+
if (runPromptSecurityHook(hookInput, "cursor", !captureIgnored))
|
|
628
|
+
return;
|
|
629
|
+
if (captureIgnored)
|
|
630
|
+
return;
|
|
631
|
+
dispatchAllowedPromptSecurityEvent(hookInput, "cursor");
|
|
632
|
+
process.exit(0);
|
|
633
|
+
}
|
|
634
|
+
if (process.argv[2] === "discover" ||
|
|
635
|
+
process.argv[2] === "discover-codex" ||
|
|
636
|
+
process.argv[2] === "discover-codie") {
|
|
637
|
+
const source = promptCommand === "discover-codex"
|
|
638
|
+
? "codex"
|
|
639
|
+
: promptCommand === "discover-codie"
|
|
640
|
+
? "codie-harness"
|
|
641
|
+
: "claude-code";
|
|
642
|
+
// Security runs first and locally. A blocked prompt is erased before it
|
|
643
|
+
// reaches the model, so discovery must not send it to the backend.
|
|
644
|
+
if (runPromptSecurityHook(hookInput, source, !captureIgnored))
|
|
645
|
+
return;
|
|
646
|
+
if (captureIgnored)
|
|
647
|
+
return;
|
|
648
|
+
dispatchAllowedPromptSecurityEvent(hookInput, source);
|
|
450
649
|
await runDiscover(hookInput.prompt);
|
|
451
650
|
return;
|
|
452
651
|
}
|
|
652
|
+
if (captureIgnored)
|
|
653
|
+
process.exit(0);
|
|
453
654
|
// StopFailure (matcher rate_limit): post the rate-limit event.
|
|
454
655
|
if (process.argv[2] === "limit") {
|
|
455
656
|
await runStopFailure(hookInput);
|
|
@@ -460,8 +661,24 @@ async function main() {
|
|
|
460
661
|
return;
|
|
461
662
|
}
|
|
462
663
|
// SessionStart: post the capture-coverage denominator ping. Tiny + fast.
|
|
463
|
-
if (process.argv[2] === "session-start" ||
|
|
464
|
-
|
|
664
|
+
if (process.argv[2] === "session-start" ||
|
|
665
|
+
process.argv[2] === "session-start-cursor" ||
|
|
666
|
+
process.argv[2] === "session-start-opencode") {
|
|
667
|
+
if ((0, securityFeature_1.isAgentSecurityEnabled)((0, utils_1.readConfig)()) && (0, securityInventory_1.securityInventoryDue)()) {
|
|
668
|
+
try {
|
|
669
|
+
const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1], "security-inventory-upload", hookInput.cwd || process.cwd()], {
|
|
670
|
+
detached: true,
|
|
671
|
+
stdio: "ignore",
|
|
672
|
+
});
|
|
673
|
+
child.unref();
|
|
674
|
+
}
|
|
675
|
+
catch { /* daily inventory is best-effort */ }
|
|
676
|
+
}
|
|
677
|
+
await runSessionStart(hookInput, process.argv[2] === "session-start-opencode"
|
|
678
|
+
? "opencode"
|
|
679
|
+
: process.argv[2] === "session-start-cursor"
|
|
680
|
+
? "cursor"
|
|
681
|
+
: "claude-code");
|
|
465
682
|
return;
|
|
466
683
|
}
|
|
467
684
|
// Self-heal: a user who upgraded the CLI but hasn't re-run `connect` gets
|
|
@@ -469,7 +686,12 @@ async function main() {
|
|
|
469
686
|
// no self-heal and could stay stripped forever while SessionStart kept
|
|
470
687
|
// pinging (the Sonifi "starts arrive, captures never" loss) — re-wired on
|
|
471
688
|
// their next Stop/SessionEnd. One read + one write, only when missing.
|
|
472
|
-
(
|
|
689
|
+
if ((process.argv[2] ?? "").endsWith("-cursor")) {
|
|
690
|
+
(0, utils_1.ensureCursorCodiedevHooksInstalled)();
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
(0, utils_1.ensureAllCodiedevHooksInstalled)();
|
|
694
|
+
}
|
|
473
695
|
if (process.argv[2] === "capture-codex")
|
|
474
696
|
(0, utils_1.ensureCodexLimitHookInstalled)();
|
|
475
697
|
const { session_id, transcript_path, cwd, workspace_roots } = hookInput;
|
|
@@ -484,13 +706,11 @@ async function main() {
|
|
|
484
706
|
// extension) where SessionEnd may never come — the silent zero-capture bug.
|
|
485
707
|
// Same detached capture-finish child as SessionEnd, marked non-final so the
|
|
486
708
|
// server renews the session lease instead of summarizing immediately.
|
|
487
|
-
if (process.argv[2] === "backup") {
|
|
709
|
+
if (process.argv[2] === "backup" || process.argv[2] === "backup-cursor") {
|
|
488
710
|
if (transcript_path) {
|
|
489
711
|
try {
|
|
490
712
|
(0, utils_1.dispatchCaptureFinish)({
|
|
491
|
-
|
|
492
|
-
// SessionEnd path below (resolveMode maps it to claude-code).
|
|
493
|
-
mode: "capture",
|
|
713
|
+
mode: process.argv[2] === "backup-cursor" ? "capture-cursor" : "capture",
|
|
494
714
|
session_id,
|
|
495
715
|
transcript_path,
|
|
496
716
|
cwd,
|
|
@@ -550,10 +770,9 @@ async function main() {
|
|
|
550
770
|
transcript_path,
|
|
551
771
|
cwd,
|
|
552
772
|
workspace_roots,
|
|
553
|
-
// final:true
|
|
554
|
-
// per-turn Stop hook through "capture-codex" —
|
|
555
|
-
// close the server lease every turn.
|
|
556
|
-
// behaviour (final omitted).
|
|
773
|
+
// final:true for real SessionEnd signals (Claude Code and Cursor).
|
|
774
|
+
// Codex routes its per-turn Stop hook through "capture-codex" —
|
|
775
|
+
// final:true there would close the server lease every turn.
|
|
557
776
|
final: (0, utils_1.resolveCaptureFinal)(process.argv[2]),
|
|
558
777
|
}, process.argv[1], { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile: jobFile, openSync: fs.openSync, closeSync: fs.closeSync });
|
|
559
778
|
}
|
|
@@ -630,7 +849,9 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
630
849
|
outcome: "skip",
|
|
631
850
|
detail: reason,
|
|
632
851
|
});
|
|
633
|
-
|
|
852
|
+
if ((0, utils_1.shouldReportCaptureSkip)(input.final)) {
|
|
853
|
+
await postSessionSkip(config, session_id, reason);
|
|
854
|
+
}
|
|
634
855
|
return;
|
|
635
856
|
}
|
|
636
857
|
// Snapshot the RAW transcript before normalization below flattens it to
|
|
@@ -642,6 +863,9 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
642
863
|
if (parserMode === "opencode") {
|
|
643
864
|
transcriptContent = (0, utils_1.normalizeOpenCodeTranscript)(transcriptContent);
|
|
644
865
|
}
|
|
866
|
+
else if (parserMode === "cursor") {
|
|
867
|
+
transcriptContent = (0, cursorHooks_1.normalizeCursorTranscript)(transcriptContent);
|
|
868
|
+
}
|
|
645
869
|
// Resolve repo URL. Codex embeds it in session_meta; CC and Cursor both
|
|
646
870
|
// need the git remote of the workspace.
|
|
647
871
|
let remoteUrl = null;
|
|
@@ -668,9 +892,26 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
668
892
|
const matchedRepo = remoteUrl ? (0, utils_1.matchRepo)(remoteUrl, config.repos) : null;
|
|
669
893
|
const stats = parserMode === "codex"
|
|
670
894
|
? (0, utils_1.parseCodexStats)(transcriptContent)
|
|
671
|
-
:
|
|
672
|
-
|
|
673
|
-
|
|
895
|
+
: (0, utils_1.parseClaudeCodeStats)(transcriptContent);
|
|
896
|
+
// Cursor can emit duplicate lifecycle notifications while one transcript
|
|
897
|
+
// path is still byte-empty. Never use parsed message count here: Cursor's
|
|
898
|
+
// private JSONL shape can evolve independently, and parser drift must not
|
|
899
|
+
// silently discard a real non-empty session.
|
|
900
|
+
if (parserMode === "cursor" && (0, cursorHooks_1.isCursorTranscriptEmpty)(rawTranscriptContent)) {
|
|
901
|
+
(0, captureLog_1.appendCaptureLog)({
|
|
902
|
+
stage: "capture-finish",
|
|
903
|
+
sessionId: session_id,
|
|
904
|
+
outcome: "skip",
|
|
905
|
+
detail: "empty-cursor-transcript",
|
|
906
|
+
});
|
|
907
|
+
// A non-final stop snapshot is only a backup; reporting it as a skipped
|
|
908
|
+
// session would close/remove server-side start coverage while the session
|
|
909
|
+
// is still alive. The real SessionEnd remains responsible for a skip ping.
|
|
910
|
+
if ((0, utils_1.shouldReportCaptureSkip)(input.final)) {
|
|
911
|
+
await postSessionSkip(config, session_id, "empty-cursor-transcript");
|
|
912
|
+
}
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
674
915
|
const transcriptBase64 = Buffer.from(transcriptContent, "utf8").toString("base64");
|
|
675
916
|
// Session→merge join keys: the branch + HEAD sha the work happened on. Matched
|
|
676
917
|
// server-side against a merged MR/PR's source_branch / commits for AI attribution.
|
|
@@ -763,7 +1004,7 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
763
1004
|
// which is the "supersede" mechanism for the deferred path.
|
|
764
1005
|
try {
|
|
765
1006
|
const hookScriptPath = process.argv[1];
|
|
766
|
-
const tmpFile =
|
|
1007
|
+
const tmpFile = (0, utils_1.captureUploadTmpFile)(session_id, decision.mode);
|
|
767
1008
|
if (decision.mode === "immediate") {
|
|
768
1009
|
(0, utils_1.dispatchCaptureDetached)({
|
|
769
1010
|
url: `${config.backendUrl}/api/captureSession`,
|
|
@@ -771,6 +1012,11 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
771
1012
|
body: payload,
|
|
772
1013
|
scopePaths,
|
|
773
1014
|
}, hookScriptPath, { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile, openSync: fs.openSync, closeSync: fs.closeSync });
|
|
1015
|
+
// The immediate payload is durable before removing the older scheduled
|
|
1016
|
+
// snapshot. Do this for every immediate dispatch, not only SessionEnd:
|
|
1017
|
+
// first/outside-window snapshots also supersede a sleeping deferred job.
|
|
1018
|
+
if (session_id)
|
|
1019
|
+
(0, utils_1.cancelDeferredCaptureUpload)(session_id);
|
|
774
1020
|
// Retry any earlier capture jobs that failed to upload (transient network
|
|
775
1021
|
// blips), excluding the one we just dispatched. Best-effort, detached.
|
|
776
1022
|
// Skip the sweep on deferred path — the deferred child will handle uploading.
|
package/dist/limitEvent.d.ts
CHANGED
|
@@ -7,6 +7,10 @@ export interface LimitEvent {
|
|
|
7
7
|
}
|
|
8
8
|
export declare function classifyLimitKind(text: string): LimitKind;
|
|
9
9
|
export declare function parseLimitEventFromTranscript(transcript: string): LimitEvent | null;
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Codex writes a structured RateLimitSnapshot on token_count events. A normal
|
|
12
|
+
* turn has `rate_limit_reached_type: null`; a blocked turn names the exact
|
|
13
|
+
* boundary. Reading that field avoids locale-dependent terminal copy.
|
|
14
|
+
*/
|
|
11
15
|
export declare function parseCodexLimitEventFromTranscript(transcript: string): LimitEvent | null;
|
|
12
16
|
export declare function tailForLimitEvent(transcript: string, maxChars?: number): string;
|
package/dist/limitEvent.js
CHANGED
|
@@ -69,7 +69,11 @@ function codexResetText(rateLimits) {
|
|
|
69
69
|
.sort((a, b) => a - b)[0];
|
|
70
70
|
return resetAt ? new Date(resetAt * 1000).toISOString() : null;
|
|
71
71
|
}
|
|
72
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Codex writes a structured RateLimitSnapshot on token_count events. A normal
|
|
74
|
+
* turn has `rate_limit_reached_type: null`; a blocked turn names the exact
|
|
75
|
+
* boundary. Reading that field avoids locale-dependent terminal copy.
|
|
76
|
+
*/
|
|
73
77
|
function parseCodexLimitEventFromTranscript(transcript) {
|
|
74
78
|
let found = null;
|
|
75
79
|
for (const line of transcript.split("\n")) {
|
package/dist/mcp.js
CHANGED
|
@@ -260,68 +260,6 @@ const TOOLS = [
|
|
|
260
260
|
required: ["artifactId"],
|
|
261
261
|
},
|
|
262
262
|
},
|
|
263
|
-
{
|
|
264
|
-
name: "codiedev_post_to_feed",
|
|
265
|
-
description: "Publish an artifact to the team's break-room feed with an intent " +
|
|
266
|
-
"and optional @mentions. Use when the user asks to 'post this to " +
|
|
267
|
-
"the team', 'publish to the feed', 'share with the team', 'ask the " +
|
|
268
|
-
"team', or when they want broader visibility than a single ping. " +
|
|
269
|
-
"Pass the artifact's filename (e.g., 'spec-214.md') and pick an " +
|
|
270
|
-
"intent that matches the request: share = here's a skill/spec; " +
|
|
271
|
-
"request_review = please review; request_expertise = I'm stuck, " +
|
|
272
|
-
"who knows this; link_share = fyi; rfc = proposing a change. " +
|
|
273
|
-
"Optionally @mention teammates (by first name) so they get a " +
|
|
274
|
-
"notification. Returns the feed post id.",
|
|
275
|
-
inputSchema: {
|
|
276
|
-
type: "object",
|
|
277
|
-
properties: {
|
|
278
|
-
filename: {
|
|
279
|
-
type: "string",
|
|
280
|
-
description: "Artifact filename to attach (e.g., 'spec-214.md'). The post " +
|
|
281
|
-
"will render a preview card linking to it.",
|
|
282
|
-
},
|
|
283
|
-
title: {
|
|
284
|
-
type: "string",
|
|
285
|
-
description: "Headline of the post. Short, imperative — e.g., 'Check my " +
|
|
286
|
-
"OAuth middleware spec' or 'Anyone know how we handle X?'.",
|
|
287
|
-
},
|
|
288
|
-
body: {
|
|
289
|
-
type: "string",
|
|
290
|
-
description: "Body of the post in markdown. A short explanation of why the " +
|
|
291
|
-
"user is posting (the artifact carries the detail).",
|
|
292
|
-
},
|
|
293
|
-
intent: {
|
|
294
|
-
type: "string",
|
|
295
|
-
enum: [
|
|
296
|
-
"share",
|
|
297
|
-
"request_review",
|
|
298
|
-
"request_expertise",
|
|
299
|
-
"link_share",
|
|
300
|
-
"rfc",
|
|
301
|
-
],
|
|
302
|
-
description: "Why the user is posting. See tool description.",
|
|
303
|
-
},
|
|
304
|
-
mentions: {
|
|
305
|
-
type: "array",
|
|
306
|
-
items: { type: "string" },
|
|
307
|
-
description: "Teammate first names (or handles) to @mention. Each mention " +
|
|
308
|
-
"creates a notification for that teammate.",
|
|
309
|
-
},
|
|
310
|
-
format: {
|
|
311
|
-
type: "string",
|
|
312
|
-
enum: ["article", "quick", "workflow", "review"],
|
|
313
|
-
description: "Post format. Default 'article' if omitted. Use 'quick' for " +
|
|
314
|
-
"short one-liners.",
|
|
315
|
-
},
|
|
316
|
-
tags: {
|
|
317
|
-
type: "array",
|
|
318
|
-
items: { type: "string" },
|
|
319
|
-
description: "Optional tags, lowercase, hyphen-safe.",
|
|
320
|
-
},
|
|
321
|
-
},
|
|
322
|
-
required: ["title", "body"],
|
|
323
|
-
},
|
|
324
|
-
},
|
|
325
263
|
{
|
|
326
264
|
name: "codiedev_share_with",
|
|
327
265
|
description: "Grant a teammate persistent read (or edit) access to an artifact " +
|
|
@@ -374,7 +312,7 @@ const TOOLS = [
|
|
|
374
312
|
"to that post with X', 'mark as used', or 'upvote'. The 🛠 " +
|
|
375
313
|
"reaction is the signal for 'I used this' and bumps the linked " +
|
|
376
314
|
"artifact's reuse count. Pass the post id (get it from " +
|
|
377
|
-
"codiedev_get_library or a previous
|
|
315
|
+
"codiedev_get_library or a previous feed response).",
|
|
378
316
|
inputSchema: {
|
|
379
317
|
type: "object",
|
|
380
318
|
properties: {
|
|
@@ -602,8 +540,6 @@ async function dispatchTool(name, args, config) {
|
|
|
602
540
|
return await handleNote(args, config);
|
|
603
541
|
case "codiedev_promote":
|
|
604
542
|
return await handlePromote(args, config);
|
|
605
|
-
case "codiedev_post_to_feed":
|
|
606
|
-
return await handlePostToFeed(args, config);
|
|
607
543
|
case "codiedev_share_with":
|
|
608
544
|
return await handleShareWith(args, config);
|
|
609
545
|
case "codiedev_send_to":
|
|
@@ -784,34 +720,6 @@ async function handlePromote(args, config) {
|
|
|
784
720
|
],
|
|
785
721
|
};
|
|
786
722
|
}
|
|
787
|
-
async function handlePostToFeed(args, config) {
|
|
788
|
-
const filename = asStringOrUndefined(args.filename);
|
|
789
|
-
const title = asString(args.title);
|
|
790
|
-
const body = asString(args.body);
|
|
791
|
-
const intent = asStringOrUndefined(args.intent);
|
|
792
|
-
const format = asStringOrUndefined(args.format);
|
|
793
|
-
const mentionsRaw = Array.isArray(args.mentions) ? args.mentions : [];
|
|
794
|
-
const mentions = mentionsRaw.filter((m) => typeof m === "string");
|
|
795
|
-
const tagsRaw = Array.isArray(args.tags) ? args.tags : [];
|
|
796
|
-
const tags = tagsRaw.filter((t) => typeof t === "string");
|
|
797
|
-
if (!title)
|
|
798
|
-
throw new Error("title required");
|
|
799
|
-
if (!body)
|
|
800
|
-
throw new Error("body required");
|
|
801
|
-
const res = await (0, shared_1.apiRequest)("POST", "/api/cli/post-to-feed", {
|
|
802
|
-
config,
|
|
803
|
-
body: { filename, title, body, intent, format, mentions, tags },
|
|
804
|
-
});
|
|
805
|
-
const pieces = [`✓ Posted to feed (id=${res.postId}).`];
|
|
806
|
-
if (res.linkedArtifact?.key)
|
|
807
|
-
pieces.push(` attached: ${res.linkedArtifact.key}`);
|
|
808
|
-
if (res.intent)
|
|
809
|
-
pieces.push(` intent: ${res.intent}`);
|
|
810
|
-
if (res.mentions?.length) {
|
|
811
|
-
pieces.push(` mentioned: ${res.mentions.map((m) => m.name).join(", ")}`);
|
|
812
|
-
}
|
|
813
|
-
return { content: [{ type: "text", text: pieces.join("\n") }] };
|
|
814
|
-
}
|
|
815
723
|
async function handleShareWith(args, config) {
|
|
816
724
|
const filename = asString(args.filename);
|
|
817
725
|
const to = asString(args.to);
|
package/dist/repoResolver.js
CHANGED
|
@@ -13,14 +13,20 @@ function parseRemoteUrl(url) {
|
|
|
13
13
|
let s = url.trim();
|
|
14
14
|
if (s.endsWith(".git"))
|
|
15
15
|
s = s.slice(0, -4);
|
|
16
|
-
const ssh = s.match(/^git@([^:]+):(.+)$/);
|
|
17
|
-
if (ssh) {
|
|
18
|
-
return { host: ssh[1].toLowerCase(), ownerName: ssh[2].toLowerCase() };
|
|
19
|
-
}
|
|
20
16
|
const https = s.match(/^https?:\/\/([^/]+)\/(.+)$/);
|
|
21
17
|
if (https) {
|
|
22
18
|
return { host: https[1].toLowerCase(), ownerName: https[2].toLowerCase() };
|
|
23
19
|
}
|
|
20
|
+
// Codie strips SSH userinfo before putting a remote in the gateway header,
|
|
21
|
+
// so `git@gitlab.com:owner/repo.git` arrives here as
|
|
22
|
+
// `gitlab.com:owner/repo.git`. Accept both forms; otherwise every SSH-cloned
|
|
23
|
+
// Harness checkout loses its repoId even though transcript capture can
|
|
24
|
+
// resolve the same raw remote correctly. Check this after URL forms so the
|
|
25
|
+
// colon in `https://` is never mistaken for an SCP separator.
|
|
26
|
+
const scp = s.match(/^(?:[^@\s]+@)?([^:/\s]+):([^/]+\/.+)$/);
|
|
27
|
+
if (scp) {
|
|
28
|
+
return { host: scp[1].toLowerCase(), ownerName: scp[2].toLowerCase() };
|
|
29
|
+
}
|
|
24
30
|
return null;
|
|
25
31
|
}
|
|
26
32
|
function resolveRepoForCapture(input) {
|