aios-management-web 0.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.
Files changed (91) hide show
  1. package/.env.json +21 -0
  2. package/README.md +257 -0
  3. package/data/management-console.db +0 -0
  4. package/data/management-console.db-shm +0 -0
  5. package/data/management-console.db-wal +0 -0
  6. package/dist/assets/index-CV_wjCAG.js +464 -0
  7. package/dist/assets/index-DfMPB0eV.css +1 -0
  8. package/dist/index.html +13 -0
  9. package/docs/spec.md +199 -0
  10. package/index.html +12 -0
  11. package/package.json +37 -0
  12. package/scripts/reset-kernel.js +59 -0
  13. package/scripts/reset-password.js +22 -0
  14. package/server/fakes.js +57 -0
  15. package/server/index.js +21 -0
  16. package/server/src/api/middleware/auth.js +29 -0
  17. package/server/src/api/middleware/internal.js +44 -0
  18. package/server/src/api/routes/index.js +677 -0
  19. package/server/src/app.js +90 -0
  20. package/server/src/background/index.js +106 -0
  21. package/server/src/background/protocol.js +15 -0
  22. package/server/src/config/env.js +90 -0
  23. package/server/src/db/index.js +501 -0
  24. package/server/src/infra/mqtt/management-rpc-client.js +213 -0
  25. package/server/src/infra/providers/hzg-provider-client.js +39 -0
  26. package/server/src/infra/s3/object-storage.js +97 -0
  27. package/server/src/services/agent-quota.js +54 -0
  28. package/server/src/services/agent-service.js +696 -0
  29. package/server/src/services/agent-status-sync-service.js +132 -0
  30. package/server/src/services/audit-log-service.js +39 -0
  31. package/server/src/services/auth-service.js +153 -0
  32. package/server/src/services/catalog-sync-service.js +712 -0
  33. package/server/src/services/external-service.js +308 -0
  34. package/server/src/services/kernel-reset-service.js +86 -0
  35. package/server/src/services/portal-service.js +555 -0
  36. package/server/src/services/system-service.js +580 -0
  37. package/server/src/services/topic-ping-service.js +282 -0
  38. package/server/src/utils/errors.js +36 -0
  39. package/server/src/utils/security.js +22 -0
  40. package/server/test/agent-service-alignment.test.js +316 -0
  41. package/server/test/agent-service-create.test.js +662 -0
  42. package/server/test/agent-status-sync-service.test.js +167 -0
  43. package/server/test/agent-update-audit.test.js +63 -0
  44. package/server/test/auth-middleware.test.js +71 -0
  45. package/server/test/background-services.test.js +160 -0
  46. package/server/test/catalog-sync-service.test.js +920 -0
  47. package/server/test/db-reset-migration.test.js +123 -0
  48. package/server/test/env-config.test.js +68 -0
  49. package/server/test/external-service.test.js +380 -0
  50. package/server/test/hzg-provider-client.test.js +50 -0
  51. package/server/test/internal-auth-middleware.test.js +66 -0
  52. package/server/test/kernel-reset-service.test.js +112 -0
  53. package/server/test/management-rpc-client.test.js +105 -0
  54. package/server/test/portal-service-access-tokens.test.js +121 -0
  55. package/server/test/portal-service-alignment.test.js +318 -0
  56. package/server/test/portal-service-management-logs.test.js +114 -0
  57. package/server/test/reset-kernel-cli.test.js +23 -0
  58. package/server/test/service-api-auth-middleware.test.js +59 -0
  59. package/server/test/system-service-alignment.test.js +265 -0
  60. package/server/test/topic-ping-service.test.js +182 -0
  61. package/server/test/usage-refresh-audit-route.test.js +82 -0
  62. package/src/App.jsx +1 -0
  63. package/src/api.js +1 -0
  64. package/src/app/App.jsx +346 -0
  65. package/src/app/api-client.js +112 -0
  66. package/src/components/AppShell.jsx +117 -0
  67. package/src/components/CardTitleWithReload.jsx +20 -0
  68. package/src/components/DeleteActionButton.jsx +31 -0
  69. package/src/main.jsx +14 -0
  70. package/src/pages/AgentsPage.jsx +647 -0
  71. package/src/pages/AiosUsersPage.jsx +151 -0
  72. package/src/pages/DashboardPage.jsx +72 -0
  73. package/src/pages/LoginPage.jsx +41 -0
  74. package/src/pages/SettingsPage.jsx +431 -0
  75. package/src/pages/SkillsPage.jsx +175 -0
  76. package/src/pages/SystemLogsPage.jsx +349 -0
  77. package/src/pages/SystemsPage.jsx +498 -0
  78. package/src/pages/TemplatesPage.jsx +207 -0
  79. package/src/pages/UserManagementPage.jsx +25 -0
  80. package/src/pages/UsersPage.jsx +192 -0
  81. package/src/pages/system-logs/SystemLogsTabs.jsx +362 -0
  82. package/src/styles.css +222 -0
  83. package/src/utils/format.js +63 -0
  84. package/test/.reports/fast-2026-05-25T08-32-39-420Z.json +299 -0
  85. package/test/integration/common.js +208 -0
  86. package/test/integration/fast.js +135 -0
  87. package/test/integration/full.js +306 -0
  88. package/test/run-browser-e2e.js +212 -0
  89. package/test/run-jasmine.js +21 -0
  90. package/test/setup.js +1 -0
  91. package/vite.config.js +12 -0
@@ -0,0 +1,712 @@
1
+ import { jsonParse, withSerializedTransaction } from "../db/index.js";
2
+ import {
3
+ normalizeAgentStatus,
4
+ normalizeUsage,
5
+ statusAfterUsageRefresh
6
+ } from "./agent-quota.js";
7
+
8
+ const USAGE_SYNC_CONCURRENCY = 2;
9
+ const USAGE_FIELDS = [
10
+ "daily",
11
+ "day",
12
+ "dailyTokens",
13
+ "today",
14
+ "totalTokens"
15
+ ];
16
+
17
+ function hasUsageFields(payload) {
18
+ if (!payload || typeof payload !== "object") {
19
+ return false;
20
+ }
21
+
22
+ return USAGE_FIELDS.some((field) => Object.prototype.hasOwnProperty.call(payload, field));
23
+ }
24
+
25
+ function parseUsagePayload(payload) {
26
+ if (!payload || typeof payload !== "object") {
27
+ throw new Error("agent.usage returned an empty payload");
28
+ }
29
+
30
+ const source = payload.usage || payload.tokens || payload;
31
+ if (!hasUsageFields(source)) {
32
+ throw new Error("agent.usage did not return usage fields");
33
+ }
34
+
35
+ return normalizeUsage(payload);
36
+ }
37
+
38
+ async function mapWithConcurrency(items, limit, mapper) {
39
+ const results = new Array(items.length);
40
+ let nextIndex = 0;
41
+
42
+ async function worker() {
43
+ while (nextIndex < items.length) {
44
+ const currentIndex = nextIndex;
45
+ nextIndex += 1;
46
+ results[currentIndex] = await mapper(items[currentIndex], currentIndex);
47
+ }
48
+ }
49
+
50
+ const workerCount = Math.min(Math.max(1, limit), Math.max(1, items.length));
51
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
52
+ return results;
53
+ }
54
+
55
+ function firstText(...values) {
56
+ for (const value of values) {
57
+ if (typeof value === "string" && value.trim()) {
58
+ return value.trim();
59
+ }
60
+ }
61
+
62
+ return "";
63
+ }
64
+
65
+ function isBuiltInItem(item) {
66
+ return Boolean(item?.["is-built-in"] ?? item?.is_builtin ?? item?.isBuiltIn);
67
+ }
68
+
69
+ export class CatalogSyncService {
70
+ constructor({ db, rpcClient, auditLogService = null, env = null }) {
71
+ this.db = db;
72
+ this.rpcClient = rpcClient;
73
+ this.auditLogService = auditLogService;
74
+ this.managementTimeoutMs = Number(env?.managementTimeoutMs || 120000);
75
+ this.pendingUsageRefresh = null;
76
+ this.pendingAgentSync = null;
77
+ this.pendingSkillSync = null;
78
+ this.pendingTemplateSync = null;
79
+ this.pendingSystemSync = null;
80
+ }
81
+
82
+ getUsageRefreshStatus() {
83
+ return this.readState("usage_refresh_state");
84
+ }
85
+
86
+ getAgentSyncStatus() {
87
+ return this.readState("agent_sync_state");
88
+ }
89
+
90
+ getSkillSyncStatus() {
91
+ return this.readState("skill_sync_state");
92
+ }
93
+
94
+ getTemplateSyncStatus() {
95
+ return this.readState("template_sync_state");
96
+ }
97
+
98
+ getSystemSyncStatus() {
99
+ return this.readState("system_sync_state");
100
+ }
101
+
102
+ callManagement(action, params = {}, timeoutMs = this.managementTimeoutMs) {
103
+ return this.rpcClient.call(action, params, timeoutMs);
104
+ }
105
+
106
+ async syncAgentsFromKernel(now = new Date().toISOString()) {
107
+ const remote = await this.callManagement("agent.list");
108
+ const items = Array.isArray(remote?.items) ? remote.items : [];
109
+ await withSerializedTransaction(this.db, () => {
110
+ this.syncAgents(items, new Map(), now);
111
+ });
112
+ return items.length;
113
+ }
114
+
115
+ async syncTemplatesFromKernel(now = new Date().toISOString()) {
116
+ const remote = await this.callManagement("agent.template.list");
117
+ const items = Array.isArray(remote?.items) ? remote.items : [];
118
+ await withSerializedTransaction(this.db, () => {
119
+ this.syncTemplates(items, now);
120
+ });
121
+ return items.length;
122
+ }
123
+
124
+ async syncSkillsFromKernel(now = new Date().toISOString()) {
125
+ const remote = await this.callManagement("skills.global.list");
126
+ const items = Array.isArray(remote?.items) ? remote.items : [];
127
+ await withSerializedTransaction(this.db, () => {
128
+ this.syncSkills(items, now);
129
+ });
130
+ return items.length;
131
+ }
132
+
133
+ async syncSystemsFromKernel(now = new Date().toISOString()) {
134
+ const [remote, ontologies] = await Promise.all([
135
+ this.callManagement("apps.list"),
136
+ this.callManagement("ontology.list")
137
+ ]);
138
+ const items = Array.isArray(remote?.items) ? remote.items : [];
139
+ const ontologyItems = Array.isArray(ontologies?.items) ? ontologies.items : [];
140
+ const builtInOntologies = new Set(ontologyItems
141
+ .filter(isBuiltInItem)
142
+ .map((item) => firstText(item?.name, item?.ontologyName, item?.id).toLowerCase())
143
+ .filter(Boolean));
144
+ await withSerializedTransaction(this.db, () => {
145
+ this.syncSystems(items, now, builtInOntologies);
146
+ });
147
+ return items.length;
148
+ }
149
+
150
+ async syncAgentsTask({ trigger = "manual" } = {}) {
151
+ if (this.pendingAgentSync) {
152
+ return await this.pendingAgentSync;
153
+ }
154
+
155
+ this.pendingAgentSync = this.runSingleSync({
156
+ tableName: "agent_sync_state",
157
+ trigger,
158
+ run: () => this.syncAgentsFromKernel(),
159
+ summaryKey: "agents"
160
+ }).finally(() => {
161
+ this.pendingAgentSync = null;
162
+ });
163
+
164
+ return await this.pendingAgentSync;
165
+ }
166
+
167
+ async syncSkillsTask({ trigger = "manual" } = {}) {
168
+ if (this.pendingSkillSync) {
169
+ return await this.pendingSkillSync;
170
+ }
171
+
172
+ this.pendingSkillSync = this.runSingleSync({
173
+ tableName: "skill_sync_state",
174
+ trigger,
175
+ run: () => this.syncSkillsFromKernel(),
176
+ summaryKey: "skills"
177
+ }).finally(() => {
178
+ this.pendingSkillSync = null;
179
+ });
180
+
181
+ return await this.pendingSkillSync;
182
+ }
183
+
184
+ async syncTemplatesTask({ trigger = "manual" } = {}) {
185
+ if (this.pendingTemplateSync) {
186
+ return await this.pendingTemplateSync;
187
+ }
188
+
189
+ this.pendingTemplateSync = this.runSingleSync({
190
+ tableName: "template_sync_state",
191
+ trigger,
192
+ run: () => this.syncTemplatesFromKernel(),
193
+ summaryKey: "templates"
194
+ }).finally(() => {
195
+ this.pendingTemplateSync = null;
196
+ });
197
+
198
+ return await this.pendingTemplateSync;
199
+ }
200
+
201
+ async syncSystemsTask({ trigger = "manual" } = {}) {
202
+ if (this.pendingSystemSync) {
203
+ return await this.pendingSystemSync;
204
+ }
205
+
206
+ this.pendingSystemSync = this.runSingleSync({
207
+ tableName: "system_sync_state",
208
+ trigger,
209
+ run: () => this.syncSystemsFromKernel(),
210
+ summaryKey: "systems"
211
+ }).finally(() => {
212
+ this.pendingSystemSync = null;
213
+ });
214
+
215
+ return await this.pendingSystemSync;
216
+ }
217
+
218
+ async fetchAgentUsageSnapshots(agentItems, { strict = false } = {}) {
219
+ const pairs = await mapWithConcurrency(agentItems, USAGE_SYNC_CONCURRENCY, async (item) => {
220
+ const agentId = String(item?.agentId || "").trim();
221
+ if (!agentId) {
222
+ return [agentId, null];
223
+ }
224
+
225
+ try {
226
+ const raw = await this.callManagement("agent.usage", { agentId });
227
+ return [agentId, {
228
+ raw,
229
+ usage: parseUsagePayload(raw),
230
+ captured_at: new Date().toISOString()
231
+ }];
232
+ } catch (error) {
233
+ if (strict) {
234
+ throw error;
235
+ }
236
+ return [agentId, null];
237
+ }
238
+ });
239
+
240
+ return new Map(pairs.filter(([agentId]) => agentId));
241
+ }
242
+
243
+ async runSingleSync({ tableName, trigger, run, summaryKey }) {
244
+ const startedAt = new Date().toISOString();
245
+
246
+ if (!this.rpcClient.isConfigured()) {
247
+ this.updateState(tableName, {
248
+ status: "skipped",
249
+ triggerSource: trigger,
250
+ startedAt,
251
+ finishedAt: startedAt,
252
+ lastSuccessAt: null,
253
+ errorMessage: "Management RPC is not configured",
254
+ summary: {}
255
+ });
256
+ return this.readState(tableName);
257
+ }
258
+
259
+ this.updateState(tableName, {
260
+ status: "running",
261
+ triggerSource: trigger,
262
+ startedAt,
263
+ finishedAt: null,
264
+ errorMessage: null,
265
+ summary: {}
266
+ });
267
+
268
+ try {
269
+ const count = await run();
270
+ const finishedAt = new Date().toISOString();
271
+ this.updateState(tableName, {
272
+ status: "success",
273
+ triggerSource: trigger,
274
+ startedAt,
275
+ finishedAt,
276
+ lastSuccessAt: finishedAt,
277
+ errorMessage: null,
278
+ summary: {
279
+ [summaryKey]: count
280
+ }
281
+ });
282
+ return this.readState(tableName);
283
+ } catch (error) {
284
+ const finishedAt = new Date().toISOString();
285
+ this.updateState(tableName, {
286
+ status: "failed",
287
+ triggerSource: trigger,
288
+ startedAt,
289
+ finishedAt,
290
+ errorMessage: error instanceof Error ? error.message : String(error),
291
+ summary: {}
292
+ });
293
+ return this.readState(tableName);
294
+ }
295
+ }
296
+
297
+ async refreshAgentUsage({ trigger = "scheduled" } = {}) {
298
+ if (this.pendingUsageRefresh) {
299
+ return await this.pendingUsageRefresh;
300
+ }
301
+
302
+ this.pendingUsageRefresh = this.runUsageRefresh(trigger).finally(() => {
303
+ this.pendingUsageRefresh = null;
304
+ });
305
+
306
+ return await this.pendingUsageRefresh;
307
+ }
308
+
309
+ async runUsageRefresh(trigger) {
310
+ const startedAt = new Date().toISOString();
311
+
312
+ if (!this.rpcClient.isConfigured()) {
313
+ this.updateState("usage_refresh_state", {
314
+ status: "skipped",
315
+ triggerSource: trigger,
316
+ startedAt,
317
+ finishedAt: startedAt,
318
+ lastSuccessAt: null,
319
+ errorMessage: "Management RPC is not configured",
320
+ summary: {
321
+ refreshed_agents: 0
322
+ }
323
+ });
324
+ return this.getUsageRefreshStatus();
325
+ }
326
+
327
+ this.updateState("usage_refresh_state", {
328
+ status: "running",
329
+ triggerSource: trigger,
330
+ startedAt,
331
+ finishedAt: null,
332
+ errorMessage: null,
333
+ summary: {}
334
+ });
335
+
336
+ const agentRows = this.db.prepare(`
337
+ SELECT *
338
+ FROM agents
339
+ ORDER BY slug
340
+ `).all();
341
+
342
+ if (agentRows.length === 0) {
343
+ this.updateState("usage_refresh_state", {
344
+ status: "skipped",
345
+ triggerSource: trigger,
346
+ startedAt,
347
+ finishedAt: startedAt,
348
+ lastSuccessAt: null,
349
+ errorMessage: null,
350
+ summary: {}
351
+ });
352
+ return this.getUsageRefreshStatus();
353
+ }
354
+
355
+ const remoteItems = agentRows.map((row) => ({
356
+ agentId: row.slug
357
+ }));
358
+ const now = new Date();
359
+ const capturedAt = now.toISOString();
360
+ const updateAgent = this.db.prepare(`
361
+ UPDATE agents
362
+ SET usage_snapshot_json = ?, status = ?, updated_at = ?
363
+ WHERE id = ?
364
+ `);
365
+
366
+ try {
367
+ const usageMap = await this.fetchAgentUsageSnapshots(remoteItems, { strict: true });
368
+ let refreshedAgents = 0;
369
+
370
+ await withSerializedTransaction(this.db, () => {
371
+ for (const row of agentRows) {
372
+ const snapshot = usageMap.get(row.slug);
373
+ if (!snapshot) {
374
+ continue;
375
+ }
376
+
377
+ const nextStatus = statusAfterUsageRefresh(row, snapshot.usage);
378
+ updateAgent.run(JSON.stringify(snapshot), nextStatus, capturedAt, row.id);
379
+ refreshedAgents += 1;
380
+ }
381
+
382
+ const finishedAt = new Date().toISOString();
383
+ this.updateState("usage_refresh_state", {
384
+ status: "success",
385
+ triggerSource: trigger,
386
+ startedAt,
387
+ finishedAt,
388
+ lastSuccessAt: finishedAt,
389
+ errorMessage: null,
390
+ summary: {
391
+ refreshed_agents: refreshedAgents,
392
+ captured_at: capturedAt
393
+ }
394
+ });
395
+ });
396
+
397
+ return this.getUsageRefreshStatus();
398
+ } catch (error) {
399
+ const finishedAt = new Date().toISOString();
400
+ this.updateState("usage_refresh_state", {
401
+ status: "failed",
402
+ triggerSource: trigger,
403
+ startedAt,
404
+ finishedAt,
405
+ errorMessage: error instanceof Error ? error.message : String(error),
406
+ summary: {}
407
+ });
408
+ return this.getUsageRefreshStatus();
409
+ }
410
+ }
411
+
412
+ syncAgents(items, usageMap, now) {
413
+ const existingRows = this.db.prepare("SELECT * FROM agents").all();
414
+ const existingMap = new Map(existingRows.map((row) => [row.slug, row]));
415
+ const seen = new Set();
416
+ const insert = this.db.prepare(`
417
+ INSERT INTO agents (
418
+ slug, agent_name, description, docs_content, template_name, status, tags_json,
419
+ daily_limit, usage_snapshot_json, remote_state_json, created_at, updated_at
420
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
421
+ `);
422
+ const update = this.db.prepare(`
423
+ UPDATE agents
424
+ SET agent_name = ?, template_name = ?, status = ?, usage_snapshot_json = ?, remote_state_json = ?, updated_at = ?
425
+ WHERE id = ?
426
+ `);
427
+ const remove = this.db.prepare("DELETE FROM agents WHERE id = ?");
428
+
429
+ for (const item of items) {
430
+ const slug = String(item?.agentId || "").trim();
431
+ if (!slug) {
432
+ continue;
433
+ }
434
+
435
+ seen.add(slug);
436
+ const existing = existingMap.get(slug);
437
+ const existingUsageSnapshot = jsonParse(existing?.usage_snapshot_json, {
438
+ usage: { daily: 0 }
439
+ });
440
+ const usageSnapshot = usageMap.get(slug) || {
441
+ ...existingUsageSnapshot,
442
+ usage: normalizeUsage(existingUsageSnapshot.usage)
443
+ };
444
+ const remoteStateJson = JSON.stringify(item ?? {});
445
+ const agentName = firstText(item?.name, slug);
446
+ const status = normalizeAgentStatus(item?.status);
447
+ const templateName = firstText(item?.templateName, item?.template, existing?.template_name, "default");
448
+
449
+ if (existing) {
450
+ const nextStatus = existing.status === "disabled"
451
+ ? "disabled"
452
+ : (existing.status === "overlimit" ? "overlimit" : normalizeAgentStatus(item?.status));
453
+ update.run(
454
+ agentName,
455
+ templateName,
456
+ nextStatus,
457
+ JSON.stringify(usageSnapshot),
458
+ remoteStateJson,
459
+ now,
460
+ existing.id
461
+ );
462
+ continue;
463
+ }
464
+
465
+ insert.run(
466
+ slug,
467
+ agentName,
468
+ "",
469
+ "",
470
+ templateName,
471
+ status,
472
+ "[]",
473
+ -1,
474
+ JSON.stringify(usageSnapshot),
475
+ remoteStateJson,
476
+ now,
477
+ now
478
+ );
479
+ }
480
+
481
+ for (const row of existingRows) {
482
+ if (!seen.has(row.slug)) {
483
+ remove.run(row.id);
484
+ }
485
+ }
486
+ }
487
+
488
+ syncTemplates(items, now) {
489
+ const existingRows = this.db.prepare("SELECT * FROM agent_templates").all();
490
+ const existingMap = new Map(existingRows.map((row) => [row.template_name, row]));
491
+ const seen = new Set();
492
+ const insert = this.db.prepare(`
493
+ INSERT INTO agent_templates (
494
+ template_name, description, artifact_id, remote_status, remote_result_json, is_builtin, created_at, updated_at
495
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
496
+ `);
497
+ const update = this.db.prepare(`
498
+ UPDATE agent_templates
499
+ SET description = ?, artifact_id = ?, remote_status = ?, remote_result_json = ?, is_builtin = ?, updated_at = ?
500
+ WHERE id = ?
501
+ `);
502
+ const remove = this.db.prepare("DELETE FROM agent_templates WHERE id = ?");
503
+
504
+ for (const item of items) {
505
+ const templateName = String(item?.templateName || "").trim();
506
+ if (!templateName) {
507
+ continue;
508
+ }
509
+
510
+ seen.add(templateName);
511
+ const existing = existingMap.get(templateName);
512
+ const artifactId = existing?.artifact_id ?? this.createPlaceholderArtifact({
513
+ kind: "template",
514
+ originalName: firstText(item?.path, `${templateName}.remote`)
515
+ });
516
+ const remoteResultJson = JSON.stringify(item ?? {});
517
+ const isBuiltin = isBuiltInItem(item) ? 1 : 0;
518
+
519
+ if (existing) {
520
+ update.run(
521
+ existing.description || "",
522
+ artifactId,
523
+ "ready",
524
+ remoteResultJson,
525
+ isBuiltin,
526
+ now,
527
+ existing.id
528
+ );
529
+ continue;
530
+ }
531
+
532
+ insert.run(
533
+ templateName,
534
+ "",
535
+ artifactId,
536
+ "ready",
537
+ remoteResultJson,
538
+ isBuiltin,
539
+ now,
540
+ now
541
+ );
542
+ }
543
+
544
+ for (const row of existingRows) {
545
+ if (!seen.has(row.template_name)) {
546
+ remove.run(row.id);
547
+ }
548
+ }
549
+ }
550
+
551
+ syncSkills(items, now) {
552
+ const existingRows = this.db.prepare("SELECT * FROM skills").all();
553
+ const existingMap = new Map(existingRows.map((row) => [row.slug, row]));
554
+ const seen = new Set();
555
+ const insert = this.db.prepare(`
556
+ INSERT INTO skills (
557
+ slug, description, artifact_id, remote_status, is_builtin, created_at, updated_at
558
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
559
+ `);
560
+ const update = this.db.prepare(`
561
+ UPDATE skills
562
+ SET description = ?, artifact_id = ?, remote_status = ?, is_builtin = ?, updated_at = ?
563
+ WHERE id = ?
564
+ `);
565
+ const remove = this.db.prepare("DELETE FROM skills WHERE id = ?");
566
+
567
+ for (const item of items) {
568
+ const slug = String(item?.slug || "").trim();
569
+ if (!slug) {
570
+ continue;
571
+ }
572
+
573
+ seen.add(slug);
574
+ const existing = existingMap.get(slug);
575
+ const origin = item?.origin && typeof item.origin === "object" ? item.origin : {};
576
+ const description = firstText(origin.description, existing?.description);
577
+ const isBuiltin = isBuiltInItem(item) ? 1 : 0;
578
+
579
+ if (existing) {
580
+ update.run(
581
+ description,
582
+ existing.artifact_id ?? null,
583
+ "installed",
584
+ isBuiltin,
585
+ now,
586
+ existing.id
587
+ );
588
+ continue;
589
+ }
590
+
591
+ insert.run(
592
+ slug,
593
+ description,
594
+ null,
595
+ "installed",
596
+ isBuiltin,
597
+ now,
598
+ now
599
+ );
600
+ }
601
+
602
+ for (const row of existingRows) {
603
+ if (!seen.has(row.slug)) {
604
+ remove.run(row.id);
605
+ }
606
+ }
607
+ }
608
+
609
+ syncSystems(items, now = new Date().toISOString(), builtInOntologies = new Set()) {
610
+ const existingRows = this.db.prepare("SELECT * FROM business_systems").all();
611
+ const seen = new Set();
612
+ const updateBuiltin = this.db.prepare(`
613
+ UPDATE business_systems
614
+ SET is_builtin = ?, updated_at = ?
615
+ WHERE id = ?
616
+ `);
617
+ const remove = this.db.prepare("DELETE FROM business_systems WHERE id = ?");
618
+
619
+ for (const item of items) {
620
+ const applicationName = firstText(item?.applicationName, item?.id, item?.name).toLowerCase();
621
+ if (!applicationName) {
622
+ continue;
623
+ }
624
+
625
+ seen.add(applicationName);
626
+ }
627
+
628
+ for (const row of existingRows) {
629
+ if (!seen.has(row.application_name)) {
630
+ remove.run(row.id);
631
+ continue;
632
+ }
633
+
634
+ const isBuiltin = builtInOntologies.has(String(row.application_name || "").toLowerCase()) ? 1 : 0;
635
+ if (Number(row.is_builtin || 0) !== isBuiltin) {
636
+ updateBuiltin.run(isBuiltin, now, row.id);
637
+ }
638
+ }
639
+ }
640
+
641
+ createPlaceholderArtifact({ kind, originalName }) {
642
+ const result = this.db.prepare(`
643
+ INSERT INTO artifacts (
644
+ kind, bucket, object_key, original_name, mime_type, byte_size, created_by, created_at
645
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
646
+ `).run(
647
+ kind,
648
+ "",
649
+ "",
650
+ originalName,
651
+ "application/octet-stream",
652
+ 0,
653
+ null,
654
+ new Date().toISOString()
655
+ );
656
+
657
+ return result.lastInsertRowid;
658
+ }
659
+
660
+ readState(tableName) {
661
+ const row = this.db.prepare(`SELECT * FROM ${tableName} WHERE id = 1`).get();
662
+ if (!row) {
663
+ return {
664
+ id: 1,
665
+ status: "idle",
666
+ trigger_source: null,
667
+ started_at: null,
668
+ finished_at: null,
669
+ last_success_at: null,
670
+ error_message: null,
671
+ summary: {}
672
+ };
673
+ }
674
+
675
+ return {
676
+ ...row,
677
+ summary: jsonParse(row.summary_json, {})
678
+ };
679
+ }
680
+
681
+ updateState(tableName, { status, triggerSource, startedAt, finishedAt, lastSuccessAt, errorMessage, summary }) {
682
+ const current = this.db.prepare(`SELECT * FROM ${tableName} WHERE id = 1`).get();
683
+ const createdAt = current?.created_at || new Date().toISOString();
684
+ const nextLastSuccessAt = lastSuccessAt ?? current?.last_success_at ?? null;
685
+
686
+ this.db.prepare(`
687
+ INSERT INTO ${tableName} (
688
+ id, status, trigger_source, started_at, finished_at, last_success_at,
689
+ error_message, summary_json, created_at, updated_at
690
+ ) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?)
691
+ ON CONFLICT(id) DO UPDATE SET
692
+ status = excluded.status,
693
+ trigger_source = excluded.trigger_source,
694
+ started_at = excluded.started_at,
695
+ finished_at = excluded.finished_at,
696
+ last_success_at = excluded.last_success_at,
697
+ error_message = excluded.error_message,
698
+ summary_json = excluded.summary_json,
699
+ updated_at = excluded.updated_at
700
+ `).run(
701
+ status,
702
+ triggerSource || null,
703
+ startedAt || null,
704
+ finishedAt || null,
705
+ nextLastSuccessAt,
706
+ errorMessage || null,
707
+ JSON.stringify(summary || {}),
708
+ createdAt,
709
+ new Date().toISOString()
710
+ );
711
+ }
712
+ }