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.
- package/dist/commands/doctor.js +83 -2
- package/dist/connect.js +40 -2
- package/dist/hook.js +15 -4
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +183 -41
- package/package.json +3 -2
package/dist/commands/doctor.js
CHANGED
|
@@ -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
|
|
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.
|
|
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)
|
|
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;
|
|
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)(
|
|
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
|
-
:
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
211
|
+
return !isCodiedevHookCommand(hook.matcher);
|
|
171
212
|
});
|
|
172
|
-
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
sessionEndHooks.push({
|
|
213
|
+
filtered.push({
|
|
176
214
|
matcher: ".*",
|
|
177
215
|
hooks: [
|
|
178
216
|
{
|
|
179
217
|
type: "command",
|
|
180
|
-
command: "
|
|
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
|
391
|
-
const
|
|
499
|
+
const existing = hooks.Stop;
|
|
500
|
+
const filtered = existing.filter((hook) => {
|
|
392
501
|
const inner = hook.hooks;
|
|
393
|
-
if (!inner)
|
|
394
|
-
return
|
|
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
|
-
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
stopHooks.push({
|
|
506
|
+
filtered.push({
|
|
404
507
|
hooks: [
|
|
405
508
|
{
|
|
406
509
|
type: "command",
|
|
407
|
-
command: "
|
|
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.
|
|
4
|
-
"description": "Connect Claude Code or
|
|
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
|
],
|