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.
- package/LICENSE +26 -667
- package/NOTICE +14 -33
- package/dist/activation.d.ts.map +1 -1
- package/dist/activation.js +1 -44
- package/dist/activation.js.map +1 -1
- package/dist/agent-daemon.js +4 -281
- package/dist/agent-loop.js +7 -332
- package/dist/analytics.js +13 -236
- package/dist/attribution.js +1 -49
- package/dist/audit.js +2 -457
- package/dist/auto-capture.js +3 -138
- package/dist/auto-orchestrator.js +1 -325
- package/dist/autoconfig.js +39 -840
- package/dist/buddy-runner.js +1 -109
- package/dist/buddy.js +14 -564
- package/dist/build-flags.js +1 -17
- package/dist/capabilities.js +3 -183
- package/dist/capture.js +1 -56
- package/dist/causality.js +6 -107
- package/dist/cli.js +20 -281
- package/dist/cloud/cli.js +5 -541
- package/dist/cloud/client.js +1 -221
- package/dist/cloud/crypto.js +1 -85
- package/dist/cloud/machine-id.js +2 -113
- package/dist/cloud/recovery.js +1 -60
- package/dist/cloud/sync-engine.js +7 -543
- package/dist/cloud-backup.js +5 -579
- package/dist/cloud-profile.js +1 -138
- package/dist/cloud-sync-entrypoint.js +1 -47
- package/dist/cloud-sync.js +2 -309
- package/dist/constellation.js +12 -168
- package/dist/context-build-budgeted.js +4 -144
- package/dist/context-ranking.js +1 -69
- package/dist/crypto.js +1 -179
- package/dist/daemon-write-endpoint.js +1 -290
- package/dist/daemon-writer.js +2 -406
- package/dist/database.js +43 -1110
- package/dist/deprecations.js +2 -162
- package/dist/design.js +13 -141
- package/dist/event-replication.js +1 -112
- package/dist/events-sse.js +7 -43
- package/dist/events.js +6 -238
- package/dist/failure-patterns.js +42 -659
- package/dist/federation.js +12 -236
- package/dist/goals.js +13 -101
- package/dist/golden.js +3 -355
- package/dist/handlers/agent.js +4 -165
- package/dist/handlers/alias-adapters.js +1 -129
- package/dist/handlers/aliases.js +1 -171
- package/dist/handlers/audit.js +1 -87
- package/dist/handlers/boundary.js +1 -221
- package/dist/handlers/capture.js +73 -1109
- package/dist/handlers/causality.js +7 -114
- package/dist/handlers/cloud.js +85 -382
- package/dist/handlers/companion.js +28 -459
- package/dist/handlers/datalake.js +7 -187
- package/dist/handlers/dispatch-context.js +0 -22
- package/dist/handlers/entity.js +25 -256
- package/dist/handlers/events.js +16 -335
- package/dist/handlers/failure.js +13 -340
- package/dist/handlers/goals.js +4 -296
- package/dist/handlers/intelligence.js +126 -674
- package/dist/handlers/invoicing.js +1 -70
- package/dist/handlers/mcpclient.js +6 -137
- package/dist/handlers/orchestration.js +40 -125
- package/dist/handlers/output-schemas.js +1 -24
- package/dist/handlers/presence.js +3 -99
- package/dist/handlers/project.js +28 -182
- package/dist/handlers/prompts.js +6 -157
- package/dist/handlers/quest.js +4 -224
- package/dist/handlers/recall.js +11 -218
- package/dist/handlers/registry.js +1 -167
- package/dist/handlers/resources.js +1 -288
- package/dist/handlers/review.js +11 -74
- package/dist/handlers/run.js +17 -487
- package/dist/handlers/search.js +15 -326
- package/dist/handlers/session.js +28 -615
- package/dist/handlers/share.js +8 -184
- package/dist/handlers/shims.js +1 -464
- package/dist/handlers/skill.js +67 -449
- package/dist/handlers/survivors.js +1 -120
- package/dist/handlers/symbols.js +8 -109
- package/dist/handlers/syncops.js +4 -302
- package/dist/handlers/types.js +1 -27
- package/dist/harvest.js +5 -191
- package/dist/hours.js +7 -156
- package/dist/http-auth.js +3 -321
- package/dist/http-fast.js +21 -1137
- package/dist/icons.js +1 -47
- package/dist/index.js +2 -924
- package/dist/indexer.js +4 -145
- package/dist/intelligence.js +31 -261
- package/dist/internal-dispatch.js +3 -212
- package/dist/keyset.js +1 -110
- package/dist/knowledge-graph.js +12 -176
- package/dist/license.d.ts +11 -0
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +2 -414
- package/dist/license.js.map +1 -1
- package/dist/logger.js +2 -199
- package/dist/maintenance.js +2 -148
- package/dist/mcp-client.js +6 -262
- package/dist/memory-artifacts.js +30 -449
- package/dist/migrate-prompt.js +2 -124
- package/dist/migrations.js +40 -655
- package/dist/performance.js +1 -228
- package/dist/presence.js +11 -140
- package/dist/priority-embed.js +5 -164
- package/dist/providers/embedding-provider.js +1 -196
- package/dist/readonly-gate.js +1 -29
- package/dist/rehydration.js +9 -157
- package/dist/reindex.js +1 -88
- package/dist/render-target.js +21 -514
- package/dist/render.js +4 -280
- package/dist/repl-guard.js +1 -173
- package/dist/replication-daemon-entrypoint.js +1 -31
- package/dist/replication-daemon.js +2 -262
- package/dist/resilience.js +1 -591
- package/dist/reverse-bridge.js +5 -360
- package/dist/security.js +1 -244
- package/dist/session-seen.js +3 -51
- package/dist/setup.js +1 -260
- package/dist/skill-author.js +5 -168
- package/dist/spec-kit.js +1 -191
- package/dist/sqlite-busy.js +1 -154
- package/dist/statusline.js +11 -315
- package/dist/sub-agent.js +13 -262
- package/dist/summarizer.js +13 -139
- package/dist/symbols.js +7 -283
- package/dist/sync.js +5 -359
- package/dist/tasks-dispatch.js +1 -84
- package/dist/tasks.js +1 -282
- package/dist/token-budget.js +1 -143
- package/dist/tool-analytics.js +7 -129
- package/dist/tool-annotations.js +1 -365
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/tool-profiles.js +1 -75
- package/dist/trace-harvest.js +6 -244
- package/dist/types.js +1 -30
- package/dist/ui-dashboard.js +41 -50
- package/dist/ulid.js +1 -81
- package/dist/validate.js +1 -129
- package/dist/vault.js +1 -534
- package/dist/vectors.js +3 -184
- package/dist/version-check.js +4 -136
- package/dist/visibility.js +19 -155
- package/dist/wyrm-cli.js +98 -2451
- package/dist/wyrm-cli.js.map +1 -1
- package/dist/wyrm-guard.js +14 -424
- package/dist/wyrm-loop.js +3 -150
- package/dist/wyrm-manifest.json +1 -1
- package/dist/wyrm-statusline-daemon.js +1 -11
- package/dist/wyrm-statusline.js +4 -56
- package/dist/wyrm-ui.js +9 -77
- 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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
299
|
-
},
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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 ?${
|
|
148
|
+
WHERE skills_fts MATCH ?${n}
|
|
766
149
|
ORDER BY rank
|
|
767
150
|
LIMIT ?
|
|
768
|
-
`);
|
|
769
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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};
|