@loreai/core 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bun/agents-file.d.ts +4 -0
- package/dist/bun/agents-file.d.ts.map +1 -1
- package/dist/bun/config.d.ts +2 -0
- package/dist/bun/config.d.ts.map +1 -1
- package/dist/bun/curator.d.ts +45 -0
- package/dist/bun/curator.d.ts.map +1 -1
- package/dist/bun/data-dir.d.ts +18 -0
- package/dist/bun/data-dir.d.ts.map +1 -0
- package/dist/bun/db.d.ts +12 -0
- package/dist/bun/db.d.ts.map +1 -1
- package/dist/bun/distillation.d.ts.map +1 -1
- package/dist/bun/embedding-vendor.d.ts +22 -38
- package/dist/bun/embedding-vendor.d.ts.map +1 -1
- package/dist/bun/embedding-worker-types.d.ts +17 -12
- package/dist/bun/embedding-worker-types.d.ts.map +1 -1
- package/dist/bun/embedding-worker.d.ts +9 -2
- package/dist/bun/embedding-worker.d.ts.map +1 -1
- package/dist/bun/embedding-worker.js +38864 -33
- package/dist/bun/embedding-worker.js.map +4 -4
- package/dist/bun/embedding.d.ts +30 -22
- package/dist/bun/embedding.d.ts.map +1 -1
- package/dist/bun/gradient.d.ts +8 -1
- package/dist/bun/gradient.d.ts.map +1 -1
- package/dist/bun/import/detect.d.ts +14 -0
- package/dist/bun/import/detect.d.ts.map +1 -0
- package/dist/bun/import/extract.d.ts +43 -0
- package/dist/bun/import/extract.d.ts.map +1 -0
- package/dist/bun/import/history.d.ts +40 -0
- package/dist/bun/import/history.d.ts.map +1 -0
- package/dist/bun/import/index.d.ts +17 -0
- package/dist/bun/import/index.d.ts.map +1 -0
- package/dist/bun/import/providers/aider.d.ts +2 -0
- package/dist/bun/import/providers/aider.d.ts.map +1 -0
- package/dist/bun/import/providers/claude-code.d.ts +2 -0
- package/dist/bun/import/providers/claude-code.d.ts.map +1 -0
- package/dist/bun/import/providers/cline.d.ts +2 -0
- package/dist/bun/import/providers/cline.d.ts.map +1 -0
- package/dist/bun/import/providers/codex.d.ts +2 -0
- package/dist/bun/import/providers/codex.d.ts.map +1 -0
- package/dist/bun/import/providers/continue.d.ts +2 -0
- package/dist/bun/import/providers/continue.d.ts.map +1 -0
- package/dist/bun/import/providers/index.d.ts +19 -0
- package/dist/bun/import/providers/index.d.ts.map +1 -0
- package/dist/bun/import/providers/opencode.d.ts +2 -0
- package/dist/bun/import/providers/opencode.d.ts.map +1 -0
- package/dist/bun/import/providers/pi.d.ts +2 -0
- package/dist/bun/import/providers/pi.d.ts.map +1 -0
- package/dist/bun/import/types.d.ts +82 -0
- package/dist/bun/import/types.d.ts.map +1 -0
- package/dist/bun/index.d.ts +4 -1
- package/dist/bun/index.d.ts.map +1 -1
- package/dist/bun/index.js +2217 -224
- package/dist/bun/index.js.map +4 -4
- package/dist/bun/instruction-detect.d.ts +66 -0
- package/dist/bun/instruction-detect.d.ts.map +1 -0
- package/dist/bun/log.d.ts +9 -0
- package/dist/bun/log.d.ts.map +1 -1
- package/dist/bun/ltm.d.ts +40 -0
- package/dist/bun/ltm.d.ts.map +1 -1
- package/dist/bun/pattern-extract.d.ts +7 -0
- package/dist/bun/pattern-extract.d.ts.map +1 -1
- package/dist/bun/prompt.d.ts +1 -1
- package/dist/bun/prompt.d.ts.map +1 -1
- package/dist/bun/recall.d.ts.map +1 -1
- package/dist/bun/search.d.ts +5 -3
- package/dist/bun/search.d.ts.map +1 -1
- package/dist/bun/temporal.d.ts.map +1 -1
- package/dist/bun/types.d.ts +1 -1
- package/dist/node/agents-file.d.ts +4 -0
- package/dist/node/agents-file.d.ts.map +1 -1
- package/dist/node/config.d.ts +2 -0
- package/dist/node/config.d.ts.map +1 -1
- package/dist/node/curator.d.ts +45 -0
- package/dist/node/curator.d.ts.map +1 -1
- package/dist/node/data-dir.d.ts +18 -0
- package/dist/node/data-dir.d.ts.map +1 -0
- package/dist/node/db.d.ts +12 -0
- package/dist/node/db.d.ts.map +1 -1
- package/dist/node/distillation.d.ts.map +1 -1
- package/dist/node/embedding-vendor.d.ts +22 -38
- package/dist/node/embedding-vendor.d.ts.map +1 -1
- package/dist/node/embedding-worker-types.d.ts +17 -12
- package/dist/node/embedding-worker-types.d.ts.map +1 -1
- package/dist/node/embedding-worker.d.ts +9 -2
- package/dist/node/embedding-worker.d.ts.map +1 -1
- package/dist/node/embedding-worker.js +38864 -33
- package/dist/node/embedding-worker.js.map +4 -4
- package/dist/node/embedding.d.ts +30 -22
- package/dist/node/embedding.d.ts.map +1 -1
- package/dist/node/gradient.d.ts +8 -1
- package/dist/node/gradient.d.ts.map +1 -1
- package/dist/node/import/detect.d.ts +14 -0
- package/dist/node/import/detect.d.ts.map +1 -0
- package/dist/node/import/extract.d.ts +43 -0
- package/dist/node/import/extract.d.ts.map +1 -0
- package/dist/node/import/history.d.ts +40 -0
- package/dist/node/import/history.d.ts.map +1 -0
- package/dist/node/import/index.d.ts +17 -0
- package/dist/node/import/index.d.ts.map +1 -0
- package/dist/node/import/providers/aider.d.ts +2 -0
- package/dist/node/import/providers/aider.d.ts.map +1 -0
- package/dist/node/import/providers/claude-code.d.ts +2 -0
- package/dist/node/import/providers/claude-code.d.ts.map +1 -0
- package/dist/node/import/providers/cline.d.ts +2 -0
- package/dist/node/import/providers/cline.d.ts.map +1 -0
- package/dist/node/import/providers/codex.d.ts +2 -0
- package/dist/node/import/providers/codex.d.ts.map +1 -0
- package/dist/node/import/providers/continue.d.ts +2 -0
- package/dist/node/import/providers/continue.d.ts.map +1 -0
- package/dist/node/import/providers/index.d.ts +19 -0
- package/dist/node/import/providers/index.d.ts.map +1 -0
- package/dist/node/import/providers/opencode.d.ts +2 -0
- package/dist/node/import/providers/opencode.d.ts.map +1 -0
- package/dist/node/import/providers/pi.d.ts +2 -0
- package/dist/node/import/providers/pi.d.ts.map +1 -0
- package/dist/node/import/types.d.ts +82 -0
- package/dist/node/import/types.d.ts.map +1 -0
- package/dist/node/index.d.ts +4 -1
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +2217 -224
- package/dist/node/index.js.map +4 -4
- package/dist/node/instruction-detect.d.ts +66 -0
- package/dist/node/instruction-detect.d.ts.map +1 -0
- package/dist/node/log.d.ts +9 -0
- package/dist/node/log.d.ts.map +1 -1
- package/dist/node/ltm.d.ts +40 -0
- package/dist/node/ltm.d.ts.map +1 -1
- package/dist/node/pattern-extract.d.ts +7 -0
- package/dist/node/pattern-extract.d.ts.map +1 -1
- package/dist/node/prompt.d.ts +1 -1
- package/dist/node/prompt.d.ts.map +1 -1
- package/dist/node/recall.d.ts.map +1 -1
- package/dist/node/search.d.ts +5 -3
- package/dist/node/search.d.ts.map +1 -1
- package/dist/node/temporal.d.ts.map +1 -1
- package/dist/node/types.d.ts +1 -1
- package/dist/types/agents-file.d.ts +4 -0
- package/dist/types/agents-file.d.ts.map +1 -1
- package/dist/types/config.d.ts +2 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/curator.d.ts +45 -0
- package/dist/types/curator.d.ts.map +1 -1
- package/dist/types/data-dir.d.ts +18 -0
- package/dist/types/data-dir.d.ts.map +1 -0
- package/dist/types/db.d.ts +12 -0
- package/dist/types/db.d.ts.map +1 -1
- package/dist/types/distillation.d.ts.map +1 -1
- package/dist/types/embedding-vendor.d.ts +22 -38
- package/dist/types/embedding-vendor.d.ts.map +1 -1
- package/dist/types/embedding-worker-types.d.ts +17 -12
- package/dist/types/embedding-worker-types.d.ts.map +1 -1
- package/dist/types/embedding-worker.d.ts +9 -2
- package/dist/types/embedding-worker.d.ts.map +1 -1
- package/dist/types/embedding.d.ts +30 -22
- package/dist/types/embedding.d.ts.map +1 -1
- package/dist/types/gradient.d.ts +8 -1
- package/dist/types/gradient.d.ts.map +1 -1
- package/dist/types/import/detect.d.ts +14 -0
- package/dist/types/import/detect.d.ts.map +1 -0
- package/dist/types/import/extract.d.ts +43 -0
- package/dist/types/import/extract.d.ts.map +1 -0
- package/dist/types/import/history.d.ts +40 -0
- package/dist/types/import/history.d.ts.map +1 -0
- package/dist/types/import/index.d.ts +17 -0
- package/dist/types/import/index.d.ts.map +1 -0
- package/dist/types/import/providers/aider.d.ts +2 -0
- package/dist/types/import/providers/aider.d.ts.map +1 -0
- package/dist/types/import/providers/claude-code.d.ts +2 -0
- package/dist/types/import/providers/claude-code.d.ts.map +1 -0
- package/dist/types/import/providers/cline.d.ts +2 -0
- package/dist/types/import/providers/cline.d.ts.map +1 -0
- package/dist/types/import/providers/codex.d.ts +2 -0
- package/dist/types/import/providers/codex.d.ts.map +1 -0
- package/dist/types/import/providers/continue.d.ts +2 -0
- package/dist/types/import/providers/continue.d.ts.map +1 -0
- package/dist/types/import/providers/index.d.ts +19 -0
- package/dist/types/import/providers/index.d.ts.map +1 -0
- package/dist/types/import/providers/opencode.d.ts +2 -0
- package/dist/types/import/providers/opencode.d.ts.map +1 -0
- package/dist/types/import/providers/pi.d.ts +2 -0
- package/dist/types/import/providers/pi.d.ts.map +1 -0
- package/dist/types/import/types.d.ts +82 -0
- package/dist/types/import/types.d.ts.map +1 -0
- package/dist/types/index.d.ts +4 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/instruction-detect.d.ts +66 -0
- package/dist/types/instruction-detect.d.ts.map +1 -0
- package/dist/types/log.d.ts +9 -0
- package/dist/types/log.d.ts.map +1 -1
- package/dist/types/ltm.d.ts +40 -0
- package/dist/types/ltm.d.ts.map +1 -1
- package/dist/types/pattern-extract.d.ts +7 -0
- package/dist/types/pattern-extract.d.ts.map +1 -1
- package/dist/types/prompt.d.ts +1 -1
- package/dist/types/prompt.d.ts.map +1 -1
- package/dist/types/recall.d.ts.map +1 -1
- package/dist/types/search.d.ts +5 -3
- package/dist/types/search.d.ts.map +1 -1
- package/dist/types/temporal.d.ts.map +1 -1
- package/dist/types/types.d.ts +1 -1
- package/package.json +2 -4
- package/src/agents-file.ts +41 -13
- package/src/config.ts +31 -18
- package/src/curator.ts +111 -75
- package/src/data-dir.ts +76 -0
- package/src/db.ts +110 -11
- package/src/distillation.ts +10 -2
- package/src/embedding-vendor.ts +23 -40
- package/src/embedding-worker-types.ts +19 -11
- package/src/embedding-worker.ts +111 -47
- package/src/embedding.ts +196 -171
- package/src/gradient.ts +9 -1
- package/src/import/detect.ts +37 -0
- package/src/import/extract.ts +137 -0
- package/src/import/history.ts +99 -0
- package/src/import/index.ts +45 -0
- package/src/import/providers/aider.ts +207 -0
- package/src/import/providers/claude-code.ts +339 -0
- package/src/import/providers/cline.ts +324 -0
- package/src/import/providers/codex.ts +369 -0
- package/src/import/providers/continue.ts +304 -0
- package/src/import/providers/index.ts +32 -0
- package/src/import/providers/opencode.ts +272 -0
- package/src/import/providers/pi.ts +332 -0
- package/src/import/types.ts +91 -0
- package/src/index.ts +5 -0
- package/src/instruction-detect.ts +275 -0
- package/src/log.ts +91 -3
- package/src/ltm.ts +316 -3
- package/src/pattern-extract.ts +41 -0
- package/src/prompt.ts +7 -1
- package/src/recall.ts +43 -5
- package/src/search.ts +7 -5
- package/src/temporal.ts +8 -6
- package/src/types.ts +1 -1
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code conversation history provider.
|
|
3
|
+
*
|
|
4
|
+
* Reads JSONL session files from ~/.claude/projects/<mangled-path>/<uuid>.jsonl
|
|
5
|
+
* Path mangling: project path with "/" replaced by "-"
|
|
6
|
+
* e.g. /home/byk/Code/foo → -home-byk-Code-foo
|
|
7
|
+
*/
|
|
8
|
+
import { readdirSync, readFileSync, statSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import type { AgentHistoryProvider, ConversationChunk, DetectedSession } from "../types";
|
|
12
|
+
import { registerProvider } from "./index";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// JSONL line types (only the fields we read)
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
type ClaudeCodeLine =
|
|
19
|
+
| {
|
|
20
|
+
type: "user";
|
|
21
|
+
message: { role: "user"; content: string | ContentBlock[] };
|
|
22
|
+
uuid: string;
|
|
23
|
+
timestamp: string;
|
|
24
|
+
sessionId: string;
|
|
25
|
+
}
|
|
26
|
+
| {
|
|
27
|
+
type: "assistant";
|
|
28
|
+
message: {
|
|
29
|
+
role: "assistant";
|
|
30
|
+
content: ContentBlock[];
|
|
31
|
+
model?: string;
|
|
32
|
+
};
|
|
33
|
+
uuid: string;
|
|
34
|
+
timestamp: string;
|
|
35
|
+
sessionId: string;
|
|
36
|
+
}
|
|
37
|
+
| { type: string; timestamp?: string; sessionId?: string };
|
|
38
|
+
|
|
39
|
+
type ContentBlock =
|
|
40
|
+
| { type: "text"; text: string }
|
|
41
|
+
| { type: "thinking"; thinking: string }
|
|
42
|
+
| { type: "tool_use"; name: string; input: Record<string, unknown> }
|
|
43
|
+
| { type: "tool_result"; tool_use_id: string; content: string | ContentBlock[] }
|
|
44
|
+
| { type: string };
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Constants
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
const CLAUDE_DIR = join(homedir(), ".claude", "projects");
|
|
51
|
+
const MAX_TOOL_OUTPUT_CHARS = 500;
|
|
52
|
+
const DEFAULT_MAX_TOKENS = 12288;
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Helpers
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/** Mangle a project path for Claude Code's directory naming. */
|
|
59
|
+
function manglePath(projectPath: string): string {
|
|
60
|
+
return projectPath.replace(/\//g, "-");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Estimate tokens from text length. */
|
|
64
|
+
function estimateTokens(text: string): number {
|
|
65
|
+
return Math.ceil(text.length / 3);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Truncate text to a max length, appending "..." if truncated. */
|
|
69
|
+
function truncate(text: string, max: number): string {
|
|
70
|
+
if (text.length <= max) return text;
|
|
71
|
+
return text.slice(0, max) + "...";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Extract text content from a single content block. */
|
|
75
|
+
function blockToText(block: ContentBlock): string | null {
|
|
76
|
+
switch (block.type) {
|
|
77
|
+
case "text":
|
|
78
|
+
return (block as { type: "text"; text: string }).text;
|
|
79
|
+
case "tool_use": {
|
|
80
|
+
const tu = block as { type: "tool_use"; name: string; input: Record<string, unknown> };
|
|
81
|
+
// Summarize tool input compactly
|
|
82
|
+
const inputSummary = truncate(JSON.stringify(tu.input), MAX_TOOL_OUTPUT_CHARS);
|
|
83
|
+
return `[tool: ${tu.name}] ${inputSummary}`;
|
|
84
|
+
}
|
|
85
|
+
case "tool_result": {
|
|
86
|
+
const tr = block as {
|
|
87
|
+
type: "tool_result";
|
|
88
|
+
content: string | ContentBlock[];
|
|
89
|
+
};
|
|
90
|
+
let content: string;
|
|
91
|
+
if (typeof tr.content === "string") {
|
|
92
|
+
content = tr.content;
|
|
93
|
+
} else if (Array.isArray(tr.content)) {
|
|
94
|
+
content = tr.content
|
|
95
|
+
.map((b) => {
|
|
96
|
+
if (b.type === "text") return (b as { type: "text"; text: string }).text;
|
|
97
|
+
return "";
|
|
98
|
+
})
|
|
99
|
+
.filter(Boolean)
|
|
100
|
+
.join("\n");
|
|
101
|
+
} else {
|
|
102
|
+
content = "";
|
|
103
|
+
}
|
|
104
|
+
return content ? `[tool_result] ${truncate(content, MAX_TOOL_OUTPUT_CHARS)}` : null;
|
|
105
|
+
}
|
|
106
|
+
case "thinking":
|
|
107
|
+
// Skip thinking/reasoning blocks entirely
|
|
108
|
+
return null;
|
|
109
|
+
default:
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Extract conversation text from a parsed JSONL line. */
|
|
115
|
+
function lineToText(parsed: ClaudeCodeLine): string | null {
|
|
116
|
+
if (parsed.type === "user") {
|
|
117
|
+
const msg = parsed as Extract<ClaudeCodeLine, { type: "user" }>;
|
|
118
|
+
const content = msg.message.content;
|
|
119
|
+
if (typeof content === "string") {
|
|
120
|
+
return `[user] ${content}`;
|
|
121
|
+
}
|
|
122
|
+
// Array content — extract text blocks, tool_result blocks
|
|
123
|
+
const parts = (content as ContentBlock[])
|
|
124
|
+
.map(blockToText)
|
|
125
|
+
.filter(Boolean) as string[];
|
|
126
|
+
return parts.length > 0 ? `[user] ${parts.join("\n")}` : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (parsed.type === "assistant") {
|
|
130
|
+
const msg = parsed as Extract<ClaudeCodeLine, { type: "assistant" }>;
|
|
131
|
+
const blocks = msg.message.content;
|
|
132
|
+
if (!Array.isArray(blocks)) return null;
|
|
133
|
+
const parts = blocks.map(blockToText).filter(Boolean) as string[];
|
|
134
|
+
return parts.length > 0 ? `[assistant] ${parts.join("\n")}` : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Parse a JSONL file into typed lines, skipping malformed lines. */
|
|
141
|
+
function parseJSONL(filePath: string): ClaudeCodeLine[] {
|
|
142
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
143
|
+
const lines: ClaudeCodeLine[] = [];
|
|
144
|
+
for (const line of raw.split("\n")) {
|
|
145
|
+
if (!line.trim()) continue;
|
|
146
|
+
try {
|
|
147
|
+
lines.push(JSON.parse(line) as ClaudeCodeLine);
|
|
148
|
+
} catch {
|
|
149
|
+
// Skip malformed lines
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return lines;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get metadata from a session file without reading the full contents.
|
|
157
|
+
* Reads first and last few lines for timestamps and session ID.
|
|
158
|
+
*/
|
|
159
|
+
function getSessionMetadata(
|
|
160
|
+
filePath: string,
|
|
161
|
+
): {
|
|
162
|
+
sessionId: string;
|
|
163
|
+
startedAt: number;
|
|
164
|
+
lastActivityAt: number;
|
|
165
|
+
messageCount: number;
|
|
166
|
+
estimatedTokens: number;
|
|
167
|
+
} | null {
|
|
168
|
+
let raw: string;
|
|
169
|
+
try {
|
|
170
|
+
raw = readFileSync(filePath, "utf-8");
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
176
|
+
if (lines.length === 0) return null;
|
|
177
|
+
|
|
178
|
+
let sessionId: string | undefined;
|
|
179
|
+
let startedAt = Infinity;
|
|
180
|
+
let lastActivityAt = 0;
|
|
181
|
+
let messageCount = 0;
|
|
182
|
+
|
|
183
|
+
for (const line of lines) {
|
|
184
|
+
try {
|
|
185
|
+
const parsed = JSON.parse(line) as ClaudeCodeLine;
|
|
186
|
+
if (parsed.sessionId && !sessionId) sessionId = parsed.sessionId;
|
|
187
|
+
|
|
188
|
+
if (parsed.timestamp) {
|
|
189
|
+
const ts = new Date(parsed.timestamp).getTime();
|
|
190
|
+
if (!Number.isNaN(ts)) {
|
|
191
|
+
if (ts < startedAt) startedAt = ts;
|
|
192
|
+
if (ts > lastActivityAt) lastActivityAt = ts;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (parsed.type === "user" || parsed.type === "assistant") {
|
|
197
|
+
messageCount++;
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// Skip malformed
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!sessionId || messageCount === 0) return null;
|
|
205
|
+
|
|
206
|
+
// Estimate tokens from file size (rough: ~3 chars per token, but JSONL
|
|
207
|
+
// has structural overhead so use ~5 chars per token for files)
|
|
208
|
+
const fileSize = raw.length;
|
|
209
|
+
const estimatedTokens = Math.ceil(fileSize / 5);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
sessionId,
|
|
213
|
+
startedAt: startedAt === Infinity ? Date.now() : startedAt,
|
|
214
|
+
lastActivityAt,
|
|
215
|
+
messageCount,
|
|
216
|
+
estimatedTokens,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Provider implementation
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
const claudeCodeProvider: AgentHistoryProvider = {
|
|
225
|
+
name: "claude-code",
|
|
226
|
+
displayName: "Claude Code",
|
|
227
|
+
|
|
228
|
+
detect(projectPath: string): DetectedSession[] {
|
|
229
|
+
const mangled = manglePath(projectPath);
|
|
230
|
+
const dir = join(CLAUDE_DIR, mangled);
|
|
231
|
+
|
|
232
|
+
let entries: string[];
|
|
233
|
+
try {
|
|
234
|
+
entries = readdirSync(dir);
|
|
235
|
+
} catch {
|
|
236
|
+
return []; // Directory doesn't exist
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const sessions: DetectedSession[] = [];
|
|
240
|
+
for (const entry of entries) {
|
|
241
|
+
if (!entry.endsWith(".jsonl")) continue;
|
|
242
|
+
|
|
243
|
+
const filePath = join(dir, entry);
|
|
244
|
+
try {
|
|
245
|
+
const stat = statSync(filePath);
|
|
246
|
+
if (!stat.isFile()) continue;
|
|
247
|
+
} catch {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const meta = getSessionMetadata(filePath);
|
|
252
|
+
if (!meta) continue;
|
|
253
|
+
|
|
254
|
+
// Skip trivially small sessions (< 3 messages)
|
|
255
|
+
if (meta.messageCount < 3) continue;
|
|
256
|
+
|
|
257
|
+
const dateStr = new Date(meta.startedAt).toISOString().slice(0, 10);
|
|
258
|
+
sessions.push({
|
|
259
|
+
id: filePath,
|
|
260
|
+
label: `${dateStr} (${meta.messageCount} messages)`,
|
|
261
|
+
startedAt: meta.startedAt,
|
|
262
|
+
lastActivityAt: meta.lastActivityAt,
|
|
263
|
+
estimatedTokens: meta.estimatedTokens,
|
|
264
|
+
messageCount: meta.messageCount,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Sort by most recent first
|
|
269
|
+
return sessions.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
readChunks(
|
|
273
|
+
_projectPath: string,
|
|
274
|
+
sessionIds: string[],
|
|
275
|
+
maxTokens: number = DEFAULT_MAX_TOKENS,
|
|
276
|
+
): ConversationChunk[] {
|
|
277
|
+
const chunks: ConversationChunk[] = [];
|
|
278
|
+
|
|
279
|
+
for (const filePath of sessionIds) {
|
|
280
|
+
const lines = parseJSONL(filePath);
|
|
281
|
+
|
|
282
|
+
// Extract conversation messages as text
|
|
283
|
+
const messages: { text: string; timestamp: number }[] = [];
|
|
284
|
+
for (const line of lines) {
|
|
285
|
+
const text = lineToText(line);
|
|
286
|
+
if (!text) continue;
|
|
287
|
+
|
|
288
|
+
const ts = "timestamp" in line && line.timestamp
|
|
289
|
+
? new Date(line.timestamp as string).getTime()
|
|
290
|
+
: Date.now();
|
|
291
|
+
|
|
292
|
+
messages.push({ text, timestamp: ts });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (messages.length === 0) continue;
|
|
296
|
+
|
|
297
|
+
// Build chunks respecting maxTokens boundaries
|
|
298
|
+
let currentTexts: string[] = [];
|
|
299
|
+
let currentTokens = 0;
|
|
300
|
+
let chunkStart = messages[0].timestamp;
|
|
301
|
+
let chunkIndex = 0;
|
|
302
|
+
|
|
303
|
+
const flushChunk = () => {
|
|
304
|
+
if (currentTexts.length === 0) return;
|
|
305
|
+
chunkIndex++;
|
|
306
|
+
const text = currentTexts.join("\n\n");
|
|
307
|
+
chunks.push({
|
|
308
|
+
label: `Claude Code ${new Date(chunkStart).toISOString().slice(0, 10)} (${chunkIndex})`,
|
|
309
|
+
text,
|
|
310
|
+
estimatedTokens: estimateTokens(text),
|
|
311
|
+
timestamp: chunkStart,
|
|
312
|
+
});
|
|
313
|
+
currentTexts = [];
|
|
314
|
+
currentTokens = 0;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
for (const msg of messages) {
|
|
318
|
+
const msgTokens = estimateTokens(msg.text);
|
|
319
|
+
|
|
320
|
+
// If adding this message would exceed the limit, flush first
|
|
321
|
+
if (currentTokens > 0 && currentTokens + msgTokens > maxTokens) {
|
|
322
|
+
flushChunk();
|
|
323
|
+
chunkStart = msg.timestamp;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
currentTexts.push(msg.text);
|
|
327
|
+
currentTokens += msgTokens;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Flush remaining
|
|
331
|
+
flushChunk();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return chunks;
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// Auto-register on import
|
|
339
|
+
registerProvider(claudeCodeProvider);
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cline (VS Code extension) conversation history provider.
|
|
3
|
+
*
|
|
4
|
+
* Reads JSON task files from VS Code's globalStorage for the Cline extension:
|
|
5
|
+
* ~/.vscode/data/User/globalStorage/saoudrizwan.claude-dev/tasks/<taskId>/
|
|
6
|
+
*
|
|
7
|
+
* Each task directory contains:
|
|
8
|
+
* - api_conversation_history.json — Anthropic MessageParam[] format
|
|
9
|
+
* - task_metadata.json — optional metadata
|
|
10
|
+
*
|
|
11
|
+
* The task history index at:
|
|
12
|
+
* globalStorage/saoudrizwan.claude-dev/state/taskHistory.json
|
|
13
|
+
* maps tasks to their CWD (cwdOnTaskInitialization).
|
|
14
|
+
*/
|
|
15
|
+
import { readdirSync, readFileSync, existsSync, statSync } from "fs";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { homedir } from "os";
|
|
18
|
+
import type { AgentHistoryProvider, ConversationChunk, DetectedSession } from "../types";
|
|
19
|
+
import { registerProvider } from "./index";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Constants
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const MAX_TOOL_OUTPUT_CHARS = 500;
|
|
26
|
+
const DEFAULT_MAX_TOKENS = 12288;
|
|
27
|
+
|
|
28
|
+
// Extension IDs — Cline has been published under multiple IDs
|
|
29
|
+
const EXTENSION_IDS = [
|
|
30
|
+
"saoudrizwan.claude-dev",
|
|
31
|
+
"cline.cline",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Types (Cline's Anthropic-compatible format)
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
type ContentBlock =
|
|
39
|
+
| { type: "text"; text: string }
|
|
40
|
+
| { type: "tool_use"; id: string; name: string; input: Record<string, unknown> }
|
|
41
|
+
| { type: "tool_result"; tool_use_id: string; content: string | ContentBlock[] }
|
|
42
|
+
| { type: "image"; source?: unknown }
|
|
43
|
+
| { type: string };
|
|
44
|
+
|
|
45
|
+
type ClineMessage = {
|
|
46
|
+
role: "user" | "assistant";
|
|
47
|
+
content: string | ContentBlock[];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type TaskHistoryItem = {
|
|
51
|
+
id: string;
|
|
52
|
+
ts: number;
|
|
53
|
+
task: string;
|
|
54
|
+
tokensIn?: number;
|
|
55
|
+
tokensOut?: number;
|
|
56
|
+
totalCost?: number;
|
|
57
|
+
cwdOnTaskInitialization?: string;
|
|
58
|
+
modelId?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Helpers
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
function estimateTokens(text: string): number {
|
|
66
|
+
return Math.ceil(text.length / 3);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function truncate(text: string, max: number): string {
|
|
70
|
+
if (text.length <= max) return text;
|
|
71
|
+
return text.slice(0, max) + "...";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Find the VS Code globalStorage directories to search.
|
|
76
|
+
* Checks multiple VS Code variants (stable, insiders, OSS) and extension IDs.
|
|
77
|
+
*/
|
|
78
|
+
function findGlobalStorageDirs(): string[] {
|
|
79
|
+
const home = homedir();
|
|
80
|
+
const dirs: string[] = [];
|
|
81
|
+
|
|
82
|
+
// VS Code storage paths by platform
|
|
83
|
+
const basePaths: string[] = [];
|
|
84
|
+
const platform = process.platform;
|
|
85
|
+
|
|
86
|
+
if (platform === "darwin") {
|
|
87
|
+
basePaths.push(
|
|
88
|
+
join(home, "Library", "Application Support", "Code", "User", "globalStorage"),
|
|
89
|
+
join(home, "Library", "Application Support", "Code - Insiders", "User", "globalStorage"),
|
|
90
|
+
join(home, "Library", "Application Support", "VSCodium", "User", "globalStorage"),
|
|
91
|
+
);
|
|
92
|
+
} else if (platform === "win32") {
|
|
93
|
+
const appdata = process.env.APPDATA || join(home, "AppData", "Roaming");
|
|
94
|
+
basePaths.push(
|
|
95
|
+
join(appdata, "Code", "User", "globalStorage"),
|
|
96
|
+
join(appdata, "Code - Insiders", "User", "globalStorage"),
|
|
97
|
+
join(appdata, "VSCodium", "User", "globalStorage"),
|
|
98
|
+
);
|
|
99
|
+
} else {
|
|
100
|
+
// Linux
|
|
101
|
+
const configHome = process.env.XDG_CONFIG_HOME || join(home, ".config");
|
|
102
|
+
basePaths.push(
|
|
103
|
+
join(configHome, "Code", "User", "globalStorage"),
|
|
104
|
+
join(configHome, "Code - Insiders", "User", "globalStorage"),
|
|
105
|
+
join(configHome, "VSCodium", "User", "globalStorage"),
|
|
106
|
+
);
|
|
107
|
+
// Also check the older data path
|
|
108
|
+
basePaths.push(
|
|
109
|
+
join(home, ".vscode", "data", "User", "globalStorage"),
|
|
110
|
+
join(home, ".vscode-insiders", "data", "User", "globalStorage"),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const base of basePaths) {
|
|
115
|
+
for (const extId of EXTENSION_IDS) {
|
|
116
|
+
const dir = join(base, extId);
|
|
117
|
+
if (existsSync(dir)) dirs.push(dir);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return dirs;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Load the task history index and filter by project CWD. */
|
|
125
|
+
function loadTaskHistory(
|
|
126
|
+
storageDir: string,
|
|
127
|
+
projectPath: string,
|
|
128
|
+
): TaskHistoryItem[] {
|
|
129
|
+
// Try both known locations for the history file
|
|
130
|
+
const paths = [
|
|
131
|
+
join(storageDir, "state", "taskHistory.json"),
|
|
132
|
+
join(storageDir, "taskHistory.json"),
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
for (const historyPath of paths) {
|
|
136
|
+
if (!existsSync(historyPath)) continue;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const raw = readFileSync(historyPath, "utf-8");
|
|
140
|
+
const items = JSON.parse(raw) as TaskHistoryItem[];
|
|
141
|
+
if (!Array.isArray(items)) continue;
|
|
142
|
+
|
|
143
|
+
return items.filter(
|
|
144
|
+
(item) => item.cwdOnTaskInitialization === projectPath,
|
|
145
|
+
);
|
|
146
|
+
} catch {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Read the API conversation history for a task. */
|
|
155
|
+
function readConversation(taskDir: string): ClineMessage[] {
|
|
156
|
+
const filePath = join(taskDir, "api_conversation_history.json");
|
|
157
|
+
if (!existsSync(filePath)) return [];
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
161
|
+
const messages = JSON.parse(raw) as ClineMessage[];
|
|
162
|
+
return Array.isArray(messages) ? messages : [];
|
|
163
|
+
} catch {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Convert a content block to text. */
|
|
169
|
+
function blockToText(block: ContentBlock): string | null {
|
|
170
|
+
switch (block.type) {
|
|
171
|
+
case "text":
|
|
172
|
+
return (block as { type: "text"; text: string }).text;
|
|
173
|
+
case "tool_use": {
|
|
174
|
+
const tu = block as { type: "tool_use"; name: string; input: Record<string, unknown> };
|
|
175
|
+
return `[tool: ${tu.name}] ${truncate(JSON.stringify(tu.input), MAX_TOOL_OUTPUT_CHARS)}`;
|
|
176
|
+
}
|
|
177
|
+
case "tool_result": {
|
|
178
|
+
const tr = block as { type: "tool_result"; content: string | ContentBlock[] };
|
|
179
|
+
let content: string;
|
|
180
|
+
if (typeof tr.content === "string") {
|
|
181
|
+
content = tr.content;
|
|
182
|
+
} else if (Array.isArray(tr.content)) {
|
|
183
|
+
content = tr.content
|
|
184
|
+
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
185
|
+
.map((b) => b.text)
|
|
186
|
+
.join("\n");
|
|
187
|
+
} else {
|
|
188
|
+
content = "";
|
|
189
|
+
}
|
|
190
|
+
return content ? `[tool_result] ${truncate(content, MAX_TOOL_OUTPUT_CHARS)}` : null;
|
|
191
|
+
}
|
|
192
|
+
default:
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Convert a ClineMessage to text. */
|
|
198
|
+
function messageToText(msg: ClineMessage): string | null {
|
|
199
|
+
if (typeof msg.content === "string") {
|
|
200
|
+
return msg.content ? `[${msg.role}] ${msg.content}` : null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const parts = (msg.content as ContentBlock[])
|
|
204
|
+
.map(blockToText)
|
|
205
|
+
.filter(Boolean) as string[];
|
|
206
|
+
return parts.length > 0 ? `[${msg.role}] ${parts.join("\n")}` : null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Provider implementation
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
const clineProvider: AgentHistoryProvider = {
|
|
214
|
+
name: "cline",
|
|
215
|
+
displayName: "Cline",
|
|
216
|
+
|
|
217
|
+
detect(projectPath: string): DetectedSession[] {
|
|
218
|
+
const sessions: DetectedSession[] = [];
|
|
219
|
+
const storageDirs = findGlobalStorageDirs();
|
|
220
|
+
|
|
221
|
+
for (const storageDir of storageDirs) {
|
|
222
|
+
const tasks = loadTaskHistory(storageDir, projectPath);
|
|
223
|
+
|
|
224
|
+
for (const task of tasks) {
|
|
225
|
+
const taskDir = join(storageDir, "tasks", task.id);
|
|
226
|
+
if (!existsSync(taskDir)) continue;
|
|
227
|
+
|
|
228
|
+
// Quick count of messages
|
|
229
|
+
const messages = readConversation(taskDir);
|
|
230
|
+
if (messages.length < 3) continue;
|
|
231
|
+
|
|
232
|
+
const dateStr = new Date(task.ts).toISOString().slice(0, 10);
|
|
233
|
+
const label = task.task
|
|
234
|
+
? `${dateStr} - ${truncate(task.task, 60)} (${messages.length} messages)`
|
|
235
|
+
: `${dateStr} (${messages.length} messages)`;
|
|
236
|
+
|
|
237
|
+
// Estimate tokens from file size
|
|
238
|
+
const historyFile = join(taskDir, "api_conversation_history.json");
|
|
239
|
+
let estimatedTokens = messages.length * 500;
|
|
240
|
+
try {
|
|
241
|
+
const stat = statSync(historyFile);
|
|
242
|
+
estimatedTokens = Math.ceil(stat.size / 5);
|
|
243
|
+
} catch {
|
|
244
|
+
// Use the message-count-based estimate
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
sessions.push({
|
|
248
|
+
id: taskDir,
|
|
249
|
+
label,
|
|
250
|
+
startedAt: task.ts,
|
|
251
|
+
lastActivityAt: task.ts,
|
|
252
|
+
estimatedTokens,
|
|
253
|
+
messageCount: messages.length,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return sessions.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
readChunks(
|
|
262
|
+
_projectPath: string,
|
|
263
|
+
sessionIds: string[],
|
|
264
|
+
maxTokens: number = DEFAULT_MAX_TOKENS,
|
|
265
|
+
): ConversationChunk[] {
|
|
266
|
+
const chunks: ConversationChunk[] = [];
|
|
267
|
+
|
|
268
|
+
for (const taskDir of sessionIds) {
|
|
269
|
+
const messages = readConversation(taskDir);
|
|
270
|
+
if (messages.length === 0) continue;
|
|
271
|
+
|
|
272
|
+
// Get timestamp from directory stat
|
|
273
|
+
let sessionTimestamp: number;
|
|
274
|
+
try {
|
|
275
|
+
sessionTimestamp = statSync(taskDir).mtimeMs;
|
|
276
|
+
} catch {
|
|
277
|
+
sessionTimestamp = Date.now();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const textMessages: { text: string }[] = [];
|
|
281
|
+
for (const msg of messages) {
|
|
282
|
+
const text = messageToText(msg);
|
|
283
|
+
if (text) textMessages.push({ text });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (textMessages.length === 0) continue;
|
|
287
|
+
|
|
288
|
+
// Build chunks respecting maxTokens boundaries
|
|
289
|
+
let currentTexts: string[] = [];
|
|
290
|
+
let currentTokens = 0;
|
|
291
|
+
let chunkIndex = 0;
|
|
292
|
+
|
|
293
|
+
const flushChunk = () => {
|
|
294
|
+
if (currentTexts.length === 0) return;
|
|
295
|
+
chunkIndex++;
|
|
296
|
+
const text = currentTexts.join("\n\n");
|
|
297
|
+
chunks.push({
|
|
298
|
+
label: `Cline ${new Date(sessionTimestamp).toISOString().slice(0, 10)} (${chunkIndex})`,
|
|
299
|
+
text,
|
|
300
|
+
estimatedTokens: estimateTokens(text),
|
|
301
|
+
timestamp: sessionTimestamp,
|
|
302
|
+
});
|
|
303
|
+
currentTexts = [];
|
|
304
|
+
currentTokens = 0;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
for (const msg of textMessages) {
|
|
308
|
+
const msgTokens = estimateTokens(msg.text);
|
|
309
|
+
if (currentTokens > 0 && currentTokens + msgTokens > maxTokens) {
|
|
310
|
+
flushChunk();
|
|
311
|
+
}
|
|
312
|
+
currentTexts.push(msg.text);
|
|
313
|
+
currentTokens += msgTokens;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
flushChunk();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return chunks;
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Auto-register on import
|
|
324
|
+
registerProvider(clineProvider);
|