codiedev 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -19,6 +19,7 @@ const connect_1 = require("./connect");
19
19
  const push_1 = require("./commands/push");
20
20
  const delete_1 = require("./commands/delete");
21
21
  const pull_1 = require("./commands/pull");
22
+ const ask_1 = require("./commands/ask");
22
23
  const ping_1 = require("./commands/ping");
23
24
  const inbox_1 = require("./commands/inbox");
24
25
  const note_1 = require("./commands/note");
@@ -43,6 +44,10 @@ Artifacts:
43
44
  codiedev delete <key> [-y] Delete every version of an artifact you
44
45
  authored (clean up bad/test pushes).
45
46
  Asks for confirmation; -y to skip.
47
+ codiedev ask "<question>" Ask a natural-language question of the
48
+ team's collective memory. Returns an
49
+ LLM-synthesized answer with citations
50
+ pointing at sessions + artifacts.
46
51
  codiedev promote <artifact-id> Promote an auto-extracted artifact
47
52
  to an authored one
48
53
  codiedev reverse-ticket [<pr-url>] Generate a Jira ticket draft.
@@ -374,6 +379,9 @@ async function main() {
374
379
  case "pull":
375
380
  await (0, pull_1.runPull)(rest);
376
381
  return;
382
+ case "ask":
383
+ await (0, ask_1.runAsk)(rest);
384
+ return;
377
385
  case "ping":
378
386
  await (0, ping_1.runPing)(rest);
379
387
  return;
@@ -0,0 +1 @@
1
+ export declare function runAsk(args: string[]): Promise<void>;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runAsk = runAsk;
4
+ const shared_1 = require("./shared");
5
+ function parseArgs(args) {
6
+ // Everything after the command, joined back into a question string. Allows
7
+ // both quoted and unquoted forms: `codiedev ask "why pg-bouncer?"` or
8
+ // `codiedev ask why did we drop pg-bouncer`.
9
+ const question = args
10
+ .filter((a) => !a.startsWith("--"))
11
+ .join(" ")
12
+ .trim();
13
+ if (!question) {
14
+ console.error('Usage: codiedev ask "<question>"');
15
+ console.error('Example: codiedev ask "why did we drop pg-bouncer?"');
16
+ process.exit(1);
17
+ }
18
+ return { question };
19
+ }
20
+ async function runAsk(args) {
21
+ const { question } = parseArgs(args);
22
+ const config = (0, shared_1.requireConfig)();
23
+ try {
24
+ const res = await (0, shared_1.apiRequest)("POST", "/api/cli/ask", {
25
+ config,
26
+ body: { question },
27
+ });
28
+ console.log("");
29
+ console.log(res.answer);
30
+ if (res.citations.length > 0) {
31
+ console.log("");
32
+ console.log("Sources:");
33
+ for (const c of res.citations) {
34
+ const label = c.kind === "artifact"
35
+ ? `[${c.type}] ${c.title}`
36
+ : `[session] ${c.title}`;
37
+ console.log(` • ${label}`);
38
+ }
39
+ }
40
+ console.log("");
41
+ }
42
+ catch (err) {
43
+ console.error(`Ask failed: ${err.message}`);
44
+ process.exit(1);
45
+ }
46
+ }
@@ -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,43 @@ 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
+ // Cursor's hooks.json schema is { hooks: { sessionEnd: [{ command, ... }] } }
80
+ // — flat array of objects with `command`, not the Claude/Codex nested
81
+ // `{ hooks: [{ command }] }` wrapper.
82
+ function hasCursorHook() {
83
+ try {
84
+ if (!fs.existsSync(CURSOR_HOOKS_PATH))
85
+ return false;
86
+ const raw = fs.readFileSync(CURSOR_HOOKS_PATH, "utf8");
87
+ const parsed = JSON.parse(raw);
88
+ const arr = parsed.hooks?.sessionEnd;
89
+ if (!Array.isArray(arr))
90
+ return false;
91
+ return arr.some((h) => {
92
+ const cmd = h.command;
93
+ return (cmd ?? "").includes("codiedev-hook");
94
+ });
95
+ }
96
+ catch {
97
+ return false;
98
+ }
99
+ }
60
100
  function hasCodiedevHook(settingsPath, hookKey) {
61
101
  try {
62
102
  if (!fs.existsSync(settingsPath))
@@ -229,7 +269,43 @@ async function runDoctor(_args) {
229
269
  detail: "not installed on this machine",
230
270
  });
231
271
  }
232
- // 7. gh CLI (optional — only needed for reverse-ticket)
272
+ // 7. Cursor setup (if present)
273
+ if (cursorInstalled()) {
274
+ checks.push({
275
+ name: "Cursor detected",
276
+ status: "pass",
277
+ detail: "~/.cursor",
278
+ });
279
+ checks.push({
280
+ name: "Cursor sessionEnd hook",
281
+ status: hasCursorHook() ? "pass" : "fail",
282
+ detail: hasCursorHook()
283
+ ? "~/.cursor/hooks.json"
284
+ : "missing — re-run `codiedev connect`",
285
+ });
286
+ checks.push({
287
+ name: "Cursor agent instructions",
288
+ status: hasCodiedevInstructions(CURSOR_INSTRUCTIONS_PATH) ? "pass" : "fail",
289
+ detail: hasCodiedevInstructions(CURSOR_INSTRUCTIONS_PATH)
290
+ ? "~/.cursor/rules/codiedev.mdc"
291
+ : "missing — re-run `codiedev connect`",
292
+ });
293
+ checks.push({
294
+ name: "Cursor MCP server",
295
+ status: hasCursorMcpEntry() ? "pass" : "fail",
296
+ detail: hasCursorMcpEntry()
297
+ ? "~/.cursor/mcp.json"
298
+ : "missing — re-run `codiedev connect`",
299
+ });
300
+ }
301
+ else {
302
+ checks.push({
303
+ name: "Cursor",
304
+ status: "warn",
305
+ detail: "not installed on this machine",
306
+ });
307
+ }
308
+ // 8. gh CLI (optional — only needed for reverse-ticket)
233
309
  checks.push({
234
310
  name: "GitHub CLI (`gh`) for reverse-ticket command",
235
311
  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/mcp.js CHANGED
@@ -384,7 +384,9 @@ const TOOLS = [
384
384
  "when the user asks 'has anyone solved X?', 'find me the spec on Y', " +
385
385
  "or 'look for prior work on Z'. Always search before pushing a new " +
386
386
  "artifact so the user doesn't duplicate work. Returns ranked hits " +
387
- "with title, type, filename key, and a snippet.",
387
+ "with title, type, filename key, and a snippet. For open-ended " +
388
+ "questions where the user wants a synthesized answer (not just a list " +
389
+ "of hits), prefer codiedev_ask instead.",
388
390
  inputSchema: {
389
391
  type: "object",
390
392
  properties: {
@@ -394,6 +396,32 @@ const TOOLS = [
394
396
  required: ["query"],
395
397
  },
396
398
  },
399
+ {
400
+ name: "codiedev_ask",
401
+ description: "Ask a natural-language question of the team's collective memory and " +
402
+ "get a synthesized answer with citations. Searches across both " +
403
+ "auto-extracted artifacts (specs / decisions / bugfixes) AND raw " +
404
+ "session transcripts, then has an LLM compose an answer that cites " +
405
+ "the actual sources. Use when the user asks 'why did we…?', 'what " +
406
+ "does the team know about X?', 'has anyone done Y here before?', " +
407
+ "'who decided Z and why?', or any open-ended question about prior " +
408
+ "work where they want context, not just a hit list. For exact-match " +
409
+ "lookups (find me spec-x), prefer codiedev_pull. For browsing or " +
410
+ "filtering by kind/tag, prefer codiedev_get_library. For surfacing " +
411
+ "ranked hits without LLM synthesis, prefer codiedev_search.",
412
+ inputSchema: {
413
+ type: "object",
414
+ properties: {
415
+ question: {
416
+ type: "string",
417
+ description: "Natural-language question. Open-ended is fine — e.g. 'why " +
418
+ "did we drop pg-bouncer?' or 'how does the team think about " +
419
+ "feature flags?'.",
420
+ },
421
+ },
422
+ required: ["question"],
423
+ },
424
+ },
397
425
  {
398
426
  name: "codiedev_get_library",
399
427
  description: "List artifacts in the team library — both docs (spec / decision / " +
@@ -571,6 +599,8 @@ async function dispatchTool(name, args, config) {
571
599
  return await handleSendTo(args, config);
572
600
  case "codiedev_react":
573
601
  return await handleReact(args, config);
602
+ case "codiedev_ask":
603
+ return await handleAsk(args, config);
574
604
  case "codiedev_search":
575
605
  return await handleSearch(args, config);
576
606
  case "codiedev_get_library":
@@ -922,6 +952,23 @@ async function handleSearch(args, config) {
922
952
  }
923
953
  return { content: [{ type: "text", text: lines.join("\n") }] };
924
954
  }
955
+ async function handleAsk(args, config) {
956
+ const question = asString(args.question);
957
+ if (!question)
958
+ throw new Error("question required");
959
+ const res = await (0, shared_1.apiRequest)("POST", "/api/cli/ask", { config, body: { question } });
960
+ const lines = [res.answer];
961
+ if (res.citations && res.citations.length > 0) {
962
+ lines.push("", "Sources:");
963
+ for (const c of res.citations) {
964
+ const label = c.kind === "artifact"
965
+ ? `[${c.type}] ${c.title}`
966
+ : `[session] ${c.title}`;
967
+ lines.push(` • ${label}`);
968
+ }
969
+ }
970
+ return { content: [{ type: "text", text: lines.join("\n") }] };
971
+ }
925
972
  async function handleGetLibrary(args, config) {
926
973
  const scope = asStringOrUndefined(args.scope);
927
974
  const type = asStringOrUndefined(args.type);
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,20 @@ 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");
136
145
  function claudeCodeInstalled() {
137
146
  return fs.existsSync(CLAUDE_DIR);
138
147
  }
139
148
  function codexInstalled() {
140
149
  return fs.existsSync(CODEX_DIR);
141
150
  }
151
+ function cursorInstalled() {
152
+ return fs.existsSync(CURSOR_DIR);
153
+ }
142
154
  function installHook() {
143
155
  let settings = {};
144
156
  try {
@@ -218,6 +230,7 @@ thought, use the \`codiedev\` CLI via Bash:**
218
230
  | "share with Greg" / "give Jason access" (no notification) | MCP tool \`codiedev_share_with\` |
219
231
  | "send this to Greg" / "ask Greg to look at this" | MCP tool \`codiedev_send_to\` |
220
232
  | "find something about X" / "has anyone solved Y?" | MCP tool \`codiedev_search\` |
233
+ | "why did we…?" / "what does the team know about X?" | MCP tool \`codiedev_ask\` (synthesized answer + citations) |
221
234
  | "show my library" / "what artifacts exist?" | MCP tool \`codiedev_get_library\` |
222
235
  | "react 🛠 to that post" / "mark as used" | MCP tool \`codiedev_react\` |
223
236
 
@@ -311,25 +324,74 @@ function installClaudeCodeInstructions() {
311
324
  function installCodexInstructions() {
312
325
  writeInstructionsBlock(CODEX_USER_INSTRUCTIONS_PATH);
313
326
  }
327
+ // Cursor uses .mdc rule files with YAML frontmatter (description, alwaysApply,
328
+ // globs). `.cursorrules` is deprecated and ignored by Agent mode as of 2026.
329
+ const CURSOR_INSTRUCTIONS_FRONTMATTER = `---
330
+ description: CodieDev team artifact layer — push, pull, search, ping, and share artifacts with teammates
331
+ alwaysApply: true
332
+ ---
333
+
334
+ `;
335
+ function installCursorInstructions() {
336
+ if (!fs.existsSync(CURSOR_RULES_DIR)) {
337
+ fs.mkdirSync(CURSOR_RULES_DIR, { recursive: true });
338
+ }
339
+ const block = CURSOR_INSTRUCTIONS_FRONTMATTER +
340
+ CODIEDEV_INSTRUCTIONS_BEGIN +
341
+ "\n" +
342
+ CODIEDEV_INSTRUCTIONS_BODY +
343
+ "\n" +
344
+ CODIEDEV_INSTRUCTIONS_END +
345
+ "\n";
346
+ let existing = "";
347
+ if (fs.existsSync(CURSOR_RULES_PATH)) {
348
+ existing = fs.readFileSync(CURSOR_RULES_PATH, "utf8");
349
+ }
350
+ // If the file already has our markers, replace just the block — preserve
351
+ // any frontmatter edits the user made.
352
+ const beginIdx = existing.indexOf(CODIEDEV_INSTRUCTIONS_BEGIN);
353
+ const endIdx = existing.indexOf(CODIEDEV_INSTRUCTIONS_END);
354
+ let next;
355
+ if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
356
+ next =
357
+ existing.slice(0, beginIdx) +
358
+ CODIEDEV_INSTRUCTIONS_BEGIN +
359
+ "\n" +
360
+ CODIEDEV_INSTRUCTIONS_BODY +
361
+ "\n" +
362
+ CODIEDEV_INSTRUCTIONS_END +
363
+ existing.slice(endIdx + CODIEDEV_INSTRUCTIONS_END.length);
364
+ }
365
+ else {
366
+ next = block;
367
+ }
368
+ fs.writeFileSync(CURSOR_RULES_PATH, next, "utf8");
369
+ }
370
+ function readMcpConfigSafely(path) {
371
+ if (!fs.existsSync(path))
372
+ return {};
373
+ const raw = fs.readFileSync(path, "utf8");
374
+ if (!raw.trim())
375
+ return {};
376
+ try {
377
+ return JSON.parse(raw);
378
+ }
379
+ catch {
380
+ const backup = `${path}.bak.${Date.now()}`;
381
+ fs.copyFileSync(path, backup);
382
+ console.warn(`Warning: ${path} contained invalid JSON. Backed up to ${backup} before rewriting.`);
383
+ return {};
384
+ }
385
+ }
314
386
  /**
315
387
  * Install the CodieDev MCP server into Claude Code's user-scope config.
316
388
  * Safe to call multiple times — updates the existing entry if present.
317
389
  */
318
390
  function installClaudeCodeMcp() {
319
- let config = {};
320
- try {
321
- if (fs.existsSync(CLAUDE_USER_CONFIG_PATH)) {
322
- const raw = fs.readFileSync(CLAUDE_USER_CONFIG_PATH, "utf8");
323
- config = raw.trim() ? JSON.parse(raw) : {};
324
- }
325
- }
326
- catch {
327
- // Start fresh if the existing file is corrupt — safer than failing the install.
328
- config = {};
329
- }
391
+ const config = readMcpConfigSafely(CLAUDE_USER_CONFIG_PATH);
330
392
  const mcpServers = config.mcpServers ?? {};
331
- // Canonical Claude Code user-scope format: just command + args.
332
- // No `type` field (some CC versions reject it; stdio is the default).
393
+ if (mcpServers.codiedev)
394
+ return;
333
395
  mcpServers.codiedev = {
334
396
  command: "npx",
335
397
  args: ["codiedev-mcp"],
@@ -337,6 +399,27 @@ function installClaudeCodeMcp() {
337
399
  config.mcpServers = mcpServers;
338
400
  fs.writeFileSync(CLAUDE_USER_CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
339
401
  }
402
+ /**
403
+ * Install the CodieDev MCP server into Cursor's user-scope config.
404
+ * Cursor uses the same `mcpServers` schema as Claude Code; just a different
405
+ * file path (~/.cursor/mcp.json). Project-scoped servers can also live at
406
+ * <project>/.cursor/mcp.json — we only manage the user-scope one.
407
+ */
408
+ function installCursorMcp() {
409
+ if (!fs.existsSync(CURSOR_DIR)) {
410
+ fs.mkdirSync(CURSOR_DIR, { recursive: true });
411
+ }
412
+ const config = readMcpConfigSafely(CURSOR_MCP_PATH);
413
+ const mcpServers = config.mcpServers ?? {};
414
+ if (mcpServers.codiedev)
415
+ return;
416
+ mcpServers.codiedev = {
417
+ command: "npx",
418
+ args: ["codiedev-mcp"],
419
+ };
420
+ config.mcpServers = mcpServers;
421
+ fs.writeFileSync(CURSOR_MCP_PATH, JSON.stringify(config, null, 2), "utf8");
422
+ }
340
423
  /**
341
424
  * Best-effort append of the CodieDev MCP server block to ~/.codex/config.toml.
342
425
  *
@@ -413,6 +496,49 @@ function installCodexHook() {
413
496
  }
414
497
  fs.writeFileSync(CODEX_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
415
498
  }
499
+ // Cursor's hooks.json uses { version: 1, hooks: { sessionEnd: [{ command, type, timeout }] } }.
500
+ // `sessionEnd` fires when a conversation closes (completed, aborted, error,
501
+ // window_close, user_close); the base stdin payload includes transcript_path
502
+ // and session_id which `codiedev-hook capture-cursor` reads to upload.
503
+ function installCursorHook() {
504
+ if (!fs.existsSync(CURSOR_DIR)) {
505
+ fs.mkdirSync(CURSOR_DIR, { recursive: true });
506
+ }
507
+ let hooksFile = {};
508
+ try {
509
+ if (fs.existsSync(CURSOR_HOOKS_PATH)) {
510
+ const raw = fs.readFileSync(CURSOR_HOOKS_PATH, "utf8");
511
+ hooksFile = raw.trim() ? JSON.parse(raw) : {};
512
+ }
513
+ }
514
+ catch {
515
+ hooksFile = {};
516
+ }
517
+ if (!hooksFile.version) {
518
+ hooksFile.version = 1;
519
+ }
520
+ if (!hooksFile.hooks) {
521
+ hooksFile.hooks = {};
522
+ }
523
+ const hooks = hooksFile.hooks;
524
+ if (!hooks.sessionEnd) {
525
+ hooks.sessionEnd = [];
526
+ }
527
+ const sessionEndHooks = hooks.sessionEnd;
528
+ const alreadyInstalled = sessionEndHooks.some((h) => {
529
+ const cmd = h.command;
530
+ return cmd && cmd.includes("codiedev-hook");
531
+ });
532
+ if (alreadyInstalled) {
533
+ return;
534
+ }
535
+ sessionEndHooks.push({
536
+ command: "npx codiedev-hook capture-cursor",
537
+ type: "command",
538
+ timeout: 30,
539
+ });
540
+ fs.writeFileSync(CURSOR_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
541
+ }
416
542
  function parseClaudeCodeStats(content) {
417
543
  let messageCount = 0;
418
544
  let toolCallCount = 0;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.5.5",
4
- "description": "Connect Claude Code or Codex to CodieDev for org-wide session capture and artifact collaboration",
3
+ "version": "0.6.0",
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
  ],