@barivia/barsom-mcp 0.22.2 → 0.23.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.
- package/dist/audit.js +3 -1
- package/dist/deferred_feedback.js +88 -0
- package/dist/index.js +1 -1
- package/dist/inventory_format.js +69 -0
- package/dist/prepare_training_prompt.js +1 -0
- package/dist/shared.js +301 -49
- package/dist/tools/account.js +118 -8
- package/dist/tools/datasets.js +61 -25
- package/dist/tools/explore_map.js +37 -18
- package/dist/tools/feedback.js +72 -6
- package/dist/tools/guide_barsom.js +9 -4
- package/dist/tools/jobs.js +73 -16
- package/dist/tools/results.js +7 -1
- package/dist/tools/train.js +28 -5
- package/dist/tools/training_core.js +6 -1
- package/dist/tools/training_monitor.js +80 -37
- package/dist/train_submit_message.js +5 -7
- package/dist/training_monitor_curve.js +32 -0
- package/dist/ui-delivery.js +185 -0
- package/dist/views/src/views/results-explorer/index.html +16 -13
- package/dist/views/src/views/training-monitor/index.html +30 -18
- package/dist/viz_links.js +25 -0
- package/package.json +1 -1
package/dist/shared.js
CHANGED
|
@@ -20,12 +20,16 @@ export const API_URL = process.env.BARIVIA_API_URL ??
|
|
|
20
20
|
"https://api.barivia.se";
|
|
21
21
|
export const API_KEY = process.env.BARIVIA_API_KEY ?? process.env.BARSOM_API_KEY ?? "";
|
|
22
22
|
export const FETCH_TIMEOUT_MS = parseInt(process.env.BARIVIA_FETCH_TIMEOUT_MS ?? "60000", 10);
|
|
23
|
-
|
|
24
|
-
export const
|
|
23
|
+
/** Retries after the first attempt; default 2 → 3 total tries. Override with BARIVIA_MAX_RETRIES. */
|
|
24
|
+
export const MAX_RETRIES = parseInt(process.env.BARIVIA_MAX_RETRIES ?? "2", 10);
|
|
25
|
+
/** Delay between burst retries (ms). Default 2000 → ~2s between each of the 3 attempts. */
|
|
26
|
+
export const RETRY_BASE_MS = parseInt(process.env.BARIVIA_RETRY_BASE_MS ?? "2000", 10);
|
|
25
27
|
export const RETRYABLE_STATUS = new Set([502, 503, 504]);
|
|
26
|
-
/**
|
|
27
|
-
export
|
|
28
|
-
|
|
28
|
+
/** Cool-down hint after the burst is exhausted — agents should wait before another round. */
|
|
29
|
+
export const UNAVAILABLE_RETRY_AFTER_SEC = 30;
|
|
30
|
+
/** Fixed delay between burst retries (not exponential). */
|
|
31
|
+
export function retryDelayMs(_attempt) {
|
|
32
|
+
return RETRY_BASE_MS;
|
|
29
33
|
}
|
|
30
34
|
function newRequestId() {
|
|
31
35
|
return randomUUID();
|
|
@@ -35,7 +39,7 @@ function newRequestId() {
|
|
|
35
39
|
* X-Barsom-Client-Version so the server can annotate tool guidance with the
|
|
36
40
|
* wrapper version each action requires. Keep in sync with package.json on bump.
|
|
37
41
|
*/
|
|
38
|
-
export const CLIENT_VERSION = "0.
|
|
42
|
+
export const CLIENT_VERSION = "0.23.0";
|
|
39
43
|
/** User-facing links; keep aligned with barivia.se / api.barivia.se. */
|
|
40
44
|
export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
|
|
41
45
|
/** Self-serve account dashboard (manage plan, billing, and API keys). */
|
|
@@ -73,6 +77,51 @@ export const LARGE_UPLOAD_BYTES = 64 * 1024 * 1024; // 64 MB
|
|
|
73
77
|
export const PRESIGNED_PUT_TIMEOUT_MS = 30 * 60_000; // 30 min
|
|
74
78
|
/** Poll window for the async stage_dataset job (large files take minutes). */
|
|
75
79
|
export const POLL_STAGE_MAX_MS = 30 * 60_000; // 30 min
|
|
80
|
+
export const BARSOM_VIZ_VIEWS = {
|
|
81
|
+
trainingMonitor: "training-monitor",
|
|
82
|
+
resultsExplorer: "results-explorer",
|
|
83
|
+
};
|
|
84
|
+
export function buildStandaloneVizPageUrl(port, viewPath, jobId) {
|
|
85
|
+
if (!port)
|
|
86
|
+
return undefined;
|
|
87
|
+
return `http://localhost:${port}/viz/${viewPath}?mode=standalone&job_id=${encodeURIComponent(jobId)}`;
|
|
88
|
+
}
|
|
89
|
+
export function buildVizRelatedUrls(port, jobId, views) {
|
|
90
|
+
if (!port)
|
|
91
|
+
return undefined;
|
|
92
|
+
return {
|
|
93
|
+
trainingMonitor: buildStandaloneVizPageUrl(port, views.trainingMonitor, jobId),
|
|
94
|
+
resultsExplorer: buildStandaloneVizPageUrl(port, views.resultsExplorer, jobId),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** Prominent standalone URL blocks for tool results — always emitted when a viz port exists. */
|
|
98
|
+
export function buildVizUrlContentBlocks(options) {
|
|
99
|
+
const { label, toolName, standaloneUrl, port, jobId, clientSupportsMcpApps } = options;
|
|
100
|
+
if (!standaloneUrl)
|
|
101
|
+
return [];
|
|
102
|
+
const embedNote = clientSupportsMcpApps
|
|
103
|
+
? "Embedded panel if your host supports MCP Apps — otherwise open the standalone URL below (expected on some Cursor builds)."
|
|
104
|
+
: "Open the standalone URL below when no inline panel appears.";
|
|
105
|
+
const health = port > 0
|
|
106
|
+
? ` Health check: http://localhost:${port}/api/health?job_id=${encodeURIComponent(jobId)}`
|
|
107
|
+
: "";
|
|
108
|
+
const blocks = [
|
|
109
|
+
{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: `${embedNote}\n` +
|
|
112
|
+
`Standalone ${label} URL (copy if Open is blocked):\n${standaloneUrl}\n` +
|
|
113
|
+
`AGENT: surface this URL prominently in your reply — use a markdown link "[Open ${label}](${standaloneUrl})" or tell the user to copy/paste into a browser. Do not bury it after metrics.\n` +
|
|
114
|
+
`This localhost port is per MCP session and goes stale if the proxy restarts — re-run ${toolName} for a fresh URL, or set BARIVIA_VIZ_PORT for a persistent port.${health}`,
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
if (!clientSupportsMcpApps) {
|
|
118
|
+
blocks.push({
|
|
119
|
+
type: "text",
|
|
120
|
+
text: `[Open ${label}](${standaloneUrl})`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return blocks;
|
|
124
|
+
}
|
|
76
125
|
// ---------------------------------------------------------------------------
|
|
77
126
|
// Shared state (mutable)
|
|
78
127
|
// ---------------------------------------------------------------------------
|
|
@@ -329,52 +378,237 @@ export async function resolveFilePathForUpload(filePath, mcpServer) {
|
|
|
329
378
|
`"C:\\\\Users\\\\you\\\\data.csv") or a file:// URI. For relative paths, set BARIVIA_WORKSPACE_ROOT in your MCP config.`);
|
|
330
379
|
}
|
|
331
380
|
}
|
|
332
|
-
|
|
333
|
-
|
|
381
|
+
function looksLikeHtml(bodyText) {
|
|
382
|
+
const t = bodyText.trim().slice(0, 200).toLowerCase();
|
|
383
|
+
return t.startsWith("<!doctype") || t.startsWith("<html") || /<\s*html[\s>]/.test(t);
|
|
384
|
+
}
|
|
385
|
+
function isStorageErrorCode(code) {
|
|
386
|
+
if (!code)
|
|
387
|
+
return false;
|
|
388
|
+
const c = code.toLowerCase();
|
|
389
|
+
return c === "storage_upload_failed" || (c.includes("storage") && !c.includes("unavailable"));
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Classify an HTTP API failure into a compact structured error.
|
|
393
|
+
* Gateway HTML 502/503 bodies are never echoed — they become api_unreachable.
|
|
394
|
+
*/
|
|
395
|
+
export function classifyApiError(status, bodyText, requestId) {
|
|
334
396
|
let parsed = null;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
397
|
+
const html = looksLikeHtml(bodyText);
|
|
398
|
+
if (!html && bodyText.trim()) {
|
|
399
|
+
try {
|
|
400
|
+
const j = JSON.parse(bodyText);
|
|
401
|
+
if (j && typeof j === "object")
|
|
402
|
+
parsed = j;
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
/* non-JSON */
|
|
406
|
+
}
|
|
342
407
|
}
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
408
|
+
const apiCode = parsed?.error_code != null ? String(parsed.error_code) : "";
|
|
409
|
+
const apiMsg = parsed?.error != null && String(parsed.error).trim()
|
|
410
|
+
? String(parsed.error).trim()
|
|
411
|
+
: "";
|
|
412
|
+
let cause = "server";
|
|
413
|
+
let error_code = apiCode || `http_${status}`;
|
|
414
|
+
let message = apiMsg || `HTTP ${status}`;
|
|
415
|
+
let hint = "";
|
|
416
|
+
let retryable = status === 502 || status === 503 || status === 504;
|
|
346
417
|
const accountHint = ` Check your email or manage your key at ${PUBLIC_DASHBOARD_URL} if needed.`;
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
418
|
+
if (status === 400) {
|
|
419
|
+
cause = "client";
|
|
420
|
+
hint = "Check parameter types and required fields.";
|
|
421
|
+
retryable = false;
|
|
422
|
+
}
|
|
423
|
+
else if (status === 401) {
|
|
424
|
+
cause = "auth";
|
|
425
|
+
error_code = apiCode || "unauthorized";
|
|
426
|
+
hint = `Check BARIVIA_API_KEY in your MCP config.${accountHint}`;
|
|
427
|
+
retryable = false;
|
|
428
|
+
}
|
|
429
|
+
else if (status === 403) {
|
|
430
|
+
cause = "auth";
|
|
431
|
+
error_code = apiCode || "forbidden";
|
|
432
|
+
hint = `Access denied for this operation (plan or permissions).${accountHint}`;
|
|
433
|
+
retryable = false;
|
|
434
|
+
}
|
|
435
|
+
else if (status === 402) {
|
|
436
|
+
cause = "client";
|
|
437
|
+
hint = `Credits or subscription may be insufficient — upgrade at ${PUBLIC_DASHBOARD_URL}.`;
|
|
438
|
+
retryable = false;
|
|
439
|
+
}
|
|
440
|
+
else if (status === 404) {
|
|
441
|
+
cause = "client";
|
|
442
|
+
hint = "The resource may not exist or may have been deleted.";
|
|
443
|
+
retryable = false;
|
|
444
|
+
}
|
|
445
|
+
else if (status === 409) {
|
|
446
|
+
cause = "client";
|
|
447
|
+
hint = "The job may not be in the expected state.";
|
|
448
|
+
retryable = false;
|
|
449
|
+
}
|
|
450
|
+
else if (status === 429) {
|
|
451
|
+
cause = "rate_limit";
|
|
452
|
+
hint = "Plan limit (e.g. dataset cap) or rate limit — delete unused datasets or wait and retry.";
|
|
453
|
+
retryable = true;
|
|
454
|
+
}
|
|
455
|
+
else if (status === 502 || status === 503 || status === 504) {
|
|
456
|
+
if (isStorageErrorCode(apiCode) || /object storage|r2\/s3|storage upload/i.test(apiMsg)) {
|
|
457
|
+
cause = "storage";
|
|
458
|
+
error_code = apiCode || "storage_upload_failed";
|
|
459
|
+
message = apiMsg || "Object storage (R2/S3) error from API.";
|
|
460
|
+
hint = "Retry later; feedback may still succeed via DB when storage is degraded.";
|
|
461
|
+
}
|
|
462
|
+
else if (html || !parsed) {
|
|
463
|
+
cause = "api_unreachable";
|
|
464
|
+
error_code = apiCode || "api_unreachable";
|
|
465
|
+
message = "API temporarily unavailable (gateway/tunnel returned a non-JSON error).";
|
|
466
|
+
hint =
|
|
467
|
+
`Proxy already tried 3 times (~2s apart). Wait ~${UNAVAILABLE_RETRY_AFTER_SEC}s, then retry. ` +
|
|
468
|
+
"Use account(action=health) — /health does not depend on R2. Pause — do not paste HTML.";
|
|
469
|
+
}
|
|
470
|
+
else if (apiCode === "system_info_unavailable") {
|
|
471
|
+
cause = "server";
|
|
472
|
+
message = apiMsg || "System info (plan/queue) temporarily unavailable.";
|
|
473
|
+
hint = `Wait ~${UNAVAILABLE_RETRY_AFTER_SEC}s, then retry.`;
|
|
474
|
+
}
|
|
475
|
+
else if (apiCode === "feedback_unavailable" || apiCode === "database_error") {
|
|
476
|
+
cause = "server";
|
|
477
|
+
error_code = apiCode;
|
|
478
|
+
message = apiMsg || "API or database temporarily unavailable.";
|
|
479
|
+
hint =
|
|
480
|
+
`Wait ~${UNAVAILABLE_RETRY_AFTER_SEC}s; send_feedback queues locally if the API stays down.`;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
cause = "server";
|
|
484
|
+
message = apiMsg || `HTTP ${status}`;
|
|
485
|
+
hint = `API or database temporarily unavailable — wait ~${UNAVAILABLE_RETRY_AFTER_SEC}s, then retry.`;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
else if (status >= 500) {
|
|
489
|
+
cause = "server";
|
|
490
|
+
hint = `Server error — wait ~${UNAVAILABLE_RETRY_AFTER_SEC}s, then retry.`;
|
|
491
|
+
retryable = true;
|
|
492
|
+
}
|
|
493
|
+
else if (!apiMsg && html) {
|
|
494
|
+
cause = "api_unreachable";
|
|
495
|
+
error_code = "api_unreachable";
|
|
496
|
+
message = `HTTP ${status} (non-JSON gateway body)`;
|
|
497
|
+
hint = `Wait ~${UNAVAILABLE_RETRY_AFTER_SEC}s, then retry. Do not paste HTML.`;
|
|
498
|
+
retryable = status >= 500;
|
|
499
|
+
}
|
|
500
|
+
const retry_after_sec = retryable ? UNAVAILABLE_RETRY_AFTER_SEC : undefined;
|
|
501
|
+
return {
|
|
502
|
+
status,
|
|
503
|
+
error_code,
|
|
504
|
+
message,
|
|
505
|
+
hint,
|
|
506
|
+
request_id: requestId,
|
|
507
|
+
retryable,
|
|
508
|
+
cause,
|
|
509
|
+
...(retry_after_sec !== undefined ? { retry_after_sec } : {}),
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
/** User-visible API error line (slim; includes request id). Exported for tests. */
|
|
513
|
+
export function formatApiErrorMessage(status, bodyText, requestId) {
|
|
514
|
+
const c = classifyApiError(status, bodyText, requestId);
|
|
515
|
+
const parts = [
|
|
516
|
+
c.message,
|
|
517
|
+
`error_code=${c.error_code}`,
|
|
518
|
+
`cause=${c.cause}`,
|
|
519
|
+
c.retry_after_sec != null ? `retry_after_sec=${c.retry_after_sec}` : "",
|
|
520
|
+
c.hint,
|
|
521
|
+
`request_id=${c.request_id}`,
|
|
522
|
+
].filter((p) => p && String(p).trim());
|
|
523
|
+
return parts.join(" | ");
|
|
372
524
|
}
|
|
373
525
|
function throwApiError(status, bodyText, requestId) {
|
|
526
|
+
const classified = classifyApiError(status, bodyText, requestId);
|
|
374
527
|
const err = new Error(formatApiErrorMessage(status, bodyText, requestId));
|
|
375
528
|
err.httpStatus = status;
|
|
529
|
+
err.errorCode = classified.error_code;
|
|
530
|
+
err.requestId = requestId;
|
|
531
|
+
err.retryable = classified.retryable;
|
|
532
|
+
err.cause = classified.cause;
|
|
533
|
+
err.retryAfterSec = classified.retry_after_sec;
|
|
534
|
+
err.classified = classified;
|
|
376
535
|
throw err;
|
|
377
536
|
}
|
|
537
|
+
/** Final network failure after the burst — same calm shape as gateway HTML 502. */
|
|
538
|
+
function throwNetworkUnreachable(requestId, err) {
|
|
539
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
540
|
+
const classified = {
|
|
541
|
+
status: 0,
|
|
542
|
+
error_code: "api_unreachable",
|
|
543
|
+
message: "API temporarily unavailable (network error).",
|
|
544
|
+
hint: `Proxy already tried 3 times (~2s apart). Wait ~${UNAVAILABLE_RETRY_AFTER_SEC}s, then retry. ` +
|
|
545
|
+
`Detail: ${detail.slice(0, 120)}`,
|
|
546
|
+
request_id: requestId,
|
|
547
|
+
retryable: true,
|
|
548
|
+
cause: "api_unreachable",
|
|
549
|
+
retry_after_sec: UNAVAILABLE_RETRY_AFTER_SEC,
|
|
550
|
+
};
|
|
551
|
+
const msg = [
|
|
552
|
+
classified.message,
|
|
553
|
+
`error_code=${classified.error_code}`,
|
|
554
|
+
`cause=${classified.cause}`,
|
|
555
|
+
`retry_after_sec=${classified.retry_after_sec}`,
|
|
556
|
+
classified.hint,
|
|
557
|
+
`request_id=${classified.request_id}`,
|
|
558
|
+
].join(" | ");
|
|
559
|
+
const out = new Error(msg);
|
|
560
|
+
out.httpStatus = 0;
|
|
561
|
+
out.errorCode = classified.error_code;
|
|
562
|
+
out.requestId = requestId;
|
|
563
|
+
out.retryable = true;
|
|
564
|
+
out.cause = "api_unreachable";
|
|
565
|
+
out.retryAfterSec = UNAVAILABLE_RETRY_AFTER_SEC;
|
|
566
|
+
out.classified = classified;
|
|
567
|
+
throw out;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Liveness (+ optional readiness) probe against the API origin.
|
|
571
|
+
* Uses GET /health which never touches R2 — safe during storage outages.
|
|
572
|
+
*/
|
|
573
|
+
export async function probeApiHealth() {
|
|
574
|
+
const base = { storage: "not_probed" };
|
|
575
|
+
try {
|
|
576
|
+
const live = await fetchWithTimeout(`${API_URL}/health`, { method: "GET" }, 8_000);
|
|
577
|
+
const liveText = await live.text();
|
|
578
|
+
if (!live.ok) {
|
|
579
|
+
return { ...base, liveness: "down", readiness: "unknown", detail: `health HTTP ${live.status}` };
|
|
580
|
+
}
|
|
581
|
+
let readiness = "unknown";
|
|
582
|
+
let db;
|
|
583
|
+
let redis;
|
|
584
|
+
try {
|
|
585
|
+
const ready = await fetchWithTimeout(`${API_URL}/ready`, { method: "GET" }, 8_000);
|
|
586
|
+
const readyText = await ready.text();
|
|
587
|
+
try {
|
|
588
|
+
const j = JSON.parse(readyText);
|
|
589
|
+
readiness = ready.ok && j.status === "ready" ? "ready" : "not_ready";
|
|
590
|
+
db = j.db;
|
|
591
|
+
redis = j.redis;
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
readiness = ready.ok ? "ready" : "not_ready";
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
readiness = "unknown";
|
|
599
|
+
}
|
|
600
|
+
void liveText;
|
|
601
|
+
return { ...base, liveness: "ok", readiness, db, redis };
|
|
602
|
+
}
|
|
603
|
+
catch (err) {
|
|
604
|
+
return {
|
|
605
|
+
...base,
|
|
606
|
+
liveness: "down",
|
|
607
|
+
readiness: "unknown",
|
|
608
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
378
612
|
/**
|
|
379
613
|
* @param requestTimeoutMs Optional per-request timeout (default `BARIVIA_FETCH_TIMEOUT_MS`).
|
|
380
614
|
*/
|
|
@@ -487,9 +721,13 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
|
|
|
487
721
|
!err.httpStatus) {
|
|
488
722
|
throw new Error(`Request timed out after ${effectiveTimeout}ms. Increase BARIVIA_FETCH_TIMEOUT_MS in your MCP env (e.g. 120000) for slow or large requests. (request id: ${requestId})`);
|
|
489
723
|
}
|
|
724
|
+
if (err instanceof TypeError)
|
|
725
|
+
throwNetworkUnreachable(requestId, err);
|
|
490
726
|
throw err;
|
|
491
727
|
}
|
|
492
728
|
}
|
|
729
|
+
if (lastError instanceof TypeError)
|
|
730
|
+
throwNetworkUnreachable(requestId, lastError);
|
|
493
731
|
throw lastError;
|
|
494
732
|
}
|
|
495
733
|
/** Fetch raw bytes from the API (for image downloads). */
|
|
@@ -551,9 +789,13 @@ export async function apiRawCall(path, requestTimeoutMs) {
|
|
|
551
789
|
!err.httpStatus) {
|
|
552
790
|
throw new Error(`Request timed out after ${effectiveTimeout}ms. Increase BARIVIA_FETCH_TIMEOUT_MS (e.g. 120000) for large images. (request id: ${requestId})`);
|
|
553
791
|
}
|
|
792
|
+
if (err instanceof TypeError)
|
|
793
|
+
throwNetworkUnreachable(requestId, err);
|
|
554
794
|
throw err;
|
|
555
795
|
}
|
|
556
796
|
}
|
|
797
|
+
if (lastError instanceof TypeError)
|
|
798
|
+
throwNetworkUnreachable(requestId, lastError);
|
|
557
799
|
throw lastError;
|
|
558
800
|
}
|
|
559
801
|
// ---------------------------------------------------------------------------
|
|
@@ -860,12 +1102,17 @@ export function getResultsImagesToFetch(jobType, summary, figures, includeIndivi
|
|
|
860
1102
|
}
|
|
861
1103
|
// train_som / train_siom
|
|
862
1104
|
const features = summary.features ?? [];
|
|
863
|
-
const
|
|
864
|
-
const
|
|
865
|
-
const
|
|
866
|
-
const
|
|
1105
|
+
const summaryFiles = summary.files ?? [];
|
|
1106
|
+
const imageFilesFromSummary = summaryFiles.filter((f) => /\.(png|pdf|svg)$/i.test(f));
|
|
1107
|
+
const combinedName = imageFilesFromSummary.find((f) => f.startsWith("combined.")) ?? `combined.${ext}`;
|
|
1108
|
+
const umatrixName = imageFilesFromSummary.find((f) => f.startsWith("umatrix.")) ?? `umatrix.${ext}`;
|
|
1109
|
+
const hitHistName = imageFilesFromSummary.find((f) => f.startsWith("hit_histogram.")) ?? `hit_histogram.${ext}`;
|
|
1110
|
+
const correlationName = imageFilesFromSummary.find((f) => f.startsWith("correlation.")) ?? `correlation.${ext}`;
|
|
867
1111
|
const componentNames = features.map((f, i) => `component_${i + 1}_${f.replace(/[^a-zA-Z0-9_]/g, "_")}.${ext}`);
|
|
868
|
-
const
|
|
1112
|
+
const publishedComponents = imageFilesFromSummary.filter((f) => /^component_\d+_/.test(f));
|
|
1113
|
+
const allList = imageFilesFromSummary.length > 0
|
|
1114
|
+
? imageFilesFromSummary
|
|
1115
|
+
: [combinedName, umatrixName, hitHistName, correlationName, ...componentNames];
|
|
869
1116
|
if (figures === undefined || figures === "default") {
|
|
870
1117
|
return includeIndividual ? allList : [combinedName];
|
|
871
1118
|
}
|
|
@@ -880,8 +1127,13 @@ export function getResultsImagesToFetch(jobType, summary, figures, includeIndivi
|
|
|
880
1127
|
hit_histogram: hitHistName,
|
|
881
1128
|
correlation: correlationName,
|
|
882
1129
|
};
|
|
1130
|
+
for (const file of imageFilesFromSummary) {
|
|
1131
|
+
const key = file.replace(/\.(png|pdf|svg)$/i, "");
|
|
1132
|
+
nameToFile[key] = file;
|
|
1133
|
+
}
|
|
883
1134
|
features.forEach((_, i) => {
|
|
884
|
-
|
|
1135
|
+
const published = publishedComponents.find((f) => f.startsWith(`component_${i + 1}_`));
|
|
1136
|
+
nameToFile[`component_${i + 1}`] = published ?? componentNames[i];
|
|
885
1137
|
});
|
|
886
1138
|
return figures
|
|
887
1139
|
.map((key) => {
|
package/dist/tools/account.js
CHANGED
|
@@ -1,26 +1,95 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAuditedTool } from "../audit.js";
|
|
3
|
-
import { apiCall } from "../shared.js";
|
|
3
|
+
import { apiCall, CLIENT_VERSION, probeApiHealth, structuredTextResult, UNAVAILABLE_RETRY_AFTER_SEC, } from "../shared.js";
|
|
4
|
+
import { getMcpClientDiagnostics } from "../ui-delivery.js";
|
|
5
|
+
import { listDeferredFeedback } from "../deferred_feedback.js";
|
|
6
|
+
import { buildJobsListQuery, extractJobsList, formatDatasetInventoryLine, formatJobInventoryLine, } from "../inventory_format.js";
|
|
7
|
+
function degradedStatusResult(opts) {
|
|
8
|
+
const retry_after_sec = UNAVAILABLE_RETRY_AFTER_SEC;
|
|
9
|
+
const payload = {
|
|
10
|
+
ok: false,
|
|
11
|
+
retry_after_sec,
|
|
12
|
+
api_reachable: false,
|
|
13
|
+
message: "Barivia is temporarily unavailable (we could not reach the API). " +
|
|
14
|
+
"Your local files and prior downloads are still fine. Try again in about 30 seconds.",
|
|
15
|
+
offline_ok: [
|
|
16
|
+
"already-downloaded results",
|
|
17
|
+
"local CSVs",
|
|
18
|
+
"draft notes / deferred feedback queue",
|
|
19
|
+
],
|
|
20
|
+
needs_api: ["upload", "train", "feedback flush", "list/status", "other cloud tools"],
|
|
21
|
+
request_id: opts.requestId ?? null,
|
|
22
|
+
hint_for_host: "Barivia: reconnecting…",
|
|
23
|
+
detail: opts.detail ?? null,
|
|
24
|
+
};
|
|
25
|
+
const rid = opts.requestId
|
|
26
|
+
? ` (request id: ${opts.requestId} — include if contacting support)`
|
|
27
|
+
: "";
|
|
28
|
+
const text = [
|
|
29
|
+
payload.message + rid,
|
|
30
|
+
`retry_after_sec=${retry_after_sec} | api_reachable=false`,
|
|
31
|
+
`AGENT: Calm banner ("${payload.hint_for_host}"); do not dump HTML. Proxy already burst-retried; wait ~${retry_after_sec}s before another round. Do not start large uploads/trains until ok.`,
|
|
32
|
+
].join("\n");
|
|
33
|
+
return structuredTextResult(payload, text);
|
|
34
|
+
}
|
|
4
35
|
export function registerAccountTool(server) {
|
|
5
|
-
registerAuditedTool(server, "account", `Manage your Barivia account — check plan/license info, view usage history, add funds.
|
|
36
|
+
registerAuditedTool(server, "account", `Manage your Barivia account — check plan/license info, view usage history, rebuild an org inventory, add funds, or probe API health.
|
|
6
37
|
|
|
7
38
|
| Action | Use when |
|
|
8
39
|
|--------|----------|
|
|
9
|
-
| status | Before large jobs — see plan tier, GPU availability, queue depth, training time estimates, credit balance |
|
|
40
|
+
| status | Before large jobs — see plan tier, GPU availability, queue depth, training time estimates, credit balance. When the API is down, returns calm \`{ok:false, retry_after_sec}\` instead of HTML. |
|
|
10
41
|
| history | Viewing recent compute usage and credit spend |
|
|
42
|
+
| inventory | Rebuild a durable markdown overview of datasets + recent jobs (agent-friendly org catalog) |
|
|
11
43
|
| add_funds | Getting instructions to add credits |
|
|
44
|
+
| health | Diagnose API reachability during outages — GET /health + /ready; does **not** probe R2/object storage |
|
|
12
45
|
|
|
13
46
|
action=status: Returns plan tier, compute class (CPU/GPU), usage limits, live queue state, training time estimates, and credit balance.
|
|
14
47
|
Use BEFORE large jobs to check GPU availability and estimate wait time.
|
|
48
|
+
When the API/gateway is down, returns structured \`{ok:false, retry_after_sec, …}\` — treat as a pause, not a crash.
|
|
49
|
+
action=inventory: Lists datasets (name/id/rows/cols/description/tags) and slim recent jobs (label/id/status/job_type/dataset_id/created_at/result). Prefer this over hand-assembling datasets(list)+jobs(list). Page jobs with jobs(action=list, cursor=…).
|
|
50
|
+
action=health: Lightweight liveness/readiness (no R2). Use when tools return api_unreachable or before retrying deferred send_feedback.
|
|
15
51
|
Capacity is shared LOCAL/GKE worker pools — there is no per-key cloud burst lease.
|
|
16
52
|
NOT FOR: Training itself — use train(action=map). This tool only manages the account.`, {
|
|
17
53
|
action: z
|
|
18
|
-
.enum(["status", "history", "add_funds"])
|
|
19
|
-
.describe("status: plan/license/queue info; history: recent compute usage; add_funds: instructions"),
|
|
20
|
-
limit: z.number().optional().describe("action=history: number of records to return (default: 10)"),
|
|
54
|
+
.enum(["status", "history", "inventory", "add_funds", "health"])
|
|
55
|
+
.describe("status: plan/license/queue info; history: recent compute usage; inventory: datasets+jobs overview; add_funds: instructions; health: API liveness without R2"),
|
|
56
|
+
limit: z.number().optional().describe("action=history or inventory: number of job records to return (default: 10 history / 50 inventory)"),
|
|
21
57
|
}, async ({ action, limit }) => {
|
|
58
|
+
if (action === "health") {
|
|
59
|
+
const health = await probeApiHealth();
|
|
60
|
+
const deferred = await listDeferredFeedback("barsom").catch(() => []);
|
|
61
|
+
const lines = [
|
|
62
|
+
`API liveness: ${health.liveness}`,
|
|
63
|
+
`API readiness: ${health.readiness}`,
|
|
64
|
+
health.db ? `DB: ${health.db}` : null,
|
|
65
|
+
health.redis ? `Redis: ${health.redis}` : null,
|
|
66
|
+
`Object storage (R2): ${health.storage} (intentionally not checked)`,
|
|
67
|
+
health.detail ? `Detail: ${health.detail}` : null,
|
|
68
|
+
`Deferred feedback drafts (barsom): ${deferred.length}`,
|
|
69
|
+
health.liveness === "down"
|
|
70
|
+
? `retry_after_sec=${UNAVAILABLE_RETRY_AFTER_SEC} (wait before another tool round)`
|
|
71
|
+
: null,
|
|
72
|
+
].filter(Boolean);
|
|
73
|
+
return structuredTextResult({
|
|
74
|
+
...health,
|
|
75
|
+
deferred_feedback_pending: deferred.length,
|
|
76
|
+
...(health.liveness === "down"
|
|
77
|
+
? { ok: false, retry_after_sec: UNAVAILABLE_RETRY_AFTER_SEC }
|
|
78
|
+
: { ok: true }),
|
|
79
|
+
}, lines.join("\n"));
|
|
80
|
+
}
|
|
22
81
|
if (action === "status") {
|
|
23
|
-
|
|
82
|
+
let data;
|
|
83
|
+
try {
|
|
84
|
+
data = (await apiCall("GET", "/v1/system/info"));
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
const e = err;
|
|
88
|
+
return degradedStatusResult({
|
|
89
|
+
requestId: e.requestId,
|
|
90
|
+
detail: e.message,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
24
93
|
const plan = data.plan ?? {};
|
|
25
94
|
const backend = data.backend ?? {};
|
|
26
95
|
const status = data.status ?? {};
|
|
@@ -80,6 +149,11 @@ NOT FOR: Training itself — use train(action=map). This tool only manages the a
|
|
|
80
149
|
lines.push(` ${k}: ~${v}s`);
|
|
81
150
|
}
|
|
82
151
|
}
|
|
152
|
+
const mcpDiag = getMcpClientDiagnostics();
|
|
153
|
+
lines.push(``, `MCP client (local proxy @ ${CLIENT_VERSION}):`, ` MCP Apps detected: ${mcpDiag.mcp_apps_supported ? "yes" : "no"}`, ` Viz port: ${mcpDiag.viz_port ?? "none"}${mcpDiag.viz_port_pinned ? " (BARIVIA_VIZ_PORT pinned)" : " (ephemeral — set BARIVIA_VIZ_PORT for stable URLs)"}`, ` UI delivery override: ${mcpDiag.ui_delivery_override}`, ` Active delivery tier: ${mcpDiag.active_delivery_tier}`);
|
|
154
|
+
if (mcpDiag.health_url) {
|
|
155
|
+
lines.push(` Viz health: ${mcpDiag.health_url}`);
|
|
156
|
+
}
|
|
83
157
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
84
158
|
}
|
|
85
159
|
if (action === "history") {
|
|
@@ -89,6 +163,42 @@ NOT FOR: Training itself — use train(action=map). This tool only manages the a
|
|
|
89
163
|
content: [{ type: "text", text: `Credit Balance: $${(data.credit_balance_cents / 100).toFixed(2)}\n\nRecent Usage:\n${history}` }]
|
|
90
164
|
};
|
|
91
165
|
}
|
|
166
|
+
if (action === "inventory") {
|
|
167
|
+
const jobLimit = limit && limit > 0 ? Math.min(Math.floor(limit), 100) : 50;
|
|
168
|
+
const [datasetsRaw, jobsRaw] = await Promise.all([
|
|
169
|
+
apiCall("GET", "/v1/datasets"),
|
|
170
|
+
apiCall("GET", buildJobsListQuery({ limit: jobLimit })),
|
|
171
|
+
]);
|
|
172
|
+
const datasets = Array.isArray(datasetsRaw) ? datasetsRaw : [];
|
|
173
|
+
const { jobs, next_cursor } = extractJobsList(jobsRaw);
|
|
174
|
+
const lines = [
|
|
175
|
+
`# Org inventory`,
|
|
176
|
+
``,
|
|
177
|
+
`## Datasets (${datasets.length})`,
|
|
178
|
+
];
|
|
179
|
+
if (datasets.length === 0) {
|
|
180
|
+
lines.push("(none)");
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
for (const ds of datasets)
|
|
184
|
+
lines.push(`- ${formatDatasetInventoryLine(ds)}`);
|
|
185
|
+
}
|
|
186
|
+
lines.push(``, `## Jobs (latest ${jobs.length}${next_cursor ? ", more available" : ""})`);
|
|
187
|
+
if (jobs.length === 0) {
|
|
188
|
+
lines.push("(none)");
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
for (const job of jobs)
|
|
192
|
+
lines.push(`- ${formatJobInventoryLine(job)}`);
|
|
193
|
+
}
|
|
194
|
+
if (next_cursor) {
|
|
195
|
+
lines.push(``);
|
|
196
|
+
lines.push(`More jobs: jobs(action=list, cursor="${next_cursor}", limit=${jobLimit})`);
|
|
197
|
+
}
|
|
198
|
+
lines.push(``);
|
|
199
|
+
lines.push(`Tips: set description/tags on datasets(upload|update) and train(...)/jobs(update). Filter jobs with status/job_type/has_results.`);
|
|
200
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
201
|
+
}
|
|
92
202
|
if (action === "add_funds") {
|
|
93
203
|
return {
|
|
94
204
|
content: [{ type: "text", text: `To add funds to your account, please visit the Barivia Billing Portal (integration pending) or ask your administrator to use the CLI tool:
|
|
@@ -96,7 +206,7 @@ bash scripts/billing/manage-credits.sh add <org_id> <amount_usd>` }]
|
|
|
96
206
|
};
|
|
97
207
|
}
|
|
98
208
|
return {
|
|
99
|
-
content: [{ type: "text", text: `Unknown action: ${action}. Valid: status, history, add_funds.` }]
|
|
209
|
+
content: [{ type: "text", text: `Unknown action: ${action}. Valid: status, history, inventory, add_funds, health.` }]
|
|
100
210
|
};
|
|
101
211
|
});
|
|
102
212
|
}
|