@loreai/core 0.17.1 → 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,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode conversation history provider.
|
|
3
|
+
*
|
|
4
|
+
* Reads from OpenCode's SQLite database at ~/.local/share/opencode/opencode.db.
|
|
5
|
+
* The message.data and part.data JSON fields use a format very close to lore's
|
|
6
|
+
* own LoreMessage/LorePart types (since lore was designed for OpenCode).
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { Database } from "#db/driver";
|
|
12
|
+
import type { AgentHistoryProvider, ConversationChunk, DetectedSession } from "../types";
|
|
13
|
+
import { registerProvider } from "./index";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Constants
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
const OPENCODE_DB_PATH = join(
|
|
20
|
+
process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"),
|
|
21
|
+
"opencode",
|
|
22
|
+
"opencode.db",
|
|
23
|
+
);
|
|
24
|
+
const MAX_TOOL_OUTPUT_CHARS = 500;
|
|
25
|
+
const DEFAULT_MAX_TOKENS = 12288;
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Helpers
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function estimateTokens(text: string): number {
|
|
32
|
+
return Math.ceil(text.length / 3);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function truncate(text: string, max: number): string {
|
|
36
|
+
if (text.length <= max) return text;
|
|
37
|
+
return text.slice(0, max) + "...";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Open OpenCode's database read-only.
|
|
42
|
+
* Returns null if the DB doesn't exist or can't be opened.
|
|
43
|
+
*
|
|
44
|
+
* Bun's `Database` uses `{ readonly: true }` while Node.js's `DatabaseSync`
|
|
45
|
+
* uses `{ readOnly: true }`. We pass both via a cast to cover both runtimes.
|
|
46
|
+
*/
|
|
47
|
+
function openDB(): InstanceType<typeof Database> | null {
|
|
48
|
+
if (!existsSync(OPENCODE_DB_PATH)) return null;
|
|
49
|
+
try {
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
return new Database(OPENCODE_DB_PATH, { readonly: true, readOnly: true } as any);
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Check if a table exists in the database. */
|
|
58
|
+
function tableExists(database: InstanceType<typeof Database>, table: string): boolean {
|
|
59
|
+
const row = database
|
|
60
|
+
.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?")
|
|
61
|
+
.get(table) as { name: string } | null;
|
|
62
|
+
return row != null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type PartData = {
|
|
66
|
+
type: string;
|
|
67
|
+
text?: string;
|
|
68
|
+
tool?: string;
|
|
69
|
+
state?: {
|
|
70
|
+
status: string;
|
|
71
|
+
output?: string;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Convert part data rows into conversation text. */
|
|
76
|
+
function partsToConversationText(parts: PartData[]): string {
|
|
77
|
+
const segments: string[] = [];
|
|
78
|
+
for (const part of parts) {
|
|
79
|
+
if (part.type === "text" && part.text) {
|
|
80
|
+
segments.push(part.text);
|
|
81
|
+
} else if (part.type === "tool" && part.tool && part.state?.status === "completed" && part.state.output) {
|
|
82
|
+
segments.push(`[tool: ${part.tool}] ${truncate(part.state.output, MAX_TOOL_OUTPUT_CHARS)}`);
|
|
83
|
+
}
|
|
84
|
+
// Skip reasoning, step-start, and other non-text parts
|
|
85
|
+
}
|
|
86
|
+
return segments.join("\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Provider implementation
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
const opencodeProvider: AgentHistoryProvider = {
|
|
94
|
+
name: "opencode",
|
|
95
|
+
displayName: "OpenCode",
|
|
96
|
+
|
|
97
|
+
detect(projectPath: string): DetectedSession[] {
|
|
98
|
+
const database = openDB();
|
|
99
|
+
if (!database) return [];
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Check required tables exist
|
|
103
|
+
if (!tableExists(database, "project") || !tableExists(database, "session") || !tableExists(database, "message")) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Find the project by worktree path
|
|
108
|
+
const project = database
|
|
109
|
+
.query("SELECT id FROM project WHERE worktree = ?")
|
|
110
|
+
.get(projectPath) as { id: string } | null;
|
|
111
|
+
if (!project) return [];
|
|
112
|
+
|
|
113
|
+
// Get sessions with message counts
|
|
114
|
+
const sessions = database
|
|
115
|
+
.query(
|
|
116
|
+
`SELECT s.id, s.title, s.time_created, s.time_updated,
|
|
117
|
+
(SELECT COUNT(*) FROM message m WHERE m.session_id = s.id) as msg_count
|
|
118
|
+
FROM session s
|
|
119
|
+
WHERE s.project_id = ? AND s.parent_id IS NULL
|
|
120
|
+
ORDER BY s.time_updated DESC`,
|
|
121
|
+
)
|
|
122
|
+
.all(project.id) as Array<{
|
|
123
|
+
id: string;
|
|
124
|
+
title: string;
|
|
125
|
+
time_created: number;
|
|
126
|
+
time_updated: number;
|
|
127
|
+
msg_count: number;
|
|
128
|
+
}>;
|
|
129
|
+
|
|
130
|
+
const results: DetectedSession[] = [];
|
|
131
|
+
for (const sess of sessions) {
|
|
132
|
+
// Skip trivially small sessions
|
|
133
|
+
if (sess.msg_count < 3) continue;
|
|
134
|
+
|
|
135
|
+
// Estimate tokens from message count (rough: ~500 tokens/message avg)
|
|
136
|
+
const estimatedTokens = sess.msg_count * 500;
|
|
137
|
+
const dateStr = new Date(sess.time_created).toISOString().slice(0, 10);
|
|
138
|
+
const label = sess.title
|
|
139
|
+
? `${dateStr} - ${sess.title} (${sess.msg_count} messages)`
|
|
140
|
+
: `${dateStr} (${sess.msg_count} messages)`;
|
|
141
|
+
|
|
142
|
+
results.push({
|
|
143
|
+
id: sess.id,
|
|
144
|
+
label,
|
|
145
|
+
startedAt: sess.time_created,
|
|
146
|
+
lastActivityAt: sess.time_updated,
|
|
147
|
+
estimatedTokens,
|
|
148
|
+
messageCount: sess.msg_count,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return results;
|
|
153
|
+
} finally {
|
|
154
|
+
database.close();
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
readChunks(
|
|
159
|
+
_projectPath: string,
|
|
160
|
+
sessionIds: string[],
|
|
161
|
+
maxTokens: number = DEFAULT_MAX_TOKENS,
|
|
162
|
+
): ConversationChunk[] {
|
|
163
|
+
const database = openDB();
|
|
164
|
+
if (!database) return [];
|
|
165
|
+
|
|
166
|
+
const chunks: ConversationChunk[] = [];
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const hasParts = tableExists(database, "part");
|
|
170
|
+
|
|
171
|
+
for (const sessionId of sessionIds) {
|
|
172
|
+
// Get messages ordered by time
|
|
173
|
+
const messages = database
|
|
174
|
+
.query(
|
|
175
|
+
`SELECT id, data, time_created FROM message
|
|
176
|
+
WHERE session_id = ?
|
|
177
|
+
ORDER BY time_created ASC`,
|
|
178
|
+
)
|
|
179
|
+
.all(sessionId) as Array<{
|
|
180
|
+
id: string;
|
|
181
|
+
data: string;
|
|
182
|
+
time_created: number;
|
|
183
|
+
}>;
|
|
184
|
+
|
|
185
|
+
if (messages.length === 0) continue;
|
|
186
|
+
|
|
187
|
+
const textMessages: { text: string; timestamp: number }[] = [];
|
|
188
|
+
|
|
189
|
+
for (const msg of messages) {
|
|
190
|
+
let msgData: { role?: string };
|
|
191
|
+
try {
|
|
192
|
+
msgData = JSON.parse(msg.data);
|
|
193
|
+
} catch {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const role = msgData.role ?? "unknown";
|
|
198
|
+
let contentText = "";
|
|
199
|
+
|
|
200
|
+
if (hasParts) {
|
|
201
|
+
// Read parts for this message
|
|
202
|
+
const parts = database
|
|
203
|
+
.query(
|
|
204
|
+
`SELECT data FROM part
|
|
205
|
+
WHERE message_id = ?
|
|
206
|
+
ORDER BY time_created ASC`,
|
|
207
|
+
)
|
|
208
|
+
.all(msg.id) as Array<{ data: string }>;
|
|
209
|
+
|
|
210
|
+
const parsedParts: PartData[] = [];
|
|
211
|
+
for (const p of parts) {
|
|
212
|
+
try {
|
|
213
|
+
parsedParts.push(JSON.parse(p.data) as PartData);
|
|
214
|
+
} catch {
|
|
215
|
+
// Skip malformed parts
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
contentText = partsToConversationText(parsedParts);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!contentText.trim()) continue;
|
|
222
|
+
|
|
223
|
+
textMessages.push({
|
|
224
|
+
text: `[${role}] ${contentText}`,
|
|
225
|
+
timestamp: msg.time_created,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (textMessages.length === 0) continue;
|
|
230
|
+
|
|
231
|
+
// Build chunks respecting maxTokens boundaries
|
|
232
|
+
let currentTexts: string[] = [];
|
|
233
|
+
let currentTokens = 0;
|
|
234
|
+
let chunkStart = textMessages[0].timestamp;
|
|
235
|
+
let chunkIndex = 0;
|
|
236
|
+
|
|
237
|
+
const flushChunk = () => {
|
|
238
|
+
if (currentTexts.length === 0) return;
|
|
239
|
+
chunkIndex++;
|
|
240
|
+
const text = currentTexts.join("\n\n");
|
|
241
|
+
chunks.push({
|
|
242
|
+
label: `OpenCode ${new Date(chunkStart).toISOString().slice(0, 10)} (${chunkIndex})`,
|
|
243
|
+
text,
|
|
244
|
+
estimatedTokens: estimateTokens(text),
|
|
245
|
+
timestamp: chunkStart,
|
|
246
|
+
});
|
|
247
|
+
currentTexts = [];
|
|
248
|
+
currentTokens = 0;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
for (const msg of textMessages) {
|
|
252
|
+
const msgTokens = estimateTokens(msg.text);
|
|
253
|
+
if (currentTokens > 0 && currentTokens + msgTokens > maxTokens) {
|
|
254
|
+
flushChunk();
|
|
255
|
+
chunkStart = msg.timestamp;
|
|
256
|
+
}
|
|
257
|
+
currentTexts.push(msg.text);
|
|
258
|
+
currentTokens += msgTokens;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
flushChunk();
|
|
262
|
+
}
|
|
263
|
+
} finally {
|
|
264
|
+
database.close();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return chunks;
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// Auto-register on import
|
|
272
|
+
registerProvider(opencodeProvider);
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi coding agent conversation history provider.
|
|
3
|
+
*
|
|
4
|
+
* Reads JSONL session files from ~/.pi/agent/sessions/<encoded-cwd>/<timestamp>_<uuid>.jsonl
|
|
5
|
+
*
|
|
6
|
+
* CWD encoding: "--<cwd-with-slashes-replaced-by-dashes>--"
|
|
7
|
+
* e.g. /home/byk/Code/foo → --home-byk-Code-foo--
|
|
8
|
+
*
|
|
9
|
+
* Pi uses a tree-structured session format where each entry has id/parentId.
|
|
10
|
+
* We reconstruct the linear conversation by following the chain from root to
|
|
11
|
+
* the latest leaf.
|
|
12
|
+
*/
|
|
13
|
+
import { readdirSync, readFileSync, existsSync, statSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
import type { AgentHistoryProvider, ConversationChunk, DetectedSession } from "../types";
|
|
17
|
+
import { registerProvider } from "./index";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Constants
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const PI_DIR = join(homedir(), ".pi", "agent", "sessions");
|
|
24
|
+
const MAX_TOOL_OUTPUT_CHARS = 500;
|
|
25
|
+
const DEFAULT_MAX_TOKENS = 12288;
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// JSONL types (only the fields we read)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
type PiLine =
|
|
32
|
+
| {
|
|
33
|
+
type: "session";
|
|
34
|
+
id: string;
|
|
35
|
+
timestamp: string;
|
|
36
|
+
cwd: string;
|
|
37
|
+
version?: number;
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
type: "message";
|
|
41
|
+
id: string;
|
|
42
|
+
parentId: string;
|
|
43
|
+
timestamp: string;
|
|
44
|
+
message: {
|
|
45
|
+
role: "user" | "assistant";
|
|
46
|
+
content: string;
|
|
47
|
+
provider?: string;
|
|
48
|
+
model?: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
| {
|
|
52
|
+
type: "compaction";
|
|
53
|
+
id: string;
|
|
54
|
+
parentId: string;
|
|
55
|
+
summary: string;
|
|
56
|
+
}
|
|
57
|
+
| {
|
|
58
|
+
type: "custom" | "custom_message";
|
|
59
|
+
id: string;
|
|
60
|
+
parentId: string;
|
|
61
|
+
customType?: string;
|
|
62
|
+
content?: string;
|
|
63
|
+
}
|
|
64
|
+
| {
|
|
65
|
+
type: string;
|
|
66
|
+
id?: string;
|
|
67
|
+
parentId?: string;
|
|
68
|
+
timestamp?: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Helpers
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
function estimateTokens(text: string): number {
|
|
76
|
+
return Math.ceil(text.length / 3);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function truncate(text: string, max: number): string {
|
|
80
|
+
if (text.length <= max) return text;
|
|
81
|
+
return text.slice(0, max) + "...";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Encode a CWD path to Pi's directory naming convention.
|
|
86
|
+
* /home/byk/Code/foo → --home-byk-Code-foo--
|
|
87
|
+
*/
|
|
88
|
+
function encodeCwd(cwd: string): string {
|
|
89
|
+
// Remove leading slash, replace remaining slashes with dashes
|
|
90
|
+
const encoded = cwd.replace(/^\//, "").replace(/\//g, "-");
|
|
91
|
+
return `--${encoded}--`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Parse a JSONL file into typed lines. */
|
|
95
|
+
function parseJSONL(filePath: string): PiLine[] {
|
|
96
|
+
let raw: string;
|
|
97
|
+
try {
|
|
98
|
+
raw = readFileSync(filePath, "utf-8");
|
|
99
|
+
} catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const lines: PiLine[] = [];
|
|
104
|
+
for (const line of raw.split("\n")) {
|
|
105
|
+
if (!line.trim()) continue;
|
|
106
|
+
try {
|
|
107
|
+
lines.push(JSON.parse(line) as PiLine);
|
|
108
|
+
} catch {
|
|
109
|
+
// Skip malformed
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return lines;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Reconstruct the linear conversation from tree-structured entries.
|
|
117
|
+
*
|
|
118
|
+
* Pi sessions use a tree structure where each entry has id/parentId.
|
|
119
|
+
* Multiple branches can exist (e.g. user edits a message, creating a fork).
|
|
120
|
+
* We follow the longest path from root to the latest leaf to get the
|
|
121
|
+
* "main" conversation thread.
|
|
122
|
+
*/
|
|
123
|
+
function linearize(lines: PiLine[]): PiLine[] {
|
|
124
|
+
if (lines.length === 0) return [];
|
|
125
|
+
|
|
126
|
+
// Build adjacency map: parentId → children
|
|
127
|
+
const children = new Map<string, PiLine[]>();
|
|
128
|
+
const byId = new Map<string, PiLine>();
|
|
129
|
+
let rootLine: PiLine | null = null;
|
|
130
|
+
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
if (line.type === "session") {
|
|
133
|
+
rootLine = line;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!line.id) continue;
|
|
138
|
+
byId.set(line.id, line);
|
|
139
|
+
|
|
140
|
+
const pid = (line as { parentId?: string }).parentId;
|
|
141
|
+
if (pid) {
|
|
142
|
+
const siblings = children.get(pid) ?? [];
|
|
143
|
+
siblings.push(line);
|
|
144
|
+
children.set(pid, siblings);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!rootLine || !rootLine.id) return lines.filter((l) => l.type === "message");
|
|
149
|
+
|
|
150
|
+
// Walk from root, always picking the child with the latest timestamp
|
|
151
|
+
// (or the last one appended if timestamps are equal)
|
|
152
|
+
const result: PiLine[] = [];
|
|
153
|
+
let currentId: string | undefined = rootLine.id;
|
|
154
|
+
|
|
155
|
+
while (currentId) {
|
|
156
|
+
const kids = children.get(currentId);
|
|
157
|
+
if (!kids || kids.length === 0) break;
|
|
158
|
+
|
|
159
|
+
// Pick the last child (append-only means last is most recent on the main branch)
|
|
160
|
+
const next = kids[kids.length - 1];
|
|
161
|
+
result.push(next);
|
|
162
|
+
currentId = next.id;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Get session metadata from a JSONL file. */
|
|
169
|
+
function getSessionMeta(
|
|
170
|
+
filePath: string,
|
|
171
|
+
): {
|
|
172
|
+
id: string;
|
|
173
|
+
cwd: string;
|
|
174
|
+
timestamp: number;
|
|
175
|
+
messageCount: number;
|
|
176
|
+
fileSize: number;
|
|
177
|
+
} | null {
|
|
178
|
+
const lines = parseJSONL(filePath);
|
|
179
|
+
if (lines.length === 0) return null;
|
|
180
|
+
|
|
181
|
+
const header = lines[0];
|
|
182
|
+
if (header.type !== "session") return null;
|
|
183
|
+
const session = header as Extract<PiLine, { type: "session" }>;
|
|
184
|
+
|
|
185
|
+
const messageCount = lines.filter((l) => l.type === "message").length;
|
|
186
|
+
let fileSize: number;
|
|
187
|
+
try {
|
|
188
|
+
fileSize = statSync(filePath).size;
|
|
189
|
+
} catch {
|
|
190
|
+
fileSize = 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const ts = new Date(session.timestamp).getTime();
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
id: session.id,
|
|
197
|
+
cwd: session.cwd,
|
|
198
|
+
timestamp: Number.isNaN(ts) ? Date.now() : ts,
|
|
199
|
+
messageCount,
|
|
200
|
+
fileSize,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Provider implementation
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
const piProvider: AgentHistoryProvider = {
|
|
209
|
+
name: "pi",
|
|
210
|
+
displayName: "Pi",
|
|
211
|
+
|
|
212
|
+
detect(projectPath: string): DetectedSession[] {
|
|
213
|
+
const encoded = encodeCwd(projectPath);
|
|
214
|
+
const dir = join(PI_DIR, encoded);
|
|
215
|
+
|
|
216
|
+
let entries: string[];
|
|
217
|
+
try {
|
|
218
|
+
entries = readdirSync(dir);
|
|
219
|
+
} catch {
|
|
220
|
+
return []; // Directory doesn't exist
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const sessions: DetectedSession[] = [];
|
|
224
|
+
for (const entry of entries) {
|
|
225
|
+
if (!entry.endsWith(".jsonl")) continue;
|
|
226
|
+
|
|
227
|
+
const filePath = join(dir, entry);
|
|
228
|
+
const meta = getSessionMeta(filePath);
|
|
229
|
+
if (!meta) continue;
|
|
230
|
+
|
|
231
|
+
// Skip trivially small sessions
|
|
232
|
+
if (meta.messageCount < 3) continue;
|
|
233
|
+
|
|
234
|
+
const dateStr = new Date(meta.timestamp).toISOString().slice(0, 10);
|
|
235
|
+
const estimatedTokens = Math.ceil(meta.fileSize / 5);
|
|
236
|
+
|
|
237
|
+
sessions.push({
|
|
238
|
+
id: filePath,
|
|
239
|
+
label: `${dateStr} (${meta.messageCount} messages)`,
|
|
240
|
+
startedAt: meta.timestamp,
|
|
241
|
+
lastActivityAt: meta.timestamp,
|
|
242
|
+
estimatedTokens,
|
|
243
|
+
messageCount: meta.messageCount,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return sessions.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
readChunks(
|
|
251
|
+
_projectPath: string,
|
|
252
|
+
sessionIds: string[],
|
|
253
|
+
maxTokens: number = DEFAULT_MAX_TOKENS,
|
|
254
|
+
): ConversationChunk[] {
|
|
255
|
+
const chunks: ConversationChunk[] = [];
|
|
256
|
+
|
|
257
|
+
for (const filePath of sessionIds) {
|
|
258
|
+
const allLines = parseJSONL(filePath);
|
|
259
|
+
const linearLines = linearize(allLines);
|
|
260
|
+
|
|
261
|
+
// Find session timestamp for labeling
|
|
262
|
+
let sessionTimestamp = Date.now();
|
|
263
|
+
const header = allLines.find((l) => l.type === "session");
|
|
264
|
+
if (header?.type === "session") {
|
|
265
|
+
const session = header as Extract<PiLine, { type: "session" }>;
|
|
266
|
+
const ts = new Date(session.timestamp).getTime();
|
|
267
|
+
if (!Number.isNaN(ts)) sessionTimestamp = ts;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const messages: { text: string; timestamp: number }[] = [];
|
|
271
|
+
|
|
272
|
+
for (const line of linearLines) {
|
|
273
|
+
if (line.type === "message") {
|
|
274
|
+
const msg = line as Extract<PiLine, { type: "message" }>;
|
|
275
|
+
const content = msg.message.content;
|
|
276
|
+
if (!content) continue;
|
|
277
|
+
|
|
278
|
+
const ts = new Date(msg.timestamp).getTime();
|
|
279
|
+
messages.push({
|
|
280
|
+
text: `[${msg.message.role}] ${content}`,
|
|
281
|
+
timestamp: Number.isNaN(ts) ? sessionTimestamp : ts,
|
|
282
|
+
});
|
|
283
|
+
} else if (line.type === "compaction") {
|
|
284
|
+
const comp = line as Extract<PiLine, { type: "compaction" }>;
|
|
285
|
+
if (comp.summary) {
|
|
286
|
+
messages.push({
|
|
287
|
+
text: `[summary] ${truncate(comp.summary, MAX_TOOL_OUTPUT_CHARS * 2)}`,
|
|
288
|
+
timestamp: sessionTimestamp,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (messages.length === 0) continue;
|
|
295
|
+
|
|
296
|
+
// Build chunks respecting maxTokens boundaries
|
|
297
|
+
let currentTexts: string[] = [];
|
|
298
|
+
let currentTokens = 0;
|
|
299
|
+
let chunkIndex = 0;
|
|
300
|
+
|
|
301
|
+
const flushChunk = () => {
|
|
302
|
+
if (currentTexts.length === 0) return;
|
|
303
|
+
chunkIndex++;
|
|
304
|
+
const text = currentTexts.join("\n\n");
|
|
305
|
+
chunks.push({
|
|
306
|
+
label: `Pi ${new Date(sessionTimestamp).toISOString().slice(0, 10)} (${chunkIndex})`,
|
|
307
|
+
text,
|
|
308
|
+
estimatedTokens: estimateTokens(text),
|
|
309
|
+
timestamp: sessionTimestamp,
|
|
310
|
+
});
|
|
311
|
+
currentTexts = [];
|
|
312
|
+
currentTokens = 0;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
for (const msg of messages) {
|
|
316
|
+
const msgTokens = estimateTokens(msg.text);
|
|
317
|
+
if (currentTokens > 0 && currentTokens + msgTokens > maxTokens) {
|
|
318
|
+
flushChunk();
|
|
319
|
+
}
|
|
320
|
+
currentTexts.push(msg.text);
|
|
321
|
+
currentTokens += msgTokens;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
flushChunk();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return chunks;
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// Auto-register on import
|
|
332
|
+
registerProvider(piProvider);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the conversation import system.
|
|
3
|
+
*
|
|
4
|
+
* The import system detects and reads conversation history from external
|
|
5
|
+
* AI coding agents (Claude Code, OpenCode, Aider, etc.) and extracts
|
|
6
|
+
* long-term knowledge entries via the curator LLM prompt.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A chunk of conversation text from a foreign agent, ready for
|
|
11
|
+
* knowledge extraction. Chunks are sized to fit within a single
|
|
12
|
+
* LLM curator call (~12K tokens).
|
|
13
|
+
*/
|
|
14
|
+
export type ConversationChunk = {
|
|
15
|
+
/** Human-readable label, e.g. "Claude Code session 2025-05-10 (1 of 3)" */
|
|
16
|
+
label: string;
|
|
17
|
+
/** Plain text of the conversation (role-prefixed lines) */
|
|
18
|
+
text: string;
|
|
19
|
+
/** Estimated token count (~text.length / 3) */
|
|
20
|
+
estimatedTokens: number;
|
|
21
|
+
/** When this chunk's messages were created (epoch ms), for sorting */
|
|
22
|
+
timestamp: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A detected session from a foreign agent.
|
|
27
|
+
*/
|
|
28
|
+
export type DetectedSession = {
|
|
29
|
+
/** Unique identifier (agent-specific: file path, session UUID, etc.) */
|
|
30
|
+
id: string;
|
|
31
|
+
/** Human-readable label */
|
|
32
|
+
label: string;
|
|
33
|
+
/** When this session started (epoch ms) */
|
|
34
|
+
startedAt: number;
|
|
35
|
+
/** When this session last had activity (epoch ms) */
|
|
36
|
+
lastActivityAt: number;
|
|
37
|
+
/** Estimated total tokens across all messages */
|
|
38
|
+
estimatedTokens: number;
|
|
39
|
+
/** Number of conversation messages (user + assistant) */
|
|
40
|
+
messageCount: number;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Summary of what was detected for a project from one agent.
|
|
45
|
+
*/
|
|
46
|
+
export type DetectionResult = {
|
|
47
|
+
/** Internal agent name (e.g. "claude-code", "opencode", "aider") */
|
|
48
|
+
agentName: string;
|
|
49
|
+
/** Human-readable agent name (e.g. "Claude Code") */
|
|
50
|
+
agentDisplayName: string;
|
|
51
|
+
/** Sessions found for this project */
|
|
52
|
+
sessions: DetectedSession[];
|
|
53
|
+
/** Total estimated tokens across all sessions */
|
|
54
|
+
totalTokens: number;
|
|
55
|
+
/** Total messages across all sessions */
|
|
56
|
+
totalMessages: number;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Adapter interface for reading conversation history from a specific
|
|
61
|
+
* AI coding agent. Each agent implements this interface.
|
|
62
|
+
*/
|
|
63
|
+
export interface AgentHistoryProvider {
|
|
64
|
+
/** Internal name (e.g. "claude-code", "opencode", "aider") */
|
|
65
|
+
readonly name: string;
|
|
66
|
+
/** Display name (e.g. "Claude Code") */
|
|
67
|
+
readonly displayName: string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Detect whether this agent has conversation history for the given
|
|
71
|
+
* project path. Should be a fast check — avoid reading full file contents.
|
|
72
|
+
*
|
|
73
|
+
* @returns Array of detected sessions, or empty array if none found.
|
|
74
|
+
*/
|
|
75
|
+
detect(projectPath: string): DetectedSession[];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Read conversation text from specific sessions, chunked for LLM
|
|
79
|
+
* consumption. Each chunk is <= maxTokens estimated tokens.
|
|
80
|
+
*
|
|
81
|
+
* @param projectPath The project directory
|
|
82
|
+
* @param sessionIds Which sessions to import (from detect() results)
|
|
83
|
+
* @param maxTokens Max estimated tokens per chunk (default: 12288)
|
|
84
|
+
* @returns Conversation chunks ready for knowledge extraction
|
|
85
|
+
*/
|
|
86
|
+
readChunks(
|
|
87
|
+
projectPath: string,
|
|
88
|
+
sessionIds: string[],
|
|
89
|
+
maxTokens?: number,
|
|
90
|
+
): ConversationChunk[];
|
|
91
|
+
}
|