codiedev 0.9.52 → 0.9.54
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 +8 -1
- package/dist/cli.js +6 -11
- package/dist/connect.js +1 -1
- package/dist/cursorHooks.d.ts +9 -0
- package/dist/cursorHooks.js +56 -0
- package/dist/hook.js +55 -16
- package/dist/mcp.js +1 -93
- package/dist/repoResolver.js +10 -4
- package/dist/utils.d.ts +5 -1
- package/dist/utils.js +144 -64
- package/package.json +4 -2
- package/dist/commands/post.d.ts +0 -2
- package/dist/commands/post.js +0 -96
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ The CLI will prompt you for an API token (get one at https://codiedev.com/portal
|
|
|
12
12
|
|
|
13
13
|
- **Claude Code** — `SessionEnd` hook in `~/.claude/settings.json`
|
|
14
14
|
- **Codex** — `Stop` hook in `~/.codex/hooks.json`
|
|
15
|
-
- **Cursor** —
|
|
15
|
+
- **Cursor** — native session, backup, and capture hooks in `~/.cursor/hooks.json`
|
|
16
16
|
- **OpenCode / Codie** — plugin in `~/.config/opencode/plugins/codiedev.js`
|
|
17
17
|
|
|
18
18
|
Installed clients are wired automatically. The OpenCode adapter is staged even
|
|
@@ -49,6 +49,13 @@ container needs its own rule unless `~/.codiedev/config.json` is persisted.
|
|
|
49
49
|
|
|
50
50
|
Codex doesn't currently expose a `SessionEnd` event, so the hook runs on `Stop` (fires after each assistant turn). The backend dedupes by `(company, session_id)` and replaces the stored transcript on re-upload so the latest turn's snapshot wins. Summarization happens once per session; subsequent turn uploads keep the raw transcript fresh but don't re-summarize (to avoid running up LLM cost).
|
|
51
51
|
|
|
52
|
+
## Cursor note
|
|
53
|
+
|
|
54
|
+
Cursor uses its native hook events for session start/end and stop-time backup.
|
|
55
|
+
Cursor's `beforeSubmitPrompt` currently cannot inject dynamic context, so task-
|
|
56
|
+
specific discovery remains unavailable there. Cursor sessions keep their source
|
|
57
|
+
label and real transcript statistics through the shared capture pipeline.
|
|
58
|
+
|
|
52
59
|
## OpenCode note
|
|
53
60
|
|
|
54
61
|
OpenCode exposes session lifecycle events through plugins. Codiedev snapshots
|
package/dist/cli.js
CHANGED
|
@@ -29,7 +29,6 @@ const createTicket_1 = require("./commands/createTicket");
|
|
|
29
29
|
const doctor_1 = require("./commands/doctor");
|
|
30
30
|
const search_1 = require("./commands/search");
|
|
31
31
|
const library_1 = require("./commands/library");
|
|
32
|
-
const post_1 = require("./commands/post");
|
|
33
32
|
const share_1 = require("./commands/share");
|
|
34
33
|
const send_1 = require("./commands/send");
|
|
35
34
|
const react_1 = require("./commands/react");
|
|
@@ -43,7 +42,7 @@ Codiedev CLI
|
|
|
43
42
|
someone left off across sessions and providers.
|
|
44
43
|
|
|
45
44
|
Connect:
|
|
46
|
-
codiedev connect Link Claude Code / Codex to your org
|
|
45
|
+
codiedev connect Link Claude Code / Codex / Cursor to your org
|
|
47
46
|
codiedev doctor Verify your setup after connecting
|
|
48
47
|
|
|
49
48
|
Harness:
|
|
@@ -98,9 +97,6 @@ Discovery:
|
|
|
98
97
|
List artifacts in your workspace
|
|
99
98
|
|
|
100
99
|
Feed:
|
|
101
|
-
codiedev post --title "<t>" --body "<b>" [--intent share|request_review|
|
|
102
|
-
request_expertise|link_share|rfc] [--filename <key>]
|
|
103
|
-
[--mention <name>]... Publish to the team feed
|
|
104
100
|
codiedev react <postId> <emoji> Toggle a reaction on a feed post
|
|
105
101
|
|
|
106
102
|
Privacy:
|
|
@@ -128,7 +124,7 @@ Other artifact types (rare — use the matching prefix):
|
|
|
128
124
|
|
|
129
125
|
Skills (slash commands):
|
|
130
126
|
Add 'name:' + 'description:' YAML frontmatter to any pushed file
|
|
131
|
-
and it shows up as /<name> in Claude Code / Codex. Filename
|
|
127
|
+
and it shows up as /<name> in Claude Code / Codex / Cursor. Filename
|
|
132
128
|
prefix doesn't matter — frontmatter is the signal.
|
|
133
129
|
|
|
134
130
|
Examples:
|
|
@@ -165,6 +161,8 @@ codiedev connect — link your agent CLI to Codiedev
|
|
|
165
161
|
- ~/.claude/CLAUDE.md instructions (teaches Claude the commands)
|
|
166
162
|
- ~/.codex/hooks.json hook (same, for Codex)
|
|
167
163
|
- ~/.codex/AGENTS.md instructions (same, for Codex)
|
|
164
|
+
- ~/.cursor/hooks.json hooks (session, prompt, security, capture)
|
|
165
|
+
- ~/.cursor/rules/codiedev.mdc (teaches Cursor the commands)
|
|
168
166
|
|
|
169
167
|
Re-run anytime to swap accounts or refresh the instruction block.
|
|
170
168
|
|
|
@@ -367,7 +365,7 @@ Default path:
|
|
|
367
365
|
# body...
|
|
368
366
|
3. codiedev push portal-design.md
|
|
369
367
|
|
|
370
|
-
It's now /portal-design in Claude Code / Codex.
|
|
368
|
+
It's now /portal-design in Claude Code / Codex / Cursor.
|
|
371
369
|
|
|
372
370
|
Filename prefix is irrelevant for skills — frontmatter is the only
|
|
373
371
|
signal. The stored artifact type still follows the filename rule
|
|
@@ -385,7 +383,7 @@ email notifications when pinged.
|
|
|
385
383
|
|
|
386
384
|
## Agent-native
|
|
387
385
|
|
|
388
|
-
If you use Claude Code or
|
|
386
|
+
If you use Claude Code, Codex, or Cursor, the agent already knows the commands —
|
|
389
387
|
'codiedev connect' installs instructions into your user-level config.
|
|
390
388
|
Just say things like:
|
|
391
389
|
|
|
@@ -503,9 +501,6 @@ async function main() {
|
|
|
503
501
|
case "ls":
|
|
504
502
|
await (0, library_1.runLibrary)(rest);
|
|
505
503
|
return;
|
|
506
|
-
case "post":
|
|
507
|
-
await (0, post_1.runPost)(rest);
|
|
508
|
-
return;
|
|
509
504
|
case "share":
|
|
510
505
|
await (0, share_1.runShare)(rest);
|
|
511
506
|
return;
|
package/dist/connect.js
CHANGED
|
@@ -310,7 +310,7 @@ async function runConnect(args = []) {
|
|
|
310
310
|
runInstall("codex-instructions", utils_1.installCodexInstructions, "Codex agent instructions (~/.codex/AGENTS.md)", "codex-instructions");
|
|
311
311
|
}
|
|
312
312
|
if (hasCursor) {
|
|
313
|
-
runInstall("cursor-hook", utils_1.installCursorHook, "Cursor
|
|
313
|
+
runInstall("cursor-hook", utils_1.installCursorHook, "Cursor sessionStart + stop backup + sessionEnd capture hooks (~/.cursor/hooks.json)", "cursor-hook");
|
|
314
314
|
runInstall("cursor-instructions", utils_1.installCursorInstructions, "Cursor agent instructions (~/.cursor/rules/codiedev.mdc)", "cursor-instructions");
|
|
315
315
|
}
|
|
316
316
|
// Always stage the adapter. It is dormant until OpenCode starts, which lets
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Normalize Cursor's native hook payload into the shared capture shape. */
|
|
2
|
+
export declare function normalizeCursorHookInput<T extends Record<string, unknown>>(input: T): T & Record<string, unknown>;
|
|
3
|
+
/** Add Claude-compatible message discriminators while preserving Cursor JSONL. */
|
|
4
|
+
export declare function normalizeCursorTranscript(content: string): string;
|
|
5
|
+
/** Cursor may change its private JSONL record shape independently of the hook
|
|
6
|
+
* contract. Only a byte-empty snapshot is safe to discard; a non-empty file
|
|
7
|
+
* that the current normalizer cannot parse must still be uploaded for backend
|
|
8
|
+
* recovery and diagnosis. */
|
|
9
|
+
export declare function isCursorTranscriptEmpty(content: string): boolean;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeCursorHookInput = normalizeCursorHookInput;
|
|
4
|
+
exports.normalizeCursorTranscript = normalizeCursorTranscript;
|
|
5
|
+
exports.isCursorTranscriptEmpty = isCursorTranscriptEmpty;
|
|
6
|
+
/** Normalize Cursor's native hook payload into the shared capture shape. */
|
|
7
|
+
function normalizeCursorHookInput(input) {
|
|
8
|
+
const sessionId = firstString(input.session_id, input.sessionId, input.conversation_id);
|
|
9
|
+
const roots = Array.isArray(input.workspace_roots)
|
|
10
|
+
? input.workspace_roots.filter((root) => typeof root === "string")
|
|
11
|
+
: [];
|
|
12
|
+
const cwd = firstString(input.cwd, roots[0]);
|
|
13
|
+
const duration = firstNumber(input.duration_ms, input.durationMs, input.duration);
|
|
14
|
+
return {
|
|
15
|
+
...input,
|
|
16
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
17
|
+
...(cwd ? { cwd } : {}),
|
|
18
|
+
...(input.tool_response === undefined && input.toolResponse === undefined && input.tool_output !== undefined
|
|
19
|
+
? { tool_response: input.tool_output }
|
|
20
|
+
: {}),
|
|
21
|
+
...(input.error === undefined && input.error_message !== undefined
|
|
22
|
+
? { error: input.error_message }
|
|
23
|
+
: {}),
|
|
24
|
+
...(duration !== undefined ? { duration_ms: duration } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Add Claude-compatible message discriminators while preserving Cursor JSONL. */
|
|
28
|
+
function normalizeCursorTranscript(content) {
|
|
29
|
+
return content.split("\n").map((line) => {
|
|
30
|
+
if (!line.trim())
|
|
31
|
+
return line;
|
|
32
|
+
try {
|
|
33
|
+
const entry = JSON.parse(line);
|
|
34
|
+
if ((entry.role === "user" || entry.role === "assistant") && entry.type === undefined) {
|
|
35
|
+
return JSON.stringify({ ...entry, type: entry.role });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Tolerant downstream parsers skip malformed/non-JSON lines.
|
|
40
|
+
}
|
|
41
|
+
return line;
|
|
42
|
+
}).join("\n");
|
|
43
|
+
}
|
|
44
|
+
/** Cursor may change its private JSONL record shape independently of the hook
|
|
45
|
+
* contract. Only a byte-empty snapshot is safe to discard; a non-empty file
|
|
46
|
+
* that the current normalizer cannot parse must still be uploaded for backend
|
|
47
|
+
* recovery and diagnosis. */
|
|
48
|
+
function isCursorTranscriptEmpty(content) {
|
|
49
|
+
return content.trim().length === 0;
|
|
50
|
+
}
|
|
51
|
+
function firstString(...values) {
|
|
52
|
+
return values.find((value) => typeof value === "string" && value.trim().length > 0);
|
|
53
|
+
}
|
|
54
|
+
function firstNumber(...values) {
|
|
55
|
+
return values.find((value) => typeof value === "number" && Number.isFinite(value));
|
|
56
|
+
}
|
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");
|
|
@@ -46,6 +45,7 @@ const limitEventReporter_1 = require("./limitEventReporter");
|
|
|
46
45
|
const version_1 = require("./version");
|
|
47
46
|
const captureLog_1 = require("./captureLog");
|
|
48
47
|
const captureExclusions_1 = require("./captureExclusions");
|
|
48
|
+
const cursorHooks_1 = require("./cursorHooks");
|
|
49
49
|
// Pure builder for the discovery additionalContext. Returns null when there are
|
|
50
50
|
// no matches. The suggested `codiedev pull` command carries the EXACT top key
|
|
51
51
|
// (e.g. "pr-push.md") rather than a "<key>" placeholder — otherwise agents
|
|
@@ -324,7 +324,7 @@ async function readStdin() {
|
|
|
324
324
|
function resolveMode(arg) {
|
|
325
325
|
if (arg === "capture-codex")
|
|
326
326
|
return "codex";
|
|
327
|
-
if (arg === "capture-cursor")
|
|
327
|
+
if (arg === "capture-cursor" || arg === "backup-cursor")
|
|
328
328
|
return "cursor";
|
|
329
329
|
if (arg === "capture-opencode" || arg === "capture-opencode-final")
|
|
330
330
|
return "opencode";
|
|
@@ -432,7 +432,10 @@ async function main() {
|
|
|
432
432
|
try {
|
|
433
433
|
const raw = await readStdin();
|
|
434
434
|
if (raw.trim()) {
|
|
435
|
-
|
|
435
|
+
const parsed = JSON.parse(raw);
|
|
436
|
+
hookInput = (process.argv[2]?.endsWith("-cursor")
|
|
437
|
+
? (0, cursorHooks_1.normalizeCursorHookInput)(parsed)
|
|
438
|
+
: parsed);
|
|
436
439
|
}
|
|
437
440
|
}
|
|
438
441
|
catch {
|
|
@@ -446,7 +449,9 @@ async function main() {
|
|
|
446
449
|
process.exit(0);
|
|
447
450
|
}
|
|
448
451
|
// UserPromptSubmit discovery path — separate from session capture.
|
|
449
|
-
if (process.argv[2] === "discover"
|
|
452
|
+
if (process.argv[2] === "discover" ||
|
|
453
|
+
process.argv[2] === "discover-codex" ||
|
|
454
|
+
process.argv[2] === "discover-codie") {
|
|
450
455
|
await runDiscover(hookInput.prompt);
|
|
451
456
|
return;
|
|
452
457
|
}
|
|
@@ -460,8 +465,14 @@ async function main() {
|
|
|
460
465
|
return;
|
|
461
466
|
}
|
|
462
467
|
// SessionStart: post the capture-coverage denominator ping. Tiny + fast.
|
|
463
|
-
if (process.argv[2] === "session-start" ||
|
|
464
|
-
|
|
468
|
+
if (process.argv[2] === "session-start" ||
|
|
469
|
+
process.argv[2] === "session-start-cursor" ||
|
|
470
|
+
process.argv[2] === "session-start-opencode") {
|
|
471
|
+
await runSessionStart(hookInput, process.argv[2] === "session-start-opencode"
|
|
472
|
+
? "opencode"
|
|
473
|
+
: process.argv[2] === "session-start-cursor"
|
|
474
|
+
? "cursor"
|
|
475
|
+
: "claude-code");
|
|
465
476
|
return;
|
|
466
477
|
}
|
|
467
478
|
// Self-heal: a user who upgraded the CLI but hasn't re-run `connect` gets
|
|
@@ -469,7 +480,12 @@ async function main() {
|
|
|
469
480
|
// no self-heal and could stay stripped forever while SessionStart kept
|
|
470
481
|
// pinging (the Sonifi "starts arrive, captures never" loss) — re-wired on
|
|
471
482
|
// their next Stop/SessionEnd. One read + one write, only when missing.
|
|
472
|
-
(
|
|
483
|
+
if ((process.argv[2] ?? "").endsWith("-cursor")) {
|
|
484
|
+
(0, utils_1.ensureCursorCodiedevHooksInstalled)();
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
(0, utils_1.ensureAllCodiedevHooksInstalled)();
|
|
488
|
+
}
|
|
473
489
|
if (process.argv[2] === "capture-codex")
|
|
474
490
|
(0, utils_1.ensureCodexLimitHookInstalled)();
|
|
475
491
|
const { session_id, transcript_path, cwd, workspace_roots } = hookInput;
|
|
@@ -484,13 +500,13 @@ async function main() {
|
|
|
484
500
|
// extension) where SessionEnd may never come — the silent zero-capture bug.
|
|
485
501
|
// Same detached capture-finish child as SessionEnd, marked non-final so the
|
|
486
502
|
// server renews the session lease instead of summarizing immediately.
|
|
487
|
-
if (process.argv[2] === "backup") {
|
|
503
|
+
if (process.argv[2] === "backup" || process.argv[2] === "backup-cursor") {
|
|
488
504
|
if (transcript_path) {
|
|
489
505
|
try {
|
|
490
506
|
(0, utils_1.dispatchCaptureFinish)({
|
|
491
507
|
// "capture" = the claude-code entrypoint literal, same as the
|
|
492
508
|
// SessionEnd path below (resolveMode maps it to claude-code).
|
|
493
|
-
mode: "capture",
|
|
509
|
+
mode: process.argv[2] === "backup-cursor" ? "capture-cursor" : "capture",
|
|
494
510
|
session_id,
|
|
495
511
|
transcript_path,
|
|
496
512
|
cwd,
|
|
@@ -552,8 +568,8 @@ async function main() {
|
|
|
552
568
|
workspace_roots,
|
|
553
569
|
// final:true ONLY for claude-code's SessionEnd. Codex routes its
|
|
554
570
|
// per-turn Stop hook through "capture-codex" — final:true there would
|
|
555
|
-
// close the server lease every turn.
|
|
556
|
-
//
|
|
571
|
+
// close the server lease every turn. Cursor SessionEnd is final:true;
|
|
572
|
+
// Codex remains the legacy final-omitted per-turn path.
|
|
557
573
|
final: (0, utils_1.resolveCaptureFinal)(process.argv[2]),
|
|
558
574
|
}, process.argv[1], { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile: jobFile, openSync: fs.openSync, closeSync: fs.closeSync });
|
|
559
575
|
}
|
|
@@ -630,7 +646,9 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
630
646
|
outcome: "skip",
|
|
631
647
|
detail: reason,
|
|
632
648
|
});
|
|
633
|
-
|
|
649
|
+
if ((0, utils_1.shouldReportCaptureSkip)(input.final)) {
|
|
650
|
+
await postSessionSkip(config, session_id, reason);
|
|
651
|
+
}
|
|
634
652
|
return;
|
|
635
653
|
}
|
|
636
654
|
// Snapshot the RAW transcript before normalization below flattens it to
|
|
@@ -642,6 +660,9 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
642
660
|
if (parserMode === "opencode") {
|
|
643
661
|
transcriptContent = (0, utils_1.normalizeOpenCodeTranscript)(transcriptContent);
|
|
644
662
|
}
|
|
663
|
+
else if (parserMode === "cursor") {
|
|
664
|
+
transcriptContent = (0, cursorHooks_1.normalizeCursorTranscript)(transcriptContent);
|
|
665
|
+
}
|
|
645
666
|
// Resolve repo URL. Codex embeds it in session_meta; CC and Cursor both
|
|
646
667
|
// need the git remote of the workspace.
|
|
647
668
|
let remoteUrl = null;
|
|
@@ -668,9 +689,23 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
668
689
|
const matchedRepo = remoteUrl ? (0, utils_1.matchRepo)(remoteUrl, config.repos) : null;
|
|
669
690
|
const stats = parserMode === "codex"
|
|
670
691
|
? (0, utils_1.parseCodexStats)(transcriptContent)
|
|
671
|
-
:
|
|
672
|
-
|
|
673
|
-
|
|
692
|
+
: (0, utils_1.parseClaudeCodeStats)(transcriptContent);
|
|
693
|
+
// Never use parsed message count here: Cursor's private JSONL shape can
|
|
694
|
+
// evolve independently, and parser drift must not discard a real session.
|
|
695
|
+
if (parserMode === "cursor" && (0, cursorHooks_1.isCursorTranscriptEmpty)(rawTranscriptContent)) {
|
|
696
|
+
(0, captureLog_1.appendCaptureLog)({
|
|
697
|
+
stage: "capture-finish",
|
|
698
|
+
sessionId: session_id,
|
|
699
|
+
outcome: "skip",
|
|
700
|
+
detail: "empty-cursor-transcript",
|
|
701
|
+
});
|
|
702
|
+
// A non-final stop snapshot is only a backup; reporting it as a skipped
|
|
703
|
+
// session would close server-side coverage while the session is alive.
|
|
704
|
+
if ((0, utils_1.shouldReportCaptureSkip)(input.final)) {
|
|
705
|
+
await postSessionSkip(config, session_id, "empty-cursor-transcript");
|
|
706
|
+
}
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
674
709
|
const transcriptBase64 = Buffer.from(transcriptContent, "utf8").toString("base64");
|
|
675
710
|
// Session→merge join keys: the branch + HEAD sha the work happened on. Matched
|
|
676
711
|
// server-side against a merged MR/PR's source_branch / commits for AI attribution.
|
|
@@ -763,7 +798,7 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
763
798
|
// which is the "supersede" mechanism for the deferred path.
|
|
764
799
|
try {
|
|
765
800
|
const hookScriptPath = process.argv[1];
|
|
766
|
-
const tmpFile =
|
|
801
|
+
const tmpFile = (0, utils_1.captureUploadTmpFile)(session_id, decision.mode);
|
|
767
802
|
if (decision.mode === "immediate") {
|
|
768
803
|
(0, utils_1.dispatchCaptureDetached)({
|
|
769
804
|
url: `${config.backendUrl}/api/captureSession`,
|
|
@@ -771,6 +806,10 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
771
806
|
body: payload,
|
|
772
807
|
scopePaths,
|
|
773
808
|
}, hookScriptPath, { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile, openSync: fs.openSync, closeSync: fs.closeSync });
|
|
809
|
+
// Persist the immediate payload before removing the older scheduled
|
|
810
|
+
// snapshot. Every immediate dispatch supersedes a sleeping deferred job.
|
|
811
|
+
if (session_id)
|
|
812
|
+
(0, utils_1.cancelDeferredCaptureUpload)(session_id);
|
|
774
813
|
// Retry any earlier capture jobs that failed to upload (transient network
|
|
775
814
|
// blips), excluding the one we just dispatched. Best-effort, detached.
|
|
776
815
|
// Skip the sweep on deferred path — the deferred child will handle uploading.
|
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) {
|
package/dist/utils.d.ts
CHANGED
|
@@ -103,7 +103,7 @@ export declare function installOpenCodeCapturePlugin(): void;
|
|
|
103
103
|
* the presence of any `github.copilot*` directory as the Copilot fingerprint.
|
|
104
104
|
*/
|
|
105
105
|
export declare function vscodeCopilotInstalled(): boolean;
|
|
106
|
-
export declare function upsertCodiedevHook(hooks: Record<string, unknown>, eventName: string, subcommand: string, matcher?: string): void;
|
|
106
|
+
export declare function upsertCodiedevHook(hooks: Record<string, unknown>, eventName: string, subcommand: string, matcher?: string | null): void;
|
|
107
107
|
export declare function installHook(): void;
|
|
108
108
|
export declare function ensureDiscoveryHookInstalled(): void;
|
|
109
109
|
export declare function ensureBackupHookInstalled(): void;
|
|
@@ -169,6 +169,7 @@ export declare function installCodexMcp(): boolean;
|
|
|
169
169
|
export declare function installCodexHook(): void;
|
|
170
170
|
export declare function ensureCodexLimitHookInstalled(): void;
|
|
171
171
|
export declare function installCursorHook(): void;
|
|
172
|
+
export declare function ensureCursorCodiedevHooksInstalled(): void;
|
|
172
173
|
export interface ParsedStats {
|
|
173
174
|
messageCount: number;
|
|
174
175
|
toolCallCount: number;
|
|
@@ -256,6 +257,8 @@ export declare const CAPTURE_THROTTLE_MS: number;
|
|
|
256
257
|
export declare const SWEEP_MIN_AGE_MS: number;
|
|
257
258
|
export declare function resolveCaptureFinal(argv2: string | undefined): boolean | undefined;
|
|
258
259
|
export declare function finishInputTmpFile(sessionId: string | undefined): string;
|
|
260
|
+
export declare function captureUploadTmpFile(sessionId: string | undefined, mode: "immediate" | "deferred", pid?: number, now?: number): string;
|
|
261
|
+
export declare function cancelDeferredCaptureUpload(sessionId: string, deps?: Pick<typeof fs, "unlinkSync">): boolean;
|
|
259
262
|
export declare function decideDispatch(args: {
|
|
260
263
|
final: boolean;
|
|
261
264
|
lastUploadAt: number | null;
|
|
@@ -330,3 +333,4 @@ interface DeferredUploadDeps extends UploadDeps {
|
|
|
330
333
|
}
|
|
331
334
|
export declare function runDeferredUpload(jobFile: string, delayMs: number, deps: DeferredUploadDeps): Promise<void>;
|
|
332
335
|
export declare function transcriptReadSkipReason(err: unknown): string;
|
|
336
|
+
export declare function shouldReportCaptureSkip(final: boolean | undefined): boolean;
|
package/dist/utils.js
CHANGED
|
@@ -84,6 +84,7 @@ exports.installCodexMcp = installCodexMcp;
|
|
|
84
84
|
exports.installCodexHook = installCodexHook;
|
|
85
85
|
exports.ensureCodexLimitHookInstalled = ensureCodexLimitHookInstalled;
|
|
86
86
|
exports.installCursorHook = installCursorHook;
|
|
87
|
+
exports.ensureCursorCodiedevHooksInstalled = ensureCursorCodiedevHooksInstalled;
|
|
87
88
|
exports.parseClaudeSteering = parseClaudeSteering;
|
|
88
89
|
exports.parseCodexSteering = parseCodexSteering;
|
|
89
90
|
exports.parseSteering = parseSteering;
|
|
@@ -99,6 +100,8 @@ exports.dispatchDeferredUpload = dispatchDeferredUpload;
|
|
|
99
100
|
exports.sweepPendingCaptures = sweepPendingCaptures;
|
|
100
101
|
exports.resolveCaptureFinal = resolveCaptureFinal;
|
|
101
102
|
exports.finishInputTmpFile = finishInputTmpFile;
|
|
103
|
+
exports.captureUploadTmpFile = captureUploadTmpFile;
|
|
104
|
+
exports.cancelDeferredCaptureUpload = cancelDeferredCaptureUpload;
|
|
102
105
|
exports.decideDispatch = decideDispatch;
|
|
103
106
|
exports.readSessionUploadState = readSessionUploadState;
|
|
104
107
|
exports.writeSessionUploadState = writeSessionUploadState;
|
|
@@ -109,6 +112,7 @@ exports.readLastCapture = readLastCapture;
|
|
|
109
112
|
exports.runCaptureUpload = runCaptureUpload;
|
|
110
113
|
exports.runDeferredUpload = runDeferredUpload;
|
|
111
114
|
exports.transcriptReadSkipReason = transcriptReadSkipReason;
|
|
115
|
+
exports.shouldReportCaptureSkip = shouldReportCaptureSkip;
|
|
112
116
|
const fs = __importStar(require("fs"));
|
|
113
117
|
const path = __importStar(require("path"));
|
|
114
118
|
const os = __importStar(require("os"));
|
|
@@ -852,7 +856,7 @@ function buildCapturePayload(input) {
|
|
|
852
856
|
sessionStartedAt: input.sessionStartedAt ?? undefined,
|
|
853
857
|
interruptCount: input.interruptCount,
|
|
854
858
|
rejectionCount: input.rejectionCount,
|
|
855
|
-
// final is only emitted when explicitly set (not for Codex
|
|
859
|
+
// final is only emitted when explicitly set (not for Codex).
|
|
856
860
|
final: input.final,
|
|
857
861
|
costUsd: input.costUsd,
|
|
858
862
|
};
|
|
@@ -1056,6 +1060,7 @@ function installOpenCodeCapturePlugin() {
|
|
|
1056
1060
|
const start = resolveHookInvocation("session-start-opencode");
|
|
1057
1061
|
const capture = resolveHookInvocation("capture-opencode");
|
|
1058
1062
|
const finish = resolveHookInvocation("capture-opencode-final");
|
|
1063
|
+
const prompt = resolveHookInvocation("discover-codie");
|
|
1059
1064
|
const source = `// Managed by codiedev connect. Safe to remove and reinstall.\n` +
|
|
1060
1065
|
`import { spawn, spawnSync } from "node:child_process";\n` +
|
|
1061
1066
|
`import { mkdir, writeFile } from "node:fs/promises";\n` +
|
|
@@ -1064,6 +1069,7 @@ function installOpenCodeCapturePlugin() {
|
|
|
1064
1069
|
`const START = ${JSON.stringify(start)};\n` +
|
|
1065
1070
|
`const CAPTURE = ${JSON.stringify(capture)};\n` +
|
|
1066
1071
|
`const FINISH = ${JSON.stringify(finish)};\n` +
|
|
1072
|
+
`const PROMPT = ${JSON.stringify(prompt)};\n` +
|
|
1067
1073
|
`const fingerprints = new Map();\n\n` +
|
|
1068
1074
|
`const transcriptPaths = new Map();\n\n` +
|
|
1069
1075
|
`function sessionId(event) {\n` +
|
|
@@ -1079,6 +1085,13 @@ function installOpenCodeCapturePlugin() {
|
|
|
1079
1085
|
`function dispatchSync(invocation, payload) {\n` +
|
|
1080
1086
|
` try { spawnSync(invocation.command, invocation.args, { input: JSON.stringify(payload), stdio: ["pipe", "ignore", "ignore"], timeout: 1500 }); } catch {}\n` +
|
|
1081
1087
|
`}\n\n` +
|
|
1088
|
+
`function invokeSync(invocation, payload) {\n` +
|
|
1089
|
+
` try {\n` +
|
|
1090
|
+
` const result = spawnSync(invocation.command, invocation.args, { input: JSON.stringify(payload), encoding: "utf8", stdio: ["pipe", "pipe", "ignore"], timeout: 1500 });\n` +
|
|
1091
|
+
` if (result.error) return null;\n` +
|
|
1092
|
+
` return JSON.parse(result.stdout || "{}");\n` +
|
|
1093
|
+
` } catch { return null; }\n` +
|
|
1094
|
+
`}\n\n` +
|
|
1082
1095
|
`export const CodiedevCapture = async ({ client, directory, worktree }) => {\n` +
|
|
1083
1096
|
` const finalizeAll = () => {\n` +
|
|
1084
1097
|
` for (const [id, transcriptPath] of transcriptPaths) {\n` +
|
|
@@ -1088,6 +1101,14 @@ function installOpenCodeCapturePlugin() {
|
|
|
1088
1101
|
` };\n` +
|
|
1089
1102
|
` process.once("exit", finalizeAll);\n` +
|
|
1090
1103
|
` return ({\n` +
|
|
1104
|
+
` "chat.message": async (input, output) => {\n` +
|
|
1105
|
+
` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
|
|
1106
|
+
` const cwd = worktree || directory || process.cwd();\n` +
|
|
1107
|
+
` const prompt = Array.isArray(output?.parts) ? output.parts.filter((part) => part?.type === "text").map((part) => part.text || "").join("\\n") : "";\n` +
|
|
1108
|
+
` const decision = invokeSync(PROMPT, { session_id: id, cwd, prompt });\n` +
|
|
1109
|
+
` const context = decision?.hookSpecificOutput?.additionalContext;\n` +
|
|
1110
|
+
` if (context && Array.isArray(output?.parts)) output.parts.push({ type: "text", text: context });\n` +
|
|
1111
|
+
` },\n` +
|
|
1091
1112
|
` event: async ({ event }) => {\n` +
|
|
1092
1113
|
` const id = sessionId(event);\n` +
|
|
1093
1114
|
` if (!id) return;\n` +
|
|
@@ -1144,7 +1165,7 @@ function vscodeCopilotInstalled() {
|
|
|
1144
1165
|
return false;
|
|
1145
1166
|
}
|
|
1146
1167
|
}
|
|
1147
|
-
// Idempotently put a single codiedev hook (matcher ".*", running our hook.js
|
|
1168
|
+
// Idempotently put a single codiedev hook (matcher ".*" by default, running our hook.js
|
|
1148
1169
|
// with `subcommand`) into the given hook event, stripping any prior codiedev
|
|
1149
1170
|
// entries first so connect re-runs / upgrades refresh the resolved binary path.
|
|
1150
1171
|
// Mutates `hooks` in place. Exported for unit testing.
|
|
@@ -1176,7 +1197,7 @@ function upsertCodiedevHook(hooks, eventName, subcommand, matcher = ".*") {
|
|
|
1176
1197
|
return !(0, detection_1.isCodiedevHookCommand)(hook.matcher);
|
|
1177
1198
|
});
|
|
1178
1199
|
filtered.push({
|
|
1179
|
-
matcher,
|
|
1200
|
+
...(matcher === null ? {} : { matcher }),
|
|
1180
1201
|
hooks: [{ type: "command", command: resolveHookCommand(subcommand, preservedNodePath) }],
|
|
1181
1202
|
});
|
|
1182
1203
|
hooks[eventName] = filtered;
|
|
@@ -1392,7 +1413,6 @@ thought, use the \`codiedev\` CLI via Bash:**
|
|
|
1392
1413
|
| "note that X is a follow-up" / "remember X" | \`codiedev note "<text>"\` |
|
|
1393
1414
|
| "promote the extracted spec" | \`codiedev promote <artifact-id>\` |
|
|
1394
1415
|
| "create a ticket" / "scope this into tickets" / "ticket this for me" | \`codiedev create-ticket [optional-inline-prompt]\` |
|
|
1395
|
-
| "post this to the team" / "publish to the feed" / "share with the team" | \`codiedev post --title "<t>" --body "<b>" [--intent <intent>] [--filename <key>] [--mention <name>]\` |
|
|
1396
1416
|
| "share with a teammate" / "give someone access" (no notification) | \`codiedev share <key> <name> [--role read\\|edit]\` |
|
|
1397
1417
|
| "send this to a teammate" / "ask someone to look at this" | \`codiedev send <key> <name> ["<msg>"]\` |
|
|
1398
1418
|
| "find something about X" / "has anyone solved Y?" | \`codiedev search "<query>" [--limit N]\` |
|
|
@@ -1426,31 +1446,6 @@ read it back to the user so they can confirm.
|
|
|
1426
1446
|
**Teammate names:** first name usually works (\`codiedev ping <name> ...\`).
|
|
1427
1447
|
If ambiguous, the CLI returns candidates — retry with the full email.
|
|
1428
1448
|
|
|
1429
|
-
**Feed posts (broad team reach):** use \`codiedev post\` when the user
|
|
1430
|
-
wants visibility beyond one teammate — e.g., announcing a new skill,
|
|
1431
|
-
asking the team who knows about X, proposing a change. Always pass
|
|
1432
|
-
\`--intent\` to match the request: \`share\` (here's a skill),
|
|
1433
|
-
\`request_review\`, \`request_expertise\` (stuck, who knows this),
|
|
1434
|
-
\`link_share\` (fyi), or \`rfc\` (proposing a change). Pass \`--filename\`
|
|
1435
|
-
to link a specific artifact and \`--mention <name>\` (repeatable) to
|
|
1436
|
-
tag teammates.
|
|
1437
|
-
|
|
1438
|
-
**Default feed-post shape.** When the user explicitly says "push/share/post
|
|
1439
|
-
to the feed", default to this shape unless they ask for something else:
|
|
1440
|
-
- **Title** ≤10 words: *what + why now*. Imperative or declarative — never
|
|
1441
|
-
"Check this out".
|
|
1442
|
-
- **Body** — three short lines, grounded in the current session and the
|
|
1443
|
-
artifact:
|
|
1444
|
-
- **Why this exists** — 1–2 sentences from session context (the problem
|
|
1445
|
-
or insight that produced the artifact).
|
|
1446
|
-
- **What's in it** — 1 sentence pulled from the artifact's frontmatter
|
|
1447
|
-
\`description:\` (skills) or first paragraph (specs). Don't restate
|
|
1448
|
-
the whole file — the card preview carries the detail.
|
|
1449
|
-
- **Ask** — 1 sentence. *Use it / review it / ping me with thoughts.*
|
|
1450
|
-
- **Filename**: always attach so the card preview renders.
|
|
1451
|
-
This is the default. If the user wants a one-liner or a different
|
|
1452
|
-
structure, follow their lead.
|
|
1453
|
-
|
|
1454
1449
|
**Search before pushing.** For any "save this as …" request, run
|
|
1455
1450
|
\`codiedev search "<query>"\` first. If prior art exists, pull it and
|
|
1456
1451
|
iterate rather than duplicating.
|
|
@@ -1808,6 +1803,9 @@ function installCodexHook() {
|
|
|
1808
1803
|
],
|
|
1809
1804
|
});
|
|
1810
1805
|
hooks.Stop = filtered;
|
|
1806
|
+
// Codex models UserPromptSubmit as a non-matcher event (the trust-key model
|
|
1807
|
+
// intentionally excludes it from MATCHER_EVENTS), so omit the matcher key.
|
|
1808
|
+
upsertCodiedevHook(hooks, "UserPromptSubmit", "discover-codex", null);
|
|
1811
1809
|
if (!fs.existsSync(CODEX_DIR)) {
|
|
1812
1810
|
fs.mkdirSync(CODEX_DIR, { recursive: true });
|
|
1813
1811
|
}
|
|
@@ -1815,7 +1813,7 @@ function installCodexHook() {
|
|
|
1815
1813
|
(0, codexHookTrust_1.writeCodexHookTrust)(CODEX_CONFIG_PATH, CODEX_HOOKS_PATH, hooksFile);
|
|
1816
1814
|
}
|
|
1817
1815
|
// Existing Codex users already have capture-codex on Stop. After a CLI
|
|
1818
|
-
// upgrade, that hook repairs
|
|
1816
|
+
// upgrade, that hook repairs newer lifecycle hooks without requiring connect.
|
|
1819
1817
|
function ensureCodexLimitHookInstalled() {
|
|
1820
1818
|
try {
|
|
1821
1819
|
if (!fs.existsSync(CODEX_HOOKS_PATH))
|
|
@@ -1823,16 +1821,31 @@ function ensureCodexLimitHookInstalled() {
|
|
|
1823
1821
|
const hooksFile = JSON.parse(fs.readFileSync(CODEX_HOOKS_PATH, "utf8"));
|
|
1824
1822
|
const hooks = (hooksFile.hooks ?? {});
|
|
1825
1823
|
const stop = Array.isArray(hooks.Stop) ? hooks.Stop : [];
|
|
1826
|
-
const
|
|
1827
|
-
const
|
|
1828
|
-
|
|
1824
|
+
const hasCommand = (entries, subcommand) => entries.some((group) => {
|
|
1825
|
+
const record = group;
|
|
1826
|
+
const flat = record.command;
|
|
1827
|
+
if (typeof flat === "string" && flat.trim().endsWith(subcommand))
|
|
1828
|
+
return true;
|
|
1829
|
+
const inner = record.hooks;
|
|
1830
|
+
return Array.isArray(inner) && inner.some((hook) => typeof hook.command === "string" && hook.command.trim().endsWith(subcommand));
|
|
1829
1831
|
});
|
|
1830
|
-
|
|
1832
|
+
let changed = false;
|
|
1833
|
+
if (!hasCommand(stop, "limit-codex")) {
|
|
1834
|
+
stop.unshift({
|
|
1835
|
+
hooks: [{ type: "command", command: resolveHookCommand("limit-codex"), timeout: 10 }],
|
|
1836
|
+
});
|
|
1837
|
+
hooks.Stop = stop;
|
|
1838
|
+
changed = true;
|
|
1839
|
+
}
|
|
1840
|
+
const promptEntries = Array.isArray(hooks.UserPromptSubmit)
|
|
1841
|
+
? hooks.UserPromptSubmit
|
|
1842
|
+
: [];
|
|
1843
|
+
if (!hasCommand(promptEntries, "discover-codex")) {
|
|
1844
|
+
upsertCodiedevHook(hooks, "UserPromptSubmit", "discover-codex", null);
|
|
1845
|
+
changed = true;
|
|
1846
|
+
}
|
|
1847
|
+
if (!changed)
|
|
1831
1848
|
return;
|
|
1832
|
-
stop.unshift({
|
|
1833
|
-
hooks: [{ type: "command", command: resolveHookCommand("limit-codex"), timeout: 10 }],
|
|
1834
|
-
});
|
|
1835
|
-
hooks.Stop = stop;
|
|
1836
1849
|
hooksFile.hooks = hooks;
|
|
1837
1850
|
fs.writeFileSync(CODEX_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
|
|
1838
1851
|
// Trust keys include each hook's positional index. Adding the new hook at
|
|
@@ -1868,25 +1881,61 @@ function installCursorHook() {
|
|
|
1868
1881
|
hooksFile.hooks = {};
|
|
1869
1882
|
}
|
|
1870
1883
|
const hooks = hooksFile.hooks;
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1884
|
+
const replaceManaged = (eventName, subcommands) => {
|
|
1885
|
+
const existing = Array.isArray(hooks[eventName])
|
|
1886
|
+
? hooks[eventName]
|
|
1887
|
+
: [];
|
|
1888
|
+
const preserved = existing.filter((hook) => !(0, detection_1.isCodiedevHookCommand)(hook.command));
|
|
1889
|
+
const managed = subcommands.map((subcommand) => ({
|
|
1890
|
+
command: resolveHookCommand(subcommand),
|
|
1891
|
+
type: "command",
|
|
1892
|
+
timeout: 30,
|
|
1893
|
+
}));
|
|
1894
|
+
if (preserved.length + managed.length === 0)
|
|
1895
|
+
delete hooks[eventName];
|
|
1896
|
+
else
|
|
1897
|
+
hooks[eventName] = [...preserved, ...managed];
|
|
1898
|
+
};
|
|
1899
|
+
replaceManaged("sessionStart", ["session-start-cursor"]);
|
|
1900
|
+
replaceManaged("sessionEnd", ["capture-cursor"]);
|
|
1901
|
+
replaceManaged("stop", ["backup-cursor"]);
|
|
1902
|
+
// Cursor cannot inject dynamic context from beforeSubmitPrompt today.
|
|
1903
|
+
// Remove older Codiedev discovery entries so prompts are not uploaded for
|
|
1904
|
+
// a response the host will discard; preserve any third-party hooks.
|
|
1905
|
+
replaceManaged("beforeSubmitPrompt", []);
|
|
1888
1906
|
fs.writeFileSync(CURSOR_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
|
|
1889
1907
|
}
|
|
1908
|
+
function ensureCursorCodiedevHooksInstalled() {
|
|
1909
|
+
try {
|
|
1910
|
+
if (!fs.existsSync(CURSOR_HOOKS_PATH))
|
|
1911
|
+
return;
|
|
1912
|
+
const parsed = JSON.parse(fs.readFileSync(CURSOR_HOOKS_PATH, "utf8"));
|
|
1913
|
+
const hooks = parsed.hooks ?? {};
|
|
1914
|
+
const required = [
|
|
1915
|
+
["sessionStart", "session-start-cursor"],
|
|
1916
|
+
["sessionEnd", "capture-cursor"],
|
|
1917
|
+
["stop", "backup-cursor"],
|
|
1918
|
+
];
|
|
1919
|
+
const has = (eventName, subcommand) => {
|
|
1920
|
+
const entries = hooks[eventName];
|
|
1921
|
+
return Array.isArray(entries) && entries.some((entry) => {
|
|
1922
|
+
const command = entry.command;
|
|
1923
|
+
return typeof command === "string"
|
|
1924
|
+
&& (0, detection_1.isCodiedevHookCommand)(command)
|
|
1925
|
+
&& command.trim().endsWith(subcommand);
|
|
1926
|
+
});
|
|
1927
|
+
};
|
|
1928
|
+
const promptEntries = hooks.beforeSubmitPrompt;
|
|
1929
|
+
const hasStalePromptHook = Array.isArray(promptEntries) && promptEntries.some((entry) => (0, detection_1.isCodiedevHookCommand)(entry.command));
|
|
1930
|
+
if (!required.every(([eventName, subcommand]) => has(eventName, subcommand)) ||
|
|
1931
|
+
hasStalePromptHook) {
|
|
1932
|
+
installCursorHook();
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
catch {
|
|
1936
|
+
// Best-effort repair only.
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1890
1939
|
function parseClaudeSteering(content) {
|
|
1891
1940
|
let interrupts = 0;
|
|
1892
1941
|
let rejections = 0;
|
|
@@ -2219,7 +2268,7 @@ function parseCodexStats(content) {
|
|
|
2219
2268
|
if (payloadType === "message") {
|
|
2220
2269
|
messageCount++;
|
|
2221
2270
|
}
|
|
2222
|
-
else if (payloadType === "function_call") {
|
|
2271
|
+
else if (payloadType === "function_call" || payloadType === "custom_tool_call") {
|
|
2223
2272
|
toolCallCount++;
|
|
2224
2273
|
}
|
|
2225
2274
|
}
|
|
@@ -2501,18 +2550,21 @@ exports.SWEEP_MIN_AGE_MS = exports.CAPTURE_THROTTLE_MS + 2 * 60 * 1000;
|
|
|
2501
2550
|
// hook with no subcommand) are a true final signal. capture-opencode/
|
|
2502
2551
|
// capture-desktop (per-turn snapshot) and capture-opencode-final/
|
|
2503
2552
|
// capture-desktop-final (real session end) get an explicit false/true.
|
|
2504
|
-
//
|
|
2505
|
-
//
|
|
2506
|
-
//
|
|
2507
|
-
//
|
|
2508
|
-
//
|
|
2509
|
-
// for every
|
|
2553
|
+
// Cursor's capture-cursor is wired only to sessionEnd, so it is also a true
|
|
2554
|
+
// final signal. Everything else — capture-codex (Codex's Stop fires per-turn;
|
|
2555
|
+
// final:true would close the server-side session lease every turn) and any
|
|
2556
|
+
// FUTURE mode (e.g. a capture-windsurf added to resolveMode but not audited
|
|
2557
|
+
// here) — gets undefined, i.e. legacy server behaviour. Defaulting unknowns
|
|
2558
|
+
// to true would silently re-introduce the per-turn lease-close bug for every
|
|
2559
|
+
// new agent.
|
|
2510
2560
|
function resolveCaptureFinal(argv2) {
|
|
2511
2561
|
if (argv2 === "capture-opencode" || argv2 === "capture-desktop")
|
|
2512
2562
|
return false;
|
|
2513
2563
|
if (argv2 === "capture-opencode-final" || argv2 === "capture-desktop-final")
|
|
2514
2564
|
return true;
|
|
2515
|
-
return argv2 === "capture" || argv2 ===
|
|
2565
|
+
return argv2 === "capture" || argv2 === "capture-cursor" || argv2 === undefined
|
|
2566
|
+
? true
|
|
2567
|
+
: undefined;
|
|
2516
2568
|
}
|
|
2517
2569
|
// Build a UNIQUE finish-input tmpFile name per dispatch. Both the Stop hook and
|
|
2518
2570
|
// SessionEnd dispatch capture-finish children, and the child unlinks its input
|
|
@@ -2525,7 +2577,29 @@ function resolveCaptureFinal(argv2) {
|
|
|
2525
2577
|
// negligible volume (one tiny JSON per crashed child), though note Windows
|
|
2526
2578
|
// %TEMP% is not auto-cleaned — acceptable.
|
|
2527
2579
|
function finishInputTmpFile(sessionId) {
|
|
2528
|
-
|
|
2580
|
+
const safeId = String(sessionId ?? process.pid).replace(/[^A-Za-z0-9._-]/g, "_");
|
|
2581
|
+
return path.join(os.tmpdir(), `codiedev-finish-${safeId}-${process.pid}-${Date.now()}.json`);
|
|
2582
|
+
}
|
|
2583
|
+
function captureUploadTmpFile(sessionId, mode, pid = process.pid, now = Date.now()) {
|
|
2584
|
+
const id = String(sessionId ?? pid).replace(/[^A-Za-z0-9._-]/g, "_");
|
|
2585
|
+
return path.join(os.tmpdir(), mode === "immediate"
|
|
2586
|
+
? `codiedev-capture-${id}-${pid}-${now}.json`
|
|
2587
|
+
: `codiedev-capture-${id}.json`);
|
|
2588
|
+
}
|
|
2589
|
+
// Best-effort cancellation for a scheduled per-turn upload when a real final
|
|
2590
|
+
// capture arrives. Deferred jobs deliberately share one canonical filename;
|
|
2591
|
+
// removing it makes every sleeping uploader lose its later claim-by-rename.
|
|
2592
|
+
// The backend's capturedAt guard covers the narrow race where a child claimed
|
|
2593
|
+
// the file immediately before this unlink.
|
|
2594
|
+
function cancelDeferredCaptureUpload(sessionId, deps = fs) {
|
|
2595
|
+
try {
|
|
2596
|
+
deps.unlinkSync(captureUploadTmpFile(sessionId, "deferred"));
|
|
2597
|
+
return true;
|
|
2598
|
+
}
|
|
2599
|
+
catch {
|
|
2600
|
+
// Missing means there was nothing pending (or another process won).
|
|
2601
|
+
return false;
|
|
2602
|
+
}
|
|
2529
2603
|
}
|
|
2530
2604
|
// Decide whether to upload immediately or defer until the throttle window expires.
|
|
2531
2605
|
// final=true → always immediate (SessionEnd: close the session now)
|
|
@@ -2842,3 +2916,9 @@ function transcriptReadSkipReason(err) {
|
|
|
2842
2916
|
return "no-transcript";
|
|
2843
2917
|
return `transcript-unreadable:${code || "unknown"}`;
|
|
2844
2918
|
}
|
|
2919
|
+
// Per-turn snapshots are backups, not session endings. Reporting their local
|
|
2920
|
+
// read/empty condition as a session skip would hide or close coverage while the
|
|
2921
|
+
// agent is still running; only final/legacy capture paths emit the skip ping.
|
|
2922
|
+
function shouldReportCaptureSkip(final) {
|
|
2923
|
+
return final !== false;
|
|
2924
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codiedev",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.54",
|
|
4
4
|
"description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration — and run the governed Codie harness",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codiedev": "dist/cli.js",
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"dev": "tsc --watch",
|
|
13
13
|
"prepublishOnly": "tsc",
|
|
14
14
|
"test": "vitest run",
|
|
15
|
-
"test:functional": "vitest run src/__tests__/limitEvent.functional.test.ts",
|
|
15
|
+
"test:functional": "vitest run src/__tests__/limitEvent.functional.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts",
|
|
16
|
+
"test:harness-parity": "vitest run src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts",
|
|
17
|
+
"test:harness-binaries": "node scripts/checkHarnessBinaries.mjs",
|
|
16
18
|
"test:watch": "vitest"
|
|
17
19
|
},
|
|
18
20
|
"files": [
|
package/dist/commands/post.d.ts
DELETED
package/dist/commands/post.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// `codiedev post --title ... --body ... [--intent share|...] [--filename ...]
|
|
3
|
-
// [--mention NAME ...] [--tag X ...]` — publish to the team feed. Intended
|
|
4
|
-
// for broad-team reach: announcing a skill, asking who knows about X,
|
|
5
|
-
// proposing a change. Use `codiedev send` to address a single teammate.
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.runPost = runPost;
|
|
8
|
-
const shared_1 = require("./shared");
|
|
9
|
-
const VALID_INTENTS = new Set([
|
|
10
|
-
"share",
|
|
11
|
-
"request_review",
|
|
12
|
-
"request_expertise",
|
|
13
|
-
"link_share",
|
|
14
|
-
"rfc",
|
|
15
|
-
]);
|
|
16
|
-
const VALID_FORMATS = new Set(["article", "quick", "workflow", "review"]);
|
|
17
|
-
function parseArgs(args) {
|
|
18
|
-
let title;
|
|
19
|
-
let body;
|
|
20
|
-
let intent;
|
|
21
|
-
let filename;
|
|
22
|
-
let format;
|
|
23
|
-
const mentions = [];
|
|
24
|
-
const tags = [];
|
|
25
|
-
for (let i = 0; i < args.length; i++) {
|
|
26
|
-
const a = args[i];
|
|
27
|
-
const next = (k) => a === k && i + 1 < args.length ? args[++i] : a.startsWith(k + "=") ? a.slice(k.length + 1) : undefined;
|
|
28
|
-
let v;
|
|
29
|
-
if ((v = next("--title")) !== undefined)
|
|
30
|
-
title = v;
|
|
31
|
-
else if ((v = next("--body")) !== undefined)
|
|
32
|
-
body = v;
|
|
33
|
-
else if ((v = next("--intent")) !== undefined)
|
|
34
|
-
intent = v;
|
|
35
|
-
else if ((v = next("--filename")) !== undefined)
|
|
36
|
-
filename = v;
|
|
37
|
-
else if ((v = next("--format")) !== undefined)
|
|
38
|
-
format = v;
|
|
39
|
-
else if ((v = next("--mention")) !== undefined)
|
|
40
|
-
mentions.push(v);
|
|
41
|
-
else if ((v = next("--tag")) !== undefined)
|
|
42
|
-
tags.push(v);
|
|
43
|
-
}
|
|
44
|
-
if (!title || !title.trim()) {
|
|
45
|
-
console.error('Usage: codiedev post --title "<title>" --body "<body>" [--intent share|request_review|request_expertise|link_share|rfc] [--filename <key>] [--mention <name>]...');
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
if (!body || !body.trim()) {
|
|
49
|
-
console.error("--body required");
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
if (intent && !VALID_INTENTS.has(intent)) {
|
|
53
|
-
console.error(`Invalid --intent. Valid: ${[...VALID_INTENTS].join(", ")}`);
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
56
|
-
if (format && !VALID_FORMATS.has(format)) {
|
|
57
|
-
console.error(`Invalid --format. Valid: ${[...VALID_FORMATS].join(", ")}`);
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
return { title, body, intent, filename, format, mentions, tags };
|
|
61
|
-
}
|
|
62
|
-
async function runPost(args, configOverride) {
|
|
63
|
-
const parsed = parseArgs(args);
|
|
64
|
-
const config = configOverride ?? (0, shared_1.requireConfig)();
|
|
65
|
-
const start = Date.now();
|
|
66
|
-
try {
|
|
67
|
-
const res = await (0, shared_1.apiRequest)("POST", "/api/cli/post-to-feed", {
|
|
68
|
-
config,
|
|
69
|
-
body: {
|
|
70
|
-
title: parsed.title,
|
|
71
|
-
body: parsed.body,
|
|
72
|
-
intent: parsed.intent,
|
|
73
|
-
filename: parsed.filename,
|
|
74
|
-
format: parsed.format,
|
|
75
|
-
mentions: parsed.mentions.length > 0 ? parsed.mentions : undefined,
|
|
76
|
-
tags: parsed.tags.length > 0 ? parsed.tags : undefined,
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
(0, shared_1.recordAgentEvent)(config, {
|
|
80
|
-
tool: "codiedev_post_to_feed",
|
|
81
|
-
ok: true,
|
|
82
|
-
latencyMs: Date.now() - start,
|
|
83
|
-
});
|
|
84
|
-
console.log(`Posted to the team feed${res.postId ? ` (id: ${res.postId})` : ""}.`);
|
|
85
|
-
}
|
|
86
|
-
catch (err) {
|
|
87
|
-
(0, shared_1.recordAgentEvent)(config, {
|
|
88
|
-
tool: "codiedev_post_to_feed",
|
|
89
|
-
ok: false,
|
|
90
|
-
latencyMs: Date.now() - start,
|
|
91
|
-
error: err.message,
|
|
92
|
-
});
|
|
93
|
-
console.error(`Post failed: ${err.message}`);
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
}
|