wyrm-mcp 7.2.0 → 7.2.2

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 (156) hide show
  1. package/LICENSE +26 -667
  2. package/NOTICE +14 -33
  3. package/dist/activation.d.ts.map +1 -1
  4. package/dist/activation.js +1 -44
  5. package/dist/activation.js.map +1 -1
  6. package/dist/agent-daemon.js +4 -281
  7. package/dist/agent-loop.js +7 -332
  8. package/dist/analytics.js +13 -236
  9. package/dist/attribution.js +1 -49
  10. package/dist/audit.js +2 -457
  11. package/dist/auto-capture.js +3 -138
  12. package/dist/auto-orchestrator.js +1 -325
  13. package/dist/autoconfig.js +39 -840
  14. package/dist/buddy-runner.js +1 -109
  15. package/dist/buddy.js +14 -564
  16. package/dist/build-flags.js +1 -17
  17. package/dist/capabilities.js +3 -183
  18. package/dist/capture.js +1 -56
  19. package/dist/causality.js +6 -107
  20. package/dist/cli.js +20 -281
  21. package/dist/cloud/cli.js +5 -541
  22. package/dist/cloud/client.js +1 -221
  23. package/dist/cloud/crypto.js +1 -85
  24. package/dist/cloud/machine-id.js +2 -113
  25. package/dist/cloud/recovery.js +1 -60
  26. package/dist/cloud/sync-engine.js +7 -543
  27. package/dist/cloud-backup.js +5 -579
  28. package/dist/cloud-profile.js +1 -138
  29. package/dist/cloud-sync-entrypoint.js +1 -47
  30. package/dist/cloud-sync.js +2 -309
  31. package/dist/constellation.js +12 -168
  32. package/dist/context-build-budgeted.js +4 -144
  33. package/dist/context-ranking.js +1 -69
  34. package/dist/crypto.js +1 -179
  35. package/dist/daemon-write-endpoint.js +1 -290
  36. package/dist/daemon-writer.js +2 -406
  37. package/dist/database.js +43 -1110
  38. package/dist/deprecations.js +2 -162
  39. package/dist/design.js +13 -141
  40. package/dist/event-replication.js +1 -112
  41. package/dist/events-sse.js +7 -43
  42. package/dist/events.js +6 -238
  43. package/dist/failure-patterns.js +42 -659
  44. package/dist/federation.js +12 -236
  45. package/dist/goals.js +13 -101
  46. package/dist/golden.js +3 -355
  47. package/dist/handlers/agent.js +4 -165
  48. package/dist/handlers/alias-adapters.js +1 -129
  49. package/dist/handlers/aliases.js +1 -171
  50. package/dist/handlers/audit.js +1 -87
  51. package/dist/handlers/boundary.js +1 -221
  52. package/dist/handlers/capture.js +73 -1109
  53. package/dist/handlers/causality.js +7 -114
  54. package/dist/handlers/cloud.js +85 -382
  55. package/dist/handlers/companion.js +28 -459
  56. package/dist/handlers/datalake.js +7 -187
  57. package/dist/handlers/dispatch-context.js +0 -22
  58. package/dist/handlers/entity.js +25 -256
  59. package/dist/handlers/events.js +16 -335
  60. package/dist/handlers/failure.js +13 -340
  61. package/dist/handlers/goals.js +4 -296
  62. package/dist/handlers/intelligence.js +126 -674
  63. package/dist/handlers/invoicing.js +1 -70
  64. package/dist/handlers/mcpclient.js +6 -137
  65. package/dist/handlers/orchestration.js +40 -125
  66. package/dist/handlers/output-schemas.js +1 -24
  67. package/dist/handlers/presence.js +3 -99
  68. package/dist/handlers/project.js +28 -182
  69. package/dist/handlers/prompts.js +6 -157
  70. package/dist/handlers/quest.js +4 -224
  71. package/dist/handlers/recall.js +11 -218
  72. package/dist/handlers/registry.js +1 -167
  73. package/dist/handlers/resources.js +1 -288
  74. package/dist/handlers/review.js +11 -74
  75. package/dist/handlers/run.js +17 -487
  76. package/dist/handlers/search.js +15 -326
  77. package/dist/handlers/session.js +28 -615
  78. package/dist/handlers/share.js +8 -184
  79. package/dist/handlers/shims.js +1 -464
  80. package/dist/handlers/skill.js +67 -449
  81. package/dist/handlers/survivors.js +1 -120
  82. package/dist/handlers/symbols.js +8 -109
  83. package/dist/handlers/syncops.js +4 -302
  84. package/dist/handlers/types.js +1 -27
  85. package/dist/harvest.js +5 -191
  86. package/dist/hours.js +7 -156
  87. package/dist/http-auth.js +3 -321
  88. package/dist/http-fast.js +21 -1137
  89. package/dist/icons.js +1 -47
  90. package/dist/index.js +2 -924
  91. package/dist/indexer.js +4 -145
  92. package/dist/intelligence.js +31 -261
  93. package/dist/internal-dispatch.js +3 -212
  94. package/dist/keyset.js +1 -110
  95. package/dist/knowledge-graph.js +12 -176
  96. package/dist/license.d.ts +11 -0
  97. package/dist/license.d.ts.map +1 -1
  98. package/dist/license.js +2 -414
  99. package/dist/license.js.map +1 -1
  100. package/dist/logger.js +2 -199
  101. package/dist/maintenance.js +2 -148
  102. package/dist/mcp-client.js +6 -262
  103. package/dist/memory-artifacts.js +30 -449
  104. package/dist/migrate-prompt.js +2 -124
  105. package/dist/migrations.js +40 -655
  106. package/dist/performance.js +1 -228
  107. package/dist/presence.js +11 -140
  108. package/dist/priority-embed.js +5 -164
  109. package/dist/providers/embedding-provider.js +1 -196
  110. package/dist/readonly-gate.js +1 -29
  111. package/dist/rehydration.js +9 -157
  112. package/dist/reindex.js +1 -88
  113. package/dist/render-target.js +21 -514
  114. package/dist/render.js +4 -280
  115. package/dist/repl-guard.js +1 -173
  116. package/dist/replication-daemon-entrypoint.js +1 -31
  117. package/dist/replication-daemon.js +2 -262
  118. package/dist/resilience.js +1 -591
  119. package/dist/reverse-bridge.js +5 -360
  120. package/dist/security.js +1 -244
  121. package/dist/session-seen.js +3 -51
  122. package/dist/setup.js +1 -260
  123. package/dist/skill-author.js +5 -168
  124. package/dist/spec-kit.js +1 -191
  125. package/dist/sqlite-busy.js +1 -154
  126. package/dist/statusline.js +11 -315
  127. package/dist/sub-agent.js +13 -262
  128. package/dist/summarizer.js +13 -139
  129. package/dist/symbols.js +7 -283
  130. package/dist/sync.js +5 -359
  131. package/dist/tasks-dispatch.js +1 -84
  132. package/dist/tasks.js +1 -282
  133. package/dist/token-budget.js +1 -143
  134. package/dist/tool-analytics.js +7 -129
  135. package/dist/tool-annotations.js +1 -365
  136. package/dist/tool-manifest-v2.json +1 -1
  137. package/dist/tool-manifest.json +1 -1
  138. package/dist/tool-profiles.js +1 -75
  139. package/dist/trace-harvest.js +6 -244
  140. package/dist/types.js +1 -30
  141. package/dist/ui-dashboard.js +41 -50
  142. package/dist/ulid.js +1 -81
  143. package/dist/validate.js +1 -129
  144. package/dist/vault.js +1 -534
  145. package/dist/vectors.js +3 -184
  146. package/dist/version-check.js +4 -136
  147. package/dist/visibility.js +19 -155
  148. package/dist/wyrm-cli.js +98 -2451
  149. package/dist/wyrm-cli.js.map +1 -1
  150. package/dist/wyrm-guard.js +14 -424
  151. package/dist/wyrm-loop.js +3 -150
  152. package/dist/wyrm-manifest.json +1 -1
  153. package/dist/wyrm-statusline-daemon.js +1 -11
  154. package/dist/wyrm-statusline.js +4 -56
  155. package/dist/wyrm-ui.js +9 -77
  156. package/package.json +4 -2
package/dist/database.js CHANGED
@@ -1,253 +1,11 @@
1
- /**
2
- * Wyrm Database - SQLite storage for infinite memory with data lake support
3
- *
4
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
5
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
6
- *
7
- * Features:
8
- * - Auto-discovers projects in configured directories
9
- * - Handles large datasets with pagination and streaming
10
- * - Write-Ahead Logging (WAL) for concurrent performance
11
- * - Full-text search for fast context retrieval
12
- * - Batch operations for bulk imports
13
- * - Resilient operations with automatic recovery
14
- */
15
- import Database from 'better-sqlite3';
16
- import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'fs';
17
- import { homedir } from 'os';
18
- import { join, basename, resolve, normalize } from 'path';
19
- import { spawnSync } from 'child_process';
20
- import { getResilienceManager } from './resilience.js';
21
- import { WyrmLogger } from './logger.js';
22
- import { runMigrations, getSchemaVersion } from './migrations.js';
23
- import { validateProjectPath, buildFtsMatchQuery } from './security.js';
24
- import { emitEvent, eventsSince, subscribeEvents, isLiveMemoryEnabled, ingestRemoteEvent, eventsForPush, pruneEvents, getMeta, setMeta } from './events.js';
25
- import { getActor } from './handlers/boundary.js';
26
- import { readSkillContent, slugify as slugifySkillName, skillContentSha } from './skill-author.js';
27
- export class WyrmDB {
28
- db;
29
- BATCH_SIZE = 1000;
30
- resilience;
31
- logger;
32
- dbPath;
33
- constructor(dbPath) {
34
- const wyrmDir = join(homedir(), '.wyrm');
35
- if (!existsSync(wyrmDir)) {
36
- mkdirSync(wyrmDir, { recursive: true });
37
- }
38
- this.dbPath = dbPath || join(wyrmDir, 'wyrm.db');
39
- this.logger = new WyrmLogger();
40
- this.resilience = getResilienceManager();
41
- // Initialize database with resilience
42
- this.db = this.initializeDatabase(this.dbPath);
43
- // Enable WAL mode for better concurrent performance and crash recovery
44
- this.db.pragma('journal_mode = WAL');
45
- this.db.pragma('synchronous = NORMAL');
46
- this.db.pragma('cache_size = -64000'); // 64MB cache
47
- this.db.pragma('temp_store = MEMORY');
48
- // busy_timeout = 5000 — the documented multi-process choice (v7 F2, T011).
49
- // Under WAL there is exactly ONE writer at a time across ALL processes
50
- // sharing this file; a contended writer spins inside SQLite for up to 5s
51
- // before SQLITE_BUSY surfaces. Typical Wyrm row writes are sub-millisecond,
52
- // so 5s absorbs entire bursts of concurrent fleet writers; anything still
53
- // BUSY after 5s means a genuinely long-held lock (bulk import / vacuum),
54
- // which the MCP dispatcher surfaces as the structured WYRM_BUSY retry body
55
- // (sqlite-busy.ts) instead of an opaque error string.
56
- this.db.pragma('busy_timeout = 5000');
57
- this.db.pragma('mmap_size = 268435456'); // 256MB memory-mapped I/O
58
- this.db.pragma('page_size = 4096'); // Optimal page size
59
- this.db.pragma('foreign_keys = ON'); // Enforce referential integrity
60
- // Run versioned migrations
61
- const applied = runMigrations(this.db);
62
- if (applied.length > 0) {
63
- this.logger.info(`Applied ${applied.length} migration(s), now at v${getSchemaVersion(this.db)}`);
64
- }
65
- // Recover any incomplete operations from previous session
66
- this.recoverIncompleteOperations();
67
- }
68
- /** Expose the raw database instance for analytics and other modules */
69
- getDatabase() {
70
- return this.db;
71
- }
72
- /** Get the database file path */
73
- getDatabasePath() {
74
- return this.dbPath;
75
- }
76
- /**
77
- * Initialize database with retry logic for handling corruption/locks
78
- */
79
- initializeDatabase(path) {
80
- const result = this.resilience.withRetrySync(() => new Database(path), 'database_init', { maxAttempts: 3, baseDelayMs: 500 });
81
- if (!result.success) {
82
- this.logger.error('Failed to initialize database', { path, error: result.error?.message });
83
- throw result.error || new Error('Database initialization failed');
84
- }
85
- return result.data;
86
- }
87
- /**
88
- * Recover incomplete operations from previous session
89
- */
90
- recoverIncompleteOperations() {
91
- const incomplete = this.resilience.getIncompleteOperations();
92
- for (const op of incomplete) {
93
- this.logger.warn('Found incomplete operation from previous session', {
94
- operation: op.operation,
95
- stage: op.stage,
96
- id: op.id,
97
- });
98
- // For now, just log - specific recovery logic can be added
99
- // based on operation type
100
- if (op.operation === 'batch_insert') {
101
- this.logger.info('Batch insert was incomplete - data may need re-import');
102
- }
103
- // Mark as handled
104
- this.resilience.completeCheckpoint(op.id);
105
- }
106
- }
107
- // ==================== WATCH DIRECTORIES ====================
108
- addWatchDir(path, recursive = true) {
109
- // Validate path is within allowed directories to prevent directory traversal
110
- validateProjectPath(path);
111
- return this.db.prepare(`
1
+ import k from"better-sqlite3";import{existsSync as u,mkdirSync as b,readdirSync as D,statSync as y,writeFileSync as L}from"fs";import{homedir as I}from"os";import{join as d,basename as A,resolve as M,normalize as v}from"path";import{spawnSync as f}from"child_process";import{getResilienceManager as H}from"./resilience.js";import{WyrmLogger as w}from"./logger.js";import{runMigrations as F,getSchemaVersion as j}from"./migrations.js";import{validateProjectPath as O,buildFtsMatchQuery as _}from"./security.js";import{emitEvent as h,eventsSince as W,subscribeEvents as U,isLiveMemoryEnabled as x,ingestRemoteEvent as P,eventsForPush as q,pruneEvents as B,getMeta as Y,setMeta as $}from"./events.js";import{getActor as N}from"./handlers/boundary.js";import{readSkillContent as C,slugify as G,skillContentSha as V}from"./skill-author.js";class ae{db;BATCH_SIZE=1e3;resilience;logger;dbPath;constructor(t){const e=d(I(),".wyrm");u(e)||b(e,{recursive:!0}),this.dbPath=t||d(e,"wyrm.db"),this.logger=new w,this.resilience=H(),this.db=this.initializeDatabase(this.dbPath),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("cache_size = -64000"),this.db.pragma("temp_store = MEMORY"),this.db.pragma("busy_timeout = 5000"),this.db.pragma("mmap_size = 268435456"),this.db.pragma("page_size = 4096"),this.db.pragma("foreign_keys = ON");const s=F(this.db);s.length>0&&this.logger.info(`Applied ${s.length} migration(s), now at v${j(this.db)}`),this.recoverIncompleteOperations()}getDatabase(){return this.db}getDatabasePath(){return this.dbPath}initializeDatabase(t){const e=this.resilience.withRetrySync(()=>new k(t),"database_init",{maxAttempts:3,baseDelayMs:500});if(!e.success)throw this.logger.error("Failed to initialize database",{path:t,error:e.error?.message}),e.error||new Error("Database initialization failed");return e.data}recoverIncompleteOperations(){const t=this.resilience.getIncompleteOperations();for(const e of t)this.logger.warn("Found incomplete operation from previous session",{operation:e.operation,stage:e.stage,id:e.id}),e.operation==="batch_insert"&&this.logger.info("Batch insert was incomplete - data may need re-import"),this.resilience.completeCheckpoint(e.id)}addWatchDir(t,e=!0){return O(t),this.db.prepare(`
112
2
  INSERT INTO watch_dirs (path, recursive)
113
3
  VALUES (?, ?)
114
4
  ON CONFLICT(path) DO UPDATE SET recursive = excluded.recursive
115
5
  RETURNING *
116
- `).get(path, recursive ? 1 : 0);
117
- }
118
- getWatchDirs() {
119
- return this.db.prepare('SELECT * FROM watch_dirs').all();
120
- }
121
- removeWatchDir(path) {
122
- this.db.prepare('DELETE FROM watch_dirs WHERE path = ?').run(path);
123
- }
124
- // ==================== AUTO-DISCOVERY ====================
125
- scanForProjects(rootPath, recursive = true) {
126
- // Validate path is within allowed directories to prevent directory traversal
127
- validateProjectPath(rootPath);
128
- const discovered = [];
129
- const scan = (dir, depth = 0) => {
130
- if (depth > 3 && recursive)
131
- return; // Max 3 levels deep
132
- try {
133
- const entries = readdirSync(dir, { withFileTypes: true });
134
- for (const entry of entries) {
135
- if (!entry.isDirectory())
136
- continue;
137
- if (entry.name.startsWith('.') && entry.name !== '.git')
138
- continue;
139
- const fullPath = join(dir, entry.name);
140
- // Check if it's a git repo
141
- const gitDir = join(fullPath, '.git');
142
- if (existsSync(gitDir)) {
143
- const project = this.registerProjectFromPath(fullPath);
144
- if (project)
145
- discovered.push(project);
146
- }
147
- else if (recursive && depth < 3) {
148
- scan(fullPath, depth + 1);
149
- }
150
- }
151
- }
152
- catch {
153
- // Skip inaccessible directories
154
- }
155
- };
156
- scan(rootPath);
157
- // Update last scan time
158
- this.db.prepare(`
6
+ `).get(t,e?1:0)}getWatchDirs(){return this.db.prepare("SELECT * FROM watch_dirs").all()}removeWatchDir(t){this.db.prepare("DELETE FROM watch_dirs WHERE path = ?").run(t)}scanForProjects(t,e=!0){O(t);const s=[],r=(n,i=0)=>{if(!(i>3&&e))try{const a=D(n,{withFileTypes:!0});for(const o of a){if(!o.isDirectory()||o.name.startsWith(".")&&o.name!==".git")continue;const l=d(n,o.name),c=d(l,".git");if(u(c)){const p=this.registerProjectFromPath(l);p&&s.push(p)}else e&&i<3&&r(l,i+1)}}catch{}};return r(t),this.db.prepare(`
159
7
  UPDATE watch_dirs SET last_scan = datetime('now') WHERE path = ?
160
- `).run(rootPath);
161
- return discovered;
162
- }
163
- scanAllWatchDirs() {
164
- const dirs = this.getWatchDirs();
165
- const all = [];
166
- for (const dir of dirs) {
167
- const found = this.scanForProjects(dir.path, !!dir.recursive);
168
- all.push(...found);
169
- }
170
- return all;
171
- }
172
- registerProjectFromPath(projectPath) {
173
- try {
174
- // SECURITY: Validate path is a real directory before any operations
175
- const normalizedPath = normalize(resolve(projectPath));
176
- if (!existsSync(normalizedPath) || !statSync(normalizedPath).isDirectory()) {
177
- return null;
178
- }
179
- const name = basename(normalizedPath);
180
- let repo;
181
- let branch;
182
- let lastCommit;
183
- let stack;
184
- try {
185
- // SECURITY: Use spawnSync with shell: false to prevent command injection
186
- const repoResult = spawnSync('git', ['config', '--get', 'remote.origin.url'], {
187
- cwd: normalizedPath,
188
- encoding: 'utf-8',
189
- timeout: 5000,
190
- shell: false // CRITICAL: No shell interpretation
191
- });
192
- if (repoResult.status === 0) {
193
- repo = repoResult.stdout.trim();
194
- }
195
- const branchResult = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
196
- cwd: normalizedPath,
197
- encoding: 'utf-8',
198
- timeout: 5000,
199
- shell: false
200
- });
201
- if (branchResult.status === 0) {
202
- branch = branchResult.stdout.trim();
203
- }
204
- const commitResult = spawnSync('git', ['log', '-1', '--format=%h %s'], {
205
- cwd: normalizedPath,
206
- encoding: 'utf-8',
207
- timeout: 5000,
208
- shell: false
209
- });
210
- if (commitResult.status === 0) {
211
- lastCommit = commitResult.stdout.trim();
212
- }
213
- }
214
- catch {
215
- // Not a git repo or git not available
216
- }
217
- // Detect stack
218
- if (existsSync(join(normalizedPath, 'package.json'))) {
219
- stack = 'Node.js';
220
- if (existsSync(join(normalizedPath, 'next.config.js')) ||
221
- existsSync(join(normalizedPath, 'next.config.ts')) ||
222
- existsSync(join(normalizedPath, 'next.config.mjs'))) {
223
- stack = 'Next.js';
224
- }
225
- else if (existsSync(join(normalizedPath, 'vite.config.ts'))) {
226
- stack = 'Vite';
227
- }
228
- }
229
- else if (existsSync(join(normalizedPath, 'requirements.txt')) ||
230
- existsSync(join(normalizedPath, 'pyproject.toml'))) {
231
- stack = 'Python';
232
- }
233
- else if (existsSync(join(normalizedPath, 'composer.json'))) {
234
- stack = 'PHP';
235
- }
236
- else if (existsSync(join(normalizedPath, 'Cargo.toml'))) {
237
- stack = 'Rust';
238
- }
239
- else if (existsSync(join(normalizedPath, 'go.mod'))) {
240
- stack = 'Go';
241
- }
242
- return this.registerProject(name, normalizedPath, repo, stack, lastCommit, branch);
243
- }
244
- catch {
245
- return null;
246
- }
247
- }
248
- // ==================== PROJECTS ====================
249
- registerProject(name, path, repo, stack, lastCommit, branch) {
250
- const stmt = this.db.prepare(`
8
+ `).run(t),s}scanAllWatchDirs(){const t=this.getWatchDirs(),e=[];for(const s of t){const r=this.scanForProjects(s.path,!!s.recursive);e.push(...r)}return e}registerProjectFromPath(t){try{const e=v(M(t));if(!u(e)||!y(e).isDirectory())return null;const s=A(e);let r,n,i,a;try{const o=f("git",["config","--get","remote.origin.url"],{cwd:e,encoding:"utf-8",timeout:5e3,shell:!1});o.status===0&&(r=o.stdout.trim());const l=f("git",["rev-parse","--abbrev-ref","HEAD"],{cwd:e,encoding:"utf-8",timeout:5e3,shell:!1});l.status===0&&(n=l.stdout.trim());const c=f("git",["log","-1","--format=%h %s"],{cwd:e,encoding:"utf-8",timeout:5e3,shell:!1});c.status===0&&(i=c.stdout.trim())}catch{}return u(d(e,"package.json"))?(a="Node.js",u(d(e,"next.config.js"))||u(d(e,"next.config.ts"))||u(d(e,"next.config.mjs"))?a="Next.js":u(d(e,"vite.config.ts"))&&(a="Vite")):u(d(e,"requirements.txt"))||u(d(e,"pyproject.toml"))?a="Python":u(d(e,"composer.json"))?a="PHP":u(d(e,"Cargo.toml"))?a="Rust":u(d(e,"go.mod"))&&(a="Go"),this.registerProject(s,e,r,a,i,n)}catch{return null}}registerProject(t,e,s,r,n,i){return this.db.prepare(`
251
9
  INSERT INTO projects (name, path, repo, stack, last_commit, branch)
252
10
  VALUES (?, ?, ?, ?, ?, ?)
253
11
  ON CONFLICT(path) DO UPDATE SET
@@ -258,136 +16,39 @@ export class WyrmDB {
258
16
  branch = COALESCE(excluded.branch, projects.branch),
259
17
  updated_at = datetime('now')
260
18
  RETURNING *
261
- `);
262
- return stmt.get(name, path, repo || null, stack || null, lastCommit || null, branch || null);
263
- }
264
- getProject(path) {
265
- return this.db.prepare('SELECT * FROM projects WHERE path = ?').get(path);
266
- }
267
- getProjectById(id) {
268
- return this.db.prepare('SELECT * FROM projects WHERE id = ?').get(id);
269
- }
270
- getProjectByName(name) {
271
- return this.db.prepare('SELECT * FROM projects WHERE name = ?').get(name);
272
- }
273
- getAllProjects(limit = 100, offset = 0) {
274
- return this.db.prepare(`
19
+ `).get(t,e,s||null,r||null,n||null,i||null)}getProject(t){return this.db.prepare("SELECT * FROM projects WHERE path = ?").get(t)}getProjectById(t){return this.db.prepare("SELECT * FROM projects WHERE id = ?").get(t)}getProjectByName(t){return this.db.prepare("SELECT * FROM projects WHERE name = ?").get(t)}getAllProjects(t=100,e=0){return this.db.prepare(`
275
20
  SELECT * FROM projects ORDER BY updated_at DESC LIMIT ? OFFSET ?
276
- `).all(limit, offset);
277
- }
278
- searchProjects(query) {
279
- const pattern = `%${query}%`;
280
- return this.db.prepare(`
21
+ `).all(t,e)}searchProjects(t){const e=`%${t}%`;return this.db.prepare(`
281
22
  SELECT * FROM projects
282
23
  WHERE name LIKE ? OR stack LIKE ? OR repo LIKE ?
283
24
  ORDER BY updated_at DESC
284
25
  LIMIT 50
285
- `).all(pattern, pattern, pattern);
286
- }
287
- // ==================== SESSIONS ====================
288
- createSession(projectId, data) {
289
- const tokensEstimate = this.estimateTokens((data.objectives || '') + (data.completed || '') + (data.issues || '') + (data.notes || ''));
290
- // v7 F2 (T009): stamp the writing actor (NULL outside a fleet context).
291
- const ambient = getActor();
292
- const result = this.resilience.withRetrySync(() => {
293
- const stmt = this.db.prepare(`
26
+ `).all(e,e,e)}createSession(t,e){const s=this.estimateTokens((e.objectives||"")+(e.completed||"")+(e.issues||"")+(e.notes||"")),r=N(),n=this.resilience.withRetrySync(()=>this.db.prepare(`
294
27
  INSERT INTO sessions (project_id, date, objectives, completed, issues, commits, files_changed, notes, tokens_estimate, agent_id, run_id)
295
28
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
296
29
  RETURNING *
297
- `);
298
- return stmt.get(projectId, data.date || new Date().toISOString().split('T')[0], data.objectives || '', data.completed || '', data.issues || '', data.commits || '', data.files_changed || '', data.notes || '', tokensEstimate, ambient.agent_id, ambient.run_id);
299
- }, 'createSession');
300
- if (!result.success) {
301
- throw result.error || new Error('Failed to create session');
302
- }
303
- // Live Memory v6.4: failure-isolated event emit (never throws, never rolls back).
304
- emitEvent(this.db, { projectId, kind: 'session_update', refTable: 'sessions', refId: result.data.id });
305
- return result.data;
306
- }
307
- updateSession(id, data) {
308
- const updates = [];
309
- const values = [];
310
- // Allowlist column names to prevent SQL injection via crafted key names
311
- const ALLOWED_COLUMNS = new Set(['objectives', 'completed', 'issues', 'commits',
312
- 'files_changed', 'notes', 'summary', 'is_archived']);
313
- for (const [key, value] of Object.entries(data)) {
314
- if (ALLOWED_COLUMNS.has(key)) {
315
- updates.push(`${key} = ?`);
316
- values.push(value);
317
- }
318
- }
319
- if (updates.length === 0)
320
- return this.getSession(id);
321
- // Recalculate tokens if content changed
322
- if (data.objectives || data.completed || data.issues || data.notes) {
323
- const session = this.getSession(id);
324
- if (session) {
325
- const newTokens = this.estimateTokens((data.objectives || session.objectives) +
326
- (data.completed || session.completed) +
327
- (data.issues || session.issues) +
328
- (data.notes || session.notes));
329
- updates.push('tokens_estimate = ?');
330
- values.push(newTokens);
331
- }
332
- }
333
- values.push(id);
334
- const result = this.resilience.withRetrySync(() => {
335
- const stmt = this.db.prepare(`
336
- UPDATE sessions SET ${updates.join(', ')} WHERE id = ? RETURNING *
337
- `);
338
- return stmt.get(...values);
339
- }, 'updateSession');
340
- if (!result.success) {
341
- throw result.error || new Error('Failed to update session');
342
- }
343
- emitEvent(this.db, { projectId: result.data.project_id, kind: 'session_update', refTable: 'sessions', refId: id });
344
- return result.data;
345
- }
346
- getSession(id) {
347
- return this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
348
- }
349
- getRecentSessions(projectId, limit = 5) {
350
- return this.db.prepare(`
30
+ `).get(t,e.date||new Date().toISOString().split("T")[0],e.objectives||"",e.completed||"",e.issues||"",e.commits||"",e.files_changed||"",e.notes||"",s,r.agent_id,r.run_id),"createSession");if(!n.success)throw n.error||new Error("Failed to create session");return h(this.db,{projectId:t,kind:"session_update",refTable:"sessions",refId:n.data.id}),n.data}updateSession(t,e){const s=[],r=[],n=new Set(["objectives","completed","issues","commits","files_changed","notes","summary","is_archived"]);for(const[a,o]of Object.entries(e))n.has(a)&&(s.push(`${a} = ?`),r.push(o));if(s.length===0)return this.getSession(t);if(e.objectives||e.completed||e.issues||e.notes){const a=this.getSession(t);if(a){const o=this.estimateTokens((e.objectives||a.objectives)+(e.completed||a.completed)+(e.issues||a.issues)+(e.notes||a.notes));s.push("tokens_estimate = ?"),r.push(o)}}r.push(t);const i=this.resilience.withRetrySync(()=>this.db.prepare(`
31
+ UPDATE sessions SET ${s.join(", ")} WHERE id = ? RETURNING *
32
+ `).get(...r),"updateSession");if(!i.success)throw i.error||new Error("Failed to update session");return h(this.db,{projectId:i.data.project_id,kind:"session_update",refTable:"sessions",refId:t}),i.data}getSession(t){return this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(t)}getRecentSessions(t,e=5){return this.db.prepare(`
351
33
  SELECT * FROM sessions
352
34
  WHERE project_id = ? AND is_archived = 0
353
35
  ORDER BY date DESC, id DESC
354
36
  LIMIT ?
355
- `).all(projectId, limit);
356
- }
357
- getTodaySession(projectId) {
358
- const today = new Date().toISOString().split('T')[0];
359
- return this.db.prepare(`
37
+ `).all(t,e)}getTodaySession(t){const e=new Date().toISOString().split("T")[0];return this.db.prepare(`
360
38
  SELECT * FROM sessions WHERE project_id = ? AND date = ?
361
- `).get(projectId, today);
362
- }
363
- searchSessions(query, projectId) {
364
- const match = buildFtsMatchQuery(query);
365
- if (!match)
366
- return [];
367
- try {
368
- if (projectId) {
369
- return this.db.prepare(`
39
+ `).get(t,e)}searchSessions(t,e){const s=_(t);if(!s)return[];try{return e?this.db.prepare(`
370
40
  SELECT s.* FROM sessions s
371
41
  JOIN sessions_fts fts ON s.id = fts.rowid
372
42
  WHERE sessions_fts MATCH ? AND s.project_id = ?
373
43
  ORDER BY bm25(sessions_fts), s.date DESC
374
44
  LIMIT 50
375
- `).all(match, projectId);
376
- }
377
- return this.db.prepare(`
45
+ `).all(s,e):this.db.prepare(`
378
46
  SELECT s.* FROM sessions s
379
47
  JOIN sessions_fts fts ON s.id = fts.rowid
380
48
  WHERE sessions_fts MATCH ?
381
49
  ORDER BY bm25(sessions_fts), s.date DESC
382
50
  LIMIT 50
383
- `).all(match);
384
- }
385
- catch {
386
- return [];
387
- }
388
- }
389
- archiveOldSessions(projectId, keepRecent = 10) {
390
- const result = this.db.prepare(`
51
+ `).all(s)}catch{return[]}}archiveOldSessions(t,e=10){return this.db.prepare(`
391
52
  UPDATE sessions
392
53
  SET is_archived = 1
393
54
  WHERE project_id = ?
@@ -398,39 +59,16 @@ export class WyrmDB {
398
59
  ORDER BY date DESC, id DESC
399
60
  LIMIT ?
400
61
  )
401
- `).run(projectId, projectId, keepRecent);
402
- return result.changes;
403
- }
404
- getSessionTokenUsage(projectId) {
405
- const result = this.db.prepare(`
62
+ `).run(t,t,e).changes}getSessionTokenUsage(t){return this.db.prepare(`
406
63
  SELECT COALESCE(SUM(tokens_estimate), 0) as total
407
64
  FROM sessions WHERE project_id = ? AND is_archived = 0
408
- `).get(projectId);
409
- return result.total;
410
- }
411
- // ==================== QUESTS ====================
412
- addQuest(projectId, title, description, priority = 'medium', tags) {
413
- // v7 F2 (T009): stamp the writing actor (NULL outside a fleet context).
414
- const ambient = getActor();
415
- const quest = this.db.prepare(`
65
+ `).get(t).total}addQuest(t,e,s,r="medium",n){const i=N(),a=this.db.prepare(`
416
66
  INSERT INTO quests (project_id, title, description, priority, tags, agent_id, run_id)
417
67
  VALUES (?, ?, ?, ?, ?, ?, ?)
418
68
  RETURNING *
419
- `).get(projectId, title, description || '', priority, tags || null, ambient.agent_id, ambient.run_id);
420
- emitEvent(this.db, { projectId, kind: 'quest', refTable: 'quests', refId: quest.id });
421
- return quest;
422
- }
423
- updateQuest(id, status) {
424
- const completedAt = status === 'completed' ? new Date().toISOString() : null;
425
- const quest = this.db.prepare(`
69
+ `).get(t,e,s||"",r,n||null,i.agent_id,i.run_id);return h(this.db,{projectId:t,kind:"quest",refTable:"quests",refId:a.id}),a}updateQuest(t,e){const s=e==="completed"?new Date().toISOString():null,r=this.db.prepare(`
426
70
  UPDATE quests SET status = ?, completed_at = ? WHERE id = ? RETURNING *
427
- `).get(status, completedAt, id);
428
- if (quest)
429
- emitEvent(this.db, { projectId: quest.project_id, kind: 'quest', refTable: 'quests', refId: id });
430
- return quest;
431
- }
432
- getPendingQuests(projectId) {
433
- return this.db.prepare(`
71
+ `).get(e,s,t);return r&&h(this.db,{projectId:r.project_id,kind:"quest",refTable:"quests",refId:t}),r}getPendingQuests(t){return this.db.prepare(`
434
72
  SELECT * FROM quests
435
73
  WHERE project_id = ? AND status IN ('pending', 'in_progress')
436
74
  ORDER BY
@@ -441,10 +79,7 @@ export class WyrmDB {
441
79
  WHEN 'low' THEN 4
442
80
  END,
443
81
  created_at ASC
444
- `).all(projectId);
445
- }
446
- getAllPendingQuests() {
447
- return this.db.prepare(`
82
+ `).all(t)}getAllPendingQuests(){return this.db.prepare(`
448
83
  SELECT q.*, p.name as project_name FROM quests q
449
84
  JOIN projects p ON q.project_id = p.id
450
85
  WHERE q.status IN ('pending', 'in_progress')
@@ -456,136 +91,36 @@ export class WyrmDB {
456
91
  WHEN 'low' THEN 4
457
92
  END,
458
93
  q.created_at ASC
459
- `).all();
460
- }
461
- searchQuests(query) {
462
- const match = buildFtsMatchQuery(query);
463
- if (!match)
464
- return [];
465
- try {
466
- return this.db.prepare(`
94
+ `).all()}searchQuests(t){const e=_(t);if(!e)return[];try{return this.db.prepare(`
467
95
  SELECT q.* FROM quests q
468
96
  JOIN quests_fts fts ON q.id = fts.rowid
469
97
  WHERE quests_fts MATCH ?
470
98
  ORDER BY bm25(quests_fts), q.created_at DESC
471
99
  LIMIT 50
472
- `).all(match);
473
- }
474
- catch {
475
- return [];
476
- } // defense-in-depth: a malformed MATCH yields [], never throws
477
- }
478
- getRecentlyCompleted(projectId, limit = 5) {
479
- return this.db.prepare(`
100
+ `).all(e)}catch{return[]}}getRecentlyCompleted(t,e=5){return this.db.prepare(`
480
101
  SELECT * FROM quests
481
102
  WHERE project_id = ? AND status = 'completed'
482
103
  ORDER BY completed_at DESC
483
104
  LIMIT ?
484
- `).all(projectId, limit);
485
- }
486
- // ==================== CONTEXT ====================
487
- setContext(projectId, key, value) {
488
- this.db.prepare(`
105
+ `).all(t,e)}setContext(t,e,s){this.db.prepare(`
489
106
  INSERT INTO context (project_id, key, value)
490
107
  VALUES (?, ?, ?)
491
108
  ON CONFLICT(project_id, key) DO UPDATE SET
492
109
  value = excluded.value,
493
110
  updated_at = datetime('now')
494
- `).run(projectId, key, value);
495
- }
496
- getContext(projectId, key) {
497
- const row = this.db.prepare(`
111
+ `).run(t,e,s)}getContext(t,e){return this.db.prepare(`
498
112
  SELECT value FROM context WHERE project_id = ? AND key = ?
499
- `).get(projectId, key);
500
- return row?.value;
501
- }
502
- getAllContext(projectId) {
503
- const rows = this.db.prepare(`
113
+ `).get(t,e)?.value}getAllContext(t){const e=this.db.prepare(`
504
114
  SELECT key, value FROM context WHERE project_id = ?
505
- `).all(projectId);
506
- const result = {};
507
- for (const row of rows) {
508
- result[row.key] = row.value;
509
- }
510
- return result;
511
- }
512
- // ==================== LIVE MEMORY (v6.4) ====================
513
- /** Is the Live Memory event log active? (WYRM_LIVE_MEMORY, default ON.) */
514
- liveMemoryEnabled() {
515
- return isLiveMemoryEnabled();
516
- }
517
- /** Pull events newer than a cursor for a project (one-shot, idempotent). */
518
- eventsSince(projectId, sinceCursor = 0, limit = 100) {
519
- return eventsSince(this.db, projectId, sinceCursor, limit);
520
- }
521
- /** Initial subscribe: current head cursor + a recent chronological window. */
522
- subscribeEvents(projectId, window = 20) {
523
- return subscribeEvents(this.db, projectId, window);
524
- }
525
- /** Manually publish an event (e.g. a tool_call marker). Failure-isolated. */
526
- publishEvent(input) {
527
- emitEvent(this.db, input);
528
- }
529
- /** Retention sweep for the Live Memory event log. Bounded + failure-isolated. */
530
- pruneEvents(opts = {}) {
531
- return pruneEvents(this.db, opts);
532
- }
533
- // Phase 3 — cross-device replication primitives.
534
- /** Ingest a peer's event (PULL). Echo-suppressed + idempotent. */
535
- ingestRemoteEvent(localProjectId, ev) {
536
- return ingestRemoteEvent(this.db, localProjectId, ev);
537
- }
538
- /** Shared events past a watermark, for PUSH up to a peer (privacy-gated). */
539
- eventsForPush(projectId, sinceCursor = 0, limit = 200) {
540
- return eventsForPush(this.db, projectId, sinceCursor, limit);
541
- }
542
- /** Persistent KV in wyrm_meta — replication watermarks live here. */
543
- getMeta(key) { return getMeta(this.db, key); }
544
- setMeta(key, value) { setMeta(this.db, key, value); }
545
- // ==================== GLOBAL CONTEXT ====================
546
- setGlobalContext(key, value) {
547
- this.db.prepare(`
115
+ `).all(t),s={};for(const r of e)s[r.key]=r.value;return s}liveMemoryEnabled(){return x()}eventsSince(t,e=0,s=100){return W(this.db,t,e,s)}subscribeEvents(t,e=20){return U(this.db,t,e)}publishEvent(t){h(this.db,t)}pruneEvents(t={}){return B(this.db,t)}ingestRemoteEvent(t,e){return P(this.db,t,e)}eventsForPush(t,e=0,s=200){return q(this.db,t,e,s)}getMeta(t){return Y(this.db,t)}setMeta(t,e){$(this.db,t,e)}setGlobalContext(t,e){this.db.prepare(`
548
116
  INSERT INTO global_context (key, value)
549
117
  VALUES (?, ?)
550
118
  ON CONFLICT(key) DO UPDATE SET
551
119
  value = excluded.value,
552
120
  updated_at = datetime('now')
553
- `).run(key, value);
554
- }
555
- getGlobalContext(key) {
556
- const row = this.db.prepare(`
121
+ `).run(t,e)}getGlobalContext(t){return this.db.prepare(`
557
122
  SELECT value FROM global_context WHERE key = ?
558
- `).get(key);
559
- return row?.value;
560
- }
561
- getAllGlobalContext() {
562
- const rows = this.db.prepare('SELECT key, value FROM global_context').all();
563
- const result = {};
564
- for (const row of rows) {
565
- result[row.key] = row.value;
566
- }
567
- return result;
568
- }
569
- // ==================== SKILLS MANAGEMENT ====================
570
- registerSkill(name, description, skillPath, category, author, version, tags, governance) {
571
- // Normalize governance to safe defaults so existing callers (no 8th arg)
572
- // keep producing 'atomic' / '[]' / '[]' — fully backward-compatible.
573
- const tier = governance?.tier ?? 'atomic';
574
- const governsJson = JSON.stringify(Array.isArray(governance?.governs) ? governance.governs : []);
575
- const composesJson = JSON.stringify(Array.isArray(governance?.composes) ? governance.composes : []);
576
- // Migration 25: capture the SKILL.md body so the skill is portable across
577
- // machines. Best-effort — an unreadable/missing file stores NULL and never
578
- // blocks registration. On re-register we only OVERWRITE stored content when
579
- // the file is freshly readable (excluded.content IS NOT NULL); otherwise we
580
- // KEEP whatever was backfilled so a transient bad path can't wipe it.
581
- // `cross_project_visibility`/`is_shared` are deliberately NOT touched here:
582
- // they keep their private-by-default value across re-registrations (the
583
- // egress gate must never silently flip on a routine re-register).
584
- const captured = readSkillContent({ skillPath, name });
585
- const content = captured?.content ?? null;
586
- const contentSha = captured?.sha256 ?? null;
587
- const contentUpdatedAt = captured ? new Date().toISOString() : null;
588
- const result = this.resilience.withRetrySync(() => this.db.prepare(`
123
+ `).get(t)?.value}getAllGlobalContext(){const t=this.db.prepare("SELECT key, value FROM global_context").all(),e={};for(const s of t)e[s.key]=s.value;return e}registerSkill(t,e,s,r,n,i,a,o){const l=o?.tier??"atomic",c=JSON.stringify(Array.isArray(o?.governs)?o.governs:[]),p=JSON.stringify(Array.isArray(o?.composes)?o.composes:[]),g=C({skillPath:s,name:t}),m=g?.content??null,S=g?.sha256??null,R=g?new Date().toISOString():null,E=this.resilience.withRetrySync(()=>this.db.prepare(`
589
124
  INSERT INTO skills (name, description, skill_path, category, author, version, tags, tier, governs, composes, content, content_sha256, content_updated_at, is_active, usage_count)
590
125
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0)
591
126
  ON CONFLICT(name) DO UPDATE SET
@@ -607,299 +142,17 @@ export class WyrmDB {
607
142
  updated_at = datetime('now'),
608
143
  is_active = 1
609
144
  RETURNING *
610
- `).get(name, description, skillPath, category || null, author || null, version || null, tags || null, tier, governsJson, composesJson, content, contentSha, contentUpdatedAt), 'registerSkill');
611
- if (!result.success) {
612
- throw result.error || new Error('Failed to register skill');
613
- }
614
- return result.data;
615
- }
616
- getSkill(name) {
617
- const skill = this.db.prepare('SELECT * FROM skills WHERE name = ?').get(name);
618
- if (skill) {
619
- // Update last_used
620
- this.db.prepare('UPDATE skills SET last_used = datetime(\'now\'), usage_count = usage_count + 1 WHERE id = ?').run(skill.id);
621
- }
622
- return skill;
623
- }
624
- /**
625
- * Backfill SKILL.md content into the registry (migration 25). Scans every
626
- * registered skill, reads its SKILL.md (skill_path, fallback
627
- * <skillsDir>/<slug>/SKILL.md) and stores content + sha. IDEMPOTENT: a skill
628
- * whose on-disk sha already matches the stored sha is skipped (unchanged).
629
- * Never throws on an unreadable file — that skill is counted `missing`.
630
- *
631
- * `read` is injected (defaults to readSkillContent) so tests can sandbox the
632
- * filesystem; it returns {content, sha256} or null.
633
- */
634
- backfillSkillContent(opts = {}) {
635
- const read = opts.read ?? readSkillContent;
636
- const skills = this.db.prepare('SELECT id, name, skill_path, content_sha256 FROM skills')
637
- .all();
638
- let filled = 0, unchanged = 0, missing = 0;
639
- const update = this.db.prepare(`UPDATE skills SET content = ?, content_sha256 = ?, content_updated_at = ?, updated_at = datetime('now') WHERE id = ?`);
640
- const tx = this.db.transaction(() => {
641
- for (const s of skills) {
642
- const got = read({ skillPath: s.skill_path, name: s.name, skillsDir: opts.skillsDir });
643
- if (!got) {
644
- missing++;
645
- continue;
646
- }
647
- if (s.content_sha256 && s.content_sha256 === got.sha256) {
648
- unchanged++;
649
- continue;
650
- }
651
- update.run(got.content, got.sha256, new Date().toISOString(), s.id);
652
- filled++;
653
- }
654
- });
655
- tx();
656
- return { total: skills.length, filled, unchanged, missing };
657
- }
658
- /**
659
- * Materialize skills from the registry to disk (migration 25): write
660
- * <targetDir>/<slug>/SKILL.md from stored `content` for every skill that has
661
- * content. This is how a fresh machine reconstitutes the actual skill files
662
- * after pulling the synced DB. Round-trips byte-faithfully (content stored
663
- * verbatim, written verbatim). Skills with NULL content are skipped (counted
664
- * `skipped_no_content`). `includeInactive=false` exports only active skills.
665
- */
666
- exportSkillContent(targetDir, opts = {}) {
667
- const where = opts.includeInactive ? '' : ' WHERE is_active = 1';
668
- // ORDER BY name = deterministic export (which row keeps the bare slug on a
669
- // collision no longer depends on rowid/insertion order).
670
- const skills = this.db.prepare(`SELECT name, content FROM skills${where} ORDER BY name`)
671
- .all();
672
- let written = 0, skipped = 0, collisions = 0;
673
- const usedSlugs = new Set();
674
- for (const s of skills) {
675
- if (s.content == null) {
676
- skipped++;
677
- continue;
678
- }
679
- let slug = slugifySkillName(s.name);
680
- if (!slug) {
681
- skipped++;
682
- continue;
683
- }
684
- // skills.name is UNIQUE but slugs are NOT (case/punctuation folding + the
685
- // 48-char cap collapse distinct names). Disambiguate deterministically so a
686
- // later skill never silently clobbers an earlier one's SKILL.md — the
687
- // authoring path (deploySkill) refuses clobber, and export must not lose data.
688
- if (usedSlugs.has(slug)) {
689
- slug = `${slug}-${skillContentSha(s.name).slice(0, 8)}`;
690
- collisions++;
691
- }
692
- usedSlugs.add(slug);
693
- const dir = join(targetDir, slug);
694
- mkdirSync(dir, { recursive: true });
695
- writeFileSync(join(dir, 'SKILL.md'), s.content, 'utf-8');
696
- written++;
697
- }
698
- return { total: skills.length, written, skipped_no_content: skipped, collisions };
699
- }
700
- /**
701
- * Promote (or re-privatize) a skill's cloud-sync visibility — the egress
702
- * gate from migration 25. Default-private skills only leave the machine once
703
- * promoted to 'org'/'public'. Returns the updated row, or undefined if the
704
- * skill does not exist.
705
- */
706
- setSkillVisibility(name, visibility) {
707
- const row = this.db.prepare(`UPDATE skills SET cross_project_visibility = ?, updated_at = datetime('now') WHERE name = ? RETURNING *`).get(visibility, name);
708
- return row;
709
- }
710
- /**
711
- * Bulk visibility set — the egress lever for promoting many skills at once
712
- * (e.g. share the whole library to a teammate / another machine). Optional
713
- * `tier` filter; active-only unless `includeInactive`. Returns rows changed.
714
- * `tier` is bound (never interpolated). Egress still gated downstream by the
715
- * per-row visibility the sync engine reads — this is just how the operator
716
- * flips many rows in one command instead of one `share <name>` at a time.
717
- */
718
- setAllSkillsVisibility(visibility, opts = {}) {
719
- const conds = [];
720
- const params = [visibility];
721
- if (!opts.includeInactive)
722
- conds.push('is_active = 1');
723
- if (opts.tier) {
724
- conds.push('tier = ?');
725
- params.push(opts.tier);
726
- }
727
- const where = conds.length ? ' WHERE ' + conds.join(' AND ') : '';
728
- const res = this.db.prepare(`UPDATE skills SET cross_project_visibility = ?, updated_at = datetime('now')${where}`).run(...params);
729
- return res.changes;
730
- }
731
- listSkills(active, category, search, tier) {
732
- let query = 'SELECT * FROM skills WHERE 1=1';
733
- const params = [];
734
- if (active !== undefined) {
735
- query += ' AND is_active = ?';
736
- params.push(active ? 1 : 0);
737
- }
738
- if (category) {
739
- query += ' AND category = ?';
740
- params.push(category);
741
- }
742
- if (tier) {
743
- query += ' AND tier = ?';
744
- params.push(tier);
745
- }
746
- if (search) {
747
- const match = buildFtsMatchQuery(search);
748
- if (match) {
749
- query += ' AND id IN (SELECT rowid FROM skills_fts WHERE skills_fts MATCH ?)';
750
- params.push(match);
751
- }
752
- }
753
- query += ' ORDER BY updated_at DESC';
754
- return this.db.prepare(query).all(...params);
755
- }
756
- searchSkills(query, limit = 20, tier) {
757
- const match = buildFtsMatchQuery(query);
758
- if (!match)
759
- return [];
760
- // Optional tier filter is applied in SQL so the FTS rank ordering is preserved.
761
- const tierClause = tier ? ' AND s.tier = ?' : '';
762
- const stmt = this.db.prepare(`
145
+ `).get(t,e,s,r||null,n||null,i||null,a||null,l,c,p,m,S,R),"registerSkill");if(!E.success)throw E.error||new Error("Failed to register skill");return E.data}getSkill(t){const e=this.db.prepare("SELECT * FROM skills WHERE name = ?").get(t);return e&&this.db.prepare("UPDATE skills SET last_used = datetime('now'), usage_count = usage_count + 1 WHERE id = ?").run(e.id),e}backfillSkillContent(t={}){const e=t.read??C,s=this.db.prepare("SELECT id, name, skill_path, content_sha256 FROM skills").all();let r=0,n=0,i=0;const a=this.db.prepare("UPDATE skills SET content = ?, content_sha256 = ?, content_updated_at = ?, updated_at = datetime('now') WHERE id = ?");return this.db.transaction(()=>{for(const l of s){const c=e({skillPath:l.skill_path,name:l.name,skillsDir:t.skillsDir});if(!c){i++;continue}if(l.content_sha256&&l.content_sha256===c.sha256){n++;continue}a.run(c.content,c.sha256,new Date().toISOString(),l.id),r++}})(),{total:s.length,filled:r,unchanged:n,missing:i}}exportSkillContent(t,e={}){const s=e.includeInactive?"":" WHERE is_active = 1",r=this.db.prepare(`SELECT name, content FROM skills${s} ORDER BY name`).all();let n=0,i=0,a=0;const o=new Set;for(const l of r){if(l.content==null){i++;continue}let c=G(l.name);if(!c){i++;continue}o.has(c)&&(c=`${c}-${V(l.name).slice(0,8)}`,a++),o.add(c);const p=d(t,c);b(p,{recursive:!0}),L(d(p,"SKILL.md"),l.content,"utf-8"),n++}return{total:r.length,written:n,skipped_no_content:i,collisions:a}}setSkillVisibility(t,e){return this.db.prepare("UPDATE skills SET cross_project_visibility = ?, updated_at = datetime('now') WHERE name = ? RETURNING *").get(e,t)}setAllSkillsVisibility(t,e={}){const s=[],r=[t];e.includeInactive||s.push("is_active = 1"),e.tier&&(s.push("tier = ?"),r.push(e.tier));const n=s.length?" WHERE "+s.join(" AND "):"";return this.db.prepare(`UPDATE skills SET cross_project_visibility = ?, updated_at = datetime('now')${n}`).run(...r).changes}listSkills(t,e,s,r){let n="SELECT * FROM skills WHERE 1=1";const i=[];if(t!==void 0&&(n+=" AND is_active = ?",i.push(t?1:0)),e&&(n+=" AND category = ?",i.push(e)),r&&(n+=" AND tier = ?",i.push(r)),s){const a=_(s);a&&(n+=" AND id IN (SELECT rowid FROM skills_fts WHERE skills_fts MATCH ?)",i.push(a))}return n+=" ORDER BY updated_at DESC",this.db.prepare(n).all(...i)}searchSkills(t,e=20,s){const r=_(t);if(!r)return[];const n=s?" AND s.tier = ?":"",i=this.db.prepare(`
763
146
  SELECT s.* FROM skills s
764
147
  JOIN skills_fts ON s.id = skills_fts.rowid
765
- WHERE skills_fts MATCH ?${tierClause}
148
+ WHERE skills_fts MATCH ?${n}
766
149
  ORDER BY rank
767
150
  LIMIT ?
768
- `);
769
- return (tier ? stmt.all(match, tier, limit) : stmt.all(match, limit));
770
- }
771
- /**
772
- * Build the GOD-SKILL SPEC v2 governance graph.
773
- *
774
- * Walks tier routing edges: a node's `governs` list names the skills it routes
775
- * DOWN to (god → mega → atomic). Each node also exposes its `composes` list
776
- * (lateral pull-ins). When `root` is given, returns that single tree; otherwise
777
- * returns a forest of every 'god' tier skill (the apexes). Cycle-safe.
778
- */
779
- getSkillGraph(root) {
780
- const all = this.db.prepare('SELECT * FROM skills').all();
781
- const byName = new Map();
782
- for (const s of all)
783
- byName.set(s.name, s);
784
- const parseArr = (v) => {
785
- if (!v)
786
- return [];
787
- try {
788
- const parsed = JSON.parse(v);
789
- return Array.isArray(parsed) ? parsed.filter((x) => typeof x === 'string') : [];
790
- }
791
- catch {
792
- return [];
793
- }
794
- };
795
- const build = (name, seen) => {
796
- const skill = byName.get(name);
797
- if (!skill)
798
- return null;
799
- const governs = parseArr(skill.governs);
800
- const composes = parseArr(skill.composes);
801
- const node = {
802
- name: skill.name,
803
- tier: skill.tier ?? 'atomic',
804
- description: skill.description,
805
- governs,
806
- composes,
807
- children: [],
808
- };
809
- const missing = [];
810
- if (!seen.has(name)) {
811
- const nextSeen = new Set(seen).add(name);
812
- for (const childName of governs) {
813
- const child = build(childName, nextSeen);
814
- if (child)
815
- node.children.push(child);
816
- else
817
- missing.push(childName);
818
- }
819
- }
820
- if (missing.length)
821
- node.missing = missing;
822
- return node;
823
- };
824
- if (root) {
825
- const node = build(root, new Set());
826
- return node ? [node] : [];
827
- }
828
- // No root → forest of all god-tier apexes (deterministic by name).
829
- const gods = all
830
- .filter((s) => (s.tier ?? 'atomic') === 'god')
831
- .map((s) => s.name)
832
- .sort();
833
- const forest = [];
834
- for (const g of gods) {
835
- const node = build(g, new Set());
836
- if (node)
837
- forest.push(node);
838
- }
839
- return forest;
840
- }
841
- updateSkill(name, updates) {
842
- const setClauses = [];
843
- const values = [];
844
- if (updates.description !== undefined) {
845
- setClauses.push('description = ?');
846
- values.push(updates.description);
847
- }
848
- if (updates.skill_path !== undefined) {
849
- setClauses.push('skill_path = ?');
850
- values.push(updates.skill_path);
851
- }
852
- if (updates.category !== undefined) {
853
- setClauses.push('category = ?');
854
- values.push(updates.category);
855
- }
856
- if (updates.is_active !== undefined) {
857
- setClauses.push('is_active = ?');
858
- values.push(updates.is_active ? 1 : 0);
859
- }
860
- if (updates.tags !== undefined) {
861
- setClauses.push('tags = ?');
862
- values.push(updates.tags);
863
- }
864
- if (updates.version !== undefined) {
865
- setClauses.push('version = ?');
866
- values.push(updates.version);
867
- }
868
- if (setClauses.length === 0) {
869
- return this.getSkill(name);
870
- }
871
- setClauses.push('updated_at = datetime(\'now\')');
872
- values.push(name);
873
- return this.db.prepare(`
874
- UPDATE skills SET ${setClauses.join(', ')} WHERE name = ? RETURNING *
875
- `).get(...values);
876
- }
877
- deleteSkill(name) {
878
- const result = this.db.prepare('DELETE FROM skills WHERE name = ?').run(name);
879
- return result.changes > 0;
880
- }
881
- deactivateSkill(name) {
882
- return this.updateSkill(name, { is_active: false });
883
- }
884
- activateSkill(name) {
885
- return this.updateSkill(name, { is_active: true });
886
- }
887
- getSkillStats() {
888
- const total = this.db.prepare('SELECT COUNT(*) as count FROM skills').get().count;
889
- const active = this.db.prepare('SELECT COUNT(*) as count FROM skills WHERE is_active = 1').get().count;
890
- const byCategoryRows = this.db.prepare(`
151
+ `);return s?i.all(r,s,e):i.all(r,e)}getSkillGraph(t){const e=this.db.prepare("SELECT * FROM skills").all(),s=new Map;for(const o of e)s.set(o.name,o);const r=o=>{if(!o)return[];try{const l=JSON.parse(o);return Array.isArray(l)?l.filter(c=>typeof c=="string"):[]}catch{return[]}},n=(o,l)=>{const c=s.get(o);if(!c)return null;const p=r(c.governs),g=r(c.composes),m={name:c.name,tier:c.tier??"atomic",description:c.description,governs:p,composes:g,children:[]},S=[];if(!l.has(o)){const R=new Set(l).add(o);for(const E of p){const T=n(E,R);T?m.children.push(T):S.push(E)}}return S.length&&(m.missing=S),m};if(t){const o=n(t,new Set);return o?[o]:[]}const i=e.filter(o=>(o.tier??"atomic")==="god").map(o=>o.name).sort(),a=[];for(const o of i){const l=n(o,new Set);l&&a.push(l)}return a}updateSkill(t,e){const s=[],r=[];return e.description!==void 0&&(s.push("description = ?"),r.push(e.description)),e.skill_path!==void 0&&(s.push("skill_path = ?"),r.push(e.skill_path)),e.category!==void 0&&(s.push("category = ?"),r.push(e.category)),e.is_active!==void 0&&(s.push("is_active = ?"),r.push(e.is_active?1:0)),e.tags!==void 0&&(s.push("tags = ?"),r.push(e.tags)),e.version!==void 0&&(s.push("version = ?"),r.push(e.version)),s.length===0?this.getSkill(t):(s.push("updated_at = datetime('now')"),r.push(t),this.db.prepare(`
152
+ UPDATE skills SET ${s.join(", ")} WHERE name = ? RETURNING *
153
+ `).get(...r))}deleteSkill(t){return this.db.prepare("DELETE FROM skills WHERE name = ?").run(t).changes>0}deactivateSkill(t){return this.updateSkill(t,{is_active:!1})}activateSkill(t){return this.updateSkill(t,{is_active:!0})}getSkillStats(){const t=this.db.prepare("SELECT COUNT(*) as count FROM skills").get().count,e=this.db.prepare("SELECT COUNT(*) as count FROM skills WHERE is_active = 1").get().count,s=this.db.prepare(`
891
154
  SELECT category, COUNT(*) as count FROM skills WHERE category IS NOT NULL GROUP BY category
892
- `).all();
893
- const byCategory = {};
894
- for (const row of byCategoryRows) {
895
- byCategory[row.category] = row.count;
896
- }
897
- return { total, active, byCategory };
898
- }
899
- // ==================== SPEC-KIT REGISTRY ====================
900
- /** Upsert a spec→project link (idempotent by project_id + spec_dir). */
901
- upsertSpec(projectId, specDir, title, summary, taskCount = 0) {
902
- return this.db.prepare(`
155
+ `).all(),r={};for(const n of s)r[n.category]=n.count;return{total:t,active:e,byCategory:r}}upsertSpec(t,e,s,r,n=0){return this.db.prepare(`
903
156
  INSERT INTO specs (project_id, spec_dir, title, summary, task_count)
904
157
  VALUES (?, ?, ?, ?, ?)
905
158
  ON CONFLICT(project_id, spec_dir) DO UPDATE SET
@@ -908,368 +161,48 @@ export class WyrmDB {
908
161
  task_count = excluded.task_count,
909
162
  updated_at = datetime('now')
910
163
  RETURNING *
911
- `).get(projectId, specDir, title || null, summary || null, taskCount);
912
- }
913
- getSpec(projectId, specDir) {
914
- return this.db.prepare('SELECT * FROM specs WHERE project_id = ? AND spec_dir = ?').get(projectId, specDir);
915
- }
916
- listSpecs(projectId) {
917
- if (projectId !== undefined) {
918
- return this.db.prepare('SELECT * FROM specs WHERE project_id = ? ORDER BY updated_at DESC').all(projectId);
919
- }
920
- return this.db.prepare('SELECT * FROM specs ORDER BY updated_at DESC').all();
921
- }
922
- /**
923
- * Find an existing quest for a project carrying a specific spec-task tag
924
- * signature. Used by wyrm_spec_register to dedupe (update-not-duplicate) on
925
- * re-run. The signature lives in the quest's comma-separated `tags` column.
926
- */
927
- findQuestBySpecSignature(projectId, signature) {
928
- return this.db.prepare(`
164
+ `).get(t,e,s||null,r||null,n)}getSpec(t,e){return this.db.prepare("SELECT * FROM specs WHERE project_id = ? AND spec_dir = ?").get(t,e)}listSpecs(t){return t!==void 0?this.db.prepare("SELECT * FROM specs WHERE project_id = ? ORDER BY updated_at DESC").all(t):this.db.prepare("SELECT * FROM specs ORDER BY updated_at DESC").all()}findQuestBySpecSignature(t,e){return this.db.prepare(`
929
165
  SELECT * FROM quests
930
166
  WHERE project_id = ?
931
167
  AND (tags = ? OR tags LIKE ? OR tags LIKE ? OR tags LIKE ?)
932
168
  LIMIT 1
933
- `).get(projectId, signature, `${signature},%`, `%,${signature}`, `%,${signature},%`);
934
- }
935
- /** Update a quest's title/description/tags in place (used by spec re-registration). */
936
- updateQuestFields(id, fields) {
937
- const setClauses = [];
938
- const values = [];
939
- if (fields.title !== undefined) {
940
- setClauses.push('title = ?');
941
- values.push(fields.title);
942
- }
943
- if (fields.description !== undefined) {
944
- setClauses.push('description = ?');
945
- values.push(fields.description);
946
- }
947
- if (fields.tags !== undefined) {
948
- setClauses.push('tags = ?');
949
- values.push(fields.tags);
950
- }
951
- if (setClauses.length === 0) {
952
- return this.db.prepare('SELECT * FROM quests WHERE id = ?').get(id);
953
- }
954
- values.push(id);
955
- const quest = this.db.prepare(`UPDATE quests SET ${setClauses.join(', ')} WHERE id = ? RETURNING *`).get(...values);
956
- if (quest)
957
- emitEvent(this.db, { projectId: quest.project_id, kind: 'quest', refTable: 'quests', refId: id });
958
- return quest;
959
- }
960
- // ==================== DATA LAKE ====================
961
- insertData(projectId, category, key, value, metadata) {
962
- const result = this.resilience.withRetrySync(() => this.db.prepare(`
169
+ `).get(t,e,`${e},%`,`%,${e}`,`%,${e},%`)}updateQuestFields(t,e){const s=[],r=[];if(e.title!==void 0&&(s.push("title = ?"),r.push(e.title)),e.description!==void 0&&(s.push("description = ?"),r.push(e.description)),e.tags!==void 0&&(s.push("tags = ?"),r.push(e.tags)),s.length===0)return this.db.prepare("SELECT * FROM quests WHERE id = ?").get(t);r.push(t);const n=this.db.prepare(`UPDATE quests SET ${s.join(", ")} WHERE id = ? RETURNING *`).get(...r);return n&&h(this.db,{projectId:n.project_id,kind:"quest",refTable:"quests",refId:t}),n}insertData(t,e,s,r,n){const i=this.resilience.withRetrySync(()=>this.db.prepare(`
963
170
  INSERT INTO data_lake (project_id, category, key, value, metadata)
964
171
  VALUES (?, ?, ?, ?, ?)
965
172
  RETURNING *
966
- `).get(projectId, category, key, value, metadata ? JSON.stringify(metadata) : null), 'insertData');
967
- if (!result.success) {
968
- throw result.error || new Error('Insert data failed');
969
- }
970
- return result.data;
971
- }
972
- /**
973
- * v7 F2 (T011): batch N arbitrary writes into ONE better-sqlite3 transaction.
974
- *
975
- * Cross-process write story — under WAL every transaction takes the single
976
- * cross-process write lock; batching N writes into one transaction acquires
977
- * it ONCE per batch instead of once per row, so a fleet writer holds the
978
- * lock for one short window rather than N tiny contended ones. The whole
979
- * batch is atomic: if any `write(item)` throws, better-sqlite3 rolls the
980
- * transaction back and NO item persists (the thrown error propagates).
981
- *
982
- * Why this is a transaction helper and NOT an in-process write queue:
983
- * better-sqlite3 is fully synchronous — two writes issued by the SAME
984
- * process can never interleave by construction (each statement completes
985
- * before the next starts), so an in-process queue would serialize something
986
- * that is already serial. The only real contention is CROSS-process, which
987
- * is handled by busy_timeout=5000 (constructor above) plus the structured
988
- * WYRM_BUSY dispatcher body (sqlite-busy.ts).
989
- *
990
- * NOTE: `write` runs inside the transaction — keep it to synchronous DB
991
- * work (no awaits; better-sqlite3 transactions cannot span async gaps).
992
- */
993
- batchWrites(items, write) {
994
- const tx = this.db.transaction((batch) => batch.map((item) => write(item)));
995
- return tx(items);
996
- }
997
- /**
998
- * Batch insert with resilience - uses checkpointing for large batches.
999
- *
1000
- * Contract: returns the inserted count ONLY when every chunk committed.
1001
- * A failed chunk must surface as a structured retryable error, not a
1002
- * success-shaped partial count — each chunk commits through one batchWrites
1003
- * transaction, so a failed chunk rolled back atomically and rethrowing lets
1004
- * the dispatcher map SQLITE_BUSY to the structured WYRM_BUSY retry body
1005
- * exactly like every sibling write path (insertData, createSession, ...).
1006
- * Cross-chunk progress rides on the thrown error as `inserted` so a caller
1007
- * batching more than one chunk (> BATCH_SIZE items) can surface how many
1008
- * rows landed in the chunks that DID commit.
1009
- */
1010
- insertDataBatch(data) {
1011
- const operationId = this.resilience.generateOperationId('batch_insert');
1012
- const batchSize = this.BATCH_SIZE;
1013
- let totalInserted = 0;
1014
- // Checkpoint for recovery
1015
- this.resilience.createCheckpoint(operationId, 'batch_insert', 'started', {
1016
- totalItems: data.length,
1017
- batchSize,
1018
- });
1019
- const insert = this.db.prepare(`
173
+ `).get(t,e,s,r,n?JSON.stringify(n):null),"insertData");if(!i.success)throw i.error||new Error("Insert data failed");return i.data}batchWrites(t,e){return this.db.transaction(r=>r.map(n=>e(n)))(t)}insertDataBatch(t){const e=this.resilience.generateOperationId("batch_insert"),s=this.BATCH_SIZE;let r=0;this.resilience.createCheckpoint(e,"batch_insert","started",{totalItems:t.length,batchSize:s});const n=this.db.prepare(`
1020
174
  INSERT INTO data_lake (project_id, category, key, value, metadata)
1021
175
  VALUES (?, ?, ?, ?, ?)
1022
- `);
1023
- try {
1024
- // Process in batches for large datasets
1025
- for (let i = 0; i < data.length; i += batchSize) {
1026
- const batch = data.slice(i, i + batchSize);
1027
- const batchNum = Math.floor(i / batchSize) + 1;
1028
- this.resilience.updateCheckpoint(operationId, `batch_${batchNum}`, {
1029
- processed: i,
1030
- currentBatch: batchNum,
1031
- });
1032
- // One transaction per chunk via batchWrites (v7 F2 review fix: this
1033
- // IS the production call site of the T011 helper — the write lock is
1034
- // taken once per chunk instead of once per row, the cross-process
1035
- // story sqlite-busy.ts documents as mitigation (b)).
1036
- const result = this.resilience.withRetrySync(() => this.batchWrites(batch, (item) => {
1037
- insert.run(item.projectId, item.category, item.key, item.value, item.metadata ? JSON.stringify(item.metadata) : null);
1038
- return 1;
1039
- }).length, `batch_insert_${batchNum}`, { maxAttempts: 3 });
1040
- if (!result.success) {
1041
- this.logger.error('Batch insert failed', {
1042
- batch: batchNum,
1043
- processed: totalInserted,
1044
- error: result.error?.message,
1045
- });
1046
- this.resilience.updateCheckpoint(operationId, 'partial_failure', {
1047
- inserted: totalInserted,
1048
- failedAt: i,
1049
- });
1050
- // A failed chunk must surface as a structured retryable error, not
1051
- // a success-shaped partial count: the chunk transaction rolled back
1052
- // atomically (nothing partial within it), and rethrowing with the
1053
- // SQLite code intact is what lets the dispatcher emit the WYRM_BUSY
1054
- // retry body. Cross-chunk progress rides on the error (see doc).
1055
- const error = result.error ?? new Error(`Batch insert failed at chunk ${batchNum}`);
1056
- error.inserted = totalInserted;
1057
- throw error;
1058
- }
1059
- totalInserted += result.data;
1060
- }
1061
- this.resilience.completeCheckpoint(operationId);
1062
- return totalInserted;
1063
- }
1064
- catch (error) {
1065
- // Same invariant as the chunk-failure branch: swallowing here would
1066
- // re-shape the failure into a success-looking count. A chunk failure
1067
- // already logged + checkpointed before throwing (it carries the
1068
- // cross-chunk progress); anything else is an unexpected exception —
1069
- // record it, attach progress, and keep it loud either way.
1070
- const alreadyHandled = error !== null && typeof error === 'object' && 'inserted' in error;
1071
- if (!alreadyHandled) {
1072
- this.logger.error('Batch insert exception', {
1073
- inserted: totalInserted,
1074
- error: error.message,
1075
- });
1076
- this.resilience.updateCheckpoint(operationId, 'exception', {
1077
- inserted: totalInserted,
1078
- error: error.message,
1079
- });
1080
- if (error !== null && typeof error === 'object') {
1081
- error.inserted = totalInserted;
1082
- }
1083
- }
1084
- throw error;
1085
- }
1086
- }
1087
- queryData(projectId, category, limit = 100, offset = 0) {
1088
- if (category) {
1089
- return this.db.prepare(`
176
+ `);try{for(let i=0;i<t.length;i+=s){const a=t.slice(i,i+s),o=Math.floor(i/s)+1;this.resilience.updateCheckpoint(e,`batch_${o}`,{processed:i,currentBatch:o});const l=this.resilience.withRetrySync(()=>this.batchWrites(a,c=>(n.run(c.projectId,c.category,c.key,c.value,c.metadata?JSON.stringify(c.metadata):null),1)).length,`batch_insert_${o}`,{maxAttempts:3});if(!l.success){this.logger.error("Batch insert failed",{batch:o,processed:r,error:l.error?.message}),this.resilience.updateCheckpoint(e,"partial_failure",{inserted:r,failedAt:i});const c=l.error??new Error(`Batch insert failed at chunk ${o}`);throw c.inserted=r,c}r+=l.data}return this.resilience.completeCheckpoint(e),r}catch(i){throw i!==null&&typeof i=="object"&&"inserted"in i||(this.logger.error("Batch insert exception",{inserted:r,error:i.message}),this.resilience.updateCheckpoint(e,"exception",{inserted:r,error:i.message}),i!==null&&typeof i=="object"&&(i.inserted=r)),i}}queryData(t,e,s=100,r=0){return e?this.db.prepare(`
1090
177
  SELECT * FROM data_lake
1091
178
  WHERE project_id = ? AND category = ?
1092
179
  ORDER BY created_at DESC
1093
180
  LIMIT ? OFFSET ?
1094
- `).all(projectId, category, limit, offset);
1095
- }
1096
- return this.db.prepare(`
181
+ `).all(t,e,s,r):this.db.prepare(`
1097
182
  SELECT * FROM data_lake
1098
183
  WHERE project_id = ?
1099
184
  ORDER BY created_at DESC
1100
185
  LIMIT ? OFFSET ?
1101
- `).all(projectId, limit, offset);
1102
- }
1103
- searchData(query, projectId) {
1104
- const match = buildFtsMatchQuery(query);
1105
- if (!match)
1106
- return [];
1107
- try {
1108
- if (projectId) {
1109
- return this.db.prepare(`
186
+ `).all(t,s,r)}searchData(t,e){const s=_(t);if(!s)return[];try{return e?this.db.prepare(`
1110
187
  SELECT d.* FROM data_lake d
1111
188
  JOIN data_lake_fts fts ON d.id = fts.rowid
1112
189
  WHERE data_lake_fts MATCH ? AND d.project_id = ?
1113
190
  ORDER BY bm25(data_lake_fts), d.created_at DESC
1114
191
  LIMIT 100
1115
- `).all(match, projectId);
1116
- }
1117
- return this.db.prepare(`
192
+ `).all(s,e):this.db.prepare(`
1118
193
  SELECT d.* FROM data_lake d
1119
194
  JOIN data_lake_fts fts ON d.id = fts.rowid
1120
195
  WHERE data_lake_fts MATCH ?
1121
196
  ORDER BY bm25(data_lake_fts), d.created_at DESC
1122
197
  LIMIT 100
1123
- `).all(match);
1124
- }
1125
- catch {
1126
- return [];
1127
- }
1128
- }
1129
- getDataCategories(projectId) {
1130
- return this.db.prepare(`
198
+ `).all(s)}catch{return[]}}getDataCategories(t){return this.db.prepare(`
1131
199
  SELECT category, COUNT(*) as count
1132
200
  FROM data_lake
1133
201
  WHERE project_id = ?
1134
202
  GROUP BY category
1135
203
  ORDER BY count DESC
1136
- `).all(projectId);
1137
- }
1138
- deleteDataCategory(projectId, category) {
1139
- const result = this.db.prepare(`
204
+ `).all(t)}deleteDataCategory(t,e){return this.db.prepare(`
1140
205
  DELETE FROM data_lake WHERE project_id = ? AND category = ?
1141
- `).run(projectId, category);
1142
- return result.changes;
1143
- }
1144
- // ==================== STREAMING ====================
1145
- *streamSessions(projectId) {
1146
- const stmt = this.db.prepare(`
206
+ `).run(t,e).changes}*streamSessions(t){const e=this.db.prepare(`
1147
207
  SELECT * FROM sessions WHERE project_id = ? ORDER BY date DESC
1148
- `);
1149
- for (const row of stmt.iterate(projectId)) {
1150
- yield row;
1151
- }
1152
- }
1153
- *streamData(projectId, category) {
1154
- const stmt = category
1155
- ? this.db.prepare('SELECT * FROM data_lake WHERE project_id = ? AND category = ?')
1156
- : this.db.prepare('SELECT * FROM data_lake WHERE project_id = ?');
1157
- const params = category ? [projectId, category] : [projectId];
1158
- for (const row of stmt.iterate(...params)) {
1159
- yield row;
1160
- }
1161
- }
1162
- // ==================== STATS & UTILITIES ====================
1163
- getStats() {
1164
- const projects = this.db.prepare('SELECT COUNT(*) as count FROM projects').get();
1165
- const sessions = this.db.prepare('SELECT COUNT(*) as count FROM sessions').get();
1166
- const quests = this.db.prepare('SELECT COUNT(*) as count FROM quests').get();
1167
- const dataPoints = this.db.prepare('SELECT COUNT(*) as count FROM data_lake').get();
1168
- const tokens = this.db.prepare('SELECT COALESCE(SUM(tokens_estimate), 0) as total FROM sessions WHERE is_archived = 0').get();
1169
- const pageCount = this.db.pragma('page_count', { simple: true });
1170
- const pageSize = this.db.pragma('page_size', { simple: true });
1171
- const dbSize = (pageCount * pageSize) / (1024 * 1024);
1172
- return {
1173
- projects: projects.count,
1174
- sessions: sessions.count,
1175
- quests: quests.count,
1176
- dataPoints: dataPoints.count,
1177
- totalTokens: tokens.total,
1178
- dbSize: `${dbSize.toFixed(2)} MB`
1179
- };
1180
- }
1181
- getProjectStats(projectId) {
1182
- const sessions = this.db.prepare('SELECT COUNT(*) as count FROM sessions WHERE project_id = ?').get(projectId);
1183
- const pendingQuests = this.db.prepare(`SELECT COUNT(*) as count FROM quests WHERE project_id = ? AND status IN ('pending', 'in_progress')`).get(projectId);
1184
- const completedQuests = this.db.prepare(`SELECT COUNT(*) as count FROM quests WHERE project_id = ? AND status = 'completed'`).get(projectId);
1185
- const dataPoints = this.db.prepare('SELECT COUNT(*) as count FROM data_lake WHERE project_id = ?').get(projectId);
1186
- const tokens = this.db.prepare('SELECT COALESCE(SUM(tokens_estimate), 0) as total FROM sessions WHERE project_id = ? AND is_archived = 0').get(projectId);
1187
- return {
1188
- sessions: sessions.count,
1189
- quests: { pending: pendingQuests.count, completed: completedQuests.count },
1190
- dataPoints: dataPoints.count,
1191
- tokens: tokens.total
1192
- };
1193
- }
1194
- estimateTokens(text) {
1195
- // Rough estimate: ~4 chars per token
1196
- return Math.ceil(text.length / 4);
1197
- }
1198
- vacuum() {
1199
- this.db.exec('VACUUM');
1200
- }
1201
- checkpoint() {
1202
- this.db.pragma('wal_checkpoint(TRUNCATE)');
1203
- }
1204
- /**
1205
- * Get resilience status for monitoring
1206
- */
1207
- getResilienceStatus() {
1208
- const circuit = this.resilience.getCircuitStatus();
1209
- const incomplete = this.resilience.getIncompleteOperations();
1210
- return {
1211
- circuitState: circuit.state,
1212
- failures: circuit.failures,
1213
- incompleteOps: incomplete.length,
1214
- };
1215
- }
1216
- /**
1217
- * Reset circuit breaker (manual recovery)
1218
- */
1219
- resetCircuitBreaker() {
1220
- this.resilience.resetCircuit();
1221
- }
1222
- /**
1223
- * Safe close with WAL checkpoint and cleanup
1224
- */
1225
- close() {
1226
- try {
1227
- // Checkpoint WAL to ensure all data is persisted
1228
- this.checkpoint();
1229
- this.logger.info('Database checkpoint completed');
1230
- }
1231
- catch (error) {
1232
- this.logger.error('Checkpoint failed during close', {
1233
- error: error.message,
1234
- });
1235
- }
1236
- try {
1237
- this.db.close();
1238
- this.logger.info('Database closed successfully');
1239
- }
1240
- catch (error) {
1241
- this.logger.error('Database close failed', {
1242
- error: error.message,
1243
- });
1244
- }
1245
- }
1246
- /**
1247
- * Check database integrity
1248
- */
1249
- checkIntegrity() {
1250
- const issues = [];
1251
- try {
1252
- const result = this.db.pragma('integrity_check', { simple: true });
1253
- if (result !== 'ok') {
1254
- issues.push(`Integrity check failed: ${result}`);
1255
- }
1256
- }
1257
- catch (error) {
1258
- issues.push(`Integrity check error: ${error.message}`);
1259
- }
1260
- try {
1261
- const fk = this.db.pragma('foreign_key_check');
1262
- if (fk.length > 0) {
1263
- issues.push(`Foreign key violations: ${fk.length}`);
1264
- }
1265
- }
1266
- catch (error) {
1267
- issues.push(`FK check error: ${error.message}`);
1268
- }
1269
- return {
1270
- ok: issues.length === 0,
1271
- issues,
1272
- };
1273
- }
1274
- }
1275
- //# sourceMappingURL=database.js.map
208
+ `);for(const s of e.iterate(t))yield s}*streamData(t,e){const s=e?this.db.prepare("SELECT * FROM data_lake WHERE project_id = ? AND category = ?"):this.db.prepare("SELECT * FROM data_lake WHERE project_id = ?"),r=e?[t,e]:[t];for(const n of s.iterate(...r))yield n}getStats(){const t=this.db.prepare("SELECT COUNT(*) as count FROM projects").get(),e=this.db.prepare("SELECT COUNT(*) as count FROM sessions").get(),s=this.db.prepare("SELECT COUNT(*) as count FROM quests").get(),r=this.db.prepare("SELECT COUNT(*) as count FROM data_lake").get(),n=this.db.prepare("SELECT COALESCE(SUM(tokens_estimate), 0) as total FROM sessions WHERE is_archived = 0").get(),i=this.db.pragma("page_count",{simple:!0}),a=this.db.pragma("page_size",{simple:!0}),o=i*a/(1024*1024);return{projects:t.count,sessions:e.count,quests:s.count,dataPoints:r.count,totalTokens:n.total,dbSize:`${o.toFixed(2)} MB`}}getProjectStats(t){const e=this.db.prepare("SELECT COUNT(*) as count FROM sessions WHERE project_id = ?").get(t),s=this.db.prepare("SELECT COUNT(*) as count FROM quests WHERE project_id = ? AND status IN ('pending', 'in_progress')").get(t),r=this.db.prepare("SELECT COUNT(*) as count FROM quests WHERE project_id = ? AND status = 'completed'").get(t),n=this.db.prepare("SELECT COUNT(*) as count FROM data_lake WHERE project_id = ?").get(t),i=this.db.prepare("SELECT COALESCE(SUM(tokens_estimate), 0) as total FROM sessions WHERE project_id = ? AND is_archived = 0").get(t);return{sessions:e.count,quests:{pending:s.count,completed:r.count},dataPoints:n.count,tokens:i.total}}estimateTokens(t){return Math.ceil(t.length/4)}vacuum(){this.db.exec("VACUUM")}checkpoint(){this.db.pragma("wal_checkpoint(TRUNCATE)")}getResilienceStatus(){const t=this.resilience.getCircuitStatus(),e=this.resilience.getIncompleteOperations();return{circuitState:t.state,failures:t.failures,incompleteOps:e.length}}resetCircuitBreaker(){this.resilience.resetCircuit()}close(){try{this.checkpoint(),this.logger.info("Database checkpoint completed")}catch(t){this.logger.error("Checkpoint failed during close",{error:t.message})}try{this.db.close(),this.logger.info("Database closed successfully")}catch(t){this.logger.error("Database close failed",{error:t.message})}}checkIntegrity(){const t=[];try{const e=this.db.pragma("integrity_check",{simple:!0});e!=="ok"&&t.push(`Integrity check failed: ${e}`)}catch(e){t.push(`Integrity check error: ${e.message}`)}try{const e=this.db.pragma("foreign_key_check");e.length>0&&t.push(`Foreign key violations: ${e.length}`)}catch(e){t.push(`FK check error: ${e.message}`)}return{ok:t.length===0,issues:t}}}export{ae as WyrmDB};