@loreai/core 0.16.0 → 0.17.1
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/README.md +11 -0
- package/dist/bun/agents-file.d.ts +13 -1
- package/dist/bun/agents-file.d.ts.map +1 -1
- package/dist/bun/config.d.ts +20 -1
- package/dist/bun/config.d.ts.map +1 -1
- package/dist/bun/data.d.ts +174 -0
- package/dist/bun/data.d.ts.map +1 -0
- package/dist/bun/db.d.ts +65 -0
- package/dist/bun/db.d.ts.map +1 -1
- package/dist/bun/distillation.d.ts +49 -6
- package/dist/bun/distillation.d.ts.map +1 -1
- package/dist/bun/embedding-vendor.d.ts +66 -0
- package/dist/bun/embedding-vendor.d.ts.map +1 -0
- package/dist/bun/embedding-worker-types.d.ts +66 -0
- package/dist/bun/embedding-worker-types.d.ts.map +1 -0
- package/dist/bun/embedding-worker.d.ts +16 -0
- package/dist/bun/embedding-worker.d.ts.map +1 -0
- package/dist/bun/embedding-worker.js +100 -0
- package/dist/bun/embedding-worker.js.map +7 -0
- package/dist/bun/embedding.d.ts +91 -8
- package/dist/bun/embedding.d.ts.map +1 -1
- package/dist/bun/git.d.ts +47 -0
- package/dist/bun/git.d.ts.map +1 -0
- package/dist/bun/gradient.d.ts +19 -1
- package/dist/bun/gradient.d.ts.map +1 -1
- package/dist/bun/index.d.ts +9 -6
- package/dist/bun/index.d.ts.map +1 -1
- package/dist/bun/index.js +13029 -10885
- package/dist/bun/index.js.map +4 -4
- package/dist/bun/lat-reader.d.ts +1 -1
- package/dist/bun/lat-reader.d.ts.map +1 -1
- package/dist/bun/ltm.d.ts.map +1 -1
- package/dist/bun/markdown.d.ts +11 -0
- package/dist/bun/markdown.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 +53 -0
- package/dist/bun/recall.d.ts.map +1 -1
- package/dist/bun/search.d.ts +29 -0
- package/dist/bun/search.d.ts.map +1 -1
- package/dist/bun/temporal.d.ts +2 -0
- package/dist/bun/temporal.d.ts.map +1 -1
- package/dist/bun/types.d.ts +15 -0
- package/dist/bun/types.d.ts.map +1 -1
- package/dist/bun/worker-model.d.ts +12 -9
- package/dist/bun/worker-model.d.ts.map +1 -1
- package/dist/node/agents-file.d.ts +13 -1
- package/dist/node/agents-file.d.ts.map +1 -1
- package/dist/node/config.d.ts +20 -1
- package/dist/node/config.d.ts.map +1 -1
- package/dist/node/data.d.ts +174 -0
- package/dist/node/data.d.ts.map +1 -0
- package/dist/node/db.d.ts +65 -0
- package/dist/node/db.d.ts.map +1 -1
- package/dist/node/distillation.d.ts +49 -6
- package/dist/node/distillation.d.ts.map +1 -1
- package/dist/node/embedding-vendor.d.ts +66 -0
- package/dist/node/embedding-vendor.d.ts.map +1 -0
- package/dist/node/embedding-worker-types.d.ts +66 -0
- package/dist/node/embedding-worker-types.d.ts.map +1 -0
- package/dist/node/embedding-worker.d.ts +16 -0
- package/dist/node/embedding-worker.d.ts.map +1 -0
- package/dist/node/embedding-worker.js +100 -0
- package/dist/node/embedding-worker.js.map +7 -0
- package/dist/node/embedding.d.ts +91 -8
- package/dist/node/embedding.d.ts.map +1 -1
- package/dist/node/git.d.ts +47 -0
- package/dist/node/git.d.ts.map +1 -0
- package/dist/node/gradient.d.ts +19 -1
- package/dist/node/gradient.d.ts.map +1 -1
- package/dist/node/index.d.ts +9 -6
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +13029 -10885
- package/dist/node/index.js.map +4 -4
- package/dist/node/lat-reader.d.ts +1 -1
- package/dist/node/lat-reader.d.ts.map +1 -1
- package/dist/node/ltm.d.ts.map +1 -1
- package/dist/node/markdown.d.ts +11 -0
- package/dist/node/markdown.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 +53 -0
- package/dist/node/recall.d.ts.map +1 -1
- package/dist/node/search.d.ts +29 -0
- package/dist/node/search.d.ts.map +1 -1
- package/dist/node/temporal.d.ts +2 -0
- package/dist/node/temporal.d.ts.map +1 -1
- package/dist/node/types.d.ts +15 -0
- package/dist/node/types.d.ts.map +1 -1
- package/dist/node/worker-model.d.ts +12 -9
- package/dist/node/worker-model.d.ts.map +1 -1
- package/dist/types/agents-file.d.ts +13 -1
- package/dist/types/agents-file.d.ts.map +1 -1
- package/dist/types/config.d.ts +20 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/data.d.ts +174 -0
- package/dist/types/data.d.ts.map +1 -0
- package/dist/types/db.d.ts +65 -0
- package/dist/types/db.d.ts.map +1 -1
- package/dist/types/distillation.d.ts +49 -6
- package/dist/types/distillation.d.ts.map +1 -1
- package/dist/types/embedding-vendor.d.ts +66 -0
- package/dist/types/embedding-vendor.d.ts.map +1 -0
- package/dist/types/embedding-worker-types.d.ts +66 -0
- package/dist/types/embedding-worker-types.d.ts.map +1 -0
- package/dist/types/embedding-worker.d.ts +16 -0
- package/dist/types/embedding-worker.d.ts.map +1 -0
- package/dist/types/embedding.d.ts +91 -8
- package/dist/types/embedding.d.ts.map +1 -1
- package/dist/types/git.d.ts +47 -0
- package/dist/types/git.d.ts.map +1 -0
- package/dist/types/gradient.d.ts +19 -1
- package/dist/types/gradient.d.ts.map +1 -1
- package/dist/types/index.d.ts +9 -6
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lat-reader.d.ts +1 -1
- package/dist/types/lat-reader.d.ts.map +1 -1
- package/dist/types/ltm.d.ts.map +1 -1
- package/dist/types/markdown.d.ts +11 -0
- package/dist/types/markdown.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 +53 -0
- package/dist/types/recall.d.ts.map +1 -1
- package/dist/types/search.d.ts +29 -0
- package/dist/types/search.d.ts.map +1 -1
- package/dist/types/temporal.d.ts +2 -0
- package/dist/types/temporal.d.ts.map +1 -1
- package/dist/types/types.d.ts +15 -0
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/worker-model.d.ts +12 -9
- package/dist/types/worker-model.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/agents-file.ts +87 -4
- package/src/config.ts +68 -5
- package/src/curator.ts +2 -2
- package/src/data.ts +768 -0
- package/src/db.ts +386 -7
- package/src/distillation.ts +178 -35
- package/src/embedding-vendor.ts +102 -0
- package/src/embedding-worker-types.ts +82 -0
- package/src/embedding-worker.ts +185 -0
- package/src/embedding.ts +607 -61
- package/src/git.ts +144 -0
- package/src/gradient.ts +174 -17
- package/src/index.ts +20 -0
- package/src/lat-reader.ts +5 -11
- package/src/ltm.ts +17 -44
- package/src/markdown.ts +15 -0
- package/src/prompt.ts +1 -2
- package/src/recall.ts +401 -70
- package/src/search.ts +71 -1
- package/src/temporal.ts +42 -35
- package/src/types.ts +15 -0
- package/src/worker-model.ts +14 -9
package/src/data.ts
ADDED
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* data.ts — Data listing, inspection, and deletion for Lore.
|
|
3
|
+
*
|
|
4
|
+
* Provides a unified API for both the CLI (`lore data`) and the web UI
|
|
5
|
+
* (`/ui/`) to browse, search, and delete stored data across all tables.
|
|
6
|
+
*
|
|
7
|
+
* Cross-cutting concerns (e.g. `clearProject` touches knowledge, temporal,
|
|
8
|
+
* distillations, and session_state in one transaction) live here instead of
|
|
9
|
+
* being spread across ltm/temporal/distillation modules.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { statSync, unlinkSync, existsSync } from "fs";
|
|
13
|
+
import {
|
|
14
|
+
db,
|
|
15
|
+
ensureProject,
|
|
16
|
+
projectId,
|
|
17
|
+
close,
|
|
18
|
+
dbPath,
|
|
19
|
+
mergeProjectInternal,
|
|
20
|
+
repoNameFromRemote,
|
|
21
|
+
} from "./db";
|
|
22
|
+
import { getGitRemote } from "./git";
|
|
23
|
+
import * as ltm from "./ltm";
|
|
24
|
+
import * as agentsFile from "./agents-file";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Types
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export type ProjectSummary = {
|
|
31
|
+
id: string;
|
|
32
|
+
path: string;
|
|
33
|
+
name: string | null;
|
|
34
|
+
git_remote: string | null;
|
|
35
|
+
created_at: number;
|
|
36
|
+
knowledge_count: number;
|
|
37
|
+
session_count: number;
|
|
38
|
+
message_count: number;
|
|
39
|
+
distillation_count: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type SessionSummary = {
|
|
43
|
+
session_id: string;
|
|
44
|
+
message_count: number;
|
|
45
|
+
first_message_at: number;
|
|
46
|
+
last_message_at: number;
|
|
47
|
+
distilled_count: number;
|
|
48
|
+
undistilled_count: number;
|
|
49
|
+
distillation_count: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type DistillationSummary = {
|
|
53
|
+
id: string;
|
|
54
|
+
session_id: string;
|
|
55
|
+
generation: number;
|
|
56
|
+
token_count: number;
|
|
57
|
+
r_compression: number | null;
|
|
58
|
+
c_norm: number | null;
|
|
59
|
+
archived: number;
|
|
60
|
+
created_at: number;
|
|
61
|
+
call_type: string | null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type DistillationDetail = DistillationSummary & {
|
|
65
|
+
project_id: string;
|
|
66
|
+
observations: string;
|
|
67
|
+
source_ids: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type ClearResult = {
|
|
71
|
+
knowledge_deleted: number;
|
|
72
|
+
temporal_deleted: number;
|
|
73
|
+
distillations_deleted: number;
|
|
74
|
+
sessions_cleared: number;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type GlobalStats = {
|
|
78
|
+
project_count: number;
|
|
79
|
+
knowledge_count: number;
|
|
80
|
+
session_count: number;
|
|
81
|
+
message_count: number;
|
|
82
|
+
distillation_count: number;
|
|
83
|
+
db_size_bytes: number;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Listing functions
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
/** List all projects with summary counts. */
|
|
91
|
+
export function listProjects(): ProjectSummary[] {
|
|
92
|
+
return db()
|
|
93
|
+
.query(
|
|
94
|
+
`SELECT p.id, p.path, p.name, p.git_remote, p.created_at,
|
|
95
|
+
(SELECT COUNT(*) FROM knowledge WHERE project_id = p.id AND confidence > 0.2) as knowledge_count,
|
|
96
|
+
(SELECT COUNT(DISTINCT session_id) FROM temporal_messages WHERE project_id = p.id) as session_count,
|
|
97
|
+
(SELECT COUNT(*) FROM temporal_messages WHERE project_id = p.id) as message_count,
|
|
98
|
+
(SELECT COUNT(*) FROM distillations WHERE project_id = p.id) as distillation_count
|
|
99
|
+
FROM projects p ORDER BY p.created_at DESC`,
|
|
100
|
+
)
|
|
101
|
+
.all() as ProjectSummary[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** List distinct sessions for a project, with message/distillation counts. */
|
|
105
|
+
export function listSessions(
|
|
106
|
+
projectPath: string,
|
|
107
|
+
limit = 50,
|
|
108
|
+
): SessionSummary[] {
|
|
109
|
+
const pid = ensureProject(projectPath);
|
|
110
|
+
return db()
|
|
111
|
+
.query(
|
|
112
|
+
`SELECT
|
|
113
|
+
session_id,
|
|
114
|
+
COUNT(*) as message_count,
|
|
115
|
+
MIN(created_at) as first_message_at,
|
|
116
|
+
MAX(created_at) as last_message_at,
|
|
117
|
+
SUM(CASE WHEN distilled = 1 THEN 1 ELSE 0 END) as distilled_count,
|
|
118
|
+
SUM(CASE WHEN distilled = 0 THEN 1 ELSE 0 END) as undistilled_count,
|
|
119
|
+
(SELECT COUNT(*) FROM distillations d
|
|
120
|
+
WHERE d.project_id = temporal_messages.project_id
|
|
121
|
+
AND d.session_id = temporal_messages.session_id) as distillation_count
|
|
122
|
+
FROM temporal_messages
|
|
123
|
+
WHERE project_id = ?
|
|
124
|
+
GROUP BY session_id
|
|
125
|
+
ORDER BY MAX(created_at) DESC
|
|
126
|
+
LIMIT ?`,
|
|
127
|
+
)
|
|
128
|
+
.all(pid, limit) as SessionSummary[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** List distillations for a project (optionally filtered by session). */
|
|
132
|
+
export function listDistillations(
|
|
133
|
+
projectPath: string,
|
|
134
|
+
opts?: { sessionId?: string; limit?: number },
|
|
135
|
+
): DistillationSummary[] {
|
|
136
|
+
const pid = ensureProject(projectPath);
|
|
137
|
+
const limit = opts?.limit ?? 50;
|
|
138
|
+
|
|
139
|
+
if (opts?.sessionId) {
|
|
140
|
+
return db()
|
|
141
|
+
.query(
|
|
142
|
+
`SELECT id, session_id, generation, token_count, r_compression, c_norm, archived, created_at, call_type
|
|
143
|
+
FROM distillations
|
|
144
|
+
WHERE project_id = ? AND session_id = ?
|
|
145
|
+
ORDER BY created_at DESC LIMIT ?`,
|
|
146
|
+
)
|
|
147
|
+
.all(pid, opts.sessionId, limit) as DistillationSummary[];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return db()
|
|
151
|
+
.query(
|
|
152
|
+
`SELECT id, session_id, generation, token_count, r_compression, c_norm, archived, created_at, call_type
|
|
153
|
+
FROM distillations
|
|
154
|
+
WHERE project_id = ?
|
|
155
|
+
ORDER BY created_at DESC LIMIT ?`,
|
|
156
|
+
)
|
|
157
|
+
.all(pid, limit) as DistillationSummary[];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Get a single distillation by ID (or resolved prefix). */
|
|
161
|
+
export function getDistillation(id: string): DistillationDetail | null {
|
|
162
|
+
return db()
|
|
163
|
+
.query(
|
|
164
|
+
`SELECT id, project_id, session_id, observations, source_ids, generation,
|
|
165
|
+
token_count, r_compression, c_norm, archived, created_at
|
|
166
|
+
FROM distillations WHERE id = ?`,
|
|
167
|
+
)
|
|
168
|
+
.get(id) as DistillationDetail | null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Resolve a partial ID prefix to a full ID for a given table.
|
|
173
|
+
* Returns null if 0 or 2+ matches (ambiguous prefix).
|
|
174
|
+
*/
|
|
175
|
+
export function resolveId(
|
|
176
|
+
table: "knowledge" | "distillations",
|
|
177
|
+
prefix: string,
|
|
178
|
+
): string | null {
|
|
179
|
+
const results = db()
|
|
180
|
+
.query(`SELECT id FROM ${table} WHERE id LIKE ? LIMIT 2`)
|
|
181
|
+
.all(prefix + "%") as Array<{ id: string }>;
|
|
182
|
+
return results.length === 1 ? results[0].id : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Stats
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
/** Global stats for the dashboard. */
|
|
190
|
+
export function globalStats(): GlobalStats {
|
|
191
|
+
const row = db()
|
|
192
|
+
.query(
|
|
193
|
+
`SELECT
|
|
194
|
+
(SELECT COUNT(*) FROM projects) as project_count,
|
|
195
|
+
(SELECT COUNT(*) FROM knowledge WHERE confidence > 0.2) as knowledge_count,
|
|
196
|
+
(SELECT COUNT(DISTINCT session_id) FROM temporal_messages) as session_count,
|
|
197
|
+
(SELECT COUNT(*) FROM temporal_messages) as message_count,
|
|
198
|
+
(SELECT COUNT(*) FROM distillations) as distillation_count`,
|
|
199
|
+
)
|
|
200
|
+
.get() as Omit<GlobalStats, "db_size_bytes">;
|
|
201
|
+
|
|
202
|
+
let db_size_bytes = 0;
|
|
203
|
+
try {
|
|
204
|
+
const p = dbPath();
|
|
205
|
+
db_size_bytes = statSync(p).size;
|
|
206
|
+
// Add WAL file size if present
|
|
207
|
+
const walPath = p + "-wal";
|
|
208
|
+
if (existsSync(walPath)) {
|
|
209
|
+
db_size_bytes += statSync(walPath).size;
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
// File may not exist yet or stat fails
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { ...row, db_size_bytes };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Deletion functions
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Count rows that will be affected, for confirmation prompts.
|
|
224
|
+
*/
|
|
225
|
+
export function countForProject(projectPath: string): {
|
|
226
|
+
knowledge: number;
|
|
227
|
+
messages: number;
|
|
228
|
+
distillations: number;
|
|
229
|
+
sessions: number;
|
|
230
|
+
} {
|
|
231
|
+
const pid = projectId(projectPath);
|
|
232
|
+
if (!pid)
|
|
233
|
+
return { knowledge: 0, messages: 0, distillations: 0, sessions: 0 };
|
|
234
|
+
|
|
235
|
+
const row = db()
|
|
236
|
+
.query(
|
|
237
|
+
`SELECT
|
|
238
|
+
(SELECT COUNT(*) FROM knowledge WHERE project_id = ? AND confidence > 0.2) as knowledge,
|
|
239
|
+
(SELECT COUNT(*) FROM temporal_messages WHERE project_id = ?) as messages,
|
|
240
|
+
(SELECT COUNT(*) FROM distillations WHERE project_id = ?) as distillations,
|
|
241
|
+
(SELECT COUNT(DISTINCT session_id) FROM temporal_messages WHERE project_id = ?) as sessions`,
|
|
242
|
+
)
|
|
243
|
+
.get(pid, pid, pid, pid) as {
|
|
244
|
+
knowledge: number;
|
|
245
|
+
messages: number;
|
|
246
|
+
distillations: number;
|
|
247
|
+
sessions: number;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
return row;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Clear all data for a project.
|
|
255
|
+
* Deletes: knowledge, temporal_messages, distillations, session_state.
|
|
256
|
+
* Does NOT delete the project row itself (preserves path->id mapping).
|
|
257
|
+
* Regenerates `.lore.md` if the project path exists on disk.
|
|
258
|
+
*/
|
|
259
|
+
export function clearProject(projectPath: string): ClearResult {
|
|
260
|
+
const pid = ensureProject(projectPath);
|
|
261
|
+
const database = db();
|
|
262
|
+
|
|
263
|
+
// Count before deleting (result.changes is inflated by FTS triggers)
|
|
264
|
+
const counts = {
|
|
265
|
+
knowledge: (
|
|
266
|
+
database
|
|
267
|
+
.query(
|
|
268
|
+
"SELECT COUNT(*) as c FROM knowledge WHERE project_id = ?",
|
|
269
|
+
)
|
|
270
|
+
.get(pid) as { c: number }
|
|
271
|
+
).c,
|
|
272
|
+
temporal: (
|
|
273
|
+
database
|
|
274
|
+
.query(
|
|
275
|
+
"SELECT COUNT(*) as c FROM temporal_messages WHERE project_id = ?",
|
|
276
|
+
)
|
|
277
|
+
.get(pid) as { c: number }
|
|
278
|
+
).c,
|
|
279
|
+
distillations: (
|
|
280
|
+
database
|
|
281
|
+
.query(
|
|
282
|
+
"SELECT COUNT(*) as c FROM distillations WHERE project_id = ?",
|
|
283
|
+
)
|
|
284
|
+
.get(pid) as { c: number }
|
|
285
|
+
).c,
|
|
286
|
+
sessions: (
|
|
287
|
+
database
|
|
288
|
+
.query(
|
|
289
|
+
"SELECT COUNT(DISTINCT session_id) as c FROM temporal_messages WHERE project_id = ?",
|
|
290
|
+
)
|
|
291
|
+
.get(pid) as { c: number }
|
|
292
|
+
).c,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Delete in dependency order
|
|
296
|
+
database.exec("BEGIN IMMEDIATE");
|
|
297
|
+
try {
|
|
298
|
+
// Delete session_state BEFORE temporal_messages (subquery needs the rows)
|
|
299
|
+
database
|
|
300
|
+
.query(
|
|
301
|
+
`DELETE FROM session_state WHERE session_id IN
|
|
302
|
+
(SELECT DISTINCT session_id FROM temporal_messages WHERE project_id = ?)`,
|
|
303
|
+
)
|
|
304
|
+
.run(pid);
|
|
305
|
+
database
|
|
306
|
+
.query("DELETE FROM knowledge WHERE project_id = ?")
|
|
307
|
+
.run(pid);
|
|
308
|
+
database
|
|
309
|
+
.query("DELETE FROM temporal_messages WHERE project_id = ?")
|
|
310
|
+
.run(pid);
|
|
311
|
+
database
|
|
312
|
+
.query("DELETE FROM distillations WHERE project_id = ?")
|
|
313
|
+
.run(pid);
|
|
314
|
+
database
|
|
315
|
+
.query("DELETE FROM lat_sections WHERE project_id = ?")
|
|
316
|
+
.run(pid);
|
|
317
|
+
database.exec("COMMIT");
|
|
318
|
+
} catch (e) {
|
|
319
|
+
database.exec("ROLLBACK");
|
|
320
|
+
throw e;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Regenerate .lore.md (will be empty/minimal after clearing knowledge)
|
|
324
|
+
if (existsSync(projectPath)) {
|
|
325
|
+
try {
|
|
326
|
+
agentsFile.exportLoreFile(projectPath);
|
|
327
|
+
} catch {
|
|
328
|
+
// Non-fatal: project dir may not be writable
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
knowledge_deleted: counts.knowledge,
|
|
334
|
+
temporal_deleted: counts.temporal,
|
|
335
|
+
distillations_deleted: counts.distillations,
|
|
336
|
+
sessions_cleared: counts.sessions,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Fully delete a project: all associated data AND the project row itself.
|
|
342
|
+
* Also removes path aliases pointing to this project.
|
|
343
|
+
*
|
|
344
|
+
* Unlike clearProject(), this does NOT call ensureProject() (avoids
|
|
345
|
+
* re-creating the project) and does NOT regenerate .lore.md.
|
|
346
|
+
*
|
|
347
|
+
* Returns deletion counts, or null if the project ID doesn't exist.
|
|
348
|
+
*/
|
|
349
|
+
export function deleteProject(projectId: string): ClearResult | null {
|
|
350
|
+
const database = db();
|
|
351
|
+
|
|
352
|
+
// Verify the project exists and collect all paths BEFORE deleting.
|
|
353
|
+
// We need these to invalidate the .lore.md file cache (kv_meta) after
|
|
354
|
+
// deletion — otherwise shouldImportLoreFile() sees the stale cache,
|
|
355
|
+
// skips re-import, and the curator overwrites .lore.md with junk.
|
|
356
|
+
const project = database
|
|
357
|
+
.query("SELECT id, path FROM projects WHERE id = ?")
|
|
358
|
+
.get(projectId) as { id: string; path: string } | null;
|
|
359
|
+
if (!project) return null;
|
|
360
|
+
|
|
361
|
+
const aliasPaths = database
|
|
362
|
+
.query("SELECT path FROM project_path_aliases WHERE project_id = ?")
|
|
363
|
+
.all(projectId) as { path: string }[];
|
|
364
|
+
const allPaths = [project.path, ...aliasPaths.map((r) => r.path)];
|
|
365
|
+
|
|
366
|
+
// Count before deleting
|
|
367
|
+
const counts = {
|
|
368
|
+
knowledge: (
|
|
369
|
+
database
|
|
370
|
+
.query(
|
|
371
|
+
"SELECT COUNT(*) as c FROM knowledge WHERE project_id = ?",
|
|
372
|
+
)
|
|
373
|
+
.get(projectId) as { c: number }
|
|
374
|
+
).c,
|
|
375
|
+
temporal: (
|
|
376
|
+
database
|
|
377
|
+
.query(
|
|
378
|
+
"SELECT COUNT(*) as c FROM temporal_messages WHERE project_id = ?",
|
|
379
|
+
)
|
|
380
|
+
.get(projectId) as { c: number }
|
|
381
|
+
).c,
|
|
382
|
+
distillations: (
|
|
383
|
+
database
|
|
384
|
+
.query(
|
|
385
|
+
"SELECT COUNT(*) as c FROM distillations WHERE project_id = ?",
|
|
386
|
+
)
|
|
387
|
+
.get(projectId) as { c: number }
|
|
388
|
+
).c,
|
|
389
|
+
sessions: (
|
|
390
|
+
database
|
|
391
|
+
.query(
|
|
392
|
+
"SELECT COUNT(DISTINCT session_id) as c FROM temporal_messages WHERE project_id = ?",
|
|
393
|
+
)
|
|
394
|
+
.get(projectId) as { c: number }
|
|
395
|
+
).c,
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
database.exec("BEGIN IMMEDIATE");
|
|
399
|
+
try {
|
|
400
|
+
// Delete session_state BEFORE temporal_messages (subquery needs the rows)
|
|
401
|
+
database
|
|
402
|
+
.query(
|
|
403
|
+
`DELETE FROM session_state WHERE session_id IN
|
|
404
|
+
(SELECT DISTINCT session_id FROM temporal_messages WHERE project_id = ?)`,
|
|
405
|
+
)
|
|
406
|
+
.run(projectId);
|
|
407
|
+
database
|
|
408
|
+
.query("DELETE FROM knowledge WHERE project_id = ?")
|
|
409
|
+
.run(projectId);
|
|
410
|
+
database
|
|
411
|
+
.query("DELETE FROM temporal_messages WHERE project_id = ?")
|
|
412
|
+
.run(projectId);
|
|
413
|
+
database
|
|
414
|
+
.query("DELETE FROM distillations WHERE project_id = ?")
|
|
415
|
+
.run(projectId);
|
|
416
|
+
database
|
|
417
|
+
.query("DELETE FROM lat_sections WHERE project_id = ?")
|
|
418
|
+
.run(projectId);
|
|
419
|
+
// Explicit delete for safety (FK CASCADE depends on PRAGMA foreign_keys)
|
|
420
|
+
database
|
|
421
|
+
.query("DELETE FROM project_path_aliases WHERE project_id = ?")
|
|
422
|
+
.run(projectId);
|
|
423
|
+
database
|
|
424
|
+
.query("DELETE FROM warmup_histograms WHERE project_id = ?")
|
|
425
|
+
.run(projectId);
|
|
426
|
+
// Finally, delete the project row itself
|
|
427
|
+
database
|
|
428
|
+
.query("DELETE FROM projects WHERE id = ?")
|
|
429
|
+
.run(projectId);
|
|
430
|
+
database.exec("COMMIT");
|
|
431
|
+
} catch (e) {
|
|
432
|
+
database.exec("ROLLBACK");
|
|
433
|
+
throw e;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Invalidate the .lore.md file cache for all known paths so that
|
|
437
|
+
// shouldImportLoreFile() re-checks the file if this project path
|
|
438
|
+
// is reused. Without this, the stale cache causes the import to be
|
|
439
|
+
// skipped, the curator creates junk entries, and exportLoreFile()
|
|
440
|
+
// overwrites the good .lore.md with garbage.
|
|
441
|
+
for (const p of allPaths) {
|
|
442
|
+
agentsFile.clearLoreFileCache(p);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
knowledge_deleted: counts.knowledge,
|
|
447
|
+
temporal_deleted: counts.temporal,
|
|
448
|
+
distillations_deleted: counts.distillations,
|
|
449
|
+
sessions_cleared: counts.sessions,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Rename a project. Returns true if the project exists and was renamed. */
|
|
454
|
+
export function renameProject(projectId: string, newName: string): boolean {
|
|
455
|
+
const result = db()
|
|
456
|
+
.query("UPDATE projects SET name = ? WHERE id = ?")
|
|
457
|
+
.run(newName.trim(), projectId);
|
|
458
|
+
return result.changes > 0;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Clear only knowledge entries for a project. Regenerates .lore.md. */
|
|
462
|
+
export function clearKnowledge(projectPath: string): number {
|
|
463
|
+
const pid = ensureProject(projectPath);
|
|
464
|
+
const count = (
|
|
465
|
+
db()
|
|
466
|
+
.query(
|
|
467
|
+
"SELECT COUNT(*) as c FROM knowledge WHERE project_id = ?",
|
|
468
|
+
)
|
|
469
|
+
.get(pid) as { c: number }
|
|
470
|
+
).c;
|
|
471
|
+
|
|
472
|
+
db().query("DELETE FROM knowledge WHERE project_id = ?").run(pid);
|
|
473
|
+
|
|
474
|
+
// Regenerate .lore.md
|
|
475
|
+
if (existsSync(projectPath)) {
|
|
476
|
+
try {
|
|
477
|
+
agentsFile.exportLoreFile(projectPath);
|
|
478
|
+
} catch {
|
|
479
|
+
// Non-fatal
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return count;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Clear only temporal messages for a project. */
|
|
487
|
+
export function clearTemporal(projectPath: string): number {
|
|
488
|
+
const pid = ensureProject(projectPath);
|
|
489
|
+
const count = (
|
|
490
|
+
db()
|
|
491
|
+
.query(
|
|
492
|
+
"SELECT COUNT(*) as c FROM temporal_messages WHERE project_id = ?",
|
|
493
|
+
)
|
|
494
|
+
.get(pid) as { c: number }
|
|
495
|
+
).c;
|
|
496
|
+
|
|
497
|
+
db()
|
|
498
|
+
.query("DELETE FROM temporal_messages WHERE project_id = ?")
|
|
499
|
+
.run(pid);
|
|
500
|
+
|
|
501
|
+
return count;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Clear only distillations for a project. */
|
|
505
|
+
export function clearDistillations(projectPath: string): number {
|
|
506
|
+
const pid = ensureProject(projectPath);
|
|
507
|
+
const count = (
|
|
508
|
+
db()
|
|
509
|
+
.query(
|
|
510
|
+
"SELECT COUNT(*) as c FROM distillations WHERE project_id = ?",
|
|
511
|
+
)
|
|
512
|
+
.get(pid) as { c: number }
|
|
513
|
+
).c;
|
|
514
|
+
|
|
515
|
+
db()
|
|
516
|
+
.query("DELETE FROM distillations WHERE project_id = ?")
|
|
517
|
+
.run(pid);
|
|
518
|
+
|
|
519
|
+
return count;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Delete a single knowledge entry. Returns true if found and deleted. */
|
|
523
|
+
export function deleteKnowledge(id: string): boolean {
|
|
524
|
+
const entry = ltm.get(id);
|
|
525
|
+
if (!entry) return false;
|
|
526
|
+
ltm.remove(id);
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Delete a single distillation. Returns true if found and deleted. */
|
|
531
|
+
export function deleteDistillation(id: string): boolean {
|
|
532
|
+
const existing = getDistillation(id);
|
|
533
|
+
if (!existing) return false;
|
|
534
|
+
db().query("DELETE FROM distillations WHERE id = ?").run(id);
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Delete all data for a specific session (messages + distillations + session_state).
|
|
540
|
+
*/
|
|
541
|
+
export function deleteSession(
|
|
542
|
+
projectPath: string,
|
|
543
|
+
sessionId: string,
|
|
544
|
+
): { messages_deleted: number; distillations_deleted: number } {
|
|
545
|
+
const pid = ensureProject(projectPath);
|
|
546
|
+
const database = db();
|
|
547
|
+
|
|
548
|
+
const msgCount = (
|
|
549
|
+
database
|
|
550
|
+
.query(
|
|
551
|
+
"SELECT COUNT(*) as c FROM temporal_messages WHERE project_id = ? AND session_id = ?",
|
|
552
|
+
)
|
|
553
|
+
.get(pid, sessionId) as { c: number }
|
|
554
|
+
).c;
|
|
555
|
+
|
|
556
|
+
const distCount = (
|
|
557
|
+
database
|
|
558
|
+
.query(
|
|
559
|
+
"SELECT COUNT(*) as c FROM distillations WHERE project_id = ? AND session_id = ?",
|
|
560
|
+
)
|
|
561
|
+
.get(pid, sessionId) as { c: number }
|
|
562
|
+
).c;
|
|
563
|
+
|
|
564
|
+
database
|
|
565
|
+
.query(
|
|
566
|
+
"DELETE FROM temporal_messages WHERE project_id = ? AND session_id = ?",
|
|
567
|
+
)
|
|
568
|
+
.run(pid, sessionId);
|
|
569
|
+
database
|
|
570
|
+
.query(
|
|
571
|
+
"DELETE FROM distillations WHERE project_id = ? AND session_id = ?",
|
|
572
|
+
)
|
|
573
|
+
.run(pid, sessionId);
|
|
574
|
+
database
|
|
575
|
+
.query("DELETE FROM session_state WHERE session_id = ?")
|
|
576
|
+
.run(sessionId);
|
|
577
|
+
|
|
578
|
+
return { messages_deleted: msgCount, distillations_deleted: distCount };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Nuclear option: close the DB, delete the file, re-initialize.
|
|
583
|
+
* Returns the path of the deleted DB file.
|
|
584
|
+
*/
|
|
585
|
+
export function wipeDatabase(): string {
|
|
586
|
+
const p = dbPath();
|
|
587
|
+
close();
|
|
588
|
+
|
|
589
|
+
// Delete DB and associated WAL/SHM files
|
|
590
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
591
|
+
const fp = p + suffix;
|
|
592
|
+
if (existsSync(fp)) {
|
|
593
|
+
try {
|
|
594
|
+
unlinkSync(fp);
|
|
595
|
+
} catch {
|
|
596
|
+
// Best-effort
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Re-initialize with fresh schema
|
|
602
|
+
db();
|
|
603
|
+
return p;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
607
|
+
// Project merging & git remote backfill
|
|
608
|
+
// ---------------------------------------------------------------------------
|
|
609
|
+
|
|
610
|
+
export type MergeResult = {
|
|
611
|
+
knowledge_moved: number;
|
|
612
|
+
messages_moved: number;
|
|
613
|
+
distillations_moved: number;
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Merge a source project into a target project.
|
|
618
|
+
*
|
|
619
|
+
* Moves all data (knowledge, messages, distillations, LAT sections, path
|
|
620
|
+
* aliases) from source to target, then deletes the source project row.
|
|
621
|
+
* The source project's path is registered as an alias of the target.
|
|
622
|
+
*
|
|
623
|
+
* Returns counts of moved rows for reporting.
|
|
624
|
+
*/
|
|
625
|
+
export function mergeProjects(sourceId: string, targetId: string): MergeResult {
|
|
626
|
+
const database = db();
|
|
627
|
+
|
|
628
|
+
// Count before merging (result.changes is inflated by FTS triggers)
|
|
629
|
+
const counts = {
|
|
630
|
+
knowledge: (
|
|
631
|
+
database
|
|
632
|
+
.query(
|
|
633
|
+
"SELECT COUNT(*) as c FROM knowledge WHERE project_id = ?",
|
|
634
|
+
)
|
|
635
|
+
.get(sourceId) as { c: number }
|
|
636
|
+
).c,
|
|
637
|
+
messages: (
|
|
638
|
+
database
|
|
639
|
+
.query(
|
|
640
|
+
"SELECT COUNT(*) as c FROM temporal_messages WHERE project_id = ?",
|
|
641
|
+
)
|
|
642
|
+
.get(sourceId) as { c: number }
|
|
643
|
+
).c,
|
|
644
|
+
distillations: (
|
|
645
|
+
database
|
|
646
|
+
.query(
|
|
647
|
+
"SELECT COUNT(*) as c FROM distillations WHERE project_id = ?",
|
|
648
|
+
)
|
|
649
|
+
.get(sourceId) as { c: number }
|
|
650
|
+
).c,
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
mergeProjectInternal(sourceId, targetId);
|
|
654
|
+
|
|
655
|
+
return {
|
|
656
|
+
knowledge_moved: counts.knowledge,
|
|
657
|
+
messages_moved: counts.messages,
|
|
658
|
+
distillations_moved: counts.distillations,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Backfill git_remote for existing projects, merge duplicates, and
|
|
664
|
+
* update project names from git remote repo names where still using
|
|
665
|
+
* the directory-basename default.
|
|
666
|
+
*
|
|
667
|
+
* Iterates all projects that lack a git_remote value, runs `git remote -v`
|
|
668
|
+
* on their stored path, and:
|
|
669
|
+
* - If no other project shares that remote: sets git_remote on the row.
|
|
670
|
+
* - If another project already has that remote: merges this project into
|
|
671
|
+
* the existing one (consolidating fragmented data).
|
|
672
|
+
*
|
|
673
|
+
* Also backfills project names: if a project's name matches the directory
|
|
674
|
+
* basename (the old default) or is null, and a git remote is available,
|
|
675
|
+
* the name is updated to the repo name from the remote URL.
|
|
676
|
+
*
|
|
677
|
+
* Skips projects whose path no longer exists on disk or is not a git repo.
|
|
678
|
+
*
|
|
679
|
+
* Returns counts for reporting.
|
|
680
|
+
*/
|
|
681
|
+
export function backfillGitRemotes(): {
|
|
682
|
+
updated: number;
|
|
683
|
+
merged: number;
|
|
684
|
+
namesBackfilled: number;
|
|
685
|
+
mergeDetails: Array<{
|
|
686
|
+
sourcePath: string;
|
|
687
|
+
targetPath: string;
|
|
688
|
+
gitRemote: string;
|
|
689
|
+
result: MergeResult;
|
|
690
|
+
}>;
|
|
691
|
+
} {
|
|
692
|
+
const projects = db()
|
|
693
|
+
.query(
|
|
694
|
+
"SELECT id, path, name, git_remote FROM projects ORDER BY created_at ASC",
|
|
695
|
+
)
|
|
696
|
+
.all() as Array<{
|
|
697
|
+
id: string;
|
|
698
|
+
path: string;
|
|
699
|
+
name: string | null;
|
|
700
|
+
git_remote: string | null;
|
|
701
|
+
}>;
|
|
702
|
+
|
|
703
|
+
let updated = 0;
|
|
704
|
+
let merged = 0;
|
|
705
|
+
let namesBackfilled = 0;
|
|
706
|
+
const mergeDetails: Array<{
|
|
707
|
+
sourcePath: string;
|
|
708
|
+
targetPath: string;
|
|
709
|
+
gitRemote: string;
|
|
710
|
+
result: MergeResult;
|
|
711
|
+
}> = [];
|
|
712
|
+
|
|
713
|
+
for (const project of projects) {
|
|
714
|
+
let gitRemote = project.git_remote;
|
|
715
|
+
|
|
716
|
+
if (!gitRemote) {
|
|
717
|
+
// Skip if path doesn't exist
|
|
718
|
+
if (!existsSync(project.path)) continue;
|
|
719
|
+
|
|
720
|
+
// Try to get git remote
|
|
721
|
+
gitRemote = getGitRemote(project.path);
|
|
722
|
+
if (!gitRemote) continue;
|
|
723
|
+
|
|
724
|
+
// Check if another project already has this git_remote
|
|
725
|
+
const existing = db()
|
|
726
|
+
.query(
|
|
727
|
+
"SELECT id, path FROM projects WHERE git_remote = ? AND id != ? LIMIT 1",
|
|
728
|
+
)
|
|
729
|
+
.get(gitRemote, project.id) as {
|
|
730
|
+
id: string;
|
|
731
|
+
path: string;
|
|
732
|
+
} | null;
|
|
733
|
+
|
|
734
|
+
if (existing) {
|
|
735
|
+
// Merge this project into the existing one
|
|
736
|
+
const result = mergeProjects(project.id, existing.id);
|
|
737
|
+
mergeDetails.push({
|
|
738
|
+
sourcePath: project.path,
|
|
739
|
+
targetPath: existing.path,
|
|
740
|
+
gitRemote,
|
|
741
|
+
result,
|
|
742
|
+
});
|
|
743
|
+
merged++;
|
|
744
|
+
continue; // project was merged away, skip name backfill
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Set the git_remote
|
|
748
|
+
db()
|
|
749
|
+
.query("UPDATE projects SET git_remote = ? WHERE id = ?")
|
|
750
|
+
.run(gitRemote, project.id);
|
|
751
|
+
updated++;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Backfill name from git remote if still using directory basename default
|
|
755
|
+
const dirBasename = project.path.split("/").pop();
|
|
756
|
+
if (project.name === dirBasename || !project.name) {
|
|
757
|
+
const repoName = repoNameFromRemote(gitRemote);
|
|
758
|
+
if (repoName && repoName !== project.name) {
|
|
759
|
+
db()
|
|
760
|
+
.query("UPDATE projects SET name = ? WHERE id = ?")
|
|
761
|
+
.run(repoName, project.id);
|
|
762
|
+
namesBackfilled++;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return { updated, merged, namesBackfilled, mergeDetails };
|
|
768
|
+
}
|