chainlesschain 0.49.0 → 0.66.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 (43) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/{AppLayout-Rvi759IS.js → AppLayout-6SPt_8Y_.js} +1 -1
  4. package/src/assets/web-panel/assets/{Dashboard-DBhFxXYQ.js → Dashboard-Br7kCwKJ.js} +2 -2
  5. package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +1 -0
  6. package/src/assets/web-panel/assets/{index-uL0cZ8N_.js → index-tN-8TosE.js} +2 -2
  7. package/src/assets/web-panel/index.html +2 -2
  8. package/src/commands/agent-network.js +785 -0
  9. package/src/commands/automation.js +654 -0
  10. package/src/commands/dao.js +565 -0
  11. package/src/commands/did-v2.js +620 -0
  12. package/src/commands/economy.js +578 -0
  13. package/src/commands/evolution.js +391 -0
  14. package/src/commands/hmemory.js +442 -0
  15. package/src/commands/ipfs.js +392 -0
  16. package/src/commands/multimodal.js +404 -0
  17. package/src/commands/perf.js +433 -0
  18. package/src/commands/pipeline.js +449 -0
  19. package/src/commands/plugin-ecosystem.js +517 -0
  20. package/src/commands/sandbox.js +401 -0
  21. package/src/commands/social.js +311 -0
  22. package/src/commands/sso.js +798 -0
  23. package/src/commands/workflow.js +320 -0
  24. package/src/commands/zkp.js +227 -1
  25. package/src/index.js +27 -0
  26. package/src/lib/agent-economy.js +479 -0
  27. package/src/lib/agent-network.js +1121 -0
  28. package/src/lib/automation-engine.js +948 -0
  29. package/src/lib/dao-governance.js +569 -0
  30. package/src/lib/did-v2-manager.js +1127 -0
  31. package/src/lib/evolution-system.js +453 -0
  32. package/src/lib/hierarchical-memory.js +481 -0
  33. package/src/lib/ipfs-storage.js +575 -0
  34. package/src/lib/multimodal.js +39 -12
  35. package/src/lib/perf-tuning.js +734 -0
  36. package/src/lib/pipeline-orchestrator.js +928 -0
  37. package/src/lib/plugin-ecosystem.js +1109 -0
  38. package/src/lib/sandbox-v2.js +306 -0
  39. package/src/lib/social-graph-analytics.js +707 -0
  40. package/src/lib/sso-manager.js +841 -0
  41. package/src/lib/workflow-engine.js +454 -1
  42. package/src/lib/zkp-engine.js +249 -20
  43. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +0 -1
@@ -0,0 +1,575 @@
1
+ /**
2
+ * IPFS Storage — CLI port of Phase 17
3
+ * (docs/design/modules/17_IPFS去中心化存储.md).
4
+ *
5
+ * Desktop uses IPFSManager with dual-engine mode
6
+ * (embedded Helia + external Kubo RPC) and 18 IPC handlers.
7
+ *
8
+ * CLI port ships:
9
+ *
10
+ * - Content-addressed store backed by SQLite (CID = sha256(content))
11
+ * - AES-256-GCM encryption (same scheme as desktop: iv + tag + ct)
12
+ * - Pin / unpin / list-pins lifecycle
13
+ * - Storage stats + quota enforcement + garbage collection
14
+ * - Knowledge attachment linkage (knowledge_id → CIDs)
15
+ * - Mode toggle (embedded/external) stored as metadata only
16
+ *
17
+ * What does NOT port: real Helia node, libp2p peer discovery,
18
+ * Kubo HTTP RPC, real DAG sharding, bitswap protocol.
19
+ * CIDs are deterministic sha256 hashes (not real CIDv1),
20
+ * so content is local-only.
21
+ */
22
+
23
+ import crypto from "crypto";
24
+
25
+ /* ── Constants ──────────────────────────────────────────── */
26
+
27
+ export const NODE_MODE = Object.freeze({
28
+ EMBEDDED: "embedded",
29
+ EXTERNAL: "external",
30
+ });
31
+
32
+ export const NODE_STATUS = Object.freeze({
33
+ STOPPED: "stopped",
34
+ STARTING: "starting",
35
+ RUNNING: "running",
36
+ ERROR: "error",
37
+ });
38
+
39
+ export const DEFAULT_QUOTA_BYTES = 1024 * 1024 * 1024; // 1 GB
40
+
41
+ /* ── State ──────────────────────────────────────────────── */
42
+
43
+ let _content = new Map(); // cid → row
44
+ let _knowledgeLinks = new Map(); // knowledge_id → Set(cid)
45
+ let _node = {
46
+ status: NODE_STATUS.STOPPED,
47
+ mode: NODE_MODE.EMBEDDED,
48
+ startedAt: null,
49
+ peerId: null,
50
+ };
51
+ let _quotaBytes = DEFAULT_QUOTA_BYTES;
52
+
53
+ /* ── Helpers ────────────────────────────────────────────── */
54
+
55
+ function _now() {
56
+ return Date.now();
57
+ }
58
+
59
+ function _strip(row) {
60
+ if (!row) return null;
61
+ const out = {};
62
+ for (const [k, v] of Object.entries(row)) {
63
+ if (k !== "_rowid_" && k !== "rowid") out[k] = v;
64
+ }
65
+ return out;
66
+ }
67
+
68
+ function _computeCid(buffer) {
69
+ // Deterministic content addressing using sha256.
70
+ // Real IPFS uses CIDv1 with multihash, but for CLI port we
71
+ // use a base58-like hex prefix to keep CIDs identifiable.
72
+ const hash = crypto.createHash("sha256").update(buffer).digest();
73
+ return `bafy${hash.toString("hex").slice(0, 48)}`;
74
+ }
75
+
76
+ function _toBuffer(content) {
77
+ if (Buffer.isBuffer(content)) return content;
78
+ if (typeof content === "string") return Buffer.from(content, "utf-8");
79
+ if (content instanceof Uint8Array) return Buffer.from(content);
80
+ return Buffer.from(JSON.stringify(content), "utf-8");
81
+ }
82
+
83
+ /* ── Schema ─────────────────────────────────────────────── */
84
+
85
+ export function ensureIpfsTables(db) {
86
+ db.exec(`CREATE TABLE IF NOT EXISTS ipfs_content (
87
+ id TEXT PRIMARY KEY,
88
+ cid TEXT NOT NULL,
89
+ filename TEXT,
90
+ size INTEGER NOT NULL,
91
+ mime_type TEXT,
92
+ pinned INTEGER DEFAULT 0,
93
+ encrypted INTEGER DEFAULT 0,
94
+ encryption_key TEXT,
95
+ knowledge_id TEXT,
96
+ metadata TEXT,
97
+ payload TEXT,
98
+ created_at INTEGER NOT NULL,
99
+ updated_at INTEGER NOT NULL
100
+ )`);
101
+
102
+ db.exec(`CREATE TABLE IF NOT EXISTS ipfs_node_config (
103
+ config_key TEXT PRIMARY KEY,
104
+ config_value TEXT,
105
+ updated_at INTEGER NOT NULL
106
+ )`);
107
+
108
+ _loadAll(db);
109
+ }
110
+
111
+ function _loadAll(db) {
112
+ _content.clear();
113
+ _knowledgeLinks.clear();
114
+
115
+ try {
116
+ for (const row of db.prepare("SELECT * FROM ipfs_content").all()) {
117
+ const r = _strip(row);
118
+ _content.set(r.cid, r);
119
+ if (r.knowledge_id) {
120
+ if (!_knowledgeLinks.has(r.knowledge_id))
121
+ _knowledgeLinks.set(r.knowledge_id, new Set());
122
+ _knowledgeLinks.get(r.knowledge_id).add(r.cid);
123
+ }
124
+ }
125
+ } catch (_e) {
126
+ /* table may not exist */
127
+ }
128
+
129
+ try {
130
+ for (const row of db.prepare("SELECT * FROM ipfs_node_config").all()) {
131
+ const r = _strip(row);
132
+ if (r.config_key === "quota")
133
+ _quotaBytes = Number(r.config_value) || DEFAULT_QUOTA_BYTES;
134
+ else if (r.config_key === "mode") _node.mode = r.config_value;
135
+ }
136
+ } catch (_e) {
137
+ /* empty */
138
+ }
139
+ }
140
+
141
+ /* ── Encryption ─────────────────────────────────────────── */
142
+
143
+ function _encrypt(data) {
144
+ const key = crypto.randomBytes(32);
145
+ const iv = crypto.randomBytes(16);
146
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
147
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
148
+ const tag = cipher.getAuthTag();
149
+ return {
150
+ encrypted: Buffer.concat([iv, tag, encrypted]),
151
+ key: key.toString("hex"),
152
+ };
153
+ }
154
+
155
+ function _decrypt(data, keyHex) {
156
+ const key = Buffer.from(keyHex, "hex");
157
+ const iv = data.subarray(0, 16);
158
+ const tag = data.subarray(16, 32);
159
+ const ciphertext = data.subarray(32);
160
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
161
+ decipher.setAuthTag(tag);
162
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
163
+ }
164
+
165
+ /* ── Node lifecycle ─────────────────────────────────────── */
166
+
167
+ export function startNode(db, { mode } = {}) {
168
+ if (_node.status === NODE_STATUS.RUNNING)
169
+ return { started: false, reason: "already_running" };
170
+
171
+ const resolvedMode = mode || _node.mode || NODE_MODE.EMBEDDED;
172
+ if (!Object.values(NODE_MODE).includes(resolvedMode))
173
+ return { started: false, reason: "invalid_mode" };
174
+
175
+ _node.status = NODE_STATUS.RUNNING;
176
+ _node.mode = resolvedMode;
177
+ _node.startedAt = _now();
178
+ _node.peerId = `sim-${crypto.randomUUID()}`;
179
+
180
+ _persistConfig(db, "mode", resolvedMode);
181
+ return {
182
+ started: true,
183
+ status: _node.status,
184
+ mode: _node.mode,
185
+ peerId: _node.peerId,
186
+ };
187
+ }
188
+
189
+ export function stopNode(db) {
190
+ if (_node.status !== NODE_STATUS.RUNNING)
191
+ return { stopped: false, reason: "not_running" };
192
+ _node.status = NODE_STATUS.STOPPED;
193
+ _node.startedAt = null;
194
+ _node.peerId = null;
195
+ return { stopped: true };
196
+ }
197
+
198
+ export function getNodeStatus() {
199
+ return {
200
+ status: _node.status,
201
+ mode: _node.mode,
202
+ startedAt: _node.startedAt,
203
+ peerId: _node.peerId,
204
+ uptimeMs: _node.startedAt ? _now() - _node.startedAt : 0,
205
+ };
206
+ }
207
+
208
+ export function setMode(db, mode) {
209
+ if (!Object.values(NODE_MODE).includes(mode))
210
+ return { set: false, reason: "invalid_mode" };
211
+ if (_node.status === NODE_STATUS.RUNNING)
212
+ return { set: false, reason: "stop_node_first" };
213
+ _node.mode = mode;
214
+ _persistConfig(db, "mode", mode);
215
+ return { set: true, mode };
216
+ }
217
+
218
+ /* ── Config persistence ─────────────────────────────────── */
219
+
220
+ function _persistConfig(db, key, value) {
221
+ const now = _now();
222
+ const valueStr = String(value);
223
+ const existing = db
224
+ .prepare("SELECT config_key FROM ipfs_node_config WHERE config_key = ?")
225
+ .get(key);
226
+ if (existing) {
227
+ db.prepare(
228
+ "UPDATE ipfs_node_config SET config_value = ?, updated_at = ? WHERE config_key = ?",
229
+ ).run(valueStr, now, key);
230
+ } else {
231
+ db.prepare(
232
+ "INSERT INTO ipfs_node_config (config_key, config_value, updated_at) VALUES (?, ?, ?)",
233
+ ).run(key, valueStr, now);
234
+ }
235
+ }
236
+
237
+ /* ── Content operations ─────────────────────────────────── */
238
+
239
+ export function addContent(
240
+ db,
241
+ content,
242
+ { filename, mimeType, encrypt, pin, knowledgeId, metadata } = {},
243
+ ) {
244
+ if (_node.status !== NODE_STATUS.RUNNING)
245
+ return { added: false, reason: "node_not_running" };
246
+ if (content == null || content === "")
247
+ return { added: false, reason: "empty_content" };
248
+
249
+ const buffer = _toBuffer(content);
250
+ const size = buffer.length;
251
+
252
+ // Quota check (sum pinned only — desktop semantics)
253
+ const pinnedSize = _pinnedBytes();
254
+ if (pin && pinnedSize + size > _quotaBytes)
255
+ return { added: false, reason: "quota_exceeded" };
256
+
257
+ let payload = buffer;
258
+ let encryptionKey = null;
259
+ if (encrypt) {
260
+ const enc = _encrypt(buffer);
261
+ payload = enc.encrypted;
262
+ encryptionKey = enc.key;
263
+ }
264
+
265
+ const cid = _computeCid(payload);
266
+
267
+ // Idempotent: if CID exists, just update flags
268
+ const existing = _content.get(cid);
269
+ if (existing) {
270
+ const now = _now();
271
+ if (pin && !existing.pinned) {
272
+ existing.pinned = 1;
273
+ existing.updated_at = now;
274
+ db.prepare(
275
+ "UPDATE ipfs_content SET pinned = 1, updated_at = ? WHERE cid = ?",
276
+ ).run(now, cid);
277
+ }
278
+ return {
279
+ added: true,
280
+ cid,
281
+ size: existing.size,
282
+ pinned: Boolean(existing.pinned),
283
+ duplicate: true,
284
+ };
285
+ }
286
+
287
+ const id = crypto.randomUUID();
288
+ const now = _now();
289
+ const metadataJson = metadata
290
+ ? typeof metadata === "string"
291
+ ? metadata
292
+ : JSON.stringify(metadata)
293
+ : null;
294
+ const payloadBase64 = payload.toString("base64");
295
+
296
+ const entry = {
297
+ id,
298
+ cid,
299
+ filename: filename || null,
300
+ size,
301
+ mime_type: mimeType || null,
302
+ pinned: pin ? 1 : 0,
303
+ encrypted: encrypt ? 1 : 0,
304
+ encryption_key: encryptionKey,
305
+ knowledge_id: knowledgeId || null,
306
+ metadata: metadataJson,
307
+ payload: payloadBase64,
308
+ created_at: now,
309
+ updated_at: now,
310
+ };
311
+
312
+ db.prepare(
313
+ `INSERT INTO ipfs_content (id, cid, filename, size, mime_type, pinned, encrypted, encryption_key, knowledge_id, metadata, payload, created_at, updated_at)
314
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
315
+ ).run(
316
+ id,
317
+ cid,
318
+ entry.filename,
319
+ size,
320
+ entry.mime_type,
321
+ entry.pinned,
322
+ entry.encrypted,
323
+ entry.encryption_key,
324
+ entry.knowledge_id,
325
+ entry.metadata,
326
+ entry.payload,
327
+ now,
328
+ now,
329
+ );
330
+
331
+ _content.set(cid, entry);
332
+ if (knowledgeId) {
333
+ if (!_knowledgeLinks.has(knowledgeId))
334
+ _knowledgeLinks.set(knowledgeId, new Set());
335
+ _knowledgeLinks.get(knowledgeId).add(cid);
336
+ }
337
+
338
+ return {
339
+ added: true,
340
+ cid,
341
+ size,
342
+ pinned: Boolean(entry.pinned),
343
+ encrypted: Boolean(entry.encrypted),
344
+ };
345
+ }
346
+
347
+ export function getContent(db, cid, { asString } = {}) {
348
+ if (_node.status !== NODE_STATUS.RUNNING) return null;
349
+ const entry = _content.get(cid);
350
+ if (!entry) return null;
351
+
352
+ let buffer = Buffer.from(entry.payload, "base64");
353
+ if (entry.encrypted && entry.encryption_key) {
354
+ try {
355
+ buffer = _decrypt(buffer, entry.encryption_key);
356
+ } catch (_e) {
357
+ return null;
358
+ }
359
+ }
360
+
361
+ return {
362
+ cid: entry.cid,
363
+ size: entry.size,
364
+ filename: entry.filename,
365
+ mimeType: entry.mime_type,
366
+ encrypted: Boolean(entry.encrypted),
367
+ pinned: Boolean(entry.pinned),
368
+ content: asString ? buffer.toString("utf-8") : buffer,
369
+ base64: buffer.toString("base64"),
370
+ metadata: entry.metadata ? _parseMaybe(entry.metadata) : null,
371
+ };
372
+ }
373
+
374
+ function _parseMaybe(raw) {
375
+ try {
376
+ return JSON.parse(raw);
377
+ } catch (_e) {
378
+ return raw;
379
+ }
380
+ }
381
+
382
+ export function hasContent(db, cid) {
383
+ return _content.has(cid);
384
+ }
385
+
386
+ export function listContent(db, { pinned, knowledgeId, limit = 50 } = {}) {
387
+ let results = [..._content.values()];
388
+ if (pinned != null) {
389
+ const v = pinned ? 1 : 0;
390
+ results = results.filter((c) => c.pinned === v);
391
+ }
392
+ if (knowledgeId)
393
+ results = results.filter((c) => c.knowledge_id === knowledgeId);
394
+ return results
395
+ .sort((a, b) => b.created_at - a.created_at)
396
+ .slice(0, limit)
397
+ .map((c) => {
398
+ const { payload, encryption_key, ...rest } = c;
399
+ return rest;
400
+ });
401
+ }
402
+
403
+ /* ── Pin management ─────────────────────────────────────── */
404
+
405
+ export function pin(db, cid) {
406
+ const entry = _content.get(cid);
407
+ if (!entry) return { pinned: false, reason: "not_found" };
408
+ if (entry.pinned) return { pinned: false, reason: "already_pinned" };
409
+
410
+ const pinnedSize = _pinnedBytes();
411
+ if (pinnedSize + entry.size > _quotaBytes)
412
+ return { pinned: false, reason: "quota_exceeded" };
413
+
414
+ const now = _now();
415
+ entry.pinned = 1;
416
+ entry.updated_at = now;
417
+ db.prepare(
418
+ "UPDATE ipfs_content SET pinned = 1, updated_at = ? WHERE cid = ?",
419
+ ).run(now, cid);
420
+ return { pinned: true, cid };
421
+ }
422
+
423
+ export function unpin(db, cid) {
424
+ const entry = _content.get(cid);
425
+ if (!entry) return { unpinned: false, reason: "not_found" };
426
+ if (!entry.pinned) return { unpinned: false, reason: "not_pinned" };
427
+
428
+ const now = _now();
429
+ entry.pinned = 0;
430
+ entry.updated_at = now;
431
+ db.prepare(
432
+ "UPDATE ipfs_content SET pinned = 0, updated_at = ? WHERE cid = ?",
433
+ ).run(now, cid);
434
+ return { unpinned: true, cid };
435
+ }
436
+
437
+ export function listPins(db, { limit = 50, sortBy = "created_at" } = {}) {
438
+ const pinned = [..._content.values()].filter((c) => c.pinned === 1);
439
+ const sorter =
440
+ sortBy === "size"
441
+ ? (a, b) => b.size - a.size
442
+ : sortBy === "filename"
443
+ ? (a, b) => (a.filename || "").localeCompare(b.filename || "")
444
+ : (a, b) => b.created_at - a.created_at;
445
+ return pinned
446
+ .sort(sorter)
447
+ .slice(0, limit)
448
+ .map((c) => {
449
+ const { payload, encryption_key, ...rest } = c;
450
+ return rest;
451
+ });
452
+ }
453
+
454
+ /* ── Storage stats + GC + quota ─────────────────────────── */
455
+
456
+ function _pinnedBytes() {
457
+ let total = 0;
458
+ for (const c of _content.values()) if (c.pinned) total += c.size;
459
+ return total;
460
+ }
461
+
462
+ function _totalBytes() {
463
+ let total = 0;
464
+ for (const c of _content.values()) total += c.size;
465
+ return total;
466
+ }
467
+
468
+ export function getStorageStats() {
469
+ const pinnedBytes = _pinnedBytes();
470
+ const totalBytes = _totalBytes();
471
+ let pinnedCount = 0;
472
+ let encryptedCount = 0;
473
+ for (const c of _content.values()) {
474
+ if (c.pinned) pinnedCount++;
475
+ if (c.encrypted) encryptedCount++;
476
+ }
477
+ return {
478
+ totalContent: _content.size,
479
+ pinnedCount,
480
+ encryptedCount,
481
+ totalBytes,
482
+ pinnedBytes,
483
+ quotaBytes: _quotaBytes,
484
+ usagePercent:
485
+ _quotaBytes > 0
486
+ ? Number(((pinnedBytes / _quotaBytes) * 100).toFixed(2))
487
+ : 0,
488
+ peerCount: _node.status === NODE_STATUS.RUNNING ? 0 : 0,
489
+ };
490
+ }
491
+
492
+ export function garbageCollect(db) {
493
+ // Remove all unpinned content (desktop semantics)
494
+ const before = _content.size;
495
+ let freedBytes = 0;
496
+ const toRemove = [];
497
+ for (const [cid, entry] of _content) {
498
+ if (!entry.pinned) {
499
+ toRemove.push(cid);
500
+ freedBytes += entry.size;
501
+ }
502
+ }
503
+ for (const cid of toRemove) {
504
+ _content.delete(cid);
505
+ db.prepare("DELETE FROM ipfs_content WHERE cid = ?").run(cid);
506
+ }
507
+ // Clean up knowledge links pointing to removed CIDs
508
+ for (const [kid, set] of _knowledgeLinks) {
509
+ for (const cid of [...set]) {
510
+ if (!_content.has(cid)) set.delete(cid);
511
+ }
512
+ if (set.size === 0) _knowledgeLinks.delete(kid);
513
+ }
514
+ return {
515
+ removed: toRemove.length,
516
+ freedBytes,
517
+ before,
518
+ after: _content.size,
519
+ };
520
+ }
521
+
522
+ export function setQuota(db, quotaBytes) {
523
+ const n = Number(quotaBytes);
524
+ if (!Number.isFinite(n) || n <= 0)
525
+ return { set: false, reason: "invalid_quota" };
526
+ _quotaBytes = Math.floor(n);
527
+ _persistConfig(db, "quota", _quotaBytes);
528
+ return { set: true, quotaBytes: _quotaBytes };
529
+ }
530
+
531
+ /* ── Knowledge attachment linkage ───────────────────────── */
532
+
533
+ export function addKnowledgeAttachment(
534
+ db,
535
+ knowledgeId,
536
+ content,
537
+ metadata = {},
538
+ ) {
539
+ if (!knowledgeId) return { added: false, reason: "missing_knowledge_id" };
540
+ const result = addContent(db, content, {
541
+ knowledgeId,
542
+ metadata,
543
+ pin: true,
544
+ filename: metadata?.filename,
545
+ mimeType: metadata?.mimeType,
546
+ });
547
+ if (!result.added) return result;
548
+ return { ...result, knowledgeId };
549
+ }
550
+
551
+ export function getKnowledgeAttachments(db, knowledgeId) {
552
+ const set = _knowledgeLinks.get(knowledgeId);
553
+ if (!set) return [];
554
+ return [...set]
555
+ .map((cid) => _content.get(cid))
556
+ .filter(Boolean)
557
+ .map((c) => {
558
+ const { payload, encryption_key, ...rest } = c;
559
+ return rest;
560
+ });
561
+ }
562
+
563
+ /* ── Reset (tests) ──────────────────────────────────────── */
564
+
565
+ export function _resetState() {
566
+ _content.clear();
567
+ _knowledgeLinks.clear();
568
+ _node = {
569
+ status: NODE_STATUS.STOPPED,
570
+ mode: NODE_MODE.EMBEDDED,
571
+ startedAt: null,
572
+ peerId: null,
573
+ };
574
+ _quotaBytes = DEFAULT_QUOTA_BYTES;
575
+ }
@@ -226,7 +226,11 @@ function _addArtifact(db, sessionId, kind) {
226
226
  return { artifactId: id, createdAt: now };
227
227
  }
228
228
 
229
- export function listArtifacts(db, sessionId, { type, modality, limit = 100 } = {}) {
229
+ export function listArtifacts(
230
+ db,
231
+ sessionId,
232
+ { type, modality, limit = 100 } = {},
233
+ ) {
230
234
  let rows = db
231
235
  .prepare("SELECT * FROM multimodal_artifacts WHERE session_id = ?")
232
236
  .all(sessionId);
@@ -433,8 +437,7 @@ export function buildContext(
433
437
  const s = getSession(db, sessionId);
434
438
  if (!s) return { built: false, reason: "session_not_found" };
435
439
  const arts = listArtifacts(db, sessionId, { type: "input" });
436
- if (arts.length === 0)
437
- return { built: false, reason: "no_input" };
440
+ if (arts.length === 0) return { built: false, reason: "no_input" };
438
441
 
439
442
  // Order artifacts by modality priority, then by createdAt
440
443
  const sorted = [...arts].sort((a, b) => {
@@ -475,7 +478,9 @@ export function buildContext(
475
478
  }
476
479
 
477
480
  const content = items
478
- .map((i) => `[${i.modality}${i.truncated ? " truncated" : ""}]\n${i.content}`)
481
+ .map(
482
+ (i) => `[${i.modality}${i.truncated ? " truncated" : ""}]\n${i.content}`,
483
+ )
479
484
  .join("\n\n");
480
485
 
481
486
  const contextData = { items, tokens, maxTokens, content };
@@ -508,8 +513,8 @@ export function clearContext(db, sessionId) {
508
513
  if (!s) return { cleared: false, reason: "session_not_found" };
509
514
  _contextCache.delete(sessionId);
510
515
  db.prepare(
511
- "UPDATE multimodal_sessions SET context = NULL, token_count = 0, updated_at = ? WHERE id = ?",
512
- ).run(_now(), sessionId);
516
+ "UPDATE multimodal_sessions SET context = ?, token_count = ?, updated_at = ? WHERE id = ?",
517
+ ).run(null, 0, _now(), sessionId);
513
518
  return { cleared: true };
514
519
  }
515
520
 
@@ -524,7 +529,12 @@ export function trimContext(context, maxTokens) {
524
529
  const remaining = maxTokens - tokens;
525
530
  if (remaining <= 0) break;
526
531
  const clipped = (i.content || "").slice(0, remaining * 4);
527
- items.push({ ...i, content: clipped, tokens: _estimateTokens(clipped), truncated: true });
532
+ items.push({
533
+ ...i,
534
+ content: clipped,
535
+ tokens: _estimateTokens(clipped),
536
+ truncated: true,
537
+ });
528
538
  tokens += _estimateTokens(clipped);
529
539
  break;
530
540
  }
@@ -532,7 +542,9 @@ export function trimContext(context, maxTokens) {
532
542
  tokens += t;
533
543
  }
534
544
  const content = items
535
- .map((i) => `[${i.modality}${i.truncated ? " truncated" : ""}]\n${i.content}`)
545
+ .map(
546
+ (i) => `[${i.modality}${i.truncated ? " truncated" : ""}]\n${i.content}`,
547
+ )
536
548
  .join("\n\n");
537
549
  return { trimmed: true, items, tokens, maxTokens, content };
538
550
  }
@@ -550,7 +562,10 @@ export function generateOutput(db, sessionId, content, format, options = {}) {
550
562
  let produced;
551
563
  switch (format) {
552
564
  case "markdown":
553
- produced = typeof content === "string" ? content : JSON.stringify(content, null, 2);
565
+ produced =
566
+ typeof content === "string"
567
+ ? content
568
+ : JSON.stringify(content, null, 2);
554
569
  break;
555
570
  case "html":
556
571
  produced = _renderHtml(content, options);
@@ -649,7 +664,13 @@ function _renderChart(data, options) {
649
664
  yAxis: {},
650
665
  series: Array.isArray(data?.series)
651
666
  ? data.series.map((s) => ({ ...s, type: s.type || chartType }))
652
- : [{ name: options.title || "series", type: chartType, data: data?.values || [] }],
667
+ : [
668
+ {
669
+ name: options.title || "series",
670
+ type: chartType,
671
+ data: data?.values || [],
672
+ },
673
+ ],
653
674
  };
654
675
  return JSON.stringify(option, null, 2);
655
676
  }
@@ -664,8 +685,14 @@ function _escapeHtml(s) {
664
685
  /* ── Stats ──────────────────────────────────────────────── */
665
686
 
666
687
  export function getMultimodalStats(db) {
667
- const rows = db.prepare("SELECT * FROM multimodal_sessions").all().map(_strip);
668
- const arts = db.prepare("SELECT * FROM multimodal_artifacts").all().map(_strip);
688
+ const rows = db
689
+ .prepare("SELECT * FROM multimodal_sessions")
690
+ .all()
691
+ .map(_strip);
692
+ const arts = db
693
+ .prepare("SELECT * FROM multimodal_artifacts")
694
+ .all()
695
+ .map(_strip);
669
696
  const byStatus = {};
670
697
  let totalTokens = 0;
671
698
  for (const r of rows) {