codeblog-mcp 0.8.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 (44) hide show
  1. package/README.md +178 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +29 -0
  4. package/dist/lib/analyzer.d.ts +2 -0
  5. package/dist/lib/analyzer.js +225 -0
  6. package/dist/lib/config.d.ts +15 -0
  7. package/dist/lib/config.js +32 -0
  8. package/dist/lib/fs-utils.d.ts +9 -0
  9. package/dist/lib/fs-utils.js +147 -0
  10. package/dist/lib/platform.d.ts +6 -0
  11. package/dist/lib/platform.js +50 -0
  12. package/dist/lib/registry.d.ts +14 -0
  13. package/dist/lib/registry.js +69 -0
  14. package/dist/lib/types.d.ts +47 -0
  15. package/dist/lib/types.js +1 -0
  16. package/dist/scanners/aider.d.ts +2 -0
  17. package/dist/scanners/aider.js +132 -0
  18. package/dist/scanners/claude-code.d.ts +2 -0
  19. package/dist/scanners/claude-code.js +193 -0
  20. package/dist/scanners/codex.d.ts +2 -0
  21. package/dist/scanners/codex.js +143 -0
  22. package/dist/scanners/continue-dev.d.ts +2 -0
  23. package/dist/scanners/continue-dev.js +136 -0
  24. package/dist/scanners/cursor.d.ts +2 -0
  25. package/dist/scanners/cursor.js +447 -0
  26. package/dist/scanners/index.d.ts +1 -0
  27. package/dist/scanners/index.js +22 -0
  28. package/dist/scanners/vscode-copilot.d.ts +2 -0
  29. package/dist/scanners/vscode-copilot.js +179 -0
  30. package/dist/scanners/warp.d.ts +2 -0
  31. package/dist/scanners/warp.js +20 -0
  32. package/dist/scanners/windsurf.d.ts +2 -0
  33. package/dist/scanners/windsurf.js +197 -0
  34. package/dist/scanners/zed.d.ts +2 -0
  35. package/dist/scanners/zed.js +121 -0
  36. package/dist/tools/forum.d.ts +2 -0
  37. package/dist/tools/forum.js +292 -0
  38. package/dist/tools/posting.d.ts +2 -0
  39. package/dist/tools/posting.js +195 -0
  40. package/dist/tools/sessions.d.ts +2 -0
  41. package/dist/tools/sessions.js +95 -0
  42. package/dist/tools/setup.d.ts +2 -0
  43. package/dist/tools/setup.js +118 -0
  44. package/package.json +48 -0
@@ -0,0 +1,50 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ export function getPlatform() {
5
+ switch (os.platform()) {
6
+ case "win32":
7
+ return "windows";
8
+ case "darwin":
9
+ return "macos";
10
+ default:
11
+ return "linux";
12
+ }
13
+ }
14
+ export function getHome() {
15
+ return os.homedir();
16
+ }
17
+ // Windows: %APPDATA%, %LOCALAPPDATA%, %USERPROFILE%
18
+ // macOS: ~/Library/Application Support, ~/
19
+ // Linux: ~/.config, ~/.local/share, ~/
20
+ export function getAppDataDir() {
21
+ const platform = getPlatform();
22
+ if (platform === "windows") {
23
+ return process.env.APPDATA || path.join(getHome(), "AppData", "Roaming");
24
+ }
25
+ if (platform === "macos") {
26
+ return path.join(getHome(), "Library", "Application Support");
27
+ }
28
+ return process.env.XDG_CONFIG_HOME || path.join(getHome(), ".config");
29
+ }
30
+ export function getLocalAppDataDir() {
31
+ const platform = getPlatform();
32
+ if (platform === "windows") {
33
+ return process.env.LOCALAPPDATA || path.join(getHome(), "AppData", "Local");
34
+ }
35
+ if (platform === "macos") {
36
+ return path.join(getHome(), "Library", "Application Support");
37
+ }
38
+ return process.env.XDG_DATA_HOME || path.join(getHome(), ".local", "share");
39
+ }
40
+ // Resolve a list of candidate paths, return all that exist
41
+ export function resolvePaths(candidates) {
42
+ return candidates.filter((p) => {
43
+ try {
44
+ return fs.existsSync(p);
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ });
50
+ }
@@ -0,0 +1,14 @@
1
+ import type { Scanner, Session, ParsedSession } from "./types.js";
2
+ export declare function registerScanner(scanner: Scanner): void;
3
+ export declare function getScanners(): Scanner[];
4
+ export declare function getScannerBySource(source: string): Scanner | undefined;
5
+ export declare function scanAll(limit?: number, source?: string): Session[];
6
+ export declare function parseSession(filePath: string, source: string, maxTurns?: number): ParsedSession | null;
7
+ export declare function listScannerStatus(): Array<{
8
+ name: string;
9
+ source: string;
10
+ description: string;
11
+ available: boolean;
12
+ dirs: string[];
13
+ error?: string;
14
+ }>;
@@ -0,0 +1,69 @@
1
+ // Scanner registry — all IDE scanners register here
2
+ // DESIGN: Every scanner is fully isolated. A single scanner crashing
3
+ // (missing deps, changed file formats, permission errors, etc.)
4
+ // MUST NEVER take down the whole MCP server.
5
+ const scanners = [];
6
+ export function registerScanner(scanner) {
7
+ scanners.push(scanner);
8
+ }
9
+ export function getScanners() {
10
+ return [...scanners];
11
+ }
12
+ export function getScannerBySource(source) {
13
+ return scanners.find((s) => s.sourceType === source);
14
+ }
15
+ // Safe wrapper: calls a scanner method, returns fallback on ANY error
16
+ function safeScannerCall(scannerName, method, fn, fallback) {
17
+ try {
18
+ return fn();
19
+ }
20
+ catch (err) {
21
+ console.error(`[codeblog] Scanner "${scannerName}" ${method} failed:`, err instanceof Error ? err.message : err);
22
+ return fallback;
23
+ }
24
+ }
25
+ // Scan all registered IDEs, merge and sort results
26
+ // If source is provided, only scan that specific IDE
27
+ export function scanAll(limit = 20, source) {
28
+ const allSessions = [];
29
+ const targets = source ? scanners.filter((s) => s.sourceType === source) : scanners;
30
+ for (const scanner of targets) {
31
+ const sessions = safeScannerCall(scanner.name, "scan", () => scanner.scan(limit), []);
32
+ allSessions.push(...sessions);
33
+ }
34
+ // Sort by modification time (newest first)
35
+ allSessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
36
+ return allSessions.slice(0, limit);
37
+ }
38
+ // Parse a session file using the appropriate scanner
39
+ export function parseSession(filePath, source, maxTurns) {
40
+ const scanner = getScannerBySource(source);
41
+ if (!scanner)
42
+ return null;
43
+ return safeScannerCall(scanner.name, "parse", () => scanner.parse(filePath, maxTurns), null);
44
+ }
45
+ // List available scanners with their status
46
+ export function listScannerStatus() {
47
+ return scanners.map((s) => {
48
+ try {
49
+ const dirs = s.getSessionDirs();
50
+ return {
51
+ name: s.name,
52
+ source: s.sourceType,
53
+ description: s.description,
54
+ available: dirs.length > 0,
55
+ dirs,
56
+ };
57
+ }
58
+ catch (err) {
59
+ return {
60
+ name: s.name,
61
+ source: s.sourceType,
62
+ description: s.description,
63
+ available: false,
64
+ dirs: [],
65
+ error: err instanceof Error ? err.message : String(err),
66
+ };
67
+ }
68
+ });
69
+ }
@@ -0,0 +1,47 @@
1
+ export interface Session {
2
+ id: string;
3
+ source: SourceType;
4
+ project: string;
5
+ projectPath?: string;
6
+ projectDescription?: string;
7
+ title: string;
8
+ messageCount: number;
9
+ humanMessages: number;
10
+ aiMessages: number;
11
+ preview: string;
12
+ filePath: string;
13
+ modifiedAt: Date;
14
+ sizeBytes: number;
15
+ }
16
+ export interface ConversationTurn {
17
+ role: "human" | "assistant" | "system" | "tool";
18
+ content: string;
19
+ timestamp?: Date;
20
+ }
21
+ export interface ParsedSession extends Session {
22
+ turns: ConversationTurn[];
23
+ }
24
+ export interface SessionAnalysis {
25
+ summary: string;
26
+ topics: string[];
27
+ languages: string[];
28
+ keyInsights: string[];
29
+ codeSnippets: Array<{
30
+ language: string;
31
+ code: string;
32
+ context: string;
33
+ }>;
34
+ problems: string[];
35
+ solutions: string[];
36
+ suggestedTitle: string;
37
+ suggestedTags: string[];
38
+ }
39
+ export type SourceType = "claude-code" | "cursor" | "windsurf" | "codex" | "warp" | "vscode-copilot" | "aider" | "continue" | "zed" | "unknown";
40
+ export interface Scanner {
41
+ name: string;
42
+ sourceType: SourceType;
43
+ description: string;
44
+ getSessionDirs(): string[];
45
+ scan(limit: number): Session[];
46
+ parse(filePath: string, maxTurns?: number): ParsedSession | null;
47
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { Scanner } from "../lib/types.js";
2
+ export declare const aiderScanner: Scanner;
@@ -0,0 +1,132 @@
1
+ import * as path from "path";
2
+ import * as fs from "fs";
3
+ import { getHome } from "../lib/platform.js";
4
+ import { listFiles, safeReadFile, safeStats } from "../lib/fs-utils.js";
5
+ // Aider stores chat history in:
6
+ // <project>/.aider.chat.history.md (markdown format)
7
+ // <project>/.aider.input.history (readline history)
8
+ // ~/.aider/history/ (global history)
9
+ // Same paths on all platforms
10
+ export const aiderScanner = {
11
+ name: "Aider",
12
+ sourceType: "aider",
13
+ description: "Aider AI pair programming sessions",
14
+ getSessionDirs() {
15
+ const home = getHome();
16
+ const candidates = [
17
+ path.join(home, ".aider", "history"),
18
+ path.join(home, ".aider"),
19
+ ];
20
+ return candidates.filter((d) => {
21
+ try {
22
+ return fs.existsSync(d);
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ });
28
+ },
29
+ scan(limit) {
30
+ const sessions = [];
31
+ const dirs = this.getSessionDirs();
32
+ for (const dir of dirs) {
33
+ const mdFiles = listFiles(dir, [".md"], true);
34
+ for (const filePath of mdFiles) {
35
+ if (!path.basename(filePath).includes("aider"))
36
+ continue;
37
+ const stats = safeStats(filePath);
38
+ if (!stats || stats.size < 100)
39
+ continue;
40
+ const content = safeReadFile(filePath);
41
+ if (!content)
42
+ continue;
43
+ const { humanCount, aiCount, preview } = parseAiderMarkdown(content);
44
+ if (humanCount === 0)
45
+ continue;
46
+ sessions.push({
47
+ id: path.basename(filePath, ".md"),
48
+ source: "aider",
49
+ project: path.basename(path.dirname(filePath)),
50
+ title: preview.slice(0, 80) || "Aider session",
51
+ messageCount: humanCount + aiCount,
52
+ humanMessages: humanCount,
53
+ aiMessages: aiCount,
54
+ preview,
55
+ filePath,
56
+ modifiedAt: stats.mtime,
57
+ sizeBytes: stats.size,
58
+ });
59
+ }
60
+ }
61
+ sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
62
+ return sessions.slice(0, limit);
63
+ },
64
+ parse(filePath, maxTurns) {
65
+ const content = safeReadFile(filePath);
66
+ if (!content)
67
+ return null;
68
+ const stats = safeStats(filePath);
69
+ const turns = parseAiderTurns(content, maxTurns);
70
+ if (turns.length === 0)
71
+ return null;
72
+ const humanMsgs = turns.filter((t) => t.role === "human");
73
+ const aiMsgs = turns.filter((t) => t.role === "assistant");
74
+ return {
75
+ id: path.basename(filePath, ".md"),
76
+ source: "aider",
77
+ project: path.basename(path.dirname(filePath)),
78
+ title: humanMsgs[0]?.content.slice(0, 80) || "Aider session",
79
+ messageCount: turns.length,
80
+ humanMessages: humanMsgs.length,
81
+ aiMessages: aiMsgs.length,
82
+ preview: humanMsgs[0]?.content.slice(0, 200) || "",
83
+ filePath,
84
+ modifiedAt: stats?.mtime || new Date(),
85
+ sizeBytes: stats?.size || 0,
86
+ turns,
87
+ };
88
+ },
89
+ };
90
+ function parseAiderMarkdown(content) {
91
+ // Aider chat history format:
92
+ // #### <user message>
93
+ // <assistant response>
94
+ const userBlocks = content.split(/^####\s+/m).filter(Boolean);
95
+ let humanCount = 0;
96
+ let aiCount = 0;
97
+ let preview = "";
98
+ for (const block of userBlocks) {
99
+ const lines = block.split("\n");
100
+ const firstLine = lines[0]?.trim();
101
+ if (firstLine) {
102
+ humanCount++;
103
+ if (!preview)
104
+ preview = firstLine.slice(0, 200);
105
+ // Everything after the first line is AI response
106
+ const rest = lines.slice(1).join("\n").trim();
107
+ if (rest)
108
+ aiCount++;
109
+ }
110
+ }
111
+ return { humanCount, aiCount, preview };
112
+ }
113
+ function parseAiderTurns(content, maxTurns) {
114
+ const turns = [];
115
+ const blocks = content.split(/^####\s+/m).filter(Boolean);
116
+ for (const block of blocks) {
117
+ if (maxTurns && turns.length >= maxTurns)
118
+ break;
119
+ const lines = block.split("\n");
120
+ const userMsg = lines[0]?.trim();
121
+ if (userMsg) {
122
+ turns.push({ role: "human", content: userMsg });
123
+ const aiResponse = lines.slice(1).join("\n").trim();
124
+ if (aiResponse) {
125
+ if (maxTurns && turns.length >= maxTurns)
126
+ break;
127
+ turns.push({ role: "assistant", content: aiResponse });
128
+ }
129
+ }
130
+ }
131
+ return turns;
132
+ }
@@ -0,0 +1,2 @@
1
+ import type { Scanner } from "../lib/types.js";
2
+ export declare const claudeCodeScanner: Scanner;
@@ -0,0 +1,193 @@
1
+ import * as path from "path";
2
+ import * as fs from "fs";
3
+ import { getHome, getPlatform } from "../lib/platform.js";
4
+ import { listFiles, listDirs, safeStats, readJsonl, extractProjectDescription } from "../lib/fs-utils.js";
5
+ export const claudeCodeScanner = {
6
+ name: "Claude Code",
7
+ sourceType: "claude-code",
8
+ description: "Claude Code CLI sessions (~/.claude/projects/)",
9
+ getSessionDirs() {
10
+ const home = getHome();
11
+ const candidates = [path.join(home, ".claude", "projects")];
12
+ return candidates.filter((d) => {
13
+ try {
14
+ return fs.existsSync(d);
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ });
20
+ },
21
+ scan(limit) {
22
+ const sessions = [];
23
+ const dirs = this.getSessionDirs();
24
+ for (const baseDir of dirs) {
25
+ const projectDirs = listDirs(baseDir);
26
+ for (const projectDir of projectDirs) {
27
+ const project = path.basename(projectDir);
28
+ const files = listFiles(projectDir, [".jsonl"]);
29
+ for (const filePath of files) {
30
+ const stats = safeStats(filePath);
31
+ if (!stats)
32
+ continue;
33
+ const lines = readJsonl(filePath);
34
+ if (lines.length < 3)
35
+ continue;
36
+ const humanMsgs = lines.filter((l) => l.type === "user");
37
+ const aiMsgs = lines.filter((l) => l.type === "assistant");
38
+ // Extract cwd (project path) from first message that has it
39
+ const cwdLine = lines.find((l) => l.cwd);
40
+ let projectPath = cwdLine?.cwd || null;
41
+ // Fallback: derive from directory name (e.g. "-Users-zhaoyifei-Foo" → "/Users/zhaoyifei/Foo")
42
+ if (!projectPath && project.startsWith("-")) {
43
+ projectPath = decodeClaudeProjectDir(project);
44
+ }
45
+ const projectName = projectPath ? path.basename(projectPath) : project;
46
+ // Get project description from README/package.json
47
+ const projectDescription = projectPath
48
+ ? extractProjectDescription(projectPath)
49
+ : null;
50
+ let preview = "";
51
+ for (const msg of humanMsgs.slice(0, 8)) {
52
+ const content = extractContent(msg);
53
+ if (!content || content.length < 10)
54
+ continue;
55
+ // Skip system-like messages and slash commands
56
+ if (content.startsWith("<local-command-caveat>"))
57
+ continue;
58
+ if (content.startsWith("<environment_context>"))
59
+ continue;
60
+ if (content.startsWith("<command-name>"))
61
+ continue;
62
+ preview = content.slice(0, 200);
63
+ break;
64
+ }
65
+ sessions.push({
66
+ id: path.basename(filePath, ".jsonl"),
67
+ source: "claude-code",
68
+ project: projectName,
69
+ projectPath: projectPath || undefined,
70
+ projectDescription: projectDescription || undefined,
71
+ title: preview.slice(0, 80) || `Claude session in ${projectName}`,
72
+ messageCount: humanMsgs.length + aiMsgs.length,
73
+ humanMessages: humanMsgs.length,
74
+ aiMessages: aiMsgs.length,
75
+ preview: preview || "(no preview)",
76
+ filePath,
77
+ modifiedAt: stats.mtime,
78
+ sizeBytes: stats.size,
79
+ });
80
+ }
81
+ }
82
+ }
83
+ // Sort by modification time (newest first), then apply limit
84
+ sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
85
+ return sessions.slice(0, limit);
86
+ },
87
+ parse(filePath, maxTurns) {
88
+ const lines = readJsonl(filePath);
89
+ if (lines.length === 0)
90
+ return null;
91
+ const stats = safeStats(filePath);
92
+ const turns = [];
93
+ // Extract project info
94
+ const cwdLine = lines.find((l) => l.cwd);
95
+ const projectPath = cwdLine?.cwd || undefined;
96
+ const projectName = projectPath
97
+ ? path.basename(projectPath)
98
+ : path.basename(path.dirname(filePath));
99
+ const projectDescription = projectPath
100
+ ? extractProjectDescription(projectPath) || undefined
101
+ : undefined;
102
+ for (const line of lines) {
103
+ if (maxTurns && turns.length >= maxTurns)
104
+ break;
105
+ if (line.type !== "user" && line.type !== "assistant")
106
+ continue;
107
+ const content = extractContent(line);
108
+ if (!content)
109
+ continue;
110
+ const role = line.type === "user" ? "human" : "assistant";
111
+ turns.push({
112
+ role,
113
+ content,
114
+ timestamp: line.timestamp ? new Date(line.timestamp) : undefined,
115
+ });
116
+ }
117
+ const humanMsgs = turns.filter((t) => t.role === "human");
118
+ const aiMsgs = turns.filter((t) => t.role === "assistant");
119
+ return {
120
+ id: path.basename(filePath, ".jsonl"),
121
+ source: "claude-code",
122
+ project: projectName,
123
+ projectPath,
124
+ projectDescription,
125
+ title: humanMsgs[0]?.content.slice(0, 80) || "Claude session",
126
+ messageCount: turns.length,
127
+ humanMessages: humanMsgs.length,
128
+ aiMessages: aiMsgs.length,
129
+ preview: humanMsgs[0]?.content.slice(0, 200) || "",
130
+ filePath,
131
+ modifiedAt: stats?.mtime || new Date(),
132
+ sizeBytes: stats?.size || 0,
133
+ turns,
134
+ };
135
+ },
136
+ };
137
+ // Decode Claude Code project directory name back to a real path.
138
+ // e.g. "-Users-zhaoyifei-VibeCodingWork-ai-code-forum" → "/Users/zhaoyifei/VibeCodingWork/ai-code-forum"
139
+ // The challenge: hyphens in the dir name could be path separators OR part of a folder name.
140
+ // Strategy: greedily build path segments, checking which paths actually exist on disk.
141
+ function decodeClaudeProjectDir(dirName) {
142
+ const platform = getPlatform();
143
+ // Remove leading dash
144
+ const stripped = dirName.startsWith("-") ? dirName.slice(1) : dirName;
145
+ const parts = stripped.split("-");
146
+ let currentPath = "";
147
+ let i = 0;
148
+ // On Windows, the first part may be a drive letter (e.g. "c" → "C:")
149
+ if (platform === "windows" && parts.length > 0 && /^[a-zA-Z]$/.test(parts[0])) {
150
+ currentPath = parts[0].toUpperCase() + ":";
151
+ i = 1;
152
+ }
153
+ while (i < parts.length) {
154
+ // Try progressively longer segments (greedy: longest existing path wins)
155
+ let bestMatch = "";
156
+ let bestLen = 0;
157
+ for (let end = parts.length; end > i; end--) {
158
+ const segment = parts.slice(i, end).join("-");
159
+ const candidate = currentPath + path.sep + segment;
160
+ try {
161
+ if (fs.existsSync(candidate)) {
162
+ bestMatch = candidate;
163
+ bestLen = end - i;
164
+ break; // Found longest match
165
+ }
166
+ }
167
+ catch { /* ignore */ }
168
+ }
169
+ if (bestLen > 0) {
170
+ currentPath = bestMatch;
171
+ i += bestLen;
172
+ }
173
+ else {
174
+ // No existing path found, just use single segment
175
+ currentPath += path.sep + parts[i];
176
+ i++;
177
+ }
178
+ }
179
+ return currentPath || null;
180
+ }
181
+ function extractContent(msg) {
182
+ if (!msg.message?.content)
183
+ return "";
184
+ if (typeof msg.message.content === "string")
185
+ return msg.message.content;
186
+ if (Array.isArray(msg.message.content)) {
187
+ return msg.message.content
188
+ .filter((c) => c.type === "text" && c.text)
189
+ .map((c) => c.text)
190
+ .join("\n");
191
+ }
192
+ return "";
193
+ }
@@ -0,0 +1,2 @@
1
+ import type { Scanner } from "../lib/types.js";
2
+ export declare const codexScanner: Scanner;
@@ -0,0 +1,143 @@
1
+ import * as path from "path";
2
+ import * as fs from "fs";
3
+ import { getHome } from "../lib/platform.js";
4
+ import { listFiles, safeStats, readJsonl, extractProjectDescription } from "../lib/fs-utils.js";
5
+ export const codexScanner = {
6
+ name: "Codex (OpenAI CLI)",
7
+ sourceType: "codex",
8
+ description: "OpenAI Codex CLI sessions (~/.codex/)",
9
+ getSessionDirs() {
10
+ const home = getHome();
11
+ const candidates = [
12
+ path.join(home, ".codex", "sessions"),
13
+ path.join(home, ".codex", "archived_sessions"),
14
+ ];
15
+ return candidates.filter((d) => {
16
+ try {
17
+ return fs.existsSync(d);
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ });
23
+ },
24
+ scan(limit) {
25
+ const sessions = [];
26
+ const dirs = this.getSessionDirs();
27
+ for (const dir of dirs) {
28
+ // Recursive scan — sessions are in nested YYYY/MM/DD/ subdirectories
29
+ const files = listFiles(dir, [".jsonl"], true);
30
+ for (const filePath of files) {
31
+ const stats = safeStats(filePath);
32
+ if (!stats)
33
+ continue;
34
+ const lines = readJsonl(filePath);
35
+ if (lines.length < 3)
36
+ continue;
37
+ const messageTurns = extractCodexTurns(lines);
38
+ const humanMsgs = messageTurns.filter((t) => t.role === "human");
39
+ const aiMsgs = messageTurns.filter((t) => t.role === "assistant");
40
+ // Extract project path (cwd) from session metadata
41
+ const startLine = lines.find((l) => l.payload?.cwd);
42
+ const projectPath = startLine?.payload?.cwd || null;
43
+ const project = projectPath
44
+ ? path.basename(projectPath)
45
+ : path.basename(dir);
46
+ const projectDescription = projectPath
47
+ ? extractProjectDescription(projectPath)
48
+ : null;
49
+ const preview = humanMsgs[0]?.content.slice(0, 200) || "(codex session)";
50
+ sessions.push({
51
+ id: path.basename(filePath, ".jsonl"),
52
+ source: "codex",
53
+ project,
54
+ projectPath: projectPath || undefined,
55
+ projectDescription: projectDescription || undefined,
56
+ title: preview.slice(0, 80) || "Codex session",
57
+ messageCount: messageTurns.length,
58
+ humanMessages: humanMsgs.length,
59
+ aiMessages: aiMsgs.length,
60
+ preview,
61
+ filePath,
62
+ modifiedAt: stats.mtime,
63
+ sizeBytes: stats.size,
64
+ });
65
+ }
66
+ }
67
+ sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
68
+ return sessions.slice(0, limit);
69
+ },
70
+ parse(filePath, maxTurns) {
71
+ const lines = readJsonl(filePath);
72
+ if (lines.length === 0)
73
+ return null;
74
+ const stats = safeStats(filePath);
75
+ const allTurns = extractCodexTurns(lines);
76
+ const turns = maxTurns ? allTurns.slice(0, maxTurns) : allTurns;
77
+ if (turns.length === 0)
78
+ return null;
79
+ const humanMsgs = turns.filter((t) => t.role === "human");
80
+ const aiMsgs = turns.filter((t) => t.role === "assistant");
81
+ const startLine = lines.find((l) => l.payload?.cwd);
82
+ const projectPath = startLine?.payload?.cwd || undefined;
83
+ const project = projectPath
84
+ ? path.basename(projectPath)
85
+ : path.basename(path.dirname(filePath));
86
+ const projectDescription = projectPath
87
+ ? extractProjectDescription(projectPath) || undefined
88
+ : undefined;
89
+ return {
90
+ id: path.basename(filePath, ".jsonl"),
91
+ source: "codex",
92
+ project,
93
+ projectPath,
94
+ projectDescription,
95
+ title: humanMsgs[0]?.content.slice(0, 80) || "Codex session",
96
+ messageCount: turns.length,
97
+ humanMessages: humanMsgs.length,
98
+ aiMessages: aiMsgs.length,
99
+ preview: humanMsgs[0]?.content.slice(0, 200) || "",
100
+ filePath,
101
+ modifiedAt: stats?.mtime || new Date(),
102
+ sizeBytes: stats?.size || 0,
103
+ turns,
104
+ };
105
+ },
106
+ };
107
+ // Extract conversation turns from Codex JSONL format
108
+ function extractCodexTurns(lines) {
109
+ const turns = [];
110
+ for (const line of lines) {
111
+ if (!line.payload)
112
+ continue;
113
+ const p = line.payload;
114
+ // Only process message-type payloads
115
+ if (p.type !== "message")
116
+ continue;
117
+ // Extract text from content array
118
+ const textParts = (p.content || [])
119
+ .filter((c) => c.text)
120
+ .map((c) => c.text || "")
121
+ .filter(Boolean);
122
+ const content = textParts.join("\n").trim();
123
+ if (!content)
124
+ continue;
125
+ // Skip developer/system messages — they are AGENTS.md instructions, not conversation
126
+ if (p.role === "developer" || p.role === "system")
127
+ continue;
128
+ // Skip system-like user messages (AGENTS.md, environment context, etc.)
129
+ if (p.role === "user" && (content.startsWith("# AGENTS.md") ||
130
+ content.startsWith("<environment_context>") ||
131
+ content.startsWith("<permissions") ||
132
+ content.startsWith("<app-context>") ||
133
+ content.startsWith("<collaboration_mode>")))
134
+ continue;
135
+ const role = p.role === "user" ? "human" : "assistant";
136
+ turns.push({
137
+ role,
138
+ content,
139
+ timestamp: line.timestamp ? new Date(line.timestamp) : undefined,
140
+ });
141
+ }
142
+ return turns;
143
+ }
@@ -0,0 +1,2 @@
1
+ import type { Scanner } from "../lib/types.js";
2
+ export declare const continueDevScanner: Scanner;