@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 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)
@@ -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
- <div class="panel" style="margin-bottom:24px;">
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 field is non-null (all-null = self-hosted
672
- // default → no element, no empty pill). Values are operator-set config, but
673
- // escapeHtml anyway (same posture as every other innerHTML sink here).
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
- ? ' <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>'
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 = (who ? who + " " : "") + plan + upgrade;
697
- if (upgrade) wireUpgrade();
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 &amp; 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