chainlesschain 0.51.0 → 0.81.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/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/a2a.js +380 -0
  9. package/src/commands/agent-network.js +785 -0
  10. package/src/commands/automation.js +654 -0
  11. package/src/commands/bi.js +348 -0
  12. package/src/commands/crosschain.js +218 -0
  13. package/src/commands/dao.js +565 -0
  14. package/src/commands/did-v2.js +620 -0
  15. package/src/commands/dlp.js +341 -0
  16. package/src/commands/economy.js +578 -0
  17. package/src/commands/evolution.js +391 -0
  18. package/src/commands/evomap.js +394 -0
  19. package/src/commands/federation.js +283 -0
  20. package/src/commands/hmemory.js +442 -0
  21. package/src/commands/inference.js +318 -0
  22. package/src/commands/lowcode.js +356 -0
  23. package/src/commands/marketplace.js +256 -0
  24. package/src/commands/perf.js +433 -0
  25. package/src/commands/pipeline.js +449 -0
  26. package/src/commands/plugin-ecosystem.js +517 -0
  27. package/src/commands/privacy.js +321 -0
  28. package/src/commands/reputation.js +261 -0
  29. package/src/commands/sandbox.js +401 -0
  30. package/src/commands/siem.js +246 -0
  31. package/src/commands/sla.js +259 -0
  32. package/src/commands/social.js +311 -0
  33. package/src/commands/sso.js +798 -0
  34. package/src/commands/stress.js +230 -0
  35. package/src/commands/terraform.js +245 -0
  36. package/src/commands/workflow.js +320 -0
  37. package/src/commands/zkp.js +562 -1
  38. package/src/index.js +21 -0
  39. package/src/lib/a2a-protocol.js +451 -0
  40. package/src/lib/agent-economy.js +479 -0
  41. package/src/lib/agent-network.js +1121 -0
  42. package/src/lib/app-builder.js +239 -0
  43. package/src/lib/automation-engine.js +948 -0
  44. package/src/lib/bi-engine.js +338 -0
  45. package/src/lib/cross-chain.js +345 -0
  46. package/src/lib/dao-governance.js +569 -0
  47. package/src/lib/did-v2-manager.js +1127 -0
  48. package/src/lib/dlp-engine.js +389 -0
  49. package/src/lib/evolution-system.js +453 -0
  50. package/src/lib/evomap-federation.js +177 -0
  51. package/src/lib/evomap-governance.js +276 -0
  52. package/src/lib/federation-hardening.js +259 -0
  53. package/src/lib/hierarchical-memory.js +481 -0
  54. package/src/lib/inference-network.js +330 -0
  55. package/src/lib/perf-tuning.js +734 -0
  56. package/src/lib/pipeline-orchestrator.js +928 -0
  57. package/src/lib/plugin-ecosystem.js +1109 -0
  58. package/src/lib/privacy-computing.js +427 -0
  59. package/src/lib/reputation-optimizer.js +299 -0
  60. package/src/lib/sandbox-v2.js +306 -0
  61. package/src/lib/siem-exporter.js +333 -0
  62. package/src/lib/skill-marketplace.js +325 -0
  63. package/src/lib/sla-manager.js +275 -0
  64. package/src/lib/social-graph-analytics.js +707 -0
  65. package/src/lib/sso-manager.js +841 -0
  66. package/src/lib/stress-tester.js +330 -0
  67. package/src/lib/terraform-manager.js +363 -0
  68. package/src/lib/workflow-engine.js +454 -1
  69. package/src/lib/zkp-engine.js +523 -20
  70. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +0 -1
@@ -135,3 +135,336 @@ export function _resetState() {
135
135
  _targets.clear();
136
136
  _exports.length = 0;
137
137
  }
138
+
139
+ // ═══════════════════════════════════════════════════════════════
140
+ // V2 Canonical Surface (Phase 51)
141
+ // ═══════════════════════════════════════════════════════════════
142
+
143
+ export const SIEM_FORMAT = Object.freeze({
144
+ JSON: "json",
145
+ CEF: "cef",
146
+ LEEF: "leef",
147
+ });
148
+
149
+ export const SIEM_TARGET_TYPE = Object.freeze({
150
+ SPLUNK_HEC: "splunk_hec",
151
+ ELASTICSEARCH: "elasticsearch",
152
+ AZURE_SENTINEL: "azure_sentinel",
153
+ });
154
+
155
+ export const SIEM_SEVERITY = Object.freeze({
156
+ DEBUG: "debug",
157
+ INFO: "info",
158
+ WARN: "warn",
159
+ ERROR: "error",
160
+ CRITICAL: "critical",
161
+ FATAL: "fatal",
162
+ });
163
+
164
+ export const SIEM_TARGET_STATUS = Object.freeze({
165
+ ACTIVE: "active",
166
+ PAUSED: "paused",
167
+ DISABLED: "disabled",
168
+ ERROR: "error",
169
+ });
170
+
171
+ export const SIEM_DEFAULT_BATCH_SIZE = 100;
172
+
173
+ // Design §二 severity-to-CEF table (0-10 scale, per ArcSight spec).
174
+ const _severityCEF = Object.freeze({
175
+ debug: 1,
176
+ info: 3,
177
+ warn: 5,
178
+ error: 7,
179
+ critical: 9,
180
+ fatal: 10,
181
+ });
182
+
183
+ const _allowedTargetTransitions = new Map([
184
+ ["active", new Set(["paused", "disabled", "error"])],
185
+ ["paused", new Set(["active", "disabled", "error"])],
186
+ ["disabled", new Set(["active", "error"])],
187
+ ["error", new Set(["active", "paused", "disabled"])],
188
+ ]);
189
+
190
+ function _validateFormat(format) {
191
+ if (!Object.values(SIEM_FORMAT).includes(format)) {
192
+ throw new Error(
193
+ `Invalid format: ${format}. Expected one of ${Object.values(SIEM_FORMAT).join("|")}`,
194
+ );
195
+ }
196
+ }
197
+
198
+ function _validateTargetType(type) {
199
+ if (!Object.values(SIEM_TARGET_TYPE).includes(type)) {
200
+ throw new Error(
201
+ `Invalid target type: ${type}. Expected one of ${Object.values(SIEM_TARGET_TYPE).join("|")}`,
202
+ );
203
+ }
204
+ }
205
+
206
+ function _validateSeverity(severity) {
207
+ if (!Object.values(SIEM_SEVERITY).includes(severity)) {
208
+ throw new Error(
209
+ `Invalid severity: ${severity}. Expected one of ${Object.values(SIEM_SEVERITY).join("|")}`,
210
+ );
211
+ }
212
+ }
213
+
214
+ function _validateStatus(status) {
215
+ if (!Object.values(SIEM_TARGET_STATUS).includes(status)) {
216
+ throw new Error(
217
+ `Invalid status: ${status}. Expected one of ${Object.values(SIEM_TARGET_STATUS).join("|")}`,
218
+ );
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Map a SIEM_SEVERITY value to a CEF severity integer (0-10).
224
+ */
225
+ export function severityToCEF(severity) {
226
+ const key = (severity || "").toLowerCase();
227
+ const n = _severityCEF[key];
228
+ if (n === undefined) {
229
+ throw new Error(`Unknown severity: ${severity}`);
230
+ }
231
+ return n;
232
+ }
233
+
234
+ // CEF extension-value escape (| \ = → escaped).
235
+ function _escapeCEFExt(value) {
236
+ return String(value == null ? "" : value)
237
+ .replace(/\\/g, "\\\\")
238
+ .replace(/\|/g, "\\|")
239
+ .replace(/=/g, "\\=")
240
+ .replace(/\r?\n/g, "\\n");
241
+ }
242
+
243
+ function _flatExtensions(log) {
244
+ const pairs = [];
245
+ const md = log.metadata || {};
246
+ for (const [k, v] of Object.entries(md)) {
247
+ pairs.push(`${k}=${_escapeCEFExt(v)}`);
248
+ }
249
+ if (log.userId != null) pairs.push(`suser=${_escapeCEFExt(log.userId)}`);
250
+ if (log.ip != null) pairs.push(`src=${_escapeCEFExt(log.ip)}`);
251
+ if (log.message != null) pairs.push(`msg=${_escapeCEFExt(log.message)}`);
252
+ if (log.timestamp != null) pairs.push(`rt=${_escapeCEFExt(log.timestamp)}`);
253
+ return pairs;
254
+ }
255
+
256
+ /**
257
+ * Format a log record as CEF (ArcSight).
258
+ * CEF:0|Vendor|Product|Version|{eventId}|{eventName}|{severity}|{extensions}
259
+ */
260
+ export function formatLogCEF(log, opts = {}) {
261
+ const vendor = opts.vendor || "ChainlessChain";
262
+ const product = opts.product || "CLI";
263
+ const version = opts.version || "1.0.0";
264
+ const eventId = log.eventId || log.id || "unknown";
265
+ const eventName = log.eventName || log.message || "event";
266
+ const severity = severityToCEF(log.severity || "info");
267
+ const pipeEscape = (s) => String(s).replace(/\|/g, "\\|");
268
+ const extensions = _flatExtensions(log).join(" ");
269
+ return `CEF:0|${pipeEscape(vendor)}|${pipeEscape(product)}|${pipeEscape(version)}|${pipeEscape(eventId)}|${pipeEscape(eventName)}|${severity}|${extensions}`;
270
+ }
271
+
272
+ /**
273
+ * Format a log record as LEEF 2.0 (IBM QRadar).
274
+ * LEEF:2.0|Vendor|Product|Version|{eventId}|<tab-separated key=value>
275
+ */
276
+ export function formatLogLEEF(log, opts = {}) {
277
+ const vendor = opts.vendor || "ChainlessChain";
278
+ const product = opts.product || "CLI";
279
+ const version = opts.version || "1.0.0";
280
+ const eventId = log.eventId || log.id || "unknown";
281
+ const pipeEscape = (s) => String(s).replace(/\|/g, "\\|");
282
+ const pairs = [];
283
+ if (log.timestamp != null) pairs.push(`devTime=${log.timestamp}`);
284
+ if (log.userId != null) pairs.push(`usrName=${log.userId}`);
285
+ if (log.action != null) pairs.push(`action=${log.action}`);
286
+ if (log.ip != null) pairs.push(`src=${log.ip}`);
287
+ if (log.message != null) pairs.push(`msg=${log.message}`);
288
+ const md = log.metadata || {};
289
+ for (const [k, v] of Object.entries(md)) {
290
+ pairs.push(`${k}=${v}`);
291
+ }
292
+ return `LEEF:2.0|${pipeEscape(vendor)}|${pipeEscape(product)}|${pipeEscape(version)}|${pipeEscape(eventId)}|${pairs.join("\t")}`;
293
+ }
294
+
295
+ /**
296
+ * Format a log record as the canonical JSON envelope (Phase 51 §二).
297
+ */
298
+ export function formatLogJSON(log) {
299
+ return {
300
+ timestamp: log.timestamp || Date.now(),
301
+ severity: (log.severity || "info").toUpperCase(),
302
+ source: log.source || "chainlesschain-cli",
303
+ message: log.message || "",
304
+ metadata: {
305
+ ...(log.metadata || {}),
306
+ eventId: log.eventId || log.id || null,
307
+ userId: log.userId || null,
308
+ action: log.action || null,
309
+ resource: log.resource || null,
310
+ ip: log.ip || null,
311
+ },
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Dispatch by format. Returns string for CEF/LEEF, object for JSON.
317
+ */
318
+ export function formatLog(format, log, opts) {
319
+ _validateFormat(format);
320
+ if (format === SIEM_FORMAT.CEF) return formatLogCEF(log, opts);
321
+ if (format === SIEM_FORMAT.LEEF) return formatLogLEEF(log, opts);
322
+ return formatLogJSON(log);
323
+ }
324
+
325
+ /**
326
+ * Add a SIEM target with V2 canonical validation.
327
+ */
328
+ export function addTargetV2(db, options) {
329
+ if (!options || typeof options !== "object") {
330
+ throw new Error("options object is required");
331
+ }
332
+ const { type, url, format = SIEM_FORMAT.JSON, config = {} } = options;
333
+ _validateTargetType(type);
334
+ if (!url) throw new Error("Target URL is required");
335
+ _validateFormat(format);
336
+ return addTarget(db, type, url, format, config);
337
+ }
338
+
339
+ /**
340
+ * Remove a target by id. Throws if unknown.
341
+ */
342
+ export function removeTarget(db, targetId) {
343
+ const target = _targets.get(targetId);
344
+ if (!target) throw new Error(`Target not found: ${targetId}`);
345
+ _targets.delete(targetId);
346
+ db.prepare(`DELETE FROM siem_exports WHERE id = ?`).run(targetId);
347
+ return { success: true, targetId };
348
+ }
349
+
350
+ /**
351
+ * Transition a target's status using the allowed state machine.
352
+ * active ↔ paused/disabled/error; disabled → active/error; error → any.
353
+ */
354
+ export function setTargetStatus(db, targetId, newStatus) {
355
+ const target = _targets.get(targetId);
356
+ if (!target) throw new Error(`Target not found: ${targetId}`);
357
+ _validateStatus(newStatus);
358
+ if (target.status === newStatus) {
359
+ return { ...target };
360
+ }
361
+ const allowed = _allowedTargetTransitions.get(target.status) || new Set();
362
+ if (!allowed.has(newStatus)) {
363
+ throw new Error(
364
+ `Invalid status transition: ${target.status} → ${newStatus}`,
365
+ );
366
+ }
367
+ target.status = newStatus;
368
+ db.prepare(`UPDATE siem_exports SET status = ? WHERE id = ?`).run(
369
+ newStatus,
370
+ targetId,
371
+ );
372
+ return { ...target };
373
+ }
374
+
375
+ /**
376
+ * Batch-export logs to a target.
377
+ * - Skips when target.status !== "active" (returns { skipped: true, ... }).
378
+ * - Chunks logs into slices of batchSize (default 100).
379
+ * - Returns { batches, exported, skipped, lastId }.
380
+ */
381
+ export function exportLogsV2(db, options) {
382
+ if (!options || typeof options !== "object") {
383
+ throw new Error("options object is required");
384
+ }
385
+ const { targetId, logs = [], batchSize = SIEM_DEFAULT_BATCH_SIZE } = options;
386
+ if (!Number.isInteger(batchSize) || batchSize <= 0) {
387
+ throw new Error("batchSize must be a positive integer");
388
+ }
389
+ const target = _targets.get(targetId);
390
+ if (!target) throw new Error(`Target not found: ${targetId}`);
391
+ if (target.status !== SIEM_TARGET_STATUS.ACTIVE) {
392
+ return {
393
+ skipped: true,
394
+ reason: `target status is ${target.status}`,
395
+ batches: 0,
396
+ exported: 0,
397
+ lastId: target.lastExportedLogId,
398
+ };
399
+ }
400
+ if (!Array.isArray(logs)) {
401
+ throw new Error("logs must be an array");
402
+ }
403
+
404
+ let batches = 0;
405
+ let exported = 0;
406
+ for (let i = 0; i < logs.length; i += batchSize) {
407
+ const slice = logs.slice(i, i + batchSize);
408
+ exportLogs(db, targetId, slice);
409
+ batches += 1;
410
+ exported += slice.length;
411
+ }
412
+ // Handle zero-length input — register one empty batch for stats parity.
413
+ if (logs.length === 0) {
414
+ exportLogs(db, targetId, []);
415
+ batches = 0; // no real batch was sent
416
+ }
417
+ return {
418
+ skipped: false,
419
+ batches,
420
+ exported,
421
+ lastId: target.lastExportedLogId,
422
+ };
423
+ }
424
+
425
+ /**
426
+ * Extended stats with byFormat / byType / byStatus breakdowns + totals.
427
+ */
428
+ export function getSIEMStatsV2() {
429
+ const targets = [..._targets.values()];
430
+ const byFormat = {};
431
+ for (const v of Object.values(SIEM_FORMAT)) byFormat[v] = 0;
432
+ const byType = {};
433
+ for (const v of Object.values(SIEM_TARGET_TYPE)) byType[v] = 0;
434
+ const byStatus = {};
435
+ for (const v of Object.values(SIEM_TARGET_STATUS)) byStatus[v] = 0;
436
+ let totalExported = 0;
437
+ for (const t of targets) {
438
+ if (byFormat[t.format] !== undefined) byFormat[t.format] += 1;
439
+ if (byType[t.type] !== undefined) byType[t.type] += 1;
440
+ if (byStatus[t.status] !== undefined) byStatus[t.status] += 1;
441
+ totalExported += t.exportedCount || 0;
442
+ }
443
+ return {
444
+ totalTargets: targets.length,
445
+ totalExported,
446
+ totalBatches: _exports.length,
447
+ byFormat,
448
+ byType,
449
+ byStatus,
450
+ targets: targets.map((t) => ({
451
+ id: t.id,
452
+ type: t.type,
453
+ url: t.url,
454
+ format: t.format,
455
+ status: t.status,
456
+ exportedCount: t.exportedCount,
457
+ lastExportAt: t.lastExportAt,
458
+ })),
459
+ };
460
+ }
461
+
462
+ /**
463
+ * List targets filtered by status.
464
+ */
465
+ export function listTargetsByStatus(status) {
466
+ _validateStatus(status);
467
+ return [..._targets.values()].filter((t) => t.status === status);
468
+ }
469
+
470
+ export { _severityCEF, _allowedTargetTransitions };
@@ -394,4 +394,329 @@ export function _resetState() {
394
394
  _services.clear();
395
395
  _invocations.clear();
396
396
  _seq = 0;
397
+ _maxConcurrentInvocationsPerService =
398
+ DEFAULT_MAX_CONCURRENT_INVOCATIONS_PER_SERVICE;
399
+ }
400
+
401
+ /* ═══════════════════════════════════════════════════════════════
402
+ * V2 (Phase 65) — Frozen enums + async invocation lifecycle +
403
+ * per-service concurrency cap + patch-merged setInvocationStatus +
404
+ * stats-v2. Strictly additive on top of the legacy surface above.
405
+ * ═══════════════════════════════════════════════════════════════ */
406
+
407
+ export const SERVICE_STATUS_V2 = Object.freeze({
408
+ DRAFT: "draft",
409
+ PUBLISHED: "published",
410
+ DEPRECATED: "deprecated",
411
+ SUSPENDED: "suspended",
412
+ });
413
+
414
+ export const INVOCATION_STATUS_V2 = Object.freeze({
415
+ PENDING: "pending",
416
+ RUNNING: "running",
417
+ SUCCESS: "success",
418
+ FAILED: "failed",
419
+ TIMEOUT: "timeout",
420
+ });
421
+
422
+ export const PRICING_MODEL_V2 = Object.freeze({
423
+ FREE: "free",
424
+ PAY_PER_CALL: "pay_per_call",
425
+ SUBSCRIPTION: "subscription",
426
+ TIERED: "tiered",
427
+ });
428
+
429
+ const DEFAULT_MAX_CONCURRENT_INVOCATIONS_PER_SERVICE = 10;
430
+ let _maxConcurrentInvocationsPerService =
431
+ DEFAULT_MAX_CONCURRENT_INVOCATIONS_PER_SERVICE;
432
+ export const MARKETPLACE_DEFAULT_MAX_CONCURRENT_INVOCATIONS =
433
+ DEFAULT_MAX_CONCURRENT_INVOCATIONS_PER_SERVICE;
434
+
435
+ export function setMaxConcurrentInvocations(n) {
436
+ if (typeof n !== "number" || !Number.isFinite(n) || n < 1) {
437
+ throw new Error("maxConcurrentInvocations must be a positive integer");
438
+ }
439
+ _maxConcurrentInvocationsPerService = Math.floor(n);
440
+ return _maxConcurrentInvocationsPerService;
441
+ }
442
+
443
+ export function getMaxConcurrentInvocations() {
444
+ return _maxConcurrentInvocationsPerService;
445
+ }
446
+
447
+ export function getActiveInvocationCount(serviceId) {
448
+ let count = 0;
449
+ for (const inv of _invocations.values()) {
450
+ if (
451
+ (serviceId == null || inv.serviceId === serviceId) &&
452
+ (inv.status === INVOCATION_STATUS_V2.PENDING ||
453
+ inv.status === INVOCATION_STATUS_V2.RUNNING)
454
+ ) {
455
+ count++;
456
+ }
457
+ }
458
+ return count;
459
+ }
460
+
461
+ // Invocation state machine:
462
+ // pending → { running, failed, timeout }
463
+ // running → { success, failed, timeout }
464
+ // success/failed/timeout are terminal.
465
+ const _invocationTerminal = new Set([
466
+ INVOCATION_STATUS_V2.SUCCESS,
467
+ INVOCATION_STATUS_V2.FAILED,
468
+ INVOCATION_STATUS_V2.TIMEOUT,
469
+ ]);
470
+ const _invocationAllowed = new Map([
471
+ [
472
+ INVOCATION_STATUS_V2.PENDING,
473
+ new Set([
474
+ INVOCATION_STATUS_V2.RUNNING,
475
+ INVOCATION_STATUS_V2.FAILED,
476
+ INVOCATION_STATUS_V2.TIMEOUT,
477
+ ]),
478
+ ],
479
+ [
480
+ INVOCATION_STATUS_V2.RUNNING,
481
+ new Set([
482
+ INVOCATION_STATUS_V2.SUCCESS,
483
+ INVOCATION_STATUS_V2.FAILED,
484
+ INVOCATION_STATUS_V2.TIMEOUT,
485
+ ]),
486
+ ],
487
+ [INVOCATION_STATUS_V2.SUCCESS, new Set([])],
488
+ [INVOCATION_STATUS_V2.FAILED, new Set([])],
489
+ [INVOCATION_STATUS_V2.TIMEOUT, new Set([])],
490
+ ]);
491
+
492
+ /**
493
+ * beginInvocationV2 — creates a PENDING invocation row (no output/duration).
494
+ * Caller drives the transition via startInvocation (→ RUNNING),
495
+ * completeInvocation (→ SUCCESS), failInvocation / timeoutInvocation,
496
+ * or the generic setInvocationStatus.
497
+ */
498
+ export function beginInvocationV2(db, config = {}) {
499
+ const serviceId = String(config.serviceId || "").trim();
500
+ if (!serviceId) throw new Error("serviceId is required");
501
+ const service = _mustGetService(serviceId);
502
+ if (service.status !== SERVICE_STATUS_V2.PUBLISHED) {
503
+ throw new Error(
504
+ `Cannot invoke non-published service (status=${service.status})`,
505
+ );
506
+ }
507
+
508
+ const activeCount = getActiveInvocationCount(serviceId);
509
+ if (activeCount >= _maxConcurrentInvocationsPerService) {
510
+ throw new Error(
511
+ `Max concurrent invocations reached: ${activeCount}/${_maxConcurrentInvocationsPerService}`,
512
+ );
513
+ }
514
+
515
+ const callerId = config.callerId ? String(config.callerId).trim() : null;
516
+ const input = config.input ?? null;
517
+ const now = Number(config.now ?? Date.now());
518
+ const id = config.id || crypto.randomUUID();
519
+
520
+ const invocation = {
521
+ id,
522
+ serviceId,
523
+ callerId,
524
+ input,
525
+ output: null,
526
+ status: INVOCATION_STATUS_V2.PENDING,
527
+ durationMs: null,
528
+ error: null,
529
+ startedAt: null,
530
+ completedAt: null,
531
+ createdAt: now,
532
+ _seq: ++_seq,
533
+ };
534
+ _invocations.set(id, invocation);
535
+
536
+ if (db) {
537
+ db.prepare(
538
+ `INSERT INTO skill_invocations (id, service_id, caller_id, input, output, status, duration_ms, error, created_at)
539
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
540
+ ).run(
541
+ id,
542
+ serviceId,
543
+ callerId,
544
+ input == null ? null : JSON.stringify(input),
545
+ null,
546
+ INVOCATION_STATUS_V2.PENDING,
547
+ null,
548
+ null,
549
+ now,
550
+ );
551
+ }
552
+
553
+ return _strip(invocation);
554
+ }
555
+
556
+ function _mustGetInvocation(invocationId) {
557
+ const inv = _invocations.get(invocationId);
558
+ if (!inv) throw new Error(`Invocation not found: ${invocationId}`);
559
+ return inv;
560
+ }
561
+
562
+ function _persistInvocation(db, invocationId, fields) {
563
+ if (!db) return;
564
+ const setClauses = Object.keys(fields)
565
+ .map((k) => `${k} = ?`)
566
+ .join(", ");
567
+ const values = Object.values(fields).map((v) =>
568
+ v && typeof v === "object" ? JSON.stringify(v) : v,
569
+ );
570
+ db.prepare(`UPDATE skill_invocations SET ${setClauses} WHERE id = ?`).run(
571
+ ...values,
572
+ invocationId,
573
+ );
574
+ }
575
+
576
+ export function startInvocation(db, invocationId, opts = {}) {
577
+ return setInvocationStatus(db, invocationId, INVOCATION_STATUS_V2.RUNNING, {
578
+ startedAt: Number(opts.now ?? Date.now()),
579
+ });
580
+ }
581
+
582
+ export function completeInvocationV2(db, invocationId, opts = {}) {
583
+ return setInvocationStatus(db, invocationId, INVOCATION_STATUS_V2.SUCCESS, {
584
+ output: opts.output ?? null,
585
+ durationMs: opts.durationMs == null ? null : Number(opts.durationMs),
586
+ });
587
+ }
588
+
589
+ export function failInvocationV2(db, invocationId, errorMessage, opts = {}) {
590
+ return setInvocationStatus(db, invocationId, INVOCATION_STATUS_V2.FAILED, {
591
+ error: errorMessage ? String(errorMessage) : null,
592
+ durationMs: opts.durationMs == null ? null : Number(opts.durationMs),
593
+ });
594
+ }
595
+
596
+ export function timeoutInvocationV2(db, invocationId, opts = {}) {
597
+ return setInvocationStatus(db, invocationId, INVOCATION_STATUS_V2.TIMEOUT, {
598
+ error: opts.error ? String(opts.error) : "timeout",
599
+ durationMs: opts.durationMs == null ? null : Number(opts.durationMs),
600
+ });
601
+ }
602
+
603
+ export function setInvocationStatus(db, invocationId, newStatus, patch = {}) {
604
+ const inv = _mustGetInvocation(invocationId);
605
+
606
+ if (!Object.values(INVOCATION_STATUS_V2).includes(newStatus)) {
607
+ throw new Error(`Unknown invocation status: ${newStatus}`);
608
+ }
609
+
610
+ const allowed = _invocationAllowed.get(inv.status);
611
+ if (!allowed || !allowed.has(newStatus)) {
612
+ throw new Error(
613
+ `Invalid invocation status transition: ${inv.status} → ${newStatus}`,
614
+ );
615
+ }
616
+
617
+ const now = Date.now();
618
+ inv.status = newStatus;
619
+
620
+ const dbFields = { status: newStatus };
621
+
622
+ if (patch.output !== undefined) {
623
+ inv.output = patch.output;
624
+ dbFields.output =
625
+ patch.output == null ? null : JSON.stringify(patch.output);
626
+ }
627
+ if (patch.error !== undefined) {
628
+ inv.error = patch.error;
629
+ dbFields.error = patch.error;
630
+ }
631
+ if (patch.durationMs !== undefined) {
632
+ const d = patch.durationMs == null ? null : Number(patch.durationMs);
633
+ if (d != null && (Number.isNaN(d) || d < 0)) {
634
+ throw new Error(`Invalid durationMs: ${patch.durationMs}`);
635
+ }
636
+ inv.durationMs = d;
637
+ dbFields.duration_ms = d;
638
+ }
639
+ if (patch.startedAt !== undefined) {
640
+ inv.startedAt = patch.startedAt;
641
+ }
642
+
643
+ if (_invocationTerminal.has(newStatus) && inv.completedAt == null) {
644
+ inv.completedAt = now;
645
+ // Bump the service's invocation counter once on terminal.
646
+ const service = _services.get(inv.serviceId);
647
+ if (service) {
648
+ service.invocationCount = (service.invocationCount || 0) + 1;
649
+ service.updatedAt = now;
650
+ _persistService(db, inv.serviceId, {
651
+ invocation_count: service.invocationCount,
652
+ updated_at: now,
653
+ });
654
+ }
655
+ }
656
+
657
+ _persistInvocation(db, invocationId, dbFields);
658
+
659
+ return _strip(inv);
660
+ }
661
+
662
+ export function getMarketplaceStatsV2() {
663
+ const services = [..._services.values()];
664
+ const invocations = [..._invocations.values()];
665
+
666
+ const servicesByStatus = {};
667
+ for (const s of Object.values(SERVICE_STATUS_V2)) servicesByStatus[s] = 0;
668
+ for (const s of services)
669
+ servicesByStatus[s.status] = (servicesByStatus[s.status] || 0) + 1;
670
+
671
+ const invocationsByStatus = {};
672
+ for (const s of Object.values(INVOCATION_STATUS_V2))
673
+ invocationsByStatus[s] = 0;
674
+ for (const inv of invocations)
675
+ invocationsByStatus[inv.status] =
676
+ (invocationsByStatus[inv.status] || 0) + 1;
677
+
678
+ const servicesByPricing = {};
679
+ for (const p of Object.values(PRICING_MODEL_V2)) servicesByPricing[p] = 0;
680
+ for (const s of services) {
681
+ const model =
682
+ s.pricing && typeof s.pricing === "object" && s.pricing.model
683
+ ? String(s.pricing.model)
684
+ : PRICING_MODEL_V2.FREE;
685
+ if (servicesByPricing[model] == null) servicesByPricing[model] = 0;
686
+ servicesByPricing[model]++;
687
+ }
688
+
689
+ let totalDuration = 0;
690
+ let durationSamples = 0;
691
+ for (const inv of invocations) {
692
+ if (inv.durationMs != null && inv.status === INVOCATION_STATUS_V2.SUCCESS) {
693
+ totalDuration += inv.durationMs;
694
+ durationSamples++;
695
+ }
696
+ }
697
+ const avgDurationMs =
698
+ durationSamples > 0
699
+ ? Number((totalDuration / durationSamples).toFixed(1))
700
+ : 0;
701
+ const successRate =
702
+ invocations.length > 0
703
+ ? Number(
704
+ (
705
+ invocationsByStatus[INVOCATION_STATUS_V2.SUCCESS] /
706
+ invocations.length
707
+ ).toFixed(3),
708
+ )
709
+ : 0;
710
+
711
+ return {
712
+ totalServices: services.length,
713
+ totalInvocations: invocations.length,
714
+ activeInvocations: getActiveInvocationCount(),
715
+ maxConcurrentInvocations: _maxConcurrentInvocationsPerService,
716
+ servicesByStatus,
717
+ invocationsByStatus,
718
+ servicesByPricing,
719
+ avgDurationMs,
720
+ successRate,
721
+ };
397
722
  }