@loreai/core 0.17.1 → 0.19.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 +85 -0
- package/dist/bun/db.d.ts.map +1 -1
- package/dist/bun/distillation.d.ts +2 -13
- 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 +35 -23
- package/dist/bun/embedding.d.ts.map +1 -1
- package/dist/bun/gradient.d.ts +17 -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 +5 -2
- package/dist/bun/index.d.ts.map +1 -1
- package/dist/bun/index.js +3150 -439
- 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 +139 -5
- 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/session-limiter.d.ts +26 -0
- package/dist/bun/session-limiter.d.ts.map +1 -0
- package/dist/bun/temporal.d.ts +2 -0
- 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 +85 -0
- package/dist/node/db.d.ts.map +1 -1
- package/dist/node/distillation.d.ts +2 -13
- 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 +35 -23
- package/dist/node/embedding.d.ts.map +1 -1
- package/dist/node/gradient.d.ts +17 -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 +5 -2
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +3150 -439
- 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 +139 -5
- 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/session-limiter.d.ts +26 -0
- package/dist/node/session-limiter.d.ts.map +1 -0
- package/dist/node/temporal.d.ts +2 -0
- 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 +85 -0
- package/dist/types/db.d.ts.map +1 -1
- package/dist/types/distillation.d.ts +2 -13
- 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 +35 -23
- package/dist/types/embedding.d.ts.map +1 -1
- package/dist/types/gradient.d.ts +17 -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 +5 -2
- 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 +139 -5
- 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/session-limiter.d.ts +26 -0
- package/dist/types/session-limiter.d.ts.map +1 -0
- package/dist/types/temporal.d.ts +2 -0
- package/dist/types/temporal.d.ts.map +1 -1
- package/dist/types/types.d.ts +1 -1
- package/package.json +3 -4
- package/src/agents-file.ts +41 -13
- package/src/config.ts +31 -18
- package/src/curator.ts +163 -75
- package/src/data-dir.ts +76 -0
- package/src/db.ts +457 -11
- package/src/distillation.ts +65 -16
- 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 +224 -174
- package/src/gradient.ts +192 -75
- 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 +13 -0
- package/src/instruction-detect.ts +275 -0
- package/src/log.ts +91 -3
- package/src/ltm.ts +789 -41
- 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/session-limiter.ts +47 -0
- package/src/temporal.ts +18 -6
- package/src/types.ts +1 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation import system — detects and imports knowledge from
|
|
3
|
+
* external AI coding agent conversation histories.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Types
|
|
7
|
+
export type {
|
|
8
|
+
ConversationChunk,
|
|
9
|
+
DetectedSession,
|
|
10
|
+
DetectionResult,
|
|
11
|
+
AgentHistoryProvider,
|
|
12
|
+
} from "./types";
|
|
13
|
+
|
|
14
|
+
// Detection
|
|
15
|
+
export { detectAll } from "./detect";
|
|
16
|
+
|
|
17
|
+
// Provider registry
|
|
18
|
+
export {
|
|
19
|
+
registerProvider,
|
|
20
|
+
getProviders,
|
|
21
|
+
getProvider,
|
|
22
|
+
clearProviders,
|
|
23
|
+
} from "./providers";
|
|
24
|
+
|
|
25
|
+
// Extraction (lazy — avoid pulling in LLM/curator deps for detection-only use)
|
|
26
|
+
export { extractKnowledge, type ExtractionProgress, type ExtractionResult } from "./extract";
|
|
27
|
+
|
|
28
|
+
// Idempotency
|
|
29
|
+
export {
|
|
30
|
+
isImported,
|
|
31
|
+
recordImport,
|
|
32
|
+
computeHash,
|
|
33
|
+
listImports,
|
|
34
|
+
type ImportRecord,
|
|
35
|
+
} from "./history";
|
|
36
|
+
|
|
37
|
+
// Register built-in providers on first import.
|
|
38
|
+
// Each provider module calls registerProvider() at load time.
|
|
39
|
+
import "./providers/claude-code";
|
|
40
|
+
import "./providers/codex";
|
|
41
|
+
import "./providers/opencode";
|
|
42
|
+
import "./providers/cline";
|
|
43
|
+
import "./providers/continue";
|
|
44
|
+
import "./providers/pi";
|
|
45
|
+
import "./providers/aider";
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aider conversation history provider.
|
|
3
|
+
*
|
|
4
|
+
* Reads from Aider's per-project chat history file:
|
|
5
|
+
* <project-dir>/.aider.chat.history.md
|
|
6
|
+
*
|
|
7
|
+
* Format: Markdown with role headers like "#### user" / "#### assistant"
|
|
8
|
+
* separated by horizontal rules (---).
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import type { AgentHistoryProvider, ConversationChunk, DetectedSession } from "../types";
|
|
13
|
+
import { registerProvider } from "./index";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Constants
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
const HISTORY_FILE = ".aider.chat.history.md";
|
|
20
|
+
const DEFAULT_MAX_TOKENS = 12288;
|
|
21
|
+
|
|
22
|
+
// Aider uses "#### role" headers and "---" separators
|
|
23
|
+
const ROLE_HEADER_RE = /^####\s+(user|assistant|system)\s*$/i;
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Helpers
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
function estimateTokens(text: string): number {
|
|
30
|
+
return Math.ceil(text.length / 3);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type ParsedMessage = {
|
|
34
|
+
role: string;
|
|
35
|
+
text: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse Aider's markdown chat history into messages.
|
|
40
|
+
*
|
|
41
|
+
* Format:
|
|
42
|
+
* ```
|
|
43
|
+
* #### user
|
|
44
|
+
* message text here
|
|
45
|
+
*
|
|
46
|
+
* #### assistant
|
|
47
|
+
* response text here
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* Messages are separated by `---` or by new `#### role` headers.
|
|
51
|
+
*/
|
|
52
|
+
function parseAiderHistory(content: string): ParsedMessage[] {
|
|
53
|
+
const lines = content.split("\n");
|
|
54
|
+
const messages: ParsedMessage[] = [];
|
|
55
|
+
let currentRole: string | null = null;
|
|
56
|
+
let currentLines: string[] = [];
|
|
57
|
+
|
|
58
|
+
const flush = () => {
|
|
59
|
+
if (currentRole && currentLines.length > 0) {
|
|
60
|
+
const text = currentLines.join("\n").trim();
|
|
61
|
+
if (text) {
|
|
62
|
+
messages.push({ role: currentRole, text });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
currentLines = [];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
// Check for role header
|
|
70
|
+
const match = ROLE_HEADER_RE.exec(line);
|
|
71
|
+
if (match) {
|
|
72
|
+
flush();
|
|
73
|
+
currentRole = match[1].toLowerCase();
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check for separator — starts a new conversation turn
|
|
78
|
+
if (line.trim() === "---") {
|
|
79
|
+
flush();
|
|
80
|
+
currentRole = null;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Accumulate content if we're in a message
|
|
85
|
+
if (currentRole) {
|
|
86
|
+
currentLines.push(line);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Flush final message
|
|
91
|
+
flush();
|
|
92
|
+
|
|
93
|
+
return messages;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Provider implementation
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
const aiderProvider: AgentHistoryProvider = {
|
|
101
|
+
name: "aider",
|
|
102
|
+
displayName: "Aider",
|
|
103
|
+
|
|
104
|
+
detect(projectPath: string): DetectedSession[] {
|
|
105
|
+
const filePath = join(projectPath, HISTORY_FILE);
|
|
106
|
+
if (!existsSync(filePath)) return [];
|
|
107
|
+
|
|
108
|
+
let stat;
|
|
109
|
+
try {
|
|
110
|
+
stat = statSync(filePath);
|
|
111
|
+
} catch {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!stat.isFile() || stat.size === 0) return [];
|
|
116
|
+
|
|
117
|
+
// Quick scan to count messages without full parsing
|
|
118
|
+
let content: string;
|
|
119
|
+
try {
|
|
120
|
+
content = readFileSync(filePath, "utf-8");
|
|
121
|
+
} catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const messages = parseAiderHistory(content);
|
|
126
|
+
if (messages.length < 3) return [];
|
|
127
|
+
|
|
128
|
+
const estimatedTokens = estimateTokens(content);
|
|
129
|
+
|
|
130
|
+
return [
|
|
131
|
+
{
|
|
132
|
+
id: filePath,
|
|
133
|
+
label: `Chat history (${messages.length} messages, ${Math.round(stat.size / 1024)}KB)`,
|
|
134
|
+
startedAt: stat.birthtimeMs || stat.ctimeMs,
|
|
135
|
+
lastActivityAt: stat.mtimeMs,
|
|
136
|
+
estimatedTokens,
|
|
137
|
+
messageCount: messages.length,
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
readChunks(
|
|
143
|
+
projectPath: string,
|
|
144
|
+
sessionIds: string[],
|
|
145
|
+
maxTokens: number = DEFAULT_MAX_TOKENS,
|
|
146
|
+
): ConversationChunk[] {
|
|
147
|
+
const chunks: ConversationChunk[] = [];
|
|
148
|
+
|
|
149
|
+
for (const filePath of sessionIds) {
|
|
150
|
+
let content: string;
|
|
151
|
+
try {
|
|
152
|
+
content = readFileSync(filePath, "utf-8");
|
|
153
|
+
} catch {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const messages = parseAiderHistory(content);
|
|
158
|
+
if (messages.length === 0) continue;
|
|
159
|
+
|
|
160
|
+
// Get file mtime for timestamp
|
|
161
|
+
let fileTimestamp: number;
|
|
162
|
+
try {
|
|
163
|
+
fileTimestamp = statSync(filePath).mtimeMs;
|
|
164
|
+
} catch {
|
|
165
|
+
fileTimestamp = Date.now();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Build chunks respecting maxTokens boundaries
|
|
169
|
+
let currentTexts: string[] = [];
|
|
170
|
+
let currentTokens = 0;
|
|
171
|
+
let chunkIndex = 0;
|
|
172
|
+
|
|
173
|
+
const flushChunk = () => {
|
|
174
|
+
if (currentTexts.length === 0) return;
|
|
175
|
+
chunkIndex++;
|
|
176
|
+
const text = currentTexts.join("\n\n");
|
|
177
|
+
chunks.push({
|
|
178
|
+
label: `Aider history (${chunkIndex})`,
|
|
179
|
+
text,
|
|
180
|
+
estimatedTokens: estimateTokens(text),
|
|
181
|
+
timestamp: fileTimestamp,
|
|
182
|
+
});
|
|
183
|
+
currentTexts = [];
|
|
184
|
+
currentTokens = 0;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
for (const msg of messages) {
|
|
188
|
+
const formatted = `[${msg.role}] ${msg.text}`;
|
|
189
|
+
const msgTokens = estimateTokens(formatted);
|
|
190
|
+
|
|
191
|
+
if (currentTokens > 0 && currentTokens + msgTokens > maxTokens) {
|
|
192
|
+
flushChunk();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
currentTexts.push(formatted);
|
|
196
|
+
currentTokens += msgTokens;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
flushChunk();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return chunks;
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Auto-register on import
|
|
207
|
+
registerProvider(aiderProvider);
|
|
@@ -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);
|