@contextq/cli 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +40 -4
  2. package/dist/index.js +63 -33
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -15,18 +15,42 @@ npm install -g @contextq/cli
15
15
  contextq init
16
16
  ```
17
17
 
18
+ ## `contextq login`
19
+
20
+ ```bash
21
+ npx @contextq/cli login [--url <url>]
22
+ ```
23
+
24
+ Prints a link and a short code, then waits while you approve this machine in the browser (sign in to the dashboard, open the link, confirm the code). The CLI fetches the key, checks it against `GET /api/workspaces`, and only then saves it to the platform credential file (`~/Library/Application Support/contextq/credentials` on macOS, `${XDG_CONFIG_HOME:-~/.config}/contextq/credentials` on Linux, mode 600, the previous file backed up first). You never type or paste the key. Ctrl-C, a denial, an expired code or a key that does not verify leave any stored credential byte-identical.
25
+
26
+ ## `contextq local start|status|stop`
27
+
28
+ ```bash
29
+ npx @contextq/cli local start [--port 47321]
30
+ npx @contextq/cli local status
31
+ npx @contextq/cli local stop
32
+ ```
33
+
34
+ Runs ContextQ memory on this machine with no account (local mode, contract: `docs/arch/edge-local-only-contract.md`). `start` installs the pinned `@contextq/edge` into a private runtime directory, checks that Node is 22 or newer and that `better-sqlite3` loads (macOS arm64 and Linux x64 with npm prebuilt binaries; anything else gets one sentence and nothing is started), then starts `contextq-edge serve` in the background on `127.0.0.1` and records its pid. Data, keys, the local token and the runtime live in `~/Library/Application Support/contextq/local/` on macOS and `${XDG_DATA_HOME:-~/.local/share}/contextq/local/` on Linux, never in the plugin cache or the repo. `start` and `stop` are idempotent; a process on the port that this CLI did not start is reported and left alone. Nothing starts at login. If `sqlite-vec` does not load, search runs keyword-only and `start` says so.
35
+
36
+ ## `contextq init --local` and `contextq attach`
37
+
38
+ `contextq init --local [--workspace <slug>] [--port <n>]` turns local mode on for the repo you are in: it runs `local start`, installs the ContextQ plugin from the public marketplace (or switches an installed one) pointed at the local server with its local token, binds `.contextq/config.json` with `mode: local`, and checks that a memory read and the boot pack answer. It needs no account, makes no authenticated call and writes no secret or machine path into tracked files; a re-run changes nothing.
39
+
40
+ `contextq attach [--url <url>]` moves local memory to an account in two journaled phases (`<state dir>/attach-journal.json`, mode 600): sign in (browser, or `--key-stdin` for automation), map each local workspace to the slug the server returns, upload every pending note with per-note acknowledgements; then lock local writes, switch the plugin and the repo binding to the cloud, and verify the cloud boot and a read before reporting success. A failure at any step rolls back to local with writes unlocked; run it again to resume (nothing is sent twice). `--leave-denied` keeps refused notes local-only, `--restart` discards an unfinished journal, `--abort` rolls back an interrupted switch.
41
+
18
42
  ## `contextq init`
19
43
 
20
- Detects which agent surfaces the current directory and machine are set up for (Claude Code, Codex, Gemini CLI, Cursor, Copilot CLI — often several at once) and writes the wiring each one needs.
44
+ Detects which agent surfaces the current directory and machine are set up for (Claude Code, Codex, Gemini CLI, Cursor, Copilot CLI, OpenCode — often several at once) and writes the wiring each one needs.
21
45
 
22
46
  | Option | Meaning |
23
47
  |---|---|
24
- | `--key <sk_live_...>` | API key. Falls back to `CONTEXT_API_KEY` / `CTQ_API_KEY` in the environment, then to an interactive prompt. |
48
+ | `--key <sk_live_...>` | API key. Falls back to the stored credential, then `CONTEXT_API_KEY` / `CTQ_API_KEY` in the environment, then (from an interactive terminal) the same browser approval as `contextq login`. |
25
49
  | `--url <url>` | ContextQ API URL. Defaults to `https://app.contextq.dev`; point it at your own instance when self-hosting. |
26
50
  | `--workspace <slug>` | Workspace the boot hook reads from. Defaults to the repo basename, sanitized. Lowercase letters, digits, `-` and `_`; must start with a letter or digit; 64 chars max. Case is folded to lowercase, since workspace slugs always are. |
27
51
  | `--agent <slug>` | Agent identity the session boots and checkpoints under. Same defaulting and same character rules as `--workspace`. |
28
52
  | `--embed-key` | Write the literal API key into `.mcp.json` instead of the `${CONTEXT_API_KEY}` placeholder. Opt-in on purpose: it trades a secret sitting on disk for not having to export the variable. |
29
- | `--surface <s>` | Force detection to exactly one of `claude-code`, `codex`, `gemini`, `cursor` or `copilot`, skipping autodetection. |
53
+ | `--surface <s>` | Force detection to exactly one of `claude-code`, `codex`, `gemini`, `cursor`, `copilot` or `opencode`, skipping autodetection. |
30
54
  | `--no-plugin` | Wire Claude Code through `.mcp.json` even when Claude Code 2.1.281+ is installed. |
31
55
  | `-h`, `--help` | Show usage. |
32
56
 
@@ -38,7 +62,7 @@ When a `claude` binary of version 2.1.281 or newer is on `PATH`, `init` installs
38
62
 
39
63
  `init` falls back to `.mcp.json` when the binary is missing or older, when the install reports a failure, or when `.mcp.json` already wires contextq (installing the plugin on top would register the same MCP server twice). Running `init` in a second repo with a different key replaces the key the first repo's plugin uses, because the plugin's `apiKey` is stored once per machine, not per repo.
40
64
 
41
- `init` checks the key against `GET /api/workspaces` before it writes anything. A key the server refuses with 401 or 403 is never saved and nothing is wired: from a terminal `init` asks for a replacement (also when the refused key is the one already stored), otherwise it exits 1 and names `init --key`. A server that cannot be reached is not treated as a bad key: the key is saved with a `could not verify` warning, and the host configs that would carry it are skipped.
65
+ `init` checks the key against `GET /api/workspaces` before it writes anything. A key the server refuses with 401 or 403 is never saved and nothing is wired: from a terminal `init` runs the browser approval to replace it (also when the refused key is the one already stored), otherwise it exits 1 and names `init --key`. A browser-approved key that cannot be verified is never saved. A server that cannot be reached is not treated as a bad key: the key is saved with a `could not verify` warning, and the host configs that would carry it are skipped.
42
66
 
43
67
  ### Gemini CLI, Cursor and Copilot CLI: remote MCP in the host's own config
44
68
 
@@ -50,6 +74,10 @@ It refuses, and changes nothing, when the file is not valid JSON, or when it sit
50
74
 
51
75
  Without the plugin, `init` writes a SessionStart hook to `.claude/settings.local.json` (Claude Code) and `.codex/hooks.json` (Codex, gitignored by `init`). The hook runs `npx -y --prefer-offline @contextq/cli hook boot --format <host>`, which reads the key from the credentials file `init` wrote (or `CONTEXT_API_KEY`), resolves the repo from the session's working directory, and asks `POST /api/agent/hooks/session-start` for the boot pack: the same endpoint and output the plugin hook uses. A refused key or an unreachable server prints one line saying so instead of nothing. `init` refuses, and changes nothing, when the hook file is not valid JSON or is tracked by git. Codex runs a project hook only after you trust it: start `codex` in the repo and approve it at the hook-trust prompt.
52
76
 
77
+ ### OpenCode: remote MCP in the user config
78
+
79
+ OpenCode is detected when `~/.config/opencode/` exists (`$XDG_CONFIG_HOME/opencode/` when set). `init` adds a `contextq` remote server pointing at `<url>/mcp` to `~/.config/opencode/opencode.json`, with the key as a literal `Authorization` header and the file at mode 600. The shape follows the installed version: OpenCode 2 (`opencode --version` 2.x, or no `opencode` on PATH) gets `mcp.servers.contextq` with `oauth: false`, OpenCode 1.x gets `mcp.contextq` with `enabled: true`. A config that only exists as `opencode.jsonc` is left alone and reported, as is one that is git-tracked and not ignored. No boot hook is wired for OpenCode; memory reaches it through the MCP tools. Check with `opencode mcp list`.
80
+
53
81
  ### Gemini CLI, Cursor and Copilot CLI: user-level boot hooks
54
82
 
55
83
  Next to the MCP entry, `init` adds a session-start hook to the host's user-level config: `hooks.SessionStart` in `~/.gemini/settings.json`, `hooks.sessionStart` in `~/.cursor/hooks.json`, and its own `~/.copilot/hooks/contextq.json` (`$COPILOT_HOME/hooks/` when set). The hook runs `npx -y --prefer-offline @contextq/cli hook boot --format <host> --require-config`: it takes the repo from the session's working directory and stays silent in any repo without a `.contextq/config.json`, so one user-level hook serves every bound repo and nothing else. `init` writes that `.contextq/config.json` for the current repo when it is missing (commit it). The same refusals apply as for the MCP entry (invalid JSON, a git-tracked config). Cursor runs `sessionStart` without waiting for it, so the first turn may start before the pack arrives.
@@ -58,6 +86,14 @@ Next to the MCP entry, `init` adds a session-start hook to the host's user-level
58
86
 
59
87
  `init --surface codex` (or a detected Codex) adds `[mcp_servers.contextq]` with `url = "<url>/mcp"` and an `[mcp_servers.contextq.http_headers]` table carrying `Authorization = "Bearer <key>"` to the repo's `.codex/config.toml`, appends `.codex/config.toml` to `.gitignore`, and sets the file to mode 600. Existing content is kept byte for byte and an existing `contextq` entry is left alone. It refuses, and changes nothing, when the file is not valid TOML, when `mcp_servers` is an inline table the new block cannot extend, or when the file is tracked by git. Codex reads a project config only in a trusted project: run `codex` once in the repo and accept the trust prompt, then `codex mcp list` shows `contextq`. If no surface ends up with an MCP connection, `init` says so and exits 1 instead of printing `Done`.
60
88
 
89
+ ## `contextq doctor`
90
+
91
+ ```bash
92
+ contextq doctor [--json]
93
+ ```
94
+
95
+ Proves a wired host actually receives memory, step by step: the configured API answers its health route, the key is found (only its length and prefix are ever printed) and accepted by the server, the repo's bound workspace from `.contextq/config.json` exists and reads, each wired host's MCP entry points at the same endpoint, each wired host's session-start boot hook is present, and one short note tagged `doctor` saves, recalls by search and deletes again (the delete is always attempted, even when recall fails). A detected but unwired host prints one `SKIP <host>` line instead of failures. Prints one `PASS`/`FAIL` line per step and exits 0 only when every step of every wired host passes; `--json` prints the same results as JSON. In a repo bound to local mode it checks the local server and its `ctq_local_` token instead of the cloud account.
96
+
61
97
  ## `contextq skills sync <dir>`
62
98
 
63
99
  Pulls this tenant's distilled procedures — the parametrized skills ContextQ mines from repeated, independently verified agent task patterns — into `<dir>` as standard Agent Skills folders, one `SKILL.md` per procedure.
package/dist/index.js CHANGED
@@ -1,36 +1,50 @@
1
1
  #!/usr/bin/env node
2
- var ct=Object.defineProperty;var r=(e,t)=>ct(e,"name",{value:t,configurable:!0});import{homedir as st}from"os";import{basename as it}from"path";import{existsSync as D}from"fs";import{execSync as Ct}from"child_process";import{homedir as ye}from"os";import{join as W}from"path";var lt="CONTEXTQ_HOST";var dt="CLAUDE_CODE_ENABLE_FUNCTION_HOOKS",ut=["claude-code","claude-desktop","codex","opencode","cyberine","muse","unknown"],pt=[["claude-code","claude-code"],["claude-desktop","claude-desktop"],["claude-ai","claude-desktop"],["claude","claude-desktop"],["codex","codex"],["opencode","opencode"],["cyberine","cyberine"],["muse","muse"],["muse-code","muse"]];function pe(e){return e.trim().toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}r(pe,"normalizeName");function ft(e){if(!e)return"unknown";let t=pe(e);if(!t)return"unknown";let o=[...pt].sort((n,s)=>s[0].length-n[0].length);for(let[n,s]of o)if(t===n||t.startsWith(`${n}-`))return s;return"unknown"}r(ft,"hostFromClientName");function gt(e){if(!e)return;let t=pe(e);return ut.find(o=>o===t)}r(gt,"parseHostOverride");function fe(e={}){let t=e.env??(typeof process<"u"?process.env:{}),o=t.CLAUDE_CODE_SESSION_ID||void 0,n=r((i,c)=>({host:i,supportsFunctionHooks:i==="claude-code"&&t[dt]==="1",...i==="claude-code"&&o?{sessionId:o}:{},via:c}),"finish");if(e.forcedHost)return n(e.forcedHost,"forced");let s=gt(t[lt]);return s?n(s,"env-override"):e.clientName!==void 0?n(ft(e.clientName),"client-info"):t.CLAUDECODE==="1"?n("claude-code","env"):n("unknown","none")}r(fe,"resolveHost");import{chmodSync as mt,existsSync as ht,mkdirSync as yt,readFileSync as kt,writeFileSync as xt}from"fs";import{execFileSync as ge}from"child_process";import{basename as wt,dirname as me,join as L}from"path";var T=["gemini","cursor","copilot"],b={gemini:{label:"Gemini CLI",configDir:r(e=>L(e,".gemini"),"configDir"),configFile:"settings.json",entry:r((e,t)=>({httpUrl:e,headers:{Authorization:`Bearer ${t}`}}),"entry"),listCommand:"gemini mcp list"},cursor:{label:"Cursor",configDir:r(e=>L(e,".cursor"),"configDir"),configFile:"mcp.json",entry:r((e,t)=>({url:e,headers:{Authorization:`Bearer ${t}`}}),"entry"),listCommand:"cursor-agent mcp list"},copilot:{label:"Copilot CLI",configDir:r((e,t)=>t.COPILOT_HOME||L(e,".copilot"),"configDir"),configFile:"mcp-config.json",entry:r((e,t)=>({type:"http",url:e,headers:{Authorization:`Bearer ${t}`},tools:["*"]}),"entry"),listCommand:"copilot mcp list"}};function H(e,t,o){return b[e].configDir(t,o)}r(H,"hostConfigDir");function St(e,t,o){return L(H(e,t,o),b[e].configFile)}r(St,"hostMcpConfigPath");function C(e){return`${e.replace(/\/+$/,"")}/mcp`}r(C,"mcpEndpoint");function V(e){let t=me(e);try{if(ge("git",["-C",t,"rev-parse","--is-inside-work-tree"],{stdio:["ignore","pipe","ignore"],encoding:"utf-8"}).trim()!=="true")return!1}catch{return!1}try{return ge("git",["-C",t,"check-ignore","-q","--",wt(e)],{stdio:"ignore"}),!1}catch(o){return o.status===1}}r(V,"gitTracksUnignored");function he(e,t){let o=b[e],n=St(e,t.homeDir,t.env),s=ht(n),i={};if(s){let a=kt(n,"utf-8");try{let d=a.trim()===""?{}:JSON.parse(a);if(!d||typeof d!="object"||Array.isArray(d))throw new Error("top level is not an object");i=d}catch(d){return{surface:e,path:n,action:"refused",detail:`not valid JSON (${d.message}); left untouched`}}}let c=i.mcpServers??{};return typeof c!="object"||Array.isArray(c)?{surface:e,path:n,action:"refused",detail:"mcpServers is not an object; left untouched"}:c.contextq?{surface:e,path:n,action:"skipped",detail:"contextq server entry already present"}:V(n)?{surface:e,path:n,action:"refused",detail:`sits in a git work tree and is not gitignored; writing the API key there would put it in a commit. Add it to that repo's .gitignore and re-run, or add the server by hand: ${o.label} -> ${C(t.apiUrl)}`}:(i.mcpServers={...c,contextq:o.entry(C(t.apiUrl),t.apiKey)},yt(me(n),{recursive:!0}),xt(n,`${JSON.stringify(i,null,2)}
3
- `,{mode:384}),mt(n,384),{surface:e,path:n,action:s?"added":"created",detail:`contextq remote MCP (${C(t.apiUrl)}) with the API key in the header, file mode 600; check with \`${o.listCommand}\``})}r(he,"wireHostMcp");function bt(e){try{return Ct("git rev-parse --show-toplevel",{cwd:e,stdio:["ignore","pipe","ignore"],encoding:"utf-8"}).trim()}catch{return""}}r(bt,"gitRoot");function vt(e){return D(W(e,".codex"))}r(vt,"hasCodexProjectDir");function _t(e){return D(W(e,".codex"))}r(_t,"hasCodexHome");function ke(e,t={}){let o=bt(e),n=o||e,{host:s}=fe(t.hostSignals);if(t.forcedSurface)return{surfaces:[t.forcedSurface],root:n,host:s};let a=D(W(e,".claude"))||!!o&&!(s!=="unknown"&&s!=="claude-code"),d=vt(n)||_t(t.homeDir??ye()),l=[];a&&l.push("claude-code"),d&&l.push("codex");let p=t.homeDir??ye(),m=t.env??process.env;for(let f of T)D(H(f,p,m))&&l.push(f);return l.length===0&&l.push("desktop"),{surfaces:l,root:a||d?n:e,host:s}}r(ke,"detectSurface");import{chmodSync as z,copyFileSync as Et,existsSync as U,mkdirSync as $t,readFileSync as Tt,writeFileSync as Pt}from"fs";import{homedir as At}from"os";import{join as x}from"path";function Z(){let e=process.env.CTQ_CRED_DIR||process.env.CONTEXTQ_CRED_DIR;if(e)return e;let t=Rt.map(Ht);return t.find(o=>U(x(o,"credentials")))??t[0]}r(Z,"platformCredDir");var Rt=["contextq","ctq-brain"];function Ht(e){let t=At();if(process.platform==="darwin")return x(t,"Library","Application Support",e);if(process.platform==="win32"){let n=process.env.APPDATA??x(t,"AppData","Roaming");return x(n,e)}let o=process.env.XDG_CONFIG_HOME??x(t,".config");return x(o,e)}r(Ht,"platformCredDirFor");function h(){return x(Z(),"credentials")}r(h,"platformCredFile");function ee(){return U(h())}r(ee,"credsFileExists");function It(e,t){return e.apiKey===t.apiKey&&e.apiUrl===t.apiUrl}r(It,"credsEqual");function Ot(){let e=new Date,t=r(o=>String(o).padStart(2,"0"),"pad");return`${e.getUTCFullYear()}${t(e.getUTCMonth()+1)}${t(e.getUTCDate())}T${t(e.getUTCHours())}${t(e.getUTCMinutes())}${t(e.getUTCSeconds())}Z`}r(Ot,"utcTimestamp");function Nt(){let e=h();if(!U(e))return null;let t=Z(),o=x(t,`credentials.bak-${Ot()}`);return Et(e,o),z(o,384),o}r(Nt,"backupCredsFile");function j(e,t){if(!U(t))return"";try{let o=Tt(t,"utf-8");for(let n of o.split(`
4
- `))if(n.startsWith(`${e}=`))return n.slice(e.length+1).trim()}catch{}return""}r(j,"readEnvVal");function w(){let e=h(),t=j("CTQ_API_KEY",e)||j("CONTEXT_API_KEY",e),o=j("CTQ_API_URL",e)||j("CONTEXT_API_URL",e);return{apiKey:t,apiUrl:o}}r(w,"readExistingCreds");function xe(e){let t=Z(),o=h();$t(t,{recursive:!0,mode:448}),z(t,448);let n=`CTQ_API_KEY=${e.apiKey}
2
+ var Wt=Object.defineProperty;var a=(e,t)=>Wt(e,"name",{value:t,configurable:!0});var C=(e,t)=>()=>(e&&(t=e(e=0)),t);var le=(e,t)=>{for(var n in t)Wt(e,n,{get:t[n],enumerable:!0})};function Zt(e){return e.trim().toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}function Po(e){if(!e)return"unknown";let t=Zt(e);if(!t)return"unknown";let n=[...Eo].sort((o,r)=>r[0].length-o[0].length);for(let[o,r]of n)if(t===o||t.startsWith(`${o}-`))return r;return"unknown"}function _o(e){if(!e)return;let t=Zt(e);return Co.find(n=>n===t)}function en(e={}){let t=e.env??(typeof process<"u"?process.env:{}),n=t.CLAUDE_CODE_SESSION_ID||void 0,o=a((s,i)=>({host:s,supportsFunctionHooks:s==="claude-code"&&t[So]==="1",...s==="claude-code"&&n?{sessionId:n}:{},via:i}),"finish");if(e.forcedHost)return o(e.forcedHost,"forced");let r=_o(t[$o]);return r?o(r,"env-override"):e.clientName!==void 0?o(Po(e.clientName),"client-info"):t.CLAUDECODE==="1"?o("claude-code","env"):o("unknown","none")}var $o,So,Co,Eo,tn=C(()=>{"use strict";$o="CONTEXTQ_HOST",So="CLAUDE_CODE_ENABLE_FUNCTION_HOOKS",Co=["claude-code","claude-desktop","codex","opencode","cyberine","muse","unknown"],Eo=[["claude-code","claude-code"],["claude-desktop","claude-desktop"],["claude-ai","claude-desktop"],["claude","claude-desktop"],["codex","codex"],["opencode","opencode"],["cyberine","cyberine"],["muse","muse"],["muse-code","muse"]];a(Zt,"normalizeName");a(Po,"hostFromClientName");a(_o,"parseHostOverride");a(en,"resolveHost")});import{chmodSync as To,existsSync as Ao,mkdirSync as Ro,readFileSync as Oo,writeFileSync as Io}from"fs";import{execFileSync as nn}from"child_process";import{basename as Do,dirname as on,join as Ae}from"path";function de(e,t,n){return V[e].configDir(t,n)}function lt(e,t,n){return Ae(de(e,t,n),V[e].configFile)}function $(e){return`${e.replace(/\/+$/,"")}/mcp`}function ue(e){let t=on(e);try{if(nn("git",["-C",t,"rev-parse","--is-inside-work-tree"],{stdio:["ignore","pipe","ignore"],encoding:"utf-8"}).trim()!=="true")return!1}catch{return!1}try{return nn("git",["-C",t,"check-ignore","-q","--",Do(e)],{stdio:"ignore"}),!1}catch(n){return n.status===1}}function rn(e,t){let n=V[e],o=lt(e,t.homeDir,t.env),r=Ao(o),s={};if(r){let c=Oo(o,"utf-8");try{let l=c.trim()===""?{}:JSON.parse(c);if(!l||typeof l!="object"||Array.isArray(l))throw new Error("top level is not an object");s=l}catch(l){return{surface:e,path:o,action:"refused",detail:`not valid JSON (${l.message}); left untouched`}}}let i=s.mcpServers??{};return typeof i!="object"||Array.isArray(i)?{surface:e,path:o,action:"refused",detail:"mcpServers is not an object; left untouched"}:i.contextq?{surface:e,path:o,action:"skipped",detail:"contextq server entry already present"}:ue(o)?{surface:e,path:o,action:"refused",detail:`sits in a git work tree and is not gitignored; writing the API key there would put it in a commit. Add it to that repo's .gitignore and re-run, or add the server by hand: ${n.label} -> ${$(t.apiUrl)}`}:(s.mcpServers={...i,contextq:n.entry($(t.apiUrl),t.apiKey)},Ro(on(o),{recursive:!0}),Io(o,`${JSON.stringify(s,null,2)}
3
+ `,{mode:384}),To(o,384),{surface:e,path:o,action:r?"added":"created",detail:`contextq remote MCP (${$(t.apiUrl)}) with the API key in the header, file mode 600; check with \`${n.listCommand}\``})}var Z,V,Q=C(()=>{"use strict";Z=["gemini","cursor","copilot"],V={gemini:{label:"Gemini CLI",configDir:a(e=>Ae(e,".gemini"),"configDir"),configFile:"settings.json",entry:a((e,t)=>({httpUrl:e,headers:{Authorization:`Bearer ${t}`}}),"entry"),listCommand:"gemini mcp list"},cursor:{label:"Cursor",configDir:a(e=>Ae(e,".cursor"),"configDir"),configFile:"mcp.json",entry:a((e,t)=>({url:e,headers:{Authorization:`Bearer ${t}`}}),"entry"),listCommand:"cursor-agent mcp list"},copilot:{label:"Copilot CLI",configDir:a((e,t)=>t.COPILOT_HOME||Ae(e,".copilot"),"configDir"),configFile:"mcp-config.json",entry:a((e,t)=>({type:"http",url:e,headers:{Authorization:`Bearer ${t}`},tools:["*"]}),"entry"),listCommand:"copilot mcp list"}};a(de,"hostConfigDir");a(lt,"hostMcpConfigPath");a($,"mcpEndpoint");a(ue,"gitTracksUnignored");a(rn,"wireHostMcp")});import{chmodSync as No,existsSync as dt,mkdirSync as qo,readFileSync as Uo,writeFileSync as Ho}from"fs";import{execFileSync as Ko}from"child_process";import{dirname as jo,join as ut}from"path";function pt(e,t){return ut(t.XDG_CONFIG_HOME||ut(e,".config"),"opencode")}function ft(e,t){return ut(pt(e,t),"opencode.json")}function sn(e="opencode"){try{let t=Ko(e,["--version"],{stdio:["ignore","pipe","ignore"],encoding:"utf-8",timeout:15e3}),n=/v?(\d+)\.\d+/.exec(t);return n?Number(n[1]):null}catch{return null}}function K(e){return e&&typeof e=="object"&&!Array.isArray(e)?e:null}function gt(e){let t=K(e.mcp);return t?K(K(t.servers)?.contextq)??K(t.contextq):null}function an(e){let t=ft(e.homeDir,e.env),n=e.major!=null&&e.major<2?"v1":"v2",o=$(e.apiUrl),r=`add the server by hand: OpenCode -> ${o}`;if(!dt(t)&&dt(`${t}c`))return{path:`${t}c`,action:"refused",shape:n,detail:`only opencode.jsonc exists, which this installer does not rewrite; ${r}`};let s=dt(t),i={};if(s){let d=Uo(t,"utf-8");try{let u=d.trim()===""?{}:JSON.parse(d),f=K(u);if(!f)throw new Error("top level is not an object");i=f}catch(u){return{path:t,action:"refused",shape:n,detail:`not valid JSON (${u.message}); left untouched`}}}if(i.mcp!==void 0&&!K(i.mcp))return{path:t,action:"refused",shape:n,detail:"mcp is not an object; left untouched"};if(gt(i))return{path:t,action:"skipped",shape:n,detail:"contextq server entry already present"};if(ue(t))return{path:t,action:"refused",shape:n,detail:`sits in a git work tree and is not gitignored; writing the API key there would put it in a commit. Add it to that repo's .gitignore and re-run, or ${r}`};let c={Authorization:`Bearer ${e.apiKey}`},l=K(i.mcp)??{};if(n==="v2"){let d=K(l.servers)??{};i.mcp={...l,servers:{...d,contextq:{type:"remote",url:o,oauth:!1,headers:c}}}}else i.mcp={...l,contextq:{type:"remote",url:o,enabled:!0,headers:c}};return s||(i={$schema:"https://opencode.ai/config.json",...i}),qo(jo(t),{recursive:!0}),Ho(t,`${JSON.stringify(i,null,2)}
4
+ `,{mode:384}),No(t,384),{path:t,action:s?"added":"created",shape:n,detail:`contextq remote MCP (${o}) in the OpenCode ${n} shape with the API key in the header, file mode 600; check with \`opencode mcp list\``}}var Re=C(()=>{"use strict";Q();a(pt,"opencodeConfigDir");a(ft,"opencodeConfigPath");a(sn,"detectOpencodeMajor");a(K,"asObject");a(gt,"opencodeContextqEntry");a(an,"wireOpencodeMcp")});import{existsSync as pe}from"fs";import{execSync as Lo}from"child_process";import{homedir as cn}from"os";import{join as ht}from"path";function Mo(e){try{return Lo("git rev-parse --show-toplevel",{cwd:e,stdio:["ignore","pipe","ignore"],encoding:"utf-8"}).trim()}catch{return""}}function Fo(e){return pe(ht(e,".codex"))}function Jo(e){return pe(ht(e,".codex"))}function fe(e,t={}){let n=Mo(e),o=n||e,{host:r}=en(t.hostSignals);if(t.forcedSurface)return{surfaces:[t.forcedSurface],root:o,host:r};let c=pe(ht(e,".claude"))||!!n&&!(r!=="unknown"&&r!=="claude-code"),l=Fo(o)||Jo(t.homeDir??cn()),d=[];c&&d.push("claude-code"),l&&d.push("codex");let u=t.homeDir??cn(),f=t.env??process.env;for(let p of Z)pe(de(p,u,f))&&d.push(p);return pe(pt(u,f))&&d.push("opencode"),d.length===0&&d.push("desktop"),{surfaces:d,root:c||l?o:e,host:r}}var mt=C(()=>{"use strict";tn();Q();Re();a(Mo,"gitRoot");a(Fo,"hasCodexProjectDir");a(Jo,"hasCodexHome");a(fe,"detectSurface")});import{chmodSync as yt,copyFileSync as Bo,existsSync as Ie,mkdirSync as Vo,readFileSync as Qo,writeFileSync as Xo}from"fs";import{homedir as Yo}from"os";import{join as j}from"path";function kt(){let e=process.env.CTQ_CRED_DIR||process.env.CONTEXTQ_CRED_DIR;if(e)return e;let t=Go.map(zo);return t.find(n=>Ie(j(n,"credentials")))??t[0]}function zo(e){let t=Yo();if(process.platform==="darwin")return j(t,"Library","Application Support",e);if(process.platform==="win32"){let o=process.env.APPDATA??j(t,"AppData","Roaming");return j(o,e)}let n=process.env.XDG_CONFIG_HOME??j(t,".config");return j(n,e)}function A(){return j(kt(),"credentials")}function xt(){return Ie(A())}function Wo(e,t){return e.apiKey===t.apiKey&&e.apiUrl===t.apiUrl}function Zo(){let e=new Date,t=a(n=>String(n).padStart(2,"0"),"pad");return`${e.getUTCFullYear()}${t(e.getUTCMonth()+1)}${t(e.getUTCDate())}T${t(e.getUTCHours())}${t(e.getUTCMinutes())}${t(e.getUTCSeconds())}Z`}function er(){let e=A();if(!Ie(e))return null;let t=kt(),n=j(t,`credentials.bak-${Zo()}`);return Bo(e,n),yt(n,384),n}function Oe(e,t){if(!Ie(t))return"";try{let n=Qo(t,"utf-8");for(let o of n.split(`
5
+ `))if(o.startsWith(`${e}=`))return o.slice(e.length+1).trim()}catch{}return""}function D(){let e=A(),t=Oe("CTQ_API_KEY",e)||Oe("CONTEXT_API_KEY",e),n=Oe("CTQ_API_URL",e)||Oe("CONTEXT_API_URL",e);return{apiKey:t,apiUrl:n}}function ln(e){let t=kt(),n=A();Vo(t,{recursive:!0,mode:448}),yt(t,448);let o=`CTQ_API_KEY=${e.apiKey}
5
6
  CTQ_API_URL=${e.apiUrl}
6
7
  CONTEXT_API_KEY=${e.apiKey}
7
8
  CONTEXT_API_URL=${e.apiUrl}
8
- `;return Pt(o,n,{mode:384}),z(o,384),o}r(xe,"writeCreds");var v=/^sk_live_[0-9a-f]{40}$/,we="[create]",Se="[rotate]",G="[skip] ";function Ce(e){let t=w(),o=ee(),n=r(s=>{if(!o)return xe(s),{action:"created",backupPath:null};if(It(t,s))return{action:"skipped",backupPath:null};let i=Nt();return xe(s),{action:"rotated",backupPath:i}},"writeSafely");if(e.explicitKey){if(!v.test(e.explicitKey))throw new Error("the provided API key does not look valid (expected sk_live_<40 hex chars>)");let s=e.explicitKey,i=n({apiKey:s,apiUrl:e.apiUrl}),c=i.action==="skipped"?`${G} credentials (unchanged)`:i.action==="rotated"?`${Se} credentials (${h()}; previous saved to ${i.backupPath})`:`${we} credentials (${h()}, mode 600)`;return{apiKey:s,credLine:c,action:i.action,backupPath:i.backupPath}}if(o&&t.apiKey){let s=e.ambientVarName?`${G} credentials (already configured at ${h()}; env ${e.ambientVarName} ignored, pass --key to rotate)`:`${G} credentials (already configured at ${h()}; pass --key to rotate)`;return{apiKey:t.apiKey,credLine:s,action:"skipped",backupPath:null}}if(e.ambientKey){if(!v.test(e.ambientKey))throw new Error("the provided API key does not look valid (expected sk_live_<40 hex chars>)");let s=e.ambientKey,i=n({apiKey:s,apiUrl:e.apiUrl}),c=i.action==="rotated"?`${Se} credentials (${h()}; previous saved to ${i.backupPath})`:`${we} credentials (${h()}, mode 600)`;return{apiKey:s,credLine:c,action:i.action,backupPath:i.backupPath}}return{apiKey:"",credLine:"",action:null,backupPath:null}}r(Ce,"decideCredentialWrite");var be="the provided API key does not look valid (expected sk_live_<40 hex chars>)",Kt="npx @contextq/cli init --key <sk_live_...>";function ve(e){return e.status===401||e.status===403}r(ve,"isRefused");function Lt(e,t){switch(e){case"explicit":return"The --key";case"ambient":return`The ${t??"environment"}`;case"stored":return`The stored (${h()})`;case"prompt":return"The typed"}}r(Lt,"describe");async function _e(e){let t=w(),o=ee()?t.apiKey:"",n,s;if(e.explicitKey){if(!v.test(e.explicitKey))throw new Error(be);n=e.explicitKey,s="explicit"}else if(o)n=o,s="stored";else if(e.ambientKey){if(!v.test(e.ambientKey))throw new Error(be);n=e.ambientKey,s="ambient"}else{let a=e.interactive?await e.prompt("ContextQ API key (sk_live_...): "):"";if(!v.test(a))throw new Error("no valid API key available (expected sk_live_<40 hex chars>). Re-run from an interactive terminal to be prompted, set CONTEXT_API_KEY, or pass --key.");n=a,s="prompt"}let i=await e.verify(n);if(ve(i)){let a=`${Lt(s,e.ambientVarName)} API key was refused by ${e.apiUrl} (HTTP ${i.status})`;if(!e.interactive)throw new Error(`${a}; nothing was written. Re-run with a working key: ${Kt}`);let d=await e.prompt(`${a}. Paste a new key (sk_live_...): `);if(!v.test(d))throw new Error(`${a}, and no valid replacement was entered; nothing was written.`);if(n=d,s="prompt",i=await e.verify(n),ve(i))throw new Error(`The typed API key was refused by ${e.apiUrl} (HTTP ${i.status}); nothing was written.`)}return{decision:Ce({explicitKey:s==="explicit"||s==="prompt"?n:void 0,ambientVarName:e.ambientVarName,ambientKey:e.ambientKey,apiUrl:e.apiUrl}),verify:i,source:s}}r(_e,"resolveVerifiedCredentials");import{existsSync as Dt,readFileSync as jt,writeFileSync as Ut}from"fs";import{execFileSync as Ft}from"child_process";import{join as qt}from"path";function Mt(e,t){try{return Ft("git",["check-ignore","-q","--",t],{cwd:e,stdio:"ignore"}),!0}catch(o){return o.status===1?!1:null}}r(Mt,"isPathGitIgnored");function Ee(e,t,o,n){let s=qt(e,".mcp.json"),i=Dt(s),c={};if(i){let a=jt(s,"utf-8");try{c=a.trim()===""?{}:JSON.parse(a)}catch(d){throw new Error(`.mcp.json exists but is not valid JSON (${d.message}) -- refusing to touch it`)}}if(c.mcpServers??={},c.mcpServers.contextq)return{path:s,action:"skipped",detail:"contextq server entry already present"};if(o){let a=Mt(e,".mcp.json");if(a===!1)throw new Error('.mcp.json is not covered by .gitignore in this repo -- refusing to write the raw API key into a file git would track. Add ".mcp.json" to .gitignore first, or re-run without --embed-key to use the safer ${CONTEXT_API_KEY} placeholder instead.');a===null&&console.log("[warn] could not verify .mcp.json is gitignored (no git repo detected at this path) -- writing the raw key anyway.")}return c.mcpServers.contextq={command:"npx",args:["-y","@contextq/mcp"],env:{CONTEXT_API_URL:t,CONTEXT_API_KEY:o?n:"${CONTEXT_API_KEY}"}},Ut(s,`${JSON.stringify(c,null,2)}
9
- `),{path:s,action:i?"added":"created",detail:`contextq server wired (key=${o?"embedded literal":"${CONTEXT_API_KEY} placeholder"})`}}r(Ee,"wireMcpJson");import{appendFileSync as Qt,chmodSync as Yt,existsSync as F,mkdirSync as Xt,readFileSync as Pe,writeFileSync as Jt}from"fs";import{execFileSync as Ae}from"child_process";import{dirname as Bt,join as q}from"path";import{parse as $e,stringify as Vt}from"smol-toml";var M=".codex/config.toml";function Wt(e){return q(e,".codex","config.toml")}r(Wt,"codexConfigPath");function Gt(e,t){if(!F(q(e,".git")))return!1;try{return Ae("git",["-C",e,"ls-files","--error-unmatch","--",t],{stdio:"ignore"}),!0}catch{return!1}}r(Gt,"gitTracked");function zt(e,t){try{return Ae("git",["-C",e,"check-ignore","-q","--",t],{stdio:"ignore"}),!0}catch{return!1}}r(zt,"gitIgnored");function Zt(e){if(!F(q(e,".git"))||zt(e,M))return;let t=q(e,".gitignore"),o=F(t)?Pe(t,"utf-8"):"",n=o===""||o.endsWith(`
9
+ `;return Xo(n,o,{mode:384}),yt(n,384),n}function L(e){let t=D(),n=xt(),o=a(r=>{if(!n)return ln(r),{action:"created",backupPath:null};if(Wo(t,r))return{action:"skipped",backupPath:null};let s=er();return ln(r),{action:"rotated",backupPath:s}},"writeSafely");if(e.explicitKey){if(!I.test(e.explicitKey))throw new Error("the provided API key does not look valid (expected sk_live_<40 hex chars>)");let r=e.explicitKey,s=o({apiKey:r,apiUrl:e.apiUrl}),i=s.action==="skipped"?`${wt} credentials (unchanged)`:s.action==="rotated"?`${un} credentials (${A()}; previous saved to ${s.backupPath})`:`${dn} credentials (${A()}, mode 600)`;return{apiKey:r,credLine:i,action:s.action,backupPath:s.backupPath}}if(n&&t.apiKey){let r=e.ambientVarName?`${wt} credentials (already configured at ${A()}; env ${e.ambientVarName} ignored, pass --key to rotate)`:`${wt} credentials (already configured at ${A()}; pass --key to rotate)`;return{apiKey:t.apiKey,credLine:r,action:"skipped",backupPath:null}}if(e.ambientKey){if(!I.test(e.ambientKey))throw new Error("the provided API key does not look valid (expected sk_live_<40 hex chars>)");let r=e.ambientKey,s=o({apiKey:r,apiUrl:e.apiUrl}),i=s.action==="rotated"?`${un} credentials (${A()}; previous saved to ${s.backupPath})`:`${dn} credentials (${A()}, mode 600)`;return{apiKey:r,credLine:i,action:s.action,backupPath:s.backupPath}}return{apiKey:"",credLine:"",action:null,backupPath:null}}var Go,I,dn,un,wt,X=C(()=>{"use strict";a(kt,"platformCredDir");Go=["contextq","ctq-brain"];a(zo,"platformCredDirFor");a(A,"platformCredFile");a(xt,"credsFileExists");a(Wo,"credsEqual");a(Zo,"utcTimestamp");a(er,"backupCredsFile");a(Oe,"readEnvVal");a(D,"readExistingCreds");a(ln,"writeCreds");I=/^sk_live_[0-9a-f]{40}$/,dn="[create]",un="[rotate]",wt="[skip] ";a(L,"decideCredentialWrite")});import{existsSync as ar,readFileSync as cr,writeFileSync as lr}from"fs";import{execFileSync as dr}from"child_process";import{join as ur}from"path";function pr(e,t){try{return dr("git",["check-ignore","-q","--",t],{cwd:e,stdio:"ignore"}),!0}catch(n){return n.status===1?!1:null}}function bt(e){return ur(e,".mcp.json")}function yn(e,t,n,o){let r=bt(e),s=ar(r),i={};if(s){let c=cr(r,"utf-8");try{i=c.trim()===""?{}:JSON.parse(c)}catch(l){throw new Error(`.mcp.json exists but is not valid JSON (${l.message}) -- refusing to touch it`)}}if(i.mcpServers??={},i.mcpServers.contextq)return{path:r,action:"skipped",detail:"contextq server entry already present"};if(n){let c=pr(e,".mcp.json");if(c===!1)throw new Error('.mcp.json is not covered by .gitignore in this repo -- refusing to write the raw API key into a file git would track. Add ".mcp.json" to .gitignore first, or re-run without --embed-key to use the safer ${CONTEXT_API_KEY} placeholder instead.');c===null&&console.log("[warn] could not verify .mcp.json is gitignored (no git repo detected at this path) -- writing the raw key anyway.")}return i.mcpServers.contextq={command:"npx",args:["-y","@contextq/mcp"],env:{CONTEXT_API_URL:t,CONTEXT_API_KEY:n?o:"${CONTEXT_API_KEY}"}},lr(r,`${JSON.stringify(i,null,2)}
10
+ `),{path:r,action:s?"added":"created",detail:`contextq server wired (key=${n?"embedded literal":"${CONTEXT_API_KEY} placeholder"})`}}var vt=C(()=>{"use strict";a(pr,"isPathGitIgnored");a(bt,"mcpJsonPath");a(yn,"wireMcpJson")});import{appendFileSync as fr,chmodSync as gr,existsSync as Ne,mkdirSync as hr,readFileSync as bn,writeFileSync as mr}from"fs";import{execFileSync as vn}from"child_process";import{dirname as wr,join as qe}from"path";import{parse as kn,stringify as yr}from"smol-toml";function $t(e){return qe(e,".codex","config.toml")}function kr(e,t){if(!Ne(qe(e,".git")))return!1;try{return vn("git",["-C",e,"ls-files","--error-unmatch","--",t],{stdio:"ignore"}),!0}catch{return!1}}function xr(e,t){try{return vn("git",["-C",e,"check-ignore","-q","--",t],{stdio:"ignore"}),!0}catch{return!1}}function br(e){if(!Ne(qe(e,".git"))||xr(e,Ue))return;let t=qe(e,".gitignore"),n=Ne(t)?bn(t,"utf-8"):"",o=n===""||n.endsWith(`
10
11
  `)?"":`
11
- `;Qt(t,`${n}${M}
12
- `)}r(Zt,"ensureConfigGitignored");function eo(e,t){return Vt({mcp_servers:{contextq:{url:C(e),http_headers:{Authorization:`Bearer ${t}`}}}})}r(eo,"codexMcpBlock");function Te(e){let t=e.mcp_servers;if(!(!t||typeof t!="object"||Array.isArray(t)))return t.contextq}r(Te,"contextqEntry");function Re(e,t,o){let n=Wt(e),s=F(n),i=s?Pe(n,"utf-8"):"";if(s){let d;try{d=$e(i)}catch(l){return{path:n,action:"refused",detail:`not valid TOML (${l.message.split(`
13
- `)[0]}); left untouched`}}if(Te(d)!==void 0)return{path:n,action:"skipped",detail:"mcp_servers.contextq already present"}}if(Gt(e,M))return{path:n,action:"refused",detail:`is tracked by git; writing the API key there would put it in a commit. Untrack it (git rm --cached ${M}) and re-run, or add the server by hand: codex -> ${C(t)}`};let c=i===""?"":i.endsWith(`
12
+ `;fr(t,`${o}${Ue}
13
+ `)}function vr(e,t){return yr({mcp_servers:{contextq:{url:$(e),http_headers:{Authorization:`Bearer ${t}`}}}})}function xn(e){let t=e.mcp_servers;if(!(!t||typeof t!="object"||Array.isArray(t)))return t.contextq}function $n(e,t,n){let o=$t(e),r=Ne(o),s=r?bn(o,"utf-8"):"";if(r){let l;try{l=kn(s)}catch(d){return{path:o,action:"refused",detail:`not valid TOML (${d.message.split(`
14
+ `)[0]}); left untouched`}}if(xn(l)!==void 0)return{path:o,action:"skipped",detail:"mcp_servers.contextq already present"}}if(kr(e,Ue))return{path:o,action:"refused",detail:`is tracked by git; writing the API key there would put it in a commit. Untrack it (git rm --cached ${Ue}) and re-run, or add the server by hand: codex -> ${$(t)}`};let i=s===""?"":s.endsWith(`
14
15
  `)?`
15
16
  `:`
16
17
 
17
- `,a=`${i}${c}${eo(t,o)}`;try{let d=$e(a);if(Te(d)===void 0)throw new Error("mcp_servers.contextq missing after merge")}catch(d){return{path:n,action:"refused",detail:`adding [mcp_servers.contextq] would not parse (${d.message.split(`
18
- `)[0]}), e.g. mcp_servers is an inline table; left untouched`}}return Xt(Bt(n),{recursive:!0}),Zt(e),Jt(n,a,{mode:384}),Yt(n,384),{path:n,action:s?"added":"created",detail:`contextq streamable HTTP MCP (${C(t)}) with the API key in http_headers, file mode 600, gitignored; Codex reads project config only in a trusted project, so run \`codex\` once here and accept the trust prompt, then check with \`codex mcp list\``}}r(Re,"wireCodexMcp");import{chmodSync as uo,existsSync as oe,mkdirSync as Le,readFileSync as po,writeFileSync as De}from"fs";import{dirname as je,join as Q}from"path";import{appendFileSync as to,existsSync as I,mkdirSync as oo,readFileSync as te,writeFileSync as no}from"fs";import{execFileSync as ro}from"child_process";import{join as S,relative as so}from"path";var io=/contextq-boot\.sh|contextq.*boot/;function He(e){return`'${e.split("'").join("'\\''")}'`}r(He,"shellQuote");var _="npx -y --prefer-offline @contextq/cli";function Ie(e,t,o,n=_){return`${n} hook boot --format ${o} --agent ${He(e)} --workspace ${He(t)}`}r(Ie,"buildBootCommand");function ao(e,t){if(!I(S(e,".git")))return!1;try{return ro("git",["-C",e,"ls-files","--error-unmatch","--",so(e,t)],{stdio:"ignore"}),!0}catch{return!1}}r(ao,"gitTracked");function Oe(e,t,o){let{path:n,dir:s,topLevelDefaults:i}=e,c=I(n),a={};if(c){let l=te(n,"utf-8");try{a=l.trim()===""?{}:JSON.parse(l)}catch(p){return{path:n,action:"refused",detail:`not valid JSON (${p.message}); left untouched`}}if(!a||typeof a!="object"||Array.isArray(a))return{path:n,action:"refused",detail:"top level is not a JSON object; left untouched"}}if(i)for(let[l,p]of Object.entries(i))a[l]===void 0&&(a[l]=p);return a.hooks??={},a.hooks.SessionStart??=[],a.hooks.SessionStart.some(l=>(l?.hooks??[]).some(p=>typeof p.command=="string"&&io.test(p.command)))?{path:n,action:"skipped",detail:"SessionStart contextq-boot hook already present"}:e.root&&ao(e.root,n)?{path:n,action:"refused",detail:"is tracked by git; a per-machine hook would be committed for every teammate. Untrack it and re-run"}:(a.hooks.SessionStart.push({hooks:[{type:"command",command:t}]}),oo(s,{recursive:!0}),no(n,`${JSON.stringify(a,null,2)}
19
- `),{path:n,action:"wired",detail:o})}r(Oe,"mergeSessionStartHook");function Ne(e,t,o,n=_){let s=S(e,".claude","settings.local.json"),i=Ie(t,o,"claude",n);return Oe({path:s,dir:S(e,".claude"),root:e},i,`SessionStart boot hook wired (agent=${t}, workspace=${o}; runs \`${n} hook boot\`, key from the credentials file)`)}r(Ne,"wireSessionHook");function co(e){let t=S(e,".codex","config.toml");if(!I(t))return!1;let o=te(t,"utf-8");return/^\s*\[{1,2}hooks(\.|\])/m.test(o)}r(co,"hasInlineHooksTable");function lo(e,t){if(!I(S(e,".git")))return;let o=S(e,".gitignore"),n=I(o)?te(o,"utf-8"):"";if(new Set(n.split(`
20
- `).map(c=>c.trim())).has(t))return;let i=`
18
+ `,c=`${s}${i}${vr(t,n)}`;try{let l=kn(c);if(xn(l)===void 0)throw new Error("mcp_servers.contextq missing after merge")}catch(l){return{path:o,action:"refused",detail:`adding [mcp_servers.contextq] would not parse (${l.message.split(`
19
+ `)[0]}), e.g. mcp_servers is an inline table; left untouched`}}return hr(wr(o),{recursive:!0}),br(e),mr(o,c,{mode:384}),gr(o,384),{path:o,action:r?"added":"created",detail:`contextq streamable HTTP MCP (${$(t)}) with the API key in http_headers, file mode 600, gitignored; Codex reads project config only in a trusted project, so run \`codex\` once here and accept the trust prompt, then check with \`codex mcp list\``}}var Ue,St=C(()=>{"use strict";Q();Ue=".codex/config.toml";a($t,"codexConfigPath");a(kr,"gitTracked");a(xr,"gitIgnored");a(br,"ensureConfigGitignored");a(vr,"codexMcpBlock");a(xn,"contextqEntry");a($n,"wireCodexMcp")});import{appendFileSync as $r,existsSync as ge,mkdirSync as Sr,readFileSync as Ct,writeFileSync as Cr}from"fs";import{execFileSync as Er}from"child_process";import{join as M,relative as Pr}from"path";function Sn(e){return`'${e.split("'").join("'\\''")}'`}function Cn(e,t,n,o=Y){return`${o} hook boot --format ${n} --agent ${Sn(e)} --workspace ${Sn(t)}`}function Tr(e,t){if(!ge(M(e,".git")))return!1;try{return Er("git",["-C",e,"ls-files","--error-unmatch","--",Pr(e,t)],{stdio:"ignore"}),!0}catch{return!1}}function En(e,t,n){let{path:o,dir:r,topLevelDefaults:s}=e,i=ge(o),c={};if(i){let d=Ct(o,"utf-8");try{c=d.trim()===""?{}:JSON.parse(d)}catch(u){return{path:o,action:"refused",detail:`not valid JSON (${u.message}); left untouched`}}if(!c||typeof c!="object"||Array.isArray(c))return{path:o,action:"refused",detail:"top level is not a JSON object; left untouched"}}if(s)for(let[d,u]of Object.entries(s))c[d]===void 0&&(c[d]=u);return c.hooks??={},c.hooks.SessionStart??=[],c.hooks.SessionStart.some(d=>(d?.hooks??[]).some(u=>typeof u.command=="string"&&_r.test(u.command)))?{path:o,action:"skipped",detail:"SessionStart contextq-boot hook already present"}:e.root&&Tr(e.root,o)?{path:o,action:"refused",detail:"is tracked by git; a per-machine hook would be committed for every teammate. Untrack it and re-run"}:(c.hooks.SessionStart.push({hooks:[{type:"command",command:t}]}),Sr(r,{recursive:!0}),Cr(o,`${JSON.stringify(c,null,2)}
20
+ `),{path:o,action:"wired",detail:n})}function He(e){return M(e,".claude","settings.local.json")}function Ke(e){return M(e,".codex","hooks.json")}function Pn(e,t,n,o=Y){let r=He(e),s=Cn(t,n,"claude",o);return En({path:r,dir:M(e,".claude"),root:e},s,`SessionStart boot hook wired (agent=${t}, workspace=${n}; runs \`${o} hook boot\`, key from the credentials file)`)}function Ar(e){let t=M(e,".codex","config.toml");if(!ge(t))return!1;let n=Ct(t,"utf-8");return/^\s*\[{1,2}hooks(\.|\])/m.test(n)}function Rr(e,t){if(!ge(M(e,".git")))return;let n=M(e,".gitignore"),o=ge(n)?Ct(n,"utf-8"):"";if(new Set(o.split(`
21
+ `).map(i=>i.trim())).has(t))return;let s=`
21
22
  # .codex/hooks.json embeds a per-machine absolute path (the contextq boot
22
23
  # hook command) -- gitignored the same way .claude/settings.local.json is.
23
24
  # Each teammate who wants it needs to run \`npx contextq init\` themselves.
24
25
  ${t}
25
- `;to(o,i)}r(lo,"ensureGitignored");function Ke(e,t,o,n=_){let s=S(e,".codex","hooks.json"),i=Ie(t,o,"codex",n),c="Codex requires this hook to be TRUSTED before it runs -- start `codex` in this repo and approve it at the hook-trust prompt (or review/trust via `/hooks` in a session)",a=co(e)?" -- WARNING: .codex/config.toml also declares an inline [hooks] table; Codex loads both hooks.json and the inline table and warns on every run. Prefer one representation per layer.":"",d=Oe({path:s,dir:S(e,".codex"),root:e,topLevelDefaults:{description:"ContextQ boot hook (managed by `npx contextq init` -- re-run the installer to update, do not hand-edit)"}},i,`SessionStart boot hook wired (agent=${t}, workspace=${o}). ${c}.${a}`);return d.action==="wired"&&lo(e,".codex/hooks.json"),d}r(Ke,"wireCodexSessionHook");var fo=/contextq.*hook boot|contextq-boot\.sh/;function go(e,t=_){return`${t} hook boot --format ${e} --require-config`}r(go,"hostHookCommand");function mo(e,t,o){let n=H(e,t,o);return e==="gemini"?Q(n,"settings.json"):e==="cursor"?Q(n,"hooks.json"):Q(n,"hooks","contextq.json")}r(mo,"hostHookPath");function ho(e){if(!oe(e))return{doc:{}};let t=po(e,"utf-8");try{let o=t.trim()===""?{}:JSON.parse(t);return!o||typeof o!="object"||Array.isArray(o)?{error:"top level is not a JSON object"}:{doc:o}}catch(o){return{error:`not valid JSON (${o.message})`}}}r(ho,"readJsonObject");function yo(e,t){let o=e.hooks??={};if(typeof o!="object"||Array.isArray(o))return;let n=o[t]??=[];return Array.isArray(n)?n:void 0}r(yo,"eventList");function ko(e){return JSON.stringify(e).match(fo)!==null}r(ko,"alreadyWired");function Ue(e,t){let o=mo(e,t.homeDir,t.env),n=oe(o),{doc:s,error:i}=ho(o);if(!s)return{surface:e,path:o,action:"refused",detail:`${i}; left untouched`};let c=go(e,t.runner),a=e==="gemini"?"SessionStart":"sessionStart";e!=="gemini"&&(s.version??=1);let d=yo(s,a);return d?ko(d)?{surface:e,path:o,action:"skipped",detail:"contextq boot hook already present"}:V(o)?{surface:e,path:o,action:"refused",detail:`sits in a git work tree and is not gitignored; add the SessionStart hook by hand: ${c}`}:(e==="gemini"?d.push({hooks:[{type:"command",name:"contextq-boot",command:c,timeout:3e4}]}):e==="cursor"?d.push({command:c,timeout:30}):d.push({type:"command",bash:c,timeoutSec:30}),Le(je(o),{recursive:!0}),De(o,`${JSON.stringify(s,null,2)}
26
- `,{mode:384}),uo(o,384),{surface:e,path:o,action:n?"added":"created",detail:`SessionStart boot hook (runs only in repos with a .contextq/config.json); ${e==="cursor"?"Cursor runs sessionStart without waiting, so the first turn may not see it":"the boot pack is injected at session start"}`}):{surface:e,path:o,action:"refused",detail:`hooks.${a} is not an array; left untouched`}}r(Ue,"wireHostHook");function Fe(e,t,o){let n=Q(e,".contextq","config.json");return oe(n)?{path:n,action:"skipped",detail:"repo already bound -- left untouched"}:(Le(je(n),{recursive:!0}),De(n,`${JSON.stringify({config_schema_version:5,workspace:t,agent:o,read_scope:"own+global"},null,2)}
27
- `),{path:n,action:"created",detail:`binds this repo to workspace ${t} for the host hooks; commit it so other machines share the binding`})}r(Fe,"ensureRepoConfig");import{spawnSync as ne}from"child_process";import{existsSync as xo,readFileSync as wo}from"fs";import{join as So}from"path";var re="contextq@contextq-plugins",qe=[2,1,281],Co="CONTEXTQ_CLAUDE_BIN";function Me(e){return e[Co]||"claude"}r(Me,"claudeBin");function bo(e){let t=/(\d+)\.(\d+)\.(\d+)/.exec(e);return t?[Number(t[1]),Number(t[2]),Number(t[3])]:null}r(bo,"parseClaudeVersion");function vo(e,t){for(let o=0;o<3;o++)if(e[o]!==t[o])return e[o]>t[o];return!0}r(vo,"versionAtLeast");function Qe(e){let t=So(e,".mcp.json");if(!xo(t))return!1;try{return!!JSON.parse(wo(t,"utf-8")).mcpServers?.contextq}catch{return!1}}r(Qe,"hasLegacyMcpEntry");function _o(e=process.env){let t=ne(Me(e),["--version"],{encoding:"utf-8",env:e});if(t.error||t.status!==0)return{reason:"the `claude` binary was not found on PATH"};let o=bo(t.stdout);return o?vo(o,qe)?{version:o.join(".")}:{reason:`Claude Code ${o.join(".")} is older than ${qe.join(".")}, the ContextQ plugin's floor (its function-hook module needs prompt.edit and ui.render AbovePrompt)`}:{reason:`could not read a version from \`claude --version\` (${t.stdout.trim()})`}}r(_o,"detectClaude");function Eo(e){let t=e.split(`
28
- `).map(o=>o.trim()).filter(o=>o.startsWith("{"));for(let o=t.length-1;o>=0;o--)try{return JSON.parse(t[o])}catch{continue}return null}r(Eo,"lastJsonLine");function $o(e){return`${e.replace(/\/$/,"")}/plugin/marketplace.json`}r($o,"marketplaceUrl");function Ye(e,t,o,n,s=process.env){let i=_o(s);if("reason"in i)return{status:"unavailable",reason:i.reason};let c=Me(s),a=ne(c,["plugin","marketplace","add",$o(t),"--scope","project"],{cwd:e,encoding:"utf-8",env:s});if(a.error||a.status!==0)return{status:"failed",reason:`\`claude plugin marketplace add\` exited ${a.status??"with an error"}`};let d=["plugin","install",re,"--scope","project","--json","--config",`apiKey=${o}`];t.replace(/\/$/,"")!==n.replace(/\/$/,"")&&d.push("--config",`apiUrl=${t}`);let l=ne(c,d,{cwd:e,encoding:"utf-8",env:s}),p=Eo(l.stdout??"");if(!p)return{status:"failed",reason:`\`claude plugin install --json\` printed no result line (exit ${l.status??"error"})`};if(p.outcome!=="ok"||l.status!==0){let f=typeof p.failureCode=="string"?` [${p.failureCode}]`:"";return{status:"failed",reason:`${String(p.message??"install failed")}${f}`}}let m=String(p.message??"installed");return{status:"installed",message:m,alreadyInstalled:/already installed/i.test(m)}}r(Ye,"installPlugin");function Xe(e){return e.noPlugin?{delegate:!1,reason:"--no-plugin: wiring Claude Code through .mcp.json"}:e.legacyMcpEntry?{delegate:!1,reason:".mcp.json already wires contextq -- keeping that path, not installing the plugin on top of it (both would register the same MCP server)"}:e.verifyStatus===401||e.verifyStatus===403?{delegate:!1,reason:`the server rejected this key (HTTP ${e.verifyStatus}) -- not handing it to the plugin, whose apiKey is stored machine-wide and would replace a working one; wiring .mcp.json instead`}:{delegate:!0}}r(Xe,"decidePluginDelegation");import{execFileSync as Po}from"child_process";import{existsSync as Je,readFileSync as Ao}from"fs";import{basename as Be,join as Ve}from"path";var To=/^[a-z0-9][a-z0-9_-]{0,63}$/,Y='lowercase letters, digits, "-", "_" (must start with a letter/digit, max 64 chars)';function se(e,t){if(!To.test(e))throw new Error(`${t} value "${e}" is not a valid slug -- allowed: ${Y}`);return e}r(se,"validateSlug");function O(e){let t=e.toLowerCase().replace(/[^a-z0-9_-]+/g,"-").replace(/^[-_]+|[-_]+$/g,"");return t=t.slice(0,64),t=t.replace(/[-_]+$/g,""),t===""?"default":t}r(O,"sanitizeSlugFallback");var ae=["claude","codex","gemini","cursor","copilot"],Ro="https://app.contextq.dev";function N(e){let t=(e??"").trim();return!t||/^\$\{[^}]*\}$/.test(t)?"":t}r(N,"usable");function Ho(e){let t=w(),o=N(e.CONTEXT_API_KEY)||N(e.CTQ_API_KEY),n=o||t.apiKey,s=(o?N(e.CONTEXT_API_URL)||N(e.CTQ_API_URL):"")||t.apiUrl||N(e.CONTEXT_API_URL)||Ro;return{apiKey:n,apiUrl:s.replace(/\/+$/,"")}}r(Ho,"resolveHookCreds");function Io(e,t){if(!t||t.trim()==="")return e;try{let o=JSON.parse(t);if(typeof o.cwd=="string"&&o.cwd)return o.cwd;if(Array.isArray(o.workspace_roots)&&typeof o.workspace_roots[0]=="string")return o.workspace_roots[0]}catch{return e}return e}r(Io,"sessionCwd");function ie(e,t){try{return Po("git",["-C",e,...t],{stdio:["ignore","pipe","ignore"],encoding:"utf-8"}).trim()}catch{return""}}r(ie,"git");function Oo(e,t,o){let n=Ve(e,".contextq","config.json");if(Je(n))return Ao(n,"utf-8");let s=O(Be(e));return JSON.stringify({workspace:(o||s).toLowerCase(),agent:t||s,read_scope:"own+global"})}r(Oo,"hookConfig");function X(e,t){return JSON.stringify(e==="cursor"?{additional_context:t}:e==="copilot"?{additionalContext:t}:{hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:t}})}r(X,"formatForHost");async function We(e){let t=Io(e.cwd,e.stdinText),o=ie(t,["rev-parse","--show-toplevel"]);if(!o||e.requireConfig&&!Je(Ve(o,".contextq","config.json")))return"";let{apiKey:n,apiUrl:s}=Ho(e.env);if(!n)return X(e.format,"ContextQ: no API key found (CONTEXT_API_KEY or the credentials file `npx @contextq/cli init` writes), so no memory was loaded this session. Fix: run npx @contextq/cli init in this repo.");let i=new URLSearchParams({config:Oo(o,e.agent,e.workspace),agent_slug:e.agent??"",project:Be(o),git_root:o,branch:ie(o,["branch","--show-current"]),head_sha:ie(o,["rev-parse","--short","HEAD"]),goal:"",board:"",token_budget:"3000"}),c=e.fetchImpl??fetch,a=0,d="";try{let l=await c(`${s}/api/agent/hooks/session-start`,{method:"POST",headers:{Authorization:`Bearer ${n}`,Accept:"application/json","Content-Type":"application/x-www-form-urlencoded"},body:i.toString(),signal:AbortSignal.timeout(e.timeoutMs??1e4)});a=l.status,d=await l.text()}catch{a=0}if(a===401||a===403)return X(e.format,`ContextQ: ${s} refused the stored API key (HTTP ${a}): it is expired, revoked or wrong, so no memory was loaded and nothing will be saved this session. Fix: npx @contextq/cli init --key <new sk_live_ key>, or run init from a terminal to be prompted.`);if(a===0||a>=500)return X(e.format,`ContextQ: ${s} is unreachable or failing (HTTP ${a}), so no memory was loaded this session. The key was not checked; memory resumes when the server answers again.`);if(a<200||a>=300||d.trim()==="")return"";try{let p=JSON.parse(d).hookSpecificOutput?.additionalContext;return typeof p!="string"||p===""?"":X(e.format,p)}catch{return""}}r(We,"runHookBoot");async function Ge(e=500){return process.stdin.isTTY?"":new Promise(t=>{let o="",n=r(()=>{process.stdin.removeAllListeners("data"),process.stdin.removeAllListeners("end"),process.stdin.pause(),t(o)},"done"),s=setTimeout(n,e);process.stdin.setEncoding("utf8"),process.stdin.on("data",i=>{o+=i}),process.stdin.on("end",()=>{clearTimeout(s),n()})})}r(Ge,"readStdinIfPiped");import{appendFileSync as No,existsSync as ze,readFileSync as Ko}from"fs";import{execFileSync as Lo}from"child_process";import{join as ce}from"path";var Do=[".contextq/tasks/TASKS.md",".contextq/tasks/tasks/T*.md",".contextq/tasks/.render*",".contextq/tasks/status.log"];function jo(e){try{return Lo("git",["-C",e,"ls-files","--error-unmatch","--",".contextq/tasks/TASKS.md"],{stdio:"ignore"}),!0}catch{return!1}}r(jo,"manifestTracked");function Ze(e){let t=ce(e,".gitignore");if(!ze(ce(e,".git")))return{path:t,action:"skipped",detail:"not a git repo -- no board render to keep out of commits"};if(jo(e))return{path:ce(e,".contextq","tasks","TASKS.md"),action:"skipped",detail:"this repo tracks a git-sourced board -- left untouched"};let o=ze(t)?Ko(t,"utf-8"):"",n=new Set(o.split(`
29
- `).map(c=>c.trim())),s=Do.filter(c=>!n.has(c));if(s.length===0)return{path:t,action:"skipped",detail:"board render already gitignored"};let i=o===""||o.endsWith(`
26
+ `;$r(n,s)}function _n(e,t,n,o=Y){let r=Ke(e),s=Cn(t,n,"codex",o),i="Codex requires this hook to be TRUSTED before it runs -- start `codex` in this repo and approve it at the hook-trust prompt (or review/trust via `/hooks` in a session)",c=Ar(e)?" -- WARNING: .codex/config.toml also declares an inline [hooks] table; Codex loads both hooks.json and the inline table and warns on every run. Prefer one representation per layer.":"",l=En({path:r,dir:M(e,".codex"),root:e,topLevelDefaults:{description:"ContextQ boot hook (managed by `npx contextq init` -- re-run the installer to update, do not hand-edit)"}},s,`SessionStart boot hook wired (agent=${t}, workspace=${n}). ${i}.${c}`);return l.action==="wired"&&Rr(e,".codex/hooks.json"),l}var _r,Y,je=C(()=>{"use strict";_r=/contextq-boot\.sh|contextq.*boot/;a(Sn,"shellQuote");Y="npx -y --prefer-offline @contextq/cli";a(Cn,"buildBootCommand");a(Tr,"gitTracked");a(En,"mergeSessionStartHook");a(He,"claudeHookPath");a(Ke,"codexHookPath");a(Pn,"wireSessionHook");a(Ar,"hasInlineHooksTable");a(Rr,"ensureGitignored");a(_n,"wireCodexSessionHook")});import{chmodSync as Or,existsSync as Et,mkdirSync as Tn,readFileSync as Ir,writeFileSync as An}from"fs";import{dirname as Rn,join as Le}from"path";function Nr(e,t=Y){return`${t} hook boot --format ${e} --require-config`}function Me(e,t,n){let o=de(e,t,n);return e==="gemini"?Le(o,"settings.json"):e==="cursor"?Le(o,"hooks.json"):Le(o,"hooks","contextq.json")}function qr(e){if(!Et(e))return{doc:{}};let t=Ir(e,"utf-8");try{let n=t.trim()===""?{}:JSON.parse(t);return!n||typeof n!="object"||Array.isArray(n)?{error:"top level is not a JSON object"}:{doc:n}}catch(n){return{error:`not valid JSON (${n.message})`}}}function Ur(e,t){let n=e.hooks??={};if(typeof n!="object"||Array.isArray(n))return;let o=n[t]??=[];return Array.isArray(o)?o:void 0}function Hr(e){return JSON.stringify(e).match(Dr)!==null}function On(e,t){let n=Me(e,t.homeDir,t.env),o=Et(n),{doc:r,error:s}=qr(n);if(!r)return{surface:e,path:n,action:"refused",detail:`${s}; left untouched`};let i=Nr(e,t.runner),c=e==="gemini"?"SessionStart":"sessionStart";e!=="gemini"&&(r.version??=1);let l=Ur(r,c);return l?Hr(l)?{surface:e,path:n,action:"skipped",detail:"contextq boot hook already present"}:ue(n)?{surface:e,path:n,action:"refused",detail:`sits in a git work tree and is not gitignored; add the SessionStart hook by hand: ${i}`}:(e==="gemini"?l.push({hooks:[{type:"command",name:"contextq-boot",command:i,timeout:3e4}]}):e==="cursor"?l.push({command:i,timeout:30}):l.push({type:"command",bash:i,timeoutSec:30}),Tn(Rn(n),{recursive:!0}),An(n,`${JSON.stringify(r,null,2)}
27
+ `,{mode:384}),Or(n,384),{surface:e,path:n,action:o?"added":"created",detail:`SessionStart boot hook (runs only in repos with a .contextq/config.json); ${e==="cursor"?"Cursor runs sessionStart without waiting, so the first turn may not see it":"the boot pack is injected at session start"}`}):{surface:e,path:n,action:"refused",detail:`hooks.${c} is not an array; left untouched`}}function In(e,t,n){let o=Le(e,".contextq","config.json");return Et(o)?{path:o,action:"skipped",detail:"repo already bound -- left untouched"}:(Tn(Rn(o),{recursive:!0}),An(o,`${JSON.stringify({config_schema_version:5,workspace:t,agent:n,read_scope:"own+global"},null,2)}
28
+ `),{path:o,action:"created",detail:`binds this repo to workspace ${t} for the host hooks; commit it so other machines share the binding`})}var Dr,Pt=C(()=>{"use strict";Q();je();Dr=/contextq.*hook boot|contextq-boot\.sh/;a(Nr,"hostHookCommand");a(Me,"hostHookPath");a(qr,"readJsonObject");a(Ur,"eventList");a(Hr,"alreadyWired");a(On,"wireHostHook");a(In,"ensureRepoConfig")});import{spawnSync as _t}from"child_process";import{existsSync as Kr,readFileSync as jr}from"fs";import{join as Lr}from"path";function Nn(e){return e[Tt]||"claude"}function Mr(e){let t=/(\d+)\.(\d+)\.(\d+)/.exec(e);return t?[Number(t[1]),Number(t[2]),Number(t[3])]:null}function Fr(e,t){for(let n=0;n<3;n++)if(e[n]!==t[n])return e[n]>t[n];return!0}function qn(e){let t=Lr(e,".mcp.json");if(!Kr(t))return!1;try{return!!JSON.parse(jr(t,"utf-8")).mcpServers?.contextq}catch{return!1}}function At(e=process.env){let t=_t(Nn(e),["--version"],{encoding:"utf-8",env:e});if(t.error||t.status!==0)return{reason:"the `claude` binary was not found on PATH"};let n=Mr(t.stdout);return n?Fr(n,Dn)?{version:n.join(".")}:{reason:`Claude Code ${n.join(".")} is older than ${Dn.join(".")}, the ContextQ plugin's floor (its function-hook module needs prompt.edit and ui.render AbovePrompt)`}:{reason:`could not read a version from \`claude --version\` (${t.stdout.trim()})`}}function Jr(e){let t=e.split(`
29
+ `).map(n=>n.trim()).filter(n=>n.startsWith("{"));for(let n=t.length-1;n>=0;n--)try{return JSON.parse(t[n])}catch{continue}return null}function Br(e){return`${e.replace(/\/$/,"")}/plugin/marketplace.json`}function Fe(e,t,n,o,r=process.env,s={}){let i=At(r);if("reason"in i)return{status:"unavailable",reason:i.reason};let c=Nn(r),l=_t(c,["plugin","marketplace","add",Br(s.marketplaceBase??t),"--scope","project"],{cwd:e,encoding:"utf-8",env:r});if(l.error||l.status!==0)return{status:"failed",reason:`\`claude plugin marketplace add\` exited ${l.status??"with an error"}`};let d=["plugin","install",E,"--scope","project","--json","--config",`apiKey=${n}`];t.replace(/\/$/,"")!==o.replace(/\/$/,"")&&d.push("--config",`apiUrl=${t}`);let u=_t(c,d,{cwd:e,encoding:"utf-8",env:r}),f=Jr(u.stdout??"");if(!f)return{status:"failed",reason:`\`claude plugin install --json\` printed no result line (exit ${u.status??"error"})`};if(f.outcome!=="ok"||u.status!==0){let g=typeof f.failureCode=="string"?` [${f.failureCode}]`:"";return{status:"failed",reason:`${String(f.message??"install failed")}${g}`}}let p=String(f.message??"installed");return{status:"installed",message:p,alreadyInstalled:/already installed/i.test(p)}}function Un(e){return e.noPlugin?{delegate:!1,reason:"--no-plugin: wiring Claude Code through .mcp.json"}:e.legacyMcpEntry?{delegate:!1,reason:".mcp.json already wires contextq -- keeping that path, not installing the plugin on top of it (both would register the same MCP server)"}:e.verifyStatus===401||e.verifyStatus===403?{delegate:!1,reason:`the server rejected this key (HTTP ${e.verifyStatus}) -- not handing it to the plugin, whose apiKey is stored machine-wide and would replace a working one; wiring .mcp.json instead`}:{delegate:!0}}var E,Dn,Tt,he=C(()=>{"use strict";E="contextq@contextq-plugins",Dn=[2,1,281],Tt="CONTEXTQ_CLAUDE_BIN";a(Nn,"claudeBin");a(Mr,"parseClaudeVersion");a(Fr,"versionAtLeast");a(qn,"hasLegacyMcpEntry");a(At,"detectClaude");a(Jr,"lastJsonLine");a(Br,"marketplaceUrl");a(Fe,"installPlugin");a(Un,"decidePluginDelegation")});function ee(e,t){if(!Vr.test(e))throw new Error(`${t} value "${e}" is not a valid slug -- allowed: ${Je}`);return e}function F(e){let t=e.toLowerCase().replace(/[^a-z0-9_-]+/g,"-").replace(/^[-_]+|[-_]+$/g,"");return t=t.slice(0,64),t=t.replace(/[-_]+$/g,""),t===""?"default":t}var Vr,Je,Be=C(()=>{"use strict";Vr=/^[a-z0-9][a-z0-9_-]{0,63}$/,Je='lowercase letters, digits, "-", "_" (must start with a letter/digit, max 64 chars)';a(ee,"validateSlug");a(F,"sanitizeSlugFallback")});import{execFileSync as Qr}from"child_process";import{existsSync as Hn,readFileSync as Xr}from"fs";import{basename as Kn,join as jn}from"path";function me(e){let t=(e??"").trim();return!t||/^\$\{[^}]*\}$/.test(t)?"":t}function It(e){let t=D(),n=me(e.CONTEXT_API_KEY)||me(e.CTQ_API_KEY),o=n||t.apiKey,r=(n?me(e.CONTEXT_API_URL)||me(e.CTQ_API_URL):"")||t.apiUrl||me(e.CONTEXT_API_URL)||Yr;return{apiKey:o,apiUrl:r.replace(/\/+$/,"")}}function Gr(e,t){if(!t||t.trim()==="")return e;try{let n=JSON.parse(t);if(typeof n.cwd=="string"&&n.cwd)return n.cwd;if(Array.isArray(n.workspace_roots)&&typeof n.workspace_roots[0]=="string")return n.workspace_roots[0]}catch{return e}return e}function Rt(e,t){try{return Qr("git",["-C",e,...t],{stdio:["ignore","pipe","ignore"],encoding:"utf-8"}).trim()}catch{return""}}function zr(e,t,n){let o=jn(e,".contextq","config.json");if(Hn(o))return Xr(o,"utf-8");let r=F(Kn(e));return JSON.stringify({workspace:(n||r).toLowerCase(),agent:t||r,read_scope:"own+global"})}function Ve(e,t){return JSON.stringify(e==="cursor"?{additional_context:t}:e==="copilot"?{additionalContext:t}:{hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:t}})}async function Ln(e){let t=Gr(e.cwd,e.stdinText),n=Rt(t,["rev-parse","--show-toplevel"]);if(!n||e.requireConfig&&!Hn(jn(n,".contextq","config.json")))return"";let{apiKey:o,apiUrl:r}=It(e.env);if(!o)return Ve(e.format,"ContextQ: no API key found (CONTEXT_API_KEY or the credentials file `npx @contextq/cli init` writes), so no memory was loaded this session. Fix: run npx @contextq/cli init in this repo.");let s=new URLSearchParams({config:zr(n,e.agent,e.workspace),agent_slug:e.agent??"",project:Kn(n),git_root:n,branch:Rt(n,["branch","--show-current"]),head_sha:Rt(n,["rev-parse","--short","HEAD"]),goal:"",board:"",token_budget:"3000"}),i=e.fetchImpl??fetch,c=0,l="";try{let d=await i(`${r}/api/agent/hooks/session-start`,{method:"POST",headers:{Authorization:`Bearer ${o}`,Accept:"application/json","Content-Type":"application/x-www-form-urlencoded"},body:s.toString(),signal:AbortSignal.timeout(e.timeoutMs??1e4)});c=d.status,l=await d.text()}catch{c=0}if(c===401||c===403)return Ve(e.format,`ContextQ: ${r} refused the stored API key (HTTP ${c}): it is expired, revoked or wrong, so no memory was loaded and nothing will be saved this session. Fix: npx @contextq/cli init --key <new sk_live_ key>, or run init from a terminal to be prompted.`);if(c===0||c>=500)return Ve(e.format,`ContextQ: ${r} is unreachable or failing (HTTP ${c}), so no memory was loaded this session. The key was not checked; memory resumes when the server answers again.`);if(c<200||c>=300||l.trim()==="")return"";try{let u=JSON.parse(l).hookSpecificOutput?.additionalContext;return typeof u!="string"||u===""?"":Ve(e.format,u)}catch{return""}}async function Mn(e=500){return process.stdin.isTTY?"":new Promise(t=>{let n="",o=a(()=>{process.stdin.removeAllListeners("data"),process.stdin.removeAllListeners("end"),process.stdin.pause(),t(n)},"done"),r=setTimeout(o,e);process.stdin.setEncoding("utf8"),process.stdin.on("data",s=>{n+=s}),process.stdin.on("end",()=>{clearTimeout(r),o()})})}var Ot,Yr,Dt=C(()=>{"use strict";X();Be();Ot=["claude","codex","gemini","cursor","copilot"],Yr="https://app.contextq.dev";a(me,"usable");a(It,"resolveHookCreds");a(Gr,"sessionCwd");a(Rt,"git");a(zr,"hookConfig");a(Ve,"formatForHost");a(Ln,"runHookBoot");a(Mn,"readStdinIfPiped")});var ro={};le(ro,{DEFAULT_LOCAL_PORT:()=>Zn,EDGE_PACKAGE:()=>jt,EDGE_SOURCE_ENV:()=>eo,EDGE_VERSION_PIN:()=>Wn,LocalRuntimeError:()=>N,MIN_NODE_MAJOR:()=>we,STATE_DIR_ENV:()=>Ht,checkNode:()=>Ge,defaultLocalStateDir:()=>U,preflight:()=>oo,provision:()=>no,start:()=>be,status:()=>ne,stop:()=>bs});import{spawn as ds,spawnSync as Ut}from"child_process";import{chmodSync as us,closeSync as ps,existsSync as te,mkdirSync as fs,openSync as gs,readFileSync as Ye,rmSync as Xe,writeFileSync as zn}from"fs";import{homedir as hs,platform as ms}from"os";import{join as R}from"path";function U(e=process.env){if(e[Ht])return e[Ht];let t=hs();if(ms()==="darwin")return R(t,"Library","Application Support","contextq","local");let n=e.XDG_DATA_HOME&&e.XDG_DATA_HOME.trim()!==""?e.XDG_DATA_HOME:R(t,".local","share");return R(n,"contextq","local")}function xe(e){let t=e.version??Wn,n=e.stateDir??U();return{stateDir:n,runtimeDir:e.runtimeDir??R(n,"runtime"),port:e.port??Zn,version:t,source:e.packageSource??process.env[eo]??`${jt}@${t}`,nodeVersion:e.nodeVersion??process.versions.node,nodeBin:e.nodeBin??process.execPath,npmBin:e.npmBin??"npm",fetchImpl:e.fetchImpl??fetch,startTimeoutMs:e.startTimeoutMs??15e3}}function to(e){return R(e.runtimeDir,`edge-${e.version}`)}function ye(e){return R(to(e),"node_modules","@contextq","edge")}function Ge(e){let t=Number(e.split(".")[0]);if(!Number.isFinite(t)||t<we)throw new N(`ContextQ local mode needs Node ${we} or newer, and this is Node ${e}; install Node ${we}+ and run \`contextq local start\` again.`)}function Kt(e){fs(e,{recursive:!0,mode:448}),us(e,448)}function Gn(e){let t=R(ye(e),"package.json");if(!te(t))return null;try{return JSON.parse(Ye(t,"utf8")).version}catch{return null}}function no(e={}){let t=xe(e);Ge(t.nodeVersion);let n=to(t);if(Gn(t)===t.version&&te(R(ye(t),"dist","cli.js")))return{dir:ye(t),installed:!1};Kt(t.stateDir),Kt(n),te(R(n,"package.json"))||zn(R(n,"package.json"),`${JSON.stringify({name:"contextq-local-runtime",private:!0},null,2)}
30
+ `);let o=Ut(t.npmBin,["install","--no-audit","--no-fund","--omit=dev","--install-links","--prefix",n,t.source],{encoding:"utf8",timeout:3e5});if(o.error||o.status!==0){let s=`${o.stderr??""}`.trim().split(`
31
+ `).slice(-3).join(" | ");throw new N(`could not install ${t.source} into ${n}${/E404|No matching version|ETARGET/.test(s)?" (that version is not published yet)":""}: ${s||o.error?.message||`npm exited ${o.status}`}`)}let r=Gn(t);if(r!==t.version)throw new N(`installed ${jt} is ${r??"missing"}, but this CLI is pinned to ${t.version}; remove ${n} and run \`contextq local start\` again.`);return{dir:ye(t),installed:!0}}function oo(e={}){let t=xe(e);Ge(t.nodeVersion);let n=ye(t),o=Ut(t.nodeBin,["--input-type=module","-e","import Database from 'better-sqlite3'; new Database(':memory:').prepare('select 1').get();"],{cwd:n,encoding:"utf8",timeout:3e4});if(o.status!==0){let s=`${o.stderr??""}`.trim().split(`
32
+ `).find(i=>/Error|error/.test(i))??`exit ${o.status}`;throw new N(`better-sqlite3 does not load on this machine (${s.slice(0,200)}); local mode supports macOS arm64 and Linux x64 with Node ${we}+ and npm prebuilt binaries.`)}return Ut(t.nodeBin,["--input-type=module","-e","import { createRequire } from 'node:module'; import Database from 'better-sqlite3'; createRequire(import.meta.url + '/../')('sqlite-vec').load(new Database(':memory:'));"],{cwd:n,encoding:"utf8",timeout:3e4}).status!==0?{vectorSearch:!1,warning:"sqlite-vec does not load here; local search runs keyword-only (FTS5)."}:{vectorSearch:!0}}function ke(e){try{return process.kill(e,0),!0}catch(t){return t.code==="EPERM"}}function ws(e){let t=q(e).pid;if(!te(t))return null;let n=Number(Ye(t,"utf8").trim());return Number.isInteger(n)&&n>0?n:null}function ys(e){let t=q(e).endpoint;if(!te(t))return null;try{return JSON.parse(Ye(t,"utf8"))}catch{return null}}async function ks(e,t){try{let n=await e(`${t}/health`,{signal:AbortSignal.timeout(2e3)});return n.ok?await n.json():null}catch{return null}}async function ne(e={}){let t=xe(e),n={running:!1,owned:!1,url:null,pid:null,edgeVersion:null,pinnedVersion:t.version,stateDir:t.stateDir},o=ws(t.stateDir),r=ys(t.stateDir);if(o===null)return{...n,note:"not started"};if(!ke(o))return Xe(q(t.stateDir).pid,{force:!0}),{...n,note:`not running (removed a stale pidfile for pid ${o})`};if(!r||r.pid!==o)return{...n,pid:o,note:`pid ${o} is alive but has not published its endpoint`};let s=await ks(t.fetchImpl,r.url);return!s||s.mode!=="local"||s.service!=="contextq-edge"?{...n,pid:o,url:r.url,note:`pid ${o} does not answer as a ContextQ local server at ${r.url}`}:{...n,running:!0,owned:!0,url:r.url,pid:o,edgeVersion:String(s.edgeVersion??"")||null}}async function xs(e){let t=`http://127.0.0.1:${e.port}`;try{let n=await e.fetchImpl(`${t}/health`,{signal:AbortSignal.timeout(1500)});return(await n.json().catch(()=>null))?.service==="contextq-edge"?`another ContextQ local server (not started from ${e.stateDir})`:`a process that is not ContextQ (HTTP ${n.status})`}catch(n){return n.cause?.code==="ECONNREFUSED"?null:"a process that does not answer HTTP"}}async function be(e={}){let t=xe(e);Ge(t.nodeVersion);let n=await ne(e);if(n.running){if(n.edgeVersion&&n.edgeVersion!==t.version)throw new N(`the running local server is edge ${n.edgeVersion}, but this CLI is pinned to ${t.version}; run \`contextq local stop\` and start it again.`);return{...n,started:!1}}let o=await xs(t);if(o)throw new N(`port ${t.port} on 127.0.0.1 is held by ${o}; it was left alone. Stop it or pass --port <n>.`);let{dir:r}=no(e),s=oo(e);Kt(t.stateDir),Xe(q(t.stateDir).endpoint,{force:!0});let i=gs(q(t.stateDir).log,"a",384),c=ds(t.nodeBin,[R(r,"dist","cli.js"),"serve","--state-dir",t.stateDir,"--port",String(t.port)],{detached:!0,stdio:["ignore",i,i]});if(ps(i),c.pid===void 0)throw new N("could not start the local server process");zn(q(t.stateDir).pid,`${c.pid}
33
+ `,{mode:384}),c.unref();let l=Date.now()+t.startTimeoutMs;for(;Date.now()<l;){let u=await ne(e);if(u.running)return{...u,started:!0,...s.warning?{warning:s.warning}:{}};if(!ke(c.pid))break;await new Promise(f=>setTimeout(f,200))}let d=te(q(t.stateDir).log)?Ye(q(t.stateDir).log,"utf8").trim().split(`
34
+ `).slice(-3).join(" | "):"";throw ke(c.pid)&&process.kill(c.pid,"SIGTERM"),Xe(q(t.stateDir).pid,{force:!0}),new N(`the local server did not come up within ${Math.round(t.startTimeoutMs/1e3)} s${d?`: ${d}`:""}`)}async function bs(e={}){let t=xe(e),n=await ne(e);if(!n.running||!n.owned||n.pid===null)return{stopped:!1,note:n.note??"not running"};process.kill(n.pid,"SIGTERM");let o=Date.now()+1e4;for(;Date.now()<o&&ke(n.pid);)await new Promise(r=>setTimeout(r,100));return ke(n.pid)?{stopped:!1,note:`pid ${n.pid} did not exit after SIGTERM; it was left running`}:(Xe(q(t.stateDir).pid,{force:!0}),{stopped:!0,note:`stopped pid ${n.pid}`})}var jt,Wn,Zn,we,Ht,eo,N,q,ve=C(()=>{"use strict";jt="@contextq/edge",Wn="0.2.0",Zn=47321,we=22,Ht="CONTEXTQ_LOCAL_STATE_DIR",eo="CONTEXTQ_EDGE_PACKAGE",N=class extends Error{static{a(this,"LocalRuntimeError")}constructor(t){super(t),this.name="LocalRuntimeError"}};a(U,"defaultLocalStateDir");a(xe,"resolve");q=a(e=>({pid:R(e,"edge.pid"),endpoint:R(e,"endpoint.json"),log:R(e,"edge.log")}),"paths");a(to,"runtimeRoot");a(ye,"edgePackageDir");a(Ge,"checkNode");a(Kt,"ensureDir");a(Gn,"installedVersion");a(no,"provision");a(oo,"preflight");a(ke,"pidAlive");a(ws,"readPid");a(ys,"readEndpoint");a(ks,"health");a(ne,"status");a(xs,"portHolder");a(be,"start");a(bs,"stop")});var Mt={};le(Mt,{readJournal:()=>io,readPluginState:()=>J,reconfigurePlugin:()=>Lt,restorePlugin:()=>Rs,supportsTerminalConfigure:()=>so,syncPlatformCredentials:()=>Os,writeJournal:()=>G});import{spawnSync as vs}from"child_process";import{chmodSync as $s,existsSync as Ss,mkdirSync as Cs,readFileSync as Es,renameSync as Ps,writeFileSync as _s}from"fs";import{dirname as Ts}from"path";function As(e){return e[Tt]||"claude"}function z(e){return e.replace(/\/+$/,"")}function ze(e,t,n){return vs(As(e),t,{encoding:"utf-8",env:e,input:n,timeout:6e4})}function so(e=process.env){let t=ze(e,["plugin","configure","--help"]);return!t.error&&t.status===0&&/--values-stdin/.test(t.stdout??"")}function J(e=process.env,t=E){let n=ze(e,["plugin","list","--json"]),o;try{o=JSON.parse(n.stdout??"[]").find(l=>l.id===t)}catch{o=void 0}if(!o)return{installed:!1,scope:null,enabled:null,apiUrl:null,apiKeySet:!1};let r=ze(e,["plugin","configure",t,"--json"]),s=null,i=!1;try{let c=JSON.parse(r.stdout??"{}");s=c.inputs?.apiUrl?z(c.inputs.apiUrl):null,i=Array.isArray(c.configured)&&c.configured.includes("apiKey")}catch{s=null}return{installed:!0,scope:o.scope??null,enabled:o.enabled??null,apiUrl:s,apiKeySet:i}}function G(e,t){Cs(Ts(e),{recursive:!0,mode:448});let n=`${e}.tmp`;_s(n,`${JSON.stringify(t,null,2)}
35
+ `,{mode:384}),$s(n,384),Ps(n,e)}function io(e){if(!Ss(e))return null;try{return JSON.parse(Es(e,"utf-8"))}catch{return null}}function ao(e,t,n){let o=ze(e,["plugin","configure",t,"--values-stdin"],JSON.stringify({apiUrl:z(n.apiUrl),apiKey:n.apiKey}));return o.error?`could not run \`claude plugin configure\` (${o.error.message})`:o.status!==0?`\`claude plugin configure\` exited ${o.status}`:null}function Lt(e){let t=e.env??process.env,n=e.pluginId??E,o=z(e.target.apiUrl);if(!so(t))return{status:"failed",reason:"this Claude Code has no `claude plugin configure --values-stdin`; update it (`claude update`) or run `/plugin configure contextq` inside a session",state:null};let r=J(t,n);if(!r.installed)return{status:"failed",reason:`${n} is not installed in this Claude Code profile`,state:r};let s=a(()=>new Date().toISOString(),"now"),i={version:1,pluginId:n,startedAt:s(),updatedAt:s(),step:"backed_up",before:{apiUrl:r.apiUrl,scope:r.scope,enabled:r.enabled},target:{apiUrl:o},previousKeySource:e.previousKeySource??null};G(e.journalPath,i);let c=ao(t,n,e.target);if(c)return G(e.journalPath,{...i,step:"failed",updatedAt:s(),error:c}),{status:"failed",reason:c,state:J(t,n)};G(e.journalPath,{...i,step:"configured",updatedAt:s()});let l=J(t,n),d=[];if(l.apiUrl!==o&&d.push(`apiUrl is ${l.apiUrl??"unset"}, expected ${o}`),l.apiKeySet||d.push("apiKey is not set"),l.scope!==r.scope&&d.push(`scope changed from ${r.scope} to ${l.scope}`),l.enabled!==r.enabled&&d.push(`enabled changed from ${r.enabled} to ${l.enabled}`),d.length>0){let u=`the effective plugin config does not match the switch: ${d.join("; ")}`;return G(e.journalPath,{...i,step:"failed",updatedAt:s(),error:u}),{status:"failed",reason:u,state:l}}return G(e.journalPath,{...i,step:"verified",updatedAt:s()}),{status:r.apiUrl===o?"updated":"switched",state:l}}function Rs(e,t,n=process.env){let o=io(e);if(!o)return{status:"failed",reason:`no plugin switch journal at ${e}`,state:null};if(o.before.apiUrl&&z(t.apiUrl)!==o.before.apiUrl)return{status:"failed",reason:`the restore endpoint ${z(t.apiUrl)} is not the one recorded before the switch (${o.before.apiUrl})`,state:null};let r=ao(n,o.pluginId,t);if(r)return{status:"failed",reason:r,state:J(n,o.pluginId)};let s=J(n,o.pluginId);return s.apiUrl!==z(t.apiUrl)||!s.apiKeySet?{status:"failed",reason:`restore did not take effect (apiUrl is ${s.apiUrl??"unset"})`,state:s}:(G(e,{...o,step:"restored",updatedAt:new Date().toISOString()}),{status:"switched",state:s})}function Os(e){if(!I.test(e.apiKey))return{written:!1,line:"[skip] platform credentials (a local token stays in the local state dir)"};let t=L({explicitKey:e.apiKey,ambientVarName:null,ambientKey:"",apiUrl:z(e.apiUrl)});return{written:t.action!=="skipped",line:t.credLine}}var We=C(()=>{"use strict";he();X();a(As,"bin");a(z,"normalizeUrl");a(ze,"run");a(so,"supportsTerminalConfigure");a(J,"readPluginState");a(G,"writeJournal");a(io,"readJournal");a(ao,"applyConfig");a(Lt,"reconfigurePlugin");a(Rs,"restorePlugin");a(Os,"syncPlatformCredentials")});var $e={};le($e,{CLOUD_MARKETPLACE_BASE:()=>Jt,appendBinding:()=>uo,gitRoot:()=>lo,readBinding:()=>Ze,runLocalInit:()=>js,writeLocalBinding:()=>po});import{spawnSync as Is}from"child_process";import{existsSync as Ds,mkdirSync as Ns,readFileSync as co,renameSync as qs,writeFileSync as Us}from"fs";import{homedir as Hs}from"os";import{basename as Ks,join as oe,resolve as Ft}from"path";function lo(e){let t=Is("git",["-C",e,"rev-parse","--show-toplevel"],{encoding:"utf8"});if(t.status!==0)throw new Error(`${e} is not inside a git repository; run \`contextq init --local\` from the project repo`);let n=t.stdout.trim(),o=Ft(Hs());if(Ft(n)===o||Ft(n)===oe(o,".claude"))throw new Error(`${n} is your home directory or ~/.claude, not a project repo; nothing was bound`);return n}function Ze(e){let t=oe(e,".contextq","config.json");if(!Ds(t))return null;try{return JSON.parse(co(t,"utf8"))}catch{throw new Error(`${t} is not valid JSON; fix or delete it first`)}}function uo(e,t,n="contextq-cli"){let o=oe(e,".contextq"),r=oe(o,"config.json"),s=Ze(e),i=s??{},c=i._entries??[],l=i.write_seq??c.length,d=!1;for(let[f,p]of t){if(i[f]===p)continue;let g=l;l+=1,c.push({key:f,value:p,written_by:n,based_on:g,seq:l,ts:new Date().toISOString().replace(/\.\d{3}Z$/,"Z")}),i[f]=p,d=!0}if(!d)return"skipped";i.config_schema_version=Math.max(5,i.config_schema_version??5),i.write_seq=l,i._entries=c,Ns(o,{recursive:!0});let u=`${r}.tmp`;return Us(u,`${JSON.stringify(i,null,2)}
36
+ `),qs(u,r),s?"updated":"created"}function po(e,t){return uo(e,[["mode","local"],["workspace",t.workspace],["agent",t.agent]])}async function js(e){let t=e.log??(b=>console.log(b)),n=e.env??process.env,o=lo(e.cwd),r=Ks(o),s=e.workspace?ee(e.workspace.toLowerCase(),"--workspace"):F(r),i=e.agent?ee(e.agent,"--agent"):F(r),c=e.runtime?.stateDir??U(n),l=await be({...e.runtime,stateDir:c});l.warning&&t(`[warn] ${l.warning}`),t(`${l.started?"[start]":"[skip] "} local server at ${l.url} (edge ${l.edgeVersion})`);let d=co(oe(c,"token"),"utf8").trim(),u=l.url,f=At(n);if("reason"in f)t(`[info] Claude Code plugin not wired: ${f.reason}. Install Claude Code 2.1.281+ and run \`contextq init --local\` again.`);else{let b=J(n);if(b.installed){let k=Lt({target:{apiUrl:u,apiKey:d},journalPath:oe(c,"plugin-config.backup.json"),previousKeySource:b.apiUrl&&b.apiUrl.startsWith("http://127.0.0.1")?"local-token":"platform-cred",env:n});if(k.status==="failed")throw new Error(`could not point the installed ${E} at the local server: ${k.reason}`);t(`${k.status==="switched"?"[rotate]":"[skip] "} ${E} -> ${u} (${k.status})`)}else{let k=Fe(o,u,d,Jt,n,{marketplaceBase:e.marketplaceBase??Jt});if(k.status!=="installed")throw new Error(`could not install ${E}: ${k.status==="unavailable",k.reason}`);t(`[add] ${E} at project scope -> ${u}`)}}let p={authorization:`Bearer ${d}`},g=await fetch(`${u}/contexts?workspace=${encodeURIComponent(s)}&limit=1`,{headers:p}),h=await fetch(`${u}/hooks/session-start`,{method:"POST",headers:{...p,"content-type":"application/json"},body:JSON.stringify({agent:i,workspace:s})}),w=h.ok?(await h.json()).hookSpecificOutput?.additionalContext??"":"";if(!g.ok||!w.startsWith("ContextQ local mode"))throw new Error(`the local server did not answer a memory read and boot-lite (HTTP ${g.status}/${h.status}); run \`contextq local status\``);let x=(await g.json()).total??0;t(`[check] local memory answers: ${x} note(s) in ${s}, boot-lite in local mode`);let y=po(o,{workspace:s,agent:i});t(`${y==="skipped"?"[skip] ":y==="created"?"[create]":"[add] "} .contextq/config.json (mode=local workspace=${s} agent=${i})`),t(""),t("Local mode is on for this repo: memory stays on this machine, no account needed. Open a new Claude Code session here to use it."),t("Board, handoffs and sessions need an account; `contextq attach` adds one later and moves your memory with you.")}var Jt,re=C(()=>{"use strict";ve();he();We();Be();Jt="https://app.contextq.dev";a(lo,"gitRoot");a(Ze,"readBinding");a(uo,"appendBinding");a(po,"writeLocalBinding");a(js,"runLocalInit")});var mo={};le(mo,{AttachError:()=>T,abortAttach:()=>Gs,attachSwitch:()=>Ys,attachTransfer:()=>Qs,ensureLocalServer:()=>at,fingerprint:()=>Vt,isSimulatedCrash:()=>Xs,journalPath:()=>Pe,reached:()=>Se,readJournal:()=>it,writeJournal:()=>_});import{createHash as Ls,randomUUID as Ms}from"crypto";import{chmodSync as Fs,existsSync as tt,mkdirSync as Js,readFileSync as Bt,renameSync as Bs,rmSync as nt,writeFileSync as go}from"fs";import{dirname as Vs,join as se}from"path";function Se(e,t){return fo.indexOf(e.state)>=fo.indexOf(t)}function Pe(e){return se(e,"attach-journal.json")}function it(e){let t=Pe(e);if(!tt(t))return null;try{return JSON.parse(Bt(t,"utf8"))}catch{throw new T(`${t} is not readable JSON; move it aside and run \`contextq attach --restart\``)}}function _(e,t){let n=Pe(e);Js(Vs(n),{recursive:!0,mode:448});let o=`${n}.tmp`;go(o,`${JSON.stringify({...t,updatedAt:new Date().toISOString()},null,2)}
37
+ `,{mode:384}),Fs(o,384),Bs(o,n)}function Vt(e){return Ls("sha256").update(e,"utf8").digest("hex").slice(0,12)}function _e(e){return e.replace(/\/+$/,"")}async function Ce(e,t,n,o,r){let s=await e(`${t.url}${o}`,{method:n,headers:{authorization:`Bearer ${t.token}`,...r?{"content-type":"application/json"}:{}},body:r?JSON.stringify(r):void 0}),i=await s.text();if(!s.ok)throw new T(`the local server answered HTTP ${s.status} for ${n} ${o}: ${i.slice(0,200)}`);return JSON.parse(i)}async function ot(e,t,n,o,r,s){let i=await e(`${t}${r}`,{method:o,headers:{authorization:`Bearer ${n}`,...s?{"content-type":"application/json"}:{}},body:s?JSON.stringify(s):void 0}),c=await i.text();return{status:i.status,text:c}}async function at(e){return{url:(await be({...e.runtime,stateDir:e.stateDir})).url,token:Bt(se(e.stateDir,"token"),"utf8").trim()}}function ho(e,t){let n=D();if(!n.apiKey||_e(n.apiUrl)!==e||Vt(n.apiKey)!==t.keyFingerprint)throw new T(`the attach in progress belongs to another login (its key is no longer the stored credential for ${e}); run \`contextq attach --restart\` to start over`);return n.apiKey}async function Qs(e){let t=e.log??(l=>console.log(l)),n=e.fetchImpl??fetch,o=e.stateDir??U(),r=_e(e.cloudUrl),s=it(o);if(s&&e.restart){if(Se(s,"freezing"))throw new T("the attach already switched or is switching; finish it or run `contextq attach --abort` first");nt(Pe(o),{force:!0}),s=null}if(s&&s.cloudUrl!==r)throw new T(`an attach to ${s.cloudUrl} is in progress; resume it with --url ${s.cloudUrl}, or run \`contextq attach --restart\``);s&&s.state==="login_pending"&&(s=null);let i=await at({stateDir:o,runtime:e.runtime}),c;if(s)c=ho(r,s),t(`[info] resuming attach ${s.attachId} at ${s.state}`);else{s={version:1,attachId:Ms(),cloudUrl:r,tenantId:null,keyFingerprint:null,state:"login_pending",workspaceMap:{},acked:{},denied:{},leaveDenied:!!e.leaveDenied,consent:{ping:null},startedAt:new Date().toISOString(),updatedAt:new Date().toISOString()},_(o,s);try{c=await e.login();let l=await ot(n,r,c,"GET","/api/workspaces");if(l.status!==200)throw new T(`${r} did not accept the approved key (HTTP ${l.status}); nothing was changed`)}catch(l){throw nt(Pe(o),{force:!0}),l}L({explicitKey:c,ambientVarName:null,ambientKey:"",apiUrl:r}),s={...s,state:"authenticated",keyFingerprint:Vt(c)},_(o,s),t(`[check] signed in to ${r}`)}if(e.leaveDenied&&(s.leaveDenied=!0),!Se(s,"workspaces_mapped")){let{workspaces:l}=await Ce(n,i,"GET","/workspaces");for(let d of l){if(d.contexts===0||s.workspaceMap[d.slug])continue;let u="global";if(d.slug!=="global"){let f=await ot(n,r,c,"POST","/api/workspaces/ensure",{name:d.slug,slug:d.slug});if(f.status!==200&&f.status!==201)throw new T(`could not ensure workspace ${d.slug} on ${r} (HTTP ${f.status}); run \`contextq attach\` again to resume`);let p=JSON.parse(f.text);if(typeof p.tenantId=="number"){if(s.tenantId!==null&&s.tenantId!==p.tenantId)throw new T(`the account at ${r} is not the one this attach started with; run \`contextq attach --restart\``);s.tenantId=p.tenantId}u=p.slug}await Ce(n,i,"PUT","/sync/workspace-map",{local:d.slug,cloud:u}),s.workspaceMap[d.slug]=u,_(o,s),t(`[map] ${d.slug} -> ${u}`)}s.state="workspaces_mapped",_(o,s)}if(!Se(s,"transferred")||s.state==="transfer_blocked"){s.state="transferring",_(o,s);let l=Number.POSITIVE_INFINITY;for(;;){let d=await Ce(n,i,"POST","/sync/push",{parentUrl:r,apiKey:c,...e.batchMaxContexts?{batchMaxContexts:e.batchMaxContexts}:{}});for(let f of d.refs)f.outcome==="accepted"&&typeof f.cloudId=="number"?(s.acked[f.ref]=f.cloudId,delete s.denied[f.ref]):(f.outcome==="denied"||f.outcome==="too_large")&&(s.denied[f.ref]=f.reason??f.outcome);if(_(o,s),d.failures.length>0)throw new T(`the transfer stopped: ${d.failures[0].error}; run \`contextq attach\` again to resume (nothing is sent twice)`);if(d.pending-(s.leaveDenied?Object.keys(s.denied).length:0)<=0)break;if(d.pending>=l||Object.keys(s.denied).length>=d.pending){s.state="transfer_blocked",_(o,s);let f=Object.entries(s.denied).map(([p,g])=>` ${p}: ${g}`).join(`
38
+ `);throw new T(`${d.pending} note(s) were refused by ${r}:
39
+ ${f}
40
+ Edit or delete them locally and run \`contextq attach\` again, or pass --leave-denied to keep them local-only`)}l=d.pending}s.state="transferred",_(o,s),t(`[push] ${Object.keys(s.acked).length} note(s) are in ${r}`)}return s}function Ee(e){return se(e,"attach-binding-before.json")}function Xs(e){return e instanceof rt}async function st(e,t,n,o){await Ce(e,t,"PUT","/sync/write-lock",{lock:n,...o?{attachedTo:o}:{}})}async function et(e,t,n,o,r){let{restorePlugin:s,readPluginState:i}=await Promise.resolve().then(()=>(We(),Mt)),{appendBinding:c}=await Promise.resolve().then(()=>(re(),$e)),l=e.fetchImpl??fetch,d=e.env??process.env,u=[],f=se(o,"plugin-config.backup.json"),p=i(d);if(p.installed&&p.apiUrl&&_e(p.apiUrl)!==_e(n.url)&&tt(f)){let g=s(f,{apiUrl:n.url,apiKey:n.token},d);u.push(g.status==="failed"?`plugin NOT restored: ${g.reason}`:"plugin points at the local server again")}if(e.repoRoot&&tt(Ee(o))){let g=JSON.parse(Bt(Ee(o),"utf8")),h=[];g.mode&&h.push(["mode",g.mode]),g.workspace&&h.push(["workspace",g.workspace]),h.length>0&&c(e.repoRoot,h),u.push("repo binding restored")}throw await st(l,n,null),t.state="transferred",_(o,t),new T(`${r}; rolled back (${[...u,"local writes unlocked"].join(", ")}). Run \`contextq attach\` again to retry.`)}async function Ys(e){let t=e.log??(h=>console.log(h)),n=e.fetchImpl??fetch,o=e.env??process.env,r=e.stateDir??U(),s=_e(e.cloudUrl),{reconfigurePlugin:i,readPluginState:c}=await Promise.resolve().then(()=>(We(),Mt)),{appendBinding:l,readBinding:d}=await Promise.resolve().then(()=>(re(),$e)),u=it(r);if(!u||!Se(u,"transferred")||u.state==="transfer_blocked")throw new T("nothing is ready to switch: run the transfer first (`contextq attach`)");if(u.state==="attached")return u;let f=await at({stateDir:r,runtime:e.runtime}),p=ho(s,u),g=a(h=>{if(e.crashAfter===h)throw new rt(`simulated crash after ${h}`)},"crash");if(u.state==="switching"&&(t("[info] an earlier attach stopped during the switch; redoing the switch (every step is idempotent)"),u.state="freezing",_(r,u)),u.state==="transferred"||u.state==="freezing"){u.consent={ping:e.pingConsent??u.consent.ping??!1},u.state="freezing",_(r,u),await st(n,f,"attach"),g("freezing");let h=await Ce(n,f,"POST","/sync/push",{parentUrl:s,apiKey:p});for(let y of h.refs)y.outcome==="accepted"&&typeof y.cloudId=="number"&&(u.acked[y.ref]=y.cloudId);let w=u.leaveDenied?Object.keys(u.denied).length:0;if((h.failures.length>0||h.pending>w)&&await et(e,u,f,r,`the final flush left ${h.pending} note(s) pending${h.failures[0]?` (${h.failures[0].error})`:""}`),u.state="switching",_(r,u),e.repoRoot&&!tt(Ee(r))){let y=d(e.repoRoot),b={mode:y?.mode??null,workspace:y?.workspace??null};go(Ee(r),`${JSON.stringify(b)}
41
+ `,{mode:384})}if(c(o).installed){let y=i({target:{apiUrl:s,apiKey:p},journalPath:se(r,"plugin-config.backup.json"),previousKeySource:"local-token",env:o});y.status==="failed"&&await et(e,u,f,r,`switching the plugin failed: ${y.reason}`),t(`[rotate] plugin -> ${s}`)}else t("[info] no installed ContextQ plugin in this Claude Code profile; only the repo binding switches");if(g("switching"),e.repoRoot){let b=d(e.repoRoot)?.workspace??"global",k=u.workspaceMap[b]??b;l(e.repoRoot,[["mode","cloud"],["workspace",k]]);for(let m of["bound-host","local-ids.json"])nt(se(e.repoRoot,".contextq","cache",m),{force:!0});t(`[bind] .contextq/config.json -> mode=cloud workspace=${k}`)}u.state="verifying",_(r,u)}if(u.state==="verifying"){let h=e.repoRoot?d(e.repoRoot)?.workspace??"global":"global",w=await ot(n,s,p,"POST","/api/agent/boot",{agent:"contextq-attach",workspace:h}),x=Object.values(u.acked)[0],y=x===void 0?{status:200,text:""}:await ot(n,s,p,"GET",`/api/contexts/${x}`),b=w.status===200&&/"mode"\s*:\s*"local"/.test(w.text);(w.status!==200||b||y.status!==200)&&await et(e,u,f,r,`the cloud did not verify (boot HTTP ${w.status}${b?", still local":""}, read HTTP ${y.status})`),await st(n,f,"attached",s),u.state="attached",_(r,u),nt(Ee(r),{force:!0}),t(`[check] ${s} answers boot and reads an imported note`)}return u}async function Gs(e){let t=e.stateDir??U(),n=it(t);if(!n)return"no attach in progress";if(n.state==="attached")return"the attach already finished; nothing to abort";let o=await at({stateDir:t,runtime:e.runtime});if(["freezing","switching","verifying"].includes(n.state))try{await et(e,n,o,t,"aborted")}catch(r){return r.message}return await st(e.fetchImpl??fetch,o,null),`attach left at ${n.state}; local writes are unlocked`}var T,fo,rt,wo=C(()=>{"use strict";ve();X();T=class extends Error{static{a(this,"AttachError")}constructor(t){super(t),this.name="AttachError"}},fo=["login_pending","authenticated","workspaces_mapped","transferring","transfer_blocked","transferred","freezing","switching","verifying","attached"];a(Se,"reached");a(Pe,"journalPath");a(it,"readJournal");a(_,"writeJournal");a(Vt,"fingerprint");a(_e,"normalizeUrl");a(Ce,"localJson");a(ot,"cloudCall");a(at,"ensureLocalServer");a(ho,"storedKeyFor");a(Qs,"attachTransfer");a(Ee,"bindingFile");rt=class extends Error{static{a(this,"SimulatedCrash")}};a(Xs,"isSimulatedCrash");a(st,"setLock");a(et,"rollback");a(Ys,"attachSwitch");a(Gs,"abortAttach")});var ko={};le(ko,{formatHuman:()=>ui,runDoctor:()=>di});import{randomUUID as zs}from"crypto";import{existsSync as Ws,readFileSync as Zs}from"fs";import{homedir as ei}from"os";import{join as Xt}from"path";import{parse as ti}from"smol-toml";function H(e){try{return Zs(e,"utf-8")}catch{return null}}function ct(e){let t=e.startsWith("sk_live_")?"sk_live_":e.startsWith("ctq_local_")?"ctq_local_":null;return t?`prefix="${t}" length=${e.length}`:`unrecognized format length=${e.length}`}async function W(e,t,n,o){let r;try{r=await fetch(t,{method:e,headers:{...n?{Authorization:`Bearer ${n}`}:{},...o!==void 0?{"content-type":"application/json"}:{}},...o!==void 0?{body:JSON.stringify(o)}:{},signal:AbortSignal.timeout(ni)})}catch(s){throw new Error(`could not reach ${t} (${s.message})`)}return{status:r.status,text:await r.text()}}function ae(e){try{return e.trim()===""?null:JSON.parse(e)}catch{return null}}function yo(e,t){if(!e)return"";let n=e[t];if(typeof n=="string")return n;let o=e._entries;if(!Array.isArray(o))return"";let r=null;for(let s of o){if(s?.key!==t||typeof s.value!="string")continue;let i=typeof s.seq=="number"?s.seq:0;(!r||i>=r.seq)&&(r={seq:i,value:s.value})}return r?.value??""}function oi(e){for(let t of e){let n=H(t);if(!n)continue;let o=/hook boot[^"\n]*--workspace\s+'?([a-z0-9][a-z0-9_-]{0,63})'?/i.exec(n);if(o)return o[1]}return""}async function ri(e,t,n,o){let{root:r}=fe(e),s=null,i;try{s=Ze(r)}catch(g){i=g.message}let c=[He(r),Ke(r),...["gemini","cursor","copilot"].map(g=>Me(g,n,t))],l=(o||yo(s,"workspace")||oi(c)).toLowerCase();if(yo(s,"mode")==="local"){let g=U(t),h=await ne({stateDir:g}),w=Xt(g,"token"),x=Ws(w)?(H(w)??"").trim():"";return{mode:"local",apiUrl:h.running&&h.url?h.url:"",apiKey:x,keySource:x?"local token file":"none",workspace:l,bindingError:i,root:r}}let{apiKey:d,apiUrl:u}=It(t),f=u||ie,p=t.CONTEXT_API_KEY??t.CTQ_API_KEY?`env ${t.CONTEXT_API_KEY?"CONTEXT_API_KEY":"CTQ_API_KEY"}`:d?"credential file":"none";return{mode:"cloud",apiUrl:f,apiKey:d,keySource:p,workspace:l,bindingError:i,root:r}}function Qt(e){let t=H(e);return t===null?{present:!1,reason:`no hook file at ${e}`}:t.includes("contextq")&&t.includes("hook boot")?{present:!0,reason:`contextq boot hook present in ${e}`}:{present:!1,reason:`${e} exists but holds no contextq boot hook`}}function si(e,t,n,o){let r=lt(e,t,n),s=H(r);if(s===null)return{pass:!1,detail:`no MCP entry: ${r} has no contextq server`};let i=ae(s);if(!i||typeof i!="object"||!i.mcpServers||typeof i.mcpServers!="object")return{pass:!1,detail:`${r} is not valid JSON or has no mcpServers object`};let c=i.mcpServers.contextq;if(!c||typeof c!="object")return{pass:!1,detail:`no MCP entry: ${r} has no contextq server`};let l=c.httpUrl??c.url;return l===$(o)?{pass:!0,detail:`contextq entry in ${r} points at ${$(o)}`}:{pass:!1,detail:`contextq entry in ${r} points at ${typeof l=="string"?l:"an unknown URL"}, not ${$(o)}`}}function ii(e,t,n){let o=ft(e,t),r=H(o),s=r===null?null:ae(r),i=s&&typeof s=="object"?gt(s):null;return i?i.url===$(n)?{pass:!0,missing:!1,detail:`contextq entry in ${o} points at ${$(n)}`}:{pass:!1,missing:!1,detail:`contextq entry in ${o} points at ${typeof i.url=="string"?i.url:"an unknown URL"}, not ${$(n)}`}:{pass:!1,missing:!0,detail:`no contextq server in ${o}`}}function ai(e,t){let n=H(Xt(e,".claude","settings.json"));if(n!==null&&n.includes(E))return{pass:!0,detail:`${E} enabled at project scope (endpoint lives in Claude Code's store)`,viaPlugin:!0};let o=bt(e),r=H(o);if(r===null)return{pass:!1,detail:`no MCP entry: ${o} has no contextq server`,viaPlugin:!1};let c=ae(r)?.mcpServers?.contextq;if(!c||typeof c!="object")return{pass:!1,detail:`no MCP entry: ${o} has no contextq server`,viaPlugin:!1};let l=c.env,d=c.url??l?.CONTEXT_API_URL;if(typeof d=="string"&&(d===t||/^\$\{[^}]*\}$/.test(d))){let u=d===t?`points at ${t}`:"uses a ${VAR} placeholder (key and URL resolve from the environment or credential file)";return{pass:!0,detail:`contextq entry in ${o} ${u}`,viaPlugin:!1}}return{pass:!1,detail:`contextq entry in ${o} points at ${typeof d=="string"?d:"an unknown URL"}, not ${t}`,viaPlugin:!1}}function ci(e,t){let n=$t(e),o=H(n);if(o===null)return{pass:!1,detail:`no MCP entry: ${n} has no mcp_servers.contextq`};let r;try{r=ti(o)}catch(c){return{pass:!1,detail:`${n} is not valid TOML (${c.message.split(`
42
+ `)[0]}); left untouched`}}let i=r.mcp_servers?.contextq;return!i||typeof i!="object"?{pass:!1,detail:`no MCP entry: ${n} has no mcp_servers.contextq`}:i.url===$(t)?{pass:!0,detail:`mcp_servers.contextq in ${n} points at ${$(t)}`}:{pass:!1,detail:`mcp_servers.contextq in ${n} points at ${typeof i.url=="string"?i.url:"an unknown URL"}, not ${$(t)}`}}function li(e){let t=H(Xt(e,".claude","settings.json"));if(t!==null&&t.includes(E))return{pass:!0,detail:`${E} carries the lifecycle hooks`,viaPlugin:!0};let n=Qt(He(e));return{pass:n.present,detail:n.reason,viaPlugin:!1}}async function di(e){let t=e.cwd??process.cwd(),n=e.homeDir??ei(),o=e.env??process.env,{surfaces:r}=fe(t),s=r.filter(p=>p!=="desktop"),i=await ri(t,o,n,e.workspace),c=[],l=a(p=>c.push(p),"push");if(!i.apiUrl)l({host:null,step:"endpoint",status:"fail",detail:"the repo is bound to local mode but the local server is not running",fix:"contextq local start"});else try{let p=await W("GET",`${i.apiUrl.replace(/\/+$/,"")}/health`,"");p.status===200?l({host:null,step:"endpoint",status:"pass",detail:`${i.apiUrl} answers its health route (HTTP 200)`}):l({host:null,step:"endpoint",status:"fail",detail:`${i.apiUrl}/health answered HTTP ${p.status}`,fix:i.mode==="local"?"contextq local start":`check that the server is up and --url points at it (now: ${i.apiUrl})`})}catch(p){l({host:null,step:"endpoint",status:"fail",detail:p.message,fix:i.mode==="local"?"contextq local start":`check that the server is up and --url points at it (now: ${i.apiUrl})`})}let d=!1;if(!i.apiKey)l({host:null,step:"credential",status:"fail",detail:i.mode==="local"?"no local token found in the edge state dir":"no API key found in CONTEXT_API_KEY/CTQ_API_KEY or the credential file",fix:i.mode==="local"?"contextq local start":"contextq init (or contextq login) to approve this machine"});else{let p=i.mode==="local"?`${i.apiUrl}/workspaces`:`${i.apiUrl.replace(/\/+$/,"")}/api/workspaces`;try{let g=await W("GET",p,i.apiKey);g.status===200?(d=!0,l({host:null,step:"credential",status:"pass",detail:`key ${ct(i.apiKey)} from ${i.keySource} accepted by the server`})):g.status===401||g.status===403?l({host:null,step:"credential",status:"fail",detail:`the server refused the key ${ct(i.apiKey)} (HTTP ${g.status})`,fix:"contextq init --key <new key> (or contextq login) to replace it"}):l({host:null,step:"credential",status:"fail",detail:`checking the key ${ct(i.apiKey)} failed (HTTP ${g.status})`,fix:i.mode==="local"?"contextq local start":`check that the server is up (now: ${i.apiUrl})`})}catch(g){l({host:null,step:"credential",status:"fail",detail:`could not check the key ${ct(i.apiKey)}: ${g.message}`,fix:i.mode==="local"?"contextq local start":`check that the server is up (now: ${i.apiUrl})`})}}let u=!1;if(i.bindingError)l({host:null,step:"workspace",status:"fail",detail:`.contextq/config.json: ${i.bindingError}`,fix:"fix or delete .contextq/config.json, then contextq init --workspace <slug>"});else if(!i.workspace)l({host:null,step:"workspace",status:"fail",detail:`no workspace bound: ${i.root} has no workspace in .contextq/config.json and no wired boot hook names one`,fix:"contextq init --workspace <slug>, or contextq doctor --workspace <slug>"});else if(!d)l({host:null,step:"workspace",status:"skip",detail:`blocked by the credential step; workspace "${i.workspace}" not checked`});else{let p=i.apiUrl.replace(/\/+$/,"");try{let g=i.mode==="local"?`${p}/workspaces`:`${p}/api/workspaces`,h=await W("GET",g,i.apiKey);if(h.status!==200)l({host:null,step:"workspace",status:"fail",detail:`listing workspaces failed (HTTP ${h.status})`,fix:"fix the credential step above first"});else{let w=ae(h.text),x=Array.isArray(w)?w:Array.isArray(w?.workspaces)?w.workspaces:[];if(!new Set(x.map(b=>typeof b?.slug=="string"?b.slug.toLowerCase():"")).has(i.workspace))u=!0,l({host:null,step:"workspace",status:"pass",detail:`workspace "${i.workspace}" does not exist yet; the first save creates it (check the slug if you expected existing memory there)`});else{let b=i.mode==="local"?`${p}/contexts?workspace=${encodeURIComponent(i.workspace)}&limit=1`:`${p}/api/contexts?workspace=${encodeURIComponent(i.workspace)}&limit=1`,k=await W("GET",b,i.apiKey);k.status===200?(u=!0,l({host:null,step:"workspace",status:"pass",detail:`workspace "${i.workspace}" exists and the key can read it`})):l({host:null,step:"workspace",status:"fail",detail:`workspace "${i.workspace}" exists but the key cannot read it (HTTP ${k.status})`,fix:"check the key has read scope for this workspace, or run contextq init --key <new key>"})}}}catch(g){l({host:null,step:"workspace",status:"fail",detail:g.message,fix:i.mode==="local"?"contextq local start":`check that the server is up (now: ${i.apiUrl})`})}}for(let p of s){if(p==="opencode"){let w=ii(n,o,i.apiUrl||ie);if(w.missing){l({host:p,step:"",status:"skip",detail:"not wired",fix:"contextq init --surface opencode"});continue}l(w.pass?{host:p,step:"mcp config",status:"pass",detail:w.detail}:{host:p,step:"mcp config",status:"fail",detail:w.detail,fix:"contextq init --surface opencode"}),l({host:p,step:"boot hook",status:"skip",detail:"contextq init wires no OpenCode boot hook; memory reaches OpenCode through the MCP tools"});continue}let g,h;if(p==="claude-code")g=ai(i.root,i.apiUrl||ie),h=li(i.root);else if(p==="codex"){g=ci(i.root,i.apiUrl||ie);let w=Qt(Ke(i.root));h={pass:w.present,detail:w.reason}}else{g=si(p,n,o,i.apiUrl||ie);let w=Qt(Me(p,n,o));h={pass:w.present,detail:w.reason}}if(!g.pass&&!h.pass){l({host:p,step:"",status:"skip",detail:"not wired",fix:`contextq init --surface ${p}`});continue}g.pass?l({host:p,step:"mcp config",status:"pass",detail:g.detail}):l({host:p,step:"mcp config",status:"fail",detail:g.detail,fix:`contextq init --surface ${p}`}),h.pass?l({host:p,step:"boot hook",status:"pass",detail:h.detail}):l({host:p,step:"boot hook",status:"fail",detail:h.detail,fix:`contextq init --surface ${p}`})}if(!d||!u)l({host:null,step:"round trip",status:"skip",detail:`blocked by ${d?"the workspace step":"the credential step"}; no probe note was saved`});else{let p=i.apiUrl.replace(/\/+$/,""),g=`doctor-probe-${zs().slice(0,8)}`,h=null,w=null;try{let k=i.mode==="local"?{workspace:i.workspace,type:"reference",name:`doctor probe ${g}`,content:`doctor round-trip probe ${g} -- safe to delete`,tags:["doctor"]}:{name:`doctor probe ${g}`,description:`contextq doctor round-trip probe ${g}`,content:`doctor round-trip probe ${g} -- safe to delete`,type:"reference",workspace:i.workspace,tags:["doctor"]},m=await W("POST",i.mode==="local"?`${p}/contexts`:`${p}/api/contexts`,i.apiKey,k);if(m.status===200||m.status===201){let v=ae(m.text);typeof v?.id=="number"||typeof v?.id=="string"?h=v.id:typeof v?.ref=="string"?h=v.ref:w="the server answered 200 but returned no id or ref"}else w=`save failed (HTTP ${m.status}): ${m.text.slice(0,160)}`}catch(k){w=`save failed: ${k.message}`}let x=!1;if(h!==null)try{let k=await W("POST",i.mode==="local"?`${p}/search`:`${p}/api/search`,i.apiKey,{query:g,workspace:i.workspace,limit:5});if(k.status===200){let m=ae(k.text);x=(Array.isArray(m?.results)?m.results:[]).some(S=>JSON.stringify(S).includes(g))}}catch{x=!1}let y=null,b=i.mode!=="local";if(h!==null&&b)try{let k=i.mode==="local"?`${p}/contexts/${encodeURIComponent(String(h))}`:`${p}/api/contexts/${encodeURIComponent(String(h))}`,m=await W("DELETE",k,i.apiKey);m.status!==200&&m.status!==204&&(y=`delete failed (HTTP ${m.status}); note ${String(h)} left in workspace "${i.workspace}"`)}catch(k){y=`delete failed: ${k.message}; note ${String(h)} left in workspace "${i.workspace}"`}if(w!==null)l({host:null,step:"round trip",status:"fail",detail:w,fix:"fix the credential and workspace steps above first"});else if(x)l(y!==null?{host:null,step:"round trip",status:"fail",detail:`saved and recalled, but ${y}`,fix:i.mode==="local"?"the local server has no note-delete API; remove the probe note after it gains one":"delete the probe note by hand, then re-run contextq doctor"}:b?{host:null,step:"round trip",status:"pass",detail:`saved one note tagged doctor into "${i.workspace}", recalled it by search, then deleted it`}:{host:null,step:"round trip",status:"pass",detail:`saved one note tagged doctor into "${i.workspace}" and recalled it by search; the local server has no delete route, so note ${String(h)} stays`});else{let k=y?` ${y}`:" (the probe note was deleted afterwards)";l({host:null,step:"round trip",status:"fail",detail:`recall failed: the saved probe note was not returned by search for "${g}".${k}`,fix:"check search is healthy (ES optional, PG tsvector fallback), then re-run contextq doctor"})}}let f=c.every(p=>p.status!=="fail");return{mode:i.mode,apiUrl:i.apiUrl||ie,workspace:i.workspace,lines:c,ok:f}}function ui(e){return e.lines.map(t=>{if(t.status==="skip")return t.host?t.fix?`SKIP ${t.host}: ${t.detail} (run ${t.fix})`:`SKIP ${t.host} ${t.step}: ${t.detail}`:`SKIP ${t.step}: ${t.detail}`;let n=t.host?`${t.host} `:"";return t.status==="pass"?`PASS ${n}${t.step} (${t.detail})`:`FAIL ${n}${t.step}: ${t.detail}. Fix: ${t.fix??"see above"}`})}var ie,ni,xo=C(()=>{"use strict";mt();Dt();vt();St();Q();Pt();re();ve();je();he();Re();ie="https://app.contextq.dev",ni=8e3;a(H,"readText");a(ct,"describeKey");a(W,"http");a(ae,"parseJson");a(yo,"bindingValue");a(oi,"workspaceFromHooks");a(ri,"resolveTarget");a(Qt,"checkHookFile");a(si,"checkMcpEntry");a(ii,"checkOpencodeMcp");a(ai,"checkClaudeMcp");a(ci,"checkCodexMcp");a(li,"checkClaudeHook");a(di,"runDoctor");a(ui,"formatHuman")});mt();X();import{homedir as Yt}from"os";import{basename as bo}from"path";X();var pn="the provided API key does not look valid (expected sk_live_<40 hex chars>)",tr="npx @contextq/cli init --key <sk_live_...>";function fn(e){return e.status===401||e.status===403}a(fn,"isRefused");function gn(e,t){switch(e){case"explicit":return"The --key";case"ambient":return`The ${t??"environment"}`;case"stored":return`The stored (${A()})`;case"prompt":return"The typed";case"device":return"The browser-approved"}}a(gn,"describe");async function hn(e){let t=D(),n=xt()?t.apiKey:"",o,r;if(e.explicitKey){if(!I.test(e.explicitKey))throw new Error(pn);o=e.explicitKey,r="explicit"}else if(n)o=n,r="stored";else if(e.ambientKey){if(!I.test(e.ambientKey))throw new Error(pn);o=e.ambientKey,r="ambient"}else if(e.interactive&&e.deviceLogin)o=await e.deviceLogin(),r="device";else{let c=e.interactive?await e.prompt("ContextQ API key (sk_live_...): "):"";if(!I.test(c))throw new Error("no valid API key available (expected sk_live_<40 hex chars>). Run `contextq login` from an interactive terminal to approve this machine in the browser, set CONTEXT_API_KEY, or pass --key.");o=c,r="prompt"}let s=await e.verify(o);if(fn(s)){let c=`${gn(r,e.ambientVarName)} API key was refused by ${e.apiUrl} (HTTP ${s.status})`;if(!e.interactive)throw new Error(`${c}; nothing was written. Re-run with a working key: ${tr}`);if(e.deviceLogin)console.log(`${c}. Approve this machine in the browser to replace it.`),o=await e.deviceLogin(),r="device";else{let l=await e.prompt(`${c}. Paste a new key (sk_live_...): `);if(!I.test(l))throw new Error(`${c}, and no valid replacement was entered; nothing was written.`);o=l,r="prompt"}if(s=await e.verify(o),fn(s))throw new Error(`${gn(r,null)} API key was refused by ${e.apiUrl} (HTTP ${s.status}); nothing was written.`)}if(r==="device"&&!s.ok){let c=s.status?`HTTP ${s.status}`:s.error??"unknown error";throw new Error(`could not verify the browser-approved key against ${e.apiUrl} (${c}); nothing was written. Run \`contextq login\` again once the server answers.`)}return{decision:L({explicitKey:r==="explicit"||r==="prompt"||r==="device"?o:void 0,ambientVarName:e.ambientVarName,ambientKey:e.ambientKey,apiUrl:e.apiUrl}),verify:s,source:r}}a(hn,"resolveVerifiedCredentials");X();import{hostname as nr}from"os";var P=class extends Error{static{a(this,"DeviceLoginError")}code;constructor(t,n){super(n),this.name="DeviceLoginError",this.code=t}},or=5,rr=5,sr=600;function ir(e,t){return new Promise((n,o)=>{if(t?.aborted)return o(new P("cancelled","login cancelled; nothing was written"));let r=setTimeout(()=>{t?.removeEventListener("abort",s),n()},e),s=a(()=>{clearTimeout(r),o(new P("cancelled","login cancelled; nothing was written"))},"onAbort");t?.addEventListener("abort",s,{once:!0})})}a(ir,"defaultSleep");async function mn(e){try{return await e.json()}catch{return null}}a(mn,"readJson");function wn(e,t){return typeof e=="number"&&Number.isInteger(e)&&e>0?e:t}a(wn,"positiveInt");async function De(e){let t=e.fetchImpl??fetch,n=e.sleep??ir,o=e.now??Date.now,r=e.apiUrl.replace(/\/+$/,""),s=a(()=>new P("cancelled","login cancelled; nothing was written"),"cancelled"),i;try{i=await t(`${r}/api/auth/device/start`,{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({client_name:e.clientName??"@contextq/cli",hostname:e.hostname??nr()}),signal:e.signal})}catch(h){throw e.signal?.aborted?s():new P("unreachable",`could not reach ${r}/api/auth/device/start (${h.message})`)}let c=i.ok?await mn(i):null,l=typeof c?.device_code=="string"?c.device_code:"",d=typeof c?.user_code=="string"?c.user_code:"";if(!l||!d)throw new P("protocol",`${r} did not start a device login (HTTP ${i.status}); is this a ContextQ server?`);let u=typeof c?.verification_uri_complete=="string"&&c.verification_uri_complete||typeof c?.verification_uri=="string"&&c.verification_uri||`${r}/link`,f=wn(c?.expires_in,sr),p=wn(c?.interval,rr);e.onPrompt({verificationUri:u,userCode:d,expiresInSeconds:f});let g=o()+f*1e3;for(;;){if(o()>=g)throw new P("expired_token","the approval window expired before this machine was approved; nothing was written");if(await n(p*1e3,e.signal),e.signal?.aborted)throw s();let h;try{h=await t(`${r}/api/auth/device/token`,{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({device_code:l}),signal:e.signal})}catch{if(e.signal?.aborted)throw s();continue}let w=await mn(h);if(h.status===200){let y=w?.api_key;if(typeof y=="string"&&I.test(y))return y;throw new P("protocol","the token endpoint answered 200 without a well-formed API key; nothing was written")}let x=typeof w?.error=="string"?w.error:"";switch(x){case"authorization_pending":continue;case"slow_down":p+=or;continue;case"access_denied":throw new P("access_denied","the request was denied in the browser; nothing was written");case"expired_token":throw new P("expired_token","the device code expired; run the command again");case"invalid_grant":throw new P("invalid_grant","the server rejected the device code; run the command again");default:if(h.status>=500||h.status===429)continue;throw new P("protocol",`unexpected answer from the token endpoint (HTTP ${h.status}${x?`, ${x}`:""})`)}}}a(De,"deviceLogin");vt();Q();Re();St();Pt();he();je();Dt();import{appendFileSync as Wr,existsSync as Fn,readFileSync as Zr}from"fs";import{execFileSync as es}from"child_process";import{join as Nt}from"path";var ts=[".contextq/tasks/TASKS.md",".contextq/tasks/tasks/T*.md",".contextq/tasks/.render*",".contextq/tasks/status.log"];function ns(e){try{return es("git",["-C",e,"ls-files","--error-unmatch","--",".contextq/tasks/TASKS.md"],{stdio:"ignore"}),!0}catch{return!1}}a(ns,"manifestTracked");function Jn(e){let t=Nt(e,".gitignore");if(!Fn(Nt(e,".git")))return{path:t,action:"skipped",detail:"not a git repo -- no board render to keep out of commits"};if(ns(e))return{path:Nt(e,".contextq","tasks","TASKS.md"),action:"skipped",detail:"this repo tracks a git-sourced board -- left untouched"};let n=Fn(t)?Zr(t,"utf-8"):"",o=new Set(n.split(`
43
+ `).map(i=>i.trim())),r=ts.filter(i=>!o.has(i));if(r.length===0)return{path:t,action:"skipped",detail:"board render already gitignored"};let s=n===""||n.endsWith(`
30
44
  `)?"":`
31
- `;return No(t,`${i}${s.join(`
45
+ `;return Wr(t,`${s}${r.join(`
32
46
  `)}
33
- `),{path:t,action:"created",detail:"gitignored the task board render (.contextq/tasks/TASKS.md is a read-only copy of the cloud board, never committed)"}}r(Ze,"initTasksDir");function et(e){console.log(`
47
+ `),{path:t,action:"created",detail:"gitignored the task board render (.contextq/tasks/TASKS.md is a read-only copy of the cloud board, never committed)"}}a(Jn,"initTasksDir");function Bn(e){console.log(`
34
48
  No git repo or existing Claude Code project found here, so there is nothing on
35
49
  disk to scaffold. For Claude Desktop, Cowork, or any other MCP-capable client
36
50
  that reads a manually-edited config file, connect ContextQ in 3 steps:
@@ -59,27 +73,34 @@ There is no auto-connect for Desktop/Cowork today -- these clients only read
59
73
  their config file on start, so step 2 has to be a manual paste. Re-run
60
74
  "npx @contextq/cli init" inside a git repo (or an existing Claude Code project) to
61
75
  get the automatic .mcp.json + hook + task-manifest scaffolding instead.
62
- `)}r(et,"printDesktopGuide");async function tt(e){return process.stdin.isTTY?new Promise(t=>{let o=process.stdin,n=process.stdout;n.write(e);let s="",i=r(c=>{let a=c.toString("utf8");switch(a){case`
63
- `:case"\r":case"":o.setRawMode?.(!1),o.pause(),o.removeListener("data",i),n.write(`
64
- `),t(s.trim());break;case"":n.write(`
65
- `),process.exit(130);break;case"\x7F":case"\b":s.length>0&&(s=s.slice(0,-1),n.write("\b \b"));break;default:s+=a,n.write("*")}},"onData");o.setRawMode?.(!0),o.resume(),o.setEncoding("utf8"),o.on("data",i)}):""}r(tt,"promptHidden");import{existsSync as le,mkdirSync as ot,readdirSync as Uo,readFileSync as nt,statSync as Fo,writeFileSync as qo}from"fs";import{join as J}from"path";function Mo(e,t){let o=new RegExp(`^ {2}${t}: "((?:[^"\\\\]|\\\\.)*)"$`,"m"),n=e.match(o);return n?n[1].replace(/\\(.)/g,"$1"):null}r(Mo,"extractFrontmatterMetadataValue");function Qo(e){return Mo(e,"cluster_key")}r(Qo,"extractClusterKey");function Yo(e){if(!le(e))return[];let t=[];for(let o of Uo(e)){let n=J(e,o);if(!Fo(n).isDirectory())continue;let s=J(n,"SKILL.md");if(!le(s))continue;let i=nt(s,"utf-8"),c=Qo(i);c&&t.push({folder:o,clusterKey:c})}return t}r(Yo,"scanLocalSkills");function Xo(e,t){return e.replace(/^name: .+$/m,`name: ${t}`)}r(Xo,"withFrontmatterName");async function rt(e){ot(e.dir,{recursive:!0});let t=Yo(e.dir),o=new Map(t.map(a=>[a.clusterKey,a.folder])),n=new Set(t.map(a=>a.folder)),s=await e.fetchSkills(),i={written:[],updated:[],unchanged:[],orphans:[]},c=new Set;for(let a of s){c.add(a.clusterKey);let l=o.get(a.clusterKey);l||(l=n.has(a.slug)?`${a.slug}-c${a.contextId}`:a.slug,n.add(l),o.set(a.clusterKey,l));let p=l===a.slug?a.markdown:Xo(a.markdown,l),m=J(e.dir,l),f=J(m,"SKILL.md"),E=le(f)?nt(f,"utf-8"):null;if(E===p){i.unchanged.push(l);continue}ot(m,{recursive:!0}),qo(f,p,"utf-8"),i[E===null?"written":"updated"].push(l)}for(let a of t)c.has(a.clusterKey)||i.orphans.push(a.folder);return i}r(rt,"syncSkills");var A="https://app.contextq.dev";function Jo(e){let t=e.length>0&&!e[0].startsWith("-"),o={command:t?e[0]:"",embedKey:!1,noPlugin:!1,help:!1};for(let n=t?1:0;n<e.length;n++){let s=e[n];switch(s){case"--key":o.key=e[++n];break;case"--url":o.url=e[++n];break;case"--workspace":o.workspace=e[++n];break;case"--agent":o.agent=e[++n];break;case"--embed-key":o.embedKey=!0;break;case"--no-plugin":o.noPlugin=!0;break;case"--surface":{let i=e[++n];if(i!=="claude-code"&&i!=="codex"&&!T.includes(i))throw new Error(`--surface value "${i}" is invalid -- allowed: claude-code, codex, ${T.join(", ")}`);o.surface=i;break}case"-h":case"--help":o.help=!0;break;default:throw new Error(`unknown argument: ${s}`)}}return o}r(Jo,"parseArgs");function K(){console.log(`contextq \u2014 one-command installer for ContextQ
76
+ `)}a(Bn,"printDesktopGuide");async function Vn(e){return process.stdin.isTTY?new Promise(t=>{let n=process.stdin,o=process.stdout;o.write(e);let r="",s=a(i=>{let c=i.toString("utf8");switch(c){case`
77
+ `:case"\r":case"":n.setRawMode?.(!1),n.pause(),n.removeListener("data",s),o.write(`
78
+ `),t(r.trim());break;case"":o.write(`
79
+ `),process.exit(130);break;case"\x7F":case"\b":r.length>0&&(r=r.slice(0,-1),o.write("\b \b"));break;default:r+=c,o.write("*")}},"onData");n.setRawMode?.(!0),n.resume(),n.setEncoding("utf8"),n.on("data",s)}):""}a(Vn,"promptHidden");Be();import{existsSync as qt,mkdirSync as Qn,readdirSync as os,readFileSync as Xn,statSync as rs,writeFileSync as ss}from"fs";import{join as Qe}from"path";function is(e,t){let n=new RegExp(`^ {2}${t}: "((?:[^"\\\\]|\\\\.)*)"$`,"m"),o=e.match(n);return o?o[1].replace(/\\(.)/g,"$1"):null}a(is,"extractFrontmatterMetadataValue");function as(e){return is(e,"cluster_key")}a(as,"extractClusterKey");function cs(e){if(!qt(e))return[];let t=[];for(let n of os(e)){let o=Qe(e,n);if(!rs(o).isDirectory())continue;let r=Qe(o,"SKILL.md");if(!qt(r))continue;let s=Xn(r,"utf-8"),i=as(s);i&&t.push({folder:n,clusterKey:i})}return t}a(cs,"scanLocalSkills");function ls(e,t){return e.replace(/^name: .+$/m,`name: ${t}`)}a(ls,"withFrontmatterName");async function Yn(e){Qn(e.dir,{recursive:!0});let t=cs(e.dir),n=new Map(t.map(c=>[c.clusterKey,c.folder])),o=new Set(t.map(c=>c.folder)),r=await e.fetchSkills(),s={written:[],updated:[],unchanged:[],orphans:[]},i=new Set;for(let c of r){i.add(c.clusterKey);let d=n.get(c.clusterKey);d||(d=o.has(c.slug)?`${c.slug}-c${c.contextId}`:c.slug,o.add(d),n.set(c.clusterKey,d));let u=d===c.slug?c.markdown:ls(c.markdown,d),f=Qe(e.dir,d),p=Qe(f,"SKILL.md"),g=qt(p)?Xn(p,"utf-8"):null;if(g===u){s.unchanged.push(d);continue}Qn(f,{recursive:!0}),ss(p,u,"utf-8"),s[g===null?"written":"updated"].push(d)}for(let c of t)i.has(c.clusterKey)||s.orphans.push(c.folder);return s}a(Yn,"syncSkills");var B="https://app.contextq.dev";function pi(e){let t=e.length>0&&!e[0].startsWith("-"),n={command:t?e[0]:"",embedKey:!1,noPlugin:!1,help:!1,local:!1};for(let o=t?1:0;o<e.length;o++){let r=e[o];switch(r){case"--key":n.key=e[++o];break;case"--url":n.url=e[++o];break;case"--workspace":n.workspace=e[++o];break;case"--agent":n.agent=e[++o];break;case"--embed-key":n.embedKey=!0;break;case"--no-plugin":n.noPlugin=!0;break;case"--local":n.local=!0;break;case"--port":{let s=e[++o]??"",i=/^\d+$/.test(s)?Number(s):NaN;if(!Number.isInteger(i)||i<1||i>65535)throw new Error(`invalid port "${s}"; use a number from 1 to 65535`);n.port=i;break}case"--surface":{let s=e[++o];if(s!=="claude-code"&&s!=="codex"&&s!=="opencode"&&!Z.includes(s))throw new Error(`--surface value "${s}" is invalid -- allowed: claude-code, codex, ${Z.join(", ")}, opencode`);n.surface=s;break}case"-h":case"--help":n.help=!0;break;default:throw new Error(`unknown argument: ${r}`)}}return n}a(pi,"parseArgs");function Te(){console.log(`contextq \u2014 one-command installer for ContextQ
66
80
 
67
81
  Usage:
82
+ npx @contextq/cli login [--url <url>]
83
+ npx @contextq/cli local start|status|stop [--port <n>] [--state-dir <dir>]
68
84
  npx @contextq/cli init [options]
85
+ npx @contextq/cli init --local [--workspace <slug>] [--agent <slug>] [--port <n>]
86
+ npx @contextq/cli attach [--url <url>] [--restart] [--leave-denied] [--abort] [--key-stdin]
87
+ npx @contextq/cli doctor [--json] [--workspace <slug>]
69
88
 
70
- API key: with none stored, init prompts for it (hidden) or reads CONTEXT_API_KEY /
71
- CTQ_API_KEY from the environment. The key is checked against the server before it
72
- is saved; a refused key is never written.
89
+ API key: \`login\` prints a link and a code; approve this machine in the browser and the
90
+ key is fetched, checked against the server and saved (mode 600), never typed or pasted.
91
+ With no key stored, init runs the same browser approval from an interactive terminal, or
92
+ reads CONTEXT_API_KEY / CTQ_API_KEY from the environment. A refused or unverified key is
93
+ never written.
73
94
 
74
95
  Options:
75
96
  --key <sk_live_...> Pass or rotate the key on the command line (visible in shell
76
97
  history; prefer the prompt or the env var)
77
- --url <url> ContextQ API URL (default: ${A})
98
+ --url <url> ContextQ API URL (default: ${B})
78
99
  --workspace <slug> Workspace slug for the contextq boot hook (default: repo basename,
79
- sanitized). Allowed: ${Y}. Case is folded to
100
+ sanitized). Allowed: ${Je}. Case is folded to
80
101
  lowercase automatically (workspace slugs are always lowercase).
81
102
  --agent <slug> Agent slug for the contextq boot hook (default: repo basename,
82
- sanitized). Allowed: ${Y}.
103
+ sanitized). Allowed: ${Je}.
83
104
  --embed-key Write the literal API key into .mcp.json instead of the
84
105
  safer \${CONTEXT_API_KEY} placeholder (opt-in: convenience
85
106
  vs. a secret sitting on disk)
@@ -88,11 +109,13 @@ Options:
88
109
  plugin at project scope instead, which carries the MCP server and
89
110
  the lifecycle hooks and keeps the key in Claude Code's own store.
90
111
  --surface <s> Force surface detection to exactly one of: claude-code, codex,
91
- gemini, cursor, copilot. Skips autodetection entirely (default:
112
+ gemini, cursor, copilot, opencode. Skips autodetection entirely (default:
92
113
  autodetect all -- a machine can be, and often is, several at once).
93
114
  gemini, cursor and copilot get the remote MCP server in their
94
115
  user-level config (~/.gemini/settings.json, ~/.cursor/mcp.json,
95
116
  ~/.copilot/mcp-config.json) with the key in the header, mode 600.
117
+ opencode gets it in ~/.config/opencode/opencode.json, in the
118
+ OpenCode 2 shape (mcp.servers) or the 1.x shape for an OpenCode 1 install.
96
119
  -h, --help Show this help
97
120
 
98
121
  Usage:
@@ -110,6 +133,13 @@ Options:
110
133
  --key <sk_live_...> API key (else CONTEXT_API_KEY/CTQ_API_KEY env, else the
111
134
  credential file written by \`contextq init\`)
112
135
  --url <url> ContextQ API URL (else the credential file, else
113
- CONTEXT_API_URL/CTQ_API_URL env, default: ${A})
114
- `)}r(K,"printHelp");function P(e,t){return t.startsWith(`${e}/`)?t.slice(e.length+1):t}r(P,"relative");function k(e){return{created:"[create]",added:"[add] ",wired:"[create]",rotated:"[rotate]",skipped:"[skip] ",verified:"[check]",failed:"[check]",refused:"[refuse]"}[e]??`[${e}]`}r(k,"label");async function Bo(e,t){try{let o=await fetch(`${e.replace(/\/$/,"")}/api/workspaces`,{headers:{Authorization:`Bearer ${t}`}});return{ok:o.status===200,status:o.status}}catch(o){return{ok:!1,error:o.message}}}r(Bo,"verifyConnection");async function Vo(e){let t=process.cwd(),{surfaces:o,root:n}=ke(t,{forcedSurface:e.surface});if(console.log("ContextQ init"),console.log("============="),o.length===1&&o[0]==="desktop"){console.log(`Surface detected: desktop/other (no git repo, .claude/, or .codex/ found at ${t})`),et(e.url??A);return}console.log(`Surface(s) detected: ${o.join(", ")} (root: ${n})`);let s=w(),i=e.url||s.apiUrl||process.env.CONTEXT_API_URL||A,c=process.env.CONTEXT_API_KEY?"CONTEXT_API_KEY":process.env.CTQ_API_KEY?"CTQ_API_KEY":null,a=c?process.env[c]:"",{decision:d,verify:l}=await _e({explicitKey:e.key,ambientVarName:c,ambientKey:a,apiUrl:i,interactive:!!process.stdin.isTTY,verify:r(u=>Bo(i,u),"verify"),prompt:tt}),p=d.apiKey;if(console.log(d.credLine),!l.ok){let u=l.status?`HTTP ${l.status}`:l.error??"unknown error";console.log(`[warn] could not verify the key against ${i} (${u}); it was kept, re-run init once the server answers`)}let m;e.agent?m=se(e.agent,"--agent"):m=O(it(n));let f;if(e.workspace){let u=e.workspace.toLowerCase();f=se(u,"--workspace"),u!==e.workspace&&console.log(`[info] --workspace lowercased to match slug convention: "${e.workspace}" -> "${u}"`)}else f=O(it(n));console.log(`[info] agent=${m} workspace=${f}`);let E=process.env.CONTEXTQ_HOOK_RUNNER||_,$,B=!1,R=[],de=!1;if(o.includes("claude-code")){let u=Xe({noPlugin:e.noPlugin,legacyMcpEntry:Qe(n),verifyStatus:l.status});if(!u.delegate)console.log(`[info] ${u.reason}`);else{let g=Ye(n,i,p,A);g.status==="installed"?(B=!0,R.push("claude-code"),console.log(`${k(g.alreadyInstalled?"skipped":"added")} .claude/settings.json (${re} at project scope -- ${g.message})`)):console.log(`[info] plugin install ${g.status}: ${g.reason} -- falling back to .mcp.json wiring`)}if(!B){$=Ee(n,i,e.embedKey,p),console.log(`${k($.action)} ${P(n,$.path)} (${$.detail})`),R.push("claude-code");let g=Ne(n,m,f,E);console.log(`${k(g.action)} ${P(n,g.path)} (${g.detail})`),g.action==="refused"&&(process.exitCode=1)}let y=Ze(n);console.log(`${k(y.action)} ${P(n,y.path)} (${y.detail})`)}if(o.includes("codex")){console.log(""),console.log("[info] Codex hooks -- assumptions this install makes:"),console.log(" - Sandbox/approval: the boot hook makes an outbound HTTP call to the ContextQ"),console.log(" API. Verified empirically (codex-cli 0.146.0, `codex exec --sandbox"),console.log(" workspace-write`, no approval flag): a TRUSTED hook's outbound call is NOT"),console.log(" gated by sandbox_mode/approval_policy the way model-issued shell commands"),console.log(" are -- the real gate is hook TRUST (see the note below), not the sandbox."),console.log(" Once trusted, this should work under any sandbox_mode/approval_policy combo."),console.log(" If it still produces no injected context, `codex doctor` shows current sandbox"),console.log(" + approval state as a starting point for diagnosis."),console.log(" - Native Memories: Codex ships its own Memories feature (off by default). If you"),console.log(" turn it on, set memories.disable_on_external_context = true in config.toml (or"),console.log(" its older alias no_memories_if_mcp_or_web_search) -- otherwise a session that"),console.log(" used MCP tools (including ContextQ's own MCP server) still feeds Codex's native"),console.log(" memory extraction too, duplicating what ContextQ already captures."),console.log("");let u=Ke(n,m,f,E);if(console.log(`${k(u.action)} ${P(n,u.path)} (${u.detail})`),u.action==="refused"&&(process.exitCode=1),!l.ok)console.log("[info] Codex MCP: not wired -- the key did not verify, and it would be written into .codex/config.toml");else{let y=Re(n,i,p);console.log(`${k(y.action)} ${P(n,y.path)} (${y.detail})`),y.action==="refused"?process.exitCode=1:R.push("codex")}}for(let u of T){if(!o.includes(u))continue;if(!l.ok){console.log(`[info] ${b[u].label}: not wired -- the key did not verify, and it would be written into the host config`);continue}let y=he(u,{homeDir:st(),env:process.env,apiUrl:i,apiKey:p});console.log(`${k(y.action)} ${y.path} (${b[u].label}: ${y.detail})`),y.action==="refused"?process.exitCode=1:R.push(u);let g=Ue(u,{homeDir:st(),env:process.env,runner:E});console.log(`${k(g.action)} ${g.path} (${b[u].label}: ${g.detail})`),g.action==="refused"?process.exitCode=1:de=!0}if(l.ok)console.log(`${k("verified")} connection to ${i} (GET /api/workspaces -> 200)`);else{let u=l.status?`HTTP ${l.status}`:l.error??"unknown error";console.log(`${k("failed")} connection to ${i} (${u}) -- check your API key and --url`),(l.status===401||l.status===403)&&(process.exitCode=1)}if($&&!e.embedKey&&$.action!=="skipped"&&(console.log(""),console.log("NOTE: .mcp.json references ${CONTEXT_API_KEY}. When that variable is unset, the"),console.log(`current @contextq/mcp reads the key from ${h()} instead.`),console.log('If `npx @contextq/mcp` still logs "Auth: NONE", export it before starting Claude Code:'),console.log(" export CONTEXT_API_KEY=<your key>"),console.log("...or re-run with --embed-key to write the literal key into .mcp.json instead.")),de){let u=Fe(n,f,m);console.log(`${k(u.action)} ${P(n,u.path)} (${u.detail})`)}console.log("");let at={"claude-code":"a new Claude Code session",codex:"a new `codex` run",gemini:"a new `gemini` session",cursor:"a new Cursor or `cursor-agent` session",copilot:"a new `copilot` session"},ue=o.filter(u=>u!=="desktop"&&R.includes(u)).map(u=>at[u]).join(" / start ");ue?console.log(`Done. Open ${ue} in this repo to pick up the wiring.`):(console.log("Nothing was wired: no surface got a ContextQ MCP connection. Fix what is reported above and re-run."),process.exitCode=1),B?(console.log(""),console.log("Next: in that Claude Code session, run /contextq:init to bind this repo to a workspace"),console.log("and project. Until it writes .contextq/config.json, the plugin's MCP tools work but its"),console.log("SessionStart/PreCompact/SessionEnd hooks stay silent and say so.")):o.includes("claude-code")&&(console.log(""),console.log("Claude Code 2.1.281+ can carry all of this as a plugin instead -- MCP server and"),console.log("lifecycle hooks, key in Claude Code's own store. Re-run init without --no-plugin after"),console.log("removing the contextq entry from .mcp.json, rather than installing the plugin on top:"),console.log("both would register the same MCP server. See https://app.contextq.dev/install.md."))}r(Vo,"runInit");function Wo(e){let t=[],o={};for(let n=0;n<e.length;n++){let s=e[n];switch(s){case"--workspace":o.workspace=e[++n];break;case"--key":o.key=e[++n];break;case"--url":o.url=e[++n];break;default:if(s.startsWith("-"))throw new Error(`unknown argument: ${s}`);t.push(s)}}if(t.length===0)throw new Error("missing <dir> argument -- usage: contextq skills sync <dir>");if(t.length>1)throw new Error(`unexpected extra argument: ${t[1]}`);return{...o,dir:t[0]}}r(Wo,"parseSkillsSyncArgs");function Go(e){let t=w(),o=e.url||t.apiUrl||process.env.CONTEXT_API_URL||process.env.CTQ_API_URL||A,n=e.key||process.env.CONTEXT_API_KEY||process.env.CTQ_API_KEY||t.apiKey;if(!n)throw new Error("no API key available -- pass --key, set CONTEXT_API_KEY, or run `contextq init` first to write the credential file");return{apiUrl:o,apiKey:n}}r(Go,"resolveApiCreds");async function zo(e,t,o){let n=new URL("/api/agent/skills/export",e.replace(/\/$/,"")+"/");n.searchParams.set("format","json"),o&&n.searchParams.set("workspace",o);let s=await fetch(n,{headers:{Authorization:`Bearer ${t}`}});if(!s.ok)throw new Error(`GET ${n.pathname}${n.search} -> HTTP ${s.status}`);return(await s.json()).skills}r(zo,"fetchSkillsFromApi");async function Zo(e){console.log("contextq skills sync"),console.log("=====================");let{apiUrl:t,apiKey:o}=Go(e);console.log(`[info] syncing to ${e.dir}${e.workspace?` (workspace: ${e.workspace})`:""}`);let n=await rt({dir:e.dir,fetchSkills:r(()=>zo(t,o,e.workspace),"fetchSkills")});for(let s of n.written)console.log(`${k("created")} ${s}/SKILL.md`);for(let s of n.updated)console.log(`${k("added")} ${s}/SKILL.md (rewritten in place)`);if(console.log(`[info] ${n.written.length} written, ${n.updated.length} updated, ${n.unchanged.length} unchanged`),n.orphans.length>0){console.log(`[warn] ${n.orphans.length} orphaned folder(s) -- left on disk, not deleted:`);for(let s of n.orphans)console.log(` ${s}/ (its context no longer resolves)`)}}r(Zo,"runSkillsSync");function en(e){let t="claude",o=!1,n,s;for(let i=0;i<e.length;i++){let c=e[i],a=e[i+1];if(c==="--format"&&a){if(!ae.includes(a))throw new Error(`--format must be one of: ${ae.join(", ")}`);t=a,i++}else if(c==="--agent"&&a)n=a,i++;else if(c==="--workspace"&&a)s=a,i++;else if(c==="--require-config")o=!0;else throw new Error(`unknown "hook boot" argument "${c}"`)}return{format:t,agent:n,workspace:s,requireConfig:o}}r(en,"parseHookBootArgs");async function tn(e){try{let t=en(e),o=await We({cwd:process.env.CLAUDE_PROJECT_DIR||process.cwd(),env:process.env,stdinText:await Ge(),...t});o&&process.stdout.write(`${o}
115
- `)}catch(t){console.error(`contextq hook boot: ${t.message}`)}}r(tn,"runHookBootCommand");async function on(){let e=process.argv.slice(2);if(e[0]==="hook"){if(e[1]!=="boot"){console.error(`contextq: unknown "hook" subcommand "${e[1]??""}" -- expected "boot"`),process.exitCode=1;return}await tn(e.slice(2));return}if(e[0]==="skills"){if(e[1]==="-h"||e[1]==="--help"){K();return}if(e[1]!=="sync"){console.error(`contextq: unknown "skills" subcommand "${e[1]??""}" -- expected "sync"`),K(),process.exitCode=1;return}try{let o=Wo(e.slice(2));await Zo(o)}catch(o){console.error(`contextq: ${o.message}`),process.exitCode=1}return}let t;try{t=Jo(e)}catch(o){console.error(`contextq: ${o.message}`),K(),process.exitCode=1;return}if(t.help||!t.command){K();return}if(t.command!=="init"){console.error(`contextq: unknown command "${t.command}"`),K(),process.exitCode=1;return}try{await Vo(t)}catch(o){console.error(`contextq: ${o.message}`),process.exitCode=1}}r(on,"main");on();
136
+ CONTEXT_API_URL/CTQ_API_URL env, default: ${B})
137
+ `)}a(Te,"printHelp");function ce(e,t){return t.startsWith(`${e}/`)?t.slice(e.length+1):t}a(ce,"relative");function O(e){return{created:"[create]",added:"[add] ",wired:"[create]",rotated:"[rotate]",skipped:"[skip] ",verified:"[check]",failed:"[check]",refused:"[refuse]"}[e]??`[${e}]`}a(O,"label");async function vo(e,t){try{let n=await fetch(`${e.replace(/\/$/,"")}/api/workspaces`,{headers:{Authorization:`Bearer ${t}`}});return{ok:n.status===200,status:n.status}}catch(n){return{ok:!1,error:n.message}}}a(vo,"verifyConnection");function Gt(e){console.log(""),console.log("Approve this machine in your browser:"),console.log(` ${e.verificationUri}`),console.log(` code: ${e.userCode}`),console.log(`Waiting for approval (expires in ${Math.max(1,Math.round(e.expiresInSeconds/60))} min, Ctrl-C to cancel)...`)}a(Gt,"printDevicePrompt");function zt(e){let t=new AbortController,n=a(()=>t.abort(),"onSigint");return process.once("SIGINT",n),e(t.signal).finally(()=>process.removeListener("SIGINT",n))}a(zt,"withSigintAbort");async function fi(e){let t=D(),n=e.url||t.apiUrl||process.env.CONTEXT_API_URL||B;console.log(`ContextQ login (${n})`);let o=await zt(i=>De({apiUrl:n,signal:i,onPrompt:Gt})),r=await vo(n,o);if(!r.ok){let i=r.status?`HTTP ${r.status}`:r.error??"unknown error";throw new Error(`could not verify the approved key against ${n} (${i}); nothing was written`)}let s=L({explicitKey:o,ambientVarName:null,ambientKey:"",apiUrl:n});console.log("Approved."),console.log(s.credLine),console.log(`[check] connected to ${n}`)}a(fi,"runLogin");async function gi(e){let t=process.cwd(),{surfaces:n,root:o}=fe(t,{forcedSurface:e.surface});if(console.log("ContextQ init"),console.log("============="),n.length===1&&n[0]==="desktop"){console.log(`Surface detected: desktop/other (no git repo, .claude/, or .codex/ found at ${t})`),Bn(e.url??B);return}console.log(`Surface(s) detected: ${n.join(", ")} (root: ${o})`);let r=D(),s=e.url||r.apiUrl||process.env.CONTEXT_API_URL||B,i=process.env.CONTEXT_API_KEY?"CONTEXT_API_KEY":process.env.CTQ_API_KEY?"CTQ_API_KEY":null,c=i?process.env[i]:"",{decision:l,verify:d}=await hn({explicitKey:e.key,ambientVarName:i,ambientKey:c,apiUrl:s,interactive:!!process.stdin.isTTY,verify:a(m=>vo(s,m),"verify"),prompt:Vn,deviceLogin:a(()=>zt(m=>De({apiUrl:s,signal:m,onPrompt:Gt})),"deviceLogin")}),u=l.apiKey;if(console.log(l.credLine),!d.ok){let m=d.status?`HTTP ${d.status}`:d.error??"unknown error";console.log(`[warn] could not verify the key against ${s} (${m}); it was kept, re-run init once the server answers`)}let f;e.agent?f=ee(e.agent,"--agent"):f=F(bo(o));let p;if(e.workspace){let m=e.workspace.toLowerCase();p=ee(m,"--workspace"),m!==e.workspace&&console.log(`[info] --workspace lowercased to match slug convention: "${e.workspace}" -> "${m}"`)}else p=F(bo(o));console.log(`[info] agent=${f} workspace=${p}`);let g=process.env.CONTEXTQ_HOOK_RUNNER||Y,h,w=!1,x=[],y=!1;if(n.includes("claude-code")){let m=Un({noPlugin:e.noPlugin,legacyMcpEntry:qn(o),verifyStatus:d.status});if(!m.delegate)console.log(`[info] ${m.reason}`);else{let S=Fe(o,s,u,B);S.status==="installed"?(w=!0,x.push("claude-code"),console.log(`${O(S.alreadyInstalled?"skipped":"added")} .claude/settings.json (${E} at project scope -- ${S.message})`)):console.log(`[info] plugin install ${S.status}: ${S.reason} -- falling back to .mcp.json wiring`)}if(!w){h=yn(o,s,e.embedKey,u),console.log(`${O(h.action)} ${ce(o,h.path)} (${h.detail})`),x.push("claude-code");let S=Pn(o,f,p,g);console.log(`${O(S.action)} ${ce(o,S.path)} (${S.detail})`),S.action==="refused"&&(process.exitCode=1)}let v=Jn(o);console.log(`${O(v.action)} ${ce(o,v.path)} (${v.detail})`)}if(n.includes("codex")){console.log(""),console.log("[info] Codex hooks -- assumptions this install makes:"),console.log(" - Sandbox/approval: the boot hook makes an outbound HTTP call to the ContextQ"),console.log(" API. Verified empirically (codex-cli 0.146.0, `codex exec --sandbox"),console.log(" workspace-write`, no approval flag): a TRUSTED hook's outbound call is NOT"),console.log(" gated by sandbox_mode/approval_policy the way model-issued shell commands"),console.log(" are -- the real gate is hook TRUST (see the note below), not the sandbox."),console.log(" Once trusted, this should work under any sandbox_mode/approval_policy combo."),console.log(" If it still produces no injected context, `codex doctor` shows current sandbox"),console.log(" + approval state as a starting point for diagnosis."),console.log(" - Native Memories: Codex ships its own Memories feature (off by default). If you"),console.log(" turn it on, set memories.disable_on_external_context = true in config.toml (or"),console.log(" its older alias no_memories_if_mcp_or_web_search) -- otherwise a session that"),console.log(" used MCP tools (including ContextQ's own MCP server) still feeds Codex's native"),console.log(" memory extraction too, duplicating what ContextQ already captures."),console.log("");let m=_n(o,f,p,g);if(console.log(`${O(m.action)} ${ce(o,m.path)} (${m.detail})`),m.action==="refused"&&(process.exitCode=1),!d.ok)console.log("[info] Codex MCP: not wired -- the key did not verify, and it would be written into .codex/config.toml");else{let v=$n(o,s,u);console.log(`${O(v.action)} ${ce(o,v.path)} (${v.detail})`),v.action==="refused"?process.exitCode=1:x.push("codex")}}for(let m of Z){if(!n.includes(m))continue;if(!d.ok){console.log(`[info] ${V[m].label}: not wired -- the key did not verify, and it would be written into the host config`);continue}let v=rn(m,{homeDir:Yt(),env:process.env,apiUrl:s,apiKey:u});console.log(`${O(v.action)} ${v.path} (${V[m].label}: ${v.detail})`),v.action==="refused"?process.exitCode=1:x.push(m);let S=On(m,{homeDir:Yt(),env:process.env,runner:g});console.log(`${O(S.action)} ${S.path} (${V[m].label}: ${S.detail})`),S.action==="refused"?process.exitCode=1:y=!0}if(n.includes("opencode"))if(!d.ok)console.log("[info] OpenCode: not wired -- the key did not verify, and it would be written into the host config");else{let m=an({homeDir:Yt(),env:process.env,apiUrl:s,apiKey:u,major:sn()});console.log(`${O(m.action)} ${m.path} (OpenCode: ${m.detail})`),m.action==="refused"?process.exitCode=1:x.push("opencode")}if(d.ok)console.log(`${O("verified")} connection to ${s} (GET /api/workspaces -> 200)`);else{let m=d.status?`HTTP ${d.status}`:d.error??"unknown error";console.log(`${O("failed")} connection to ${s} (${m}) -- check your API key and --url`),(d.status===401||d.status===403)&&(process.exitCode=1)}if(h&&!e.embedKey&&h.action!=="skipped"&&(console.log(""),console.log("NOTE: .mcp.json references ${CONTEXT_API_KEY}. When that variable is unset, the"),console.log(`current @contextq/mcp reads the key from ${A()} instead.`),console.log('If `npx @contextq/mcp` still logs "Auth: NONE", export it before starting Claude Code:'),console.log(" export CONTEXT_API_KEY=<your key>"),console.log("...or re-run with --embed-key to write the literal key into .mcp.json instead.")),y){let m=In(o,p,f);console.log(`${O(m.action)} ${ce(o,m.path)} (${m.detail})`)}console.log("");let b={"claude-code":"a new Claude Code session",codex:"a new `codex` run",gemini:"a new `gemini` session",cursor:"a new Cursor or `cursor-agent` session",copilot:"a new `copilot` session",opencode:"a new `opencode` session"},k=n.filter(m=>m!=="desktop"&&x.includes(m)).map(m=>b[m]).join(" / start ");k?console.log(`Done. Open ${k} in this repo to pick up the wiring.`):(console.log("Nothing was wired: no surface got a ContextQ MCP connection. Fix what is reported above and re-run."),process.exitCode=1),w?(console.log(""),console.log("Next: in that Claude Code session, run /contextq:init to bind this repo to a workspace"),console.log("and project. Until it writes .contextq/config.json, the plugin's MCP tools work but its"),console.log("SessionStart/PreCompact/SessionEnd hooks stay silent and say so.")):n.includes("claude-code")&&(console.log(""),console.log("Claude Code 2.1.281+ can carry all of this as a plugin instead -- MCP server and"),console.log("lifecycle hooks, key in Claude Code's own store. Re-run init without --no-plugin after"),console.log("removing the contextq entry from .mcp.json, rather than installing the plugin on top:"),console.log("both would register the same MCP server. See https://app.contextq.dev/install.md."))}a(gi,"runInit");function hi(e){let t=[],n={};for(let o=0;o<e.length;o++){let r=e[o];switch(r){case"--workspace":n.workspace=e[++o];break;case"--key":n.key=e[++o];break;case"--url":n.url=e[++o];break;default:if(r.startsWith("-"))throw new Error(`unknown argument: ${r}`);t.push(r)}}if(t.length===0)throw new Error("missing <dir> argument -- usage: contextq skills sync <dir>");if(t.length>1)throw new Error(`unexpected extra argument: ${t[1]}`);return{...n,dir:t[0]}}a(hi,"parseSkillsSyncArgs");function mi(e){let t=D(),n=e.url||t.apiUrl||process.env.CONTEXT_API_URL||process.env.CTQ_API_URL||B,o=e.key||process.env.CONTEXT_API_KEY||process.env.CTQ_API_KEY||t.apiKey;if(!o)throw new Error("no API key available -- pass --key, set CONTEXT_API_KEY, or run `contextq init` first to write the credential file");return{apiUrl:n,apiKey:o}}a(mi,"resolveApiCreds");async function wi(e,t,n){let o=new URL("/api/agent/skills/export",e.replace(/\/$/,"")+"/");o.searchParams.set("format","json"),n&&o.searchParams.set("workspace",n);let r=await fetch(o,{headers:{Authorization:`Bearer ${t}`}});if(!r.ok)throw new Error(`GET ${o.pathname}${o.search} -> HTTP ${r.status}`);return(await r.json()).skills}a(wi,"fetchSkillsFromApi");async function yi(e){console.log("contextq skills sync"),console.log("=====================");let{apiUrl:t,apiKey:n}=mi(e);console.log(`[info] syncing to ${e.dir}${e.workspace?` (workspace: ${e.workspace})`:""}`);let o=await Yn({dir:e.dir,fetchSkills:a(()=>wi(t,n,e.workspace),"fetchSkills")});for(let r of o.written)console.log(`${O("created")} ${r}/SKILL.md`);for(let r of o.updated)console.log(`${O("added")} ${r}/SKILL.md (rewritten in place)`);if(console.log(`[info] ${o.written.length} written, ${o.updated.length} updated, ${o.unchanged.length} unchanged`),o.orphans.length>0){console.log(`[warn] ${o.orphans.length} orphaned folder(s) -- left on disk, not deleted:`);for(let r of o.orphans)console.log(` ${r}/ (its context no longer resolves)`)}}a(yi,"runSkillsSync");function ki(e){let t="claude",n=!1,o,r;for(let s=0;s<e.length;s++){let i=e[s],c=e[s+1];if(i==="--format"&&c){if(!Ot.includes(c))throw new Error(`--format must be one of: ${Ot.join(", ")}`);t=c,s++}else if(i==="--agent"&&c)o=c,s++;else if(i==="--workspace"&&c)r=c,s++;else if(i==="--require-config")n=!0;else throw new Error(`unknown "hook boot" argument "${i}"`)}return{format:t,agent:o,workspace:r,requireConfig:n}}a(ki,"parseHookBootArgs");async function xi(e){try{let t=ki(e),n=await Ln({cwd:process.env.CLAUDE_PROJECT_DIR||process.cwd(),env:process.env,stdinText:await Mn(),...t});n&&process.stdout.write(`${n}
138
+ `)}catch(t){console.error(`contextq hook boot: ${t.message}`)}}a(xi,"runHookBootCommand");async function bi(e){let t={url:B,restart:!1,leaveDenied:!1,abort:!1,keyStdin:!1,ping:void 0};for(let s=0;s<e.length;s++){let i=e[s];if(i==="--url")t.url=e[++s]??"";else if(i==="--restart")t.restart=!0;else if(i==="--leave-denied")t.leaveDenied=!0;else if(i==="--abort")t.abort=!0;else if(i==="--key-stdin")t.keyStdin=!0;else if(i==="--ping")t.ping=!0;else if(i==="--no-ping")t.ping=!1;else{console.error(`contextq: unknown option "${i}" for attach; usage: contextq attach [--url <url>] [--restart] [--leave-denied] [--abort] [--key-stdin] [--ping|--no-ping]`),process.exitCode=1;return}}let n=await Promise.resolve().then(()=>(wo(),mo)),{gitRoot:o}=await Promise.resolve().then(()=>(re(),$e)),r=null;try{r=o(process.cwd())}catch{r=null}try{if(t.abort){console.log(await n.abortAttach({cloudUrl:t.url,repoRoot:r}));return}console.log(`ContextQ attach -> ${t.url}`),await n.attachTransfer({cloudUrl:t.url,restart:t.restart,leaveDenied:t.leaveDenied,login:t.keyStdin?async()=>{let i=[];for await(let l of process.stdin)i.push(l);let c=Buffer.concat(i).toString("utf8").trim();if(!I.test(c))throw new Error("--key-stdin expects one sk_live_ key on stdin");return c}:()=>zt(i=>De({apiUrl:t.url,signal:i,onPrompt:Gt}))});let s=await n.attachSwitch({cloudUrl:t.url,repoRoot:r,pingConsent:t.ping});console.log(""),console.log(`Attached: ${Object.keys(s.acked).length} note(s) now live in your ContextQ account at ${t.url}.`),console.log("The local copy and its keys stay on this machine, read-only. Open a new Claude Code session to use the cloud memory, board and handoffs.")}catch(s){console.error(`contextq: ${s.message}`),process.exitCode=1}}a(bi,"runAttachCommand");async function vi(e){let t=!1,n;for(let i=0;i<e.length;i++){let c=e[i];if(c==="--json")t=!0;else if(c==="--workspace"&&e[i+1])n=e[++i];else if(c==="-h"||c==="--help"){console.log(`Usage:
139
+ npx @contextq/cli doctor [--json] [--workspace <slug>]
140
+
141
+ Checks, for every detected and wired host, that the configured API answers,
142
+ the key is accepted, the bound workspace reads, the host MCP entry points at
143
+ the same endpoint, the boot hook is wired, and a probe note round-trips.
144
+ Prints one PASS/FAIL line per step (SKIP when a detected host is not wired)
145
+ and exits 0 only when everything passes. --json prints the same as JSON.`);return}else{console.error(`contextq: unknown option "${c}" for "doctor"; usage: contextq doctor [--json] [--workspace <slug>]`),process.exitCode=1;return}}let{runDoctor:o,formatHuman:r}=await Promise.resolve().then(()=>(xo(),ko)),s=await o({workspace:n});if(t)console.log(JSON.stringify({ok:s.ok,mode:s.mode,apiUrl:s.apiUrl,workspace:s.workspace,results:s.lines},null,2));else for(let i of r(s))console.log(i);s.ok||(process.exitCode=1)}a(vi,"runDoctorCommand");async function $i(e){let t=await Promise.resolve().then(()=>(ve(),ro)),n=e[0],o={};for(let r=1;r<e.length;r++)if(e[r]==="--port"){let s=e[++r]??"",i=/^\d+$/.test(s)?Number(s):NaN;if(!Number.isInteger(i)||i<1||i>65535){console.error(`contextq: invalid port "${s}"; use a number from 1 to 65535`),process.exitCode=1;return}o.port=i}else if(e[r]==="--state-dir")o.stateDir=e[++r];else{console.error(`contextq: unknown option "${e[r]}" for "local ${n??""}"`),process.exitCode=1;return}try{if(n==="start"){let r=await t.start(o);r.warning&&console.log(`[warn] ${r.warning}`),console.log(`${r.started?"[start]":"[skip] "} ContextQ local server ${r.started?"started":"already running"} at ${r.url} (pid ${r.pid}, edge ${r.edgeVersion})`),console.log(`[info] state: ${r.stateDir}`)}else if(n==="status"){let r=await t.status(o);r.running?console.log(`running at ${r.url} (pid ${r.pid}, edge ${r.edgeVersion}${r.edgeVersion!==r.pinnedVersion?`, pinned ${r.pinnedVersion}`:""})`):(console.log(`not running: ${r.note??"not started"}`),process.exitCode=3),console.log(`state: ${r.stateDir}`)}else if(n==="stop"){let r=await t.stop(o);console.log(r.stopped?`[stop] ${r.note}`:`[skip] ${r.note}`)}else console.error("contextq: usage: contextq local start|status|stop [--port <n>] [--state-dir <dir>]"),process.exitCode=1}catch(r){console.error(`contextq: ${r.message}`),process.exitCode=1}}a($i,"runLocalCommand");async function Si(){let e=process.argv.slice(2);if(e[0]==="hook"){if(e[1]!=="boot"){console.error(`contextq: unknown "hook" subcommand "${e[1]??""}" -- expected "boot"`),process.exitCode=1;return}await xi(e.slice(2));return}if(e[0]==="attach"){await bi(e.slice(1));return}if(e[0]==="local"){await $i(e.slice(1));return}if(e[0]==="doctor"){await vi(e.slice(1));return}if(e[0]==="skills"){if(e[1]==="-h"||e[1]==="--help"){Te();return}if(e[1]!=="sync"){console.error(`contextq: unknown "skills" subcommand "${e[1]??""}" -- expected "sync"`),Te(),process.exitCode=1;return}try{let n=hi(e.slice(2));await yi(n)}catch(n){console.error(`contextq: ${n.message}`),process.exitCode=1}return}let t;try{t=pi(e)}catch(n){console.error(`contextq: ${n.message}`),Te(),process.exitCode=1;return}if(t.help||!t.command){Te();return}if(t.command==="login"){try{await fi(t)}catch(n){console.error(`contextq: ${n.message}`),process.exitCode=n instanceof P&&n.code==="cancelled"?130:1}return}if(t.command!=="init"){console.error(`contextq: unknown command "${t.command}"`),Te(),process.exitCode=1;return}try{if(t.local){if(t.key||t.url||t.embedKey||t.noPlugin||t.surface)throw new Error("--local takes only --workspace, --agent and --port: it needs no key, URL or surface");let{runLocalInit:n}=await Promise.resolve().then(()=>(re(),$e));await n({cwd:process.cwd(),workspace:t.workspace,agent:t.agent,runtime:t.port?{port:t.port}:void 0});return}await gi(t)}catch(n){console.error(`contextq: ${n.message}`),process.exitCode=1}}a(Si,"main");Si();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contextq/cli",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "One-command installer for ContextQ -- wires the MCP server, the contextq SessionStart boot hook, and the task manifest into a project (or prints a manual connect guide for Desktop/Cowork).",
5
5
  "type": "module",
6
6
  "bin": {