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 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** — `sessionEnd` hook in `~/.cursor/hooks.json`
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 Codex, the agent already knows the commands —
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 sessionEnd (capture + auto-backup) hooks (~/.cursor/hooks.json)", "cursor-hook");
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
- hookInput = JSON.parse(raw);
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" || process.argv[2] === "session-start-opencode") {
464
- await runSessionStart(hookInput, process.argv[2] === "session-start-opencode" ? "opencode" : "claude-code");
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
- (0, utils_1.ensureAllCodiedevHooksInstalled)();
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. Codex/Cursor keep legacy
556
- // behaviour (final omitted).
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
- await postSessionSkip(config, session_id, reason);
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
- : parserMode === "cursor"
672
- ? { messageCount: 0, toolCallCount: 0 }
673
- : (0, utils_1.parseClaudeCodeStats)(transcriptContent);
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 = path.join(os.tmpdir(), `codiedev-capture-${session_id ?? process.pid}.json`);
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 post-to-feed response).",
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);
@@ -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/Cursor).
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 the new limit reporter without requiring connect.
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 present = stop.some((group) => {
1827
- const inner = group.hooks;
1828
- return Array.isArray(inner) && inner.some((hook) => typeof hook.command === "string" && /(?:^|\s)limit-codex(?:\s|$)/.test(hook.command));
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
- if (present)
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
- if (!hooks.sessionEnd) {
1872
- hooks.sessionEnd = [];
1873
- }
1874
- const existing = hooks.sessionEnd;
1875
- const filtered = existing.filter((h) => !(0, detection_1.isCodiedevHookCommand)(h.command));
1876
- filtered.push({
1877
- command: resolveHookCommand("capture-cursor"),
1878
- type: "command",
1879
- timeout: 30,
1880
- });
1881
- // sessionEnd → backup: auto-back-up changed artifact files to the private library.
1882
- filtered.push({
1883
- command: resolveHookCommand("backup"),
1884
- type: "command",
1885
- timeout: 30,
1886
- });
1887
- hooks.sessionEnd = filtered;
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
- // Everything else — capture-codex (Codex's Stop fires per-turn; final:true
2505
- // would close the server-side session lease every turn), capture-cursor, and
2506
- // any FUTURE mode (e.g. a capture-windsurf added to resolveMode but not
2507
- // audited here) — gets undefined, i.e. legacy server behaviour. Defaulting
2508
- // unknowns to true would silently re-introduce the per-turn lease-close bug
2509
- // for every new agent.
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 === undefined ? true : undefined;
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
- return path.join(os.tmpdir(), `codiedev-finish-${sessionId ?? process.pid}-${process.pid}-${Date.now()}.json`);
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.52",
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": [
@@ -1,2 +0,0 @@
1
- import type { CodiedevConfig } from "../utils";
2
- export declare function runPost(args: string[], configOverride?: CodiedevConfig): Promise<void>;
@@ -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
- }