@viberlabs/orchestrator 2.1.0
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.md +25 -0
- package/README.md +170 -0
- package/dist/core/src/db/index.d.ts +295 -0
- package/dist/core/src/db/index.d.ts.map +1 -0
- package/dist/core/src/db/index.js +1114 -0
- package/dist/core/src/db/index.js.map +1 -0
- package/dist/core/src/engine/index.d.ts +38 -0
- package/dist/core/src/engine/index.d.ts.map +1 -0
- package/dist/core/src/engine/index.js +117 -0
- package/dist/core/src/engine/index.js.map +1 -0
- package/dist/core/src/engine/sentry.d.ts +36 -0
- package/dist/core/src/engine/sentry.d.ts.map +1 -0
- package/dist/core/src/engine/sentry.js +131 -0
- package/dist/core/src/engine/sentry.js.map +1 -0
- package/dist/core/src/protocol/index.d.ts +140 -0
- package/dist/core/src/protocol/index.d.ts.map +1 -0
- package/dist/core/src/protocol/index.js +7 -0
- package/dist/core/src/protocol/index.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/opencode/src/index.d.ts +56 -0
- package/dist/opencode/src/index.d.ts.map +1 -0
- package/dist/opencode/src/index.js +322 -0
- package/dist/opencode/src/index.js.map +1 -0
- package/dist/orca/index.d.ts +137 -0
- package/dist/orca/index.d.ts.map +1 -0
- package/dist/orca/index.js +215 -0
- package/dist/orca/index.js.map +1 -0
- package/dist/orchestrator/src/daemon.d.ts +66 -0
- package/dist/orchestrator/src/daemon.d.ts.map +1 -0
- package/dist/orchestrator/src/daemon.js +523 -0
- package/dist/orchestrator/src/daemon.js.map +1 -0
- package/dist/orchestrator/src/index.d.ts +8 -0
- package/dist/orchestrator/src/index.d.ts.map +1 -0
- package/dist/orchestrator/src/index.js +15 -0
- package/dist/orchestrator/src/index.js.map +1 -0
- package/dist/orchestrator/src/orca/index.d.ts +137 -0
- package/dist/orchestrator/src/orca/index.d.ts.map +1 -0
- package/dist/orchestrator/src/orca/index.js +215 -0
- package/dist/orchestrator/src/orca/index.js.map +1 -0
- package/dist/orchestrator/src/pool/index.d.ts +51 -0
- package/dist/orchestrator/src/pool/index.d.ts.map +1 -0
- package/dist/orchestrator/src/pool/index.js +152 -0
- package/dist/orchestrator/src/pool/index.js.map +1 -0
- package/dist/orchestrator/src/workflow/index.d.ts +65 -0
- package/dist/orchestrator/src/workflow/index.d.ts.map +1 -0
- package/dist/orchestrator/src/workflow/index.js +148 -0
- package/dist/orchestrator/src/workflow/index.js.map +1 -0
- package/dist/pool/index.d.ts +51 -0
- package/dist/pool/index.d.ts.map +1 -0
- package/dist/pool/index.js +152 -0
- package/dist/pool/index.js.map +1 -0
- package/dist/workflow/index.d.ts +65 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +148 -0
- package/dist/workflow/index.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,1114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @viberlabs/viber-core/db
|
|
4
|
+
* SQLite database layer for VIBER using sql.js (pure JS, no native dependencies)
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - ACID transactions for atomic operations
|
|
8
|
+
* - WAL mode optimization for better concurrency
|
|
9
|
+
* - Batch event inserts for performance
|
|
10
|
+
* - Agent pool restart recovery
|
|
11
|
+
* - OpenCode session tracking
|
|
12
|
+
* - Debounced persistence
|
|
13
|
+
*/
|
|
14
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.ViberDatabase = void 0;
|
|
19
|
+
const sql_js_1 = __importDefault(require("sql.js"));
|
|
20
|
+
const fs_1 = require("fs");
|
|
21
|
+
const path_1 = require("path");
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// ViberDatabase Class
|
|
24
|
+
// ============================================================================
|
|
25
|
+
class ViberDatabase {
|
|
26
|
+
db = null;
|
|
27
|
+
dbPath;
|
|
28
|
+
initialized = false;
|
|
29
|
+
saveTimer = null;
|
|
30
|
+
pendingSaves = 0;
|
|
31
|
+
constructor(dbPath = ".viber/state.db") {
|
|
32
|
+
this.dbPath = dbPath;
|
|
33
|
+
}
|
|
34
|
+
async initialize() {
|
|
35
|
+
if (this.initialized)
|
|
36
|
+
return;
|
|
37
|
+
const SQL = await (0, sql_js_1.default)();
|
|
38
|
+
const dbDir = (0, path_1.dirname)(this.dbPath);
|
|
39
|
+
if (!(0, fs_1.existsSync)(dbDir)) {
|
|
40
|
+
(0, fs_1.mkdirSync)(dbDir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
if ((0, fs_1.existsSync)(this.dbPath)) {
|
|
43
|
+
try {
|
|
44
|
+
const buffer = (0, fs_1.readFileSync)(this.dbPath);
|
|
45
|
+
this.db = new SQL.Database(buffer);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
console.warn("[ViberDatabase] Failed to load database, creating new one:", error);
|
|
49
|
+
this.db = new SQL.Database();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
this.db = new SQL.Database();
|
|
54
|
+
}
|
|
55
|
+
await this.runMigrations();
|
|
56
|
+
this.configurePragmas();
|
|
57
|
+
this.initialized = true;
|
|
58
|
+
}
|
|
59
|
+
async runMigrations() {
|
|
60
|
+
if (!this.db)
|
|
61
|
+
throw new Error("Database not initialized");
|
|
62
|
+
this.ensureSchemaVersionTable();
|
|
63
|
+
const currentVersion = this.getSchemaVersion();
|
|
64
|
+
const migrations = [
|
|
65
|
+
{
|
|
66
|
+
version: 1,
|
|
67
|
+
name: "initial schema",
|
|
68
|
+
run: () => {
|
|
69
|
+
// NOTE: This is the oldest supported schema. New columns are added via later migrations.
|
|
70
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS tasks (
|
|
71
|
+
id TEXT PRIMARY KEY, title TEXT NOT NULL, description TEXT NOT NULL,
|
|
72
|
+
status TEXT NOT NULL DEFAULT 'backlog', logic_status TEXT NOT NULL DEFAULT 'valley',
|
|
73
|
+
probability REAL NOT NULL DEFAULT 0.5, project_id TEXT NOT NULL, session_id TEXT,
|
|
74
|
+
created_at TEXT NOT NULL, updated_at TEXT NOT NULL, result TEXT, error TEXT, metadata TEXT
|
|
75
|
+
)`);
|
|
76
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS agents (
|
|
77
|
+
id TEXT PRIMARY KEY, task_id TEXT NOT NULL, role TEXT NOT NULL, status TEXT NOT NULL,
|
|
78
|
+
opencode_session_id TEXT, model TEXT, started_at TEXT NOT NULL,
|
|
79
|
+
completed_at TEXT, duration_ms INTEGER, input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
80
|
+
output_tokens INTEGER NOT NULL DEFAULT 0, cost_usd REAL NOT NULL DEFAULT 0,
|
|
81
|
+
result TEXT, depends_on TEXT
|
|
82
|
+
)`);
|
|
83
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS sessions (
|
|
84
|
+
id TEXT PRIMARY KEY, project_id TEXT NOT NULL, status TEXT NOT NULL,
|
|
85
|
+
started_at TEXT NOT NULL, ended_at TEXT, handoff_path TEXT NOT NULL DEFAULT 'HANDOFF.md',
|
|
86
|
+
tasks_completed INTEGER NOT NULL DEFAULT 0, agents_spawned INTEGER NOT NULL DEFAULT 0
|
|
87
|
+
)`);
|
|
88
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS session_notes (
|
|
89
|
+
id TEXT PRIMARY KEY, session_id TEXT NOT NULL, type TEXT NOT NULL, content TEXT NOT NULL, created_at TEXT NOT NULL
|
|
90
|
+
)`);
|
|
91
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS events (
|
|
92
|
+
id TEXT PRIMARY KEY, entity_type TEXT NOT NULL, entity_id TEXT NOT NULL,
|
|
93
|
+
event_type TEXT NOT NULL, data TEXT, timestamp TEXT NOT NULL
|
|
94
|
+
)`);
|
|
95
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS premises (
|
|
96
|
+
id TEXT PRIMARY KEY, task_id TEXT NOT NULL, statement TEXT NOT NULL, logic_var TEXT NOT NULL,
|
|
97
|
+
is_verified INTEGER NOT NULL DEFAULT 0, is_critical INTEGER NOT NULL DEFAULT 1, confidence REAL NOT NULL DEFAULT 0.5,
|
|
98
|
+
evidence TEXT, created_at TEXT NOT NULL
|
|
99
|
+
)`);
|
|
100
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS ip_assets (
|
|
101
|
+
id TEXT PRIMARY KEY, project_id TEXT NOT NULL, name TEXT NOT NULL, type TEXT NOT NULL,
|
|
102
|
+
description TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'identified',
|
|
103
|
+
created_at TEXT NOT NULL, updated_at TEXT NOT NULL, file_date TEXT, grant_date TEXT, metadata TEXT
|
|
104
|
+
)`);
|
|
105
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS ip_milestones (
|
|
106
|
+
id TEXT PRIMARY KEY, ip_asset_id TEXT NOT NULL, name TEXT NOT NULL, description TEXT NOT NULL,
|
|
107
|
+
status TEXT NOT NULL DEFAULT 'pending', created_at TEXT NOT NULL, completed_at TEXT, evidence TEXT
|
|
108
|
+
)`);
|
|
109
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS projects (
|
|
110
|
+
id TEXT PRIMARY KEY,
|
|
111
|
+
name TEXT NOT NULL,
|
|
112
|
+
created_at TEXT NOT NULL,
|
|
113
|
+
updated_at TEXT NOT NULL
|
|
114
|
+
)`);
|
|
115
|
+
// Indexes that only rely on v1 columns
|
|
116
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)`);
|
|
117
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id)`);
|
|
118
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id)`);
|
|
119
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_agents_task ON agents(task_id)`);
|
|
120
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status)`);
|
|
121
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_events_entity ON events(entity_type, entity_id)`);
|
|
122
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp)`);
|
|
123
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status)`);
|
|
124
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id)`);
|
|
125
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_session_notes_session ON session_notes(session_id)`);
|
|
126
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_premises_task ON premises(task_id)`);
|
|
127
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_premises_verified ON premises(is_verified)`);
|
|
128
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_ip_assets_project ON ip_assets(project_id)`);
|
|
129
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_ip_assets_status ON ip_assets(status)`);
|
|
130
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_ip_milestones_asset ON ip_milestones(ip_asset_id)`);
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
version: 2,
|
|
135
|
+
name: "tasks: add priority",
|
|
136
|
+
run: () => {
|
|
137
|
+
this.addColumnIfMissing("tasks", "priority", `priority TEXT NOT NULL DEFAULT 'P2'`);
|
|
138
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority)`);
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
version: 3,
|
|
143
|
+
name: "sessions: add task_id",
|
|
144
|
+
run: () => {
|
|
145
|
+
this.addColumnIfMissing("sessions", "task_id", `task_id TEXT`);
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
version: 4,
|
|
150
|
+
name: "projects: add vision_path + metadata",
|
|
151
|
+
run: () => {
|
|
152
|
+
this.addColumnIfMissing("projects", "vision_path", `vision_path TEXT NOT NULL DEFAULT ''`);
|
|
153
|
+
this.addColumnIfMissing("projects", "metadata", `metadata TEXT`);
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
version: 5,
|
|
158
|
+
name: "agents: add provider/error/timestamps",
|
|
159
|
+
run: () => {
|
|
160
|
+
this.addColumnIfMissing("agents", "provider", `provider TEXT`);
|
|
161
|
+
this.addColumnIfMissing("agents", "error", `error TEXT`);
|
|
162
|
+
this.addColumnIfMissing("agents", "created_at", `created_at TEXT NOT NULL DEFAULT ''`);
|
|
163
|
+
this.addColumnIfMissing("agents", "updated_at", `updated_at TEXT NOT NULL DEFAULT ''`);
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
version: 6,
|
|
168
|
+
name: "seed: default project",
|
|
169
|
+
run: () => {
|
|
170
|
+
const now = new Date().toISOString();
|
|
171
|
+
// After v4, projects has the full column set.
|
|
172
|
+
this.db.run(`INSERT OR IGNORE INTO projects (id, name, vision_path, created_at, updated_at, metadata) VALUES (?, ?, ?, ?, ?, ?)`, ["default", "VIBER Project", "", now, now, null]);
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
for (const m of migrations) {
|
|
177
|
+
if (m.version <= currentVersion)
|
|
178
|
+
continue;
|
|
179
|
+
m.run();
|
|
180
|
+
this.setSchemaVersion(m.version);
|
|
181
|
+
}
|
|
182
|
+
// Migrations must persist immediately so we never re-run them on next startup.
|
|
183
|
+
this.persistNow();
|
|
184
|
+
}
|
|
185
|
+
persistNow() {
|
|
186
|
+
if (!this.db)
|
|
187
|
+
throw new Error("Database not initialized");
|
|
188
|
+
const data = this.db.export();
|
|
189
|
+
(0, fs_1.writeFileSync)(this.dbPath, Buffer.from(data));
|
|
190
|
+
this.pendingSaves = 0;
|
|
191
|
+
if (this.saveTimer) {
|
|
192
|
+
clearTimeout(this.saveTimer);
|
|
193
|
+
this.saveTimer = null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
ensureSchemaVersionTable() {
|
|
197
|
+
if (!this.db)
|
|
198
|
+
throw new Error("Database not initialized");
|
|
199
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS main.schema_version (
|
|
200
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
201
|
+
version INTEGER NOT NULL
|
|
202
|
+
)`);
|
|
203
|
+
this.db.run(`INSERT OR IGNORE INTO main.schema_version (id, version) VALUES (1, 0)`);
|
|
204
|
+
const verify = this.db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'");
|
|
205
|
+
if (verify.length === 0 || verify[0].values.length === 0) {
|
|
206
|
+
throw new Error("[ViberDatabase] schema_version table missing after creation");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
getSchemaVersion() {
|
|
210
|
+
if (!this.db)
|
|
211
|
+
throw new Error("Database not initialized");
|
|
212
|
+
const result = this.db.exec(`SELECT version FROM main.schema_version WHERE id = 1`);
|
|
213
|
+
if (result.length === 0 || result[0].values.length === 0)
|
|
214
|
+
return 0;
|
|
215
|
+
const raw = result[0].values[0]?.[0];
|
|
216
|
+
const n = typeof raw === "number" ? raw : Number(raw);
|
|
217
|
+
return Number.isFinite(n) ? n : 0;
|
|
218
|
+
}
|
|
219
|
+
setSchemaVersion(version) {
|
|
220
|
+
if (!this.db)
|
|
221
|
+
throw new Error("Database not initialized");
|
|
222
|
+
this.db.run(`UPDATE main.schema_version SET version = ? WHERE id = 1`, [
|
|
223
|
+
version,
|
|
224
|
+
]);
|
|
225
|
+
}
|
|
226
|
+
addColumnIfMissing(tableName, columnName, columnSql) {
|
|
227
|
+
if (!this.db)
|
|
228
|
+
throw new Error("Database not initialized");
|
|
229
|
+
if (!this.tableHasColumn(tableName, columnName)) {
|
|
230
|
+
this.db.run(`ALTER TABLE ${tableName} ADD COLUMN ${columnSql}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
tableHasColumn(tableName, columnName) {
|
|
234
|
+
if (!this.db)
|
|
235
|
+
throw new Error("Database not initialized");
|
|
236
|
+
const safeTable = tableName.replace(/'/g, "''");
|
|
237
|
+
const safeCol = columnName.replace(/'/g, "''");
|
|
238
|
+
// Use pragma_table_info() (table-valued) instead of PRAGMA table_info,
|
|
239
|
+
// because it behaves like a normal SELECT across environments.
|
|
240
|
+
const result = this.db.exec(`SELECT 1 FROM pragma_table_info('${safeTable}') WHERE name = '${safeCol}' LIMIT 1`);
|
|
241
|
+
return result.length > 0 && result[0].values.length > 0;
|
|
242
|
+
}
|
|
243
|
+
configurePragmas() {
|
|
244
|
+
this.db.run(`PRAGMA foreign_keys = ON`);
|
|
245
|
+
this.db.run(`PRAGMA optimize`);
|
|
246
|
+
}
|
|
247
|
+
// Persistence with debouncing
|
|
248
|
+
save() {
|
|
249
|
+
this.pendingSaves++;
|
|
250
|
+
this.debouncedSave();
|
|
251
|
+
}
|
|
252
|
+
debouncedSave() {
|
|
253
|
+
if (this.saveTimer)
|
|
254
|
+
clearTimeout(this.saveTimer);
|
|
255
|
+
this.saveTimer = setTimeout(() => this.flush(), 100);
|
|
256
|
+
}
|
|
257
|
+
flush() {
|
|
258
|
+
if (!this.db || this.pendingSaves === 0)
|
|
259
|
+
return;
|
|
260
|
+
try {
|
|
261
|
+
const data = this.db.export();
|
|
262
|
+
(0, fs_1.writeFileSync)(this.dbPath, Buffer.from(data));
|
|
263
|
+
this.pendingSaves = 0;
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
console.error("[ViberDatabase] Failed to save:", error);
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Transactions
|
|
271
|
+
async transaction(callback) {
|
|
272
|
+
if (!this.db)
|
|
273
|
+
await this.initialize();
|
|
274
|
+
this.db.run("BEGIN TRANSACTION");
|
|
275
|
+
try {
|
|
276
|
+
const result = await callback(this);
|
|
277
|
+
this.db.run("COMMIT");
|
|
278
|
+
this.save();
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
this.db.run("ROLLBACK");
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Batch operations
|
|
287
|
+
async batchInsertEvents(events) {
|
|
288
|
+
if (!this.db)
|
|
289
|
+
await this.initialize();
|
|
290
|
+
if (events.length === 0)
|
|
291
|
+
return;
|
|
292
|
+
const stmt = this.db.prepare(`INSERT INTO events (id, entity_type, entity_id, event_type, data, timestamp) VALUES (?, ?, ?, ?, ?, ?)`);
|
|
293
|
+
for (const event of events) {
|
|
294
|
+
stmt.run([
|
|
295
|
+
event.id,
|
|
296
|
+
event.entityType,
|
|
297
|
+
event.entityId,
|
|
298
|
+
event.eventType,
|
|
299
|
+
event.data || null,
|
|
300
|
+
event.timestamp,
|
|
301
|
+
]);
|
|
302
|
+
}
|
|
303
|
+
stmt.free();
|
|
304
|
+
this.save();
|
|
305
|
+
}
|
|
306
|
+
// Task operations
|
|
307
|
+
async clearTasks() {
|
|
308
|
+
if (!this.db)
|
|
309
|
+
await this.initialize();
|
|
310
|
+
this.db.run(`DELETE FROM tasks`);
|
|
311
|
+
this.save();
|
|
312
|
+
}
|
|
313
|
+
async createTask(task) {
|
|
314
|
+
if (!this.db)
|
|
315
|
+
await this.initialize();
|
|
316
|
+
const now = new Date().toISOString();
|
|
317
|
+
const record = {
|
|
318
|
+
...task,
|
|
319
|
+
status: task.status || "backlog",
|
|
320
|
+
priority: task.priority || "P2",
|
|
321
|
+
logicStatus: task.logicStatus || "valley",
|
|
322
|
+
probability: task.probability || 0.5,
|
|
323
|
+
createdAt: now,
|
|
324
|
+
updatedAt: now,
|
|
325
|
+
};
|
|
326
|
+
this.db.run(`INSERT INTO tasks (id, title, description, status, priority, logic_status, probability, project_id, session_id, created_at, updated_at, metadata)
|
|
327
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
328
|
+
record.id,
|
|
329
|
+
record.title,
|
|
330
|
+
record.description,
|
|
331
|
+
record.status,
|
|
332
|
+
record.priority,
|
|
333
|
+
record.logicStatus,
|
|
334
|
+
record.probability,
|
|
335
|
+
record.projectId,
|
|
336
|
+
record.sessionId || null,
|
|
337
|
+
record.createdAt,
|
|
338
|
+
record.updatedAt,
|
|
339
|
+
record.metadata || null,
|
|
340
|
+
]);
|
|
341
|
+
this.save();
|
|
342
|
+
return record;
|
|
343
|
+
}
|
|
344
|
+
async getTask(id) {
|
|
345
|
+
if (!this.db)
|
|
346
|
+
await this.initialize();
|
|
347
|
+
const result = this.db.exec(`SELECT id, title, description, status, priority, logic_status, probability, project_id, session_id, created_at, updated_at, result, error, metadata
|
|
348
|
+
FROM tasks WHERE id = ?`, [id]);
|
|
349
|
+
if (result.length === 0 || result[0].values.length === 0)
|
|
350
|
+
return undefined;
|
|
351
|
+
return this.rowToTask(result[0].values[0]);
|
|
352
|
+
}
|
|
353
|
+
async getTasks(filter) {
|
|
354
|
+
if (!this.db)
|
|
355
|
+
await this.initialize();
|
|
356
|
+
let query = `SELECT id, title, description, status, priority, logic_status, probability, project_id, session_id, created_at, updated_at, result, error, metadata FROM tasks`;
|
|
357
|
+
const params = [];
|
|
358
|
+
if (filter) {
|
|
359
|
+
const conditions = [];
|
|
360
|
+
if (filter.status) {
|
|
361
|
+
conditions.push("status = ?");
|
|
362
|
+
params.push(filter.status);
|
|
363
|
+
}
|
|
364
|
+
if (filter.projectId) {
|
|
365
|
+
conditions.push("project_id = ?");
|
|
366
|
+
params.push(filter.projectId);
|
|
367
|
+
}
|
|
368
|
+
if (filter.sessionId) {
|
|
369
|
+
conditions.push("session_id = ?");
|
|
370
|
+
params.push(filter.sessionId);
|
|
371
|
+
}
|
|
372
|
+
if (conditions.length > 0)
|
|
373
|
+
query += ` WHERE ${conditions.join(" AND ")}`;
|
|
374
|
+
}
|
|
375
|
+
query += ` ORDER BY created_at DESC`;
|
|
376
|
+
const result = this.db.exec(query, params);
|
|
377
|
+
if (result.length === 0)
|
|
378
|
+
return [];
|
|
379
|
+
return result[0].values.map((row) => this.rowToTask(row));
|
|
380
|
+
}
|
|
381
|
+
async getEligibleTasksForScheduling(args) {
|
|
382
|
+
if (!this.db)
|
|
383
|
+
await this.initialize();
|
|
384
|
+
const allowSlope = Boolean(args.allowSlope);
|
|
385
|
+
const logicStatuses = allowSlope ? ["peak", "slope"] : ["peak"];
|
|
386
|
+
let query = `SELECT id, title, description, status, priority, logic_status, probability, project_id, session_id, created_at, updated_at, result, error, metadata
|
|
387
|
+
FROM tasks WHERE status = ? AND logic_status IN (${logicStatuses
|
|
388
|
+
.map(() => "?")
|
|
389
|
+
.join(", ")})`;
|
|
390
|
+
const params = ["backlog", ...logicStatuses];
|
|
391
|
+
if (args.projectId) {
|
|
392
|
+
query += ` AND project_id = ?`;
|
|
393
|
+
params.push(args.projectId);
|
|
394
|
+
}
|
|
395
|
+
query += ` ORDER BY CASE priority
|
|
396
|
+
WHEN 'P0' THEN 0
|
|
397
|
+
WHEN 'P1' THEN 1
|
|
398
|
+
WHEN 'P2' THEN 2
|
|
399
|
+
WHEN 'P3' THEN 3
|
|
400
|
+
WHEN 'P4' THEN 4
|
|
401
|
+
ELSE 5 END, updated_at ASC`;
|
|
402
|
+
const result = this.db.exec(query, params);
|
|
403
|
+
if (result.length === 0)
|
|
404
|
+
return [];
|
|
405
|
+
return result[0].values.map((row) => this.rowToTask(row));
|
|
406
|
+
}
|
|
407
|
+
async taskHasAgents(taskId) {
|
|
408
|
+
if (!this.db)
|
|
409
|
+
await this.initialize();
|
|
410
|
+
const result = this.db.exec(`SELECT 1 FROM agents WHERE task_id = ? LIMIT 1`, [taskId]);
|
|
411
|
+
return result.length > 0 && result[0].values.length > 0;
|
|
412
|
+
}
|
|
413
|
+
async updateTask(id, updates) {
|
|
414
|
+
if (!this.db)
|
|
415
|
+
await this.initialize();
|
|
416
|
+
updates.updatedAt = new Date().toISOString();
|
|
417
|
+
const setClause = Object.keys(updates)
|
|
418
|
+
.map((k) => this.camelToSnake(k) + " = ?")
|
|
419
|
+
.join(", ");
|
|
420
|
+
const values = [...Object.values(updates), id];
|
|
421
|
+
this.db.run(`UPDATE tasks SET ${setClause} WHERE id = ?`, values);
|
|
422
|
+
this.save();
|
|
423
|
+
const task = await this.getTask(id);
|
|
424
|
+
if (!task)
|
|
425
|
+
throw new Error(`Task ${id} not found`);
|
|
426
|
+
return task;
|
|
427
|
+
}
|
|
428
|
+
// Premise operations
|
|
429
|
+
async clearPremises() {
|
|
430
|
+
if (!this.db)
|
|
431
|
+
await this.initialize();
|
|
432
|
+
this.db.run(`DELETE FROM premises`);
|
|
433
|
+
this.save();
|
|
434
|
+
}
|
|
435
|
+
async replacePremisesForTask(taskId, premises) {
|
|
436
|
+
if (!this.db)
|
|
437
|
+
await this.initialize();
|
|
438
|
+
this.db.run(`DELETE FROM premises WHERE task_id = ?`, [taskId]);
|
|
439
|
+
if (premises.length === 0) {
|
|
440
|
+
this.save();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const stmt = this.db.prepare(`INSERT INTO premises (id, task_id, statement, logic_var, is_verified, is_critical, confidence, evidence, created_at)
|
|
444
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
445
|
+
const now = new Date().toISOString();
|
|
446
|
+
for (const p of premises) {
|
|
447
|
+
stmt.run([
|
|
448
|
+
p.id,
|
|
449
|
+
p.taskId,
|
|
450
|
+
p.statement,
|
|
451
|
+
p.logicVar,
|
|
452
|
+
p.isVerified ? 1 : 0,
|
|
453
|
+
p.isCritical ? 1 : 0,
|
|
454
|
+
p.confidence,
|
|
455
|
+
p.evidence ?? null,
|
|
456
|
+
now,
|
|
457
|
+
]);
|
|
458
|
+
}
|
|
459
|
+
stmt.free();
|
|
460
|
+
this.save();
|
|
461
|
+
}
|
|
462
|
+
async getPremises(taskId) {
|
|
463
|
+
if (!this.db)
|
|
464
|
+
await this.initialize();
|
|
465
|
+
const result = this.db.exec(`SELECT id, task_id, statement, logic_var, is_verified, is_critical, confidence, evidence, created_at
|
|
466
|
+
FROM premises WHERE task_id = ? ORDER BY created_at DESC`, [taskId]);
|
|
467
|
+
if (result.length === 0)
|
|
468
|
+
return [];
|
|
469
|
+
return result[0].values.map((row) => this.rowToPremise(row));
|
|
470
|
+
}
|
|
471
|
+
// Agent operations
|
|
472
|
+
async clearAgents() {
|
|
473
|
+
if (!this.db)
|
|
474
|
+
await this.initialize();
|
|
475
|
+
this.db.run(`DELETE FROM agents`);
|
|
476
|
+
this.save();
|
|
477
|
+
}
|
|
478
|
+
async createAgent(agent) {
|
|
479
|
+
if (!this.db)
|
|
480
|
+
await this.initialize();
|
|
481
|
+
const now = new Date().toISOString();
|
|
482
|
+
const record = {
|
|
483
|
+
...agent,
|
|
484
|
+
status: agent.status || "pending",
|
|
485
|
+
startedAt: now,
|
|
486
|
+
inputTokens: 0,
|
|
487
|
+
outputTokens: 0,
|
|
488
|
+
costUsd: 0,
|
|
489
|
+
};
|
|
490
|
+
this.db.run(`INSERT INTO agents (id, task_id, role, status, opencode_session_id, model, provider, started_at, depends_on, created_at, updated_at)
|
|
491
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
492
|
+
record.id,
|
|
493
|
+
record.taskId,
|
|
494
|
+
record.role,
|
|
495
|
+
record.status,
|
|
496
|
+
record.opencodeSessionId || null,
|
|
497
|
+
record.model || null,
|
|
498
|
+
record.provider || null,
|
|
499
|
+
record.startedAt,
|
|
500
|
+
record.dependsOn || null,
|
|
501
|
+
now,
|
|
502
|
+
now,
|
|
503
|
+
]);
|
|
504
|
+
this.save();
|
|
505
|
+
return record;
|
|
506
|
+
}
|
|
507
|
+
async getAgent(id) {
|
|
508
|
+
if (!this.db)
|
|
509
|
+
await this.initialize();
|
|
510
|
+
const result = this.db.exec(`SELECT id, task_id, role, status, opencode_session_id, model, provider, started_at, completed_at, duration_ms, input_tokens, output_tokens, cost_usd, result, error, depends_on
|
|
511
|
+
FROM agents WHERE id = ?`, [id]);
|
|
512
|
+
if (result.length === 0 || result[0].values.length === 0)
|
|
513
|
+
return undefined;
|
|
514
|
+
return this.rowToAgent(result[0].values[0]);
|
|
515
|
+
}
|
|
516
|
+
async getAgents(filter) {
|
|
517
|
+
if (!this.db)
|
|
518
|
+
await this.initialize();
|
|
519
|
+
let query = `SELECT id, task_id, role, status, opencode_session_id, model, provider, started_at, completed_at, duration_ms, input_tokens, output_tokens, cost_usd, result, error, depends_on FROM agents`;
|
|
520
|
+
const params = [];
|
|
521
|
+
if (filter) {
|
|
522
|
+
const conditions = [];
|
|
523
|
+
if (filter.taskId) {
|
|
524
|
+
conditions.push("task_id = ?");
|
|
525
|
+
params.push(filter.taskId);
|
|
526
|
+
}
|
|
527
|
+
if (filter.status) {
|
|
528
|
+
if (Array.isArray(filter.status)) {
|
|
529
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(", ")})`);
|
|
530
|
+
params.push(...filter.status);
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
conditions.push("status = ?");
|
|
534
|
+
params.push(filter.status);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (conditions.length > 0)
|
|
538
|
+
query += ` WHERE ${conditions.join(" AND ")}`;
|
|
539
|
+
}
|
|
540
|
+
query += ` ORDER BY started_at DESC`;
|
|
541
|
+
const result = this.db.exec(query, params);
|
|
542
|
+
if (result.length === 0)
|
|
543
|
+
return [];
|
|
544
|
+
return result[0].values.map((row) => this.rowToAgent(row));
|
|
545
|
+
}
|
|
546
|
+
async updateAgent(id, updates) {
|
|
547
|
+
if (!this.db)
|
|
548
|
+
await this.initialize();
|
|
549
|
+
// sql.js cannot bind `undefined`. Treat undefined as "no update".
|
|
550
|
+
updates.updatedAt = new Date().toISOString();
|
|
551
|
+
const entries = Object.entries(updates).filter(([, v]) => v !== undefined);
|
|
552
|
+
if (entries.length === 0) {
|
|
553
|
+
const agent = await this.getAgent(id);
|
|
554
|
+
if (!agent)
|
|
555
|
+
throw new Error(`Agent ${id} not found`);
|
|
556
|
+
return agent;
|
|
557
|
+
}
|
|
558
|
+
const setClause = entries
|
|
559
|
+
.map(([k]) => this.camelToSnake(k) + " = ?")
|
|
560
|
+
.join(", ");
|
|
561
|
+
const values = [...entries.map(([, v]) => v), id];
|
|
562
|
+
this.db.run(`UPDATE agents SET ${setClause} WHERE id = ?`, values);
|
|
563
|
+
this.save();
|
|
564
|
+
const agent = await this.getAgent(id);
|
|
565
|
+
if (!agent)
|
|
566
|
+
throw new Error(`Agent ${id} not found`);
|
|
567
|
+
return agent;
|
|
568
|
+
}
|
|
569
|
+
// Session operations
|
|
570
|
+
async createSession(session) {
|
|
571
|
+
if (!this.db)
|
|
572
|
+
await this.initialize();
|
|
573
|
+
const now = new Date().toISOString();
|
|
574
|
+
const record = {
|
|
575
|
+
...session,
|
|
576
|
+
status: "active",
|
|
577
|
+
startedAt: now,
|
|
578
|
+
handoffPath: "HANDOFF.md",
|
|
579
|
+
tasksCompleted: 0,
|
|
580
|
+
agentsSpawned: 0,
|
|
581
|
+
};
|
|
582
|
+
this.db.run(`INSERT INTO sessions (id, project_id, status, task_id, started_at, handoff_path, tasks_completed, agents_spawned)
|
|
583
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
584
|
+
record.id,
|
|
585
|
+
record.projectId,
|
|
586
|
+
record.status,
|
|
587
|
+
record.taskId || null,
|
|
588
|
+
record.startedAt,
|
|
589
|
+
record.handoffPath,
|
|
590
|
+
record.tasksCompleted,
|
|
591
|
+
record.agentsSpawned,
|
|
592
|
+
]);
|
|
593
|
+
this.save();
|
|
594
|
+
return record;
|
|
595
|
+
}
|
|
596
|
+
async getSession(id) {
|
|
597
|
+
if (!this.db)
|
|
598
|
+
await this.initialize();
|
|
599
|
+
const result = this.db.exec(`SELECT id, project_id, status, task_id, started_at, ended_at, handoff_path, tasks_completed, agents_spawned FROM sessions WHERE id = ?`, [id]);
|
|
600
|
+
if (result.length === 0 || result[0].values.length === 0)
|
|
601
|
+
return undefined;
|
|
602
|
+
return this.rowToSession(result[0].values[0]);
|
|
603
|
+
}
|
|
604
|
+
async getSessions(filter) {
|
|
605
|
+
if (!this.db)
|
|
606
|
+
await this.initialize();
|
|
607
|
+
let query = `SELECT id, project_id, status, task_id, started_at, ended_at, handoff_path, tasks_completed, agents_spawned FROM sessions`;
|
|
608
|
+
const params = [];
|
|
609
|
+
if (filter) {
|
|
610
|
+
const conditions = [];
|
|
611
|
+
if (filter.status) {
|
|
612
|
+
conditions.push("status = ?");
|
|
613
|
+
params.push(filter.status);
|
|
614
|
+
}
|
|
615
|
+
if (filter.projectId) {
|
|
616
|
+
conditions.push("project_id = ?");
|
|
617
|
+
params.push(filter.projectId);
|
|
618
|
+
}
|
|
619
|
+
if (conditions.length > 0)
|
|
620
|
+
query += ` WHERE ${conditions.join(" AND ")}`;
|
|
621
|
+
}
|
|
622
|
+
query += ` ORDER BY started_at DESC`;
|
|
623
|
+
const result = this.db.exec(query, params);
|
|
624
|
+
if (result.length === 0)
|
|
625
|
+
return [];
|
|
626
|
+
return result[0].values.map((row) => this.rowToSession(row));
|
|
627
|
+
}
|
|
628
|
+
async getActiveSession(projectId) {
|
|
629
|
+
if (!this.db)
|
|
630
|
+
await this.initialize();
|
|
631
|
+
let query = `SELECT id, project_id, status, task_id, started_at, ended_at, handoff_path, tasks_completed, agents_spawned FROM sessions WHERE status = 'active'`;
|
|
632
|
+
const params = [];
|
|
633
|
+
if (projectId) {
|
|
634
|
+
query += ` AND project_id = ?`;
|
|
635
|
+
params.push(projectId);
|
|
636
|
+
}
|
|
637
|
+
query += ` ORDER BY started_at DESC LIMIT 1`;
|
|
638
|
+
const result = this.db.exec(query, params);
|
|
639
|
+
if (result.length === 0 || result[0].values.length === 0)
|
|
640
|
+
return undefined;
|
|
641
|
+
return this.rowToSession(result[0].values[0]);
|
|
642
|
+
}
|
|
643
|
+
async updateSession(id, updates) {
|
|
644
|
+
if (!this.db)
|
|
645
|
+
await this.initialize();
|
|
646
|
+
const setClause = Object.keys(updates)
|
|
647
|
+
.map((k) => this.camelToSnake(k) + " = ?")
|
|
648
|
+
.join(", ");
|
|
649
|
+
const values = [...Object.values(updates), id];
|
|
650
|
+
this.db.run(`UPDATE sessions SET ${setClause} WHERE id = ?`, values);
|
|
651
|
+
this.save();
|
|
652
|
+
const session = await this.getSession(id);
|
|
653
|
+
if (!session)
|
|
654
|
+
throw new Error(`Session ${id} not found`);
|
|
655
|
+
return session;
|
|
656
|
+
}
|
|
657
|
+
async endSession(sessionId, summary) {
|
|
658
|
+
if (!this.db)
|
|
659
|
+
await this.initialize();
|
|
660
|
+
const now = new Date().toISOString();
|
|
661
|
+
this.db.run(`UPDATE sessions SET status = ?, ended_at = ? WHERE id = ?`, [
|
|
662
|
+
"completed",
|
|
663
|
+
now,
|
|
664
|
+
sessionId,
|
|
665
|
+
]);
|
|
666
|
+
if (summary) {
|
|
667
|
+
this.db.run(`INSERT INTO session_notes (id, session_id, type, content, created_at) VALUES (?, ?, ?, ?, ?)`, [`note-${Date.now()}`, sessionId, "critical", summary, now]);
|
|
668
|
+
}
|
|
669
|
+
this.save();
|
|
670
|
+
const session = await this.getSession(sessionId);
|
|
671
|
+
if (!session)
|
|
672
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
673
|
+
return session;
|
|
674
|
+
}
|
|
675
|
+
async createSessionNote(note) {
|
|
676
|
+
if (!this.db)
|
|
677
|
+
await this.initialize();
|
|
678
|
+
const now = new Date().toISOString();
|
|
679
|
+
this.db.run(`INSERT INTO session_notes (id, session_id, type, content, created_at) VALUES (?, ?, ?, ?, ?)`, [note.id, note.sessionId, note.type, note.content, now]);
|
|
680
|
+
this.save();
|
|
681
|
+
return { ...note, createdAt: now };
|
|
682
|
+
}
|
|
683
|
+
async getSessionNotes(sessionId) {
|
|
684
|
+
if (!this.db)
|
|
685
|
+
await this.initialize();
|
|
686
|
+
const result = this.db.exec(`SELECT id, session_id, type, content, created_at FROM session_notes WHERE session_id = ? ORDER BY created_at DESC`, [sessionId]);
|
|
687
|
+
if (result.length === 0)
|
|
688
|
+
return [];
|
|
689
|
+
return result[0].values.map((row) => ({
|
|
690
|
+
id: row[0],
|
|
691
|
+
sessionId: row[1],
|
|
692
|
+
type: row[2],
|
|
693
|
+
content: row[3],
|
|
694
|
+
createdAt: row[4],
|
|
695
|
+
}));
|
|
696
|
+
}
|
|
697
|
+
// Event operations
|
|
698
|
+
async clearEvents() {
|
|
699
|
+
if (!this.db)
|
|
700
|
+
await this.initialize();
|
|
701
|
+
this.db.run(`DELETE FROM events`);
|
|
702
|
+
this.save();
|
|
703
|
+
}
|
|
704
|
+
async createEvent(event) {
|
|
705
|
+
if (!this.db)
|
|
706
|
+
await this.initialize();
|
|
707
|
+
this.db.run(`INSERT INTO events (id, entity_type, entity_id, event_type, data, timestamp) VALUES (?, ?, ?, ?, ?, ?)`, [
|
|
708
|
+
event.id,
|
|
709
|
+
event.entityType,
|
|
710
|
+
event.entityId,
|
|
711
|
+
event.eventType,
|
|
712
|
+
event.data || null,
|
|
713
|
+
event.timestamp,
|
|
714
|
+
]);
|
|
715
|
+
this.save();
|
|
716
|
+
return event;
|
|
717
|
+
}
|
|
718
|
+
async getEvents(filter) {
|
|
719
|
+
if (!this.db)
|
|
720
|
+
await this.initialize();
|
|
721
|
+
let query = `SELECT * FROM events`;
|
|
722
|
+
const params = [];
|
|
723
|
+
const conditions = [];
|
|
724
|
+
if (filter) {
|
|
725
|
+
if (filter.entityType) {
|
|
726
|
+
conditions.push("entity_type = ?");
|
|
727
|
+
params.push(filter.entityType);
|
|
728
|
+
}
|
|
729
|
+
if (filter.entityId) {
|
|
730
|
+
conditions.push("entity_id = ?");
|
|
731
|
+
params.push(filter.entityId);
|
|
732
|
+
}
|
|
733
|
+
if (filter.since) {
|
|
734
|
+
conditions.push("timestamp > ?");
|
|
735
|
+
params.push(filter.since);
|
|
736
|
+
}
|
|
737
|
+
if (conditions.length > 0)
|
|
738
|
+
query += ` WHERE ${conditions.join(" AND ")}`;
|
|
739
|
+
}
|
|
740
|
+
query += ` ORDER BY timestamp DESC`;
|
|
741
|
+
if (filter?.limit) {
|
|
742
|
+
query += ` LIMIT ?`;
|
|
743
|
+
params.push(filter.limit);
|
|
744
|
+
}
|
|
745
|
+
const result = this.db.exec(query, params);
|
|
746
|
+
if (result.length === 0)
|
|
747
|
+
return [];
|
|
748
|
+
return result[0].values.map((row) => this.rowToEvent(row));
|
|
749
|
+
}
|
|
750
|
+
// IP Asset operations
|
|
751
|
+
async createIpAsset(asset) {
|
|
752
|
+
if (!this.db)
|
|
753
|
+
await this.initialize();
|
|
754
|
+
const now = new Date().toISOString();
|
|
755
|
+
const record = {
|
|
756
|
+
...asset,
|
|
757
|
+
status: asset.status || "identified",
|
|
758
|
+
createdAt: now,
|
|
759
|
+
updatedAt: now,
|
|
760
|
+
};
|
|
761
|
+
this.db.run(`INSERT INTO ip_assets (id, project_id, name, type, description, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
762
|
+
record.id,
|
|
763
|
+
record.projectId,
|
|
764
|
+
record.name,
|
|
765
|
+
record.type,
|
|
766
|
+
record.description,
|
|
767
|
+
record.status,
|
|
768
|
+
record.createdAt,
|
|
769
|
+
record.updatedAt,
|
|
770
|
+
]);
|
|
771
|
+
this.save();
|
|
772
|
+
return record;
|
|
773
|
+
}
|
|
774
|
+
async getIpAsset(id) {
|
|
775
|
+
if (!this.db)
|
|
776
|
+
await this.initialize();
|
|
777
|
+
const result = this.db.exec(`SELECT * FROM ip_assets WHERE id = ?`, [id]);
|
|
778
|
+
if (result.length === 0 || result[0].values.length === 0)
|
|
779
|
+
return undefined;
|
|
780
|
+
return this.rowToIpAsset(result[0].values[0]);
|
|
781
|
+
}
|
|
782
|
+
async getIpAssets(filter) {
|
|
783
|
+
if (!this.db)
|
|
784
|
+
await this.initialize();
|
|
785
|
+
let query = `SELECT * FROM ip_assets`;
|
|
786
|
+
const params = [];
|
|
787
|
+
const conditions = [];
|
|
788
|
+
if (filter) {
|
|
789
|
+
if (filter.type) {
|
|
790
|
+
conditions.push("type = ?");
|
|
791
|
+
params.push(filter.type);
|
|
792
|
+
}
|
|
793
|
+
if (filter.status) {
|
|
794
|
+
conditions.push("status = ?");
|
|
795
|
+
params.push(filter.status);
|
|
796
|
+
}
|
|
797
|
+
if (filter.projectId) {
|
|
798
|
+
conditions.push("project_id = ?");
|
|
799
|
+
params.push(filter.projectId);
|
|
800
|
+
}
|
|
801
|
+
if (conditions.length > 0)
|
|
802
|
+
query += ` WHERE ${conditions.join(" AND ")}`;
|
|
803
|
+
}
|
|
804
|
+
query += ` ORDER BY created_at DESC`;
|
|
805
|
+
const result = this.db.exec(query, params);
|
|
806
|
+
if (result.length === 0)
|
|
807
|
+
return [];
|
|
808
|
+
return result[0].values.map((row) => this.rowToIpAsset(row));
|
|
809
|
+
}
|
|
810
|
+
async updateIpAsset(id, updates) {
|
|
811
|
+
if (!this.db)
|
|
812
|
+
await this.initialize();
|
|
813
|
+
updates.updatedAt = new Date().toISOString();
|
|
814
|
+
const setClause = Object.keys(updates)
|
|
815
|
+
.map((k) => this.camelToSnake(k) + " = ?")
|
|
816
|
+
.join(", ");
|
|
817
|
+
const values = [...Object.values(updates), id];
|
|
818
|
+
this.db.run(`UPDATE ip_assets SET ${setClause} WHERE id = ?`, values);
|
|
819
|
+
this.save();
|
|
820
|
+
const asset = await this.getIpAsset(id);
|
|
821
|
+
if (!asset)
|
|
822
|
+
throw new Error(`IP Asset ${id} not found`);
|
|
823
|
+
return asset;
|
|
824
|
+
}
|
|
825
|
+
async createIpMilestone(milestone) {
|
|
826
|
+
if (!this.db)
|
|
827
|
+
await this.initialize();
|
|
828
|
+
const now = new Date().toISOString();
|
|
829
|
+
const record = {
|
|
830
|
+
...milestone,
|
|
831
|
+
status: "pending",
|
|
832
|
+
createdAt: now,
|
|
833
|
+
};
|
|
834
|
+
this.db.run(`INSERT INTO ip_milestones (id, ip_asset_id, name, description, status, created_at) VALUES (?, ?, ?, ?, ?, ?)`, [
|
|
835
|
+
record.id,
|
|
836
|
+
record.ipAssetId,
|
|
837
|
+
record.name,
|
|
838
|
+
record.description,
|
|
839
|
+
record.status,
|
|
840
|
+
record.createdAt,
|
|
841
|
+
]);
|
|
842
|
+
this.save();
|
|
843
|
+
return record;
|
|
844
|
+
}
|
|
845
|
+
async getIpMilestones(ipAssetId) {
|
|
846
|
+
if (!this.db)
|
|
847
|
+
await this.initialize();
|
|
848
|
+
const result = this.db.exec(`SELECT * FROM ip_milestones WHERE ip_asset_id = ? ORDER BY created_at ASC`, [ipAssetId]);
|
|
849
|
+
if (result.length === 0)
|
|
850
|
+
return [];
|
|
851
|
+
return result[0].values.map((row) => this.rowToIpMilestone(row));
|
|
852
|
+
}
|
|
853
|
+
async updateIpMilestone(id, updates) {
|
|
854
|
+
if (!this.db)
|
|
855
|
+
await this.initialize();
|
|
856
|
+
const setClause = Object.keys(updates)
|
|
857
|
+
.map((k) => this.camelToSnake(k) + " = ?")
|
|
858
|
+
.join(", ");
|
|
859
|
+
const values = [...Object.values(updates), id];
|
|
860
|
+
this.db.run(`UPDATE ip_milestones SET ${setClause} WHERE id = ?`, values);
|
|
861
|
+
this.save();
|
|
862
|
+
const result = this.db.exec(`SELECT * FROM ip_milestones WHERE id = ?`, [
|
|
863
|
+
id,
|
|
864
|
+
]);
|
|
865
|
+
if (result.length === 0 || result[0].values.length === 0)
|
|
866
|
+
throw new Error(`IP Milestone ${id} not found`);
|
|
867
|
+
return this.rowToIpMilestone(result[0].values[0]);
|
|
868
|
+
}
|
|
869
|
+
// Project operations
|
|
870
|
+
async createProject(project) {
|
|
871
|
+
if (!this.db)
|
|
872
|
+
await this.initialize();
|
|
873
|
+
const now = new Date().toISOString();
|
|
874
|
+
const record = {
|
|
875
|
+
id: project.id,
|
|
876
|
+
name: project.name,
|
|
877
|
+
visionPath: project.visionPath || "",
|
|
878
|
+
createdAt: now,
|
|
879
|
+
updatedAt: now,
|
|
880
|
+
metadata: undefined,
|
|
881
|
+
};
|
|
882
|
+
this.db.run(`INSERT INTO projects (id, name, vision_path, created_at, updated_at, metadata)
|
|
883
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [
|
|
884
|
+
record.id,
|
|
885
|
+
record.name,
|
|
886
|
+
record.visionPath,
|
|
887
|
+
record.createdAt,
|
|
888
|
+
record.updatedAt,
|
|
889
|
+
record.metadata || null,
|
|
890
|
+
]);
|
|
891
|
+
this.save();
|
|
892
|
+
return record;
|
|
893
|
+
}
|
|
894
|
+
async getProject(id) {
|
|
895
|
+
if (!this.db)
|
|
896
|
+
await this.initialize();
|
|
897
|
+
const result = this.db.exec(`SELECT id, name, vision_path, created_at, updated_at, metadata FROM projects WHERE id = ?`, [id]);
|
|
898
|
+
if (result.length === 0 || result[0].values.length === 0)
|
|
899
|
+
return undefined;
|
|
900
|
+
return this.rowToProject(result[0].values[0]);
|
|
901
|
+
}
|
|
902
|
+
async getProjects() {
|
|
903
|
+
if (!this.db)
|
|
904
|
+
await this.initialize();
|
|
905
|
+
const result = this.db.exec(`SELECT id, name, vision_path, created_at, updated_at, metadata FROM projects ORDER BY created_at DESC`);
|
|
906
|
+
if (result.length === 0)
|
|
907
|
+
return [];
|
|
908
|
+
return result[0].values.map((row) => this.rowToProject(row));
|
|
909
|
+
}
|
|
910
|
+
async updateProject(id, updates) {
|
|
911
|
+
if (!this.db)
|
|
912
|
+
await this.initialize();
|
|
913
|
+
updates.updatedAt = new Date().toISOString();
|
|
914
|
+
const setClause = Object.keys(updates)
|
|
915
|
+
.map((k) => this.camelToSnake(k) + " = ?")
|
|
916
|
+
.join(", ");
|
|
917
|
+
const values = [...Object.values(updates), id];
|
|
918
|
+
this.db.run(`UPDATE projects SET ${setClause} WHERE id = ?`, values);
|
|
919
|
+
this.save();
|
|
920
|
+
const project = await this.getProject(id);
|
|
921
|
+
if (!project)
|
|
922
|
+
throw new Error(`Project ${id} not found`);
|
|
923
|
+
return project;
|
|
924
|
+
}
|
|
925
|
+
parseProjectMetadata(raw) {
|
|
926
|
+
if (!raw)
|
|
927
|
+
return {};
|
|
928
|
+
try {
|
|
929
|
+
const parsed = JSON.parse(raw);
|
|
930
|
+
return typeof parsed === "object" && parsed ? parsed : {};
|
|
931
|
+
}
|
|
932
|
+
catch {
|
|
933
|
+
return {};
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
deriveProjectCode(name) {
|
|
937
|
+
const source = (name ?? "").toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
938
|
+
const trimmed = source.slice(0, 6);
|
|
939
|
+
if (trimmed.length >= 2)
|
|
940
|
+
return trimmed;
|
|
941
|
+
return "V2";
|
|
942
|
+
}
|
|
943
|
+
normalizeProjectCode(code, fallback) {
|
|
944
|
+
const cleaned = (code ?? "").toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
945
|
+
if (cleaned.length >= 2)
|
|
946
|
+
return cleaned.slice(0, 6);
|
|
947
|
+
if (fallback)
|
|
948
|
+
return this.deriveProjectCode(fallback);
|
|
949
|
+
return "V2";
|
|
950
|
+
}
|
|
951
|
+
async getNextTaskId(projectId, projectName) {
|
|
952
|
+
if (!this.db)
|
|
953
|
+
await this.initialize();
|
|
954
|
+
let project = await this.getProject(projectId);
|
|
955
|
+
if (!project) {
|
|
956
|
+
project = await this.createProject({
|
|
957
|
+
id: projectId,
|
|
958
|
+
name: projectName ?? "VIBER Project",
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
const metadata = this.parseProjectMetadata(project.metadata);
|
|
962
|
+
const projectCode = this.normalizeProjectCode(typeof metadata.projectCode === "string" ? metadata.projectCode : undefined, project.name || projectName);
|
|
963
|
+
const currentCounter = Number(metadata.taskCounter ?? 0);
|
|
964
|
+
const nextCounter = Number.isFinite(currentCounter) ? currentCounter + 1 : 1;
|
|
965
|
+
metadata.projectCode = projectCode;
|
|
966
|
+
metadata.taskCounter = nextCounter;
|
|
967
|
+
await this.updateProject(project.id, {
|
|
968
|
+
metadata: JSON.stringify(metadata),
|
|
969
|
+
});
|
|
970
|
+
const seq = String(nextCounter).padStart(3, "0");
|
|
971
|
+
return `VL-${projectCode}-${seq}`;
|
|
972
|
+
}
|
|
973
|
+
// Recovery methods
|
|
974
|
+
async recoverActiveAgents() {
|
|
975
|
+
if (!this.db)
|
|
976
|
+
await this.initialize();
|
|
977
|
+
const result = this.db.exec(`SELECT * FROM agents WHERE status IN ('pending', 'spawned', 'working') ORDER BY started_at ASC`);
|
|
978
|
+
if (result.length === 0)
|
|
979
|
+
return [];
|
|
980
|
+
return result[0].values.map((row) => this.rowToAgent(row));
|
|
981
|
+
}
|
|
982
|
+
async recoverActiveSessions() {
|
|
983
|
+
if (!this.db)
|
|
984
|
+
await this.initialize();
|
|
985
|
+
const result = this.db.exec(`SELECT * FROM sessions WHERE status = 'active' ORDER BY started_at DESC`);
|
|
986
|
+
if (result.length === 0)
|
|
987
|
+
return [];
|
|
988
|
+
return result[0].values.map((row) => this.rowToSession(row));
|
|
989
|
+
}
|
|
990
|
+
// Close
|
|
991
|
+
close() {
|
|
992
|
+
if (this.saveTimer)
|
|
993
|
+
clearTimeout(this.saveTimer);
|
|
994
|
+
this.flush();
|
|
995
|
+
if (this.db) {
|
|
996
|
+
this.db.close();
|
|
997
|
+
this.db = null;
|
|
998
|
+
this.initialized = false;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
// Row helpers
|
|
1002
|
+
rowToTask(row) {
|
|
1003
|
+
return {
|
|
1004
|
+
id: row[0],
|
|
1005
|
+
title: row[1],
|
|
1006
|
+
description: row[2],
|
|
1007
|
+
status: row[3],
|
|
1008
|
+
priority: row[4],
|
|
1009
|
+
logicStatus: row[5],
|
|
1010
|
+
probability: row[6],
|
|
1011
|
+
projectId: row[7],
|
|
1012
|
+
sessionId: row[8],
|
|
1013
|
+
createdAt: row[9],
|
|
1014
|
+
updatedAt: row[10],
|
|
1015
|
+
result: row[11],
|
|
1016
|
+
error: row[12],
|
|
1017
|
+
metadata: row[13],
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
rowToAgent(row) {
|
|
1021
|
+
return {
|
|
1022
|
+
id: row[0],
|
|
1023
|
+
taskId: row[1],
|
|
1024
|
+
role: row[2],
|
|
1025
|
+
status: row[3],
|
|
1026
|
+
opencodeSessionId: row[4],
|
|
1027
|
+
model: row[5],
|
|
1028
|
+
provider: row[6],
|
|
1029
|
+
startedAt: row[7],
|
|
1030
|
+
completedAt: row[8],
|
|
1031
|
+
durationMs: row[9],
|
|
1032
|
+
inputTokens: row[10],
|
|
1033
|
+
outputTokens: row[11],
|
|
1034
|
+
costUsd: row[12],
|
|
1035
|
+
result: row[13],
|
|
1036
|
+
error: row[14],
|
|
1037
|
+
dependsOn: row[15],
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
rowToSession(row) {
|
|
1041
|
+
return {
|
|
1042
|
+
id: row[0],
|
|
1043
|
+
projectId: row[1],
|
|
1044
|
+
status: row[2],
|
|
1045
|
+
taskId: row[3],
|
|
1046
|
+
startedAt: row[4],
|
|
1047
|
+
endedAt: row[5],
|
|
1048
|
+
handoffPath: row[6],
|
|
1049
|
+
tasksCompleted: row[7],
|
|
1050
|
+
agentsSpawned: row[8],
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
rowToEvent(row) {
|
|
1054
|
+
return {
|
|
1055
|
+
id: row[0],
|
|
1056
|
+
entityType: row[1],
|
|
1057
|
+
entityId: row[2],
|
|
1058
|
+
eventType: row[3],
|
|
1059
|
+
data: row[4],
|
|
1060
|
+
timestamp: row[5],
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
rowToPremise(row) {
|
|
1064
|
+
return {
|
|
1065
|
+
id: row[0],
|
|
1066
|
+
taskId: row[1],
|
|
1067
|
+
statement: row[2],
|
|
1068
|
+
logicVar: row[3],
|
|
1069
|
+
isVerified: Boolean(row[4]),
|
|
1070
|
+
isCritical: Boolean(row[5]),
|
|
1071
|
+
confidence: row[6],
|
|
1072
|
+
evidence: row[7],
|
|
1073
|
+
createdAt: row[8],
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
rowToIpAsset(row) {
|
|
1077
|
+
return {
|
|
1078
|
+
id: row[0],
|
|
1079
|
+
projectId: row[1],
|
|
1080
|
+
name: row[2],
|
|
1081
|
+
type: row[3],
|
|
1082
|
+
description: row[4],
|
|
1083
|
+
status: row[5],
|
|
1084
|
+
createdAt: row[6],
|
|
1085
|
+
updatedAt: row[7],
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
rowToIpMilestone(row) {
|
|
1089
|
+
return {
|
|
1090
|
+
id: row[0],
|
|
1091
|
+
ipAssetId: row[1],
|
|
1092
|
+
name: row[2],
|
|
1093
|
+
description: row[3],
|
|
1094
|
+
status: row[4],
|
|
1095
|
+
createdAt: row[5],
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
rowToProject(row) {
|
|
1099
|
+
return {
|
|
1100
|
+
id: row[0],
|
|
1101
|
+
name: row[1],
|
|
1102
|
+
visionPath: row[2],
|
|
1103
|
+
createdAt: row[3],
|
|
1104
|
+
updatedAt: row[4],
|
|
1105
|
+
metadata: row[5],
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
camelToSnake(str) {
|
|
1109
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
exports.ViberDatabase = ViberDatabase;
|
|
1113
|
+
exports.default = ViberDatabase;
|
|
1114
|
+
//# sourceMappingURL=index.js.map
|