codiedev 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # codiedev
2
2
 
3
- Connect Claude Code to CodieDev for org-wide session capture.
3
+ Connect Claude Code or Codex to CodieDev for org-wide session capture.
4
4
 
5
5
  ## Usage
6
6
 
@@ -8,7 +8,12 @@ Connect Claude Code to CodieDev for org-wide session capture.
8
8
  npx codiedev connect
9
9
  ```
10
10
 
11
- The CLI will prompt you for an API token (get one at https://codiedev.com/portal/integrations/claude-code), then install a Claude Code `SessionEnd` hook that uploads transcripts from your tracked repos to your CodieDev workspace.
11
+ The CLI will prompt you for an API token (get one at https://codiedev.com/portal/integrations/claude-code), then install a capture hook into whichever agent CLIs are present on your machine:
12
+
13
+ - **Claude Code** — `SessionEnd` hook in `~/.claude/settings.json`
14
+ - **Codex** — `Stop` hook in `~/.codex/hooks.json`
15
+
16
+ If both are installed, hooks are wired up for both. If neither is installed, the config is saved and you can re-run `connect` after installing one.
12
17
 
13
18
  ## What gets captured
14
19
 
@@ -16,8 +21,12 @@ The CLI will prompt you for an API token (get one at https://codiedev.com/portal
16
21
  - Transcript + basic stats (message count, tool calls)
17
22
  - Processed into summaries and embeddings server-side for search in `/portal/memories`
18
23
 
19
- ## Config
24
+ Each captured memory records its `source` (`claude-code` or `codex`) so you can filter in the portal.
20
25
 
21
- Written to `~/.codiedev/config.json`. The `SessionEnd` hook is added to `~/.claude/settings.json`.
26
+ ## Codex note
27
+
28
+ 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).
29
+
30
+ ## Config
22
31
 
23
- Override the backend with `CODIEDEV_URL` if needed.
32
+ Written to `~/.codiedev/config.json`. Override the backend with `CODIEDEV_URL` if needed.
package/dist/connect.js CHANGED
@@ -139,17 +139,43 @@ async function main() {
139
139
  console.error(`\nError: Failed to save config — ${err.message}`);
140
140
  process.exit(1);
141
141
  }
142
- try {
143
- (0, utils_1.installHook)();
142
+ const hasClaude = (0, utils_1.claudeCodeInstalled)();
143
+ const hasCodex = (0, utils_1.codexInstalled)();
144
+ const installed = [];
145
+ if (hasClaude) {
146
+ try {
147
+ (0, utils_1.installHook)();
148
+ installed.push("Claude Code (SessionEnd → ~/.claude/settings.json)");
149
+ }
150
+ catch (err) {
151
+ console.error(`\nWarning: Failed to install Claude Code hook — ${err.message}`);
152
+ }
144
153
  }
145
- catch (err) {
146
- console.error(`\nWarning: Failed to install SessionEnd hook — ${err.message}`);
147
- console.error("You can install it manually by adding npx codiedev-hook capture to your ~/.claude/settings.json hooks.");
148
- // Don't exit — config was saved successfully
154
+ if (hasCodex) {
155
+ try {
156
+ (0, utils_1.installCodexHook)();
157
+ installed.push("Codex (Stop ~/.codex/hooks.json)");
158
+ }
159
+ catch (err) {
160
+ console.error(`\nWarning: Failed to install Codex hook — ${err.message}`);
161
+ }
162
+ }
163
+ if (!hasClaude && !hasCodex) {
164
+ console.warn("\nNo Claude Code (~/.claude) or Codex (~/.codex) install detected.");
165
+ console.warn("Config saved. Install one, then re-run `npx codiedev connect` to wire up capture hooks.");
149
166
  }
150
167
  console.log(`\nConnected to ${companyName}`);
151
168
  console.log(`Tracking ${repos.length} repo${repos.length === 1 ? "" : "s"}`);
152
- console.log("SessionEnd hook installed sessions will be captured automatically.\n");
169
+ if (installed.length > 0) {
170
+ console.log("Installed capture hook(s):");
171
+ for (const target of installed) {
172
+ console.log(` - ${target}`);
173
+ }
174
+ console.log("Sessions will be captured automatically.\n");
175
+ }
176
+ else {
177
+ console.log();
178
+ }
153
179
  }
154
180
  main().catch((err) => {
155
181
  console.error("Unexpected error:", err);
package/dist/hook.js CHANGED
@@ -73,41 +73,6 @@ function postJson(url, body, bearerToken) {
73
73
  req.end();
74
74
  });
75
75
  }
76
- function parseTranscriptStats(transcriptPath) {
77
- let messageCount = 0;
78
- let toolCallCount = 0;
79
- try {
80
- const content = fs.readFileSync(transcriptPath, "utf8");
81
- const lines = content.split("\n").filter((l) => l.trim());
82
- for (const line of lines) {
83
- try {
84
- const entry = JSON.parse(line);
85
- if (!entry || typeof entry !== "object")
86
- continue;
87
- // Claude Code transcript entries have type: "user" or "assistant" for messages
88
- if (entry.type === "user" || entry.type === "assistant") {
89
- messageCount++;
90
- }
91
- // Tool calls live in the message.content array as blocks with type "tool_use"
92
- const content = entry.message?.content;
93
- if (Array.isArray(content)) {
94
- for (const block of content) {
95
- if (block && block.type === "tool_use") {
96
- toolCallCount++;
97
- }
98
- }
99
- }
100
- }
101
- catch {
102
- // Skip malformed lines
103
- }
104
- }
105
- }
106
- catch {
107
- // If we can't read/parse, return zeros
108
- }
109
- return { messageCount, toolCallCount };
110
- }
111
76
  async function readStdin() {
112
77
  return new Promise((resolve, reject) => {
113
78
  let data = "";
@@ -119,8 +84,13 @@ async function readStdin() {
119
84
  process.stdin.on("error", reject);
120
85
  });
121
86
  }
87
+ function resolveMode(arg) {
88
+ if (arg === "capture-codex")
89
+ return "codex";
90
+ return "claude-code";
91
+ }
122
92
  async function main() {
123
- // Read hook input from stdin
93
+ const mode = resolveMode(process.argv[2]);
124
94
  let hookInput = {};
125
95
  try {
126
96
  const raw = await readStdin();
@@ -129,67 +99,61 @@ async function main() {
129
99
  }
130
100
  }
131
101
  catch {
132
- // If stdin is malformed, exit silently
133
102
  process.exit(0);
134
103
  }
135
104
  const { session_id, transcript_path, cwd } = hookInput;
136
- // Read config — if missing, exit silently
137
105
  const config = (0, utils_1.readConfig)();
138
106
  if (!config) {
139
107
  process.exit(0);
140
108
  }
141
- // Determine working directory
142
- const workingDir = cwd || process.cwd();
143
- // Get git remote URL for this directory
144
- const remoteUrl = (0, utils_1.getGitRemoteUrl)(workingDir);
145
- if (!remoteUrl) {
146
- // Not a git repo or no remote — exit silently
147
- process.exit(0);
148
- }
149
- // Check if this repo is in our tracked repos
150
- const matchedRepo = (0, utils_1.matchRepo)(remoteUrl, config.repos);
151
- if (!matchedRepo) {
152
- // Repo not tracked — exit silently
153
- process.exit(0);
154
- }
155
- // Validate transcript path
156
109
  if (!transcript_path) {
157
110
  process.exit(0);
158
111
  }
159
- // Read transcript file
160
112
  let transcriptContent;
161
113
  try {
162
114
  transcriptContent = fs.readFileSync(transcript_path, "utf8");
163
115
  }
164
116
  catch {
165
- // Can't read transcript — exit silently
166
117
  process.exit(0);
167
118
  }
168
- // Parse transcript stats
169
- const { messageCount, toolCallCount } = parseTranscriptStats(transcript_path);
170
- // Base64 encode transcript
119
+ // Resolve repo URL. Codex embeds it in session_meta; fall back to git for CC.
120
+ let remoteUrl = null;
121
+ if (mode === "codex") {
122
+ remoteUrl = (0, utils_1.extractCodexRepoUrl)(transcriptContent);
123
+ }
124
+ if (!remoteUrl) {
125
+ remoteUrl = (0, utils_1.getGitRemoteUrl)(cwd || process.cwd());
126
+ }
127
+ if (!remoteUrl) {
128
+ process.exit(0);
129
+ }
130
+ const matchedRepo = (0, utils_1.matchRepo)(remoteUrl, config.repos);
131
+ if (!matchedRepo) {
132
+ process.exit(0);
133
+ }
134
+ const stats = mode === "codex"
135
+ ? (0, utils_1.parseCodexStats)(transcriptContent)
136
+ : (0, utils_1.parseClaudeCodeStats)(transcriptContent);
171
137
  const transcriptBase64 = Buffer.from(transcriptContent, "utf8").toString("base64");
172
- // POST to backend
173
138
  const payload = {
174
139
  sessionId: session_id,
175
140
  repoUrl: remoteUrl,
176
141
  repoId: matchedRepo.repoId,
177
142
  companyId: config.companyId,
178
143
  transcript: transcriptBase64,
179
- messageCount,
180
- toolCallCount,
144
+ messageCount: stats.messageCount,
145
+ toolCallCount: stats.toolCallCount,
181
146
  capturedAt: Date.now(),
147
+ source: mode,
182
148
  };
183
149
  try {
184
150
  await postJson(`${config.backendUrl}/api/captureSession`, payload, config.token);
185
151
  }
186
152
  catch (err) {
187
- // Write warning to stderr, but exit 0 — never block Claude Code
188
153
  process.stderr.write(`[codiedev-hook] Warning: Failed to capture session — ${err.message}\n`);
189
154
  }
190
155
  process.exit(0);
191
156
  }
192
157
  main().catch(() => {
193
- // Never block Claude Code on any error
194
158
  process.exit(0);
195
159
  });
package/dist/utils.d.ts CHANGED
@@ -24,4 +24,14 @@ export declare function matchRepo(remoteUrl: string, repos: Array<{
24
24
  fullName: string;
25
25
  } | null;
26
26
  export declare function hashToken(token: string): string;
27
+ export declare function claudeCodeInstalled(): boolean;
28
+ export declare function codexInstalled(): boolean;
27
29
  export declare function installHook(): void;
30
+ export declare function installCodexHook(): void;
31
+ export interface ParsedStats {
32
+ messageCount: number;
33
+ toolCallCount: number;
34
+ }
35
+ export declare function parseClaudeCodeStats(content: string): ParsedStats;
36
+ export declare function parseCodexStats(content: string): ParsedStats;
37
+ export declare function extractCodexRepoUrl(content: string): string | null;
package/dist/utils.js CHANGED
@@ -39,7 +39,13 @@ exports.getGitRemoteUrl = getGitRemoteUrl;
39
39
  exports.normalizeRepoUrl = normalizeRepoUrl;
40
40
  exports.matchRepo = matchRepo;
41
41
  exports.hashToken = hashToken;
42
+ exports.claudeCodeInstalled = claudeCodeInstalled;
43
+ exports.codexInstalled = codexInstalled;
42
44
  exports.installHook = installHook;
45
+ exports.installCodexHook = installCodexHook;
46
+ exports.parseClaudeCodeStats = parseClaudeCodeStats;
47
+ exports.parseCodexStats = parseCodexStats;
48
+ exports.extractCodexRepoUrl = extractCodexRepoUrl;
43
49
  const fs = __importStar(require("fs"));
44
50
  const path = __importStar(require("path"));
45
51
  const os = __importStar(require("os"));
@@ -120,6 +126,15 @@ function hashToken(token) {
120
126
  return hash.toString(36);
121
127
  }
122
128
  const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
129
+ const CLAUDE_DIR = path.join(os.homedir(), ".claude");
130
+ const CODEX_HOOKS_PATH = path.join(os.homedir(), ".codex", "hooks.json");
131
+ const CODEX_DIR = path.join(os.homedir(), ".codex");
132
+ function claudeCodeInstalled() {
133
+ return fs.existsSync(CLAUDE_DIR);
134
+ }
135
+ function codexInstalled() {
136
+ return fs.existsSync(CODEX_DIR);
137
+ }
123
138
  function installHook() {
124
139
  let settings = {};
125
140
  try {
@@ -131,17 +146,14 @@ function installHook() {
131
146
  catch {
132
147
  // Start with empty settings if file doesn't exist or is invalid
133
148
  }
134
- // Ensure hooks object exists
135
149
  if (!settings.hooks) {
136
150
  settings.hooks = {};
137
151
  }
138
152
  const hooks = settings.hooks;
139
- // Ensure SessionEnd array exists
140
153
  if (!hooks.SessionEnd) {
141
154
  hooks.SessionEnd = [];
142
155
  }
143
156
  const sessionEndHooks = hooks.SessionEnd;
144
- // Check if codiedev-hook is already installed
145
157
  const alreadyInstalled = sessionEndHooks.some((hook) => {
146
158
  const matcher = hook.matcher;
147
159
  const hooks2 = hook.hooks;
@@ -156,7 +168,6 @@ function installHook() {
156
168
  if (alreadyInstalled) {
157
169
  return;
158
170
  }
159
- // Add the codiedev-hook capture entry
160
171
  sessionEndHooks.push({
161
172
  matcher: ".*",
162
173
  hooks: [
@@ -166,10 +177,137 @@ function installHook() {
166
177
  },
167
178
  ],
168
179
  });
169
- // Ensure directory exists
170
- const settingsDir = path.dirname(CLAUDE_SETTINGS_PATH);
171
- if (!fs.existsSync(settingsDir)) {
172
- fs.mkdirSync(settingsDir, { recursive: true });
180
+ if (!fs.existsSync(CLAUDE_DIR)) {
181
+ fs.mkdirSync(CLAUDE_DIR, { recursive: true });
173
182
  }
174
183
  fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf8");
175
184
  }
185
+ // Codex doesn't have a SessionEnd event yet — Stop fires after every turn.
186
+ // The backend dedupes on (companyId, sessionId) and replaces the stored
187
+ // transcript on re-upload, so the latest turn's snapshot wins.
188
+ function installCodexHook() {
189
+ let hooksFile = {};
190
+ try {
191
+ if (fs.existsSync(CODEX_HOOKS_PATH)) {
192
+ const raw = fs.readFileSync(CODEX_HOOKS_PATH, "utf8");
193
+ hooksFile = JSON.parse(raw);
194
+ }
195
+ }
196
+ catch {
197
+ // Start fresh if missing or invalid
198
+ }
199
+ if (!hooksFile.hooks) {
200
+ hooksFile.hooks = {};
201
+ }
202
+ const hooks = hooksFile.hooks;
203
+ if (!hooks.Stop) {
204
+ hooks.Stop = [];
205
+ }
206
+ const stopHooks = hooks.Stop;
207
+ const alreadyInstalled = stopHooks.some((hook) => {
208
+ const inner = hook.hooks;
209
+ if (!inner)
210
+ return false;
211
+ return inner.some((h) => {
212
+ const cmd = h.command;
213
+ return cmd && cmd.includes("codiedev-hook");
214
+ });
215
+ });
216
+ if (alreadyInstalled) {
217
+ return;
218
+ }
219
+ stopHooks.push({
220
+ hooks: [
221
+ {
222
+ type: "command",
223
+ command: "npx codiedev-hook capture-codex",
224
+ timeout: 30,
225
+ },
226
+ ],
227
+ });
228
+ if (!fs.existsSync(CODEX_DIR)) {
229
+ fs.mkdirSync(CODEX_DIR, { recursive: true });
230
+ }
231
+ fs.writeFileSync(CODEX_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
232
+ }
233
+ function parseClaudeCodeStats(content) {
234
+ let messageCount = 0;
235
+ let toolCallCount = 0;
236
+ const lines = content.split("\n").filter((l) => l.trim());
237
+ for (const line of lines) {
238
+ try {
239
+ const entry = JSON.parse(line);
240
+ if (!entry || typeof entry !== "object")
241
+ continue;
242
+ if (entry.type === "user" || entry.type === "assistant") {
243
+ messageCount++;
244
+ }
245
+ const c = entry.message?.content;
246
+ if (Array.isArray(c)) {
247
+ for (const block of c) {
248
+ if (block && block.type === "tool_use")
249
+ toolCallCount++;
250
+ }
251
+ }
252
+ }
253
+ catch {
254
+ // Skip malformed lines
255
+ }
256
+ }
257
+ return { messageCount, toolCallCount };
258
+ }
259
+ // Codex session JSONL format (rollout-*.jsonl):
260
+ // {type:"session_meta", payload:{id, cwd, git:{repository_url,...}, ...}}
261
+ // {type:"response_item", payload:{type:"message", role, content:[{type:"input_text",text}|{type:"output_text",text}]}}
262
+ // {type:"response_item", payload:{type:"function_call", name, arguments, call_id}}
263
+ // {type:"response_item", payload:{type:"function_call_output", call_id, output}}
264
+ // {type:"event_msg", payload:{type:"token_count"|"agent_reasoning"|...}}
265
+ // {type:"turn_context", payload:{cwd, model, ...}}
266
+ function parseCodexStats(content) {
267
+ let messageCount = 0;
268
+ let toolCallCount = 0;
269
+ const lines = content.split("\n").filter((l) => l.trim());
270
+ for (const line of lines) {
271
+ try {
272
+ const entry = JSON.parse(line);
273
+ if (!entry || typeof entry !== "object")
274
+ continue;
275
+ if (entry.type !== "response_item")
276
+ continue;
277
+ const payloadType = entry.payload?.type;
278
+ if (payloadType === "message") {
279
+ messageCount++;
280
+ }
281
+ else if (payloadType === "function_call") {
282
+ toolCallCount++;
283
+ }
284
+ }
285
+ catch {
286
+ // Skip malformed lines
287
+ }
288
+ }
289
+ return { messageCount, toolCallCount };
290
+ }
291
+ // Pull repository URL from the Codex transcript's session_meta line so we
292
+ // don't depend on the hook's cwd being a git repo.
293
+ function extractCodexRepoUrl(content) {
294
+ const lines = content.split("\n");
295
+ for (const line of lines) {
296
+ const trimmed = line.trim();
297
+ if (!trimmed)
298
+ continue;
299
+ try {
300
+ const entry = JSON.parse(trimmed);
301
+ if (entry?.type === "session_meta") {
302
+ const url = entry.payload?.git?.repository_url;
303
+ if (typeof url === "string" && url.length > 0)
304
+ return url;
305
+ return null; // session_meta is first — no point scanning further
306
+ }
307
+ }
308
+ catch {
309
+ // Keep scanning
310
+ }
311
+ }
312
+ return null;
313
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.1.0",
4
- "description": "Connect Claude Code to CodieDev for org-wide session capture",
3
+ "version": "0.2.0",
4
+ "description": "Connect Claude Code or Codex to CodieDev for org-wide session capture",
5
5
  "bin": {
6
6
  "codiedev": "./dist/connect.js",
7
7
  "codiedev-hook": "./dist/hook.js"
@@ -16,6 +16,8 @@
16
16
  "codiedev",
17
17
  "claude-code",
18
18
  "claude",
19
+ "codex",
20
+ "openai",
19
21
  "ai",
20
22
  "session-capture"
21
23
  ],