memhouse 0.0.1 → 0.3.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.
Files changed (96) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +59 -3
  3. package/bin/memhouse.js +487 -0
  4. package/cache.js +850 -0
  5. package/ch-ingest.js +329 -0
  6. package/ch-schema.js +152 -0
  7. package/clickhouse.js +87 -0
  8. package/editors/antigravity.js +1019 -0
  9. package/editors/base.js +321 -0
  10. package/editors/claude.js +386 -0
  11. package/editors/codebuff.js +338 -0
  12. package/editors/codex.js +537 -0
  13. package/editors/commandcode.js +166 -0
  14. package/editors/copilot-jetbrains.js +207 -0
  15. package/editors/copilot.js +260 -0
  16. package/editors/cursor-agent.js +202 -0
  17. package/editors/cursor.js +438 -0
  18. package/editors/gemini.js +231 -0
  19. package/editors/goose.js +358 -0
  20. package/editors/gsd.js +366 -0
  21. package/editors/index.js +179 -0
  22. package/editors/kiro.js +315 -0
  23. package/editors/opencode.js +374 -0
  24. package/editors/vscode.js +413 -0
  25. package/editors/windsurf.js +634 -0
  26. package/editors/zed.js +213 -0
  27. package/index.js +424 -2
  28. package/mcp-server.js +294 -0
  29. package/mem-house/DESIGN.md +119 -0
  30. package/mem-house/delivery/AGENT-INSTALL.md +75 -0
  31. package/mem-house/delivery/PROMPT.md +42 -0
  32. package/mem-house/delivery/install.sh +181 -0
  33. package/mem-house/delivery/kernel-install.md +76 -0
  34. package/mem-house/delivery/plugin/.claude-plugin/plugin.json +8 -0
  35. package/mem-house/delivery/skills/memhouse-search/SKILL.md +104 -0
  36. package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +94 -0
  37. package/mem-house/delivery/skills/memhouse-sql/SKILL.md +115 -0
  38. package/mem-house/rls.sql +17 -0
  39. package/mem-house/schema.sql +145 -0
  40. package/mem-house/server/queries.js +878 -0
  41. package/mem-house/server/server.js +228 -0
  42. package/mem-house/shipper/ship.js +402 -0
  43. package/misc/syntax-check.js +38 -0
  44. package/package.json +56 -7
  45. package/pricing.js +88 -0
  46. package/pricing.json +992 -0
  47. package/public/assets/index-BsgjOSTv.js +75 -0
  48. package/public/assets/index-DV6ONi_F.css +2 -0
  49. package/public/index.html +16 -0
  50. package/relay-client.js +306 -0
  51. package/relay-server.js +577 -0
  52. package/server.js +860 -0
  53. package/share-image.js +367 -0
  54. package/ui/eslint.config.js +29 -0
  55. package/ui/index.html +15 -0
  56. package/ui/package-lock.json +4229 -0
  57. package/ui/package.json +38 -0
  58. package/ui/src/App.jsx +364 -0
  59. package/ui/src/components/ActivityHeatmap.jsx +206 -0
  60. package/ui/src/components/AiAuditCard.jsx +254 -0
  61. package/ui/src/components/AnimatedLoader.jsx +14 -0
  62. package/ui/src/components/AnimatedLogo.jsx +96 -0
  63. package/ui/src/components/ChatSidebar.jsx +226 -0
  64. package/ui/src/components/DateRangePicker.jsx +104 -0
  65. package/ui/src/components/EditorDot.jsx +11 -0
  66. package/ui/src/components/EditorIcon.jsx +117 -0
  67. package/ui/src/components/KpiCard.jsx +9 -0
  68. package/ui/src/components/LiveFeed.jsx +128 -0
  69. package/ui/src/components/LoginScreen.jsx +81 -0
  70. package/ui/src/components/MessageRenderer.jsx +148 -0
  71. package/ui/src/components/PageHeader.jsx +11 -0
  72. package/ui/src/components/SectionTitle.jsx +3 -0
  73. package/ui/src/components/ShareModal.jsx +305 -0
  74. package/ui/src/components/TokenTimeline.jsx +258 -0
  75. package/ui/src/hooks/useLive.jsx +15 -0
  76. package/ui/src/index.css +83 -0
  77. package/ui/src/lib/api.js +343 -0
  78. package/ui/src/lib/constants.js +88 -0
  79. package/ui/src/lib/theme.jsx +23 -0
  80. package/ui/src/main.jsx +16 -0
  81. package/ui/src/pages/Artifacts.jsx +575 -0
  82. package/ui/src/pages/Compare.jsx +232 -0
  83. package/ui/src/pages/CostAnalysis.jsx +354 -0
  84. package/ui/src/pages/Dashboard.jsx +529 -0
  85. package/ui/src/pages/DeepAnalysis.jsx +495 -0
  86. package/ui/src/pages/GSD.jsx +726 -0
  87. package/ui/src/pages/MCPs.jsx +744 -0
  88. package/ui/src/pages/ProjectDetail.jsx +325 -0
  89. package/ui/src/pages/Projects.jsx +249 -0
  90. package/ui/src/pages/RelayDashboard.jsx +590 -0
  91. package/ui/src/pages/RelayUserDetail.jsx +466 -0
  92. package/ui/src/pages/Sessions.jsx +535 -0
  93. package/ui/src/pages/Settings.jsx +241 -0
  94. package/ui/src/pages/SqlViewer.jsx +344 -0
  95. package/ui/src/pages/Subscriptions.jsx +411 -0
  96. package/ui/vite.config.js +18 -0
@@ -0,0 +1,537 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+
5
+ const name = 'codex';
6
+ const DEFAULT_CODEX_HOME = path.join(os.homedir(), '.codex');
7
+ const SESSION_SUBDIR = 'sessions';
8
+ const ARCHIVED_SESSION_SUBDIR = 'archived_sessions';
9
+ const MAX_TOOL_RESULT_PREVIEW = 500;
10
+
11
+ function getChats() {
12
+ const dirs = [getSessionsDir(), getArchivedSessionsDir()];
13
+ const seen = new Set();
14
+ const chats = [];
15
+
16
+ for (const dir of dirs) {
17
+ if (!fs.existsSync(dir)) continue;
18
+ for (const filePath of walkJsonlFiles(dir)) {
19
+ const chat = readChatMetadata(filePath);
20
+ if (!chat) continue;
21
+ if (seen.has(chat.composerId)) continue;
22
+ seen.add(chat.composerId);
23
+ chats.push(chat);
24
+ }
25
+ }
26
+
27
+ return chats;
28
+ }
29
+
30
+ function getMessages(chat) {
31
+ const filePath = chat && chat._filePath;
32
+ if (!filePath || !fs.existsSync(filePath)) return [];
33
+ return parseSessionMessages(filePath);
34
+ }
35
+
36
+ function getCodexHome() {
37
+ return process.env.CODEX_HOME && process.env.CODEX_HOME.trim()
38
+ ? path.resolve(process.env.CODEX_HOME.trim())
39
+ : DEFAULT_CODEX_HOME;
40
+ }
41
+
42
+ function getSessionsDir() {
43
+ return path.join(getCodexHome(), SESSION_SUBDIR);
44
+ }
45
+
46
+ function getArchivedSessionsDir() {
47
+ return path.join(getCodexHome(), ARCHIVED_SESSION_SUBDIR);
48
+ }
49
+
50
+ function walkJsonlFiles(dir) {
51
+ const results = [];
52
+ const stack = [dir];
53
+
54
+ while (stack.length > 0) {
55
+ const current = stack.pop();
56
+ let entries;
57
+ try {
58
+ entries = fs.readdirSync(current, { withFileTypes: true });
59
+ } catch {
60
+ continue;
61
+ }
62
+
63
+ for (const entry of entries) {
64
+ const fullPath = path.join(current, entry.name);
65
+ if (entry.isDirectory()) {
66
+ stack.push(fullPath);
67
+ } else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
68
+ results.push(fullPath);
69
+ }
70
+ }
71
+ }
72
+
73
+ return results.sort();
74
+ }
75
+
76
+ function readChatMetadata(filePath) {
77
+ const lines = readLines(filePath);
78
+ if (lines.length === 0) return null;
79
+
80
+ const first = safeParseJson(lines[0]);
81
+ if (!first || first.type !== 'session_meta' || !first.payload) return null;
82
+
83
+ let title = null;
84
+ for (let i = 1; i < lines.length; i++) {
85
+ const entry = safeParseJson(lines[i]);
86
+ if (!entry || entry.type !== 'response_item' || !entry.payload) continue;
87
+ const payload = entry.payload;
88
+ if (payload.type !== 'message' || payload.role !== 'user') continue;
89
+ const text = extractUserText(payload.content);
90
+ if (!text || isBootstrapMessage(text)) continue;
91
+ title = cleanPrompt(text);
92
+ break;
93
+ }
94
+
95
+ let stat;
96
+ try {
97
+ stat = fs.statSync(filePath);
98
+ } catch {
99
+ stat = null;
100
+ }
101
+
102
+ const payload = first.payload;
103
+ return {
104
+ source: 'codex',
105
+ composerId: payload.id || path.basename(filePath, '.jsonl'),
106
+ name: title,
107
+ createdAt: toTimestamp(payload.timestamp) || (stat ? stat.birthtimeMs : null),
108
+ lastUpdatedAt: stat ? stat.mtimeMs : null,
109
+ mode: 'codex',
110
+ folder: payload.cwd || null,
111
+ encrypted: false,
112
+ bubbleCount: 0,
113
+ _filePath: filePath,
114
+ _rawSource: payload.source || null,
115
+ _originator: payload.originator || null,
116
+ _cliVersion: payload.cli_version || null,
117
+ _modelProvider: payload.model_provider || null,
118
+ };
119
+ }
120
+
121
+ function parseSessionMessages(filePath) {
122
+ const lines = readLines(filePath);
123
+ const messages = [];
124
+
125
+ let currentModel = null;
126
+ let previousTotals = null;
127
+ let currentTurn = createTurnState();
128
+ let turnHasStarted = false;
129
+ const toolNamesByCallId = new Map();
130
+
131
+ function flushTurn() {
132
+ const hasAssistantContent = currentTurn.parts.length > 0;
133
+ const hasTokens = currentTurn.inputTokens > 0 || currentTurn.outputTokens > 0 || currentTurn.cacheRead > 0;
134
+ const hasTools = currentTurn.toolCalls.length > 0;
135
+ if (!hasAssistantContent && !hasTokens && !hasTools) {
136
+ currentTurn = createTurnState();
137
+ toolNamesByCallId.clear();
138
+ return;
139
+ }
140
+
141
+ messages.push(composeAssistantMessage(currentTurn));
142
+ currentTurn = createTurnState();
143
+ toolNamesByCallId.clear();
144
+ }
145
+
146
+ for (const line of lines) {
147
+ const entry = safeParseJson(line);
148
+ if (!entry) continue;
149
+
150
+ if (entry.type === 'turn_context') {
151
+ if (turnHasStarted) flushTurn();
152
+ turnHasStarted = true;
153
+ const model = extractModel(entry.payload);
154
+ if (model) {
155
+ currentModel = model;
156
+ currentTurn.model = model;
157
+ }
158
+ continue;
159
+ }
160
+
161
+ if (entry.type === 'response_item' && entry.payload) {
162
+ const payload = entry.payload;
163
+
164
+ if (payload.type === 'message') {
165
+ if (payload.role === 'user') {
166
+ const text = extractUserText(payload.content);
167
+ if (!text || isBootstrapMessage(text)) continue;
168
+ if (turnHasStarted) flushTurn();
169
+ messages.push({ role: 'user', content: text });
170
+ } else if (payload.role === 'assistant') {
171
+ const text = extractAssistantText(payload.content);
172
+ if (text) currentTurn.parts.push(text);
173
+ if (!currentTurn.model && currentModel) currentTurn.model = currentModel;
174
+ } else if (payload.role === 'system') {
175
+ const text = extractAssistantText(payload.content);
176
+ if (text) messages.push({ role: 'system', content: text });
177
+ }
178
+ continue;
179
+ }
180
+
181
+ if (payload.type === 'reasoning') {
182
+ const summary = extractReasoningSummary(payload);
183
+ if (summary) currentTurn.parts.push(summary);
184
+ continue;
185
+ }
186
+
187
+ if (isToolCallPayload(payload.type)) {
188
+ const toolCall = normalizeToolCall(payload);
189
+ currentTurn.parts.push(toolCall.line);
190
+ currentTurn.toolCalls.push({ name: toolCall.name, args: toolCall.args });
191
+ if (payload.call_id) toolNamesByCallId.set(payload.call_id, toolCall.name);
192
+ continue;
193
+ }
194
+
195
+ if (isToolOutputPayload(payload.type)) {
196
+ const lineText = normalizeToolResult(payload, toolNamesByCallId.get(payload.call_id));
197
+ if (lineText) currentTurn.parts.push(lineText);
198
+ continue;
199
+ }
200
+
201
+ continue;
202
+ }
203
+
204
+ if (entry.type === 'event_msg' && entry.payload && entry.payload.type === 'token_count') {
205
+ const tokenInfo = entry.payload.info || {};
206
+ const lastUsage = normalizeRawUsage(tokenInfo.last_token_usage);
207
+ const totalUsage = normalizeRawUsage(tokenInfo.total_token_usage);
208
+
209
+ let rawUsage = lastUsage;
210
+ if (!rawUsage && totalUsage) rawUsage = subtractRawUsage(totalUsage, previousTotals);
211
+ if (totalUsage) previousTotals = totalUsage;
212
+ if (!rawUsage) continue;
213
+
214
+ const delta = convertToDelta(rawUsage);
215
+ if (delta.inputTokens === 0 && delta.outputTokens === 0 && delta.cacheRead === 0) continue;
216
+
217
+ currentTurn.inputTokens += delta.inputTokens;
218
+ currentTurn.outputTokens += delta.outputTokens;
219
+ currentTurn.cacheRead += delta.cacheRead;
220
+
221
+ const model = extractModel(tokenInfo) || extractModel(entry.payload);
222
+ if (model) {
223
+ currentModel = model;
224
+ currentTurn.model = model;
225
+ } else if (!currentTurn.model && currentModel) {
226
+ currentTurn.model = currentModel;
227
+ }
228
+ }
229
+ }
230
+
231
+ if (turnHasStarted) flushTurn();
232
+
233
+ return messages;
234
+ }
235
+
236
+ function createTurnState() {
237
+ return {
238
+ parts: [],
239
+ toolCalls: [],
240
+ inputTokens: 0,
241
+ outputTokens: 0,
242
+ cacheRead: 0,
243
+ model: null,
244
+ };
245
+ }
246
+
247
+ function composeAssistantMessage(turn) {
248
+ const content = turn.parts.join('\n') || '[assistant activity]';
249
+ return {
250
+ role: 'assistant',
251
+ content,
252
+ _model: turn.model || undefined,
253
+ _inputTokens: turn.inputTokens || undefined,
254
+ _outputTokens: turn.outputTokens || undefined,
255
+ _cacheRead: turn.cacheRead || undefined,
256
+ _toolCalls: turn.toolCalls.length > 0 ? turn.toolCalls : undefined,
257
+ };
258
+ }
259
+
260
+ function extractUserText(content) {
261
+ if (!Array.isArray(content)) return '';
262
+ return content
263
+ .filter((item) => item && item.type === 'input_text' && typeof item.text === 'string')
264
+ .map((item) => item.text.trim())
265
+ .filter(Boolean)
266
+ .join('\n')
267
+ .trim();
268
+ }
269
+
270
+ function extractAssistantText(content) {
271
+ if (!Array.isArray(content)) return '';
272
+ return content
273
+ .filter((item) => item && item.type === 'output_text' && typeof item.text === 'string')
274
+ .map((item) => item.text.trim())
275
+ .filter(Boolean)
276
+ .join('\n')
277
+ .trim();
278
+ }
279
+
280
+ function extractReasoningSummary(payload) {
281
+ if (!Array.isArray(payload.summary)) return '';
282
+ return payload.summary
283
+ .filter((item) => item && typeof item.text === 'string' && item.text.trim())
284
+ .map((item) => `[thinking] ${item.text.trim()}`)
285
+ .join('\n')
286
+ .trim();
287
+ }
288
+
289
+ function isBootstrapMessage(text) {
290
+ const trimmed = text.trim();
291
+ return trimmed.startsWith('<user_instructions>') || trimmed.startsWith('<environment_context>');
292
+ }
293
+
294
+ function cleanPrompt(text) {
295
+ return text.replace(/\s+/g, ' ').trim().substring(0, 120) || null;
296
+ }
297
+
298
+ function isToolCallPayload(type) {
299
+ return type === 'function_call' || type === 'custom_tool_call' || type === 'web_search_call';
300
+ }
301
+
302
+ function isToolOutputPayload(type) {
303
+ return type === 'function_call_output' || type === 'custom_tool_call_output';
304
+ }
305
+
306
+ function normalizeToolCall(payload) {
307
+ const name = payload.name || (payload.type === 'web_search_call' ? 'web_search' : 'tool');
308
+ const args = parseToolArgs(payload);
309
+ const argKeys = Object.keys(args).join(', ');
310
+ return {
311
+ name,
312
+ args,
313
+ line: `[tool-call: ${name}(${argKeys})]`,
314
+ };
315
+ }
316
+
317
+ function normalizeToolResult(payload, fallbackName) {
318
+ const name = payload.name || fallbackName || 'tool';
319
+ const preview = previewToolOutput(payload.output);
320
+ return preview ? `[tool-result: ${name}] ${preview}` : `[tool-result: ${name}]`;
321
+ }
322
+
323
+ function parseToolArgs(payload) {
324
+ if (payload.type === 'function_call') {
325
+ return parseJsonRecord(payload.arguments);
326
+ }
327
+ if (payload.type === 'custom_tool_call') {
328
+ return { input: truncateSingleLine(String(payload.input || ''), 300) };
329
+ }
330
+ if (payload.type === 'web_search_call') {
331
+ return parseJsonRecord(payload.arguments || payload.input || payload.query);
332
+ }
333
+ return {};
334
+ }
335
+
336
+ function previewToolOutput(output) {
337
+ if (output == null) return '';
338
+ let value = output;
339
+ if (typeof value === 'string') {
340
+ const parsed = safeParseJson(value);
341
+ if (parsed && typeof parsed === 'object' && typeof parsed.output === 'string') {
342
+ value = parsed.output;
343
+ } else {
344
+ value = value;
345
+ }
346
+ } else if (typeof value === 'object') {
347
+ value = JSON.stringify(value);
348
+ } else {
349
+ value = String(value);
350
+ }
351
+
352
+ const trimmed = String(value).trim();
353
+ if (!trimmed) return '';
354
+ return truncateSingleLine(trimmed, MAX_TOOL_RESULT_PREVIEW);
355
+ }
356
+
357
+ function truncateSingleLine(text, maxLen) {
358
+ const oneLine = String(text).replace(/\s+/g, ' ').trim();
359
+ return oneLine.length > maxLen ? oneLine.substring(0, maxLen) + '…' : oneLine;
360
+ }
361
+
362
+ function parseJsonRecord(value) {
363
+ if (value == null) return {};
364
+ if (typeof value === 'object' && !Array.isArray(value)) return value;
365
+ if (typeof value !== 'string') return {};
366
+ const parsed = safeParseJson(value);
367
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
368
+ }
369
+
370
+ function normalizeRawUsage(value) {
371
+ if (!value || typeof value !== 'object') return null;
372
+ const input = ensureNumber(value.input_tokens);
373
+ const cached = ensureNumber(value.cached_input_tokens != null ? value.cached_input_tokens : value.cache_read_input_tokens);
374
+ const output = ensureNumber(value.output_tokens);
375
+ const total = ensureNumber(value.total_tokens);
376
+ return {
377
+ input_tokens: input,
378
+ cached_input_tokens: cached,
379
+ output_tokens: output,
380
+ total_tokens: total > 0 ? total : input + output,
381
+ };
382
+ }
383
+
384
+ function subtractRawUsage(current, previous) {
385
+ return {
386
+ input_tokens: Math.max(current.input_tokens - (previous ? previous.input_tokens : 0), 0),
387
+ cached_input_tokens: Math.max(current.cached_input_tokens - (previous ? previous.cached_input_tokens : 0), 0),
388
+ output_tokens: Math.max(current.output_tokens - (previous ? previous.output_tokens : 0), 0),
389
+ total_tokens: Math.max(current.total_tokens - (previous ? previous.total_tokens : 0), 0),
390
+ };
391
+ }
392
+
393
+ function convertToDelta(raw) {
394
+ const cacheRead = Math.min(raw.cached_input_tokens, raw.input_tokens);
395
+ const billableInput = Math.max(raw.input_tokens - cacheRead, 0);
396
+ return {
397
+ inputTokens: billableInput,
398
+ cacheRead,
399
+ outputTokens: raw.output_tokens,
400
+ totalTokens: raw.total_tokens > 0 ? raw.total_tokens : raw.input_tokens + raw.output_tokens,
401
+ };
402
+ }
403
+
404
+ function extractModel(value) {
405
+ if (!value || typeof value !== 'object') return null;
406
+
407
+ const direct = asNonEmptyString(value.model)
408
+ || asNonEmptyString(value.model_name);
409
+ if (direct) return direct;
410
+
411
+ if (value.info && typeof value.info === 'object') {
412
+ const infoModel = extractModel(value.info);
413
+ if (infoModel) return infoModel;
414
+ }
415
+
416
+ if (value.metadata && typeof value.metadata === 'object') {
417
+ const metadataModel = extractModel(value.metadata);
418
+ if (metadataModel) return metadataModel;
419
+ }
420
+
421
+ return null;
422
+ }
423
+
424
+ function asNonEmptyString(value) {
425
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
426
+ }
427
+
428
+ function ensureNumber(value) {
429
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
430
+ }
431
+
432
+ function toTimestamp(value) {
433
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
434
+ if (typeof value !== 'string' || !value.trim()) return null;
435
+ const parsed = Date.parse(value);
436
+ return Number.isFinite(parsed) ? parsed : null;
437
+ }
438
+
439
+ function readLines(filePath) {
440
+ try {
441
+ return fs.readFileSync(filePath, 'utf-8').split(/\r?\n/).filter(Boolean);
442
+ } catch {
443
+ return [];
444
+ }
445
+ }
446
+
447
+ function safeParseJson(value) {
448
+ try {
449
+ return JSON.parse(value);
450
+ } catch {
451
+ return null;
452
+ }
453
+ }
454
+
455
+ // ============================================================
456
+ // Usage / quota data from Codex auth.json JWT
457
+ // ============================================================
458
+
459
+ function getCodexAuth() {
460
+ const authPath = path.join(getCodexHome(), 'auth.json');
461
+ try {
462
+ return JSON.parse(fs.readFileSync(authPath, 'utf-8'));
463
+ } catch { return null; }
464
+ }
465
+
466
+ function decodeJwtPayload(token) {
467
+ if (!token) return null;
468
+ try {
469
+ const parts = token.split('.');
470
+ if (parts.length < 2) return null;
471
+ let payload = parts[1];
472
+ // Fix base64url padding
473
+ payload += '='.repeat((4 - payload.length % 4) % 4);
474
+ const decoded = Buffer.from(payload, 'base64').toString('utf-8');
475
+ return JSON.parse(decoded);
476
+ } catch { return null; }
477
+ }
478
+
479
+ async function getUsage() {
480
+ const { isSubscriptionAccessAllowed } = require('./base');
481
+ if (!isSubscriptionAccessAllowed()) return null;
482
+ const auth = getCodexAuth();
483
+ if (!auth || !auth.tokens) return null;
484
+
485
+ const idPayload = decodeJwtPayload(auth.tokens.id_token);
486
+ const accessPayload = decodeJwtPayload(auth.tokens.access_token);
487
+
488
+ const authClaims = idPayload?.['https://api.openai.com/auth'] || accessPayload?.['https://api.openai.com/auth'] || {};
489
+ const profileClaims = idPayload?.['https://api.openai.com/profile'] || accessPayload?.['https://api.openai.com/profile'] || {};
490
+
491
+ const planType = authClaims.chatgpt_plan_type || null;
492
+ const email = profileClaims.email || null;
493
+ const subscriptionStart = authClaims.chatgpt_subscription_active_start || null;
494
+ const subscriptionEnd = authClaims.chatgpt_subscription_active_until || null;
495
+
496
+ if (!planType && !email) return null;
497
+
498
+ return {
499
+ source: 'codex',
500
+ plan: {
501
+ name: planType,
502
+ subscriptionStart,
503
+ subscriptionEnd,
504
+ },
505
+ user: {
506
+ email,
507
+ },
508
+ authMode: auth.auth_mode || null,
509
+ };
510
+ }
511
+
512
+ const labels = { 'codex': 'Codex' };
513
+
514
+ function getArtifacts(folder) {
515
+ const { scanArtifacts } = require('./base');
516
+ return scanArtifacts(folder, {
517
+ editor: 'codex',
518
+ label: 'Codex',
519
+ files: ['AGENTS.md', 'codex.md'],
520
+ dirs: [],
521
+ });
522
+ }
523
+
524
+ function getMCPServers() {
525
+ // Codex doesn't have native MCP server configuration
526
+ return [];
527
+ }
528
+
529
+ module.exports = {
530
+ name,
531
+ labels,
532
+ getArtifacts,
533
+ getChats,
534
+ getMessages,
535
+ getUsage,
536
+ getMCPServers,
537
+ };
@@ -0,0 +1,166 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+
5
+ const COMMANDCODE_DIR = path.join(os.homedir(), '.commandcode');
6
+ const PROJECTS_DIR = path.join(COMMANDCODE_DIR, 'projects');
7
+
8
+ // ============================================================
9
+ // Adapter interface
10
+ // ============================================================
11
+
12
+ const name = 'commandcode';
13
+
14
+ function getChats() {
15
+ const chats = [];
16
+ if (!fs.existsSync(PROJECTS_DIR)) return chats;
17
+
18
+ let projDirs;
19
+ try { projDirs = fs.readdirSync(PROJECTS_DIR); } catch { return chats; }
20
+
21
+ for (const projDir of projDirs) {
22
+ const dir = path.join(PROJECTS_DIR, projDir);
23
+ try { if (!fs.statSync(dir).isDirectory()) continue; } catch { continue; }
24
+
25
+ // Decode folder path from dir name (e.g. users-fka-code-foo -> /users/fka/code/foo)
26
+ const decodedFolder = '/' + projDir.replace(/-/g, '/');
27
+
28
+ let files;
29
+ try { files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl') && !f.includes('.checkpoints.')); } catch { continue; }
30
+
31
+ for (const file of files) {
32
+ const sessionId = file.replace('.jsonl', '');
33
+ const fullPath = path.join(dir, file);
34
+ const metaPath = path.join(dir, `${sessionId}.meta.json`);
35
+
36
+ // Read meta.json for title
37
+ let title = null;
38
+ try {
39
+ const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
40
+ title = meta.title || null;
41
+ } catch { /* no meta */ }
42
+
43
+ // Parse first and last lines for timestamps
44
+ try {
45
+ const stat = fs.statSync(fullPath);
46
+ const lines = fs.readFileSync(fullPath, 'utf-8').split('\n').filter(Boolean);
47
+ if (lines.length === 0) continue;
48
+
49
+ const first = JSON.parse(lines[0]);
50
+ const last = JSON.parse(lines[lines.length - 1]);
51
+
52
+ const firstPrompt = extractFirstPrompt(first);
53
+ const bubbleCount = lines.length;
54
+
55
+ chats.push({
56
+ source: 'commandcode',
57
+ composerId: sessionId,
58
+ name: title || cleanPrompt(firstPrompt),
59
+ createdAt: first.timestamp ? new Date(first.timestamp).getTime() : stat.birthtime.getTime(),
60
+ lastUpdatedAt: last.timestamp ? new Date(last.timestamp).getTime() : stat.mtime.getTime(),
61
+ mode: 'commandcode',
62
+ folder: decodedFolder,
63
+ encrypted: false,
64
+ bubbleCount,
65
+ _fullPath: fullPath,
66
+ _gitBranch: first.gitBranch || null,
67
+ });
68
+ } catch { /* skip */ }
69
+ }
70
+ }
71
+
72
+ return chats;
73
+ }
74
+
75
+ function extractFirstPrompt(obj) {
76
+ if (obj.role !== 'user') return null;
77
+ if (!obj.content) return null;
78
+ if (typeof obj.content === 'string') return obj.content;
79
+ if (Array.isArray(obj.content)) {
80
+ return obj.content
81
+ .filter(c => c.type === 'text')
82
+ .map(c => c.text)
83
+ .join(' ')
84
+ .substring(0, 200);
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function cleanPrompt(text) {
90
+ if (!text) return null;
91
+ return text.replace(/\s+/g, ' ').trim().substring(0, 120) || null;
92
+ }
93
+
94
+ function getMessages(chat) {
95
+ const filePath = chat._fullPath;
96
+ if (!filePath || !fs.existsSync(filePath)) return [];
97
+
98
+ const messages = [];
99
+ const lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
100
+
101
+ for (const line of lines) {
102
+ let obj;
103
+ try { obj = JSON.parse(line); } catch { continue; }
104
+
105
+ if (obj.role === 'user') {
106
+ const content = extractContent(obj.content);
107
+ if (content) messages.push({ role: 'user', content });
108
+ } else if (obj.role === 'assistant') {
109
+ const { text, toolCalls } = extractAssistantContent(obj.content);
110
+ if (text) {
111
+ messages.push({
112
+ role: 'assistant',
113
+ content: text,
114
+ _toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
115
+ });
116
+ }
117
+ } else if (obj.role === 'system') {
118
+ const text = extractContent(obj.content);
119
+ if (text) messages.push({ role: 'system', content: text });
120
+ }
121
+ }
122
+
123
+ return messages;
124
+ }
125
+
126
+ function extractContent(content) {
127
+ if (typeof content === 'string') return content;
128
+ if (!Array.isArray(content)) return '';
129
+ // Skip tool_result blocks for user messages (they are tool outputs)
130
+ return content
131
+ .filter(c => c.type === 'text')
132
+ .map(c => c.text)
133
+ .join('\n') || '';
134
+ }
135
+
136
+ function extractAssistantContent(content) {
137
+ if (typeof content === 'string') return { text: content, toolCalls: [] };
138
+ if (!Array.isArray(content)) return { text: '', toolCalls: [] };
139
+ const parts = [];
140
+ const toolCalls = [];
141
+ for (const block of content) {
142
+ if (block.type === 'thinking' && block.thinking) {
143
+ parts.push(`[thinking] ${block.thinking}`);
144
+ } else if (block.type === 'text' && block.text) {
145
+ parts.push(block.text);
146
+ } else if (block.type === 'tool_use') {
147
+ const args = block.input || {};
148
+ const argKeys = Object.keys(args).join(', ');
149
+ parts.push(`[tool-call: ${block.name || 'unknown'}(${argKeys})]`);
150
+ toolCalls.push({ name: block.name || 'unknown', args });
151
+ } else if (block.type === 'tool_result') {
152
+ const text = typeof block.content === 'string' ? block.content : '';
153
+ parts.push(`[tool-result: ${block.name || 'tool'}] ${text.substring(0, 500)}`);
154
+ }
155
+ }
156
+ return { text: parts.join('\n') || '', toolCalls };
157
+ }
158
+
159
+ const labels = { 'commandcode': 'Command Code' };
160
+
161
+ function getMCPServers() {
162
+ // CommandCode uses Claude's .mcp.json format (project-level, handled by claude.js)
163
+ return [];
164
+ }
165
+
166
+ module.exports = { name, labels, getChats, getMessages, getMCPServers };