@gamaze/hicortex 0.20.2 → 0.20.4
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/README.md +16 -0
- package/assets/dashboard.html +318 -8
- package/assets/identity.html +349 -8
- package/assets/viz.html +352 -8
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +22 -0
- package/dist/dashboard.d.ts +17 -0
- package/dist/dashboard.js +32 -0
- package/dist/mcp-server.js +10 -0
- package/dist/mcp-stdio.d.ts +138 -0
- package/dist/mcp-stdio.js +308 -0
- package/package.json +4 -2
- package/server.json +44 -0
package/assets/viz.html
CHANGED
|
@@ -80,6 +80,68 @@
|
|
|
80
80
|
background: rgba(128,128,128,0.18); font-size: 11px; color: var(--text-dim);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/* ---- account menu (#365) ---- */
|
|
84
|
+
/* Dropdown under the nav account element: token reveal/copy, connect
|
|
85
|
+
snippets, plan & billing, sign-out. The wrapper (#nav-account) becomes
|
|
86
|
+
the positioning context; the panel is absolute + right-aligned under it,
|
|
87
|
+
above graph content (the topbar is z-10, the panel gets 60 within it). */
|
|
88
|
+
.hc-nav-account { position: relative; }
|
|
89
|
+
.acct-trigger {
|
|
90
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
91
|
+
background: transparent; color: var(--text); border: none; border-radius: 6px;
|
|
92
|
+
padding: 4px 8px; font: inherit; cursor: pointer;
|
|
93
|
+
}
|
|
94
|
+
.acct-trigger:hover { background: rgba(128,128,128,0.15); }
|
|
95
|
+
.acct-caret { color: var(--text-dim); font-size: 10px; }
|
|
96
|
+
.acct-menu {
|
|
97
|
+
display: none; position: absolute; top: calc(100% + 8px); right: 0;
|
|
98
|
+
min-width: 320px; max-width: min(420px, calc(100vw - 40px));
|
|
99
|
+
background: var(--panel); border: 1px solid var(--panel-border); border-radius: 8px;
|
|
100
|
+
padding: 12px; z-index: 60;
|
|
101
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
|
|
102
|
+
color: var(--text); font-size: 13px; text-align: left;
|
|
103
|
+
}
|
|
104
|
+
.acct-menu.open { display: block; }
|
|
105
|
+
.acct-label {
|
|
106
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
|
|
107
|
+
color: var(--text-dim); margin: 8px 0 4px;
|
|
108
|
+
}
|
|
109
|
+
.acct-menu .acct-label:first-child { margin-top: 0; }
|
|
110
|
+
.acct-token-row { display: flex; align-items: center; gap: 6px; }
|
|
111
|
+
.acct-token {
|
|
112
|
+
flex: 1; min-width: 0;
|
|
113
|
+
font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
114
|
+
background: var(--bg); border: 1px solid var(--panel-border); border-radius: 4px;
|
|
115
|
+
padding: 4px 8px; color: var(--text);
|
|
116
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
117
|
+
}
|
|
118
|
+
.acct-btn {
|
|
119
|
+
background: var(--panel); color: var(--text);
|
|
120
|
+
border: 1px solid var(--panel-border); border-radius: 4px;
|
|
121
|
+
padding: 4px 10px; font: inherit; font-size: 12px; cursor: pointer;
|
|
122
|
+
}
|
|
123
|
+
.acct-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
124
|
+
.acct-btn:disabled { opacity: 0.5; cursor: default; }
|
|
125
|
+
.acct-menu details { border-top: 1px solid var(--panel-border); margin-top: 10px; padding-top: 8px; }
|
|
126
|
+
.acct-menu summary { cursor: pointer; color: var(--text-dim); font-size: 13px; }
|
|
127
|
+
.acct-menu summary:hover { color: var(--accent); }
|
|
128
|
+
.acct-snippet { margin-bottom: 8px; }
|
|
129
|
+
.acct-snippet pre {
|
|
130
|
+
background: var(--bg); border: 1px solid var(--panel-border); border-radius: 4px;
|
|
131
|
+
padding: 8px; margin: 4px 0;
|
|
132
|
+
font: 11.5px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
133
|
+
white-space: pre-wrap; word-break: break-all;
|
|
134
|
+
max-height: 200px; overflow-y: auto;
|
|
135
|
+
}
|
|
136
|
+
.acct-link {
|
|
137
|
+
display: block; width: 100%; text-align: left;
|
|
138
|
+
background: transparent; border: none; border-radius: 4px;
|
|
139
|
+
color: var(--text); font: inherit; font-size: 13px;
|
|
140
|
+
padding: 6px 8px; cursor: pointer;
|
|
141
|
+
}
|
|
142
|
+
.acct-link:hover { background: rgba(128,128,128,0.15); }
|
|
143
|
+
.acct-link.acct-danger:hover { color: var(--danger); }
|
|
144
|
+
|
|
83
145
|
/* ---- left control panel ---- */
|
|
84
146
|
#panel {
|
|
85
147
|
position: fixed; top: 48px; left: 12px; width: 236px;
|
|
@@ -385,28 +447,43 @@
|
|
|
385
447
|
});
|
|
386
448
|
|
|
387
449
|
// =========================================================================
|
|
388
|
-
// Account identity in the nav — GET /account (the
|
|
389
|
-
// same shape the dashboard payload carries).
|
|
390
|
-
// leaves the span empty (fetchGraph owns the 401
|
|
391
|
-
// account (self-hosted default) renders nothing
|
|
392
|
-
//
|
|
450
|
+
// Account identity in the nav + the ACCOUNT MENU (#365) — GET /account (the
|
|
451
|
+
// lightweight endpoint, the same shape the dashboard payload carries).
|
|
452
|
+
// Fail-soft: 401/fetch error leaves the span empty (fetchGraph owns the 401
|
|
453
|
+
// token prompt); an all-null account (self-hosted default) renders nothing —
|
|
454
|
+
// no menu either. createElement/textContent only — this page uses innerHTML
|
|
455
|
+
// exclusively for the escaped 3D-force-graph tooltips.
|
|
393
456
|
// =========================================================================
|
|
394
457
|
function renderAccount(a) {
|
|
395
458
|
var host = document.getElementById("nav-account");
|
|
396
459
|
if (!host) return;
|
|
397
460
|
host.textContent = "";
|
|
398
461
|
if (!a || (a.name === null && a.org === null && a.plan === null)) return;
|
|
462
|
+
var trigger = document.createElement("button");
|
|
463
|
+
trigger.type = "button";
|
|
464
|
+
trigger.id = "acct-trigger";
|
|
465
|
+
trigger.className = "acct-trigger";
|
|
466
|
+
trigger.setAttribute("aria-haspopup", "menu");
|
|
467
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
399
468
|
var who = [a.name, a.org].filter(function (s) { return typeof s === "string" && s !== ""; });
|
|
400
469
|
who.forEach(function (s, i) {
|
|
401
|
-
if (i > 0)
|
|
402
|
-
|
|
470
|
+
if (i > 0) trigger.appendChild(document.createTextNode(" · "));
|
|
471
|
+
trigger.appendChild(document.createTextNode(s));
|
|
403
472
|
});
|
|
404
473
|
if (typeof a.plan === "string" && a.plan !== "") {
|
|
405
474
|
var pill = document.createElement("span");
|
|
406
475
|
pill.className = "plan";
|
|
407
476
|
pill.textContent = a.plan;
|
|
408
|
-
|
|
477
|
+
trigger.appendChild(pill);
|
|
409
478
|
}
|
|
479
|
+
var caret = document.createElement("span");
|
|
480
|
+
caret.className = "acct-caret";
|
|
481
|
+
caret.setAttribute("aria-hidden", "true");
|
|
482
|
+
caret.textContent = "▾";
|
|
483
|
+
trigger.appendChild(caret);
|
|
484
|
+
host.appendChild(trigger);
|
|
485
|
+
host.appendChild(buildAcctMenu());
|
|
486
|
+
wireAcctMenu();
|
|
410
487
|
}
|
|
411
488
|
function loadAccount() {
|
|
412
489
|
var headers = {};
|
|
@@ -417,6 +494,273 @@
|
|
|
417
494
|
.catch(function () { /* leave the span empty — nav is never worth an error */ });
|
|
418
495
|
}
|
|
419
496
|
|
|
497
|
+
// =========================================================================
|
|
498
|
+
// Account menu (#365) — dropdown under the nav account element: token
|
|
499
|
+
// reveal/copy, connect snippets, plan & billing, sign-out. The token shown
|
|
500
|
+
// is AUTHORITATIVE server truth: a lazy GET /account/token on the FIRST
|
|
501
|
+
// open (echo-only endpoint — the fetch already proves the caller holds the
|
|
502
|
+
// token). localStorage is NEVER trusted for the display: it can be stale
|
|
503
|
+
// after token rotation and is absent entirely for browser sessions the
|
|
504
|
+
// hosted router authenticates via its session→bearer injection. On fetch
|
|
505
|
+
// failure the row fails explicitly ("token unavailable", actions disabled).
|
|
506
|
+
// Built with createElement/textContent only (page convention).
|
|
507
|
+
// =========================================================================
|
|
508
|
+
var acctToken = null; // token from /account/token (server truth)
|
|
509
|
+
var acctTokenState = "unfetched"; // "unfetched" | "ok" | "unavailable"
|
|
510
|
+
var acctTokenFetched = false; // one lazy fetch attempt per page load
|
|
511
|
+
var acctRevealed = false; // unmasked while the menu is open
|
|
512
|
+
|
|
513
|
+
function maskToken(t) {
|
|
514
|
+
// hctx-••••••23f9 — first 5 chars + 6 bullets + last 4. Real tokens are
|
|
515
|
+
// hctx-<32hex>; the guard keeps a degenerate short token from leaking.
|
|
516
|
+
if (typeof t !== "string" || t.length < 10) return "••••••••••";
|
|
517
|
+
return t.slice(0, 5) + "••••••" + t.slice(-4);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function el(tag, id, cls, text) {
|
|
521
|
+
var n = document.createElement(tag);
|
|
522
|
+
if (id) n.id = id;
|
|
523
|
+
if (cls) n.className = cls;
|
|
524
|
+
if (text !== undefined) n.textContent = text;
|
|
525
|
+
return n;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Static menu skeleton — token-dependent values fill in renderAcctToken().
|
|
529
|
+
function buildAcctMenu() {
|
|
530
|
+
var menu = el("div", "acct-menu", "acct-menu");
|
|
531
|
+
menu.appendChild(el("div", null, "acct-label", "Connection token"));
|
|
532
|
+
var row = el("div", null, "acct-token-row");
|
|
533
|
+
row.appendChild(el("code", "acct-token-val", "acct-token", "…"));
|
|
534
|
+
row.appendChild(el("button", "acct-reveal", "acct-btn", "Reveal"));
|
|
535
|
+
row.appendChild(el("button", "acct-copy-token", "acct-btn", "Copy"));
|
|
536
|
+
menu.appendChild(row);
|
|
537
|
+
var details = el("details", "acct-connect");
|
|
538
|
+
details.appendChild(el("summary", null, null, "Connect an agent"));
|
|
539
|
+
details.appendChild(el("div", null, "acct-label", "CLI"));
|
|
540
|
+
var snip1 = el("div", null, "acct-snippet");
|
|
541
|
+
snip1.appendChild(el("pre", "acct-cli"));
|
|
542
|
+
snip1.appendChild(el("button", "acct-copy-cli", "acct-btn", "Copy"));
|
|
543
|
+
details.appendChild(snip1);
|
|
544
|
+
details.appendChild(el("div", null, "acct-label", "MCP — ~/.claude.json"));
|
|
545
|
+
var snip2 = el("div", null, "acct-snippet");
|
|
546
|
+
snip2.appendChild(el("pre", "acct-mcp"));
|
|
547
|
+
snip2.appendChild(el("button", "acct-copy-mcp", "acct-btn", "Copy"));
|
|
548
|
+
details.appendChild(snip2);
|
|
549
|
+
menu.appendChild(details);
|
|
550
|
+
menu.appendChild(el("button", "acct-plan", "acct-link", "Plan & billing"));
|
|
551
|
+
menu.appendChild(el("button", "acct-signout", "acct-link acct-danger", "Sign out"));
|
|
552
|
+
// All menu buttons are type=button (never submit — no form here, but the
|
|
553
|
+
// page's convention is explicit types).
|
|
554
|
+
var btns = menu.querySelectorAll("button");
|
|
555
|
+
for (var i = 0; i < btns.length; i++) btns[i].type = "button";
|
|
556
|
+
return menu;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// The connect line: server URL + token on ONE line, space-separated.
|
|
560
|
+
function acctConnectLine() {
|
|
561
|
+
return location.origin + " " + acctToken;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// CLI snippet — init has NO --token flag; it prompts interactively. The
|
|
565
|
+
// comment mirrors init.ts's own printed hint ("When prompted for the token,
|
|
566
|
+
// paste: …").
|
|
567
|
+
function acctCliText() {
|
|
568
|
+
return "# When prompted for the token, paste: " + acctToken +
|
|
569
|
+
"\nnpx @gamaze/hicortex init --server " + location.origin;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// MCP snippet — mirrors EXACTLY what runClientInit writes into
|
|
573
|
+
// ~/.claude.json (init.ts): SSE transport at <origin>/sse with a bearer
|
|
574
|
+
// Authorization header.
|
|
575
|
+
function acctMcpText() {
|
|
576
|
+
return JSON.stringify({
|
|
577
|
+
mcpServers: {
|
|
578
|
+
hicortex: {
|
|
579
|
+
type: "sse",
|
|
580
|
+
url: location.origin + "/sse",
|
|
581
|
+
headers: { Authorization: "Bearer " + acctToken }
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}, null, 2);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function renderAcctToken() {
|
|
588
|
+
var val = document.getElementById("acct-token-val");
|
|
589
|
+
if (!val) return; // menu not rendered (self-hosted all-null account)
|
|
590
|
+
var revealBtn = document.getElementById("acct-reveal");
|
|
591
|
+
var copyToken = document.getElementById("acct-copy-token");
|
|
592
|
+
var copyCli = document.getElementById("acct-copy-cli");
|
|
593
|
+
var copyMcp = document.getElementById("acct-copy-mcp");
|
|
594
|
+
var cli = document.getElementById("acct-cli");
|
|
595
|
+
var mcp = document.getElementById("acct-mcp");
|
|
596
|
+
if (acctTokenState !== "ok") {
|
|
597
|
+
val.textContent = acctTokenState === "unfetched" ? "…" : "token unavailable";
|
|
598
|
+
revealBtn.disabled = true;
|
|
599
|
+
copyToken.disabled = true;
|
|
600
|
+
copyCli.disabled = true;
|
|
601
|
+
copyMcp.disabled = true;
|
|
602
|
+
cli.textContent = acctTokenState === "unfetched" ? "" : "token unavailable";
|
|
603
|
+
mcp.textContent = acctTokenState === "unfetched" ? "" : "token unavailable";
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
revealBtn.disabled = false;
|
|
607
|
+
copyToken.disabled = false;
|
|
608
|
+
copyCli.disabled = false;
|
|
609
|
+
copyMcp.disabled = false;
|
|
610
|
+
val.textContent = acctRevealed ? acctToken : maskToken(acctToken);
|
|
611
|
+
revealBtn.textContent = acctRevealed ? "Hide" : "Reveal";
|
|
612
|
+
cli.textContent = acctCliText();
|
|
613
|
+
mcp.textContent = acctMcpText();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function fetchAcctToken() {
|
|
617
|
+
if (acctTokenFetched) return;
|
|
618
|
+
acctTokenFetched = true;
|
|
619
|
+
var headers = {};
|
|
620
|
+
if (token) headers["Authorization"] = "Bearer " + token; // same as loadAccount
|
|
621
|
+
fetch("/account/token", { headers: headers })
|
|
622
|
+
.then(function (resp) {
|
|
623
|
+
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
|
624
|
+
return resp.json();
|
|
625
|
+
})
|
|
626
|
+
.then(function (data) {
|
|
627
|
+
if (typeof data.token !== "string" || !data.token) throw new Error("bad payload");
|
|
628
|
+
acctToken = data.token;
|
|
629
|
+
acctTokenState = "ok";
|
|
630
|
+
renderAcctToken();
|
|
631
|
+
})
|
|
632
|
+
.catch(function () {
|
|
633
|
+
acctTokenState = "unavailable"; // fail explicitly — no localStorage fallback
|
|
634
|
+
renderAcctToken();
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function closeAcctMenu() {
|
|
639
|
+
var menu = document.getElementById("acct-menu");
|
|
640
|
+
if (!menu) return;
|
|
641
|
+
menu.classList.remove("open");
|
|
642
|
+
var trigger = document.getElementById("acct-trigger");
|
|
643
|
+
if (trigger) trigger.setAttribute("aria-expanded", "false");
|
|
644
|
+
// Masked again + connect section collapsed on EVERY close (locked design).
|
|
645
|
+
acctRevealed = false;
|
|
646
|
+
var details = document.getElementById("acct-connect");
|
|
647
|
+
if (details) details.removeAttribute("open");
|
|
648
|
+
renderAcctToken();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function openAcctMenu() {
|
|
652
|
+
var menu = document.getElementById("acct-menu");
|
|
653
|
+
if (!menu) return;
|
|
654
|
+
menu.classList.add("open");
|
|
655
|
+
document.getElementById("acct-trigger").setAttribute("aria-expanded", "true");
|
|
656
|
+
fetchAcctToken(); // lazy — one attempt, on first open only
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Clipboard helper: async API when available, temporary <textarea> +
|
|
660
|
+
// execCommand fallback (http remote origins lack the async clipboard API).
|
|
661
|
+
// Brief "Copied" feedback (label swap for ~1.5 s); total failure alerts.
|
|
662
|
+
function copyToClipboard(text, btn) {
|
|
663
|
+
var flash = function () {
|
|
664
|
+
var orig = btn.dataset.origLabel || (btn.dataset.origLabel = btn.textContent);
|
|
665
|
+
btn.textContent = "Copied";
|
|
666
|
+
clearTimeout(btn._copiedTimer);
|
|
667
|
+
btn._copiedTimer = setTimeout(function () { btn.textContent = orig; }, 1500);
|
|
668
|
+
};
|
|
669
|
+
var legacy = function () {
|
|
670
|
+
try {
|
|
671
|
+
var ta = document.createElement("textarea");
|
|
672
|
+
ta.value = text;
|
|
673
|
+
ta.style.position = "fixed";
|
|
674
|
+
ta.style.opacity = "0";
|
|
675
|
+
document.body.appendChild(ta);
|
|
676
|
+
ta.select();
|
|
677
|
+
var ok = document.execCommand("copy");
|
|
678
|
+
document.body.removeChild(ta);
|
|
679
|
+
return ok;
|
|
680
|
+
} catch (e) { return false; }
|
|
681
|
+
};
|
|
682
|
+
var fallback = function () {
|
|
683
|
+
if (legacy()) { flash(); return; }
|
|
684
|
+
alert("Copy failed — select the text and copy it manually.");
|
|
685
|
+
};
|
|
686
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
687
|
+
navigator.clipboard.writeText(text).then(flash, fallback);
|
|
688
|
+
} else {
|
|
689
|
+
fallback();
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Sign-out is CLIENT-SIDE ONLY (#365): there is no server session to
|
|
694
|
+
// invalidate in the Hicortex server itself — auth is per-request bearer, so
|
|
695
|
+
// dropping the locally stored tokens IS the sign-out. (The hosted router
|
|
696
|
+
// separately keeps a Google session cookie and offers /auth/logout — out of
|
|
697
|
+
// scope here; this only ends the browser's console session.)
|
|
698
|
+
function signOut() {
|
|
699
|
+
try {
|
|
700
|
+
localStorage.removeItem("hicortexToken");
|
|
701
|
+
localStorage.removeItem("hicortex-dashboard-token");
|
|
702
|
+
} catch (e) { /* private mode — reload anyway */ }
|
|
703
|
+
location.reload(); // back to the unauthenticated 401-prompt state
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function wireAcctMenu() {
|
|
707
|
+
var trigger = document.getElementById("acct-trigger");
|
|
708
|
+
if (!trigger) return;
|
|
709
|
+
trigger.addEventListener("click", function () {
|
|
710
|
+
var menu = document.getElementById("acct-menu");
|
|
711
|
+
if (menu && menu.classList.contains("open")) closeAcctMenu();
|
|
712
|
+
else openAcctMenu();
|
|
713
|
+
});
|
|
714
|
+
document.getElementById("acct-reveal").addEventListener("click", function () {
|
|
715
|
+
acctRevealed = !acctRevealed;
|
|
716
|
+
renderAcctToken();
|
|
717
|
+
});
|
|
718
|
+
var copyToken = document.getElementById("acct-copy-token");
|
|
719
|
+
copyToken.addEventListener("click", function () {
|
|
720
|
+
if (acctTokenState === "ok") copyToClipboard(acctConnectLine(), copyToken);
|
|
721
|
+
});
|
|
722
|
+
var copyCli = document.getElementById("acct-copy-cli");
|
|
723
|
+
copyCli.addEventListener("click", function () {
|
|
724
|
+
if (acctTokenState === "ok") {
|
|
725
|
+
copyToClipboard("npx @gamaze/hicortex init --server " + location.origin, copyCli);
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
var copyMcp = document.getElementById("acct-copy-mcp");
|
|
729
|
+
copyMcp.addEventListener("click", function () {
|
|
730
|
+
if (acctTokenState === "ok") copyToClipboard(acctMcpText(), copyMcp);
|
|
731
|
+
});
|
|
732
|
+
// Plan & billing lives on the dashboard page — navigate there (append
|
|
733
|
+
// ?token= when one is known, mirroring the shared-nav link handler) and
|
|
734
|
+
// land on the digest panel's budget/usage anchor (#371 lands there).
|
|
735
|
+
document.getElementById("acct-plan").addEventListener("click", function () {
|
|
736
|
+
var t = null;
|
|
737
|
+
try {
|
|
738
|
+
t = token
|
|
739
|
+
|| localStorage.getItem("hicortexToken")
|
|
740
|
+
|| localStorage.getItem("hicortex-dashboard-token");
|
|
741
|
+
} catch (e) { /* private mode — navigate without a token */ }
|
|
742
|
+
var u = new URL("/dashboard", location.origin);
|
|
743
|
+
if (t) u.searchParams.set("token", t);
|
|
744
|
+
u.hash = "plan-billing";
|
|
745
|
+
location.href = u.toString();
|
|
746
|
+
});
|
|
747
|
+
document.getElementById("acct-signout").addEventListener("click", signOut);
|
|
748
|
+
renderAcctToken();
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Click-outside + Escape close the menu (wired once; the handlers no-op
|
|
752
|
+
// when the menu isn't rendered).
|
|
753
|
+
document.addEventListener("click", function (e) {
|
|
754
|
+
var menu = document.getElementById("acct-menu");
|
|
755
|
+
if (!menu || !menu.classList.contains("open")) return;
|
|
756
|
+
var wrap = document.getElementById("nav-account");
|
|
757
|
+
if (wrap && wrap.contains(e.target)) return;
|
|
758
|
+
closeAcctMenu();
|
|
759
|
+
});
|
|
760
|
+
window.addEventListener("keydown", function (e) {
|
|
761
|
+
if (e.key === "Escape") closeAcctMenu();
|
|
762
|
+
});
|
|
763
|
+
|
|
420
764
|
// =========================================================================
|
|
421
765
|
// DOM references
|
|
422
766
|
// =========================================================================
|
package/dist/cli.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Commands:
|
|
6
6
|
* server Start the MCP HTTP/SSE server (persistent daemon)
|
|
7
|
+
* mcp Speak MCP over stdio (bridge to the local daemon or HICORTEX_SERVER_URL)
|
|
7
8
|
* init Detect existing setup and configure for CC/OC
|
|
8
9
|
* nightly Run capture + consolidate (manual trigger)
|
|
9
10
|
* nightly --capture-only Capture only, skip consolidation
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Commands:
|
|
7
7
|
* server Start the MCP HTTP/SSE server (persistent daemon)
|
|
8
|
+
* mcp Speak MCP over stdio (bridge to the local daemon or HICORTEX_SERVER_URL)
|
|
8
9
|
* init Detect existing setup and configure for CC/OC
|
|
9
10
|
* nightly Run capture + consolidate (manual trigger)
|
|
10
11
|
* nightly --capture-only Capture only, skip consolidation
|
|
@@ -34,6 +35,26 @@ switch (command) {
|
|
|
34
35
|
});
|
|
35
36
|
break;
|
|
36
37
|
}
|
|
38
|
+
case "mcp": {
|
|
39
|
+
// Stdio MCP bridge (#375): speak MCP on stdin/stdout, backed by the
|
|
40
|
+
// daemon's SSE endpoint (or a remote server via HICORTEX_SERVER_URL).
|
|
41
|
+
// Registry clients (Claude Desktop, Cursor, the MCP Registry's install
|
|
42
|
+
// flow) launch stdio commands — this is the command the registry's
|
|
43
|
+
// server.json declares (`npx -y @gamaze/hicortex mcp`). Starts a local
|
|
44
|
+
// daemon when the target is loopback and none is running; never spawns
|
|
45
|
+
// for remote targets. stdout carries ONLY the MCP protocol — all
|
|
46
|
+
// diagnostics go to stderr (the bridge owns that discipline internally).
|
|
47
|
+
import("./mcp-stdio.js").then(({ runMcpStdio }) => {
|
|
48
|
+
runMcpStdio().catch((err) => {
|
|
49
|
+
console.error(err instanceof Error ? err.message : `[hicortex] mcp bridge failed: ${err}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
|
52
|
+
}).catch((err) => {
|
|
53
|
+
console.error("[hicortex] Failed to load the mcp bridge:", err);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
});
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
37
58
|
case "init": {
|
|
38
59
|
const serverArg = process.argv.indexOf("--server");
|
|
39
60
|
const serverUrl = serverArg !== -1 ? process.argv[serverArg + 1] : undefined;
|
|
@@ -331,6 +352,7 @@ Usage: hicortex <command> [options]
|
|
|
331
352
|
|
|
332
353
|
Commands:
|
|
333
354
|
server Start the MCP HTTP/SSE server (server mode)
|
|
355
|
+
mcp Speak MCP over stdio (bridge to the local daemon or HICORTEX_SERVER_URL)
|
|
334
356
|
init Set up Hicortex (server mode, local DB + daemon)
|
|
335
357
|
Scaffolds 5 editable default memory domains (Work, Personal,
|
|
336
358
|
People, Health, Finance) in ~/.hicortex/config.json
|
package/dist/dashboard.d.ts
CHANGED
|
@@ -351,3 +351,20 @@ export declare function dashboardDataHandler(getDb: () => Database.Database, get
|
|
|
351
351
|
* 500 with the usual {error} shape (same as dashboardDataHandler).
|
|
352
352
|
*/
|
|
353
353
|
export declare function accountHandler(getConfig: () => Record<string, unknown> | null | undefined): express.RequestHandler;
|
|
354
|
+
/**
|
|
355
|
+
* Express adapter for GET /account/token — the install's connection token for
|
|
356
|
+
* the console account menu (#365). SECURITY: echo-only — the caller must
|
|
357
|
+
* ALREADY present the token (bearer, or the localhost bypass) to receive it,
|
|
358
|
+
* so this endpoint grants no privilege. It exists so the menu shows the
|
|
359
|
+
* AUTHORITATIVE server-side token instead of trusting localStorage, which can
|
|
360
|
+
* be stale after token rotation and is absent entirely for browser sessions
|
|
361
|
+
* the hosted router authenticates via its session→bearer injection.
|
|
362
|
+
*
|
|
363
|
+
* `getToken` receives the boot-resolved PRIMARY token (config authToken ??
|
|
364
|
+
* HICORTEX_AUTH_TOKEN env) — the value the auth middleware itself accepts as
|
|
365
|
+
* current, so the menu survives rotation and never echoes the rotation-grace
|
|
366
|
+
* token. When no token is configured the handler answers 503 (mirrors how the
|
|
367
|
+
* /auth/* endpoints answer "not configured"); other failures surface as a 500
|
|
368
|
+
* {error} exactly like accountHandler.
|
|
369
|
+
*/
|
|
370
|
+
export declare function accountTokenHandler(getToken: () => string | undefined): express.RequestHandler;
|
package/dist/dashboard.js
CHANGED
|
@@ -26,6 +26,7 @@ exports.backfillSnapshots = backfillSnapshots;
|
|
|
26
26
|
exports.handleDashboardData = handleDashboardData;
|
|
27
27
|
exports.dashboardDataHandler = dashboardDataHandler;
|
|
28
28
|
exports.accountHandler = accountHandler;
|
|
29
|
+
exports.accountTokenHandler = accountTokenHandler;
|
|
29
30
|
const recall_index_js_1 = require("./recall-index.js");
|
|
30
31
|
const config_read_js_1 = require("./config-read.js");
|
|
31
32
|
const consolidate_js_1 = require("./consolidate.js");
|
|
@@ -526,3 +527,34 @@ function accountHandler(getConfig) {
|
|
|
526
527
|
}
|
|
527
528
|
};
|
|
528
529
|
}
|
|
530
|
+
/**
|
|
531
|
+
* Express adapter for GET /account/token — the install's connection token for
|
|
532
|
+
* the console account menu (#365). SECURITY: echo-only — the caller must
|
|
533
|
+
* ALREADY present the token (bearer, or the localhost bypass) to receive it,
|
|
534
|
+
* so this endpoint grants no privilege. It exists so the menu shows the
|
|
535
|
+
* AUTHORITATIVE server-side token instead of trusting localStorage, which can
|
|
536
|
+
* be stale after token rotation and is absent entirely for browser sessions
|
|
537
|
+
* the hosted router authenticates via its session→bearer injection.
|
|
538
|
+
*
|
|
539
|
+
* `getToken` receives the boot-resolved PRIMARY token (config authToken ??
|
|
540
|
+
* HICORTEX_AUTH_TOKEN env) — the value the auth middleware itself accepts as
|
|
541
|
+
* current, so the menu survives rotation and never echoes the rotation-grace
|
|
542
|
+
* token. When no token is configured the handler answers 503 (mirrors how the
|
|
543
|
+
* /auth/* endpoints answer "not configured"); other failures surface as a 500
|
|
544
|
+
* {error} exactly like accountHandler.
|
|
545
|
+
*/
|
|
546
|
+
function accountTokenHandler(getToken) {
|
|
547
|
+
return (_req, res) => {
|
|
548
|
+
try {
|
|
549
|
+
const token = getToken();
|
|
550
|
+
if (!token) {
|
|
551
|
+
res.status(503).json({ error: "no auth token configured on this install" });
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
res.status(200).json({ token });
|
|
555
|
+
}
|
|
556
|
+
catch (err) {
|
|
557
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -1599,6 +1599,16 @@ async function startServer(options = {}) {
|
|
|
1599
1599
|
// (standard auth middleware, no shell exemption — it carries data); localhost
|
|
1600
1600
|
// bypass applies. Handler lives in src/dashboard.ts next to its twin.
|
|
1601
1601
|
app.get("/account", (0, dashboard_js_1.accountHandler)(() => readConfigFile(stateDir)));
|
|
1602
|
+
// GET /account/token — the install's connection token for the console
|
|
1603
|
+
// account menu (#365). Echo-only (see accountTokenHandler's JSDoc): the
|
|
1604
|
+
// caller must already present the token (bearer or localhost bypass), so
|
|
1605
|
+
// this grants no privilege — it exists so the menu shows authoritative
|
|
1606
|
+
// server-side truth instead of a possibly-stale localStorage copy. Passes
|
|
1607
|
+
// the boot-resolved PRIMARY token (the one the auth middleware itself
|
|
1608
|
+
// accepts as current — survives rotation, never echoes the grace token).
|
|
1609
|
+
// Bearer-only like /account: standard auth middleware, no shell exemption
|
|
1610
|
+
// (it carries data); localhost bypass applies.
|
|
1611
|
+
app.get("/account/token", (0, dashboard_js_1.accountTokenHandler)(() => authToken));
|
|
1602
1612
|
// SSE endpoint — each connection gets its own McpServer + transport
|
|
1603
1613
|
app.get("/sse", async (req, res) => {
|
|
1604
1614
|
const transport = new sse_js_1.SSEServerTransport("/messages", res);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hicortex stdio MCP bridge (#375) — the `hicortex mcp` subcommand.
|
|
3
|
+
*
|
|
4
|
+
* Registry/stdio MCP clients (Claude Desktop, Cursor, the MCP Registry's own
|
|
5
|
+
* install flow) launch a stdio command and speak MCP over stdin/stdout. The
|
|
6
|
+
* daemon's MCP surface is HTTP/SSE on :8787, so this module bridges the two:
|
|
7
|
+
* a low-level SDK `Server` over `StdioServerTransport` downstream (the client
|
|
8
|
+
* side) and an SDK `Client` over `SSEClientTransport` upstream (the daemon
|
|
9
|
+
* side), forwarding tools/list + tools/call. The daemon's MCP surface is
|
|
10
|
+
* tools-only (no resources/prompts registered in mcp-server.ts) and ping is
|
|
11
|
+
* auto-answered by the SDK Protocol base, so tools-forwarding loses nothing.
|
|
12
|
+
*
|
|
13
|
+
* WHY proxy instead of in-process stdio with direct DB access (design note,
|
|
14
|
+
* issue #375):
|
|
15
|
+
* 1. db.ts enables WAL but sets no busy_timeout — a second writer process
|
|
16
|
+
* (the common case: the daemon already running on server-mode installs)
|
|
17
|
+
* takes immediate SQLITE_BUSY during nightly consolidation's long
|
|
18
|
+
* transactions → tool-call failures.
|
|
19
|
+
* 2. createMcpServer()'s nine tool handlers close over ~10 module-level
|
|
20
|
+
* vars initialized by the ~250-line boot inside startServer() —
|
|
21
|
+
* in-process would mean refactoring the production boot path or
|
|
22
|
+
* duplicating it (drift).
|
|
23
|
+
* 3. warmEmbedder loads a 150-300 MB ONNX model per process — one per MCP
|
|
24
|
+
* client (Claude Desktop + CC + Cursor = 3x), vs zero for the bridge.
|
|
25
|
+
* 4. mcp-server.ts's own header states the model: "One process, one DB
|
|
26
|
+
* connection, one embedder".
|
|
27
|
+
*
|
|
28
|
+
* Target resolution (precedence): HICORTEX_SERVER_URL env → remote bridge,
|
|
29
|
+
* NEVER spawns anything; else config via the SAME semantics as
|
|
30
|
+
* learnings-identity.resolveConfig() (client-mode serverUrl → remote;
|
|
31
|
+
* server-mode → http://127.0.0.1:<port ?? 8787>); no usable config →
|
|
32
|
+
* http://127.0.0.1:8787. Token: HICORTEX_AUTH_TOKEN env → config.authToken.
|
|
33
|
+
* The token rides SSEClientTransport's requestInit headers (verified in the
|
|
34
|
+
* installed SDK 1.28: merged into BOTH the GET /sse and POST /messages).
|
|
35
|
+
*
|
|
36
|
+
* Local autostart: when the target is loopback and /health is
|
|
37
|
+
* connection-refused, spawn a DETACHED `cli.js server --port <n>` (unref,
|
|
38
|
+
* stdio ignored) and poll /health (~250 ms interval, 30 s cap). Concurrent
|
|
39
|
+
* bridges racing EADDRINUSE self-heal — the loser's child dies, the winner's
|
|
40
|
+
* daemon answers the poll, so the loop keeps polling regardless of child
|
|
41
|
+
* state. /health answering but not ok = a foreign or broken service on the
|
|
42
|
+
* port: explicit error, never a spawn. A remote target that is down is
|
|
43
|
+
* likewise an explicit error — we never spawn for remote URLs.
|
|
44
|
+
*
|
|
45
|
+
* STDIO DISCIPLINE: stdout carries ONLY the MCP protocol. Every diagnostic
|
|
46
|
+
* goes to stderr; fatal errors are a one-liner on stderr + non-zero exit
|
|
47
|
+
* (thrown to cli.ts's catch). Cancellation downstream→upstream rides the
|
|
48
|
+
* SDK-native path: the Protocol base aborts the handler's extra.signal on
|
|
49
|
+
* notifications/cancelled, and passing that signal into client.callTool makes
|
|
50
|
+
* the upstream Client emit its own notifications/cancelled with the CORRECT
|
|
51
|
+
* upstream request id (a verbatim forward would carry the downstream id,
|
|
52
|
+
* which means nothing to the daemon) — and reject the in-flight bridge call.
|
|
53
|
+
*/
|
|
54
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
55
|
+
/** Where the bridge target came from — surfaces in the startup diagnostic. */
|
|
56
|
+
export type BridgeTargetSource = "option" | "env" | "config" | "default";
|
|
57
|
+
export interface BridgeTarget {
|
|
58
|
+
/** Base URL, trailing slashes stripped (endpoints append /sse, /health). */
|
|
59
|
+
url: string;
|
|
60
|
+
/** Loopback target → eligible for local autostart. */
|
|
61
|
+
local: boolean;
|
|
62
|
+
/** Port of the resolved URL — what an autospawned daemon must listen on. */
|
|
63
|
+
port: number;
|
|
64
|
+
source: BridgeTargetSource;
|
|
65
|
+
}
|
|
66
|
+
/** Loopback check for URL hostnames (Node's URL keeps the brackets on [::1]). */
|
|
67
|
+
export declare function isLoopbackHost(hostname: string): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the daemon/server the bridge should talk to. Precedence: explicit
|
|
70
|
+
* option (the runMcpStdio test seam) → HICORTEX_SERVER_URL env → config file
|
|
71
|
+
* (client-mode serverUrl = remote; server-mode = localhost, the
|
|
72
|
+
* resolveConfig() semantics already shared by both CC hooks) → default local
|
|
73
|
+
* 8787. Blank/whitespace env values are ignored, not mistaken for targets.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveBridgeTarget(explicitUrl?: string): BridgeTarget;
|
|
76
|
+
/**
|
|
77
|
+
* Resolve the bearer token for the upstream connection: explicit option →
|
|
78
|
+
* HICORTEX_AUTH_TOKEN env → config.authToken. Undefined = no token (a local
|
|
79
|
+
* daemon needs none — loopback bypasses auth).
|
|
80
|
+
*/
|
|
81
|
+
export declare function resolveBridgeToken(explicitToken?: string): string | undefined;
|
|
82
|
+
export interface HealthProbe {
|
|
83
|
+
/** Any HTTP response arrived (even a non-200 one). */
|
|
84
|
+
reachable: boolean;
|
|
85
|
+
/** The endpoint answered ok — a healthy Hicortex /health. */
|
|
86
|
+
ok: boolean;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* One GET /health probe. Connection refused / timeout / DNS failure →
|
|
90
|
+
* { reachable: false }; a response that is not ok → { reachable: true, ok:
|
|
91
|
+
* false } — the two carry different autostart decisions, so a boolean alone
|
|
92
|
+
* cannot express them.
|
|
93
|
+
*/
|
|
94
|
+
export declare function probeHealthOnce(url: string, timeoutMs?: number): Promise<HealthProbe>;
|
|
95
|
+
export type AutostartDecision = {
|
|
96
|
+
action: "bridge";
|
|
97
|
+
} | {
|
|
98
|
+
action: "spawn";
|
|
99
|
+
} | {
|
|
100
|
+
action: "fail";
|
|
101
|
+
reason: string;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Pure decision from one health probe: healthy → bridge; refused + loopback
|
|
105
|
+
* → spawn a local daemon; refused + remote → fail with an actionable message
|
|
106
|
+
* (never spawn for remote URLs); answering-but-not-ok → fail explicitly (a
|
|
107
|
+
* foreign or broken service owns the port — spawning next to it cannot help).
|
|
108
|
+
*/
|
|
109
|
+
export declare function decideAutostart(probe: HealthProbe, target: BridgeTarget): AutostartDecision;
|
|
110
|
+
export interface EnsureDaemonOptions {
|
|
111
|
+
/** Local autostart toggle (default true). Remote targets never spawn. */
|
|
112
|
+
autostart?: boolean;
|
|
113
|
+
probeHealth?: (url: string) => Promise<HealthProbe>;
|
|
114
|
+
spawnDaemon?: (port: number) => Promise<void> | void;
|
|
115
|
+
/** Poll pacing overrides — small values keep the timeout test fast. */
|
|
116
|
+
pollIntervalMs?: number;
|
|
117
|
+
pollTotalMs?: number;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Ensure something healthy answers at the target before bridging: probe once,
|
|
121
|
+
* decide, and if spawning — poll until healthy (or the deadline). Throws on
|
|
122
|
+
* every fail-path (explicit error, never silent degradation).
|
|
123
|
+
*/
|
|
124
|
+
export declare function ensureDaemonReady(target: BridgeTarget, options?: EnsureDaemonOptions): Promise<void>;
|
|
125
|
+
export interface McpStdioOptions extends EnsureDaemonOptions {
|
|
126
|
+
/** Explicit target URL (test seam; normally resolved from env/config). */
|
|
127
|
+
serverUrl?: string;
|
|
128
|
+
/** Explicit bearer token (test seam; normally env → config). */
|
|
129
|
+
authToken?: string;
|
|
130
|
+
/** Injectable downstream transport (test seam; default: real stdio). */
|
|
131
|
+
downstream?: Transport;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Run the stdio MCP bridge. Resolves only after the downstream transport
|
|
135
|
+
* closes (the lifecycle handlers then exit the process); every setup failure
|
|
136
|
+
* throws for cli.ts to report on stderr and exit 1.
|
|
137
|
+
*/
|
|
138
|
+
export declare function runMcpStdio(options?: McpStdioOptions): Promise<void>;
|