@gamaze/hicortex 0.20.3 → 0.20.5
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 +20 -1
- package/assets/dashboard.html +318 -8
- package/assets/identity.html +349 -8
- package/assets/viz.html +352 -8
- package/dist/backup.d.ts +12 -8
- package/dist/backup.js +13 -9
- package/dist/claude-desktop.d.ts +138 -0
- package/dist/claude-desktop.js +251 -0
- package/dist/cli.d.ts +7 -2
- package/dist/cli.js +84 -3
- package/dist/consolidate.d.ts +8 -1
- package/dist/consolidate.js +82 -2
- package/dist/dashboard.d.ts +17 -0
- package/dist/dashboard.js +32 -0
- package/dist/db.js +36 -0
- package/dist/dedup.d.ts +157 -25
- package/dist/dedup.js +376 -83
- package/dist/domain-classify.js +4 -2
- package/dist/index.js +7 -7
- package/dist/init.d.ts +4 -1
- package/dist/init.js +103 -1
- package/dist/llm.d.ts +19 -13
- package/dist/llm.js +25 -14
- package/dist/mcp-server.d.ts +6 -0
- package/dist/mcp-server.js +94 -13
- package/dist/mcp-stdio.d.ts +138 -0
- package/dist/mcp-stdio.js +313 -0
- package/dist/memory-instructions.d.ts +18 -0
- package/dist/memory-instructions.js +39 -2
- package/dist/nightly.js +14 -1
- package/dist/reconsolidation.d.ts +323 -0
- package/dist/reconsolidation.js +1226 -0
- package/dist/retrieval.d.ts +14 -0
- package/dist/retrieval.js +41 -3
- package/dist/state.d.ts +23 -1
- package/dist/storage.d.ts +25 -0
- package/dist/storage.js +49 -7
- package/dist/type-classify.js +4 -2
- package/dist/types.d.ts +188 -0
- package/hermes-plugin/hicortex/provider.py +29 -17
- package/opencode-plugin/hicortex/index.ts +7 -7
- package/package.json +4 -2
- package/pi-extension/hicortex/index.ts +7 -7
- 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/backup.d.ts
CHANGED
|
@@ -129,19 +129,23 @@ export declare function runBackupHook(artifactPath: string, command: string | un
|
|
|
129
129
|
*/
|
|
130
130
|
export declare function newestBackupArtifactMs(dir: string): number | undefined;
|
|
131
131
|
/**
|
|
132
|
-
* Prune a backup dir to the `retention` newest artifacts (#327).
|
|
133
|
-
* files named `hicortex-*.tar.gz` (the nightly/CLI artifact
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
132
|
+
* Prune a backup dir to the `retention` newest artifacts (#327). By default
|
|
133
|
+
* matches ONLY files named `hicortex-*.tar.gz` (the nightly/CLI artifact
|
|
134
|
+
* pattern); the optional `pattern` override scopes the prune to another
|
|
135
|
+
* product-owned filename family — e.g. the pre-dedup merge backups
|
|
136
|
+
* (`pre-dedup-*.db`, #392) keep their own retention count INDEPENDENT of the
|
|
137
|
+
* full-backup artifacts. Anything else in the dir (operator copies, notes) is
|
|
138
|
+
* never touched. Keeps the newest `retention` by mtime, with the ISO filename
|
|
139
|
+
* (time-ordered by construction) as a DESCENDING tie-break so equal mtimes
|
|
140
|
+
* resolve deterministically (the just-written artifact is the newest and
|
|
141
|
+
* always survives); deletes the rest, oldest first. `retention <= 0` keeps
|
|
142
|
+
* all.
|
|
139
143
|
*
|
|
140
144
|
* Best-effort by design: a per-file unlink failure logs and continues (a
|
|
141
145
|
* stale extra artifact is cheap; failing the nightly AFTER a good backup was
|
|
142
146
|
* written is not). Returns the number actually removed.
|
|
143
147
|
*/
|
|
144
|
-
export declare function pruneBackupArtifacts(dir: string, retention: number): number;
|
|
148
|
+
export declare function pruneBackupArtifacts(dir: string, retention: number, pattern?: RegExp): number;
|
|
145
149
|
export interface BackupCliOptions {
|
|
146
150
|
/** `--out <dir>` — output directory (the artifact is auto-named). Takes precedence over `config.backupDir`. Mutually exclusive with stdout. */
|
|
147
151
|
outDir?: string;
|
package/dist/backup.js
CHANGED
|
@@ -282,19 +282,23 @@ function newestBackupArtifactMs(dir) {
|
|
|
282
282
|
return newest;
|
|
283
283
|
}
|
|
284
284
|
/**
|
|
285
|
-
* Prune a backup dir to the `retention` newest artifacts (#327).
|
|
286
|
-
* files named `hicortex-*.tar.gz` (the nightly/CLI artifact
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
285
|
+
* Prune a backup dir to the `retention` newest artifacts (#327). By default
|
|
286
|
+
* matches ONLY files named `hicortex-*.tar.gz` (the nightly/CLI artifact
|
|
287
|
+
* pattern); the optional `pattern` override scopes the prune to another
|
|
288
|
+
* product-owned filename family — e.g. the pre-dedup merge backups
|
|
289
|
+
* (`pre-dedup-*.db`, #392) keep their own retention count INDEPENDENT of the
|
|
290
|
+
* full-backup artifacts. Anything else in the dir (operator copies, notes) is
|
|
291
|
+
* never touched. Keeps the newest `retention` by mtime, with the ISO filename
|
|
292
|
+
* (time-ordered by construction) as a DESCENDING tie-break so equal mtimes
|
|
293
|
+
* resolve deterministically (the just-written artifact is the newest and
|
|
294
|
+
* always survives); deletes the rest, oldest first. `retention <= 0` keeps
|
|
295
|
+
* all.
|
|
292
296
|
*
|
|
293
297
|
* Best-effort by design: a per-file unlink failure logs and continues (a
|
|
294
298
|
* stale extra artifact is cheap; failing the nightly AFTER a good backup was
|
|
295
299
|
* written is not). Returns the number actually removed.
|
|
296
300
|
*/
|
|
297
|
-
function pruneBackupArtifacts(dir, retention) {
|
|
301
|
+
function pruneBackupArtifacts(dir, retention, pattern = /^hicortex-.*\.tar\.gz$/) {
|
|
298
302
|
if (!Number.isFinite(retention) || retention <= 0)
|
|
299
303
|
return 0;
|
|
300
304
|
let names;
|
|
@@ -306,7 +310,7 @@ function pruneBackupArtifacts(dir, retention) {
|
|
|
306
310
|
return 0;
|
|
307
311
|
}
|
|
308
312
|
const artifacts = names
|
|
309
|
-
.filter((n) =>
|
|
313
|
+
.filter((n) => pattern.test(n))
|
|
310
314
|
.map((n) => {
|
|
311
315
|
const abs = (0, node_path_1.join)(dir, n);
|
|
312
316
|
let mtimeMs = 0;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Desktop auto-configuration (#381) — the init-side writer for the
|
|
3
|
+
* `claude_desktop_config.json` MCP entry.
|
|
4
|
+
*
|
|
5
|
+
* Claude Desktop's ONLY stdio MCP route is this hand-edited file (no CLI, no
|
|
6
|
+
* plugin discovery), which is why Desktop never had an init step while CC
|
|
7
|
+
* (`claude mcp add`), Pi, and opencode (dropped extension files) did. The
|
|
8
|
+
* 0.20.4 `hicortex mcp` stdio bridge makes such an entry meaningful, so init
|
|
9
|
+
* now offers to write it — the same auto-detect pattern as the other
|
|
10
|
+
* harnesses, gated on the Claude Desktop config DIRECTORY existing (the
|
|
11
|
+
* config FILE may not exist yet; creating it fresh is in scope).
|
|
12
|
+
*
|
|
13
|
+
* Two properties dominate the design:
|
|
14
|
+
*
|
|
15
|
+
* 1. IT IS ANOTHER APP'S FILE. `claude_desktop_config.json` is owned by
|
|
16
|
+
* Claude Desktop and carries the user's other servers and app settings.
|
|
17
|
+
* The write is therefore merge-safe (preserve EVERY top-level key and
|
|
18
|
+
* every other server; touch only `mcpServers.hicortex`), takes a
|
|
19
|
+
* timestamped `.bak` copy of the existing file first, lands via
|
|
20
|
+
* tmp-write + JSON-validate + rename in the same directory (atomic),
|
|
21
|
+
* and REFUSES — touching nothing, not even a backup — when the existing
|
|
22
|
+
* file is not valid JSON. Unlike our own config.json there is no repair
|
|
23
|
+
* flow here (quarantening another app's config would break IT); the
|
|
24
|
+
* user fixes the file by hand with the snippet init prints.
|
|
25
|
+
*
|
|
26
|
+
* 2. THE COMMAND MUST BE AN ABSOLUTE NPX PATH. Desktop is a GUI app and
|
|
27
|
+
* does not inherit the shell PATH — a bare "npx" command is the #1
|
|
28
|
+
* Desktop MCP failure mode. Resolution walks PATH plus the common
|
|
29
|
+
* install locations and rejects npm's ephemeral `/_npx/` cache (#176: a
|
|
30
|
+
* path that dies on the next cache GC — the entry would break silently
|
|
31
|
+
* weeks later). Unresolvable → manual instructions, nothing written.
|
|
32
|
+
* Windows .cmd shims cannot be spawned by Electron without a shell, so
|
|
33
|
+
* they are wrapped in `cmd /c`.
|
|
34
|
+
*
|
|
35
|
+
* Every helper is pure/parametrised (platform/env/home/exists injected) so
|
|
36
|
+
* the suite covers darwin/win32/linux without touching a real machine. This
|
|
37
|
+
* module deliberately imports NOTHING from init.ts (init imports this — no
|
|
38
|
+
* cycle); the package spec is passed in from init's getPackageSpec().
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* The Claude Desktop config directory for this platform, or null where no
|
|
42
|
+
* Desktop build exists (Linux → silent skip). Parametrised so tests cover
|
|
43
|
+
* the platform matrix without a real machine. Detection in init is
|
|
44
|
+
* `desktopConfigDir() !== null && existsSync(dir)` — the config FILE itself
|
|
45
|
+
* may legitimately not exist yet.
|
|
46
|
+
*/
|
|
47
|
+
export declare function desktopConfigDir(platform?: NodeJS.Platform, env?: Record<string, string | undefined>, home?: string): string | null;
|
|
48
|
+
export interface DesktopNpxResolveOptions {
|
|
49
|
+
/** The PATH string to search (default: process.env.PATH). */
|
|
50
|
+
pathEnv?: string;
|
|
51
|
+
/** Platform under test (default: the real one). */
|
|
52
|
+
platform?: NodeJS.Platform;
|
|
53
|
+
/** Home dir for the ~-relative common locations (default: homedir()). */
|
|
54
|
+
home?: string;
|
|
55
|
+
/** Env for APPDATA on win32 (default: process.env). */
|
|
56
|
+
env?: Record<string, string | undefined>;
|
|
57
|
+
/** Existence seam (default: existsSync) — unit tests pass fixture sets. */
|
|
58
|
+
exists?: (candidatePath: string) => boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve an ABSOLUTE npx path for the Desktop entry, or null when nothing
|
|
62
|
+
* durable exists (init then prints manual instructions and writes nothing).
|
|
63
|
+
* Rejects npm's ephemeral npx cache in BOTH separator spellings — unix
|
|
64
|
+
* `/_npx/` and Windows `\_npx\` — a bare includes("/_npx/") would let the
|
|
65
|
+
* win32 form through (#176: the path dies on the next cache GC).
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolveDesktopNpxPath(options?: DesktopNpxResolveOptions): string | null;
|
|
68
|
+
/** The `mcpServers.hicortex` value — a Claude Desktop stdio server entry. */
|
|
69
|
+
export interface DesktopServerEntry {
|
|
70
|
+
command: string;
|
|
71
|
+
args: string[];
|
|
72
|
+
/** Only in remote mode; absent (not empty) in local/loopback mode. */
|
|
73
|
+
env?: Record<string, string>;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build the stdio entry for the `hicortex mcp` bridge. NO "type" field —
|
|
77
|
+
* stdio is implied for Desktop entries (the CC `.claude.json` writer needs
|
|
78
|
+
* `"type":"sse"`; this is the other shape). A `.cmd`/`.bat` shim is wrapped
|
|
79
|
+
* in `cmd /c` because Electron spawns without a shell and cannot exec a cmd
|
|
80
|
+
* script directly. An empty/absent env omits the key entirely — a local
|
|
81
|
+
* entry must carry no env (the bridge autostarts the daemon; loopback
|
|
82
|
+
* bypasses auth).
|
|
83
|
+
*/
|
|
84
|
+
export declare function buildDesktopServerEntry(npxPath: string, packageSpec: string, env?: Record<string, string>): DesktopServerEntry;
|
|
85
|
+
/**
|
|
86
|
+
* Loopback check for a server URL — decides whether the Desktop entry needs
|
|
87
|
+
* an env block at all (local: none, the bridge resolves + autostarts the
|
|
88
|
+
* daemon; remote: URL + token). Mirrors mcp-stdio's isLoopbackHost (kept
|
|
89
|
+
* local rather than imported to avoid dragging the SDK into init's graph).
|
|
90
|
+
* An unparseable URL is NOT local — fail toward carrying the env, which
|
|
91
|
+
* still works everywhere the URL is real.
|
|
92
|
+
*/
|
|
93
|
+
export declare function isLocalServerUrl(url: string): boolean;
|
|
94
|
+
export type DesktopMergeResult = {
|
|
95
|
+
ok: true;
|
|
96
|
+
config: Record<string, unknown>;
|
|
97
|
+
} | {
|
|
98
|
+
ok: false;
|
|
99
|
+
reason: string;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Parse the existing config text and merge our entry in, preserving every
|
|
103
|
+
* top-level key and every other server. `rawText === null` means "no file"
|
|
104
|
+
* (fresh install — a config containing only our entry is created). Malformed
|
|
105
|
+
* JSON, a non-object document, or a non-object `mcpServers` value returns
|
|
106
|
+
* `{ ok: false }` — the caller refuses to write, so the ORIGINAL file and
|
|
107
|
+
* its bytes are what the user keeps.
|
|
108
|
+
*/
|
|
109
|
+
export declare function mergeDesktopServerConfig(rawText: string | null, entry: DesktopServerEntry): DesktopMergeResult;
|
|
110
|
+
export type DesktopWriteResult = {
|
|
111
|
+
status: "written";
|
|
112
|
+
backupPath?: string;
|
|
113
|
+
} | {
|
|
114
|
+
status: "refused";
|
|
115
|
+
reason: string;
|
|
116
|
+
} | {
|
|
117
|
+
status: "failed";
|
|
118
|
+
reason: string;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Orchestrate the merge-safe, atomic write of our entry into
|
|
122
|
+
* `claude_desktop_config.json`:
|
|
123
|
+
*
|
|
124
|
+
* 1. Read the existing file (ENOENT → fresh-install path).
|
|
125
|
+
* 2. Merge; a malformed/shape-refused file → `{ status: "refused" }` with
|
|
126
|
+
* NOTHING touched — no backup, no tmp, no bytes changed.
|
|
127
|
+
* 3. Copy the existing file to `<path>.bak-<ISO-colons-stripped>` BEFORE
|
|
128
|
+
* writing (the quarantineMalformedConfig naming convention).
|
|
129
|
+
* 4. Write the serialized payload to a tmp file in the SAME directory
|
|
130
|
+
* (same filesystem → the rename is atomic), JSON-parse the exact bytes
|
|
131
|
+
* that landed on disk, then renameSync over the target.
|
|
132
|
+
*
|
|
133
|
+
* Any unexpected I/O error returns `{ status: "failed" }` after removing the
|
|
134
|
+
* tmp file — a half-written tmp must never masquerade as a config. Never
|
|
135
|
+
* throws; the init wiring turns every non-"written" result into a printed
|
|
136
|
+
* warning and init continues.
|
|
137
|
+
*/
|
|
138
|
+
export declare function writeDesktopServerConfig(configPath: string, entry: DesktopServerEntry): DesktopWriteResult;
|