@memtensor/memos-local-openclaw-plugin 0.1.4 → 0.1.5
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 +196 -84
- package/dist/ingest/dedup.d.ts +8 -0
- package/dist/ingest/dedup.d.ts.map +1 -1
- package/dist/ingest/dedup.js +21 -0
- package/dist/ingest/dedup.js.map +1 -1
- package/dist/ingest/providers/anthropic.d.ts +14 -0
- package/dist/ingest/providers/anthropic.d.ts.map +1 -1
- package/dist/ingest/providers/anthropic.js +104 -0
- package/dist/ingest/providers/anthropic.js.map +1 -1
- package/dist/ingest/providers/bedrock.d.ts +14 -0
- package/dist/ingest/providers/bedrock.d.ts.map +1 -1
- package/dist/ingest/providers/bedrock.js +100 -0
- package/dist/ingest/providers/bedrock.js.map +1 -1
- package/dist/ingest/providers/gemini.d.ts +14 -0
- package/dist/ingest/providers/gemini.d.ts.map +1 -1
- package/dist/ingest/providers/gemini.js +96 -0
- package/dist/ingest/providers/gemini.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +22 -0
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +68 -0
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +22 -0
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +143 -0
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/ingest/task-processor.d.ts +2 -0
- package/dist/ingest/task-processor.d.ts.map +1 -1
- package/dist/ingest/task-processor.js +15 -0
- package/dist/ingest/task-processor.js.map +1 -1
- package/dist/ingest/worker.d.ts +2 -0
- package/dist/ingest/worker.d.ts.map +1 -1
- package/dist/ingest/worker.js +115 -12
- package/dist/ingest/worker.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +1 -0
- package/dist/recall/engine.js.map +1 -1
- package/dist/skill/bundled-memory-guide.d.ts +6 -0
- package/dist/skill/bundled-memory-guide.d.ts.map +1 -0
- package/dist/skill/bundled-memory-guide.js +95 -0
- package/dist/skill/bundled-memory-guide.js.map +1 -0
- package/dist/skill/evaluator.d.ts +31 -0
- package/dist/skill/evaluator.d.ts.map +1 -0
- package/dist/skill/evaluator.js +194 -0
- package/dist/skill/evaluator.js.map +1 -0
- package/dist/skill/evolver.d.ts +22 -0
- package/dist/skill/evolver.d.ts.map +1 -0
- package/dist/skill/evolver.js +193 -0
- package/dist/skill/evolver.js.map +1 -0
- package/dist/skill/generator.d.ts +25 -0
- package/dist/skill/generator.d.ts.map +1 -0
- package/dist/skill/generator.js +477 -0
- package/dist/skill/generator.js.map +1 -0
- package/dist/skill/installer.d.ts +16 -0
- package/dist/skill/installer.d.ts.map +1 -0
- package/dist/skill/installer.js +89 -0
- package/dist/skill/installer.js.map +1 -0
- package/dist/skill/upgrader.d.ts +19 -0
- package/dist/skill/upgrader.d.ts.map +1 -0
- package/dist/skill/upgrader.js +263 -0
- package/dist/skill/upgrader.js.map +1 -0
- package/dist/skill/validator.d.ts +29 -0
- package/dist/skill/validator.d.ts.map +1 -0
- package/dist/skill/validator.js +227 -0
- package/dist/skill/validator.js.map +1 -0
- package/dist/storage/sqlite.d.ts +75 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +417 -6
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +1549 -113
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +13 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +289 -4
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +489 -181
- package/package.json +1 -1
- package/skill/memos-memory-guide/SKILL.md +86 -0
- package/src/ingest/dedup.ts +29 -0
- package/src/ingest/providers/anthropic.ts +130 -0
- package/src/ingest/providers/bedrock.ts +126 -0
- package/src/ingest/providers/gemini.ts +124 -0
- package/src/ingest/providers/index.ts +86 -4
- package/src/ingest/providers/openai.ts +174 -0
- package/src/ingest/task-processor.ts +16 -0
- package/src/ingest/worker.ts +126 -21
- package/src/recall/engine.ts +1 -0
- package/src/skill/bundled-memory-guide.ts +91 -0
- package/src/skill/evaluator.ts +220 -0
- package/src/skill/evolver.ts +169 -0
- package/src/skill/generator.ts +506 -0
- package/src/skill/installer.ts +59 -0
- package/src/skill/upgrader.ts +257 -0
- package/src/skill/validator.ts +227 -0
- package/src/storage/sqlite.ts +508 -6
- package/src/types.ts +77 -0
- package/src/viewer/html.ts +1549 -113
- package/src/viewer/server.ts +285 -4
- package/skill/SKILL.md +0 -59
package/src/storage/sqlite.ts
CHANGED
|
@@ -2,7 +2,7 @@ import Database from "better-sqlite3";
|
|
|
2
2
|
import { createHash } from "crypto";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
|
-
import type { Chunk, ChunkRef, Task, TaskStatus, Logger } from "../types";
|
|
5
|
+
import type { Chunk, ChunkRef, DedupStatus, Task, TaskStatus, Skill, SkillStatus, SkillVersion, TaskSkillLink, TaskSkillRelation, Logger } from "../types";
|
|
6
6
|
|
|
7
7
|
export class SqliteStore {
|
|
8
8
|
private db: Database.Database;
|
|
@@ -95,6 +95,14 @@ export class SqliteStore {
|
|
|
95
95
|
|
|
96
96
|
this.migrateTaskId();
|
|
97
97
|
this.migrateContentHash();
|
|
98
|
+
this.migrateSkillTables();
|
|
99
|
+
this.migrateSkillId();
|
|
100
|
+
this.migrateSkillQualityScore();
|
|
101
|
+
this.migrateTaskSkillMeta();
|
|
102
|
+
this.migrateToolCalls();
|
|
103
|
+
this.migrateMergeFields();
|
|
104
|
+
this.migrateApiLogs();
|
|
105
|
+
this.migrateDedupStatus();
|
|
98
106
|
this.log.debug("Database schema initialized");
|
|
99
107
|
}
|
|
100
108
|
|
|
@@ -125,6 +133,295 @@ export class SqliteStore {
|
|
|
125
133
|
}
|
|
126
134
|
}
|
|
127
135
|
|
|
136
|
+
private migrateSkillTables(): void {
|
|
137
|
+
this.db.exec(`
|
|
138
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
139
|
+
id TEXT PRIMARY KEY,
|
|
140
|
+
name TEXT NOT NULL UNIQUE,
|
|
141
|
+
description TEXT NOT NULL DEFAULT '',
|
|
142
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
143
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
144
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
145
|
+
source_type TEXT NOT NULL DEFAULT 'task',
|
|
146
|
+
dir_path TEXT NOT NULL DEFAULT '',
|
|
147
|
+
installed INTEGER NOT NULL DEFAULT 0,
|
|
148
|
+
created_at INTEGER NOT NULL,
|
|
149
|
+
updated_at INTEGER NOT NULL
|
|
150
|
+
);
|
|
151
|
+
CREATE INDEX IF NOT EXISTS idx_skills_status ON skills(status);
|
|
152
|
+
CREATE INDEX IF NOT EXISTS idx_skills_name ON skills(name);
|
|
153
|
+
|
|
154
|
+
CREATE TABLE IF NOT EXISTS skill_versions (
|
|
155
|
+
id TEXT PRIMARY KEY,
|
|
156
|
+
skill_id TEXT NOT NULL REFERENCES skills(id),
|
|
157
|
+
version INTEGER NOT NULL,
|
|
158
|
+
content TEXT NOT NULL,
|
|
159
|
+
changelog TEXT NOT NULL DEFAULT '',
|
|
160
|
+
upgrade_type TEXT NOT NULL DEFAULT 'create',
|
|
161
|
+
source_task_id TEXT,
|
|
162
|
+
metrics TEXT NOT NULL DEFAULT '{}',
|
|
163
|
+
created_at INTEGER NOT NULL,
|
|
164
|
+
UNIQUE(skill_id, version)
|
|
165
|
+
);
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_skill_versions_skill ON skill_versions(skill_id);
|
|
167
|
+
|
|
168
|
+
CREATE TABLE IF NOT EXISTS task_skills (
|
|
169
|
+
task_id TEXT NOT NULL REFERENCES tasks(id),
|
|
170
|
+
skill_id TEXT NOT NULL REFERENCES skills(id),
|
|
171
|
+
relation TEXT NOT NULL DEFAULT 'generated_from',
|
|
172
|
+
version_at INTEGER NOT NULL DEFAULT 1,
|
|
173
|
+
created_at INTEGER NOT NULL,
|
|
174
|
+
PRIMARY KEY (task_id, skill_id)
|
|
175
|
+
);
|
|
176
|
+
`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private migrateSkillId(): void {
|
|
180
|
+
const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
|
|
181
|
+
if (!cols.some((c) => c.name === "skill_id")) {
|
|
182
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN skill_id TEXT");
|
|
183
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_skill ON chunks(skill_id)");
|
|
184
|
+
this.log.info("Migrated: added skill_id column to chunks");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private migrateSkillQualityScore(): void {
|
|
189
|
+
const skillCols = this.db.prepare("PRAGMA table_info(skills)").all() as Array<{ name: string }>;
|
|
190
|
+
if (!skillCols.some((c) => c.name === "quality_score")) {
|
|
191
|
+
this.db.exec("ALTER TABLE skills ADD COLUMN quality_score REAL");
|
|
192
|
+
this.log.info("Migrated: added quality_score column to skills");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const versionCols = this.db.prepare("PRAGMA table_info(skill_versions)").all() as Array<{ name: string }>;
|
|
196
|
+
if (!versionCols.some((c) => c.name === "quality_score")) {
|
|
197
|
+
this.db.exec("ALTER TABLE skill_versions ADD COLUMN quality_score REAL");
|
|
198
|
+
this.log.info("Migrated: added quality_score column to skill_versions");
|
|
199
|
+
}
|
|
200
|
+
if (!versionCols.some((c) => c.name === "change_summary")) {
|
|
201
|
+
this.db.exec("ALTER TABLE skill_versions ADD COLUMN change_summary TEXT NOT NULL DEFAULT ''");
|
|
202
|
+
this.log.info("Migrated: added change_summary column to skill_versions");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private migrateTaskSkillMeta(): void {
|
|
207
|
+
const cols = this.db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
|
208
|
+
if (!cols.some((c) => c.name === "skill_status")) {
|
|
209
|
+
this.db.exec("ALTER TABLE tasks ADD COLUMN skill_status TEXT DEFAULT NULL");
|
|
210
|
+
this.db.exec("ALTER TABLE tasks ADD COLUMN skill_reason TEXT DEFAULT NULL");
|
|
211
|
+
this.log.info("Migrated: added skill_status/skill_reason columns to tasks");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
setTaskSkillMeta(taskId: string, meta: { skillStatus: string; skillReason: string }): void {
|
|
216
|
+
this.db.prepare("UPDATE tasks SET skill_status = ?, skill_reason = ?, updated_at = ? WHERE id = ?")
|
|
217
|
+
.run(meta.skillStatus, meta.skillReason, Date.now(), taskId);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private migrateMergeFields(): void {
|
|
221
|
+
const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
|
|
222
|
+
if (!cols.some((c) => c.name === "merge_count")) {
|
|
223
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN merge_count INTEGER NOT NULL DEFAULT 0");
|
|
224
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN last_hit_at INTEGER");
|
|
225
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN merge_history TEXT NOT NULL DEFAULT '[]'");
|
|
226
|
+
this.log.info("Migrated: added merge_count/last_hit_at/merge_history columns to chunks");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private migrateApiLogs(): void {
|
|
231
|
+
this.db.exec(`
|
|
232
|
+
CREATE TABLE IF NOT EXISTS api_logs (
|
|
233
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
234
|
+
tool_name TEXT NOT NULL,
|
|
235
|
+
input_data TEXT NOT NULL DEFAULT '{}',
|
|
236
|
+
output_data TEXT NOT NULL DEFAULT '',
|
|
237
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
238
|
+
success INTEGER NOT NULL DEFAULT 1,
|
|
239
|
+
called_at INTEGER NOT NULL
|
|
240
|
+
);
|
|
241
|
+
CREATE INDEX IF NOT EXISTS idx_api_logs_at ON api_logs(called_at);
|
|
242
|
+
CREATE INDEX IF NOT EXISTS idx_api_logs_name ON api_logs(tool_name);
|
|
243
|
+
`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private migrateDedupStatus(): void {
|
|
247
|
+
const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
|
|
248
|
+
if (!cols.some((c) => c.name === "dedup_status")) {
|
|
249
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN dedup_status TEXT NOT NULL DEFAULT 'active'");
|
|
250
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN dedup_target TEXT DEFAULT NULL");
|
|
251
|
+
this.db.exec("ALTER TABLE chunks ADD COLUMN dedup_reason TEXT DEFAULT NULL");
|
|
252
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_status ON chunks(dedup_status)");
|
|
253
|
+
this.log.info("Migrated: added dedup_status/dedup_target/dedup_reason columns to chunks");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
recordApiLog(toolName: string, input: unknown, output: string, durationMs: number, success: boolean): void {
|
|
258
|
+
const inputStr = typeof input === "string" ? input : JSON.stringify(input ?? {});
|
|
259
|
+
this.db.prepare(
|
|
260
|
+
"INSERT INTO api_logs (tool_name, input_data, output_data, duration_ms, success, called_at) VALUES (?, ?, ?, ?, ?, ?)",
|
|
261
|
+
).run(toolName, inputStr, output, Math.round(durationMs), success ? 1 : 0, Date.now());
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
getApiLogs(limit: number = 50, offset: number = 0, toolFilter?: string): {
|
|
265
|
+
logs: Array<{ id: number; toolName: string; input: string; output: string; durationMs: number; success: boolean; calledAt: number }>;
|
|
266
|
+
total: number;
|
|
267
|
+
} {
|
|
268
|
+
const whereClause = toolFilter ? " WHERE tool_name = ?" : "";
|
|
269
|
+
const filterParams: unknown[] = toolFilter ? [toolFilter] : [];
|
|
270
|
+
|
|
271
|
+
const countRow = this.db.prepare("SELECT COUNT(*) as c FROM api_logs" + whereClause).get(...filterParams) as { c: number };
|
|
272
|
+
|
|
273
|
+
const rows = this.db.prepare(
|
|
274
|
+
"SELECT id, tool_name, input_data, output_data, duration_ms, success, called_at FROM api_logs" +
|
|
275
|
+
whereClause + " ORDER BY called_at DESC LIMIT ? OFFSET ?",
|
|
276
|
+
).all(...filterParams, limit, offset) as Array<{
|
|
277
|
+
id: number; tool_name: string; input_data: string; output_data: string;
|
|
278
|
+
duration_ms: number; success: number; called_at: number;
|
|
279
|
+
}>;
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
logs: rows.map((r) => ({
|
|
283
|
+
id: r.id,
|
|
284
|
+
toolName: r.tool_name,
|
|
285
|
+
input: r.input_data,
|
|
286
|
+
output: r.output_data,
|
|
287
|
+
durationMs: r.duration_ms,
|
|
288
|
+
success: r.success === 1,
|
|
289
|
+
calledAt: r.called_at,
|
|
290
|
+
})),
|
|
291
|
+
total: countRow.c,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
getApiLogToolNames(): string[] {
|
|
296
|
+
const rows = this.db.prepare("SELECT DISTINCT tool_name FROM api_logs ORDER BY tool_name").all() as Array<{ tool_name: string }>;
|
|
297
|
+
return rows.map((r) => r.tool_name);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
recordMergeHit(chunkId: string, action: "DUPLICATE" | "UPDATE", reason: string, oldSummary?: string, newSummary?: string): void {
|
|
301
|
+
const chunk = this.getChunk(chunkId);
|
|
302
|
+
if (!chunk) return;
|
|
303
|
+
|
|
304
|
+
const history = JSON.parse(chunk.mergeHistory || "[]") as any[];
|
|
305
|
+
const entry: Record<string, unknown> = { at: Date.now(), action, reason };
|
|
306
|
+
if (action === "UPDATE" && oldSummary && newSummary) {
|
|
307
|
+
entry.from = oldSummary;
|
|
308
|
+
entry.to = newSummary;
|
|
309
|
+
}
|
|
310
|
+
history.push(entry);
|
|
311
|
+
|
|
312
|
+
this.db.prepare(`
|
|
313
|
+
UPDATE chunks SET merge_count = merge_count + 1, last_hit_at = ?, merge_history = ?, updated_at = ?
|
|
314
|
+
WHERE id = ?
|
|
315
|
+
`).run(Date.now(), JSON.stringify(history), Date.now(), chunkId);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
updateChunkSummaryAndContent(chunkId: string, newSummary: string, appendContent: string): void {
|
|
319
|
+
this.db.prepare(`
|
|
320
|
+
UPDATE chunks SET summary = ?, content = content || ? || ?, updated_at = ? WHERE id = ?
|
|
321
|
+
`).run(newSummary, "\n\n---\n\n", appendContent, Date.now(), chunkId);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private migrateToolCalls(): void {
|
|
325
|
+
this.db.exec(`
|
|
326
|
+
CREATE TABLE IF NOT EXISTS tool_calls (
|
|
327
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
328
|
+
tool_name TEXT NOT NULL,
|
|
329
|
+
duration_ms INTEGER NOT NULL,
|
|
330
|
+
success INTEGER NOT NULL DEFAULT 1,
|
|
331
|
+
called_at INTEGER NOT NULL
|
|
332
|
+
);
|
|
333
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_at ON tool_calls(called_at);
|
|
334
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_name ON tool_calls(tool_name);
|
|
335
|
+
`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
recordToolCall(toolName: string, durationMs: number, success: boolean): void {
|
|
339
|
+
this.db.prepare(
|
|
340
|
+
"INSERT INTO tool_calls (tool_name, duration_ms, success, called_at) VALUES (?, ?, ?, ?)",
|
|
341
|
+
).run(toolName, Math.round(durationMs), success ? 1 : 0, Date.now());
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
getToolMetrics(minutes: number): {
|
|
345
|
+
tools: string[];
|
|
346
|
+
series: Array<{ minute: string; [tool: string]: number | string }>;
|
|
347
|
+
aggregated: Array<{ tool: string; totalCalls: number; avgMs: number; p95Ms: number; errorCount: number }>;
|
|
348
|
+
} {
|
|
349
|
+
const since = Date.now() - minutes * 60 * 1000;
|
|
350
|
+
|
|
351
|
+
const rows = this.db.prepare(
|
|
352
|
+
`SELECT tool_name,
|
|
353
|
+
duration_ms,
|
|
354
|
+
success,
|
|
355
|
+
strftime('%Y-%m-%d %H:%M', called_at/1000, 'unixepoch', 'localtime') as minute_key
|
|
356
|
+
FROM tool_calls
|
|
357
|
+
WHERE called_at >= ?
|
|
358
|
+
ORDER BY called_at`,
|
|
359
|
+
).all(since) as Array<{ tool_name: string; duration_ms: number; success: number; minute_key: string }>;
|
|
360
|
+
|
|
361
|
+
const toolSet = new Set<string>();
|
|
362
|
+
const minuteMap = new Map<string, Map<string, { total: number; count: number }>>();
|
|
363
|
+
const aggMap = new Map<string, { durations: number[]; errors: number }>();
|
|
364
|
+
|
|
365
|
+
for (const r of rows) {
|
|
366
|
+
toolSet.add(r.tool_name);
|
|
367
|
+
|
|
368
|
+
if (!aggMap.has(r.tool_name)) aggMap.set(r.tool_name, { durations: [], errors: 0 });
|
|
369
|
+
const agg = aggMap.get(r.tool_name)!;
|
|
370
|
+
agg.durations.push(r.duration_ms);
|
|
371
|
+
if (!r.success) agg.errors++;
|
|
372
|
+
|
|
373
|
+
if (!minuteMap.has(r.minute_key)) minuteMap.set(r.minute_key, new Map());
|
|
374
|
+
const toolMap = minuteMap.get(r.minute_key)!;
|
|
375
|
+
if (!toolMap.has(r.tool_name)) toolMap.set(r.tool_name, { total: 0, count: 0 });
|
|
376
|
+
const entry = toolMap.get(r.tool_name)!;
|
|
377
|
+
entry.total += r.duration_ms;
|
|
378
|
+
entry.count++;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const tools = Array.from(toolSet).sort();
|
|
382
|
+
|
|
383
|
+
const allMinutes: string[] = [];
|
|
384
|
+
if (minutes > 0) {
|
|
385
|
+
const startMinute = new Date(since);
|
|
386
|
+
startMinute.setSeconds(0, 0);
|
|
387
|
+
const now = new Date();
|
|
388
|
+
for (let t = startMinute.getTime(); t <= now.getTime(); t += 60000) {
|
|
389
|
+
const d = new Date(t);
|
|
390
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
391
|
+
allMinutes.push(`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const series = allMinutes.map((m) => {
|
|
396
|
+
const entry: { minute: string; [tool: string]: number | string } = { minute: m };
|
|
397
|
+
const toolMap = minuteMap.get(m);
|
|
398
|
+
for (const t of tools) {
|
|
399
|
+
const data = toolMap?.get(t);
|
|
400
|
+
entry[t] = data ? Math.round(data.total / data.count) : 0;
|
|
401
|
+
}
|
|
402
|
+
return entry;
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const p95 = (arr: number[]) => {
|
|
406
|
+
if (arr.length === 0) return 0;
|
|
407
|
+
const sorted = [...arr].sort((a, b) => a - b);
|
|
408
|
+
return sorted[Math.floor(sorted.length * 0.95)] ?? sorted[sorted.length - 1];
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const aggregated = tools.map((t) => {
|
|
412
|
+
const agg = aggMap.get(t)!;
|
|
413
|
+
return {
|
|
414
|
+
tool: t,
|
|
415
|
+
totalCalls: agg.durations.length,
|
|
416
|
+
avgMs: Math.round(agg.durations.reduce((s, v) => s + v, 0) / agg.durations.length),
|
|
417
|
+
p95Ms: p95(agg.durations),
|
|
418
|
+
errorCount: agg.errors,
|
|
419
|
+
};
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
return { tools, series, aggregated };
|
|
423
|
+
}
|
|
424
|
+
|
|
128
425
|
/** Record a viewer API call for analytics (list, search, etc.). */
|
|
129
426
|
recordViewerEvent(eventType: string): void {
|
|
130
427
|
this.db.prepare("INSERT INTO viewer_events (event_type, created_at) VALUES (?, ?)").run(eventType, Date.now());
|
|
@@ -202,8 +499,8 @@ export class SqliteStore {
|
|
|
202
499
|
|
|
203
500
|
insertChunk(chunk: Chunk): void {
|
|
204
501
|
const stmt = this.db.prepare(`
|
|
205
|
-
INSERT OR REPLACE INTO chunks (id, session_key, turn_id, seq, role, content, kind, summary, task_id, content_hash, created_at, updated_at)
|
|
206
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
502
|
+
INSERT OR REPLACE INTO chunks (id, session_key, turn_id, seq, role, content, kind, summary, task_id, content_hash, dedup_status, dedup_target, dedup_reason, created_at, updated_at)
|
|
503
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
207
504
|
`);
|
|
208
505
|
stmt.run(
|
|
209
506
|
chunk.id,
|
|
@@ -216,11 +513,20 @@ export class SqliteStore {
|
|
|
216
513
|
chunk.summary,
|
|
217
514
|
chunk.taskId,
|
|
218
515
|
contentHash(chunk.content),
|
|
516
|
+
chunk.dedupStatus ?? "active",
|
|
517
|
+
chunk.dedupTarget ?? null,
|
|
518
|
+
chunk.dedupReason ?? null,
|
|
219
519
|
chunk.createdAt,
|
|
220
520
|
chunk.updatedAt,
|
|
221
521
|
);
|
|
222
522
|
}
|
|
223
523
|
|
|
524
|
+
markDedupStatus(chunkId: string, status: "duplicate" | "merged", targetChunkId: string | null, reason: string): void {
|
|
525
|
+
this.db.prepare(
|
|
526
|
+
"UPDATE chunks SET dedup_status = ?, dedup_target = ?, dedup_reason = ?, updated_at = ? WHERE id = ?",
|
|
527
|
+
).run(status, targetChunkId, reason, Date.now(), chunkId);
|
|
528
|
+
}
|
|
529
|
+
|
|
224
530
|
updateSummary(chunkId: string, summary: string): void {
|
|
225
531
|
this.db.prepare("UPDATE chunks SET summary = ?, updated_at = ? WHERE id = ?").run(
|
|
226
532
|
summary,
|
|
@@ -277,7 +583,7 @@ export class SqliteStore {
|
|
|
277
583
|
SELECT c.id as chunk_id, rank
|
|
278
584
|
FROM chunks_fts f
|
|
279
585
|
JOIN chunks c ON c.rowid = f.rowid
|
|
280
|
-
WHERE chunks_fts MATCH ?
|
|
586
|
+
WHERE chunks_fts MATCH ? AND c.dedup_status = 'active'
|
|
281
587
|
ORDER BY rank
|
|
282
588
|
LIMIT ?
|
|
283
589
|
`).all(sanitized, limit) as Array<{ chunk_id: string; rank: number }>;
|
|
@@ -311,7 +617,7 @@ export class SqliteStore {
|
|
|
311
617
|
const rows = this.db.prepare(`
|
|
312
618
|
SELECT c.id as chunk_id, c.content, c.role, c.created_at
|
|
313
619
|
FROM chunks c
|
|
314
|
-
WHERE (${whereClause})${roleClause}
|
|
620
|
+
WHERE (${whereClause})${roleClause} AND c.dedup_status = 'active'
|
|
315
621
|
ORDER BY c.created_at DESC
|
|
316
622
|
LIMIT ?
|
|
317
623
|
`).all(...params) as Array<{ chunk_id: string; content: string; role: string; created_at: number }>;
|
|
@@ -331,7 +637,9 @@ export class SqliteStore {
|
|
|
331
637
|
|
|
332
638
|
getAllEmbeddings(): Array<{ chunkId: string; vector: number[] }> {
|
|
333
639
|
const rows = this.db.prepare(
|
|
334
|
-
|
|
640
|
+
`SELECT e.chunk_id, e.vector, e.dimensions FROM embeddings e
|
|
641
|
+
JOIN chunks c ON c.id = e.chunk_id
|
|
642
|
+
WHERE c.dedup_status = 'active'`,
|
|
335
643
|
).all() as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
|
|
336
644
|
|
|
337
645
|
return rows.map((r) => ({
|
|
@@ -395,9 +703,14 @@ export class SqliteStore {
|
|
|
395
703
|
}
|
|
396
704
|
|
|
397
705
|
deleteAll(): number {
|
|
706
|
+
this.db.exec("PRAGMA foreign_keys = OFF");
|
|
707
|
+
this.db.prepare("DELETE FROM task_skills").run();
|
|
708
|
+
this.db.prepare("DELETE FROM skill_versions").run();
|
|
709
|
+
this.db.prepare("DELETE FROM skills").run();
|
|
398
710
|
this.db.prepare("DELETE FROM chunks").run();
|
|
399
711
|
this.db.prepare("DELETE FROM tasks").run();
|
|
400
712
|
this.db.prepare("DELETE FROM viewer_events").run();
|
|
713
|
+
this.db.exec("PRAGMA foreign_keys = ON");
|
|
401
714
|
const remaining = this.countChunks();
|
|
402
715
|
return remaining === 0 ? 1 : 0;
|
|
403
716
|
}
|
|
@@ -511,6 +824,119 @@ export class SqliteStore {
|
|
|
511
824
|
return row.cnt;
|
|
512
825
|
}
|
|
513
826
|
|
|
827
|
+
// ─── Skill CRUD ───
|
|
828
|
+
|
|
829
|
+
insertSkill(skill: Skill): void {
|
|
830
|
+
this.db.prepare(`
|
|
831
|
+
INSERT OR REPLACE INTO skills (id, name, description, version, status, tags, source_type, dir_path, installed, quality_score, created_at, updated_at)
|
|
832
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
833
|
+
`).run(skill.id, skill.name, skill.description, skill.version, skill.status, skill.tags, skill.sourceType, skill.dirPath, skill.installed, skill.qualityScore, skill.createdAt, skill.updatedAt);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
getSkill(skillId: string): Skill | null {
|
|
837
|
+
const row = this.db.prepare("SELECT * FROM skills WHERE id = ?").get(skillId) as SkillRow | undefined;
|
|
838
|
+
return row ? rowToSkill(row) : null;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
getSkillByName(name: string): Skill | null {
|
|
842
|
+
const row = this.db.prepare("SELECT * FROM skills WHERE name = ?").get(name) as SkillRow | undefined;
|
|
843
|
+
return row ? rowToSkill(row) : null;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
updateSkill(skillId: string, fields: { description?: string; version?: number; status?: SkillStatus; installed?: number; qualityScore?: number | null; updatedAt?: number }): void {
|
|
847
|
+
const sets: string[] = [];
|
|
848
|
+
const params: unknown[] = [];
|
|
849
|
+
if (fields.description !== undefined) { sets.push("description = ?"); params.push(fields.description); }
|
|
850
|
+
if (fields.version !== undefined) { sets.push("version = ?"); params.push(fields.version); }
|
|
851
|
+
if (fields.status !== undefined) { sets.push("status = ?"); params.push(fields.status); }
|
|
852
|
+
if (fields.installed !== undefined) { sets.push("installed = ?"); params.push(fields.installed); }
|
|
853
|
+
if (fields.qualityScore !== undefined) { sets.push("quality_score = ?"); params.push(fields.qualityScore); }
|
|
854
|
+
if (sets.length === 0) return;
|
|
855
|
+
sets.push("updated_at = ?");
|
|
856
|
+
params.push(fields.updatedAt ?? Date.now());
|
|
857
|
+
params.push(skillId);
|
|
858
|
+
this.db.prepare(`UPDATE skills SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
listSkills(opts: { status?: string } = {}): Skill[] {
|
|
862
|
+
const cond = opts.status ? "WHERE status = ?" : "";
|
|
863
|
+
const params = opts.status ? [opts.status] : [];
|
|
864
|
+
const rows = this.db.prepare(`SELECT * FROM skills ${cond} ORDER BY updated_at DESC`).all(...params) as SkillRow[];
|
|
865
|
+
return rows.map(rowToSkill);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// ─── Skill Versions ───
|
|
869
|
+
|
|
870
|
+
insertSkillVersion(sv: SkillVersion): void {
|
|
871
|
+
this.db.prepare(`
|
|
872
|
+
INSERT OR REPLACE INTO skill_versions (id, skill_id, version, content, changelog, change_summary, upgrade_type, source_task_id, metrics, quality_score, created_at)
|
|
873
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
874
|
+
`).run(sv.id, sv.skillId, sv.version, sv.content, sv.changelog, sv.changeSummary, sv.upgradeType, sv.sourceTaskId, sv.metrics, sv.qualityScore, sv.createdAt);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
getLatestSkillVersion(skillId: string): SkillVersion | null {
|
|
878
|
+
const row = this.db.prepare("SELECT * FROM skill_versions WHERE skill_id = ? ORDER BY version DESC LIMIT 1").get(skillId) as SkillVersionRow | undefined;
|
|
879
|
+
return row ? rowToSkillVersion(row) : null;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
getSkillVersions(skillId: string): SkillVersion[] {
|
|
883
|
+
const rows = this.db.prepare("SELECT * FROM skill_versions WHERE skill_id = ? ORDER BY version DESC").all(skillId) as SkillVersionRow[];
|
|
884
|
+
return rows.map(rowToSkillVersion);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
getSkillVersion(skillId: string, version: number): SkillVersion | null {
|
|
888
|
+
const row = this.db.prepare("SELECT * FROM skill_versions WHERE skill_id = ? AND version = ?").get(skillId, version) as SkillVersionRow | undefined;
|
|
889
|
+
return row ? rowToSkillVersion(row) : null;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// ─── Task-Skill Links ───
|
|
893
|
+
|
|
894
|
+
linkTaskSkill(taskId: string, skillId: string, relation: TaskSkillRelation, versionAt: number): void {
|
|
895
|
+
this.db.prepare(`
|
|
896
|
+
INSERT OR REPLACE INTO task_skills (task_id, skill_id, relation, version_at, created_at)
|
|
897
|
+
VALUES (?, ?, ?, ?, ?)
|
|
898
|
+
`).run(taskId, skillId, relation, versionAt, Date.now());
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
getSkillsByTask(taskId: string): Array<{ skill: Skill; relation: TaskSkillRelation; versionAt: number }> {
|
|
902
|
+
const rows = this.db.prepare(`
|
|
903
|
+
SELECT s.*, ts.relation, ts.version_at
|
|
904
|
+
FROM task_skills ts JOIN skills s ON s.id = ts.skill_id
|
|
905
|
+
WHERE ts.task_id = ?
|
|
906
|
+
`).all(taskId) as Array<SkillRow & { relation: string; version_at: number }>;
|
|
907
|
+
return rows.map(r => ({
|
|
908
|
+
skill: rowToSkill(r),
|
|
909
|
+
relation: r.relation as TaskSkillRelation,
|
|
910
|
+
versionAt: r.version_at,
|
|
911
|
+
}));
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
getTasksBySkill(skillId: string): Array<{ task: Task; relation: TaskSkillRelation }> {
|
|
915
|
+
const rows = this.db.prepare(`
|
|
916
|
+
SELECT t.*, ts.relation
|
|
917
|
+
FROM task_skills ts JOIN tasks t ON t.id = ts.task_id
|
|
918
|
+
WHERE ts.skill_id = ?
|
|
919
|
+
ORDER BY t.started_at DESC
|
|
920
|
+
`).all(skillId) as Array<TaskRow & { relation: string }>;
|
|
921
|
+
return rows.map(r => ({
|
|
922
|
+
task: rowToTask(r),
|
|
923
|
+
relation: r.relation as TaskSkillRelation,
|
|
924
|
+
}));
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
countSkills(status?: string): number {
|
|
928
|
+
const cond = status ? "WHERE status = ?" : "";
|
|
929
|
+
const params = status ? [status] : [];
|
|
930
|
+
const row = this.db.prepare(`SELECT COUNT(*) as c FROM skills ${cond}`).get(...params) as { c: number };
|
|
931
|
+
return row.c;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// ─── Chunk-Skill ───
|
|
935
|
+
|
|
936
|
+
setChunkSkillId(chunkId: string, skillId: string): void {
|
|
937
|
+
this.db.prepare("UPDATE chunks SET skill_id = ?, updated_at = ? WHERE id = ?").run(skillId, Date.now(), chunkId);
|
|
938
|
+
}
|
|
939
|
+
|
|
514
940
|
close(): void {
|
|
515
941
|
this.db.close();
|
|
516
942
|
}
|
|
@@ -548,6 +974,13 @@ interface ChunkRow {
|
|
|
548
974
|
kind: string;
|
|
549
975
|
summary: string;
|
|
550
976
|
task_id: string | null;
|
|
977
|
+
skill_id: string | null;
|
|
978
|
+
dedup_status: string;
|
|
979
|
+
dedup_target: string | null;
|
|
980
|
+
dedup_reason: string | null;
|
|
981
|
+
merge_count: number;
|
|
982
|
+
last_hit_at: number | null;
|
|
983
|
+
merge_history: string;
|
|
551
984
|
created_at: number;
|
|
552
985
|
updated_at: number;
|
|
553
986
|
}
|
|
@@ -564,6 +997,13 @@ function rowToChunk(row: ChunkRow): Chunk {
|
|
|
564
997
|
summary: row.summary,
|
|
565
998
|
embedding: null,
|
|
566
999
|
taskId: row.task_id,
|
|
1000
|
+
skillId: row.skill_id ?? null,
|
|
1001
|
+
dedupStatus: (row.dedup_status ?? "active") as DedupStatus,
|
|
1002
|
+
dedupTarget: row.dedup_target ?? null,
|
|
1003
|
+
dedupReason: row.dedup_reason ?? null,
|
|
1004
|
+
mergeCount: row.merge_count ?? 0,
|
|
1005
|
+
lastHitAt: row.last_hit_at ?? null,
|
|
1006
|
+
mergeHistory: row.merge_history ?? "[]",
|
|
567
1007
|
createdAt: row.created_at,
|
|
568
1008
|
updatedAt: row.updated_at,
|
|
569
1009
|
};
|
|
@@ -593,6 +1033,68 @@ function rowToTask(row: TaskRow): Task {
|
|
|
593
1033
|
};
|
|
594
1034
|
}
|
|
595
1035
|
|
|
1036
|
+
interface SkillRow {
|
|
1037
|
+
id: string;
|
|
1038
|
+
name: string;
|
|
1039
|
+
description: string;
|
|
1040
|
+
version: number;
|
|
1041
|
+
status: string;
|
|
1042
|
+
tags: string;
|
|
1043
|
+
source_type: string;
|
|
1044
|
+
dir_path: string;
|
|
1045
|
+
installed: number;
|
|
1046
|
+
quality_score: number | null;
|
|
1047
|
+
created_at: number;
|
|
1048
|
+
updated_at: number;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function rowToSkill(row: SkillRow): Skill {
|
|
1052
|
+
return {
|
|
1053
|
+
id: row.id,
|
|
1054
|
+
name: row.name,
|
|
1055
|
+
description: row.description,
|
|
1056
|
+
version: row.version,
|
|
1057
|
+
status: row.status as Skill["status"],
|
|
1058
|
+
tags: row.tags,
|
|
1059
|
+
sourceType: row.source_type as Skill["sourceType"],
|
|
1060
|
+
dirPath: row.dir_path,
|
|
1061
|
+
installed: row.installed,
|
|
1062
|
+
qualityScore: row.quality_score ?? null,
|
|
1063
|
+
createdAt: row.created_at,
|
|
1064
|
+
updatedAt: row.updated_at,
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
interface SkillVersionRow {
|
|
1069
|
+
id: string;
|
|
1070
|
+
skill_id: string;
|
|
1071
|
+
version: number;
|
|
1072
|
+
content: string;
|
|
1073
|
+
changelog: string;
|
|
1074
|
+
change_summary: string;
|
|
1075
|
+
upgrade_type: string;
|
|
1076
|
+
source_task_id: string | null;
|
|
1077
|
+
metrics: string;
|
|
1078
|
+
quality_score: number | null;
|
|
1079
|
+
created_at: number;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function rowToSkillVersion(row: SkillVersionRow): SkillVersion {
|
|
1083
|
+
return {
|
|
1084
|
+
id: row.id,
|
|
1085
|
+
skillId: row.skill_id,
|
|
1086
|
+
version: row.version,
|
|
1087
|
+
content: row.content,
|
|
1088
|
+
changelog: row.changelog,
|
|
1089
|
+
changeSummary: row.change_summary ?? "",
|
|
1090
|
+
upgradeType: row.upgrade_type as SkillVersion["upgradeType"],
|
|
1091
|
+
sourceTaskId: row.source_task_id,
|
|
1092
|
+
metrics: row.metrics,
|
|
1093
|
+
qualityScore: row.quality_score ?? null,
|
|
1094
|
+
createdAt: row.created_at,
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
596
1098
|
function contentHash(content: string): string {
|
|
597
1099
|
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
598
1100
|
}
|