opencodekit 0.17.13 → 0.18.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.
Files changed (44) hide show
  1. package/dist/index.js +4 -6
  2. package/dist/template/.opencode/AGENTS.md +57 -0
  3. package/dist/template/.opencode/agent/scout.md +0 -37
  4. package/dist/template/.opencode/command/resume.md +1 -1
  5. package/dist/template/.opencode/command/status.md +7 -14
  6. package/dist/template/.opencode/dcp.jsonc +81 -81
  7. package/dist/template/.opencode/memory/memory.db +0 -0
  8. package/dist/template/.opencode/memory.db +0 -0
  9. package/dist/template/.opencode/memory.db-shm +0 -0
  10. package/dist/template/.opencode/memory.db-wal +0 -0
  11. package/dist/template/.opencode/opencode.json +199 -23
  12. package/dist/template/.opencode/opencode.json.tui-migration.bak +1380 -0
  13. package/dist/template/.opencode/package.json +1 -1
  14. package/dist/template/.opencode/plugin/README.md +37 -25
  15. package/dist/template/.opencode/plugin/lib/capture.ts +177 -0
  16. package/dist/template/.opencode/plugin/lib/context.ts +194 -0
  17. package/dist/template/.opencode/plugin/lib/curator.ts +234 -0
  18. package/dist/template/.opencode/plugin/lib/db/maintenance.ts +312 -0
  19. package/dist/template/.opencode/plugin/lib/db/observations.ts +299 -0
  20. package/dist/template/.opencode/plugin/lib/db/pipeline.ts +520 -0
  21. package/dist/template/.opencode/plugin/lib/db/schema.ts +356 -0
  22. package/dist/template/.opencode/plugin/lib/db/types.ts +211 -0
  23. package/dist/template/.opencode/plugin/lib/distill.ts +376 -0
  24. package/dist/template/.opencode/plugin/lib/inject.ts +126 -0
  25. package/dist/template/.opencode/plugin/lib/memory-admin-tools.ts +188 -0
  26. package/dist/template/.opencode/plugin/lib/memory-db.ts +54 -936
  27. package/dist/template/.opencode/plugin/lib/memory-helpers.ts +202 -0
  28. package/dist/template/.opencode/plugin/lib/memory-hooks.ts +240 -0
  29. package/dist/template/.opencode/plugin/lib/memory-tools.ts +341 -0
  30. package/dist/template/.opencode/plugin/memory.ts +56 -60
  31. package/dist/template/.opencode/plugin/sessions.ts +372 -93
  32. package/dist/template/.opencode/skill/memory-system/SKILL.md +103 -60
  33. package/dist/template/.opencode/skill/session-management/SKILL.md +22 -35
  34. package/dist/template/.opencode/tui.json +15 -0
  35. package/package.json +1 -1
  36. package/dist/template/.opencode/plugin/compaction.ts +0 -190
  37. package/dist/template/.opencode/tool/action-queue.ts +0 -313
  38. package/dist/template/.opencode/tool/memory-admin.ts +0 -445
  39. package/dist/template/.opencode/tool/memory-get.ts +0 -143
  40. package/dist/template/.opencode/tool/memory-read.ts +0 -45
  41. package/dist/template/.opencode/tool/memory-search.ts +0 -264
  42. package/dist/template/.opencode/tool/memory-timeline.ts +0 -105
  43. package/dist/template/.opencode/tool/memory-update.ts +0 -63
  44. package/dist/template/.opencode/tool/observation.ts +0 -357
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Database Maintenance & Auxiliary Operations
3
+ *
4
+ * Memory files, FTS5 maintenance, archiving, and full maintenance cycle.
5
+ */
6
+
7
+ import { purgeOldTemporalMessages } from "./pipeline.js";
8
+ import { getMemoryDB } from "./schema.js";
9
+ import type {
10
+ ArchiveOptions,
11
+ MaintenanceStats,
12
+ MemoryFileRow,
13
+ } from "./types.js";
14
+
15
+ // ============================================================================
16
+ // Memory File Operations
17
+ // ============================================================================
18
+
19
+ /**
20
+ * Store or update a memory file.
21
+ */
22
+ export function upsertMemoryFile(
23
+ filePath: string,
24
+ content: string,
25
+ mode: "replace" | "append" = "replace",
26
+ ): void {
27
+ const db = getMemoryDB();
28
+ const now = new Date();
29
+
30
+ db.run(
31
+ `
32
+ INSERT INTO memory_files (file_path, content, mode, created_at, created_at_epoch)
33
+ VALUES (?, ?, ?, ?, ?)
34
+ ON CONFLICT(file_path) DO UPDATE SET
35
+ content = CASE WHEN excluded.mode = 'append' THEN memory_files.content || '\n\n' || excluded.content ELSE excluded.content END,
36
+ mode = excluded.mode,
37
+ updated_at = ?,
38
+ updated_at_epoch = ?
39
+ `,
40
+ [
41
+ filePath,
42
+ content,
43
+ mode,
44
+ now.toISOString(),
45
+ now.getTime(),
46
+ now.toISOString(),
47
+ now.getTime(),
48
+ ],
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Get a memory file by path.
54
+ */
55
+ export function getMemoryFile(filePath: string): MemoryFileRow | null {
56
+ const db = getMemoryDB();
57
+ return db
58
+ .query("SELECT * FROM memory_files WHERE file_path = ?")
59
+ .get(filePath) as MemoryFileRow | null;
60
+ }
61
+
62
+ // ============================================================================
63
+ // FTS5 Maintenance
64
+ // ============================================================================
65
+
66
+ /**
67
+ * Optimize FTS5 indexes (run periodically).
68
+ */
69
+ export function optimizeFTS5(): void {
70
+ const db = getMemoryDB();
71
+ db.run("INSERT INTO observations_fts(observations_fts) VALUES('optimize')");
72
+ try {
73
+ db.run(
74
+ "INSERT INTO distillations_fts(distillations_fts) VALUES('optimize')",
75
+ );
76
+ } catch {
77
+ // distillations_fts may not exist yet
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Rebuild FTS5 indexes from scratch.
83
+ */
84
+ export function rebuildFTS5(): void {
85
+ const db = getMemoryDB();
86
+ db.run("INSERT INTO observations_fts(observations_fts) VALUES('rebuild')");
87
+ try {
88
+ db.run(
89
+ "INSERT INTO distillations_fts(distillations_fts) VALUES('rebuild')",
90
+ );
91
+ } catch {
92
+ // distillations_fts may not exist yet
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Check if FTS5 is available and working.
98
+ */
99
+ export function checkFTS5Available(): boolean {
100
+ try {
101
+ const db = getMemoryDB();
102
+ db.query("SELECT * FROM observations_fts LIMIT 1").get();
103
+ return true;
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ // ============================================================================
110
+ // Database Maintenance
111
+ // ============================================================================
112
+
113
+ /**
114
+ * Archive old observations to a separate table.
115
+ */
116
+ export function archiveOldObservations(options: ArchiveOptions = {}): number {
117
+ const db = getMemoryDB();
118
+ const olderThanDays = options.olderThanDays ?? 90;
119
+ const includeSuperseded = options.includeSuperseded ?? true;
120
+ const dryRun = options.dryRun ?? false;
121
+
122
+ const cutoffEpoch = Date.now() - olderThanDays * 24 * 60 * 60 * 1000;
123
+
124
+ // Create archive table if not exists
125
+ db.run(`
126
+ CREATE TABLE IF NOT EXISTS observations_archive (
127
+ id INTEGER PRIMARY KEY,
128
+ type TEXT NOT NULL,
129
+ title TEXT NOT NULL,
130
+ subtitle TEXT,
131
+ facts TEXT,
132
+ narrative TEXT,
133
+ concepts TEXT,
134
+ files_read TEXT,
135
+ files_modified TEXT,
136
+ confidence TEXT,
137
+ bead_id TEXT,
138
+ supersedes INTEGER,
139
+ superseded_by INTEGER,
140
+ valid_until TEXT,
141
+ markdown_file TEXT,
142
+ source TEXT DEFAULT 'manual',
143
+ created_at TEXT NOT NULL,
144
+ created_at_epoch INTEGER NOT NULL,
145
+ updated_at TEXT,
146
+ archived_at TEXT NOT NULL
147
+ )
148
+ `);
149
+
150
+ // Build WHERE clause
151
+ let whereClause = `created_at_epoch < ${cutoffEpoch}`;
152
+ if (includeSuperseded) {
153
+ whereClause = `(${whereClause} OR superseded_by IS NOT NULL)`;
154
+ }
155
+
156
+ // Count candidates
157
+ const countResult = db
158
+ .query(`SELECT COUNT(*) as count FROM observations WHERE ${whereClause}`)
159
+ .get() as { count: number };
160
+
161
+ if (dryRun || countResult.count === 0) {
162
+ return countResult.count;
163
+ }
164
+
165
+ // Move to archive
166
+ const now = new Date().toISOString();
167
+ db.run(`
168
+ INSERT INTO observations_archive
169
+ SELECT *, '${now}' as archived_at FROM observations WHERE ${whereClause}
170
+ `);
171
+
172
+ // Delete from main table (triggers will remove from FTS)
173
+ db.run(`DELETE FROM observations WHERE ${whereClause}`);
174
+
175
+ return countResult.count;
176
+ }
177
+
178
+ /**
179
+ * Checkpoint WAL file back to main database.
180
+ */
181
+ export function checkpointWAL(): { walSize: number; checkpointed: boolean } {
182
+ const db = getMemoryDB();
183
+
184
+ try {
185
+ const result = db.query("PRAGMA wal_checkpoint(TRUNCATE)").get() as {
186
+ busy: number;
187
+ log: number;
188
+ checkpointed: number;
189
+ };
190
+
191
+ return {
192
+ walSize: result.log,
193
+ checkpointed: result.busy === 0,
194
+ };
195
+ } catch {
196
+ return { walSize: 0, checkpointed: false };
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Vacuum database to reclaim space and defragment.
202
+ */
203
+ export function vacuumDatabase(): boolean {
204
+ const db = getMemoryDB();
205
+ try {
206
+ db.run("VACUUM");
207
+ return true;
208
+ } catch {
209
+ return false;
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Get database file sizes.
215
+ */
216
+ export function getDatabaseSizes(): {
217
+ mainDb: number;
218
+ wal: number;
219
+ shm: number;
220
+ total: number;
221
+ } {
222
+ const db = getMemoryDB();
223
+
224
+ try {
225
+ const pageCount = db.query("PRAGMA page_count").get() as {
226
+ page_count: number;
227
+ };
228
+ const pageSize = db.query("PRAGMA page_size").get() as {
229
+ page_size: number;
230
+ };
231
+ const mainDb = pageCount.page_count * pageSize.page_size;
232
+
233
+ const walResult = db.query("PRAGMA wal_checkpoint").get() as {
234
+ busy: number;
235
+ log: number;
236
+ checkpointed: number;
237
+ };
238
+ const wal = walResult.log * pageSize.page_size;
239
+
240
+ return {
241
+ mainDb,
242
+ wal,
243
+ shm: 32768,
244
+ total: mainDb + wal + 32768,
245
+ };
246
+ } catch {
247
+ return { mainDb: 0, wal: 0, shm: 0, total: 0 };
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Get list of markdown files that exist in SQLite (for pruning).
253
+ */
254
+ export function getMarkdownFilesInSqlite(): string[] {
255
+ const db = getMemoryDB();
256
+ const rows = db
257
+ .query(
258
+ "SELECT markdown_file FROM observations WHERE markdown_file IS NOT NULL",
259
+ )
260
+ .all() as { markdown_file: string }[];
261
+
262
+ return rows.map((r) => r.markdown_file);
263
+ }
264
+
265
+ /**
266
+ * Run full maintenance cycle.
267
+ */
268
+ export function runFullMaintenance(
269
+ options: ArchiveOptions = {},
270
+ ): MaintenanceStats {
271
+ const sizesBefore = getDatabaseSizes();
272
+
273
+ // 1. Archive old observations
274
+ const archived = archiveOldObservations(options);
275
+
276
+ // 2. Purge old temporal messages
277
+ let purgedMessages = 0;
278
+ if (!options.dryRun) {
279
+ purgedMessages = purgeOldTemporalMessages();
280
+ }
281
+
282
+ // 3. Optimize FTS5
283
+ if (!options.dryRun) {
284
+ optimizeFTS5();
285
+ }
286
+
287
+ // 4. Checkpoint WAL
288
+ let checkpointed = false;
289
+ if (!options.dryRun) {
290
+ const walResult = checkpointWAL();
291
+ checkpointed = walResult.checkpointed;
292
+ }
293
+
294
+ // 5. Vacuum
295
+ let vacuumed = false;
296
+ if (!options.dryRun) {
297
+ vacuumed = vacuumDatabase();
298
+ }
299
+
300
+ const sizesAfter = getDatabaseSizes();
301
+
302
+ return {
303
+ archived,
304
+ vacuumed,
305
+ checkpointed,
306
+ prunedMarkdown: 0,
307
+ purgedMessages,
308
+ freedBytes: sizesBefore.total - sizesAfter.total,
309
+ dbSizeBefore: sizesBefore.total,
310
+ dbSizeAfter: sizesAfter.total,
311
+ };
312
+ }
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Observation Operations
3
+ *
4
+ * CRUD, search, timeline, and stats for the observations table.
5
+ * Uses FTS5 with porter stemming for full-text search with BM25 ranking.
6
+ */
7
+
8
+ import { getMemoryDB } from "./schema.js";
9
+ import type {
10
+ ObservationInput,
11
+ ObservationRow,
12
+ ObservationType,
13
+ SearchIndexResult,
14
+ } from "./types.js";
15
+
16
+ // ============================================================================
17
+ // CRUD
18
+ // ============================================================================
19
+
20
+ /**
21
+ * Store a new observation in the database.
22
+ */
23
+ export function storeObservation(input: ObservationInput): number {
24
+ const db = getMemoryDB();
25
+ const now = new Date();
26
+
27
+ const result = db
28
+ .query(
29
+ `
30
+ INSERT INTO observations (
31
+ type, title, subtitle, facts, narrative, concepts,
32
+ files_read, files_modified, confidence, bead_id,
33
+ supersedes, markdown_file, source, created_at, created_at_epoch
34
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
35
+ `,
36
+ )
37
+ .run(
38
+ input.type,
39
+ input.title,
40
+ input.subtitle ?? null,
41
+ input.facts ? JSON.stringify(input.facts) : null,
42
+ input.narrative ?? null,
43
+ input.concepts ? JSON.stringify(input.concepts) : null,
44
+ input.files_read ? JSON.stringify(input.files_read) : null,
45
+ input.files_modified ? JSON.stringify(input.files_modified) : null,
46
+ input.confidence ?? "high",
47
+ input.bead_id ?? null,
48
+ input.supersedes ?? null,
49
+ input.markdown_file ?? null,
50
+ input.source ?? "manual",
51
+ now.toISOString(),
52
+ now.getTime(),
53
+ );
54
+
55
+ const insertedId = Number(result.lastInsertRowid);
56
+
57
+ // Update supersedes relationship
58
+ if (input.supersedes) {
59
+ db.run("UPDATE observations SET superseded_by = ? WHERE id = ?", [
60
+ insertedId,
61
+ input.supersedes,
62
+ ]);
63
+ }
64
+
65
+ return insertedId;
66
+ }
67
+
68
+ /**
69
+ * Get observation by ID.
70
+ */
71
+ export function getObservationById(id: number): ObservationRow | null {
72
+ const db = getMemoryDB();
73
+ return db
74
+ .query("SELECT * FROM observations WHERE id = ?")
75
+ .get(id) as ObservationRow | null;
76
+ }
77
+
78
+ /**
79
+ * Get multiple observations by IDs.
80
+ */
81
+ export function getObservationsByIds(ids: number[]): ObservationRow[] {
82
+ if (ids.length === 0) return [];
83
+
84
+ const db = getMemoryDB();
85
+ const placeholders = ids.map(() => "?").join(",");
86
+ return db
87
+ .query(`SELECT * FROM observations WHERE id IN (${placeholders})`)
88
+ .all(...ids) as ObservationRow[];
89
+ }
90
+
91
+ // ============================================================================
92
+ // Search
93
+ // ============================================================================
94
+
95
+ /**
96
+ * Search observations using FTS5.
97
+ * Returns compact index results for progressive disclosure.
98
+ */
99
+ export function searchObservationsFTS(
100
+ query: string,
101
+ options: {
102
+ type?: ObservationType;
103
+ concepts?: string[];
104
+ limit?: number;
105
+ } = {},
106
+ ): SearchIndexResult[] {
107
+ const db = getMemoryDB();
108
+ const limit = options.limit ?? 10;
109
+
110
+ // Build FTS5 query — porter stemming handles word forms automatically
111
+ const ftsQuery = query
112
+ .replace(/['"]/g, '""')
113
+ .split(/\s+/)
114
+ .filter((term) => term.length > 0)
115
+ .map((term) => `"${term}"*`)
116
+ .join(" OR ");
117
+
118
+ if (!ftsQuery) {
119
+ // Empty query — return recent observations
120
+ return db
121
+ .query(
122
+ `
123
+ SELECT id, type, title,
124
+ substr(COALESCE(narrative, ''), 1, 100) as snippet,
125
+ created_at,
126
+ 0 as relevance_score
127
+ FROM observations
128
+ WHERE superseded_by IS NULL
129
+ ${options.type ? "AND type = ?" : ""}
130
+ ORDER BY created_at_epoch DESC
131
+ LIMIT ?
132
+ `,
133
+ )
134
+ .all(
135
+ ...(options.type ? [options.type, limit] : [limit]),
136
+ ) as SearchIndexResult[];
137
+ }
138
+
139
+ try {
140
+ // Use FTS5 with BM25 ranking
141
+ let sql = `
142
+ SELECT o.id, o.type, o.title,
143
+ substr(COALESCE(o.narrative, ''), 1, 100) as snippet,
144
+ o.created_at,
145
+ bm25(observations_fts) as relevance_score
146
+ FROM observations o
147
+ JOIN observations_fts fts ON fts.rowid = o.id
148
+ WHERE observations_fts MATCH ?
149
+ AND o.superseded_by IS NULL
150
+ `;
151
+
152
+ const params: (string | number)[] = [ftsQuery];
153
+
154
+ if (options.type) {
155
+ sql += " AND o.type = ?";
156
+ params.push(options.type);
157
+ }
158
+
159
+ sql += " ORDER BY relevance_score LIMIT ?";
160
+ params.push(limit);
161
+
162
+ return db.query(sql).all(...params) as SearchIndexResult[];
163
+ } catch {
164
+ // FTS5 query failed, fallback to LIKE search
165
+ return fallbackLikeSearch(db, query, options.type, limit);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Fallback search using LIKE (for when FTS5 fails).
171
+ */
172
+ function fallbackLikeSearch(
173
+ db: ReturnType<typeof getMemoryDB>,
174
+ query: string,
175
+ type: ObservationType | undefined,
176
+ limit: number,
177
+ ): SearchIndexResult[] {
178
+ const likePattern = `%${query}%`;
179
+
180
+ let sql = `
181
+ SELECT id, type, title,
182
+ substr(COALESCE(narrative, ''), 1, 100) as snippet,
183
+ created_at,
184
+ 0 as relevance_score
185
+ FROM observations
186
+ WHERE superseded_by IS NULL
187
+ AND (title LIKE ? OR narrative LIKE ? OR concepts LIKE ?)
188
+ `;
189
+
190
+ const params: (string | number)[] = [likePattern, likePattern, likePattern];
191
+
192
+ if (type) {
193
+ sql += " AND type = ?";
194
+ params.push(type);
195
+ }
196
+
197
+ sql += " ORDER BY created_at_epoch DESC LIMIT ?";
198
+ params.push(limit);
199
+
200
+ return db.query(sql).all(...params) as SearchIndexResult[];
201
+ }
202
+
203
+ // ============================================================================
204
+ // Timeline & Stats
205
+ // ============================================================================
206
+
207
+ /**
208
+ * Get timeline around an anchor observation.
209
+ */
210
+ export function getTimelineAroundObservation(
211
+ anchorId: number,
212
+ depthBefore = 5,
213
+ depthAfter = 5,
214
+ ): {
215
+ anchor: ObservationRow | null;
216
+ before: SearchIndexResult[];
217
+ after: SearchIndexResult[];
218
+ } {
219
+ const db = getMemoryDB();
220
+
221
+ const anchor = getObservationById(anchorId);
222
+ if (!anchor) {
223
+ return { anchor: null, before: [], after: [] };
224
+ }
225
+
226
+ const before = db
227
+ .query(
228
+ `
229
+ SELECT id, type, title,
230
+ substr(COALESCE(narrative, ''), 1, 100) as snippet,
231
+ created_at,
232
+ 0 as relevance_score
233
+ FROM observations
234
+ WHERE created_at_epoch < ?
235
+ AND superseded_by IS NULL
236
+ ORDER BY created_at_epoch DESC
237
+ LIMIT ?
238
+ `,
239
+ )
240
+ .all(anchor.created_at_epoch, depthBefore) as SearchIndexResult[];
241
+
242
+ const after = db
243
+ .query(
244
+ `
245
+ SELECT id, type, title,
246
+ substr(COALESCE(narrative, ''), 1, 100) as snippet,
247
+ created_at,
248
+ 0 as relevance_score
249
+ FROM observations
250
+ WHERE created_at_epoch > ?
251
+ AND superseded_by IS NULL
252
+ ORDER BY created_at_epoch ASC
253
+ LIMIT ?
254
+ `,
255
+ )
256
+ .all(anchor.created_at_epoch, depthAfter) as SearchIndexResult[];
257
+
258
+ return {
259
+ anchor,
260
+ before: before.reverse(),
261
+ after,
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Get most recent observation.
267
+ */
268
+ export function getMostRecentObservation(): ObservationRow | null {
269
+ const db = getMemoryDB();
270
+ return db
271
+ .query(
272
+ "SELECT * FROM observations WHERE superseded_by IS NULL ORDER BY created_at_epoch DESC LIMIT 1",
273
+ )
274
+ .get() as ObservationRow | null;
275
+ }
276
+
277
+ /**
278
+ * Get observation count by type.
279
+ */
280
+ export function getObservationStats(): Record<string, number> {
281
+ const db = getMemoryDB();
282
+ const rows = db
283
+ .query(
284
+ `
285
+ SELECT type, COUNT(*) as count
286
+ FROM observations
287
+ WHERE superseded_by IS NULL
288
+ GROUP BY type
289
+ `,
290
+ )
291
+ .all() as { type: string; count: number }[];
292
+
293
+ const stats: Record<string, number> = { total: 0 };
294
+ for (const row of rows) {
295
+ stats[row.type] = row.count;
296
+ stats.total += row.count;
297
+ }
298
+ return stats;
299
+ }