chainlesschain 0.47.9 → 0.49.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 (70) hide show
  1. package/bin/chainlesschain.js +0 -0
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AppLayout-6SPt_8Y_.js → AppLayout-Rvi759IS.js} +1 -1
  5. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
  6. package/src/assets/web-panel/assets/{Dashboard-Br7kCwKJ.js → Dashboard-DBhFxXYQ.js} +2 -2
  7. package/src/assets/web-panel/assets/{index-tN-8TosE.js → index-uL0cZ8N_.js} +2 -2
  8. package/src/assets/web-panel/index.html +2 -2
  9. package/src/commands/codegen.js +303 -0
  10. package/src/commands/collab.js +482 -0
  11. package/src/commands/crosschain.js +382 -0
  12. package/src/commands/dbevo.js +388 -0
  13. package/src/commands/dev.js +411 -0
  14. package/src/commands/federation.js +427 -0
  15. package/src/commands/fusion.js +332 -0
  16. package/src/commands/governance.js +505 -0
  17. package/src/commands/hardening.js +110 -0
  18. package/src/commands/incentive.js +373 -0
  19. package/src/commands/inference.js +304 -0
  20. package/src/commands/infra.js +361 -0
  21. package/src/commands/kg.js +371 -0
  22. package/src/commands/marketplace.js +326 -0
  23. package/src/commands/mcp.js +97 -18
  24. package/src/commands/nlprog.js +329 -0
  25. package/src/commands/ops.js +408 -0
  26. package/src/commands/perception.js +385 -0
  27. package/src/commands/pqc.js +34 -0
  28. package/src/commands/privacy.js +345 -0
  29. package/src/commands/quantization.js +280 -0
  30. package/src/commands/recommend.js +336 -0
  31. package/src/commands/reputation.js +349 -0
  32. package/src/commands/runtime.js +500 -0
  33. package/src/commands/sla.js +352 -0
  34. package/src/commands/stress.js +252 -0
  35. package/src/commands/tech.js +268 -0
  36. package/src/commands/tenant.js +576 -0
  37. package/src/commands/trust.js +366 -0
  38. package/src/harness/mcp-client.js +330 -54
  39. package/src/index.js +112 -0
  40. package/src/lib/aiops.js +523 -0
  41. package/src/lib/autonomous-developer.js +524 -0
  42. package/src/lib/code-agent.js +442 -0
  43. package/src/lib/collaboration-governance.js +556 -0
  44. package/src/lib/community-governance.js +649 -0
  45. package/src/lib/content-recommendation.js +600 -0
  46. package/src/lib/cross-chain.js +669 -0
  47. package/src/lib/dbevo.js +669 -0
  48. package/src/lib/decentral-infra.js +445 -0
  49. package/src/lib/federation-hardening.js +587 -0
  50. package/src/lib/hardening-manager.js +409 -0
  51. package/src/lib/inference-network.js +407 -0
  52. package/src/lib/knowledge-graph.js +530 -0
  53. package/src/lib/mcp-client.js +3 -0
  54. package/src/lib/multimodal.js +698 -0
  55. package/src/lib/nl-programming.js +595 -0
  56. package/src/lib/perception.js +500 -0
  57. package/src/lib/pqc-manager.js +141 -9
  58. package/src/lib/privacy-computing.js +575 -0
  59. package/src/lib/protocol-fusion.js +535 -0
  60. package/src/lib/quantization.js +362 -0
  61. package/src/lib/reputation-optimizer.js +509 -0
  62. package/src/lib/skill-marketplace.js +397 -0
  63. package/src/lib/sla-manager.js +484 -0
  64. package/src/lib/stress-tester.js +383 -0
  65. package/src/lib/tech-learning-engine.js +651 -0
  66. package/src/lib/tenant-saas.js +831 -0
  67. package/src/lib/token-incentive.js +513 -0
  68. package/src/lib/trust-security.js +473 -0
  69. package/src/lib/universal-runtime.js +771 -0
  70. package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +0 -1
@@ -0,0 +1,698 @@
1
+ /**
2
+ * Multimodal Collaboration — CLI port of Phase 27
3
+ * (docs/design/modules/27_多模态协作.md).
4
+ *
5
+ * Desktop exposes 12 IPC handlers for ModalityFusion /
6
+ * DocumentParser / MultimodalContext / MultimodalOutput.
7
+ *
8
+ * CLI port ships:
9
+ *
10
+ * - 5 input modalities (text / document / image / audio / screen)
11
+ * with weighted fusion (same weights as desktop)
12
+ * - Session + artifact persistence in SQLite
13
+ * - Native parse for txt / md / csv / json; report-only for
14
+ * pdf / docx / xlsx (no heavy deps in CLI)
15
+ * - Context builder with 4000-token cap and heuristic token count
16
+ * (~chars/4). No cache TTL (CLI is one-shot).
17
+ * - 6 output formats: markdown / html / json / csv / slides
18
+ * (Reveal.js skeleton) / chart (ECharts option JSON)
19
+ *
20
+ * What does NOT port: OCR / ASR, real PDF/DOCX/XLSX parsing,
21
+ * 5-minute cache, EventEmitter events, IPC channels.
22
+ */
23
+
24
+ import fs from "fs";
25
+ import path from "path";
26
+ import crypto from "crypto";
27
+
28
+ /* ── Constants ──────────────────────────────────────────── */
29
+
30
+ export const MODALITIES = Object.freeze([
31
+ "text",
32
+ "document",
33
+ "image",
34
+ "audio",
35
+ "screen",
36
+ ]);
37
+
38
+ export const MODALITY_WEIGHTS = Object.freeze({
39
+ text: 1.0,
40
+ document: 0.9,
41
+ image: 0.8,
42
+ audio: 0.7,
43
+ screen: 0.6,
44
+ });
45
+
46
+ export const INPUT_FORMATS = Object.freeze([
47
+ "pdf",
48
+ "docx",
49
+ "xlsx",
50
+ "txt",
51
+ "md",
52
+ "csv",
53
+ "json",
54
+ ]);
55
+
56
+ export const NATIVE_FORMATS = Object.freeze(["txt", "md", "csv", "json"]);
57
+
58
+ export const OUTPUT_FORMATS = Object.freeze([
59
+ "markdown",
60
+ "html",
61
+ "chart",
62
+ "slides",
63
+ "json",
64
+ "csv",
65
+ ]);
66
+
67
+ export const SESSION_STATUS = Object.freeze({
68
+ ACTIVE: "active",
69
+ COMPLETED: "completed",
70
+ });
71
+
72
+ export const DEFAULT_MAX_TOKENS = 4000;
73
+
74
+ const PRIORITY_ORDER = ["text", "document", "image", "audio", "screen"];
75
+
76
+ /* ── State ──────────────────────────────────────────────── */
77
+
78
+ let _contextCache = new Map(); // sessionId → { tokens, content, items }
79
+
80
+ /* ── Helpers ────────────────────────────────────────────── */
81
+
82
+ function _now() {
83
+ return Date.now();
84
+ }
85
+
86
+ function _strip(row) {
87
+ if (!row) return null;
88
+ const out = {};
89
+ for (const [k, v] of Object.entries(row)) {
90
+ if (k !== "_rowid_" && k !== "rowid") out[k] = v;
91
+ }
92
+ return out;
93
+ }
94
+
95
+ function _parseMaybe(raw) {
96
+ if (raw == null) return null;
97
+ if (typeof raw !== "string") return raw;
98
+ try {
99
+ return JSON.parse(raw);
100
+ } catch (_e) {
101
+ return raw;
102
+ }
103
+ }
104
+
105
+ function _estimateTokens(s) {
106
+ if (!s) return 0;
107
+ const str = typeof s === "string" ? s : JSON.stringify(s);
108
+ return Math.ceil(str.length / 4);
109
+ }
110
+
111
+ /* ── Schema ─────────────────────────────────────────────── */
112
+
113
+ export function ensureMultimodalTables(db) {
114
+ db.exec(`CREATE TABLE IF NOT EXISTS multimodal_sessions (
115
+ id TEXT PRIMARY KEY,
116
+ modalities TEXT,
117
+ context TEXT,
118
+ status TEXT DEFAULT 'active',
119
+ token_count INTEGER DEFAULT 0,
120
+ created_at INTEGER NOT NULL,
121
+ updated_at INTEGER NOT NULL
122
+ )`);
123
+
124
+ db.exec(`CREATE TABLE IF NOT EXISTS multimodal_artifacts (
125
+ id TEXT PRIMARY KEY,
126
+ session_id TEXT NOT NULL,
127
+ type TEXT NOT NULL,
128
+ modality TEXT,
129
+ format TEXT,
130
+ content TEXT,
131
+ metadata TEXT,
132
+ created_at INTEGER NOT NULL
133
+ )`);
134
+ }
135
+
136
+ /* ── Session management ─────────────────────────────────── */
137
+
138
+ export function createSession(db, { title, metadata } = {}) {
139
+ const id = crypto.randomUUID();
140
+ const now = _now();
141
+ const meta = metadata
142
+ ? typeof metadata === "string"
143
+ ? metadata
144
+ : JSON.stringify(metadata)
145
+ : null;
146
+ db.prepare(
147
+ `INSERT INTO multimodal_sessions (id, modalities, context, status, token_count, created_at, updated_at)
148
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
149
+ ).run(id, JSON.stringify([]), meta, SESSION_STATUS.ACTIVE, 0, now, now);
150
+ return { sessionId: id, createdAt: now };
151
+ }
152
+
153
+ export function getSession(db, sessionId) {
154
+ const row = _strip(
155
+ db.prepare("SELECT * FROM multimodal_sessions WHERE id = ?").get(sessionId),
156
+ );
157
+ if (!row) return null;
158
+ return {
159
+ id: row.id,
160
+ modalities: _parseMaybe(row.modalities) || [],
161
+ context: _parseMaybe(row.context),
162
+ status: row.status,
163
+ tokenCount: row.token_count,
164
+ createdAt: row.created_at,
165
+ updatedAt: row.updated_at,
166
+ };
167
+ }
168
+
169
+ export function listSessions(db, { status, limit = 50 } = {}) {
170
+ let rows = db.prepare("SELECT * FROM multimodal_sessions").all();
171
+ rows = rows.map(_strip);
172
+ if (status) rows = rows.filter((r) => r.status === status);
173
+ return rows
174
+ .sort((a, b) => b.created_at - a.created_at)
175
+ .slice(0, limit)
176
+ .map((r) => ({
177
+ id: r.id,
178
+ status: r.status,
179
+ tokenCount: r.token_count,
180
+ modalities: _parseMaybe(r.modalities) || [],
181
+ createdAt: r.created_at,
182
+ updatedAt: r.updated_at,
183
+ }));
184
+ }
185
+
186
+ export function completeSession(db, sessionId) {
187
+ const s = getSession(db, sessionId);
188
+ if (!s) return { completed: false, reason: "not_found" };
189
+ db.prepare(
190
+ "UPDATE multimodal_sessions SET status = ?, updated_at = ? WHERE id = ?",
191
+ ).run(SESSION_STATUS.COMPLETED, _now(), sessionId);
192
+ return { completed: true };
193
+ }
194
+
195
+ export function deleteSession(db, sessionId) {
196
+ const s = getSession(db, sessionId);
197
+ if (!s) return { deleted: false, reason: "not_found" };
198
+ db.prepare("DELETE FROM multimodal_artifacts WHERE session_id = ?").run(
199
+ sessionId,
200
+ );
201
+ db.prepare("DELETE FROM multimodal_sessions WHERE id = ?").run(sessionId);
202
+ _contextCache.delete(sessionId);
203
+ return { deleted: true };
204
+ }
205
+
206
+ /* ── Artifact storage ───────────────────────────────────── */
207
+
208
+ function _addArtifact(db, sessionId, kind) {
209
+ const id = crypto.randomUUID();
210
+ const now = _now();
211
+ db.prepare(
212
+ `INSERT INTO multimodal_artifacts (id, session_id, type, modality, format, content, metadata, created_at)
213
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
214
+ ).run(
215
+ id,
216
+ sessionId,
217
+ kind.type,
218
+ kind.modality || null,
219
+ kind.format || null,
220
+ typeof kind.content === "string"
221
+ ? kind.content
222
+ : JSON.stringify(kind.content ?? null),
223
+ kind.metadata ? JSON.stringify(kind.metadata) : null,
224
+ now,
225
+ );
226
+ return { artifactId: id, createdAt: now };
227
+ }
228
+
229
+ export function listArtifacts(db, sessionId, { type, modality, limit = 100 } = {}) {
230
+ let rows = db
231
+ .prepare("SELECT * FROM multimodal_artifacts WHERE session_id = ?")
232
+ .all(sessionId);
233
+ rows = rows.map(_strip);
234
+ if (type) rows = rows.filter((r) => r.type === type);
235
+ if (modality) rows = rows.filter((r) => r.modality === modality);
236
+ return rows
237
+ .sort((a, b) => a.created_at - b.created_at)
238
+ .slice(0, limit)
239
+ .map((r) => ({
240
+ id: r.id,
241
+ type: r.type,
242
+ modality: r.modality,
243
+ format: r.format,
244
+ content: _parseMaybe(r.content),
245
+ metadata: _parseMaybe(r.metadata),
246
+ createdAt: r.created_at,
247
+ }));
248
+ }
249
+
250
+ /* ── Modality operations ────────────────────────────────── */
251
+
252
+ export function addModality(db, sessionId, modality, data, { metadata } = {}) {
253
+ if (!MODALITIES.includes(modality))
254
+ return { added: false, reason: "unknown_modality" };
255
+ const s = getSession(db, sessionId);
256
+ if (!s) return { added: false, reason: "session_not_found" };
257
+
258
+ const artifact = _addArtifact(db, sessionId, {
259
+ type: "input",
260
+ modality,
261
+ format: metadata?.format || null,
262
+ content: data,
263
+ metadata,
264
+ });
265
+
266
+ const modalities = new Set(s.modalities);
267
+ modalities.add(modality);
268
+ db.prepare(
269
+ "UPDATE multimodal_sessions SET modalities = ?, updated_at = ? WHERE id = ?",
270
+ ).run(JSON.stringify([...modalities]), _now(), sessionId);
271
+ _contextCache.delete(sessionId);
272
+
273
+ return { added: true, modality, artifactId: artifact.artifactId };
274
+ }
275
+
276
+ export function getSessionModalities(db, sessionId) {
277
+ const s = getSession(db, sessionId);
278
+ if (!s) return [];
279
+ const arts = listArtifacts(db, sessionId, { type: "input" });
280
+ const byModality = {};
281
+ for (const m of s.modalities) byModality[m] = [];
282
+ for (const a of arts) {
283
+ if (!byModality[a.modality]) byModality[a.modality] = [];
284
+ byModality[a.modality].push({
285
+ artifactId: a.id,
286
+ content: a.content,
287
+ format: a.format,
288
+ metadata: a.metadata,
289
+ createdAt: a.createdAt,
290
+ });
291
+ }
292
+ return byModality;
293
+ }
294
+
295
+ export function fuse(db, sessionId) {
296
+ const s = getSession(db, sessionId);
297
+ if (!s) return { fused: false, reason: "session_not_found" };
298
+ const arts = listArtifacts(db, sessionId, { type: "input" });
299
+ if (arts.length === 0) return { fused: false, reason: "no_input" };
300
+
301
+ // Weighted aggregation: concatenate contents ordered by priority,
302
+ // compute sum(weight) and per-modality summary.
303
+ const byModality = {};
304
+ for (const a of arts) {
305
+ if (!byModality[a.modality]) byModality[a.modality] = [];
306
+ byModality[a.modality].push(a);
307
+ }
308
+
309
+ const parts = [];
310
+ const summary = [];
311
+ let totalWeight = 0;
312
+ for (const m of PRIORITY_ORDER) {
313
+ if (!byModality[m]) continue;
314
+ const weight = MODALITY_WEIGHTS[m] || 0.5;
315
+ totalWeight += weight;
316
+ for (const a of byModality[m]) {
317
+ const contentStr =
318
+ typeof a.content === "string"
319
+ ? a.content
320
+ : JSON.stringify(a.content ?? "");
321
+ parts.push(`[${m} w=${weight}] ${contentStr}`);
322
+ }
323
+ summary.push({
324
+ modality: m,
325
+ count: byModality[m].length,
326
+ weight,
327
+ });
328
+ }
329
+
330
+ const fused = parts.join("\n\n");
331
+ return {
332
+ fused: true,
333
+ content: fused,
334
+ summary,
335
+ totalWeight: Number(totalWeight.toFixed(2)),
336
+ tokenEstimate: _estimateTokens(fused),
337
+ };
338
+ }
339
+
340
+ /* ── Document parsing ───────────────────────────────────── */
341
+
342
+ export function getSupportedFormats() {
343
+ return { formats: [...INPUT_FORMATS], native: [...NATIVE_FORMATS] };
344
+ }
345
+
346
+ export function parseDocument(filePath, { content } = {}) {
347
+ const ext = path.extname(filePath).slice(1).toLowerCase();
348
+ if (!INPUT_FORMATS.includes(ext))
349
+ return { parsed: false, reason: "unsupported_format", ext };
350
+
351
+ let text;
352
+ if (content != null) {
353
+ text = typeof content === "string" ? content : content.toString("utf-8");
354
+ } else {
355
+ try {
356
+ text = fs.readFileSync(filePath, "utf-8");
357
+ } catch (_e) {
358
+ return { parsed: false, reason: "read_failed", ext };
359
+ }
360
+ }
361
+
362
+ if (ext === "txt" || ext === "md") {
363
+ return {
364
+ parsed: true,
365
+ format: ext,
366
+ text,
367
+ length: text.length,
368
+ tokenEstimate: _estimateTokens(text),
369
+ };
370
+ }
371
+
372
+ if (ext === "json") {
373
+ let json;
374
+ try {
375
+ json = JSON.parse(text);
376
+ } catch (_e) {
377
+ return { parsed: false, reason: "invalid_json", ext };
378
+ }
379
+ return {
380
+ parsed: true,
381
+ format: "json",
382
+ text,
383
+ json,
384
+ length: text.length,
385
+ tokenEstimate: _estimateTokens(text),
386
+ };
387
+ }
388
+
389
+ if (ext === "csv") {
390
+ const rows = _parseCsv(text);
391
+ return {
392
+ parsed: true,
393
+ format: "csv",
394
+ text,
395
+ rows,
396
+ rowCount: rows.length,
397
+ tokenEstimate: _estimateTokens(text),
398
+ };
399
+ }
400
+
401
+ // pdf / docx / xlsx — not implemented in CLI port
402
+ return {
403
+ parsed: false,
404
+ reason: "parser_not_available",
405
+ ext,
406
+ hint: `Install dedicated parser (pdf-parse / mammoth / xlsx) and use desktop for ${ext} parsing`,
407
+ };
408
+ }
409
+
410
+ function _parseCsv(text) {
411
+ const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
412
+ if (lines.length === 0) return [];
413
+ const header = lines[0].split(",").map((c) => c.trim());
414
+ const out = [];
415
+ for (let i = 1; i < lines.length; i++) {
416
+ const cells = lines[i].split(",").map((c) => c.trim());
417
+ const row = {};
418
+ header.forEach((h, idx) => {
419
+ row[h] = cells[idx] ?? "";
420
+ });
421
+ out.push(row);
422
+ }
423
+ return out;
424
+ }
425
+
426
+ /* ── Context management ─────────────────────────────────── */
427
+
428
+ export function buildContext(
429
+ db,
430
+ sessionId,
431
+ { maxTokens = DEFAULT_MAX_TOKENS } = {},
432
+ ) {
433
+ const s = getSession(db, sessionId);
434
+ if (!s) return { built: false, reason: "session_not_found" };
435
+ const arts = listArtifacts(db, sessionId, { type: "input" });
436
+ if (arts.length === 0)
437
+ return { built: false, reason: "no_input" };
438
+
439
+ // Order artifacts by modality priority, then by createdAt
440
+ const sorted = [...arts].sort((a, b) => {
441
+ const pa = PRIORITY_ORDER.indexOf(a.modality);
442
+ const pb = PRIORITY_ORDER.indexOf(b.modality);
443
+ if (pa !== pb) return pa - pb;
444
+ return a.createdAt - b.createdAt;
445
+ });
446
+
447
+ const items = [];
448
+ let tokens = 0;
449
+ for (const a of sorted) {
450
+ const contentStr =
451
+ typeof a.content === "string"
452
+ ? a.content
453
+ : JSON.stringify(a.content ?? "");
454
+ const t = _estimateTokens(contentStr);
455
+ if (tokens + t > maxTokens) {
456
+ const remaining = maxTokens - tokens;
457
+ if (remaining <= 0) break;
458
+ const clipped = contentStr.slice(0, remaining * 4);
459
+ items.push({
460
+ modality: a.modality,
461
+ content: clipped,
462
+ tokens: _estimateTokens(clipped),
463
+ truncated: true,
464
+ });
465
+ tokens += _estimateTokens(clipped);
466
+ break;
467
+ }
468
+ items.push({
469
+ modality: a.modality,
470
+ content: contentStr,
471
+ tokens: t,
472
+ truncated: false,
473
+ });
474
+ tokens += t;
475
+ }
476
+
477
+ const content = items
478
+ .map((i) => `[${i.modality}${i.truncated ? " truncated" : ""}]\n${i.content}`)
479
+ .join("\n\n");
480
+
481
+ const contextData = { items, tokens, maxTokens, content };
482
+ _contextCache.set(sessionId, contextData);
483
+
484
+ db.prepare(
485
+ "UPDATE multimodal_sessions SET context = ?, token_count = ?, updated_at = ? WHERE id = ?",
486
+ ).run(JSON.stringify(contextData), tokens, _now(), sessionId);
487
+
488
+ return {
489
+ built: true,
490
+ tokens,
491
+ maxTokens,
492
+ itemCount: items.length,
493
+ content,
494
+ items,
495
+ };
496
+ }
497
+
498
+ export function getContext(db, sessionId) {
499
+ const cached = _contextCache.get(sessionId);
500
+ if (cached) return cached;
501
+ const s = getSession(db, sessionId);
502
+ if (!s || !s.context) return null;
503
+ return s.context;
504
+ }
505
+
506
+ export function clearContext(db, sessionId) {
507
+ const s = getSession(db, sessionId);
508
+ if (!s) return { cleared: false, reason: "session_not_found" };
509
+ _contextCache.delete(sessionId);
510
+ db.prepare(
511
+ "UPDATE multimodal_sessions SET context = NULL, token_count = 0, updated_at = ? WHERE id = ?",
512
+ ).run(_now(), sessionId);
513
+ return { cleared: true };
514
+ }
515
+
516
+ export function trimContext(context, maxTokens) {
517
+ if (!context || !Array.isArray(context.items))
518
+ return { trimmed: false, reason: "invalid_context" };
519
+ let tokens = 0;
520
+ const items = [];
521
+ for (const i of context.items) {
522
+ const t = i.tokens ?? _estimateTokens(i.content);
523
+ if (tokens + t > maxTokens) {
524
+ const remaining = maxTokens - tokens;
525
+ if (remaining <= 0) break;
526
+ const clipped = (i.content || "").slice(0, remaining * 4);
527
+ items.push({ ...i, content: clipped, tokens: _estimateTokens(clipped), truncated: true });
528
+ tokens += _estimateTokens(clipped);
529
+ break;
530
+ }
531
+ items.push(i);
532
+ tokens += t;
533
+ }
534
+ const content = items
535
+ .map((i) => `[${i.modality}${i.truncated ? " truncated" : ""}]\n${i.content}`)
536
+ .join("\n\n");
537
+ return { trimmed: true, items, tokens, maxTokens, content };
538
+ }
539
+
540
+ /* ── Output generation ──────────────────────────────────── */
541
+
542
+ export function getOutputFormats() {
543
+ return [...OUTPUT_FORMATS];
544
+ }
545
+
546
+ export function generateOutput(db, sessionId, content, format, options = {}) {
547
+ if (!OUTPUT_FORMATS.includes(format))
548
+ return { generated: false, reason: "unsupported_format", format };
549
+
550
+ let produced;
551
+ switch (format) {
552
+ case "markdown":
553
+ produced = typeof content === "string" ? content : JSON.stringify(content, null, 2);
554
+ break;
555
+ case "html":
556
+ produced = _renderHtml(content, options);
557
+ break;
558
+ case "json":
559
+ produced =
560
+ typeof content === "string"
561
+ ? JSON.stringify({ content }, null, 2)
562
+ : JSON.stringify(content, null, 2);
563
+ break;
564
+ case "csv":
565
+ produced = _renderCsv(content);
566
+ break;
567
+ case "slides":
568
+ produced = _renderSlides(content, options);
569
+ break;
570
+ case "chart":
571
+ produced = _renderChart(content, options);
572
+ break;
573
+ default:
574
+ return { generated: false, reason: "unsupported_format", format };
575
+ }
576
+
577
+ if (sessionId) {
578
+ const s = getSession(db, sessionId);
579
+ if (s) {
580
+ _addArtifact(db, sessionId, {
581
+ type: "output",
582
+ format,
583
+ content: produced,
584
+ });
585
+ }
586
+ }
587
+
588
+ return {
589
+ generated: true,
590
+ format,
591
+ content: produced,
592
+ size: typeof produced === "string" ? produced.length : 0,
593
+ };
594
+ }
595
+
596
+ function _renderHtml(content, options) {
597
+ const title = options.title || "Multimodal Output";
598
+ const body =
599
+ typeof content === "string"
600
+ ? content
601
+ : `<pre>${JSON.stringify(content, null, 2)}</pre>`;
602
+ return `<!DOCTYPE html>
603
+ <html><head><meta charset="UTF-8"><title>${title}</title></head>
604
+ <body>
605
+ ${body}
606
+ </body></html>`;
607
+ }
608
+
609
+ function _renderCsv(content) {
610
+ if (typeof content === "string") return content;
611
+ if (!Array.isArray(content)) return "";
612
+ if (content.length === 0) return "";
613
+ const header = Object.keys(content[0]);
614
+ const lines = [header.join(",")];
615
+ for (const row of content) {
616
+ lines.push(header.map((h) => String(row[h] ?? "")).join(","));
617
+ }
618
+ return lines.join("\n");
619
+ }
620
+
621
+ function _renderSlides(content, options) {
622
+ const title = options.title || "Multimodal Slides";
623
+ const slides = Array.isArray(content)
624
+ ? content
625
+ : typeof content === "string"
626
+ ? content.split(/\n---\n/)
627
+ : [String(content)];
628
+ const sections = slides
629
+ .map((s) => `<section>${_escapeHtml(s)}</section>`)
630
+ .join("\n");
631
+ return `<!DOCTYPE html>
632
+ <html><head><meta charset="UTF-8"><title>${title}</title>
633
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@5.0.0/dist/reveal.min.css">
634
+ </head><body>
635
+ <div class="reveal"><div class="slides">
636
+ ${sections}
637
+ </div></div>
638
+ <script src="https://cdn.jsdelivr.net/npm/reveal.js@5.0.0/dist/reveal.min.js"></script>
639
+ <script>Reveal.initialize();</script>
640
+ </body></html>`;
641
+ }
642
+
643
+ function _renderChart(data, options) {
644
+ const chartType = options.chartType || "line";
645
+ const option = {
646
+ title: { text: options.title || "Chart" },
647
+ tooltip: {},
648
+ xAxis: { data: data?.categories || [] },
649
+ yAxis: {},
650
+ series: Array.isArray(data?.series)
651
+ ? data.series.map((s) => ({ ...s, type: s.type || chartType }))
652
+ : [{ name: options.title || "series", type: chartType, data: data?.values || [] }],
653
+ };
654
+ return JSON.stringify(option, null, 2);
655
+ }
656
+
657
+ function _escapeHtml(s) {
658
+ return String(s)
659
+ .replace(/&/g, "&amp;")
660
+ .replace(/</g, "&lt;")
661
+ .replace(/>/g, "&gt;");
662
+ }
663
+
664
+ /* ── Stats ──────────────────────────────────────────────── */
665
+
666
+ 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);
669
+ const byStatus = {};
670
+ let totalTokens = 0;
671
+ for (const r of rows) {
672
+ byStatus[r.status] = (byStatus[r.status] || 0) + 1;
673
+ totalTokens += r.token_count || 0;
674
+ }
675
+ const byModality = {};
676
+ let inputs = 0;
677
+ let outputs = 0;
678
+ for (const a of arts) {
679
+ if (a.type === "input") inputs++;
680
+ else if (a.type === "output") outputs++;
681
+ if (a.modality) byModality[a.modality] = (byModality[a.modality] || 0) + 1;
682
+ }
683
+ return {
684
+ sessions: rows.length,
685
+ byStatus,
686
+ artifacts: arts.length,
687
+ inputs,
688
+ outputs,
689
+ byModality,
690
+ totalTokens,
691
+ };
692
+ }
693
+
694
+ /* ── Reset (tests) ──────────────────────────────────────── */
695
+
696
+ export function _resetState() {
697
+ _contextCache.clear();
698
+ }