codiedev 0.5.6 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,6 +44,9 @@ const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json")
44
44
  const CLAUDE_INSTRUCTIONS_PATH = path.join(os.homedir(), ".claude", "CLAUDE.md");
45
45
  const CODEX_HOOKS_PATH = path.join(os.homedir(), ".codex", "hooks.json");
46
46
  const CODEX_INSTRUCTIONS_PATH = path.join(os.homedir(), ".codex", "AGENTS.md");
47
+ const CURSOR_HOOKS_PATH = path.join(os.homedir(), ".cursor", "hooks.json");
48
+ const CURSOR_INSTRUCTIONS_PATH = path.join(os.homedir(), ".cursor", "rules", "codiedev.mdc");
49
+ const CURSOR_MCP_PATH = path.join(os.homedir(), ".cursor", "mcp.json");
47
50
  function symbol(status) {
48
51
  if (status === "pass")
49
52
  return "✓";
@@ -57,6 +60,48 @@ function claudeCodeInstalled() {
57
60
  function codexInstalled() {
58
61
  return fs.existsSync(path.join(os.homedir(), ".codex"));
59
62
  }
63
+ function cursorInstalled() {
64
+ return fs.existsSync(path.join(os.homedir(), ".cursor"));
65
+ }
66
+ function hasCursorMcpEntry() {
67
+ try {
68
+ if (!fs.existsSync(CURSOR_MCP_PATH))
69
+ return false;
70
+ const raw = fs.readFileSync(CURSOR_MCP_PATH, "utf8");
71
+ const parsed = JSON.parse(raw);
72
+ const servers = parsed.mcpServers;
73
+ return !!servers && "codiedev" in servers;
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ // Match both legacy `npx codiedev-hook ...` and absolute-path forms
80
+ // `<node> <.../codiedev/dist/hook.js> ...` — installer switched to absolute
81
+ // paths in 0.6.1 to work in GUI-launched contexts where shell PATH is missing.
82
+ function isCodiedevHookCommand(cmd) {
83
+ if (!cmd)
84
+ return false;
85
+ return cmd.includes("codiedev-hook") || /codiedev[\\/]dist[\\/]hook/.test(cmd);
86
+ }
87
+ // Cursor's hooks.json schema is { hooks: { sessionEnd: [{ command, ... }] } }
88
+ // — flat array of objects with `command`, not the Claude/Codex nested
89
+ // `{ hooks: [{ command }] }` wrapper.
90
+ function hasCursorHook() {
91
+ try {
92
+ if (!fs.existsSync(CURSOR_HOOKS_PATH))
93
+ return false;
94
+ const raw = fs.readFileSync(CURSOR_HOOKS_PATH, "utf8");
95
+ const parsed = JSON.parse(raw);
96
+ const arr = parsed.hooks?.sessionEnd;
97
+ if (!Array.isArray(arr))
98
+ return false;
99
+ return arr.some((h) => isCodiedevHookCommand(h.command));
100
+ }
101
+ catch {
102
+ return false;
103
+ }
104
+ }
60
105
  function hasCodiedevHook(settingsPath, hookKey) {
61
106
  try {
62
107
  if (!fs.existsSync(settingsPath))
@@ -70,7 +115,7 @@ function hasCodiedevHook(settingsPath, hookKey) {
70
115
  const inner = h.hooks;
71
116
  if (!Array.isArray(inner))
72
117
  return false;
73
- return inner.some((x) => (x.command ?? "").includes("codiedev-hook"));
118
+ return inner.some((x) => isCodiedevHookCommand(x.command));
74
119
  });
75
120
  }
76
121
  catch {
@@ -229,7 +274,43 @@ async function runDoctor(_args) {
229
274
  detail: "not installed on this machine",
230
275
  });
231
276
  }
232
- // 7. gh CLI (optional — only needed for reverse-ticket)
277
+ // 7. Cursor setup (if present)
278
+ if (cursorInstalled()) {
279
+ checks.push({
280
+ name: "Cursor detected",
281
+ status: "pass",
282
+ detail: "~/.cursor",
283
+ });
284
+ checks.push({
285
+ name: "Cursor sessionEnd hook",
286
+ status: hasCursorHook() ? "pass" : "fail",
287
+ detail: hasCursorHook()
288
+ ? "~/.cursor/hooks.json"
289
+ : "missing — re-run `codiedev connect`",
290
+ });
291
+ checks.push({
292
+ name: "Cursor agent instructions",
293
+ status: hasCodiedevInstructions(CURSOR_INSTRUCTIONS_PATH) ? "pass" : "fail",
294
+ detail: hasCodiedevInstructions(CURSOR_INSTRUCTIONS_PATH)
295
+ ? "~/.cursor/rules/codiedev.mdc"
296
+ : "missing — re-run `codiedev connect`",
297
+ });
298
+ checks.push({
299
+ name: "Cursor MCP server",
300
+ status: hasCursorMcpEntry() ? "pass" : "fail",
301
+ detail: hasCursorMcpEntry()
302
+ ? "~/.cursor/mcp.json"
303
+ : "missing — re-run `codiedev connect`",
304
+ });
305
+ }
306
+ else {
307
+ checks.push({
308
+ name: "Cursor",
309
+ status: "warn",
310
+ detail: "not installed on this machine",
311
+ });
312
+ }
313
+ // 8. gh CLI (optional — only needed for reverse-ticket)
233
314
  checks.push({
234
315
  name: "GitHub CLI (`gh`) for reverse-ticket command",
235
316
  status: hasGhCli() ? "pass" : "warn",
package/dist/connect.js CHANGED
@@ -141,6 +141,7 @@ async function runConnect() {
141
141
  }
142
142
  const hasClaude = (0, utils_1.claudeCodeInstalled)();
143
143
  const hasCodex = (0, utils_1.codexInstalled)();
144
+ const hasCursor = (0, utils_1.cursorInstalled)();
144
145
  const installed = [];
145
146
  if (hasClaude) {
146
147
  try {
@@ -157,6 +158,13 @@ async function runConnect() {
157
158
  catch (err) {
158
159
  console.error(`\nWarning: Failed to install Claude Code instructions — ${err.message}`);
159
160
  }
161
+ try {
162
+ (0, utils_1.installClaudeCodeMcp)();
163
+ installed.push("Claude Code MCP server (~/.claude.json)");
164
+ }
165
+ catch (err) {
166
+ console.error(`\nWarning: Failed to install Claude Code MCP server — ${err.message}`);
167
+ }
160
168
  }
161
169
  if (hasCodex) {
162
170
  try {
@@ -173,9 +181,39 @@ async function runConnect() {
173
181
  catch (err) {
174
182
  console.error(`\nWarning: Failed to install Codex instructions — ${err.message}`);
175
183
  }
184
+ try {
185
+ (0, utils_1.installCodexMcp)();
186
+ installed.push("Codex MCP server (~/.codex/config.toml)");
187
+ }
188
+ catch (err) {
189
+ console.error(`\nWarning: Failed to install Codex MCP server — ${err.message}`);
190
+ }
191
+ }
192
+ if (hasCursor) {
193
+ try {
194
+ (0, utils_1.installCursorHook)();
195
+ installed.push("Cursor sessionEnd hook (~/.cursor/hooks.json)");
196
+ }
197
+ catch (err) {
198
+ console.error(`\nWarning: Failed to install Cursor hook — ${err.message}`);
199
+ }
200
+ try {
201
+ (0, utils_1.installCursorInstructions)();
202
+ installed.push("Cursor agent instructions (~/.cursor/rules/codiedev.mdc)");
203
+ }
204
+ catch (err) {
205
+ console.error(`\nWarning: Failed to install Cursor instructions — ${err.message}`);
206
+ }
207
+ try {
208
+ (0, utils_1.installCursorMcp)();
209
+ installed.push("Cursor MCP server (~/.cursor/mcp.json)");
210
+ }
211
+ catch (err) {
212
+ console.error(`\nWarning: Failed to install Cursor MCP server — ${err.message}`);
213
+ }
176
214
  }
177
- if (!hasClaude && !hasCodex) {
178
- console.warn("\nNo Claude Code (~/.claude) or Codex (~/.codex) install detected.");
215
+ if (!hasClaude && !hasCodex && !hasCursor) {
216
+ console.warn("\nNo Claude Code (~/.claude), Codex (~/.codex), or Cursor (~/.cursor) install detected.");
179
217
  console.warn("Config saved. Install one, then re-run `npx codiedev connect` to wire up capture hooks.");
180
218
  }
181
219
  console.log(`\nConnected to ${companyName}`);
package/dist/hook.js CHANGED
@@ -87,6 +87,8 @@ async function readStdin() {
87
87
  function resolveMode(arg) {
88
88
  if (arg === "capture-codex")
89
89
  return "codex";
90
+ if (arg === "capture-cursor")
91
+ return "cursor";
90
92
  return "claude-code";
91
93
  }
92
94
  async function main() {
@@ -101,7 +103,10 @@ async function main() {
101
103
  catch {
102
104
  process.exit(0);
103
105
  }
104
- const { session_id, transcript_path, cwd } = hookInput;
106
+ const { session_id, transcript_path, cwd, workspace_roots } = hookInput;
107
+ const workspaceRoot = workspace_roots && workspace_roots.length > 0
108
+ ? workspace_roots[0]
109
+ : cwd || process.cwd();
105
110
  const config = (0, utils_1.readConfig)();
106
111
  if (!config) {
107
112
  process.exit(0);
@@ -116,13 +121,14 @@ async function main() {
116
121
  catch {
117
122
  process.exit(0);
118
123
  }
119
- // Resolve repo URL. Codex embeds it in session_meta; fall back to git for CC.
124
+ // Resolve repo URL. Codex embeds it in session_meta; CC and Cursor both
125
+ // need the git remote of the workspace.
120
126
  let remoteUrl = null;
121
127
  if (mode === "codex") {
122
128
  remoteUrl = (0, utils_1.extractCodexRepoUrl)(transcriptContent);
123
129
  }
124
130
  if (!remoteUrl) {
125
- remoteUrl = (0, utils_1.getGitRemoteUrl)(cwd || process.cwd());
131
+ remoteUrl = (0, utils_1.getGitRemoteUrl)(workspaceRoot);
126
132
  }
127
133
  if (!remoteUrl) {
128
134
  process.exit(0);
@@ -131,9 +137,14 @@ async function main() {
131
137
  if (!matchedRepo) {
132
138
  process.exit(0);
133
139
  }
140
+ // Cursor's transcript format is undocumented — skip stats parsing for now
141
+ // and let the backend handle it once we have a real session to inspect.
142
+ // The transcript itself still uploads.
134
143
  const stats = mode === "codex"
135
144
  ? (0, utils_1.parseCodexStats)(transcriptContent)
136
- : (0, utils_1.parseClaudeCodeStats)(transcriptContent);
145
+ : mode === "cursor"
146
+ ? { messageCount: 0, toolCallCount: 0 }
147
+ : (0, utils_1.parseClaudeCodeStats)(transcriptContent);
137
148
  const transcriptBase64 = Buffer.from(transcriptContent, "utf8").toString("base64");
138
149
  const payload = {
139
150
  sessionId: session_id,
package/dist/utils.d.ts CHANGED
@@ -26,14 +26,23 @@ export declare function matchRepo(remoteUrl: string, repos: Array<{
26
26
  export declare function hashToken(token: string): string;
27
27
  export declare function claudeCodeInstalled(): boolean;
28
28
  export declare function codexInstalled(): boolean;
29
+ export declare function cursorInstalled(): boolean;
29
30
  export declare function installHook(): void;
30
31
  export declare function installClaudeCodeInstructions(): void;
31
32
  export declare function installCodexInstructions(): void;
33
+ export declare function installCursorInstructions(): void;
32
34
  /**
33
35
  * Install the CodieDev MCP server into Claude Code's user-scope config.
34
36
  * Safe to call multiple times — updates the existing entry if present.
35
37
  */
36
38
  export declare function installClaudeCodeMcp(): void;
39
+ /**
40
+ * Install the CodieDev MCP server into Cursor's user-scope config.
41
+ * Cursor uses the same `mcpServers` schema as Claude Code; just a different
42
+ * file path (~/.cursor/mcp.json). Project-scoped servers can also live at
43
+ * <project>/.cursor/mcp.json — we only manage the user-scope one.
44
+ */
45
+ export declare function installCursorMcp(): void;
37
46
  /**
38
47
  * Best-effort append of the CodieDev MCP server block to ~/.codex/config.toml.
39
48
  *
@@ -42,6 +51,7 @@ export declare function installClaudeCodeMcp(): void;
42
51
  */
43
52
  export declare function installCodexMcp(): boolean;
44
53
  export declare function installCodexHook(): void;
54
+ export declare function installCursorHook(): void;
45
55
  export interface ParsedStats {
46
56
  messageCount: number;
47
57
  toolCallCount: number;
package/dist/utils.js CHANGED
@@ -41,12 +41,16 @@ exports.matchRepo = matchRepo;
41
41
  exports.hashToken = hashToken;
42
42
  exports.claudeCodeInstalled = claudeCodeInstalled;
43
43
  exports.codexInstalled = codexInstalled;
44
+ exports.cursorInstalled = cursorInstalled;
44
45
  exports.installHook = installHook;
45
46
  exports.installClaudeCodeInstructions = installClaudeCodeInstructions;
46
47
  exports.installCodexInstructions = installCodexInstructions;
48
+ exports.installCursorInstructions = installCursorInstructions;
47
49
  exports.installClaudeCodeMcp = installClaudeCodeMcp;
50
+ exports.installCursorMcp = installCursorMcp;
48
51
  exports.installCodexMcp = installCodexMcp;
49
52
  exports.installCodexHook = installCodexHook;
53
+ exports.installCursorHook = installCursorHook;
50
54
  exports.parseClaudeCodeStats = parseClaudeCodeStats;
51
55
  exports.parseCodexStats = parseCodexStats;
52
56
  exports.extractCodexRepoUrl = extractCodexRepoUrl;
@@ -133,12 +137,50 @@ const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json")
133
137
  const CLAUDE_DIR = path.join(os.homedir(), ".claude");
134
138
  const CODEX_HOOKS_PATH = path.join(os.homedir(), ".codex", "hooks.json");
135
139
  const CODEX_DIR = path.join(os.homedir(), ".codex");
140
+ const CURSOR_DIR = path.join(os.homedir(), ".cursor");
141
+ const CURSOR_HOOKS_PATH = path.join(CURSOR_DIR, "hooks.json");
142
+ const CURSOR_MCP_PATH = path.join(CURSOR_DIR, "mcp.json");
143
+ const CURSOR_RULES_DIR = path.join(CURSOR_DIR, "rules");
144
+ const CURSOR_RULES_PATH = path.join(CURSOR_RULES_DIR, "codiedev.mdc");
145
+ // GUI-launched agents (Cursor.app, future JetBrains plugins) don't source the
146
+ // user's shell rc, so nvm-managed `npx` and `node` aren't on PATH. Resolve the
147
+ // absolute path to the current node binary and our hook.js at install time and
148
+ // write that into hook configs so execution doesn't depend on the spawned
149
+ // shell's PATH. Falls back to `npx` if we can't resolve dist/hook.js.
150
+ function shellQuote(s) {
151
+ return `'${s.replace(/'/g, `'\\''`)}'`;
152
+ }
153
+ function resolveHookCommand(subcommand) {
154
+ try {
155
+ const cliEntry = process.argv[1];
156
+ if (cliEntry) {
157
+ const realCli = fs.realpathSync(cliEntry);
158
+ const distDir = path.dirname(realCli);
159
+ const hookScript = path.join(distDir, "hook.js");
160
+ if (fs.existsSync(hookScript)) {
161
+ return `${shellQuote(process.execPath)} ${shellQuote(hookScript)} ${subcommand}`;
162
+ }
163
+ }
164
+ }
165
+ catch {
166
+ // Fall through to npx fallback.
167
+ }
168
+ return `npx codiedev-hook ${subcommand}`;
169
+ }
170
+ function isCodiedevHookCommand(cmd) {
171
+ if (!cmd)
172
+ return false;
173
+ return cmd.includes("codiedev-hook") || /codiedev[\\/]dist[\\/]hook/.test(cmd);
174
+ }
136
175
  function claudeCodeInstalled() {
137
176
  return fs.existsSync(CLAUDE_DIR);
138
177
  }
139
178
  function codexInstalled() {
140
179
  return fs.existsSync(CODEX_DIR);
141
180
  }
181
+ function cursorInstalled() {
182
+ return fs.existsSync(CURSOR_DIR);
183
+ }
142
184
  function installHook() {
143
185
  let settings = {};
144
186
  try {
@@ -157,30 +199,27 @@ function installHook() {
157
199
  if (!hooks.SessionEnd) {
158
200
  hooks.SessionEnd = [];
159
201
  }
160
- const sessionEndHooks = hooks.SessionEnd;
161
- const alreadyInstalled = sessionEndHooks.some((hook) => {
162
- const matcher = hook.matcher;
163
- const hooks2 = hook.hooks;
164
- if (hooks2) {
165
- return hooks2.some((h) => {
166
- const cmd = h.command;
167
- return cmd && cmd.includes("codiedev-hook");
168
- });
202
+ // Drop any prior codiedev entries (legacy `npx` form or older absolute
203
+ // paths) before re-adding so connect re-runs upgrade the resolved binary
204
+ // path instead of leaving stale entries behind.
205
+ const existing = hooks.SessionEnd;
206
+ const filtered = existing.filter((hook) => {
207
+ const inner = hook.hooks;
208
+ if (Array.isArray(inner)) {
209
+ return !inner.some((h) => isCodiedevHookCommand(h.command));
169
210
  }
170
- return matcher && matcher.includes("codiedev-hook");
211
+ return !isCodiedevHookCommand(hook.matcher);
171
212
  });
172
- if (alreadyInstalled) {
173
- return;
174
- }
175
- sessionEndHooks.push({
213
+ filtered.push({
176
214
  matcher: ".*",
177
215
  hooks: [
178
216
  {
179
217
  type: "command",
180
- command: "npx codiedev-hook capture",
218
+ command: resolveHookCommand("capture"),
181
219
  },
182
220
  ],
183
221
  });
222
+ hooks.SessionEnd = filtered;
184
223
  if (!fs.existsSync(CLAUDE_DIR)) {
185
224
  fs.mkdirSync(CLAUDE_DIR, { recursive: true });
186
225
  }
@@ -312,25 +351,74 @@ function installClaudeCodeInstructions() {
312
351
  function installCodexInstructions() {
313
352
  writeInstructionsBlock(CODEX_USER_INSTRUCTIONS_PATH);
314
353
  }
354
+ // Cursor uses .mdc rule files with YAML frontmatter (description, alwaysApply,
355
+ // globs). `.cursorrules` is deprecated and ignored by Agent mode as of 2026.
356
+ const CURSOR_INSTRUCTIONS_FRONTMATTER = `---
357
+ description: CodieDev team artifact layer — push, pull, search, ping, and share artifacts with teammates
358
+ alwaysApply: true
359
+ ---
360
+
361
+ `;
362
+ function installCursorInstructions() {
363
+ if (!fs.existsSync(CURSOR_RULES_DIR)) {
364
+ fs.mkdirSync(CURSOR_RULES_DIR, { recursive: true });
365
+ }
366
+ const block = CURSOR_INSTRUCTIONS_FRONTMATTER +
367
+ CODIEDEV_INSTRUCTIONS_BEGIN +
368
+ "\n" +
369
+ CODIEDEV_INSTRUCTIONS_BODY +
370
+ "\n" +
371
+ CODIEDEV_INSTRUCTIONS_END +
372
+ "\n";
373
+ let existing = "";
374
+ if (fs.existsSync(CURSOR_RULES_PATH)) {
375
+ existing = fs.readFileSync(CURSOR_RULES_PATH, "utf8");
376
+ }
377
+ // If the file already has our markers, replace just the block — preserve
378
+ // any frontmatter edits the user made.
379
+ const beginIdx = existing.indexOf(CODIEDEV_INSTRUCTIONS_BEGIN);
380
+ const endIdx = existing.indexOf(CODIEDEV_INSTRUCTIONS_END);
381
+ let next;
382
+ if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
383
+ next =
384
+ existing.slice(0, beginIdx) +
385
+ CODIEDEV_INSTRUCTIONS_BEGIN +
386
+ "\n" +
387
+ CODIEDEV_INSTRUCTIONS_BODY +
388
+ "\n" +
389
+ CODIEDEV_INSTRUCTIONS_END +
390
+ existing.slice(endIdx + CODIEDEV_INSTRUCTIONS_END.length);
391
+ }
392
+ else {
393
+ next = block;
394
+ }
395
+ fs.writeFileSync(CURSOR_RULES_PATH, next, "utf8");
396
+ }
397
+ function readMcpConfigSafely(path) {
398
+ if (!fs.existsSync(path))
399
+ return {};
400
+ const raw = fs.readFileSync(path, "utf8");
401
+ if (!raw.trim())
402
+ return {};
403
+ try {
404
+ return JSON.parse(raw);
405
+ }
406
+ catch {
407
+ const backup = `${path}.bak.${Date.now()}`;
408
+ fs.copyFileSync(path, backup);
409
+ console.warn(`Warning: ${path} contained invalid JSON. Backed up to ${backup} before rewriting.`);
410
+ return {};
411
+ }
412
+ }
315
413
  /**
316
414
  * Install the CodieDev MCP server into Claude Code's user-scope config.
317
415
  * Safe to call multiple times — updates the existing entry if present.
318
416
  */
319
417
  function installClaudeCodeMcp() {
320
- let config = {};
321
- try {
322
- if (fs.existsSync(CLAUDE_USER_CONFIG_PATH)) {
323
- const raw = fs.readFileSync(CLAUDE_USER_CONFIG_PATH, "utf8");
324
- config = raw.trim() ? JSON.parse(raw) : {};
325
- }
326
- }
327
- catch {
328
- // Start fresh if the existing file is corrupt — safer than failing the install.
329
- config = {};
330
- }
418
+ const config = readMcpConfigSafely(CLAUDE_USER_CONFIG_PATH);
331
419
  const mcpServers = config.mcpServers ?? {};
332
- // Canonical Claude Code user-scope format: just command + args.
333
- // No `type` field (some CC versions reject it; stdio is the default).
420
+ if (mcpServers.codiedev)
421
+ return;
334
422
  mcpServers.codiedev = {
335
423
  command: "npx",
336
424
  args: ["codiedev-mcp"],
@@ -338,6 +426,27 @@ function installClaudeCodeMcp() {
338
426
  config.mcpServers = mcpServers;
339
427
  fs.writeFileSync(CLAUDE_USER_CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
340
428
  }
429
+ /**
430
+ * Install the CodieDev MCP server into Cursor's user-scope config.
431
+ * Cursor uses the same `mcpServers` schema as Claude Code; just a different
432
+ * file path (~/.cursor/mcp.json). Project-scoped servers can also live at
433
+ * <project>/.cursor/mcp.json — we only manage the user-scope one.
434
+ */
435
+ function installCursorMcp() {
436
+ if (!fs.existsSync(CURSOR_DIR)) {
437
+ fs.mkdirSync(CURSOR_DIR, { recursive: true });
438
+ }
439
+ const config = readMcpConfigSafely(CURSOR_MCP_PATH);
440
+ const mcpServers = config.mcpServers ?? {};
441
+ if (mcpServers.codiedev)
442
+ return;
443
+ mcpServers.codiedev = {
444
+ command: "npx",
445
+ args: ["codiedev-mcp"],
446
+ };
447
+ config.mcpServers = mcpServers;
448
+ fs.writeFileSync(CURSOR_MCP_PATH, JSON.stringify(config, null, 2), "utf8");
449
+ }
341
450
  /**
342
451
  * Best-effort append of the CodieDev MCP server block to ~/.codex/config.toml.
343
452
  *
@@ -387,33 +496,66 @@ function installCodexHook() {
387
496
  if (!hooks.Stop) {
388
497
  hooks.Stop = [];
389
498
  }
390
- const stopHooks = hooks.Stop;
391
- const alreadyInstalled = stopHooks.some((hook) => {
499
+ const existing = hooks.Stop;
500
+ const filtered = existing.filter((hook) => {
392
501
  const inner = hook.hooks;
393
- if (!inner)
394
- return false;
395
- return inner.some((h) => {
396
- const cmd = h.command;
397
- return cmd && cmd.includes("codiedev-hook");
398
- });
502
+ if (!Array.isArray(inner))
503
+ return true;
504
+ return !inner.some((h) => isCodiedevHookCommand(h.command));
399
505
  });
400
- if (alreadyInstalled) {
401
- return;
402
- }
403
- stopHooks.push({
506
+ filtered.push({
404
507
  hooks: [
405
508
  {
406
509
  type: "command",
407
- command: "npx codiedev-hook capture-codex",
510
+ command: resolveHookCommand("capture-codex"),
408
511
  timeout: 30,
409
512
  },
410
513
  ],
411
514
  });
515
+ hooks.Stop = filtered;
412
516
  if (!fs.existsSync(CODEX_DIR)) {
413
517
  fs.mkdirSync(CODEX_DIR, { recursive: true });
414
518
  }
415
519
  fs.writeFileSync(CODEX_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
416
520
  }
521
+ // Cursor's hooks.json uses { version: 1, hooks: { sessionEnd: [{ command, type, timeout }] } }.
522
+ // `sessionEnd` fires when a conversation closes (completed, aborted, error,
523
+ // window_close, user_close); the base stdin payload includes transcript_path
524
+ // and session_id which `codiedev-hook capture-cursor` reads to upload.
525
+ function installCursorHook() {
526
+ if (!fs.existsSync(CURSOR_DIR)) {
527
+ fs.mkdirSync(CURSOR_DIR, { recursive: true });
528
+ }
529
+ let hooksFile = {};
530
+ try {
531
+ if (fs.existsSync(CURSOR_HOOKS_PATH)) {
532
+ const raw = fs.readFileSync(CURSOR_HOOKS_PATH, "utf8");
533
+ hooksFile = raw.trim() ? JSON.parse(raw) : {};
534
+ }
535
+ }
536
+ catch {
537
+ hooksFile = {};
538
+ }
539
+ if (!hooksFile.version) {
540
+ hooksFile.version = 1;
541
+ }
542
+ if (!hooksFile.hooks) {
543
+ hooksFile.hooks = {};
544
+ }
545
+ const hooks = hooksFile.hooks;
546
+ if (!hooks.sessionEnd) {
547
+ hooks.sessionEnd = [];
548
+ }
549
+ const existing = hooks.sessionEnd;
550
+ const filtered = existing.filter((h) => !isCodiedevHookCommand(h.command));
551
+ filtered.push({
552
+ command: resolveHookCommand("capture-cursor"),
553
+ type: "command",
554
+ timeout: 30,
555
+ });
556
+ hooks.sessionEnd = filtered;
557
+ fs.writeFileSync(CURSOR_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
558
+ }
417
559
  function parseClaudeCodeStats(content) {
418
560
  let messageCount = 0;
419
561
  let toolCallCount = 0;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.5.6",
4
- "description": "Connect Claude Code or Codex to CodieDev for org-wide session capture and artifact collaboration",
3
+ "version": "0.6.1",
4
+ "description": "Connect Claude Code, Codex, or Cursor to CodieDev for org-wide session capture and artifact collaboration",
5
5
  "bin": {
6
6
  "codiedev": "./dist/cli.js",
7
7
  "codiedev-hook": "./dist/hook.js"
@@ -21,6 +21,7 @@
21
21
  "claude",
22
22
  "codex",
23
23
  "openai",
24
+ "cursor",
24
25
  "ai",
25
26
  "session-capture"
26
27
  ],