@danieltmn/tamnora 0.25.1 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +13 -25
- package/SECURITY.md +3 -4
- package/harness/packages/cli/src/cli.js +0 -3
- package/harness/packages/cli/src/commands/bridge.js +0 -1
- package/harness/packages/cli/src/commands/repl.js +3 -3
- package/harness/packages/cli/src/commands/run.js +1 -1
- package/harness/packages/cli/src/commands/server.js +1 -1
- package/harness/packages/cli/src/commands/tui.js +1 -1
- package/harness/packages/cli/src/lib/util.js +2 -2
- package/harness/packages/cli/src/tui/app.js +2 -2
- package/harness/packages/cli/test/tunnel.test.js +5 -32
- package/harness/packages/core/src/agents/index.js +13 -41
- package/harness/packages/core/src/config/app.js +0 -4
- package/harness/packages/core/src/config/home.js +1 -20
- package/harness/packages/core/src/config/index.js +5 -8
- package/harness/packages/core/src/extensions/load.js +22 -0
- package/harness/packages/core/src/frontmatter.js +32 -0
- package/harness/packages/core/src/hooks/index.js +2 -16
- package/harness/packages/core/src/index.js +2 -14
- package/harness/packages/core/src/plugins/index.js +2 -16
- package/harness/packages/core/src/procs/project.js +68 -0
- package/harness/packages/core/src/procs/static.js +17 -0
- package/harness/packages/core/src/rules/index.js +8 -27
- package/harness/packages/core/src/skills/index.js +8 -19
- package/harness/packages/core/src/storage/sqlite.js +595 -690
- package/harness/packages/core/src/tunnel.js +9 -147
- package/harness/packages/core/test/home.test.js +2 -35
- package/harness/packages/core/test/procs.test.js +3 -64
- package/harness/packages/core/test/storage.test.js +1 -12
- package/harness/packages/server/public/app.js +2 -19
- package/harness/packages/server/public/appearance.js +2 -56
- package/harness/packages/server/public/boot.js +2 -2
- package/harness/packages/server/public/cards.js +2 -21
- package/harness/packages/server/public/chat.js +0 -16
- package/harness/packages/server/public/chatFormat.js +0 -11
- package/harness/packages/server/public/chrome.js +2 -5
- package/harness/packages/server/public/commands.js +1 -2
- package/harness/packages/server/public/explorer.js +7 -10
- package/harness/packages/server/public/icons/icon-128x128.png +0 -0
- package/harness/packages/server/public/icons/icon-144x144.png +0 -0
- package/harness/packages/server/public/icons/icon-152x152.png +0 -0
- package/harness/packages/server/public/icons/icon-192x192.png +0 -0
- package/harness/packages/server/public/icons/icon-384x384.png +0 -0
- package/harness/packages/server/public/icons/icon-512x512.png +0 -0
- package/harness/packages/server/public/icons/icon-72x72.png +0 -0
- package/harness/packages/server/public/icons/icon-96x96.png +0 -0
- package/harness/packages/server/public/icons/logo.png +0 -0
- package/harness/packages/server/public/icons/logo_light.png +0 -0
- package/harness/packages/server/public/index.html +1228 -1326
- package/harness/packages/server/public/landing/boot.js +2 -3
- package/harness/packages/server/public/landing/index.html +3 -367
- package/harness/packages/server/public/logo.png +0 -0
- package/harness/packages/server/public/settings.js +1255 -1322
- package/harness/packages/server/public/settingsNav.js +0 -1
- package/harness/packages/server/public/styles.css +2341 -2867
- package/harness/packages/server/public/system.js +356 -495
- package/harness/packages/server/public/voice.js +6 -22
- package/harness/packages/server/src/auth.js +4 -15
- package/harness/packages/server/src/cli.js +1 -1
- package/harness/packages/server/src/lib/agents.js +13 -27
- package/harness/packages/server/src/lib/appearance.js +3 -22
- package/harness/packages/server/src/lib/channels/telegram/commands.js +1 -1
- package/harness/packages/server/src/lib/definitions.js +40 -0
- package/harness/packages/server/src/lib/skills.js +14 -21
- package/harness/packages/server/src/lib/ttsSettings.js +2 -2
- package/harness/packages/server/src/lib/workspace.js +9 -11
- package/harness/packages/server/src/routes/chat.js +0 -43
- package/harness/packages/server/src/routes/preview.js +5 -22
- package/harness/packages/server/src/routes/procs.js +2 -4
- package/harness/packages/server/src/routes/tts.js +1 -1
- package/harness/packages/server/src/server.js +1 -5
- package/harness/packages/server/test/appearance-route.test.js +3 -3
- package/harness/packages/server/test/appearance.test.js +12 -38
- package/harness/packages/server/test/chatFormat.test.js +0 -8
- package/harness/packages/server/test/consoleTabs.test.js +4 -4
- package/harness/packages/server/test/ops.test.js +8 -56
- package/harness/packages/server/test/preview.test.js +2 -42
- package/harness/packages/server/test/security.test.js +194 -370
- package/harness/packages/server/test/settingsNav.test.js +2 -2
- package/harness/packages/server/test/tts.test.js +4 -4
- package/harness/tamnora.example.json +1 -1
- package/package.json +2 -2
- package/harness/packages/cli/src/commands/eval.js +0 -186
- package/harness/packages/core/src/eval/dataset.js +0 -47
- package/harness/packages/core/src/eval/judge.js +0 -39
- package/harness/packages/core/src/eval/report.js +0 -48
- package/harness/packages/core/src/eval/runTask.js +0 -62
- package/harness/packages/core/src/eval/runner.js +0 -153
- package/harness/packages/core/src/eval/scorers.js +0 -217
- package/harness/packages/core/src/eval/task.js +0 -59
- package/harness/packages/core/src/procs/detect.js +0 -149
- package/harness/packages/core/test/eval.test.js +0 -144
- package/harness/packages/server/public/icons (1).zip +0 -0
- package/harness/packages/server/src/lib/advisor.js +0 -198
- package/harness/packages/server/src/routes/advisor.js +0 -79
- package/harness/packages/server/src/routes/eval.js +0 -100
- package/harness/packages/server/test/advisor.test.js +0 -249
- package/harness/scripts/example-dataset.jsonl +0 -4
|
@@ -1,690 +1,595 @@
|
|
|
1
|
-
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { mkdirSync } from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
|
|
6
|
-
function likePattern(query) {
|
|
7
|
-
return `%${String(query).replace(/[\\%_]/g, (ch) => `\\${ch}`)}%`;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const SCHEMA = `
|
|
11
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
12
|
-
id TEXT PRIMARY KEY,
|
|
13
|
-
title TEXT,
|
|
14
|
-
provider TEXT,
|
|
15
|
-
model TEXT,
|
|
16
|
-
cwd TEXT,
|
|
17
|
-
created_at INTEGER,
|
|
18
|
-
updated_at INTEGER,
|
|
19
|
-
meta TEXT
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
23
|
-
id TEXT PRIMARY KEY,
|
|
24
|
-
session_id TEXT NOT NULL,
|
|
25
|
-
role TEXT NOT NULL,
|
|
26
|
-
content TEXT,
|
|
27
|
-
tool_calls TEXT,
|
|
28
|
-
tool_call_id TEXT,
|
|
29
|
-
name TEXT,
|
|
30
|
-
mode TEXT,
|
|
31
|
-
agent_id TEXT,
|
|
32
|
-
tokens_in INTEGER,
|
|
33
|
-
tokens_out INTEGER,
|
|
34
|
-
created_at INTEGER
|
|
35
|
-
);
|
|
36
|
-
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, created_at);
|
|
37
|
-
|
|
38
|
-
CREATE TABLE IF NOT EXISTS agents (
|
|
39
|
-
id TEXT PRIMARY KEY,
|
|
40
|
-
session_id TEXT NOT NULL,
|
|
41
|
-
parent_agent_id TEXT,
|
|
42
|
-
kind TEXT,
|
|
43
|
-
title TEXT,
|
|
44
|
-
status TEXT,
|
|
45
|
-
model TEXT,
|
|
46
|
-
summary TEXT,
|
|
47
|
-
created_at INTEGER,
|
|
48
|
-
updated_at INTEGER
|
|
49
|
-
);
|
|
50
|
-
CREATE INDEX IF NOT EXISTS idx_agents_session ON agents(session_id, created_at);
|
|
51
|
-
|
|
52
|
-
CREATE TABLE IF NOT EXISTS session_events (
|
|
53
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
54
|
-
session_id TEXT NOT NULL,
|
|
55
|
-
agent_id TEXT,
|
|
56
|
-
type TEXT NOT NULL,
|
|
57
|
-
data TEXT,
|
|
58
|
-
created_at INTEGER
|
|
59
|
-
);
|
|
60
|
-
CREATE INDEX IF NOT EXISTS idx_session_events ON session_events(session_id, id);
|
|
61
|
-
|
|
62
|
-
CREATE TABLE IF NOT EXISTS
|
|
63
|
-
id TEXT PRIMARY KEY,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
CREATE
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
this.db
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
message.
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
message
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
this.db
|
|
358
|
-
.prepare(
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
return
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
.
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
.
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
)
|
|
537
|
-
.run(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
deleteSchedule(id) {
|
|
599
|
-
this.db.prepare(`DELETE FROM schedules WHERE id = ?`).run(id);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
setScheduleEnabled(id, enabled) {
|
|
603
|
-
this.db.prepare(`UPDATE schedules SET enabled = ?, updated_at = ? WHERE id = ?`).run(enabled ? 1 : 0, now(), id);
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
markScheduleRun(id, { status, nextRun } = {}) {
|
|
607
|
-
this.db
|
|
608
|
-
.prepare(`UPDATE schedules SET last_run = ?, last_status = ?, next_run = ?, updated_at = ? WHERE id = ?`)
|
|
609
|
-
.run(now(), status ?? null, nextRun ?? null, now(), id);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/** Tareas habilitadas cuyo `next_run` ya venció. */
|
|
613
|
-
dueSchedules(at = Date.now()) {
|
|
614
|
-
return this.db
|
|
615
|
-
.prepare(`SELECT * FROM schedules WHERE enabled = 1 AND next_run IS NOT NULL AND next_run <= ? ORDER BY next_run ASC`)
|
|
616
|
-
.all(at)
|
|
617
|
-
.map(toSchedule);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// --- Settings (key/value) ----------------------------------------------
|
|
621
|
-
getSetting(key, fallback = null) {
|
|
622
|
-
const row = this.db.prepare(`SELECT value FROM settings WHERE key = ?`).get(key);
|
|
623
|
-
return row ? parseJson(row.value) : fallback;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
setSetting(key, value) {
|
|
627
|
-
this.db
|
|
628
|
-
.prepare(
|
|
629
|
-
`INSERT INTO settings (key, value) VALUES (?, ?)
|
|
630
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
631
|
-
)
|
|
632
|
-
.run(key, json(value));
|
|
633
|
-
return value;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
allSettings() {
|
|
637
|
-
const out = {};
|
|
638
|
-
for (const row of this.db.prepare(`SELECT key, value FROM settings`).all()) {
|
|
639
|
-
out[row.key] = parseJson(row.value);
|
|
640
|
-
}
|
|
641
|
-
return out;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
function toAgent(row) {
|
|
646
|
-
return {
|
|
647
|
-
id: row.id,
|
|
648
|
-
sessionId: row.session_id,
|
|
649
|
-
parentAgentId: row.parent_agent_id ?? null,
|
|
650
|
-
kind: row.kind ?? 'task',
|
|
651
|
-
title: row.title ?? '',
|
|
652
|
-
status: row.status ?? 'running',
|
|
653
|
-
model: row.model ?? null,
|
|
654
|
-
summary: row.summary ?? null,
|
|
655
|
-
createdAt: row.created_at,
|
|
656
|
-
updatedAt: row.updated_at,
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
function toSchedule(row) {
|
|
661
|
-
return {
|
|
662
|
-
id: row.id,
|
|
663
|
-
name: row.name ?? row.id,
|
|
664
|
-
type: row.type ?? 'agent',
|
|
665
|
-
spec: row.spec ?? '',
|
|
666
|
-
payload: parseJson(row.payload) ?? {},
|
|
667
|
-
cwd: row.cwd ?? null,
|
|
668
|
-
enabled: row.enabled === 0 ? false : true,
|
|
669
|
-
lastRun: row.last_run ?? null,
|
|
670
|
-
lastStatus: row.last_status ?? null,
|
|
671
|
-
nextRun: row.next_run ?? null,
|
|
672
|
-
createdAt: row.created_at,
|
|
673
|
-
updatedAt: row.updated_at,
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function toProvider(row) {
|
|
678
|
-
return {
|
|
679
|
-
id: row.id,
|
|
680
|
-
label: row.label ?? row.id,
|
|
681
|
-
baseUrl: row.base_url ?? '',
|
|
682
|
-
apiKey: row.api_key ?? '',
|
|
683
|
-
models: parseJson(row.models) ?? [],
|
|
684
|
-
visibleModels: parseJson(row.visible_models) ?? [],
|
|
685
|
-
requiresKey: row.requires_key === null ? undefined : Boolean(row.requires_key),
|
|
686
|
-
custom: Boolean(row.custom),
|
|
687
|
-
enabled: row.enabled === 0 ? false : true,
|
|
688
|
-
updatedAt: row.updated_at ?? null,
|
|
689
|
-
};
|
|
690
|
-
}
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdirSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
function likePattern(query) {
|
|
7
|
+
return `%${String(query).replace(/[\\%_]/g, (ch) => `\\${ch}`)}%`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const SCHEMA = `
|
|
11
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
title TEXT,
|
|
14
|
+
provider TEXT,
|
|
15
|
+
model TEXT,
|
|
16
|
+
cwd TEXT,
|
|
17
|
+
created_at INTEGER,
|
|
18
|
+
updated_at INTEGER,
|
|
19
|
+
meta TEXT
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
23
|
+
id TEXT PRIMARY KEY,
|
|
24
|
+
session_id TEXT NOT NULL,
|
|
25
|
+
role TEXT NOT NULL,
|
|
26
|
+
content TEXT,
|
|
27
|
+
tool_calls TEXT,
|
|
28
|
+
tool_call_id TEXT,
|
|
29
|
+
name TEXT,
|
|
30
|
+
mode TEXT,
|
|
31
|
+
agent_id TEXT,
|
|
32
|
+
tokens_in INTEGER,
|
|
33
|
+
tokens_out INTEGER,
|
|
34
|
+
created_at INTEGER
|
|
35
|
+
);
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, created_at);
|
|
37
|
+
|
|
38
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
session_id TEXT NOT NULL,
|
|
41
|
+
parent_agent_id TEXT,
|
|
42
|
+
kind TEXT,
|
|
43
|
+
title TEXT,
|
|
44
|
+
status TEXT,
|
|
45
|
+
model TEXT,
|
|
46
|
+
summary TEXT,
|
|
47
|
+
created_at INTEGER,
|
|
48
|
+
updated_at INTEGER
|
|
49
|
+
);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_agents_session ON agents(session_id, created_at);
|
|
51
|
+
|
|
52
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
53
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
54
|
+
session_id TEXT NOT NULL,
|
|
55
|
+
agent_id TEXT,
|
|
56
|
+
type TEXT NOT NULL,
|
|
57
|
+
data TEXT,
|
|
58
|
+
created_at INTEGER
|
|
59
|
+
);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_session_events ON session_events(session_id, id);
|
|
61
|
+
|
|
62
|
+
CREATE TABLE IF NOT EXISTS mcp_servers (
|
|
63
|
+
id TEXT PRIMARY KEY,
|
|
64
|
+
name TEXT,
|
|
65
|
+
transport TEXT,
|
|
66
|
+
command TEXT,
|
|
67
|
+
url TEXT,
|
|
68
|
+
enabled INTEGER,
|
|
69
|
+
config TEXT
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
CREATE TABLE IF NOT EXISTS providers (
|
|
73
|
+
id TEXT PRIMARY KEY,
|
|
74
|
+
label TEXT,
|
|
75
|
+
base_url TEXT,
|
|
76
|
+
api_key TEXT,
|
|
77
|
+
models TEXT,
|
|
78
|
+
visible_models TEXT,
|
|
79
|
+
requires_key INTEGER,
|
|
80
|
+
custom INTEGER,
|
|
81
|
+
enabled INTEGER,
|
|
82
|
+
updated_at INTEGER
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
86
|
+
key TEXT PRIMARY KEY,
|
|
87
|
+
value TEXT
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
CREATE TABLE IF NOT EXISTS schedules (
|
|
91
|
+
id TEXT PRIMARY KEY,
|
|
92
|
+
name TEXT,
|
|
93
|
+
type TEXT,
|
|
94
|
+
spec TEXT,
|
|
95
|
+
payload TEXT,
|
|
96
|
+
cwd TEXT,
|
|
97
|
+
enabled INTEGER,
|
|
98
|
+
last_run INTEGER,
|
|
99
|
+
last_status TEXT,
|
|
100
|
+
next_run INTEGER,
|
|
101
|
+
created_at INTEGER,
|
|
102
|
+
updated_at INTEGER
|
|
103
|
+
);
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
/** Open (creating if needed) a SQLite database with the harness schema. */
|
|
107
|
+
export function openDatabase(file) {
|
|
108
|
+
if (file && file !== ':memory:') {
|
|
109
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
const db = new DatabaseSync(file ?? ':memory:');
|
|
112
|
+
db.exec('PRAGMA journal_mode = WAL;');
|
|
113
|
+
db.exec(SCHEMA);
|
|
114
|
+
migrate(db);
|
|
115
|
+
return db;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function migrate(db) {
|
|
119
|
+
const messages = db.prepare('PRAGMA table_info(messages)').all().map((c) => c.name);
|
|
120
|
+
if (!messages.includes('mode')) db.exec('ALTER TABLE messages ADD COLUMN mode TEXT');
|
|
121
|
+
if (!messages.includes('agent_id')) db.exec('ALTER TABLE messages ADD COLUMN agent_id TEXT');
|
|
122
|
+
|
|
123
|
+
const providers = db.prepare('PRAGMA table_info(providers)').all().map((c) => c.name);
|
|
124
|
+
if (!providers.includes('visible_models')) db.exec('ALTER TABLE providers ADD COLUMN visible_models TEXT');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const now = () => Date.now();
|
|
128
|
+
const json = (value) => (value === undefined || value === null ? null : JSON.stringify(value));
|
|
129
|
+
const parseJson = (value) => {
|
|
130
|
+
if (value === null || value === undefined) return null;
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse(value);
|
|
133
|
+
} catch {
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** Thin data-access layer over the SQLite database. */
|
|
139
|
+
export class Store {
|
|
140
|
+
constructor(file) {
|
|
141
|
+
this.db = openDatabase(file);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
close() {
|
|
145
|
+
this.db.close();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Sessions -----------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
createSession({ title = 'Nueva sesión', provider, model, cwd, meta } = {}) {
|
|
151
|
+
const id = randomUUID();
|
|
152
|
+
const ts = now();
|
|
153
|
+
this.db
|
|
154
|
+
.prepare(
|
|
155
|
+
`INSERT INTO sessions (id, title, provider, model, cwd, created_at, updated_at, meta)
|
|
156
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
157
|
+
)
|
|
158
|
+
.run(id, title, provider ?? null, model ?? null, cwd ?? null, ts, ts, json(meta));
|
|
159
|
+
return id;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
touchSession(id, { provider, model, title, meta } = {}) {
|
|
163
|
+
const session = this.getSession(id);
|
|
164
|
+
if (!session) return;
|
|
165
|
+
const mergedMeta = meta === undefined ? session.meta : { ...(session.meta ?? {}), ...meta };
|
|
166
|
+
this.db
|
|
167
|
+
.prepare(`UPDATE sessions SET updated_at = ?, provider = ?, model = ?, title = ?, meta = ? WHERE id = ?`)
|
|
168
|
+
.run(
|
|
169
|
+
now(),
|
|
170
|
+
provider ?? session.provider,
|
|
171
|
+
model ?? session.model,
|
|
172
|
+
title ?? session.title,
|
|
173
|
+
json(mergedMeta),
|
|
174
|
+
id
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
clearMessages(sessionId) {
|
|
179
|
+
this.db.prepare(`DELETE FROM messages WHERE session_id = ?`).run(sessionId);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Borra desde `messageId` (incluido) hasta el final. Devuelve cuantos borro. */
|
|
183
|
+
deleteMessagesFrom(sessionId, messageId) {
|
|
184
|
+
const row = this.db
|
|
185
|
+
.prepare(`SELECT rowid FROM messages WHERE id = ? AND session_id = ?`)
|
|
186
|
+
.get(messageId, sessionId);
|
|
187
|
+
if (!row) return 0;
|
|
188
|
+
const info = this.db.prepare(`DELETE FROM messages WHERE session_id = ? AND rowid >= ?`).run(sessionId, row.rowid);
|
|
189
|
+
return info.changes;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
sessionTokens(sessionId) {
|
|
193
|
+
const row = this.db
|
|
194
|
+
.prepare(
|
|
195
|
+
`SELECT COALESCE(SUM(tokens_in), 0) AS input_tokens, COALESCE(SUM(tokens_out), 0) AS output_tokens
|
|
196
|
+
FROM messages WHERE session_id = ?`
|
|
197
|
+
)
|
|
198
|
+
.get(sessionId);
|
|
199
|
+
return { in: row.input_tokens, out: row.output_tokens };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
getSession(id) {
|
|
203
|
+
const row = this.db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(id);
|
|
204
|
+
if (!row) return null;
|
|
205
|
+
return { ...row, meta: parseJson(row.meta) };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
listSessions(limit = 20) {
|
|
209
|
+
return this.db
|
|
210
|
+
.prepare(`SELECT * FROM sessions ORDER BY updated_at DESC LIMIT ?`)
|
|
211
|
+
.all(limit)
|
|
212
|
+
.map((row) => ({ ...row, meta: parseJson(row.meta) }));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
latestSession() {
|
|
216
|
+
return this.listSessions(1)[0] ?? null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
searchSessions(query, limit = 20) {
|
|
220
|
+
const like = likePattern(query);
|
|
221
|
+
return this.db
|
|
222
|
+
.prepare(`SELECT * FROM sessions WHERE title LIKE ? ESCAPE '\\' ORDER BY updated_at DESC LIMIT ?`)
|
|
223
|
+
.all(like, limit)
|
|
224
|
+
.map((row) => ({ ...row, meta: parseJson(row.meta) }));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
searchMessages(query, limit = 30) {
|
|
228
|
+
const like = likePattern(query);
|
|
229
|
+
return this.db
|
|
230
|
+
.prepare(
|
|
231
|
+
`SELECT m.id, m.session_id, m.role, m.content, m.created_at, s.title, s.cwd
|
|
232
|
+
FROM messages m JOIN sessions s ON s.id = m.session_id
|
|
233
|
+
WHERE m.content LIKE ? ESCAPE '\\'
|
|
234
|
+
ORDER BY m.created_at DESC LIMIT ?`
|
|
235
|
+
)
|
|
236
|
+
.all(like, limit)
|
|
237
|
+
.map((row) => ({
|
|
238
|
+
id: row.id,
|
|
239
|
+
sessionId: row.session_id,
|
|
240
|
+
role: row.role,
|
|
241
|
+
content: parseJson(row.content),
|
|
242
|
+
title: row.title,
|
|
243
|
+
cwd: row.cwd,
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
deleteSession(id) {
|
|
248
|
+
this.db.prepare(`DELETE FROM messages WHERE session_id = ?`).run(id);
|
|
249
|
+
this.db.prepare(`DELETE FROM sessions WHERE id = ?`).run(id);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Borra todas las sesiones (y sus mensajes) de una carpeta. No toca archivos. */
|
|
253
|
+
deleteSessionsByCwd(cwd) {
|
|
254
|
+
const value = cwd === undefined || cwd === '' ? null : cwd;
|
|
255
|
+
const messages = this.db
|
|
256
|
+
.prepare(`DELETE FROM messages WHERE session_id IN (SELECT id FROM sessions WHERE cwd IS ?)`)
|
|
257
|
+
.run(value);
|
|
258
|
+
const sessions = this.db.prepare(`DELETE FROM sessions WHERE cwd IS ?`).run(value);
|
|
259
|
+
return { sessions: sessions.changes, messages: messages.changes };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Carpetas de trabajo con cantidad de sesiones (los "proyectos" del sidebar). */
|
|
263
|
+
listProjects() {
|
|
264
|
+
return this.db
|
|
265
|
+
.prepare(
|
|
266
|
+
`SELECT cwd, COUNT(*) AS sessions, MAX(updated_at) AS updated_at
|
|
267
|
+
FROM sessions GROUP BY cwd ORDER BY updated_at DESC`
|
|
268
|
+
)
|
|
269
|
+
.all()
|
|
270
|
+
.map((row) => ({ cwd: row.cwd, sessions: row.sessions, updatedAt: row.updated_at }));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// --- Messages -----------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
addMessage(sessionId, message) {
|
|
276
|
+
const id = randomUUID();
|
|
277
|
+
this.db
|
|
278
|
+
.prepare(
|
|
279
|
+
`INSERT INTO messages (id, session_id, role, content, tool_calls, tool_call_id, name, mode, agent_id, tokens_in, tokens_out, created_at)
|
|
280
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
281
|
+
)
|
|
282
|
+
.run(
|
|
283
|
+
id,
|
|
284
|
+
sessionId,
|
|
285
|
+
message.role,
|
|
286
|
+
json(message.content),
|
|
287
|
+
json(message.tool_calls),
|
|
288
|
+
message.tool_call_id ?? null,
|
|
289
|
+
message.name ?? null,
|
|
290
|
+
message.mode ?? null,
|
|
291
|
+
message.agent_id ?? null,
|
|
292
|
+
message.tokens_in ?? null,
|
|
293
|
+
message.tokens_out ?? null,
|
|
294
|
+
now()
|
|
295
|
+
);
|
|
296
|
+
return id;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
addMessages(sessionId, messages) {
|
|
300
|
+
for (const message of messages) this.addMessage(sessionId, message);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
getMessages(sessionId) {
|
|
304
|
+
return this.db
|
|
305
|
+
.prepare(`SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC`)
|
|
306
|
+
.all(sessionId)
|
|
307
|
+
.map((row) => {
|
|
308
|
+
const message = {
|
|
309
|
+
id: row.id,
|
|
310
|
+
role: row.role,
|
|
311
|
+
content: parseJson(row.content),
|
|
312
|
+
};
|
|
313
|
+
if (row.tool_calls) message.tool_calls = parseJson(row.tool_calls);
|
|
314
|
+
if (row.tool_call_id) message.tool_call_id = row.tool_call_id;
|
|
315
|
+
if (row.name) message.name = row.name;
|
|
316
|
+
if (row.mode) message.mode = row.mode;
|
|
317
|
+
if (row.agent_id) message.agent_id = row.agent_id;
|
|
318
|
+
return message;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// --- Agents (subagentes) ------------------------------------------------
|
|
323
|
+
|
|
324
|
+
createAgent({ sessionId, parentAgentId = null, kind = 'task', title, model } = {}) {
|
|
325
|
+
const id = randomUUID();
|
|
326
|
+
const ts = now();
|
|
327
|
+
this.db
|
|
328
|
+
.prepare(
|
|
329
|
+
`INSERT INTO agents (id, session_id, parent_agent_id, kind, title, status, model, created_at, updated_at)
|
|
330
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
331
|
+
)
|
|
332
|
+
.run(id, sessionId, parentAgentId, kind, title ?? null, 'running', model ?? null, ts, ts);
|
|
333
|
+
return id;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
finishAgent(id, { status = 'done', summary } = {}) {
|
|
337
|
+
this.db
|
|
338
|
+
.prepare(`UPDATE agents SET status = ?, summary = ?, updated_at = ? WHERE id = ?`)
|
|
339
|
+
.run(status, summary ?? null, now(), id);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
getAgent(id) {
|
|
343
|
+
const row = this.db.prepare(`SELECT * FROM agents WHERE id = ?`).get(id);
|
|
344
|
+
return row ? toAgent(row) : null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
listAgents(sessionId) {
|
|
348
|
+
return this.db
|
|
349
|
+
.prepare(`SELECT * FROM agents WHERE session_id = ? ORDER BY created_at ASC`)
|
|
350
|
+
.all(sessionId)
|
|
351
|
+
.map(toAgent);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// --- Session events (run log for reconnection) --------------------------
|
|
355
|
+
|
|
356
|
+
addSessionEvent(sessionId, { agentId = null, type, data } = {}) {
|
|
357
|
+
const info = this.db
|
|
358
|
+
.prepare(`INSERT INTO session_events (session_id, agent_id, type, data, created_at) VALUES (?, ?, ?, ?, ?)`)
|
|
359
|
+
.run(sessionId, agentId, type, json(data), now());
|
|
360
|
+
return Number(info.lastInsertRowid);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
updateSessionEvent(id, data) {
|
|
364
|
+
this.db.prepare(`UPDATE session_events SET data = ? WHERE id = ?`).run(json(data), id);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
getSessionEvents(sessionId, { from = 0, limit = 5000 } = {}) {
|
|
368
|
+
return this.db
|
|
369
|
+
.prepare(`SELECT * FROM session_events WHERE session_id = ? AND id > ? ORDER BY id ASC LIMIT ?`)
|
|
370
|
+
.all(sessionId, from, limit)
|
|
371
|
+
.map((row) => ({ id: row.id, agentId: row.agent_id, type: row.type, data: parseJson(row.data), createdAt: row.created_at }));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
lastSessionEventId(sessionId) {
|
|
375
|
+
const row = this.db
|
|
376
|
+
.prepare(`SELECT COALESCE(MAX(id), 0) AS id FROM session_events WHERE session_id = ?`)
|
|
377
|
+
.get(sessionId);
|
|
378
|
+
return row?.id ?? 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
clearSessionEvents(sessionId) {
|
|
382
|
+
this.db.prepare(`DELETE FROM session_events WHERE session_id = ?`).run(sessionId);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// --- MCP servers --------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
listMcpServers() {
|
|
388
|
+
return this.db
|
|
389
|
+
.prepare(`SELECT * FROM mcp_servers ORDER BY name ASC`)
|
|
390
|
+
.all()
|
|
391
|
+
.map((row) => ({ ...row, enabled: Boolean(row.enabled), config: parseJson(row.config) }));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
saveMcpServer(server) {
|
|
395
|
+
this.db
|
|
396
|
+
.prepare(
|
|
397
|
+
`INSERT INTO mcp_servers (id, name, transport, command, url, enabled, config)
|
|
398
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
399
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
400
|
+
name = excluded.name, transport = excluded.transport, command = excluded.command,
|
|
401
|
+
url = excluded.url, enabled = excluded.enabled, config = excluded.config`
|
|
402
|
+
)
|
|
403
|
+
.run(
|
|
404
|
+
server.id,
|
|
405
|
+
server.name ?? server.id,
|
|
406
|
+
server.transport ?? 'http',
|
|
407
|
+
server.command ?? null,
|
|
408
|
+
server.url ?? null,
|
|
409
|
+
server.enabled === false ? 0 : 1,
|
|
410
|
+
json(server.config)
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
deleteMcpServer(id) {
|
|
415
|
+
this.db.prepare(`DELETE FROM mcp_servers WHERE id = ?`).run(id);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// --- Providers ----------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
listProviders() {
|
|
421
|
+
return this.db
|
|
422
|
+
.prepare(`SELECT * FROM providers ORDER BY id ASC`)
|
|
423
|
+
.all()
|
|
424
|
+
.map(toProvider);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
getProvider(id) {
|
|
428
|
+
const row = this.db.prepare(`SELECT * FROM providers WHERE id = ?`).get(id);
|
|
429
|
+
return row ? toProvider(row) : null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
saveProvider(provider) {
|
|
433
|
+
this.db
|
|
434
|
+
.prepare(
|
|
435
|
+
`INSERT INTO providers (id, label, base_url, api_key, models, visible_models, requires_key, custom, enabled, updated_at)
|
|
436
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
437
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
438
|
+
label = excluded.label, base_url = excluded.base_url, api_key = excluded.api_key,
|
|
439
|
+
models = excluded.models, visible_models = excluded.visible_models, requires_key = excluded.requires_key,
|
|
440
|
+
custom = excluded.custom, enabled = excluded.enabled, updated_at = excluded.updated_at`
|
|
441
|
+
)
|
|
442
|
+
.run(
|
|
443
|
+
provider.id,
|
|
444
|
+
provider.label ?? provider.id,
|
|
445
|
+
provider.baseUrl ?? null,
|
|
446
|
+
provider.apiKey ?? null,
|
|
447
|
+
json(provider.models ?? []),
|
|
448
|
+
json(provider.visibleModels ?? []),
|
|
449
|
+
provider.requiresKey === undefined ? null : provider.requiresKey ? 1 : 0,
|
|
450
|
+
provider.custom ? 1 : 0,
|
|
451
|
+
provider.enabled === false ? 0 : 1,
|
|
452
|
+
now()
|
|
453
|
+
);
|
|
454
|
+
return this.getProvider(provider.id);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
deleteProvider(id) {
|
|
458
|
+
this.db.prepare(`DELETE FROM providers WHERE id = ?`).run(id);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// --- Schedules (tareas programadas) ------------------------------------
|
|
462
|
+
|
|
463
|
+
listSchedules() {
|
|
464
|
+
return this.db
|
|
465
|
+
.prepare(`SELECT * FROM schedules ORDER BY created_at ASC`)
|
|
466
|
+
.all()
|
|
467
|
+
.map(toSchedule);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
getSchedule(id) {
|
|
471
|
+
const row = this.db.prepare(`SELECT * FROM schedules WHERE id = ?`).get(id);
|
|
472
|
+
return row ? toSchedule(row) : null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
saveSchedule(schedule) {
|
|
476
|
+
const ts = now();
|
|
477
|
+
const existing = this.getSchedule(schedule.id);
|
|
478
|
+
this.db
|
|
479
|
+
.prepare(
|
|
480
|
+
`INSERT INTO schedules (id, name, type, spec, payload, cwd, enabled, last_run, last_status, next_run, created_at, updated_at)
|
|
481
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
482
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
483
|
+
name = excluded.name, type = excluded.type, spec = excluded.spec, payload = excluded.payload,
|
|
484
|
+
cwd = excluded.cwd, enabled = excluded.enabled, next_run = excluded.next_run, updated_at = excluded.updated_at`
|
|
485
|
+
)
|
|
486
|
+
.run(
|
|
487
|
+
schedule.id,
|
|
488
|
+
schedule.name ?? schedule.id,
|
|
489
|
+
schedule.type ?? 'agent',
|
|
490
|
+
schedule.spec ?? '',
|
|
491
|
+
json(schedule.payload ?? {}),
|
|
492
|
+
schedule.cwd ?? null,
|
|
493
|
+
schedule.enabled === false ? 0 : 1,
|
|
494
|
+
schedule.lastRun ?? existing?.lastRun ?? null,
|
|
495
|
+
schedule.lastStatus ?? existing?.lastStatus ?? null,
|
|
496
|
+
schedule.nextRun ?? null,
|
|
497
|
+
existing?.createdAt ?? ts,
|
|
498
|
+
ts
|
|
499
|
+
);
|
|
500
|
+
return this.getSchedule(schedule.id);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
deleteSchedule(id) {
|
|
504
|
+
this.db.prepare(`DELETE FROM schedules WHERE id = ?`).run(id);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
setScheduleEnabled(id, enabled) {
|
|
508
|
+
this.db.prepare(`UPDATE schedules SET enabled = ?, updated_at = ? WHERE id = ?`).run(enabled ? 1 : 0, now(), id);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
markScheduleRun(id, { status, nextRun } = {}) {
|
|
512
|
+
this.db
|
|
513
|
+
.prepare(`UPDATE schedules SET last_run = ?, last_status = ?, next_run = ?, updated_at = ? WHERE id = ?`)
|
|
514
|
+
.run(now(), status ?? null, nextRun ?? null, now(), id);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Tareas habilitadas cuyo `next_run` ya venció. */
|
|
518
|
+
dueSchedules(at = Date.now()) {
|
|
519
|
+
return this.db
|
|
520
|
+
.prepare(`SELECT * FROM schedules WHERE enabled = 1 AND next_run IS NOT NULL AND next_run <= ? ORDER BY next_run ASC`)
|
|
521
|
+
.all(at)
|
|
522
|
+
.map(toSchedule);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// --- Settings (key/value) ----------------------------------------------
|
|
526
|
+
getSetting(key, fallback = null) {
|
|
527
|
+
const row = this.db.prepare(`SELECT value FROM settings WHERE key = ?`).get(key);
|
|
528
|
+
return row ? parseJson(row.value) : fallback;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
setSetting(key, value) {
|
|
532
|
+
this.db
|
|
533
|
+
.prepare(
|
|
534
|
+
`INSERT INTO settings (key, value) VALUES (?, ?)
|
|
535
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
536
|
+
)
|
|
537
|
+
.run(key, json(value));
|
|
538
|
+
return value;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
allSettings() {
|
|
542
|
+
const out = {};
|
|
543
|
+
for (const row of this.db.prepare(`SELECT key, value FROM settings`).all()) {
|
|
544
|
+
out[row.key] = parseJson(row.value);
|
|
545
|
+
}
|
|
546
|
+
return out;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function toAgent(row) {
|
|
551
|
+
return {
|
|
552
|
+
id: row.id,
|
|
553
|
+
sessionId: row.session_id,
|
|
554
|
+
parentAgentId: row.parent_agent_id ?? null,
|
|
555
|
+
kind: row.kind ?? 'task',
|
|
556
|
+
title: row.title ?? '',
|
|
557
|
+
status: row.status ?? 'running',
|
|
558
|
+
model: row.model ?? null,
|
|
559
|
+
summary: row.summary ?? null,
|
|
560
|
+
createdAt: row.created_at,
|
|
561
|
+
updatedAt: row.updated_at,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function toSchedule(row) {
|
|
566
|
+
return {
|
|
567
|
+
id: row.id,
|
|
568
|
+
name: row.name ?? row.id,
|
|
569
|
+
type: row.type ?? 'agent',
|
|
570
|
+
spec: row.spec ?? '',
|
|
571
|
+
payload: parseJson(row.payload) ?? {},
|
|
572
|
+
cwd: row.cwd ?? null,
|
|
573
|
+
enabled: row.enabled === 0 ? false : true,
|
|
574
|
+
lastRun: row.last_run ?? null,
|
|
575
|
+
lastStatus: row.last_status ?? null,
|
|
576
|
+
nextRun: row.next_run ?? null,
|
|
577
|
+
createdAt: row.created_at,
|
|
578
|
+
updatedAt: row.updated_at,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function toProvider(row) {
|
|
583
|
+
return {
|
|
584
|
+
id: row.id,
|
|
585
|
+
label: row.label ?? row.id,
|
|
586
|
+
baseUrl: row.base_url ?? '',
|
|
587
|
+
apiKey: row.api_key ?? '',
|
|
588
|
+
models: parseJson(row.models) ?? [],
|
|
589
|
+
visibleModels: parseJson(row.visible_models) ?? [],
|
|
590
|
+
requiresKey: row.requires_key === null ? undefined : Boolean(row.requires_key),
|
|
591
|
+
custom: Boolean(row.custom),
|
|
592
|
+
enabled: row.enabled === 0 ? false : true,
|
|
593
|
+
updatedAt: row.updated_at ?? null,
|
|
594
|
+
};
|
|
595
|
+
}
|