@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,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Continue (VS Code/JetBrains extension) conversation history provider.
|
|
3
|
+
*
|
|
4
|
+
* Reads JSON session files from ~/.continue/sessions/<sessionId>.json
|
|
5
|
+
* with the sessions index at ~/.continue/sessions/sessions.json.
|
|
6
|
+
*
|
|
7
|
+
* Each session JSON contains:
|
|
8
|
+
* { sessionId, title, workspaceDirectory, history: ChatHistoryItem[] }
|
|
9
|
+
*
|
|
10
|
+
* The CONTINUE_GLOBAL_DIR env var overrides the default ~/.continue path.
|
|
11
|
+
*/
|
|
12
|
+
import { readdirSync, readFileSync, existsSync } from "fs";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import type { AgentHistoryProvider, ConversationChunk, DetectedSession } from "../types";
|
|
16
|
+
import { registerProvider } from "./index";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Constants
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
const MAX_TOOL_OUTPUT_CHARS = 500;
|
|
23
|
+
const DEFAULT_MAX_TOKENS = 12288;
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Types (Continue's format)
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
type SessionMetadata = {
|
|
30
|
+
sessionId: string;
|
|
31
|
+
title: string;
|
|
32
|
+
dateCreated: string;
|
|
33
|
+
workspaceDirectory?: string;
|
|
34
|
+
messageCount?: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type ChatMessage = {
|
|
38
|
+
role: "user" | "assistant" | "system" | "tool";
|
|
39
|
+
content: string | ContentPart[];
|
|
40
|
+
toolCalls?: ToolCall[];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type ContentPart =
|
|
44
|
+
| { type: "text"; text: string }
|
|
45
|
+
| { type: "imageUrl"; imageUrl?: { url: string } }
|
|
46
|
+
| { type: string; [key: string]: unknown };
|
|
47
|
+
|
|
48
|
+
type ToolCall = {
|
|
49
|
+
id: string;
|
|
50
|
+
type: "function";
|
|
51
|
+
function: { name: string; arguments: string };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type ChatHistoryItem = {
|
|
55
|
+
message: ChatMessage;
|
|
56
|
+
contextItems?: unknown[];
|
|
57
|
+
toolCallStates?: Array<{
|
|
58
|
+
toolCallId: string;
|
|
59
|
+
status: string;
|
|
60
|
+
output?: string;
|
|
61
|
+
}>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type SessionFile = {
|
|
65
|
+
sessionId: string;
|
|
66
|
+
title: string;
|
|
67
|
+
workspaceDirectory?: string;
|
|
68
|
+
history: ChatHistoryItem[];
|
|
69
|
+
mode?: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Helpers
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
function estimateTokens(text: string): number {
|
|
77
|
+
return Math.ceil(text.length / 3);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function truncate(text: string, max: number): string {
|
|
81
|
+
if (text.length <= max) return text;
|
|
82
|
+
return text.slice(0, max) + "...";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Get the Continue global directory. */
|
|
86
|
+
function continueDir(): string {
|
|
87
|
+
return process.env.CONTINUE_GLOBAL_DIR || join(homedir(), ".continue");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Load the sessions index. */
|
|
91
|
+
function loadSessionIndex(): SessionMetadata[] {
|
|
92
|
+
const indexPath = join(continueDir(), "sessions", "sessions.json");
|
|
93
|
+
if (!existsSync(indexPath)) return [];
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const raw = readFileSync(indexPath, "utf-8");
|
|
97
|
+
const parsed = JSON.parse(raw);
|
|
98
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
99
|
+
} catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Load a full session file. */
|
|
105
|
+
function loadSession(sessionId: string): SessionFile | null {
|
|
106
|
+
const filePath = join(continueDir(), "sessions", `${sessionId}.json`);
|
|
107
|
+
if (!existsSync(filePath)) return null;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
111
|
+
return JSON.parse(raw) as SessionFile;
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Extract text from a chat message's content. */
|
|
118
|
+
function extractMessageContent(content: string | ContentPart[]): string {
|
|
119
|
+
if (typeof content === "string") return content;
|
|
120
|
+
if (!Array.isArray(content)) return "";
|
|
121
|
+
|
|
122
|
+
return content
|
|
123
|
+
.filter((part): part is { type: "text"; text: string } =>
|
|
124
|
+
part.type === "text" && typeof (part as { text?: string }).text === "string",
|
|
125
|
+
)
|
|
126
|
+
.map((part) => part.text)
|
|
127
|
+
.join("\n");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Convert a ChatHistoryItem to text. */
|
|
131
|
+
function historyItemToText(item: ChatHistoryItem): string | null {
|
|
132
|
+
const msg = item.message;
|
|
133
|
+
if (!msg) return null;
|
|
134
|
+
|
|
135
|
+
// Skip system messages
|
|
136
|
+
if (msg.role === "system") return null;
|
|
137
|
+
|
|
138
|
+
const parts: string[] = [];
|
|
139
|
+
|
|
140
|
+
// Message content
|
|
141
|
+
const content = extractMessageContent(msg.content);
|
|
142
|
+
if (content) parts.push(content);
|
|
143
|
+
|
|
144
|
+
// Tool calls (from assistant)
|
|
145
|
+
if (msg.toolCalls) {
|
|
146
|
+
for (const call of msg.toolCalls) {
|
|
147
|
+
if (call.function) {
|
|
148
|
+
const args = truncate(call.function.arguments || "{}", MAX_TOOL_OUTPUT_CHARS);
|
|
149
|
+
parts.push(`[tool: ${call.function.name}] ${args}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Tool call results
|
|
155
|
+
if (item.toolCallStates) {
|
|
156
|
+
for (const state of item.toolCallStates) {
|
|
157
|
+
if (state.output && state.status === "done") {
|
|
158
|
+
parts.push(`[tool_result] ${truncate(state.output, MAX_TOOL_OUTPUT_CHARS)}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (parts.length === 0) return null;
|
|
164
|
+
|
|
165
|
+
const role = msg.role === "tool" ? "tool_result" : msg.role;
|
|
166
|
+
return `[${role}] ${parts.join("\n")}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Provider implementation
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
const continueProvider: AgentHistoryProvider = {
|
|
174
|
+
name: "continue",
|
|
175
|
+
displayName: "Continue",
|
|
176
|
+
|
|
177
|
+
detect(projectPath: string): DetectedSession[] {
|
|
178
|
+
const sessions: DetectedSession[] = [];
|
|
179
|
+
const index = loadSessionIndex();
|
|
180
|
+
|
|
181
|
+
for (const meta of index) {
|
|
182
|
+
// Filter by workspace directory
|
|
183
|
+
if (meta.workspaceDirectory !== projectPath) continue;
|
|
184
|
+
|
|
185
|
+
// Load the full session to count messages
|
|
186
|
+
const session = loadSession(meta.sessionId);
|
|
187
|
+
if (!session || !session.history || session.history.length < 3) continue;
|
|
188
|
+
|
|
189
|
+
const ts = new Date(meta.dateCreated).getTime();
|
|
190
|
+
const dateStr = new Date(ts).toISOString().slice(0, 10);
|
|
191
|
+
const messageCount = session.history.length;
|
|
192
|
+
|
|
193
|
+
const label = meta.title
|
|
194
|
+
? `${dateStr} - ${truncate(meta.title, 60)} (${messageCount} messages)`
|
|
195
|
+
: `${dateStr} (${messageCount} messages)`;
|
|
196
|
+
|
|
197
|
+
// Estimate tokens
|
|
198
|
+
const estimatedTokens = messageCount * 500;
|
|
199
|
+
|
|
200
|
+
sessions.push({
|
|
201
|
+
id: meta.sessionId,
|
|
202
|
+
label,
|
|
203
|
+
startedAt: ts,
|
|
204
|
+
lastActivityAt: ts,
|
|
205
|
+
estimatedTokens,
|
|
206
|
+
messageCount,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Also scan for session files not in the index (some versions don't maintain it)
|
|
211
|
+
const sessionsDir = join(continueDir(), "sessions");
|
|
212
|
+
if (existsSync(sessionsDir)) {
|
|
213
|
+
const existingIds = new Set(sessions.map((s) => s.id));
|
|
214
|
+
let entries: string[];
|
|
215
|
+
try {
|
|
216
|
+
entries = readdirSync(sessionsDir);
|
|
217
|
+
} catch {
|
|
218
|
+
entries = [];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const entry of entries) {
|
|
222
|
+
if (!entry.endsWith(".json") || entry === "sessions.json") continue;
|
|
223
|
+
const sessionId = entry.replace(".json", "");
|
|
224
|
+
if (existingIds.has(sessionId)) continue;
|
|
225
|
+
|
|
226
|
+
const session = loadSession(sessionId);
|
|
227
|
+
if (!session) continue;
|
|
228
|
+
if (session.workspaceDirectory !== projectPath) continue;
|
|
229
|
+
if (!session.history || session.history.length < 3) continue;
|
|
230
|
+
|
|
231
|
+
const dateStr = session.title ? truncate(session.title, 60) : sessionId.slice(0, 8);
|
|
232
|
+
sessions.push({
|
|
233
|
+
id: sessionId,
|
|
234
|
+
label: `${dateStr} (${session.history.length} messages)`,
|
|
235
|
+
startedAt: Date.now(),
|
|
236
|
+
lastActivityAt: Date.now(),
|
|
237
|
+
estimatedTokens: session.history.length * 500,
|
|
238
|
+
messageCount: session.history.length,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return sessions.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
readChunks(
|
|
247
|
+
_projectPath: string,
|
|
248
|
+
sessionIds: string[],
|
|
249
|
+
maxTokens: number = DEFAULT_MAX_TOKENS,
|
|
250
|
+
): ConversationChunk[] {
|
|
251
|
+
const chunks: ConversationChunk[] = [];
|
|
252
|
+
|
|
253
|
+
for (const sessionId of sessionIds) {
|
|
254
|
+
const session = loadSession(sessionId);
|
|
255
|
+
if (!session || !session.history) continue;
|
|
256
|
+
|
|
257
|
+
const textMessages: { text: string }[] = [];
|
|
258
|
+
for (const item of session.history) {
|
|
259
|
+
const text = historyItemToText(item);
|
|
260
|
+
if (text) textMessages.push({ text });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (textMessages.length === 0) continue;
|
|
264
|
+
|
|
265
|
+
// Session timestamp
|
|
266
|
+
const sessionTimestamp = Date.now();
|
|
267
|
+
|
|
268
|
+
// Build chunks respecting maxTokens boundaries
|
|
269
|
+
let currentTexts: string[] = [];
|
|
270
|
+
let currentTokens = 0;
|
|
271
|
+
let chunkIndex = 0;
|
|
272
|
+
|
|
273
|
+
const flushChunk = () => {
|
|
274
|
+
if (currentTexts.length === 0) return;
|
|
275
|
+
chunkIndex++;
|
|
276
|
+
const text = currentTexts.join("\n\n");
|
|
277
|
+
chunks.push({
|
|
278
|
+
label: `Continue ${session.title || sessionId.slice(0, 8)} (${chunkIndex})`,
|
|
279
|
+
text,
|
|
280
|
+
estimatedTokens: estimateTokens(text),
|
|
281
|
+
timestamp: sessionTimestamp,
|
|
282
|
+
});
|
|
283
|
+
currentTexts = [];
|
|
284
|
+
currentTokens = 0;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
for (const msg of textMessages) {
|
|
288
|
+
const msgTokens = estimateTokens(msg.text);
|
|
289
|
+
if (currentTokens > 0 && currentTokens + msgTokens > maxTokens) {
|
|
290
|
+
flushChunk();
|
|
291
|
+
}
|
|
292
|
+
currentTexts.push(msg.text);
|
|
293
|
+
currentTokens += msgTokens;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
flushChunk();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return chunks;
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Auto-register on import
|
|
304
|
+
registerProvider(continueProvider);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider registry — maintains a list of all known agent history providers.
|
|
3
|
+
*
|
|
4
|
+
* Providers register themselves at import time. The detection orchestrator
|
|
5
|
+
* iterates over all registered providers to scan for conversation history.
|
|
6
|
+
*/
|
|
7
|
+
import type { AgentHistoryProvider } from "../types";
|
|
8
|
+
|
|
9
|
+
const providers: AgentHistoryProvider[] = [];
|
|
10
|
+
|
|
11
|
+
/** Register a provider. Called at module load time by each provider module. */
|
|
12
|
+
export function registerProvider(provider: AgentHistoryProvider): void {
|
|
13
|
+
providers.push(provider);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Get all registered providers. */
|
|
17
|
+
export function getProviders(): readonly AgentHistoryProvider[] {
|
|
18
|
+
return providers;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Get a provider by internal name. */
|
|
22
|
+
export function getProvider(name: string): AgentHistoryProvider | undefined {
|
|
23
|
+
return providers.find((p) => p.name === name);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Clear all registered providers.
|
|
28
|
+
* Test-only — allows resetting the registry between test runs.
|
|
29
|
+
*/
|
|
30
|
+
export function clearProviders(): void {
|
|
31
|
+
providers.length = 0;
|
|
32
|
+
}
|
|
@@ -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);
|