@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/README.md
CHANGED
|
@@ -156,6 +156,21 @@ The run is resumable — interrupt it any time and it continues where it stopped
|
|
|
156
156
|
|
|
157
157
|
Explicit learnings: call `hicortex_ingest` directly (capture is otherwise automatic, nightly).
|
|
158
158
|
|
|
159
|
+
### Connect an MCP client (stdio)
|
|
160
|
+
|
|
161
|
+
MCP clients that launch stdio servers (Claude Desktop, Cursor, the MCP Registry's install flow) should run:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npx -y @gamaze/hicortex mcp
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The command speaks MCP over stdin/stdout and bridges to the Hicortex daemon — it does not open the database itself. Two optional environment variables control where it connects:
|
|
168
|
+
|
|
169
|
+
- `HICORTEX_SERVER_URL` — URL of a remote Hicortex server (e.g. `https://your-server:8787`). Omit to use the local machine.
|
|
170
|
+
- `HICORTEX_AUTH_TOKEN` — bearer token for a remote server (the server prints it via `hicortex status`). A local server needs no token.
|
|
171
|
+
|
|
172
|
+
With no server running, a local one is started automatically (detached; it keeps running after the MCP client exits — the same persistent daemon `init` installs). Registry/stdio configs (Claude Desktop, Cursor, etc.) point at the command above.
|
|
173
|
+
|
|
159
174
|
## Identity Layer
|
|
160
175
|
|
|
161
176
|
Beyond auto-distilled memories and lessons, Hicortex holds a **hand-edited identity layer** — standing "who you are + how to work" Markdown injected into every session at start. Unlike memories, it is **never distilled, scored, or decayed**: what you write stays verbatim until you change it.
|
|
@@ -183,6 +198,7 @@ One server serves a fleet of distinct-persona agents. Each agent can have its **
|
|
|
183
198
|
|
|
184
199
|
```bash
|
|
185
200
|
npx @gamaze/hicortex server # Start MCP server (port 8787)
|
|
201
|
+
npx @gamaze/hicortex mcp # Speak MCP over stdio (bridge to the local daemon or HICORTEX_SERVER_URL)
|
|
186
202
|
npx @gamaze/hicortex init # Set up server mode
|
|
187
203
|
npx @gamaze/hicortex init --server <url> # Set up client mode
|
|
188
204
|
npx @gamaze/hicortex nightly # Run distill + consolidate (full nightly)
|
package/assets/dashboard.html
CHANGED
|
@@ -121,6 +121,69 @@
|
|
|
121
121
|
display: inline-block; padding: 1px 8px; border-radius: 10px;
|
|
122
122
|
background: rgba(128,128,128,0.18); font-size: 11px; color: var(--text-dim);
|
|
123
123
|
}
|
|
124
|
+
|
|
125
|
+
/* ---- account menu (#365) ---- */
|
|
126
|
+
/* Dropdown under the nav account element: token reveal/copy, connect
|
|
127
|
+
snippets, plan & billing, sign-out. The wrapper (#nav-account) becomes
|
|
128
|
+
the positioning context; the panel is absolute + right-aligned under it,
|
|
129
|
+
above page content (nav is z-50, the panel gets 60). */
|
|
130
|
+
.hc-nav-account { position: relative; }
|
|
131
|
+
#plan-billing { scroll-margin-top: 68px; } /* sticky nav clears the anchor jump */
|
|
132
|
+
.acct-trigger {
|
|
133
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
134
|
+
background: transparent; color: var(--text); border: none; border-radius: 6px;
|
|
135
|
+
padding: 4px 8px; font: inherit; cursor: pointer;
|
|
136
|
+
}
|
|
137
|
+
.acct-trigger:hover { background: rgba(128,128,128,0.15); }
|
|
138
|
+
.acct-caret { color: var(--text-dim); font-size: 10px; }
|
|
139
|
+
.acct-menu {
|
|
140
|
+
display: none; position: absolute; top: calc(100% + 8px); right: 0;
|
|
141
|
+
min-width: 320px; max-width: min(420px, calc(100vw - 40px));
|
|
142
|
+
background: var(--panel); border: 1px solid var(--panel-border); border-radius: 8px;
|
|
143
|
+
padding: 12px; z-index: 60;
|
|
144
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
|
|
145
|
+
color: var(--text); font-size: 13px; text-align: left;
|
|
146
|
+
}
|
|
147
|
+
.acct-menu.open { display: block; }
|
|
148
|
+
.acct-label {
|
|
149
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
|
|
150
|
+
color: var(--text-dim); margin: 8px 0 4px;
|
|
151
|
+
}
|
|
152
|
+
.acct-menu .acct-label:first-child { margin-top: 0; }
|
|
153
|
+
.acct-token-row { display: flex; align-items: center; gap: 6px; }
|
|
154
|
+
.acct-token {
|
|
155
|
+
flex: 1; min-width: 0;
|
|
156
|
+
font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
157
|
+
background: var(--bg); border: 1px solid var(--panel-border); border-radius: 4px;
|
|
158
|
+
padding: 4px 8px; color: var(--text);
|
|
159
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
160
|
+
}
|
|
161
|
+
.acct-btn {
|
|
162
|
+
background: var(--panel); color: var(--text);
|
|
163
|
+
border: 1px solid var(--panel-border); border-radius: 4px;
|
|
164
|
+
padding: 4px 10px; font: inherit; font-size: 12px; cursor: pointer;
|
|
165
|
+
}
|
|
166
|
+
.acct-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
167
|
+
.acct-btn:disabled { opacity: 0.5; cursor: default; }
|
|
168
|
+
.acct-menu details { border-top: 1px solid var(--panel-border); margin-top: 10px; padding-top: 8px; }
|
|
169
|
+
.acct-menu summary { cursor: pointer; color: var(--text-dim); font-size: 13px; }
|
|
170
|
+
.acct-menu summary:hover { color: var(--accent); }
|
|
171
|
+
.acct-snippet { margin-bottom: 8px; }
|
|
172
|
+
.acct-snippet pre {
|
|
173
|
+
background: var(--bg); border: 1px solid var(--panel-border); border-radius: 4px;
|
|
174
|
+
padding: 8px; margin: 4px 0;
|
|
175
|
+
font: 11.5px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
176
|
+
white-space: pre-wrap; word-break: break-all;
|
|
177
|
+
max-height: 200px; overflow-y: auto;
|
|
178
|
+
}
|
|
179
|
+
.acct-link {
|
|
180
|
+
display: block; width: 100%; text-align: left;
|
|
181
|
+
background: transparent; border: none; border-radius: 4px;
|
|
182
|
+
color: var(--text); font: inherit; font-size: 13px;
|
|
183
|
+
padding: 6px 8px; cursor: pointer;
|
|
184
|
+
}
|
|
185
|
+
.acct-link:hover { background: rgba(128,128,128,0.15); }
|
|
186
|
+
.acct-link.acct-danger:hover { color: var(--danger); }
|
|
124
187
|
</style>
|
|
125
188
|
</head>
|
|
126
189
|
<body>
|
|
@@ -180,7 +243,10 @@
|
|
|
180
243
|
</div>
|
|
181
244
|
</div>
|
|
182
245
|
|
|
183
|
-
|
|
246
|
+
<!-- #plan-billing: the always-rendered "upgrade + usage" digest panel — the
|
|
247
|
+
account menu's "Plan & billing" scrolls here, and the monthly plan
|
|
248
|
+
selector lands in this section (#371, separate issue). -->
|
|
249
|
+
<div id="plan-billing" class="panel" style="margin-bottom:24px;">
|
|
184
250
|
<h2>Nightly activity digest — <span id="digest-date" class="muted"></span></h2>
|
|
185
251
|
<div id="digest-stages" style="margin-bottom:12px;"></div>
|
|
186
252
|
<h2 style="margin-top:16px;">New memories (sample, rendered as in recall)</h2>
|
|
@@ -667,19 +733,22 @@ function escapeHtml(s) {
|
|
|
667
733
|
}
|
|
668
734
|
|
|
669
735
|
// ---------------------------------------------------------------------------
|
|
670
|
-
// Render: account identity (hosted) — "Name · Org" + plan pill in the nav
|
|
671
|
-
// Rendered ONLY when at least one
|
|
672
|
-
// default → no element, no empty
|
|
673
|
-
//
|
|
736
|
+
// Render: account identity (hosted) — "Name · Org" + plan pill in the nav,
|
|
737
|
+
// wrapped as the ACCOUNT MENU trigger (#365). Rendered ONLY when at least one
|
|
738
|
+
// field is non-null (all-null = self-hosted default → no element, no empty
|
|
739
|
+
// pill, no menu). Values are operator-set config, but escapeHtml anyway (same
|
|
740
|
+
// posture as every other innerHTML sink here).
|
|
674
741
|
// ---------------------------------------------------------------------------
|
|
675
742
|
function renderAccount(a) {
|
|
676
743
|
const host = $("nav-account");
|
|
677
744
|
// Trial users (no plan) get the Upgrade pill — the checkout entry (#298 drill,
|
|
678
745
|
// #299's first brick). Session-authenticated: the browser's hicortex-session
|
|
679
746
|
// cookie rides this same-origin fetch; the router maps it to the tenant.
|
|
747
|
+
// #365: the Upgrade button keeps its render position + behavior but sits
|
|
748
|
+
// OUTSIDE the menu trigger; stopPropagation so it never toggles the menu.
|
|
680
749
|
const upgrade =
|
|
681
750
|
a && a.plan === null
|
|
682
|
-
? '
|
|
751
|
+
? '<button id="nav-upgrade" type="button" style="background:var(--accent);color:#fff;border:none;border-radius:4px;padding:4px 12px;font:inherit;font-size:13px;cursor:pointer;">Upgrade</button>'
|
|
683
752
|
: "";
|
|
684
753
|
if (!a || (a.name === null && a.org === null && a.plan === null)) {
|
|
685
754
|
host.innerHTML = upgrade || "";
|
|
@@ -693,10 +762,251 @@ function renderAccount(a) {
|
|
|
693
762
|
const plan = typeof a.plan === "string" && a.plan !== ""
|
|
694
763
|
? `<span class="plan">${escapeHtml(a.plan)}</span>`
|
|
695
764
|
: "";
|
|
696
|
-
host.innerHTML =
|
|
697
|
-
|
|
765
|
+
host.innerHTML = `
|
|
766
|
+
<button id="acct-trigger" type="button" class="acct-trigger" aria-haspopup="menu" aria-expanded="false">
|
|
767
|
+
${who ? who + " " : ""}${plan}<span class="acct-caret" aria-hidden="true">▾</span>
|
|
768
|
+
</button>${upgrade}
|
|
769
|
+
<div id="acct-menu" class="acct-menu">
|
|
770
|
+
<div class="acct-label">Connection token</div>
|
|
771
|
+
<div class="acct-token-row">
|
|
772
|
+
<code id="acct-token-val" class="acct-token">…</code>
|
|
773
|
+
<button id="acct-reveal" type="button" class="acct-btn" disabled>Reveal</button>
|
|
774
|
+
<button id="acct-copy-token" type="button" class="acct-btn" disabled>Copy</button>
|
|
775
|
+
</div>
|
|
776
|
+
<details id="acct-connect">
|
|
777
|
+
<summary>Connect an agent</summary>
|
|
778
|
+
<div class="acct-label">CLI</div>
|
|
779
|
+
<div class="acct-snippet">
|
|
780
|
+
<pre id="acct-cli"></pre>
|
|
781
|
+
<button id="acct-copy-cli" type="button" class="acct-btn" disabled>Copy</button>
|
|
782
|
+
</div>
|
|
783
|
+
<div class="acct-label">MCP — ~/.claude.json</div>
|
|
784
|
+
<div class="acct-snippet">
|
|
785
|
+
<pre id="acct-mcp"></pre>
|
|
786
|
+
<button id="acct-copy-mcp" type="button" class="acct-btn" disabled>Copy</button>
|
|
787
|
+
</div>
|
|
788
|
+
</details>
|
|
789
|
+
<button id="acct-plan" type="button" class="acct-link">Plan & billing</button>
|
|
790
|
+
<button id="acct-signout" type="button" class="acct-link acct-danger">Sign out</button>
|
|
791
|
+
</div>`;
|
|
792
|
+
wireAcctMenu();
|
|
793
|
+
if (upgrade) {
|
|
794
|
+
wireUpgrade();
|
|
795
|
+
$("nav-upgrade").addEventListener("click", (e) => e.stopPropagation());
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// ---------------------------------------------------------------------------
|
|
800
|
+
// Account menu (#365) — dropdown under the nav account element. The token
|
|
801
|
+
// displayed is AUTHORITATIVE server truth: a lazy GET /account/token on the
|
|
802
|
+
// FIRST open (echo-only endpoint — the fetch already proves the caller holds
|
|
803
|
+
// the token). localStorage is NEVER trusted for the display: it can be stale
|
|
804
|
+
// after token rotation and is absent entirely for browser sessions the
|
|
805
|
+
// hosted router authenticates via its session→bearer injection. On fetch
|
|
806
|
+
// failure the row fails explicitly ("token unavailable", actions disabled).
|
|
807
|
+
// ---------------------------------------------------------------------------
|
|
808
|
+
let acctToken = null; // token from /account/token (server truth)
|
|
809
|
+
let acctTokenState = "unfetched"; // "unfetched" | "ok" | "unavailable"
|
|
810
|
+
let acctTokenFetched = false; // one lazy fetch attempt per page load
|
|
811
|
+
let acctRevealed = false; // unmasked while the menu is open
|
|
812
|
+
|
|
813
|
+
function maskToken(t) {
|
|
814
|
+
// hctx-••••••23f9 — first 5 chars + 6 bullets + last 4. Real tokens are
|
|
815
|
+
// hctx-<32hex>; the guard keeps a degenerate short token from leaking.
|
|
816
|
+
if (typeof t !== "string" || t.length < 10) return "••••••••••";
|
|
817
|
+
return t.slice(0, 5) + "••••••" + t.slice(-4);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// The connect line: server URL + token on ONE line, space-separated.
|
|
821
|
+
function acctConnectLine() {
|
|
822
|
+
return location.origin + " " + acctToken;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// CLI snippet — init has NO --token flag; it prompts interactively. The
|
|
826
|
+
// comment mirrors init.ts's own printed hint ("When prompted for the token,
|
|
827
|
+
// paste: …").
|
|
828
|
+
function acctCliText() {
|
|
829
|
+
return "# When prompted for the token, paste: " + acctToken +
|
|
830
|
+
"\nnpx @gamaze/hicortex init --server " + location.origin;
|
|
698
831
|
}
|
|
699
832
|
|
|
833
|
+
// MCP snippet — mirrors EXACTLY what runClientInit writes into ~/.claude.json
|
|
834
|
+
// (init.ts): SSE transport at <origin>/sse with a bearer Authorization header.
|
|
835
|
+
function acctMcpText() {
|
|
836
|
+
return JSON.stringify({
|
|
837
|
+
mcpServers: {
|
|
838
|
+
hicortex: {
|
|
839
|
+
type: "sse",
|
|
840
|
+
url: location.origin + "/sse",
|
|
841
|
+
headers: { Authorization: "Bearer " + acctToken },
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
}, null, 2);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function renderAcctToken() {
|
|
848
|
+
const val = $("acct-token-val");
|
|
849
|
+
if (!val) return; // menu not rendered (self-hosted all-null account)
|
|
850
|
+
const revealBtn = $("acct-reveal");
|
|
851
|
+
const copyToken = $("acct-copy-token");
|
|
852
|
+
const copyCli = $("acct-copy-cli");
|
|
853
|
+
const copyMcp = $("acct-copy-mcp");
|
|
854
|
+
if (acctTokenState !== "ok") {
|
|
855
|
+
val.textContent = acctTokenState === "unfetched" ? "…" : "token unavailable";
|
|
856
|
+
revealBtn.disabled = true;
|
|
857
|
+
copyToken.disabled = true;
|
|
858
|
+
copyCli.disabled = true;
|
|
859
|
+
copyMcp.disabled = true;
|
|
860
|
+
$("acct-cli").textContent = acctTokenState === "unfetched" ? "" : "token unavailable";
|
|
861
|
+
$("acct-mcp").textContent = acctTokenState === "unfetched" ? "" : "token unavailable";
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
revealBtn.disabled = false;
|
|
865
|
+
copyToken.disabled = false;
|
|
866
|
+
copyCli.disabled = false;
|
|
867
|
+
copyMcp.disabled = false;
|
|
868
|
+
val.textContent = acctRevealed ? acctToken : maskToken(acctToken);
|
|
869
|
+
revealBtn.textContent = acctRevealed ? "Hide" : "Reveal";
|
|
870
|
+
$("acct-cli").textContent = acctCliText();
|
|
871
|
+
$("acct-mcp").textContent = acctMcpText();
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async function fetchAcctToken() {
|
|
875
|
+
if (acctTokenFetched) return;
|
|
876
|
+
acctTokenFetched = true;
|
|
877
|
+
const headers = {};
|
|
878
|
+
if (token) headers["Authorization"] = "Bearer " + token; // same as loadAccount
|
|
879
|
+
try {
|
|
880
|
+
const resp = await fetch("/account/token", { headers });
|
|
881
|
+
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
|
882
|
+
const data = await resp.json();
|
|
883
|
+
if (typeof data.token !== "string" || !data.token) throw new Error("bad payload");
|
|
884
|
+
acctToken = data.token;
|
|
885
|
+
acctTokenState = "ok";
|
|
886
|
+
} catch (e) {
|
|
887
|
+
acctTokenState = "unavailable"; // fail explicitly — no localStorage fallback
|
|
888
|
+
}
|
|
889
|
+
renderAcctToken();
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function closeAcctMenu() {
|
|
893
|
+
const menu = document.getElementById("acct-menu");
|
|
894
|
+
if (!menu) return;
|
|
895
|
+
menu.classList.remove("open");
|
|
896
|
+
const trigger = document.getElementById("acct-trigger");
|
|
897
|
+
if (trigger) trigger.setAttribute("aria-expanded", "false");
|
|
898
|
+
// Masked again + connect section collapsed on EVERY close (locked design).
|
|
899
|
+
acctRevealed = false;
|
|
900
|
+
const details = document.getElementById("acct-connect");
|
|
901
|
+
if (details) details.removeAttribute("open");
|
|
902
|
+
renderAcctToken();
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function openAcctMenu() {
|
|
906
|
+
const menu = document.getElementById("acct-menu");
|
|
907
|
+
if (!menu) return;
|
|
908
|
+
menu.classList.add("open");
|
|
909
|
+
document.getElementById("acct-trigger").setAttribute("aria-expanded", "true");
|
|
910
|
+
fetchAcctToken(); // lazy — one attempt, on first open only
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Clipboard helper: async API when available, temporary <textarea> +
|
|
914
|
+
// execCommand fallback (http remote origins lack the async clipboard API).
|
|
915
|
+
// Brief "Copied" feedback (label swap for ~1.5 s); total failure alerts.
|
|
916
|
+
function copyToClipboard(text, btn) {
|
|
917
|
+
const flash = () => {
|
|
918
|
+
const orig = btn.dataset.origLabel || (btn.dataset.origLabel = btn.textContent);
|
|
919
|
+
btn.textContent = "Copied";
|
|
920
|
+
clearTimeout(btn._copiedTimer);
|
|
921
|
+
btn._copiedTimer = setTimeout(() => { btn.textContent = orig; }, 1500);
|
|
922
|
+
};
|
|
923
|
+
const legacy = () => {
|
|
924
|
+
try {
|
|
925
|
+
const ta = document.createElement("textarea");
|
|
926
|
+
ta.value = text;
|
|
927
|
+
ta.style.position = "fixed";
|
|
928
|
+
ta.style.opacity = "0";
|
|
929
|
+
document.body.appendChild(ta);
|
|
930
|
+
ta.select();
|
|
931
|
+
const ok = document.execCommand("copy");
|
|
932
|
+
document.body.removeChild(ta);
|
|
933
|
+
return ok;
|
|
934
|
+
} catch (e) { return false; }
|
|
935
|
+
};
|
|
936
|
+
const fallback = () => {
|
|
937
|
+
if (legacy()) { flash(); return; }
|
|
938
|
+
alert("Copy failed — select the text and copy it manually.");
|
|
939
|
+
};
|
|
940
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
941
|
+
navigator.clipboard.writeText(text).then(flash, fallback);
|
|
942
|
+
} else {
|
|
943
|
+
fallback();
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Sign-out is CLIENT-SIDE ONLY (#365): there is no server session to
|
|
948
|
+
// invalidate in the Hicortex server itself — auth is per-request bearer, so
|
|
949
|
+
// dropping the locally stored tokens IS the sign-out. (The hosted router
|
|
950
|
+
// separately keeps a Google session cookie and offers /auth/logout — out of
|
|
951
|
+
// scope here; this only ends the browser's console session.)
|
|
952
|
+
function signOut() {
|
|
953
|
+
try {
|
|
954
|
+
localStorage.removeItem("hicortexToken");
|
|
955
|
+
localStorage.removeItem("hicortex-dashboard-token");
|
|
956
|
+
} catch (e) { /* private mode — reload anyway */ }
|
|
957
|
+
location.reload(); // back to the unauthenticated 401-prompt state
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function wireAcctMenu() {
|
|
961
|
+
const trigger = document.getElementById("acct-trigger");
|
|
962
|
+
if (!trigger) return;
|
|
963
|
+
trigger.addEventListener("click", () => {
|
|
964
|
+
const menu = document.getElementById("acct-menu");
|
|
965
|
+
if (menu && menu.classList.contains("open")) closeAcctMenu();
|
|
966
|
+
else openAcctMenu();
|
|
967
|
+
});
|
|
968
|
+
$("acct-reveal").addEventListener("click", () => {
|
|
969
|
+
acctRevealed = !acctRevealed;
|
|
970
|
+
renderAcctToken();
|
|
971
|
+
});
|
|
972
|
+
const copyToken = $("acct-copy-token");
|
|
973
|
+
copyToken.addEventListener("click", () => {
|
|
974
|
+
if (acctTokenState === "ok") copyToClipboard(acctConnectLine(), copyToken);
|
|
975
|
+
});
|
|
976
|
+
const copyCli = $("acct-copy-cli");
|
|
977
|
+
copyCli.addEventListener("click", () => {
|
|
978
|
+
if (acctTokenState === "ok") {
|
|
979
|
+
copyToClipboard("npx @gamaze/hicortex init --server " + location.origin, copyCli);
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
const copyMcp = $("acct-copy-mcp");
|
|
983
|
+
copyMcp.addEventListener("click", () => {
|
|
984
|
+
if (acctTokenState === "ok") copyToClipboard(acctMcpText(), copyMcp);
|
|
985
|
+
});
|
|
986
|
+
// Plan & billing (same page): close the menu, scroll to the digest panel's
|
|
987
|
+
// budget/usage section — the area #371's plan selector will land in.
|
|
988
|
+
$("acct-plan").addEventListener("click", () => {
|
|
989
|
+
closeAcctMenu();
|
|
990
|
+
const el = document.getElementById("plan-billing");
|
|
991
|
+
if (el) el.scrollIntoView({ behavior: "smooth" });
|
|
992
|
+
});
|
|
993
|
+
$("acct-signout").addEventListener("click", signOut);
|
|
994
|
+
renderAcctToken();
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Click-outside + Escape close the menu (wired once at document level; the
|
|
998
|
+
// handlers no-op when the menu isn't rendered).
|
|
999
|
+
document.addEventListener("click", (e) => {
|
|
1000
|
+
const menu = document.getElementById("acct-menu");
|
|
1001
|
+
if (!menu || !menu.classList.contains("open")) return;
|
|
1002
|
+
const wrap = document.getElementById("nav-account");
|
|
1003
|
+
if (wrap && wrap.contains(e.target)) return;
|
|
1004
|
+
closeAcctMenu();
|
|
1005
|
+
});
|
|
1006
|
+
document.addEventListener("keydown", (e) => {
|
|
1007
|
+
if (e.key === "Escape") closeAcctMenu();
|
|
1008
|
+
});
|
|
1009
|
+
|
|
700
1010
|
// Account data source: the dedicated lightweight GET /account (same shape as
|
|
701
1011
|
// the account block /dashboard/data still carries — kept there for the stable
|
|
702
1012
|
// payload contract). Fetched independently so the nav renders fail-soft and
|