ei-tui 0.1.3
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/LICENSE +21 -0
- package/README.md +170 -0
- package/package.json +63 -0
- package/src/README.md +96 -0
- package/src/cli/README.md +47 -0
- package/src/cli/commands/facts.ts +25 -0
- package/src/cli/commands/people.ts +25 -0
- package/src/cli/commands/quotes.ts +19 -0
- package/src/cli/commands/topics.ts +25 -0
- package/src/cli/commands/traits.ts +25 -0
- package/src/cli/retrieval.ts +269 -0
- package/src/cli.ts +176 -0
- package/src/core/AGENTS.md +104 -0
- package/src/core/embedding-service.ts +241 -0
- package/src/core/handlers/index.ts +1057 -0
- package/src/core/index.ts +4 -0
- package/src/core/llm-client.ts +265 -0
- package/src/core/model-context-windows.ts +49 -0
- package/src/core/orchestrators/ceremony.ts +500 -0
- package/src/core/orchestrators/extraction-chunker.ts +138 -0
- package/src/core/orchestrators/human-extraction.ts +457 -0
- package/src/core/orchestrators/index.ts +28 -0
- package/src/core/orchestrators/persona-generation.ts +76 -0
- package/src/core/orchestrators/persona-topics.ts +117 -0
- package/src/core/personas/index.ts +5 -0
- package/src/core/personas/opencode-agent.ts +81 -0
- package/src/core/processor.ts +1413 -0
- package/src/core/queue-processor.ts +197 -0
- package/src/core/state/checkpoints.ts +68 -0
- package/src/core/state/human.ts +176 -0
- package/src/core/state/index.ts +5 -0
- package/src/core/state/personas.ts +217 -0
- package/src/core/state/queue.ts +144 -0
- package/src/core/state-manager.ts +347 -0
- package/src/core/types.ts +421 -0
- package/src/core/utils/decay.ts +33 -0
- package/src/index.ts +1 -0
- package/src/integrations/opencode/importer.ts +896 -0
- package/src/integrations/opencode/index.ts +16 -0
- package/src/integrations/opencode/json-reader.ts +304 -0
- package/src/integrations/opencode/reader-factory.ts +35 -0
- package/src/integrations/opencode/sqlite-reader.ts +189 -0
- package/src/integrations/opencode/types.ts +244 -0
- package/src/prompts/AGENTS.md +62 -0
- package/src/prompts/ceremony/description-check.ts +47 -0
- package/src/prompts/ceremony/expire.ts +30 -0
- package/src/prompts/ceremony/explore.ts +60 -0
- package/src/prompts/ceremony/index.ts +11 -0
- package/src/prompts/ceremony/types.ts +42 -0
- package/src/prompts/generation/descriptions.ts +91 -0
- package/src/prompts/generation/index.ts +15 -0
- package/src/prompts/generation/persona.ts +155 -0
- package/src/prompts/generation/seeds.ts +31 -0
- package/src/prompts/generation/types.ts +47 -0
- package/src/prompts/heartbeat/check.ts +179 -0
- package/src/prompts/heartbeat/ei.ts +208 -0
- package/src/prompts/heartbeat/index.ts +15 -0
- package/src/prompts/heartbeat/types.ts +70 -0
- package/src/prompts/human/fact-scan.ts +152 -0
- package/src/prompts/human/index.ts +32 -0
- package/src/prompts/human/item-match.ts +74 -0
- package/src/prompts/human/item-update.ts +322 -0
- package/src/prompts/human/person-scan.ts +115 -0
- package/src/prompts/human/topic-scan.ts +135 -0
- package/src/prompts/human/trait-scan.ts +115 -0
- package/src/prompts/human/types.ts +127 -0
- package/src/prompts/index.ts +90 -0
- package/src/prompts/message-utils.ts +39 -0
- package/src/prompts/persona/index.ts +16 -0
- package/src/prompts/persona/topics-match.ts +69 -0
- package/src/prompts/persona/topics-scan.ts +98 -0
- package/src/prompts/persona/topics-update.ts +157 -0
- package/src/prompts/persona/traits.ts +117 -0
- package/src/prompts/persona/types.ts +74 -0
- package/src/prompts/response/index.ts +147 -0
- package/src/prompts/response/sections.ts +355 -0
- package/src/prompts/response/types.ts +38 -0
- package/src/prompts/validation/ei.ts +93 -0
- package/src/prompts/validation/index.ts +6 -0
- package/src/prompts/validation/types.ts +22 -0
- package/src/storage/crypto.ts +96 -0
- package/src/storage/index.ts +5 -0
- package/src/storage/interface.ts +9 -0
- package/src/storage/local.ts +79 -0
- package/src/storage/merge.ts +69 -0
- package/src/storage/remote.ts +145 -0
- package/src/templates/welcome.ts +91 -0
- package/tui/README.md +62 -0
- package/tui/bunfig.toml +4 -0
- package/tui/src/app.tsx +55 -0
- package/tui/src/commands/archive.tsx +93 -0
- package/tui/src/commands/context.tsx +124 -0
- package/tui/src/commands/delete.tsx +71 -0
- package/tui/src/commands/details.tsx +41 -0
- package/tui/src/commands/editor.tsx +46 -0
- package/tui/src/commands/help.tsx +12 -0
- package/tui/src/commands/me.tsx +145 -0
- package/tui/src/commands/model.ts +47 -0
- package/tui/src/commands/new.ts +31 -0
- package/tui/src/commands/pause.ts +46 -0
- package/tui/src/commands/persona.tsx +58 -0
- package/tui/src/commands/provider.tsx +124 -0
- package/tui/src/commands/quit.ts +22 -0
- package/tui/src/commands/quotes.tsx +172 -0
- package/tui/src/commands/registry.test.ts +137 -0
- package/tui/src/commands/registry.ts +130 -0
- package/tui/src/commands/resume.ts +39 -0
- package/tui/src/commands/setsync.tsx +43 -0
- package/tui/src/commands/settings.tsx +83 -0
- package/tui/src/components/ConfirmOverlay.tsx +51 -0
- package/tui/src/components/ConflictOverlay.tsx +78 -0
- package/tui/src/components/HelpOverlay.tsx +69 -0
- package/tui/src/components/Layout.tsx +24 -0
- package/tui/src/components/MessageList.tsx +174 -0
- package/tui/src/components/PersonaListOverlay.tsx +186 -0
- package/tui/src/components/PromptInput.tsx +145 -0
- package/tui/src/components/ProviderListOverlay.tsx +208 -0
- package/tui/src/components/QuotesOverlay.tsx +157 -0
- package/tui/src/components/Sidebar.tsx +95 -0
- package/tui/src/components/StatusBar.tsx +77 -0
- package/tui/src/components/WelcomeOverlay.tsx +73 -0
- package/tui/src/context/ei.tsx +623 -0
- package/tui/src/context/keyboard.tsx +164 -0
- package/tui/src/context/overlay.tsx +53 -0
- package/tui/src/index.tsx +8 -0
- package/tui/src/storage/file.ts +185 -0
- package/tui/src/util/duration.ts +32 -0
- package/tui/src/util/editor.ts +188 -0
- package/tui/src/util/logger.ts +109 -0
- package/tui/src/util/persona-editor.tsx +181 -0
- package/tui/src/util/provider-editor.tsx +168 -0
- package/tui/src/util/syntax.ts +35 -0
- package/tui/src/util/yaml-serializers.ts +755 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { JsonReader } from "./json-reader.js";
|
|
2
|
+
// SqliteReader not exported directly - uses bun:sqlite which breaks Vite builds
|
|
3
|
+
// Use createOpenCodeReader() factory instead, which dynamically imports when available
|
|
4
|
+
export { createOpenCodeReader } from "./reader-factory.js";
|
|
5
|
+
export { importOpenCodeSessions } from "./importer.js";
|
|
6
|
+
export type { ImportResult, OpenCodeImporterOptions } from "./importer.js";
|
|
7
|
+
export type {
|
|
8
|
+
IOpenCodeReader,
|
|
9
|
+
OpenCodeSession,
|
|
10
|
+
OpenCodeSessionRaw,
|
|
11
|
+
OpenCodeMessage,
|
|
12
|
+
OpenCodeMessageRaw,
|
|
13
|
+
OpenCodePartRaw,
|
|
14
|
+
OpenCodeAgent,
|
|
15
|
+
} from "./types.js";
|
|
16
|
+
export { BUILTIN_AGENTS } from "./types.js";
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IOpenCodeReader,
|
|
3
|
+
OpenCodeSession,
|
|
4
|
+
OpenCodeSessionRaw,
|
|
5
|
+
OpenCodeMessage,
|
|
6
|
+
OpenCodeMessageRaw,
|
|
7
|
+
OpenCodePartRaw,
|
|
8
|
+
OpenCodeAgent,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
import { BUILTIN_AGENTS } from "./types.js";
|
|
11
|
+
|
|
12
|
+
// OpenTUI polyfills window but not document - check document for real browser
|
|
13
|
+
const isBrowser = typeof document !== "undefined";
|
|
14
|
+
|
|
15
|
+
let _join: typeof import("path").join;
|
|
16
|
+
let _readdir: typeof import("fs/promises").readdir;
|
|
17
|
+
let _readFile: typeof import("fs/promises").readFile;
|
|
18
|
+
let _nodeModulesLoaded = false;
|
|
19
|
+
|
|
20
|
+
async function ensureNodeModules(): Promise<boolean> {
|
|
21
|
+
if (isBrowser) return false;
|
|
22
|
+
if (_nodeModulesLoaded) return true;
|
|
23
|
+
|
|
24
|
+
const PATH_MODULE = "path";
|
|
25
|
+
const FS_MODULE = "fs/promises";
|
|
26
|
+
|
|
27
|
+
const pathMod = await import(/* @vite-ignore */ PATH_MODULE);
|
|
28
|
+
const fsMod = await import(/* @vite-ignore */ FS_MODULE);
|
|
29
|
+
|
|
30
|
+
_join = pathMod.join;
|
|
31
|
+
_readdir = fsMod.readdir;
|
|
32
|
+
_readFile = fsMod.readFile;
|
|
33
|
+
_nodeModulesLoaded = true;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getDefaultStoragePath(): string {
|
|
38
|
+
if (!_join) return "";
|
|
39
|
+
return _join(
|
|
40
|
+
process.env.HOME || "~",
|
|
41
|
+
".local",
|
|
42
|
+
"share",
|
|
43
|
+
"opencode",
|
|
44
|
+
"storage"
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class JsonReader implements IOpenCodeReader {
|
|
49
|
+
private storagePath: string | null = null;
|
|
50
|
+
private readonly configuredPath?: string;
|
|
51
|
+
|
|
52
|
+
constructor(storagePath?: string) {
|
|
53
|
+
this.configuredPath = storagePath;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async init(): Promise<boolean> {
|
|
57
|
+
if (!(await ensureNodeModules())) return false;
|
|
58
|
+
if (!this.storagePath) {
|
|
59
|
+
this.storagePath = this.configuredPath || getDefaultStoragePath();
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async getSessionsUpdatedSince(since: Date): Promise<OpenCodeSession[]> {
|
|
65
|
+
if (!(await this.init())) return [];
|
|
66
|
+
|
|
67
|
+
const sinceMs = since.getTime();
|
|
68
|
+
const sessions: OpenCodeSession[] = [];
|
|
69
|
+
const sessionDir = _join(this.storagePath!, "session");
|
|
70
|
+
|
|
71
|
+
let projectDirs: string[];
|
|
72
|
+
try {
|
|
73
|
+
projectDirs = await _readdir(sessionDir);
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const projectHash of projectDirs) {
|
|
79
|
+
if (projectHash.startsWith(".")) continue;
|
|
80
|
+
|
|
81
|
+
const projectPath = _join(sessionDir, projectHash);
|
|
82
|
+
let sessionFiles: string[];
|
|
83
|
+
try {
|
|
84
|
+
sessionFiles = await _readdir(projectPath);
|
|
85
|
+
} catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const fileName of sessionFiles) {
|
|
90
|
+
if (!fileName.endsWith(".json")) continue;
|
|
91
|
+
|
|
92
|
+
const filePath = _join(projectPath, fileName);
|
|
93
|
+
const raw = await this.readJsonFile<OpenCodeSessionRaw>(filePath);
|
|
94
|
+
if (!raw) continue;
|
|
95
|
+
|
|
96
|
+
if (raw.time.updated > sinceMs) {
|
|
97
|
+
sessions.push({
|
|
98
|
+
id: raw.id,
|
|
99
|
+
title: raw.title,
|
|
100
|
+
directory: raw.directory,
|
|
101
|
+
projectId: raw.projectID,
|
|
102
|
+
parentId: raw.parentID,
|
|
103
|
+
time: {
|
|
104
|
+
created: raw.time.created,
|
|
105
|
+
updated: raw.time.updated,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return sessions.sort((a, b) => b.time.updated - a.time.updated);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async getSessionsInRange(from: Date, to: Date): Promise<OpenCodeSession[]> {
|
|
116
|
+
if (!(await this.init())) return [];
|
|
117
|
+
|
|
118
|
+
const fromMs = from.getTime();
|
|
119
|
+
const toMs = to.getTime();
|
|
120
|
+
const sessions: OpenCodeSession[] = [];
|
|
121
|
+
const sessionDir = _join(this.storagePath!, "session");
|
|
122
|
+
|
|
123
|
+
let projectDirs: string[];
|
|
124
|
+
try {
|
|
125
|
+
projectDirs = await _readdir(sessionDir);
|
|
126
|
+
} catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const projectHash of projectDirs) {
|
|
131
|
+
if (projectHash.startsWith(".")) continue;
|
|
132
|
+
|
|
133
|
+
const projectPath = _join(sessionDir, projectHash);
|
|
134
|
+
let sessionFiles: string[];
|
|
135
|
+
try {
|
|
136
|
+
sessionFiles = await _readdir(projectPath);
|
|
137
|
+
} catch {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const fileName of sessionFiles) {
|
|
142
|
+
if (!fileName.endsWith(".json")) continue;
|
|
143
|
+
|
|
144
|
+
const filePath = _join(projectPath, fileName);
|
|
145
|
+
const raw = await this.readJsonFile<OpenCodeSessionRaw>(filePath);
|
|
146
|
+
if (!raw) continue;
|
|
147
|
+
|
|
148
|
+
if (raw.time.updated > fromMs && raw.time.updated <= toMs && !raw.parentID) {
|
|
149
|
+
sessions.push({
|
|
150
|
+
id: raw.id,
|
|
151
|
+
title: raw.title,
|
|
152
|
+
directory: raw.directory,
|
|
153
|
+
projectId: raw.projectID,
|
|
154
|
+
parentId: raw.parentID,
|
|
155
|
+
time: {
|
|
156
|
+
created: raw.time.created,
|
|
157
|
+
updated: raw.time.updated,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return sessions.sort((a, b) => a.time.updated - b.time.updated);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async getMessagesForSession(
|
|
168
|
+
sessionId: string,
|
|
169
|
+
since?: Date
|
|
170
|
+
): Promise<OpenCodeMessage[]> {
|
|
171
|
+
if (!(await this.init())) return [];
|
|
172
|
+
|
|
173
|
+
const sinceMs = since?.getTime() ?? 0;
|
|
174
|
+
const messages: OpenCodeMessage[] = [];
|
|
175
|
+
const messageDir = _join(this.storagePath!, "message", sessionId);
|
|
176
|
+
|
|
177
|
+
let messageFiles: string[];
|
|
178
|
+
try {
|
|
179
|
+
messageFiles = await _readdir(messageDir);
|
|
180
|
+
} catch {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const fileName of messageFiles) {
|
|
185
|
+
if (!fileName.endsWith(".json")) continue;
|
|
186
|
+
|
|
187
|
+
const filePath = _join(messageDir, fileName);
|
|
188
|
+
const raw = await this.readJsonFile<OpenCodeMessageRaw>(filePath);
|
|
189
|
+
if (!raw) continue;
|
|
190
|
+
|
|
191
|
+
if (raw.time.created <= sinceMs) continue;
|
|
192
|
+
|
|
193
|
+
const content = await this.getMessageContent(raw.id);
|
|
194
|
+
if (!content) continue;
|
|
195
|
+
|
|
196
|
+
messages.push({
|
|
197
|
+
id: raw.id,
|
|
198
|
+
sessionId: raw.sessionID,
|
|
199
|
+
role: raw.role,
|
|
200
|
+
agent: (raw.agent || "build").toLowerCase(),
|
|
201
|
+
content,
|
|
202
|
+
timestamp: new Date(raw.time.created).toISOString(),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return messages.sort(
|
|
207
|
+
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async getAgentInfo(agentName: string): Promise<OpenCodeAgent | null> {
|
|
212
|
+
const normalized = agentName.toLowerCase();
|
|
213
|
+
if (BUILTIN_AGENTS[normalized]) {
|
|
214
|
+
return BUILTIN_AGENTS[normalized];
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
name: agentName,
|
|
218
|
+
description: "OpenCode coding agent",
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async getAllUniqueAgents(sessionId: string): Promise<string[]> {
|
|
223
|
+
const messages = await this.getMessagesForSession(sessionId);
|
|
224
|
+
const agents = new Set<string>();
|
|
225
|
+
for (const msg of messages) {
|
|
226
|
+
agents.add(msg.agent);
|
|
227
|
+
}
|
|
228
|
+
return Array.from(agents);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async getFirstAgent(sessionId: string): Promise<string | null> {
|
|
232
|
+
if (!(await this.init())) return null;
|
|
233
|
+
|
|
234
|
+
const messageDir = _join(this.storagePath!, "message", sessionId);
|
|
235
|
+
|
|
236
|
+
let messageFiles: string[];
|
|
237
|
+
try {
|
|
238
|
+
messageFiles = await _readdir(messageDir);
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let earliest: { agent: string; created: number } | null = null;
|
|
244
|
+
|
|
245
|
+
for (const fileName of messageFiles) {
|
|
246
|
+
if (!fileName.endsWith(".json")) continue;
|
|
247
|
+
|
|
248
|
+
const filePath = _join(messageDir, fileName);
|
|
249
|
+
const raw = await this.readJsonFile<OpenCodeMessageRaw>(filePath);
|
|
250
|
+
if (!raw) continue;
|
|
251
|
+
|
|
252
|
+
if (!earliest || raw.time.created < earliest.created) {
|
|
253
|
+
earliest = { agent: (raw.agent || "build").toLowerCase(), created: raw.time.created };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return earliest?.agent ?? null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private async getMessageContent(messageId: string): Promise<string | null> {
|
|
261
|
+
const partDir = _join(this.storagePath!, "part", messageId);
|
|
262
|
+
|
|
263
|
+
let partFiles: string[];
|
|
264
|
+
try {
|
|
265
|
+
partFiles = await _readdir(partDir);
|
|
266
|
+
} catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const textParts: { text: string; time?: number }[] = [];
|
|
271
|
+
|
|
272
|
+
for (const fileName of partFiles) {
|
|
273
|
+
if (!fileName.endsWith(".json")) continue;
|
|
274
|
+
|
|
275
|
+
const filePath = _join(partDir, fileName);
|
|
276
|
+
const raw = await this.readJsonFile<OpenCodePartRaw>(filePath);
|
|
277
|
+
if (!raw) continue;
|
|
278
|
+
|
|
279
|
+
if (raw.type !== "text") continue;
|
|
280
|
+
if (raw.synthetic === true) continue;
|
|
281
|
+
if (!raw.text) continue;
|
|
282
|
+
|
|
283
|
+
textParts.push({
|
|
284
|
+
text: raw.text,
|
|
285
|
+
time: raw.time?.start,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (textParts.length === 0) return null;
|
|
290
|
+
|
|
291
|
+
textParts.sort((a, b) => (a.time ?? 0) - (b.time ?? 0));
|
|
292
|
+
return textParts.map((p) => p.text).join("\n\n");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private async readJsonFile<T>(filePath: string): Promise<T | null> {
|
|
296
|
+
try {
|
|
297
|
+
const text = await _readFile(filePath, "utf-8");
|
|
298
|
+
if (!text) return null;
|
|
299
|
+
return JSON.parse(text) as T;
|
|
300
|
+
} catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import type { IOpenCodeReader } from "./types.js";
|
|
4
|
+
import { JsonReader } from "./json-reader.js";
|
|
5
|
+
|
|
6
|
+
export async function createOpenCodeReader(basePath?: string): Promise<IOpenCodeReader> {
|
|
7
|
+
const dataDir = basePath ?? getDefaultDataDir();
|
|
8
|
+
const dbPath = join(dataDir, "opencode.db");
|
|
9
|
+
const storagePath = join(dataDir, "storage");
|
|
10
|
+
|
|
11
|
+
if (existsSync(dbPath)) {
|
|
12
|
+
try {
|
|
13
|
+
const { SqliteReader } = await import("./sqlite-reader.js");
|
|
14
|
+
console.log("[OpenCode] Using SQLite reader");
|
|
15
|
+
return new SqliteReader(dbPath);
|
|
16
|
+
} catch {
|
|
17
|
+
console.log("[OpenCode] SQLite not available, falling back to JSON reader");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (existsSync(storagePath)) {
|
|
22
|
+
console.log("[OpenCode] Using JSON reader (legacy)");
|
|
23
|
+
return new JsonReader(storagePath);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log("[OpenCode] No OpenCode data found");
|
|
27
|
+
return new JsonReader(storagePath);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getDefaultDataDir(): string {
|
|
31
|
+
return (
|
|
32
|
+
process.env.EI_OPENCODE_DATA_PATH ??
|
|
33
|
+
join(process.env.HOME || "~", ".local", "share", "opencode")
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import type {
|
|
3
|
+
IOpenCodeReader,
|
|
4
|
+
OpenCodeSession,
|
|
5
|
+
OpenCodeMessage,
|
|
6
|
+
OpenCodeAgent,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
import { BUILTIN_AGENTS } from "./types.js";
|
|
9
|
+
|
|
10
|
+
export class SqliteReader implements IOpenCodeReader {
|
|
11
|
+
private db: Database;
|
|
12
|
+
|
|
13
|
+
constructor(dbPath: string) {
|
|
14
|
+
this.db = new Database(dbPath, { readonly: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async getSessionsUpdatedSince(since: Date): Promise<OpenCodeSession[]> {
|
|
18
|
+
const sinceMs = since.getTime();
|
|
19
|
+
const rows = this.db
|
|
20
|
+
.query(
|
|
21
|
+
`
|
|
22
|
+
SELECT id, title, directory, project_id, parent_id, time_created, time_updated
|
|
23
|
+
FROM session
|
|
24
|
+
WHERE time_updated > ?1 AND parent_id IS NULL
|
|
25
|
+
ORDER BY time_updated DESC
|
|
26
|
+
`
|
|
27
|
+
)
|
|
28
|
+
.all(sinceMs) as Array<{
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
directory: string;
|
|
32
|
+
project_id: string;
|
|
33
|
+
parent_id: string | null;
|
|
34
|
+
time_created: number;
|
|
35
|
+
time_updated: number;
|
|
36
|
+
}>;
|
|
37
|
+
|
|
38
|
+
return rows.map((row) => ({
|
|
39
|
+
id: row.id,
|
|
40
|
+
title: row.title,
|
|
41
|
+
directory: row.directory,
|
|
42
|
+
projectId: row.project_id,
|
|
43
|
+
parentId: row.parent_id ?? undefined,
|
|
44
|
+
time: {
|
|
45
|
+
created: row.time_created,
|
|
46
|
+
updated: row.time_updated,
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async getSessionsInRange(from: Date, to: Date): Promise<OpenCodeSession[]> {
|
|
52
|
+
const fromMs = from.getTime();
|
|
53
|
+
const toMs = to.getTime();
|
|
54
|
+
const rows = this.db
|
|
55
|
+
.query(
|
|
56
|
+
`
|
|
57
|
+
SELECT id, title, directory, project_id, parent_id, time_created, time_updated
|
|
58
|
+
FROM session
|
|
59
|
+
WHERE time_updated > ?1 AND time_updated <= ?2 AND parent_id IS NULL
|
|
60
|
+
ORDER BY time_updated ASC
|
|
61
|
+
`
|
|
62
|
+
)
|
|
63
|
+
.all(fromMs, toMs) as Array<{
|
|
64
|
+
id: string;
|
|
65
|
+
title: string;
|
|
66
|
+
directory: string;
|
|
67
|
+
project_id: string;
|
|
68
|
+
parent_id: string | null;
|
|
69
|
+
time_created: number;
|
|
70
|
+
time_updated: number;
|
|
71
|
+
}>;
|
|
72
|
+
|
|
73
|
+
return rows.map((row) => ({
|
|
74
|
+
id: row.id,
|
|
75
|
+
title: row.title,
|
|
76
|
+
directory: row.directory,
|
|
77
|
+
projectId: row.project_id,
|
|
78
|
+
parentId: row.parent_id ?? undefined,
|
|
79
|
+
time: {
|
|
80
|
+
created: row.time_created,
|
|
81
|
+
updated: row.time_updated,
|
|
82
|
+
},
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async getMessagesForSession(
|
|
87
|
+
sessionId: string,
|
|
88
|
+
since?: Date
|
|
89
|
+
): Promise<OpenCodeMessage[]> {
|
|
90
|
+
const sinceMs = since?.getTime() ?? 0;
|
|
91
|
+
|
|
92
|
+
const messages = this.db
|
|
93
|
+
.query(
|
|
94
|
+
`
|
|
95
|
+
SELECT id, session_id, time_created, data
|
|
96
|
+
FROM message
|
|
97
|
+
WHERE session_id = ?1 AND time_created > ?2
|
|
98
|
+
ORDER BY time_created ASC
|
|
99
|
+
`
|
|
100
|
+
)
|
|
101
|
+
.all(sessionId, sinceMs) as Array<{
|
|
102
|
+
id: string;
|
|
103
|
+
session_id: string;
|
|
104
|
+
time_created: number;
|
|
105
|
+
data: string;
|
|
106
|
+
}>;
|
|
107
|
+
|
|
108
|
+
const result: OpenCodeMessage[] = [];
|
|
109
|
+
|
|
110
|
+
for (const msg of messages) {
|
|
111
|
+
const msgData = JSON.parse(msg.data) as { role: "user" | "assistant"; agent?: string };
|
|
112
|
+
const content = this.getMessageContent(msg.id);
|
|
113
|
+
if (!content) continue;
|
|
114
|
+
|
|
115
|
+
result.push({
|
|
116
|
+
id: msg.id,
|
|
117
|
+
sessionId: msg.session_id,
|
|
118
|
+
role: msgData.role,
|
|
119
|
+
agent: (msgData.agent || "build").toLowerCase(),
|
|
120
|
+
content,
|
|
121
|
+
timestamp: new Date(msg.time_created).toISOString(),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private getMessageContent(messageId: string): string | null {
|
|
129
|
+
const parts = this.db
|
|
130
|
+
.query(
|
|
131
|
+
`
|
|
132
|
+
SELECT data, time_created FROM part
|
|
133
|
+
WHERE message_id = ?1
|
|
134
|
+
ORDER BY time_created ASC
|
|
135
|
+
`
|
|
136
|
+
)
|
|
137
|
+
.all(messageId) as Array<{ data: string; time_created: number }>;
|
|
138
|
+
|
|
139
|
+
const textParts: string[] = [];
|
|
140
|
+
|
|
141
|
+
for (const part of parts) {
|
|
142
|
+
const partData = JSON.parse(part.data) as {
|
|
143
|
+
type: string;
|
|
144
|
+
synthetic?: boolean;
|
|
145
|
+
text?: string;
|
|
146
|
+
};
|
|
147
|
+
if (partData.type !== "text") continue;
|
|
148
|
+
if (partData.synthetic === true) continue;
|
|
149
|
+
if (!partData.text) continue;
|
|
150
|
+
textParts.push(partData.text);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return textParts.length > 0 ? textParts.join("\n\n") : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async getAgentInfo(agentName: string): Promise<OpenCodeAgent | null> {
|
|
157
|
+
const normalized = agentName.toLowerCase();
|
|
158
|
+
if (BUILTIN_AGENTS[normalized]) {
|
|
159
|
+
return BUILTIN_AGENTS[normalized];
|
|
160
|
+
}
|
|
161
|
+
return { name: agentName, description: "OpenCode coding agent" };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async getAllUniqueAgents(sessionId: string): Promise<string[]> {
|
|
165
|
+
const messages = await this.getMessagesForSession(sessionId);
|
|
166
|
+
return [...new Set(messages.map((m) => m.agent))];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async getFirstAgent(sessionId: string): Promise<string | null> {
|
|
170
|
+
const row = this.db
|
|
171
|
+
.query(
|
|
172
|
+
`
|
|
173
|
+
SELECT data FROM message
|
|
174
|
+
WHERE session_id = ?1
|
|
175
|
+
ORDER BY time_created ASC
|
|
176
|
+
LIMIT 1
|
|
177
|
+
`
|
|
178
|
+
)
|
|
179
|
+
.get(sessionId) as { data: string } | null;
|
|
180
|
+
|
|
181
|
+
if (!row) return null;
|
|
182
|
+
const msgData = JSON.parse(row.data) as { agent?: string };
|
|
183
|
+
return (msgData.agent || "build").toLowerCase();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
close(): void {
|
|
187
|
+
this.db.close();
|
|
188
|
+
}
|
|
189
|
+
}
|