@contextq/cli 0.2.0 → 0.3.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 +26 -2
  2. package/dist/index.js +49 -31
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -15,13 +15,37 @@ 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
44
  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.
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`. |
@@ -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
 
package/dist/index.js CHANGED
@@ -1,36 +1,48 @@
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 bt=Object.defineProperty;var s=(e,t)=>bt(e,"name",{value:t,configurable:!0});var M=(e,t)=>()=>(e&&(t=e(e=0)),t);var ye=(e,t)=>{for(var n in t)bt(e,n,{get:t[n],enumerable:!0})};import{chmodSync as We,copyFileSync as io,existsSync as be,mkdirSync as so,readFileSync as ao,writeFileSync as co}from"fs";import{homedir as lo}from"os";import{join as D}from"path";function ze(){let e=process.env.CTQ_CRED_DIR||process.env.CONTEXTQ_CRED_DIR;if(e)return e;let t=uo.map(po);return t.find(n=>be(D(n,"credentials")))??t[0]}function po(e){let t=lo();if(process.platform==="darwin")return D(t,"Library","Application Support",e);if(process.platform==="win32"){let o=process.env.APPDATA??D(t,"AppData","Roaming");return D(o,e)}let n=process.env.XDG_CONFIG_HOME??D(t,".config");return D(n,e)}function _(){return D(ze(),"credentials")}function Ze(){return be(_())}function go(e,t){return e.apiKey===t.apiKey&&e.apiUrl===t.apiUrl}function fo(){let e=new Date,t=s(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 mo(){let e=_();if(!be(e))return null;let t=ze(),n=D(t,`credentials.bak-${fo()}`);return io(e,n),We(n,384),n}function xe(e,t){if(!be(t))return"";try{let n=ao(t,"utf-8");for(let o of n.split(`
3
+ `))if(o.startsWith(`${e}=`))return o.slice(e.length+1).trim()}catch{}return""}function R(){let e=_(),t=xe("CTQ_API_KEY",e)||xe("CONTEXT_API_KEY",e),n=xe("CTQ_API_URL",e)||xe("CONTEXT_API_URL",e);return{apiKey:t,apiUrl:n}}function Tt(e){let t=ze(),n=_();so(t,{recursive:!0,mode:448}),We(t,448);let o=`CTQ_API_KEY=${e.apiKey}
5
4
  CTQ_API_URL=${e.apiUrl}
6
5
  CONTEXT_API_KEY=${e.apiKey}
7
6
  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(`
7
+ `;return co(n,o,{mode:384}),We(n,384),n}function U(e){let t=R(),n=Ze(),o=s(r=>{if(!n)return Tt(r),{action:"created",backupPath:null};if(go(t,r))return{action:"skipped",backupPath:null};let i=mo();return Tt(r),{action:"rotated",backupPath:i}},"writeSafely");if(e.explicitKey){if(!T.test(e.explicitKey))throw new Error("the provided API key does not look valid (expected sk_live_<40 hex chars>)");let r=e.explicitKey,i=o({apiKey:r,apiUrl:e.apiUrl}),c=i.action==="skipped"?`${Ge} credentials (unchanged)`:i.action==="rotated"?`${Rt} credentials (${_()}; previous saved to ${i.backupPath})`:`${At} credentials (${_()}, mode 600)`;return{apiKey:r,credLine:c,action:i.action,backupPath:i.backupPath}}if(n&&t.apiKey){let r=e.ambientVarName?`${Ge} credentials (already configured at ${_()}; env ${e.ambientVarName} ignored, pass --key to rotate)`:`${Ge} credentials (already configured at ${_()}; pass --key to rotate)`;return{apiKey:t.apiKey,credLine:r,action:"skipped",backupPath:null}}if(e.ambientKey){if(!T.test(e.ambientKey))throw new Error("the provided API key does not look valid (expected sk_live_<40 hex chars>)");let r=e.ambientKey,i=o({apiKey:r,apiUrl:e.apiUrl}),c=i.action==="rotated"?`${Rt} credentials (${_()}; previous saved to ${i.backupPath})`:`${At} credentials (${_()}, mode 600)`;return{apiKey:r,credLine:c,action:i.action,backupPath:i.backupPath}}return{apiKey:"",credLine:"",action:null,backupPath:null}}var uo,T,At,Rt,Ge,B=M(()=>{"use strict";s(ze,"platformCredDir");uo=["contextq","ctq-brain"];s(po,"platformCredDirFor");s(_,"platformCredFile");s(Ze,"credsFileExists");s(go,"credsEqual");s(fo,"utcTimestamp");s(mo,"backupCredsFile");s(xe,"readEnvVal");s(R,"readExistingCreds");s(Tt,"writeCreds");T=/^sk_live_[0-9a-f]{40}$/,At="[create]",Rt="[rotate]",Ge="[skip] ";s(U,"decideCredentialWrite")});import{spawnSync as nt}from"child_process";import{existsSync as nr,readFileSync as or}from"fs";import{join as rr}from"path";function tn(e){return e[ot]||"claude"}function ir(e){let t=/(\d+)\.(\d+)\.(\d+)/.exec(e);return t?[Number(t[1]),Number(t[2]),Number(t[3])]:null}function sr(e,t){for(let n=0;n<3;n++)if(e[n]!==t[n])return e[n]>t[n];return!0}function nn(e){let t=rr(e,".mcp.json");if(!nr(t))return!1;try{return!!JSON.parse(or(t,"utf-8")).mcpServers?.contextq}catch{return!1}}function rt(e=process.env){let t=nt(tn(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=ir(t.stdout);return n?sr(n,en)?{version:n.join(".")}:{reason:`Claude Code ${n.join(".")} is older than ${en.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 ar(e){let t=e.split(`
8
+ `).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 cr(e){return`${e.replace(/\/$/,"")}/plugin/marketplace.json`}function Ee(e,t,n,o,r=process.env,i={}){let c=rt(r);if("reason"in c)return{status:"unavailable",reason:c.reason};let a=tn(r),d=nt(a,["plugin","marketplace","add",cr(i.marketplaceBase??t),"--scope","project"],{cwd:e,encoding:"utf-8",env:r});if(d.error||d.status!==0)return{status:"failed",reason:`\`claude plugin marketplace add\` exited ${d.status??"with an error"}`};let l=["plugin","install",O,"--scope","project","--json","--config",`apiKey=${n}`];t.replace(/\/$/,"")!==o.replace(/\/$/,"")&&l.push("--config",`apiUrl=${t}`);let u=nt(a,l,{cwd:e,encoding:"utf-8",env:r}),p=ar(u.stdout??"");if(!p)return{status:"failed",reason:`\`claude plugin install --json\` printed no result line (exit ${u.status??"error"})`};if(p.outcome!=="ok"||u.status!==0){let h=typeof p.failureCode=="string"?` [${p.failureCode}]`:"";return{status:"failed",reason:`${String(p.message??"install failed")}${h}`}}let g=String(p.message??"installed");return{status:"installed",message:g,alreadyInstalled:/already installed/i.test(g)}}function on(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 O,en,ot,Pe=M(()=>{"use strict";O="contextq@contextq-plugins",en=[2,1,281],ot="CONTEXTQ_CLAUDE_BIN";s(tn,"claudeBin");s(ir,"parseClaudeVersion");s(sr,"versionAtLeast");s(nn,"hasLegacyMcpEntry");s(rt,"detectClaude");s(ar,"lastJsonLine");s(cr,"marketplaceUrl");s(Ee,"installPlugin");s(on,"decidePluginDelegation")});function G(e,t){if(!lr.test(e))throw new Error(`${t} value "${e}" is not a valid slug -- allowed: ${Te}`);return e}function K(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 lr,Te,Ae=M(()=>{"use strict";lr=/^[a-z0-9][a-z0-9_-]{0,63}$/,Te='lowercase letters, digits, "-", "_" (must start with a letter/digit, max 64 chars)';s(G,"validateSlug");s(K,"sanitizeSlugFallback")});var Cn={};ye(Cn,{DEFAULT_LOCAL_PORT:()=>xn,EDGE_PACKAGE:()=>pt,EDGE_SOURCE_ENV:()=>bn,EDGE_VERSION_PIN:()=>kn,LocalRuntimeError:()=>I,MIN_NODE_MAJOR:()=>re,STATE_DIR_ENV:()=>dt,checkNode:()=>Ue,defaultLocalStateDir:()=>H,preflight:()=>$n,provision:()=>Sn,start:()=>ce,status:()=>Ne,stop:()=>Hr});import{spawn as Pr,spawnSync as lt}from"child_process";import{chmodSync as Tr,closeSync as Ar,existsSync as W,mkdirSync as Rr,openSync as Or,readFileSync as De,rmSync as Ie,writeFileSync as wn}from"fs";import{homedir as Ir,platform as Nr}from"os";import{join as E}from"path";function H(e=process.env){if(e[dt])return e[dt];let t=Ir();if(Nr()==="darwin")return E(t,"Library","Application Support","contextq","local");let n=e.XDG_DATA_HOME&&e.XDG_DATA_HOME.trim()!==""?e.XDG_DATA_HOME:E(t,".local","share");return E(n,"contextq","local")}function ae(e){let t=e.version??kn,n=e.stateDir??H();return{stateDir:n,runtimeDir:e.runtimeDir??E(n,"runtime"),port:e.port??xn,version:t,source:e.packageSource??process.env[bn]??`${pt}@${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 vn(e){return E(e.runtimeDir,`edge-${e.version}`)}function ie(e){return E(vn(e),"node_modules","@contextq","edge")}function Ue(e){let t=Number(e.split(".")[0]);if(!Number.isFinite(t)||t<re)throw new I(`ContextQ local mode needs Node ${re} or newer, and this is Node ${e}; install Node ${re}+ and run \`contextq local start\` again.`)}function ut(e){Rr(e,{recursive:!0,mode:448}),Tr(e,448)}function yn(e){let t=E(ie(e),"package.json");if(!W(t))return null;try{return JSON.parse(De(t,"utf8")).version}catch{return null}}function Sn(e={}){let t=ae(e);Ue(t.nodeVersion);let n=vn(t);if(yn(t)===t.version&&W(E(ie(t),"dist","cli.js")))return{dir:ie(t),installed:!1};ut(t.stateDir),ut(n),W(E(n,"package.json"))||wn(E(n,"package.json"),`${JSON.stringify({name:"contextq-local-runtime",private:!0},null,2)}
9
+ `);let o=lt(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 i=`${o.stderr??""}`.trim().split(`
10
+ `).slice(-3).join(" | ");throw new I(`could not install ${t.source} into ${n}${/E404|No matching version|ETARGET/.test(i)?" (that version is not published yet)":""}: ${i||o.error?.message||`npm exited ${o.status}`}`)}let r=yn(t);if(r!==t.version)throw new I(`installed ${pt} is ${r??"missing"}, but this CLI is pinned to ${t.version}; remove ${n} and run \`contextq local start\` again.`);return{dir:ie(t),installed:!0}}function $n(e={}){let t=ae(e);Ue(t.nodeVersion);let n=ie(t),o=lt(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 i=`${o.stderr??""}`.trim().split(`
11
+ `).find(c=>/Error|error/.test(c))??`exit ${o.status}`;throw new I(`better-sqlite3 does not load on this machine (${i.slice(0,200)}); local mode supports macOS arm64 and Linux x64 with Node ${re}+ and npm prebuilt binaries.`)}return lt(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 se(e){try{return process.kill(e,0),!0}catch(t){return t.code==="EPERM"}}function Dr(e){let t=N(e).pid;if(!W(t))return null;let n=Number(De(t,"utf8").trim());return Number.isInteger(n)&&n>0?n:null}function Ur(e){let t=N(e).endpoint;if(!W(t))return null;try{return JSON.parse(De(t,"utf8"))}catch{return null}}async function Lr(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=ae(e),n={running:!1,owned:!1,url:null,pid:null,edgeVersion:null,pinnedVersion:t.version,stateDir:t.stateDir},o=Dr(t.stateDir),r=Ur(t.stateDir);if(o===null)return{...n,note:"not started"};if(!se(o))return Ie(N(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 i=await Lr(t.fetchImpl,r.url);return!i||i.mode!=="local"||i.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(i.edgeVersion??"")||null}}async function Kr(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 ce(e={}){let t=ae(e);Ue(t.nodeVersion);let n=await Ne(e);if(n.running){if(n.edgeVersion&&n.edgeVersion!==t.version)throw new I(`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 Kr(t);if(o)throw new I(`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}=Sn(e),i=$n(e);ut(t.stateDir),Ie(N(t.stateDir).endpoint,{force:!0});let c=Or(N(t.stateDir).log,"a",384),a=Pr(t.nodeBin,[E(r,"dist","cli.js"),"serve","--state-dir",t.stateDir,"--port",String(t.port)],{detached:!0,stdio:["ignore",c,c]});if(Ar(c),a.pid===void 0)throw new I("could not start the local server process");wn(N(t.stateDir).pid,`${a.pid}
12
+ `,{mode:384}),a.unref();let d=Date.now()+t.startTimeoutMs;for(;Date.now()<d;){let u=await Ne(e);if(u.running)return{...u,started:!0,...i.warning?{warning:i.warning}:{}};if(!se(a.pid))break;await new Promise(p=>setTimeout(p,200))}let l=W(N(t.stateDir).log)?De(N(t.stateDir).log,"utf8").trim().split(`
13
+ `).slice(-3).join(" | "):"";throw se(a.pid)&&process.kill(a.pid,"SIGTERM"),Ie(N(t.stateDir).pid,{force:!0}),new I(`the local server did not come up within ${Math.round(t.startTimeoutMs/1e3)} s${l?`: ${l}`:""}`)}async function Hr(e={}){let t=ae(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&&se(n.pid);)await new Promise(r=>setTimeout(r,100));return se(n.pid)?{stopped:!1,note:`pid ${n.pid} did not exit after SIGTERM; it was left running`}:(Ie(N(t.stateDir).pid,{force:!0}),{stopped:!0,note:`stopped pid ${n.pid}`})}var pt,kn,xn,re,dt,bn,I,N,Le=M(()=>{"use strict";pt="@contextq/edge",kn="0.2.0",xn=47321,re=22,dt="CONTEXTQ_LOCAL_STATE_DIR",bn="CONTEXTQ_EDGE_PACKAGE",I=class extends Error{static{s(this,"LocalRuntimeError")}constructor(t){super(t),this.name="LocalRuntimeError"}};s(H,"defaultLocalStateDir");s(ae,"resolve");N=s(e=>({pid:E(e,"edge.pid"),endpoint:E(e,"endpoint.json"),log:E(e,"edge.log")}),"paths");s(vn,"runtimeRoot");s(ie,"edgePackageDir");s(Ue,"checkNode");s(ut,"ensureDir");s(yn,"installedVersion");s(Sn,"provision");s($n,"preflight");s(se,"pidAlive");s(Dr,"readPid");s(Ur,"readEndpoint");s(Lr,"health");s(Ne,"status");s(Kr,"portHolder");s(ce,"start");s(Hr,"stop")});var ft={};ye(ft,{readJournal:()=>En,readPluginState:()=>j,reconfigurePlugin:()=>gt,restorePlugin:()=>Yr,supportsTerminalConfigure:()=>_n,syncPlatformCredentials:()=>Gr,writeJournal:()=>Q});import{spawnSync as jr}from"child_process";import{chmodSync as qr,existsSync as Mr,mkdirSync as Fr,readFileSync as Jr,renameSync as Br,writeFileSync as Vr}from"fs";import{dirname as Qr}from"path";function Xr(e){return e[ot]||"claude"}function X(e){return e.replace(/\/+$/,"")}function Ke(e,t,n){return jr(Xr(e),t,{encoding:"utf-8",env:e,input:n,timeout:6e4})}function _n(e=process.env){let t=Ke(e,["plugin","configure","--help"]);return!t.error&&t.status===0&&/--values-stdin/.test(t.stdout??"")}function j(e=process.env,t=O){let n=Ke(e,["plugin","list","--json"]),o;try{o=JSON.parse(n.stdout??"[]").find(d=>d.id===t)}catch{o=void 0}if(!o)return{installed:!1,scope:null,enabled:null,apiUrl:null,apiKeySet:!1};let r=Ke(e,["plugin","configure",t,"--json"]),i=null,c=!1;try{let a=JSON.parse(r.stdout??"{}");i=a.inputs?.apiUrl?X(a.inputs.apiUrl):null,c=Array.isArray(a.configured)&&a.configured.includes("apiKey")}catch{i=null}return{installed:!0,scope:o.scope??null,enabled:o.enabled??null,apiUrl:i,apiKeySet:c}}function Q(e,t){Fr(Qr(e),{recursive:!0,mode:448});let n=`${e}.tmp`;Vr(n,`${JSON.stringify(t,null,2)}
14
+ `,{mode:384}),qr(n,384),Br(n,e)}function En(e){if(!Mr(e))return null;try{return JSON.parse(Jr(e,"utf-8"))}catch{return null}}function Pn(e,t,n){let o=Ke(e,["plugin","configure",t,"--values-stdin"],JSON.stringify({apiUrl:X(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 gt(e){let t=e.env??process.env,n=e.pluginId??O,o=X(e.target.apiUrl);if(!_n(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 i=s(()=>new Date().toISOString(),"now"),c={version:1,pluginId:n,startedAt:i(),updatedAt:i(),step:"backed_up",before:{apiUrl:r.apiUrl,scope:r.scope,enabled:r.enabled},target:{apiUrl:o},previousKeySource:e.previousKeySource??null};Q(e.journalPath,c);let a=Pn(t,n,e.target);if(a)return Q(e.journalPath,{...c,step:"failed",updatedAt:i(),error:a}),{status:"failed",reason:a,state:j(t,n)};Q(e.journalPath,{...c,step:"configured",updatedAt:i()});let d=j(t,n),l=[];if(d.apiUrl!==o&&l.push(`apiUrl is ${d.apiUrl??"unset"}, expected ${o}`),d.apiKeySet||l.push("apiKey is not set"),d.scope!==r.scope&&l.push(`scope changed from ${r.scope} to ${d.scope}`),d.enabled!==r.enabled&&l.push(`enabled changed from ${r.enabled} to ${d.enabled}`),l.length>0){let u=`the effective plugin config does not match the switch: ${l.join("; ")}`;return Q(e.journalPath,{...c,step:"failed",updatedAt:i(),error:u}),{status:"failed",reason:u,state:d}}return Q(e.journalPath,{...c,step:"verified",updatedAt:i()}),{status:r.apiUrl===o?"updated":"switched",state:d}}function Yr(e,t,n=process.env){let o=En(e);if(!o)return{status:"failed",reason:`no plugin switch journal at ${e}`,state:null};if(o.before.apiUrl&&X(t.apiUrl)!==o.before.apiUrl)return{status:"failed",reason:`the restore endpoint ${X(t.apiUrl)} is not the one recorded before the switch (${o.before.apiUrl})`,state:null};let r=Pn(n,o.pluginId,t);if(r)return{status:"failed",reason:r,state:j(n,o.pluginId)};let i=j(n,o.pluginId);return i.apiUrl!==X(t.apiUrl)||!i.apiKeySet?{status:"failed",reason:`restore did not take effect (apiUrl is ${i.apiUrl??"unset"})`,state:i}:(Q(e,{...o,step:"restored",updatedAt:new Date().toISOString()}),{status:"switched",state:i})}function Gr(e){if(!T.test(e.apiKey))return{written:!1,line:"[skip] platform credentials (a local token stays in the local state dir)"};let t=U({explicitKey:e.apiKey,ambientVarName:null,ambientKey:"",apiUrl:X(e.apiUrl)});return{written:t.action!=="skipped",line:t.credLine}}var He=M(()=>{"use strict";Pe();B();s(Xr,"bin");s(X,"normalizeUrl");s(Ke,"run");s(_n,"supportsTerminalConfigure");s(j,"readPluginState");s(Q,"writeJournal");s(En,"readJournal");s(Pn,"applyConfig");s(gt,"reconfigurePlugin");s(Yr,"restorePlugin");s(Gr,"syncPlatformCredentials")});var le={};ye(le,{CLOUD_MARKETPLACE_BASE:()=>ht,appendBinding:()=>On,gitRoot:()=>An,readBinding:()=>Rn,runLocalInit:()=>ri,writeLocalBinding:()=>In});import{spawnSync as Wr}from"child_process";import{existsSync as zr,mkdirSync as Zr,readFileSync as Tn,renameSync as ei,writeFileSync as ti}from"fs";import{homedir as ni}from"os";import{basename as oi,join as z,resolve as mt}from"path";function An(e){let t=Wr("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=mt(ni());if(mt(n)===o||mt(n)===z(o,".claude"))throw new Error(`${n} is your home directory or ~/.claude, not a project repo; nothing was bound`);return n}function Rn(e){let t=z(e,".contextq","config.json");if(!zr(t))return null;try{return JSON.parse(Tn(t,"utf8"))}catch{throw new Error(`${t} is not valid JSON; fix or delete it first`)}}function On(e,t,n="contextq-cli"){let o=z(e,".contextq"),r=z(o,"config.json"),i=Rn(e),c=i??{},a=c._entries??[],d=c.write_seq??a.length,l=!1;for(let[p,g]of t){if(c[p]===g)continue;let h=d;d+=1,a.push({key:p,value:g,written_by:n,based_on:h,seq:d,ts:new Date().toISOString().replace(/\.\d{3}Z$/,"Z")}),c[p]=g,l=!0}if(!l)return"skipped";c.config_schema_version=Math.max(5,c.config_schema_version??5),c.write_seq=d,c._entries=a,Zr(o,{recursive:!0});let u=`${r}.tmp`;return ti(u,`${JSON.stringify(c,null,2)}
15
+ `),ei(u,r),i?"updated":"created"}function In(e,t){return On(e,[["mode","local"],["workspace",t.workspace],["agent",t.agent]])}async function ri(e){let t=e.log??(x=>console.log(x)),n=e.env??process.env,o=An(e.cwd),r=oi(o),i=e.workspace?G(e.workspace.toLowerCase(),"--workspace"):K(r),c=e.agent?G(e.agent,"--agent"):K(r),a=e.runtime?.stateDir??H(n),d=await ce({...e.runtime,stateDir:a});d.warning&&t(`[warn] ${d.warning}`),t(`${d.started?"[start]":"[skip] "} local server at ${d.url} (edge ${d.edgeVersion})`);let l=Tn(z(a,"token"),"utf8").trim(),u=d.url,p=rt(n);if("reason"in p)t(`[info] Claude Code plugin not wired: ${p.reason}. Install Claude Code 2.1.281+ and run \`contextq init --local\` again.`);else{let x=j(n);if(x.installed){let b=gt({target:{apiUrl:u,apiKey:l},journalPath:z(a,"plugin-config.backup.json"),previousKeySource:x.apiUrl&&x.apiUrl.startsWith("http://127.0.0.1")?"local-token":"platform-cred",env:n});if(b.status==="failed")throw new Error(`could not point the installed ${O} at the local server: ${b.reason}`);t(`${b.status==="switched"?"[rotate]":"[skip] "} ${O} -> ${u} (${b.status})`)}else{let b=Ee(o,u,l,ht,n,{marketplaceBase:e.marketplaceBase??ht});if(b.status!=="installed")throw new Error(`could not install ${O}: ${b.status==="unavailable",b.reason}`);t(`[add] ${O} at project scope -> ${u}`)}}let g={authorization:`Bearer ${l}`},h=await fetch(`${u}/contexts?workspace=${encodeURIComponent(i)}&limit=1`,{headers:g}),f=await fetch(`${u}/hooks/session-start`,{method:"POST",headers:{...g,"content-type":"application/json"},body:JSON.stringify({agent:c,workspace:i})}),w=f.ok?(await f.json()).hookSpecificOutput?.additionalContext??"":"";if(!h.ok||!w.startsWith("ContextQ local mode"))throw new Error(`the local server did not answer a memory read and boot-lite (HTTP ${h.status}/${f.status}); run \`contextq local status\``);let k=(await h.json()).total??0;t(`[check] local memory answers: ${k} note(s) in ${i}, boot-lite in local mode`);let y=In(o,{workspace:i,agent:c});t(`${y==="skipped"?"[skip] ":y==="created"?"[create]":"[add] "} .contextq/config.json (mode=local workspace=${i} agent=${c})`),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 ht,de=M(()=>{"use strict";Le();Pe();He();Ae();ht="https://app.contextq.dev";s(An,"gitRoot");s(Rn,"readBinding");s(On,"appendBinding");s(In,"writeLocalBinding");s(ri,"runLocalInit")});var Ln={};ye(Ln,{AttachError:()=>C,abortAttach:()=>fi,attachSwitch:()=>gi,attachTransfer:()=>ui,ensureLocalServer:()=>Qe,fingerprint:()=>wt,isSimulatedCrash:()=>pi,journalPath:()=>fe,reached:()=>ue,readJournal:()=>Ve,writeJournal:()=>$});import{createHash as ii,randomUUID as si}from"crypto";import{chmodSync as ai,existsSync as qe,mkdirSync as ci,readFileSync as yt,renameSync as li,rmSync as Me,writeFileSync as Dn}from"fs";import{dirname as di,join as Z}from"path";function ue(e,t){return Nn.indexOf(e.state)>=Nn.indexOf(t)}function fe(e){return Z(e,"attach-journal.json")}function Ve(e){let t=fe(e);if(!qe(t))return null;try{return JSON.parse(yt(t,"utf8"))}catch{throw new C(`${t} is not readable JSON; move it aside and run \`contextq attach --restart\``)}}function $(e,t){let n=fe(e);ci(di(n),{recursive:!0,mode:448});let o=`${n}.tmp`;Dn(o,`${JSON.stringify({...t,updatedAt:new Date().toISOString()},null,2)}
16
+ `,{mode:384}),ai(o,384),li(o,n)}function wt(e){return ii("sha256").update(e,"utf8").digest("hex").slice(0,12)}function me(e){return e.replace(/\/+$/,"")}async function pe(e,t,n,o,r){let i=await e(`${t.url}${o}`,{method:n,headers:{authorization:`Bearer ${t.token}`,...r?{"content-type":"application/json"}:{}},body:r?JSON.stringify(r):void 0}),c=await i.text();if(!i.ok)throw new C(`the local server answered HTTP ${i.status} for ${n} ${o}: ${c.slice(0,200)}`);return JSON.parse(c)}async function Fe(e,t,n,o,r,i){let c=await e(`${t}${r}`,{method:o,headers:{authorization:`Bearer ${n}`,...i?{"content-type":"application/json"}:{}},body:i?JSON.stringify(i):void 0}),a=await c.text();return{status:c.status,text:a}}async function Qe(e){return{url:(await ce({...e.runtime,stateDir:e.stateDir})).url,token:yt(Z(e.stateDir,"token"),"utf8").trim()}}function Un(e,t){let n=R();if(!n.apiKey||me(n.apiUrl)!==e||wt(n.apiKey)!==t.keyFingerprint)throw new C(`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 ui(e){let t=e.log??(d=>console.log(d)),n=e.fetchImpl??fetch,o=e.stateDir??H(),r=me(e.cloudUrl),i=Ve(o);if(i&&e.restart){if(ue(i,"freezing"))throw new C("the attach already switched or is switching; finish it or run `contextq attach --abort` first");Me(fe(o),{force:!0}),i=null}if(i&&i.cloudUrl!==r)throw new C(`an attach to ${i.cloudUrl} is in progress; resume it with --url ${i.cloudUrl}, or run \`contextq attach --restart\``);i&&i.state==="login_pending"&&(i=null);let c=await Qe({stateDir:o,runtime:e.runtime}),a;if(i)a=Un(r,i),t(`[info] resuming attach ${i.attachId} at ${i.state}`);else{i={version:1,attachId:si(),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,i);try{a=await e.login();let d=await Fe(n,r,a,"GET","/api/workspaces");if(d.status!==200)throw new C(`${r} did not accept the approved key (HTTP ${d.status}); nothing was changed`)}catch(d){throw Me(fe(o),{force:!0}),d}U({explicitKey:a,ambientVarName:null,ambientKey:"",apiUrl:r}),i={...i,state:"authenticated",keyFingerprint:wt(a)},$(o,i),t(`[check] signed in to ${r}`)}if(e.leaveDenied&&(i.leaveDenied=!0),!ue(i,"workspaces_mapped")){let{workspaces:d}=await pe(n,c,"GET","/workspaces");for(let l of d){if(l.contexts===0||i.workspaceMap[l.slug])continue;let u="global";if(l.slug!=="global"){let p=await Fe(n,r,a,"POST","/api/workspaces/ensure",{name:l.slug,slug:l.slug});if(p.status!==200&&p.status!==201)throw new C(`could not ensure workspace ${l.slug} on ${r} (HTTP ${p.status}); run \`contextq attach\` again to resume`);let g=JSON.parse(p.text);if(typeof g.tenantId=="number"){if(i.tenantId!==null&&i.tenantId!==g.tenantId)throw new C(`the account at ${r} is not the one this attach started with; run \`contextq attach --restart\``);i.tenantId=g.tenantId}u=g.slug}await pe(n,c,"PUT","/sync/workspace-map",{local:l.slug,cloud:u}),i.workspaceMap[l.slug]=u,$(o,i),t(`[map] ${l.slug} -> ${u}`)}i.state="workspaces_mapped",$(o,i)}if(!ue(i,"transferred")||i.state==="transfer_blocked"){i.state="transferring",$(o,i);let d=Number.POSITIVE_INFINITY;for(;;){let l=await pe(n,c,"POST","/sync/push",{parentUrl:r,apiKey:a,...e.batchMaxContexts?{batchMaxContexts:e.batchMaxContexts}:{}});for(let p of l.refs)p.outcome==="accepted"&&typeof p.cloudId=="number"?(i.acked[p.ref]=p.cloudId,delete i.denied[p.ref]):(p.outcome==="denied"||p.outcome==="too_large")&&(i.denied[p.ref]=p.reason??p.outcome);if($(o,i),l.failures.length>0)throw new C(`the transfer stopped: ${l.failures[0].error}; run \`contextq attach\` again to resume (nothing is sent twice)`);if(l.pending-(i.leaveDenied?Object.keys(i.denied).length:0)<=0)break;if(l.pending>=d||Object.keys(i.denied).length>=l.pending){i.state="transfer_blocked",$(o,i);let p=Object.entries(i.denied).map(([g,h])=>` ${g}: ${h}`).join(`
17
+ `);throw new C(`${l.pending} note(s) were refused by ${r}:
18
+ ${p}
19
+ Edit or delete them locally and run \`contextq attach\` again, or pass --leave-denied to keep them local-only`)}d=l.pending}i.state="transferred",$(o,i),t(`[push] ${Object.keys(i.acked).length} note(s) are in ${r}`)}return i}function ge(e){return Z(e,"attach-binding-before.json")}function pi(e){return e instanceof Je}async function Be(e,t,n,o){await pe(e,t,"PUT","/sync/write-lock",{lock:n,...o?{attachedTo:o}:{}})}async function je(e,t,n,o,r){let{restorePlugin:i,readPluginState:c}=await Promise.resolve().then(()=>(He(),ft)),{appendBinding:a}=await Promise.resolve().then(()=>(de(),le)),d=e.fetchImpl??fetch,l=e.env??process.env,u=[],p=Z(o,"plugin-config.backup.json"),g=c(l);if(g.installed&&g.apiUrl&&me(g.apiUrl)!==me(n.url)&&qe(p)){let h=i(p,{apiUrl:n.url,apiKey:n.token},l);u.push(h.status==="failed"?`plugin NOT restored: ${h.reason}`:"plugin points at the local server again")}if(e.repoRoot&&qe(ge(o))){let h=JSON.parse(yt(ge(o),"utf8")),f=[];h.mode&&f.push(["mode",h.mode]),h.workspace&&f.push(["workspace",h.workspace]),f.length>0&&a(e.repoRoot,f),u.push("repo binding restored")}throw await Be(d,n,null),t.state="transferred",$(o,t),new C(`${r}; rolled back (${[...u,"local writes unlocked"].join(", ")}). Run \`contextq attach\` again to retry.`)}async function gi(e){let t=e.log??(f=>console.log(f)),n=e.fetchImpl??fetch,o=e.env??process.env,r=e.stateDir??H(),i=me(e.cloudUrl),{reconfigurePlugin:c,readPluginState:a}=await Promise.resolve().then(()=>(He(),ft)),{appendBinding:d,readBinding:l}=await Promise.resolve().then(()=>(de(),le)),u=Ve(r);if(!u||!ue(u,"transferred")||u.state==="transfer_blocked")throw new C("nothing is ready to switch: run the transfer first (`contextq attach`)");if(u.state==="attached")return u;let p=await Qe({stateDir:r,runtime:e.runtime}),g=Un(i,u),h=s(f=>{if(e.crashAfter===f)throw new Je(`simulated crash after ${f}`)},"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 Be(n,p,"attach"),h("freezing");let f=await pe(n,p,"POST","/sync/push",{parentUrl:i,apiKey:g});for(let y of f.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((f.failures.length>0||f.pending>w)&&await je(e,u,p,r,`the final flush left ${f.pending} note(s) pending${f.failures[0]?` (${f.failures[0].error})`:""}`),u.state="switching",$(r,u),e.repoRoot&&!qe(ge(r))){let y=l(e.repoRoot),x={mode:y?.mode??null,workspace:y?.workspace??null};Dn(ge(r),`${JSON.stringify(x)}
20
+ `,{mode:384})}if(a(o).installed){let y=c({target:{apiUrl:i,apiKey:g},journalPath:Z(r,"plugin-config.backup.json"),previousKeySource:"local-token",env:o});y.status==="failed"&&await je(e,u,p,r,`switching the plugin failed: ${y.reason}`),t(`[rotate] plugin -> ${i}`)}else t("[info] no installed ContextQ plugin in this Claude Code profile; only the repo binding switches");if(h("switching"),e.repoRoot){let x=l(e.repoRoot)?.workspace??"global",b=u.workspaceMap[x]??x;d(e.repoRoot,[["mode","cloud"],["workspace",b]]);for(let m of["bound-host","local-ids.json"])Me(Z(e.repoRoot,".contextq","cache",m),{force:!0});t(`[bind] .contextq/config.json -> mode=cloud workspace=${b}`)}u.state="verifying",$(r,u)}if(u.state==="verifying"){let f=e.repoRoot?l(e.repoRoot)?.workspace??"global":"global",w=await Fe(n,i,g,"POST","/api/agent/boot",{agent:"contextq-attach",workspace:f}),k=Object.values(u.acked)[0],y=k===void 0?{status:200,text:""}:await Fe(n,i,g,"GET",`/api/contexts/${k}`),x=w.status===200&&/"mode"\s*:\s*"local"/.test(w.text);(w.status!==200||x||y.status!==200)&&await je(e,u,p,r,`the cloud did not verify (boot HTTP ${w.status}${x?", still local":""}, read HTTP ${y.status})`),await Be(n,p,"attached",i),u.state="attached",$(r,u),Me(ge(r),{force:!0}),t(`[check] ${i} answers boot and reads an imported note`)}return u}async function fi(e){let t=e.stateDir??H(),n=Ve(t);if(!n)return"no attach in progress";if(n.state==="attached")return"the attach already finished; nothing to abort";let o=await Qe({stateDir:t,runtime:e.runtime});if(["freezing","switching","verifying"].includes(n.state))try{await je(e,n,o,t,"aborted")}catch(r){return r.message}return await Be(e.fetchImpl??fetch,o,null),`attach left at ${n.state}; local writes are unlocked`}var C,Nn,Je,Kn=M(()=>{"use strict";Le();B();C=class extends Error{static{s(this,"AttachError")}constructor(t){super(t),this.name="AttachError"}},Nn=["login_pending","authenticated","workspaces_mapped","transferring","transfer_blocked","transferred","freezing","switching","verifying","attached"];s(ue,"reached");s(fe,"journalPath");s(Ve,"readJournal");s($,"writeJournal");s(wt,"fingerprint");s(me,"normalizeUrl");s(pe,"localJson");s(Fe,"cloudCall");s(Qe,"ensureLocalServer");s(Un,"storedKeyFor");s(ui,"attachTransfer");s(ge,"bindingFile");Je=class extends Error{static{s(this,"SimulatedCrash")}};s(pi,"isSimulatedCrash");s(Be,"setLock");s(je,"rollback");s(gi,"attachSwitch");s(fi,"abortAttach")});import{homedir as Hn}from"os";import{basename as jn}from"path";import{existsSync as ke}from"fs";import{execSync as to}from"child_process";import{homedir as Et}from"os";import{join as Ye}from"path";var Mn="CONTEXTQ_HOST";var Fn="CLAUDE_CODE_ENABLE_FUNCTION_HOOKS",Jn=["claude-code","claude-desktop","codex","opencode","cyberine","muse","unknown"],Bn=[["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 vt(e){return e.trim().toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}s(vt,"normalizeName");function Vn(e){if(!e)return"unknown";let t=vt(e);if(!t)return"unknown";let n=[...Bn].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"}s(Vn,"hostFromClientName");function Qn(e){if(!e)return;let t=vt(e);return Jn.find(n=>n===t)}s(Qn,"parseHostOverride");function St(e={}){let t=e.env??(typeof process<"u"?process.env:{}),n=t.CLAUDE_CODE_SESSION_ID||void 0,o=s((i,c)=>({host:i,supportsFunctionHooks:i==="claude-code"&&t[Fn]==="1",...i==="claude-code"&&n?{sessionId:n}:{},via:c}),"finish");if(e.forcedHost)return o(e.forcedHost,"forced");let r=Qn(t[Mn]);return r?o(r,"env-override"):e.clientName!==void 0?o(Vn(e.clientName),"client-info"):t.CLAUDECODE==="1"?o("claude-code","env"):o("unknown","none")}s(St,"resolveHost");import{chmodSync as Xn,existsSync as Yn,mkdirSync as Gn,readFileSync as Wn,writeFileSync as zn}from"fs";import{execFileSync as $t}from"child_process";import{basename as Zn,dirname as Ct,join as we}from"path";var Y=["gemini","cursor","copilot"],J={gemini:{label:"Gemini CLI",configDir:s(e=>we(e,".gemini"),"configDir"),configFile:"settings.json",entry:s((e,t)=>({httpUrl:e,headers:{Authorization:`Bearer ${t}`}}),"entry"),listCommand:"gemini mcp list"},cursor:{label:"Cursor",configDir:s(e=>we(e,".cursor"),"configDir"),configFile:"mcp.json",entry:s((e,t)=>({url:e,headers:{Authorization:`Bearer ${t}`}}),"entry"),listCommand:"cursor-agent mcp list"},copilot:{label:"Copilot CLI",configDir:s((e,t)=>t.COPILOT_HOME||we(e,".copilot"),"configDir"),configFile:"mcp-config.json",entry:s((e,t)=>({type:"http",url:e,headers:{Authorization:`Bearer ${t}`},tools:["*"]}),"entry"),listCommand:"copilot mcp list"}};function te(e,t,n){return J[e].configDir(t,n)}s(te,"hostConfigDir");function eo(e,t,n){return we(te(e,t,n),J[e].configFile)}s(eo,"hostMcpConfigPath");function F(e){return`${e.replace(/\/+$/,"")}/mcp`}s(F,"mcpEndpoint");function Xe(e){let t=Ct(e);try{if($t("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 $t("git",["-C",t,"check-ignore","-q","--",Zn(e)],{stdio:"ignore"}),!1}catch(n){return n.status===1}}s(Xe,"gitTracksUnignored");function _t(e,t){let n=J[e],o=eo(e,t.homeDir,t.env),r=Yn(o),i={};if(r){let a=Wn(o,"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:o,action:"refused",detail:`not valid JSON (${d.message}); left untouched`}}}let c=i.mcpServers??{};return typeof c!="object"||Array.isArray(c)?{surface:e,path:o,action:"refused",detail:"mcpServers is not an object; left untouched"}:c.contextq?{surface:e,path:o,action:"skipped",detail:"contextq server entry already present"}:Xe(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} -> ${F(t.apiUrl)}`}:(i.mcpServers={...c,contextq:n.entry(F(t.apiUrl),t.apiKey)},Gn(Ct(o),{recursive:!0}),zn(o,`${JSON.stringify(i,null,2)}
21
+ `,{mode:384}),Xn(o,384),{surface:e,path:o,action:r?"added":"created",detail:`contextq remote MCP (${F(t.apiUrl)}) with the API key in the header, file mode 600; check with \`${n.listCommand}\``})}s(_t,"wireHostMcp");function no(e){try{return to("git rev-parse --show-toplevel",{cwd:e,stdio:["ignore","pipe","ignore"],encoding:"utf-8"}).trim()}catch{return""}}s(no,"gitRoot");function oo(e){return ke(Ye(e,".codex"))}s(oo,"hasCodexProjectDir");function ro(e){return ke(Ye(e,".codex"))}s(ro,"hasCodexHome");function Pt(e,t={}){let n=no(e),o=n||e,{host:r}=St(t.hostSignals);if(t.forcedSurface)return{surfaces:[t.forcedSurface],root:o,host:r};let a=ke(Ye(e,".claude"))||!!n&&!(r!=="unknown"&&r!=="claude-code"),d=oo(o)||ro(t.homeDir??Et()),l=[];a&&l.push("claude-code"),d&&l.push("codex");let u=t.homeDir??Et(),p=t.env??process.env;for(let g of Y)ke(te(g,u,p))&&l.push(g);return l.length===0&&l.push("desktop"),{surfaces:l,root:a||d?o:e,host:r}}s(Pt,"detectSurface");B();B();var Ot="the provided API key does not look valid (expected sk_live_<40 hex chars>)",ho="npx @contextq/cli init --key <sk_live_...>";function It(e){return e.status===401||e.status===403}s(It,"isRefused");function Nt(e,t){switch(e){case"explicit":return"The --key";case"ambient":return`The ${t??"environment"}`;case"stored":return`The stored (${_()})`;case"prompt":return"The typed";case"device":return"The browser-approved"}}s(Nt,"describe");async function Dt(e){let t=R(),n=Ze()?t.apiKey:"",o,r;if(e.explicitKey){if(!T.test(e.explicitKey))throw new Error(Ot);o=e.explicitKey,r="explicit"}else if(n)o=n,r="stored";else if(e.ambientKey){if(!T.test(e.ambientKey))throw new Error(Ot);o=e.ambientKey,r="ambient"}else if(e.interactive&&e.deviceLogin)o=await e.deviceLogin(),r="device";else{let a=e.interactive?await e.prompt("ContextQ API key (sk_live_...): "):"";if(!T.test(a))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=a,r="prompt"}let i=await e.verify(o);if(It(i)){let a=`${Nt(r,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: ${ho}`);if(e.deviceLogin)console.log(`${a}. Approve this machine in the browser to replace it.`),o=await e.deviceLogin(),r="device";else{let d=await e.prompt(`${a}. Paste a new key (sk_live_...): `);if(!T.test(d))throw new Error(`${a}, and no valid replacement was entered; nothing was written.`);o=d,r="prompt"}if(i=await e.verify(o),It(i))throw new Error(`${Nt(r,null)} API key was refused by ${e.apiUrl} (HTTP ${i.status}); nothing was written.`)}if(r==="device"&&!i.ok){let a=i.status?`HTTP ${i.status}`:i.error??"unknown error";throw new Error(`could not verify the browser-approved key against ${e.apiUrl} (${a}); nothing was written. Run \`contextq login\` again once the server answers.`)}return{decision:U({explicitKey:r==="explicit"||r==="prompt"||r==="device"?o:void 0,ambientVarName:e.ambientVarName,ambientKey:e.ambientKey,apiUrl:e.apiUrl}),verify:i,source:r}}s(Dt,"resolveVerifiedCredentials");B();import{hostname as yo}from"os";var S=class extends Error{static{s(this,"DeviceLoginError")}code;constructor(t,n){super(n),this.name="DeviceLoginError",this.code=t}},wo=5,ko=5,xo=600;function bo(e,t){return new Promise((n,o)=>{if(t?.aborted)return o(new S("cancelled","login cancelled; nothing was written"));let r=setTimeout(()=>{t?.removeEventListener("abort",i),n()},e),i=s(()=>{clearTimeout(r),o(new S("cancelled","login cancelled; nothing was written"))},"onAbort");t?.addEventListener("abort",i,{once:!0})})}s(bo,"defaultSleep");async function Ut(e){try{return await e.json()}catch{return null}}s(Ut,"readJson");function Lt(e,t){return typeof e=="number"&&Number.isInteger(e)&&e>0?e:t}s(Lt,"positiveInt");async function ve(e){let t=e.fetchImpl??fetch,n=e.sleep??bo,o=e.now??Date.now,r=e.apiUrl.replace(/\/+$/,""),i=s(()=>new S("cancelled","login cancelled; nothing was written"),"cancelled"),c;try{c=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??yo()}),signal:e.signal})}catch(f){throw e.signal?.aborted?i():new S("unreachable",`could not reach ${r}/api/auth/device/start (${f.message})`)}let a=c.ok?await Ut(c):null,d=typeof a?.device_code=="string"?a.device_code:"",l=typeof a?.user_code=="string"?a.user_code:"";if(!d||!l)throw new S("protocol",`${r} did not start a device login (HTTP ${c.status}); is this a ContextQ server?`);let u=typeof a?.verification_uri_complete=="string"&&a.verification_uri_complete||typeof a?.verification_uri=="string"&&a.verification_uri||`${r}/link`,p=Lt(a?.expires_in,xo),g=Lt(a?.interval,ko);e.onPrompt({verificationUri:u,userCode:l,expiresInSeconds:p});let h=o()+p*1e3;for(;;){if(o()>=h)throw new S("expired_token","the approval window expired before this machine was approved; nothing was written");if(await n(g*1e3,e.signal),e.signal?.aborted)throw i();let f;try{f=await t(`${r}/api/auth/device/token`,{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({device_code:d}),signal:e.signal})}catch{if(e.signal?.aborted)throw i();continue}let w=await Ut(f);if(f.status===200){let y=w?.api_key;if(typeof y=="string"&&T.test(y))return y;throw new S("protocol","the token endpoint answered 200 without a well-formed API key; nothing was written")}let k=typeof w?.error=="string"?w.error:"";switch(k){case"authorization_pending":continue;case"slow_down":g+=wo;continue;case"access_denied":throw new S("access_denied","the request was denied in the browser; nothing was written");case"expired_token":throw new S("expired_token","the device code expired; run the command again");case"invalid_grant":throw new S("invalid_grant","the server rejected the device code; run the command again");default:if(f.status>=500||f.status===429)continue;throw new S("protocol",`unexpected answer from the token endpoint (HTTP ${f.status}${k?`, ${k}`:""})`)}}}s(ve,"deviceLogin");import{existsSync as vo,readFileSync as So,writeFileSync as $o}from"fs";import{execFileSync as Co}from"child_process";import{join as _o}from"path";function Eo(e,t){try{return Co("git",["check-ignore","-q","--",t],{cwd:e,stdio:"ignore"}),!0}catch(n){return n.status===1?!1:null}}s(Eo,"isPathGitIgnored");function Kt(e,t,n,o){let r=_o(e,".mcp.json"),i=vo(r),c={};if(i){let a=So(r,"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:r,action:"skipped",detail:"contextq server entry already present"};if(n){let a=Eo(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:n?o:"${CONTEXT_API_KEY}"}},$o(r,`${JSON.stringify(c,null,2)}
22
+ `),{path:r,action:i?"added":"created",detail:`contextq server wired (key=${n?"embedded literal":"${CONTEXT_API_KEY} placeholder"})`}}s(Kt,"wireMcpJson");import{appendFileSync as Po,chmodSync as To,existsSync as Se,mkdirSync as Ao,readFileSync as qt,writeFileSync as Ro}from"fs";import{execFileSync as Mt}from"child_process";import{dirname as Oo,join as $e}from"path";import{parse as Ht,stringify as Io}from"smol-toml";var Ce=".codex/config.toml";function No(e){return $e(e,".codex","config.toml")}s(No,"codexConfigPath");function Do(e,t){if(!Se($e(e,".git")))return!1;try{return Mt("git",["-C",e,"ls-files","--error-unmatch","--",t],{stdio:"ignore"}),!0}catch{return!1}}s(Do,"gitTracked");function Uo(e,t){try{return Mt("git",["-C",e,"check-ignore","-q","--",t],{stdio:"ignore"}),!0}catch{return!1}}s(Uo,"gitIgnored");function Lo(e){if(!Se($e(e,".git"))||Uo(e,Ce))return;let t=$e(e,".gitignore"),n=Se(t)?qt(t,"utf-8"):"",o=n===""||n.endsWith(`
10
23
  `)?"":`
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(`
24
+ `;Po(t,`${o}${Ce}
25
+ `)}s(Lo,"ensureConfigGitignored");function Ko(e,t){return Io({mcp_servers:{contextq:{url:F(e),http_headers:{Authorization:`Bearer ${t}`}}}})}s(Ko,"codexMcpBlock");function jt(e){let t=e.mcp_servers;if(!(!t||typeof t!="object"||Array.isArray(t)))return t.contextq}s(jt,"contextqEntry");function Ft(e,t,n){let o=No(e),r=Se(o),i=r?qt(o,"utf-8"):"";if(r){let d;try{d=Ht(i)}catch(l){return{path:o,action:"refused",detail:`not valid TOML (${l.message.split(`
26
+ `)[0]}); left untouched`}}if(jt(d)!==void 0)return{path:o,action:"skipped",detail:"mcp_servers.contextq already present"}}if(Do(e,Ce))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 ${Ce}) and re-run, or add the server by hand: codex -> ${F(t)}`};let c=i===""?"":i.endsWith(`
14
27
  `)?`
15
28
  `:`
16
29
 
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(`
30
+ `,a=`${i}${c}${Ko(t,n)}`;try{let d=Ht(a);if(jt(d)===void 0)throw new Error("mcp_servers.contextq missing after merge")}catch(d){return{path:o,action:"refused",detail:`adding [mcp_servers.contextq] would not parse (${d.message.split(`
31
+ `)[0]}), e.g. mcp_servers is an inline table; left untouched`}}return Ao(Oo(o),{recursive:!0}),Lo(e),Ro(o,a,{mode:384}),To(o,384),{path:o,action:r?"added":"created",detail:`contextq streamable HTTP MCP (${F(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\``}}s(Ft,"wireCodexMcp");import{chmodSync as Xo,existsSync as tt,mkdirSync as Yt,readFileSync as Yo,writeFileSync as Gt}from"fs";import{dirname as Wt,join as _e}from"path";import{appendFileSync as Ho,existsSync as ne,mkdirSync as jo,readFileSync as et,writeFileSync as qo}from"fs";import{execFileSync as Mo}from"child_process";import{join as L,relative as Fo}from"path";var Jo=/contextq-boot\.sh|contextq.*boot/;function Jt(e){return`'${e.split("'").join("'\\''")}'`}s(Jt,"shellQuote");var V="npx -y --prefer-offline @contextq/cli";function Bt(e,t,n,o=V){return`${o} hook boot --format ${n} --agent ${Jt(e)} --workspace ${Jt(t)}`}s(Bt,"buildBootCommand");function Bo(e,t){if(!ne(L(e,".git")))return!1;try{return Mo("git",["-C",e,"ls-files","--error-unmatch","--",Fo(e,t)],{stdio:"ignore"}),!0}catch{return!1}}s(Bo,"gitTracked");function Vt(e,t,n){let{path:o,dir:r,topLevelDefaults:i}=e,c=ne(o),a={};if(c){let l=et(o,"utf-8");try{a=l.trim()===""?{}:JSON.parse(l)}catch(u){return{path:o,action:"refused",detail:`not valid JSON (${u.message}); left untouched`}}if(!a||typeof a!="object"||Array.isArray(a))return{path:o,action:"refused",detail:"top level is not a JSON object; left untouched"}}if(i)for(let[l,u]of Object.entries(i))a[l]===void 0&&(a[l]=u);return a.hooks??={},a.hooks.SessionStart??=[],a.hooks.SessionStart.some(l=>(l?.hooks??[]).some(u=>typeof u.command=="string"&&Jo.test(u.command)))?{path:o,action:"skipped",detail:"SessionStart contextq-boot hook already present"}:e.root&&Bo(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"}:(a.hooks.SessionStart.push({hooks:[{type:"command",command:t}]}),jo(r,{recursive:!0}),qo(o,`${JSON.stringify(a,null,2)}
32
+ `),{path:o,action:"wired",detail:n})}s(Vt,"mergeSessionStartHook");function Qt(e,t,n,o=V){let r=L(e,".claude","settings.local.json"),i=Bt(t,n,"claude",o);return Vt({path:r,dir:L(e,".claude"),root:e},i,`SessionStart boot hook wired (agent=${t}, workspace=${n}; runs \`${o} hook boot\`, key from the credentials file)`)}s(Qt,"wireSessionHook");function Vo(e){let t=L(e,".codex","config.toml");if(!ne(t))return!1;let n=et(t,"utf-8");return/^\s*\[{1,2}hooks(\.|\])/m.test(n)}s(Vo,"hasInlineHooksTable");function Qo(e,t){if(!ne(L(e,".git")))return;let n=L(e,".gitignore"),o=ne(n)?et(n,"utf-8"):"";if(new Set(o.split(`
20
33
  `).map(c=>c.trim())).has(t))return;let i=`
21
34
  # .codex/hooks.json embeds a per-machine absolute path (the contextq boot
22
35
  # hook command) -- gitignored the same way .claude/settings.local.json is.
23
36
  # Each teammate who wants it needs to run \`npx contextq init\` themselves.
24
37
  ${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(`
38
+ `;Ho(n,i)}s(Qo,"ensureGitignored");function Xt(e,t,n,o=V){let r=L(e,".codex","hooks.json"),i=Bt(t,n,"codex",o),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=Vo(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=Vt({path:r,dir:L(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=${n}). ${c}.${a}`);return d.action==="wired"&&Qo(e,".codex/hooks.json"),d}s(Xt,"wireCodexSessionHook");var Go=/contextq.*hook boot|contextq-boot\.sh/;function Wo(e,t=V){return`${t} hook boot --format ${e} --require-config`}s(Wo,"hostHookCommand");function zo(e,t,n){let o=te(e,t,n);return e==="gemini"?_e(o,"settings.json"):e==="cursor"?_e(o,"hooks.json"):_e(o,"hooks","contextq.json")}s(zo,"hostHookPath");function Zo(e){if(!tt(e))return{doc:{}};let t=Yo(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})`}}}s(Zo,"readJsonObject");function er(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}s(er,"eventList");function tr(e){return JSON.stringify(e).match(Go)!==null}s(tr,"alreadyWired");function zt(e,t){let n=zo(e,t.homeDir,t.env),o=tt(n),{doc:r,error:i}=Zo(n);if(!r)return{surface:e,path:n,action:"refused",detail:`${i}; left untouched`};let c=Wo(e,t.runner),a=e==="gemini"?"SessionStart":"sessionStart";e!=="gemini"&&(r.version??=1);let d=er(r,a);return d?tr(d)?{surface:e,path:n,action:"skipped",detail:"contextq boot hook already present"}:Xe(n)?{surface:e,path:n,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}),Yt(Wt(n),{recursive:!0}),Gt(n,`${JSON.stringify(r,null,2)}
39
+ `,{mode:384}),Xo(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.${a} is not an array; left untouched`}}s(zt,"wireHostHook");function Zt(e,t,n){let o=_e(e,".contextq","config.json");return tt(o)?{path:o,action:"skipped",detail:"repo already bound -- left untouched"}:(Yt(Wt(o),{recursive:!0}),Gt(o,`${JSON.stringify({config_schema_version:5,workspace:t,agent:n,read_scope:"own+global"},null,2)}
40
+ `),{path:o,action:"created",detail:`binds this repo to workspace ${t} for the host hooks; commit it so other machines share the binding`})}s(Zt,"ensureRepoConfig");Pe();B();Ae();import{execFileSync as dr}from"child_process";import{existsSync as rn,readFileSync as ur}from"fs";import{basename as sn,join as an}from"path";var st=["claude","codex","gemini","cursor","copilot"],pr="https://app.contextq.dev";function oe(e){let t=(e??"").trim();return!t||/^\$\{[^}]*\}$/.test(t)?"":t}s(oe,"usable");function gr(e){let t=R(),n=oe(e.CONTEXT_API_KEY)||oe(e.CTQ_API_KEY),o=n||t.apiKey,r=(n?oe(e.CONTEXT_API_URL)||oe(e.CTQ_API_URL):"")||t.apiUrl||oe(e.CONTEXT_API_URL)||pr;return{apiKey:o,apiUrl:r.replace(/\/+$/,"")}}s(gr,"resolveHookCreds");function fr(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}s(fr,"sessionCwd");function it(e,t){try{return dr("git",["-C",e,...t],{stdio:["ignore","pipe","ignore"],encoding:"utf-8"}).trim()}catch{return""}}s(it,"git");function mr(e,t,n){let o=an(e,".contextq","config.json");if(rn(o))return ur(o,"utf-8");let r=K(sn(e));return JSON.stringify({workspace:(n||r).toLowerCase(),agent:t||r,read_scope:"own+global"})}s(mr,"hookConfig");function Re(e,t){return JSON.stringify(e==="cursor"?{additional_context:t}:e==="copilot"?{additionalContext:t}:{hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:t}})}s(Re,"formatForHost");async function cn(e){let t=fr(e.cwd,e.stdinText),n=it(t,["rev-parse","--show-toplevel"]);if(!n||e.requireConfig&&!rn(an(n,".contextq","config.json")))return"";let{apiKey:o,apiUrl:r}=gr(e.env);if(!o)return Re(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:mr(n,e.agent,e.workspace),agent_slug:e.agent??"",project:sn(n),git_root:n,branch:it(n,["branch","--show-current"]),head_sha:it(n,["rev-parse","--short","HEAD"]),goal:"",board:"",token_budget:"3000"}),c=e.fetchImpl??fetch,a=0,d="";try{let l=await c(`${r}/api/agent/hooks/session-start`,{method:"POST",headers:{Authorization:`Bearer ${o}`,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 Re(e.format,`ContextQ: ${r} 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 Re(e.format,`ContextQ: ${r} 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 u=JSON.parse(d).hookSpecificOutput?.additionalContext;return typeof u!="string"||u===""?"":Re(e.format,u)}catch{return""}}s(cn,"runHookBoot");async function ln(e=500){return process.stdin.isTTY?"":new Promise(t=>{let n="",o=s(()=>{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",i=>{n+=i}),process.stdin.on("end",()=>{clearTimeout(r),o()})})}s(ln,"readStdinIfPiped");import{appendFileSync as hr,existsSync as dn,readFileSync as yr}from"fs";import{execFileSync as wr}from"child_process";import{join as at}from"path";var kr=[".contextq/tasks/TASKS.md",".contextq/tasks/tasks/T*.md",".contextq/tasks/.render*",".contextq/tasks/status.log"];function xr(e){try{return wr("git",["-C",e,"ls-files","--error-unmatch","--",".contextq/tasks/TASKS.md"],{stdio:"ignore"}),!0}catch{return!1}}s(xr,"manifestTracked");function un(e){let t=at(e,".gitignore");if(!dn(at(e,".git")))return{path:t,action:"skipped",detail:"not a git repo -- no board render to keep out of commits"};if(xr(e))return{path:at(e,".contextq","tasks","TASKS.md"),action:"skipped",detail:"this repo tracks a git-sourced board -- left untouched"};let n=dn(t)?yr(t,"utf-8"):"",o=new Set(n.split(`
41
+ `).map(c=>c.trim())),r=kr.filter(c=>!o.has(c));if(r.length===0)return{path:t,action:"skipped",detail:"board render already gitignored"};let i=n===""||n.endsWith(`
30
42
  `)?"":`
31
- `;return No(t,`${i}${s.join(`
43
+ `;return hr(t,`${i}${r.join(`
32
44
  `)}
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(`
45
+ `),{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)"}}s(un,"initTasksDir");function pn(e){console.log(`
34
46
  No git repo or existing Claude Code project found here, so there is nothing on
35
47
  disk to scaffold. For Claude Desktop, Cowork, or any other MCP-capable client
36
48
  that reads a manually-edited config file, connect ContextQ in 3 steps:
@@ -59,27 +71,33 @@ There is no auto-connect for Desktop/Cowork today -- these clients only read
59
71
  their config file on start, so step 2 has to be a manual paste. Re-run
60
72
  "npx @contextq/cli init" inside a git repo (or an existing Claude Code project) to
61
73
  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
74
+ `)}s(pn,"printDesktopGuide");async function gn(e){return process.stdin.isTTY?new Promise(t=>{let n=process.stdin,o=process.stdout;o.write(e);let r="",i=s(c=>{let a=c.toString("utf8");switch(a){case`
75
+ `:case"\r":case"":n.setRawMode?.(!1),n.pause(),n.removeListener("data",i),o.write(`
76
+ `),t(r.trim());break;case"":o.write(`
77
+ `),process.exit(130);break;case"\x7F":case"\b":r.length>0&&(r=r.slice(0,-1),o.write("\b \b"));break;default:r+=a,o.write("*")}},"onData");n.setRawMode?.(!0),n.resume(),n.setEncoding("utf8"),n.on("data",i)}):""}s(gn,"promptHidden");Ae();import{existsSync as ct,mkdirSync as fn,readdirSync as br,readFileSync as mn,statSync as vr,writeFileSync as Sr}from"fs";import{join as Oe}from"path";function $r(e,t){let n=new RegExp(`^ {2}${t}: "((?:[^"\\\\]|\\\\.)*)"$`,"m"),o=e.match(n);return o?o[1].replace(/\\(.)/g,"$1"):null}s($r,"extractFrontmatterMetadataValue");function Cr(e){return $r(e,"cluster_key")}s(Cr,"extractClusterKey");function _r(e){if(!ct(e))return[];let t=[];for(let n of br(e)){let o=Oe(e,n);if(!vr(o).isDirectory())continue;let r=Oe(o,"SKILL.md");if(!ct(r))continue;let i=mn(r,"utf-8"),c=Cr(i);c&&t.push({folder:n,clusterKey:c})}return t}s(_r,"scanLocalSkills");function Er(e,t){return e.replace(/^name: .+$/m,`name: ${t}`)}s(Er,"withFrontmatterName");async function hn(e){fn(e.dir,{recursive:!0});let t=_r(e.dir),n=new Map(t.map(a=>[a.clusterKey,a.folder])),o=new Set(t.map(a=>a.folder)),r=await e.fetchSkills(),i={written:[],updated:[],unchanged:[],orphans:[]},c=new Set;for(let a of r){c.add(a.clusterKey);let l=n.get(a.clusterKey);l||(l=o.has(a.slug)?`${a.slug}-c${a.contextId}`:a.slug,o.add(l),n.set(a.clusterKey,l));let u=l===a.slug?a.markdown:Er(a.markdown,l),p=Oe(e.dir,l),g=Oe(p,"SKILL.md"),h=ct(g)?mn(g,"utf-8"):null;if(h===u){i.unchanged.push(l);continue}fn(p,{recursive:!0}),Sr(g,u,"utf-8"),i[h===null?"written":"updated"].push(l)}for(let a of t)c.has(a.clusterKey)||i.orphans.push(a.folder);return i}s(hn,"syncSkills");var q="https://app.contextq.dev";function mi(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 i=e[++o]??"",c=/^\d+$/.test(i)?Number(i):NaN;if(!Number.isInteger(c)||c<1||c>65535)throw new Error(`invalid port "${i}"; use a number from 1 to 65535`);n.port=c;break}case"--surface":{let i=e[++o];if(i!=="claude-code"&&i!=="codex"&&!Y.includes(i))throw new Error(`--surface value "${i}" is invalid -- allowed: claude-code, codex, ${Y.join(", ")}`);n.surface=i;break}case"-h":case"--help":n.help=!0;break;default:throw new Error(`unknown argument: ${r}`)}}return n}s(mi,"parseArgs");function he(){console.log(`contextq \u2014 one-command installer for ContextQ
66
78
 
67
79
  Usage:
80
+ npx @contextq/cli login [--url <url>]
81
+ npx @contextq/cli local start|status|stop [--port <n>] [--state-dir <dir>]
68
82
  npx @contextq/cli init [options]
83
+ npx @contextq/cli init --local [--workspace <slug>] [--agent <slug>] [--port <n>]
84
+ npx @contextq/cli attach [--url <url>] [--restart] [--leave-denied] [--abort] [--key-stdin]
69
85
 
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.
86
+ API key: \`login\` prints a link and a code; approve this machine in the browser and the
87
+ key is fetched, checked against the server and saved (mode 600), never typed or pasted.
88
+ With no key stored, init runs the same browser approval from an interactive terminal, or
89
+ reads CONTEXT_API_KEY / CTQ_API_KEY from the environment. A refused or unverified key is
90
+ never written.
73
91
 
74
92
  Options:
75
93
  --key <sk_live_...> Pass or rotate the key on the command line (visible in shell
76
94
  history; prefer the prompt or the env var)
77
- --url <url> ContextQ API URL (default: ${A})
95
+ --url <url> ContextQ API URL (default: ${q})
78
96
  --workspace <slug> Workspace slug for the contextq boot hook (default: repo basename,
79
- sanitized). Allowed: ${Y}. Case is folded to
97
+ sanitized). Allowed: ${Te}. Case is folded to
80
98
  lowercase automatically (workspace slugs are always lowercase).
81
99
  --agent <slug> Agent slug for the contextq boot hook (default: repo basename,
82
- sanitized). Allowed: ${Y}.
100
+ sanitized). Allowed: ${Te}.
83
101
  --embed-key Write the literal API key into .mcp.json instead of the
84
102
  safer \${CONTEXT_API_KEY} placeholder (opt-in: convenience
85
103
  vs. a secret sitting on disk)
@@ -110,6 +128,6 @@ Options:
110
128
  --key <sk_live_...> API key (else CONTEXT_API_KEY/CTQ_API_KEY env, else the
111
129
  credential file written by \`contextq init\`)
112
130
  --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();
131
+ CONTEXT_API_URL/CTQ_API_URL env, default: ${q})
132
+ `)}s(he,"printHelp");function ee(e,t){return t.startsWith(`${e}/`)?t.slice(e.length+1):t}s(ee,"relative");function A(e){return{created:"[create]",added:"[add] ",wired:"[create]",rotated:"[rotate]",skipped:"[skip] ",verified:"[check]",failed:"[check]",refused:"[refuse]"}[e]??`[${e}]`}s(A,"label");async function qn(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}}}s(qn,"verifyConnection");function kt(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)...`)}s(kt,"printDevicePrompt");function xt(e){let t=new AbortController,n=s(()=>t.abort(),"onSigint");return process.once("SIGINT",n),e(t.signal).finally(()=>process.removeListener("SIGINT",n))}s(xt,"withSigintAbort");async function hi(e){let t=R(),n=e.url||t.apiUrl||process.env.CONTEXT_API_URL||q;console.log(`ContextQ login (${n})`);let o=await xt(c=>ve({apiUrl:n,signal:c,onPrompt:kt})),r=await qn(n,o);if(!r.ok){let c=r.status?`HTTP ${r.status}`:r.error??"unknown error";throw new Error(`could not verify the approved key against ${n} (${c}); nothing was written`)}let i=U({explicitKey:o,ambientVarName:null,ambientKey:"",apiUrl:n});console.log("Approved."),console.log(i.credLine),console.log(`[check] connected to ${n}`)}s(hi,"runLogin");async function yi(e){let t=process.cwd(),{surfaces:n,root:o}=Pt(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})`),pn(e.url??q);return}console.log(`Surface(s) detected: ${n.join(", ")} (root: ${o})`);let r=R(),i=e.url||r.apiUrl||process.env.CONTEXT_API_URL||q,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 Dt({explicitKey:e.key,ambientVarName:c,ambientKey:a,apiUrl:i,interactive:!!process.stdin.isTTY,verify:s(m=>qn(i,m),"verify"),prompt:gn,deviceLogin:s(()=>xt(m=>ve({apiUrl:i,signal:m,onPrompt:kt})),"deviceLogin")}),u=d.apiKey;if(console.log(d.credLine),!l.ok){let m=l.status?`HTTP ${l.status}`:l.error??"unknown error";console.log(`[warn] could not verify the key against ${i} (${m}); it was kept, re-run init once the server answers`)}let p;e.agent?p=G(e.agent,"--agent"):p=K(jn(o));let g;if(e.workspace){let m=e.workspace.toLowerCase();g=G(m,"--workspace"),m!==e.workspace&&console.log(`[info] --workspace lowercased to match slug convention: "${e.workspace}" -> "${m}"`)}else g=K(jn(o));console.log(`[info] agent=${p} workspace=${g}`);let h=process.env.CONTEXTQ_HOOK_RUNNER||V,f,w=!1,k=[],y=!1;if(n.includes("claude-code")){let m=on({noPlugin:e.noPlugin,legacyMcpEntry:nn(o),verifyStatus:l.status});if(!m.delegate)console.log(`[info] ${m.reason}`);else{let v=Ee(o,i,u,q);v.status==="installed"?(w=!0,k.push("claude-code"),console.log(`${A(v.alreadyInstalled?"skipped":"added")} .claude/settings.json (${O} at project scope -- ${v.message})`)):console.log(`[info] plugin install ${v.status}: ${v.reason} -- falling back to .mcp.json wiring`)}if(!w){f=Kt(o,i,e.embedKey,u),console.log(`${A(f.action)} ${ee(o,f.path)} (${f.detail})`),k.push("claude-code");let v=Qt(o,p,g,h);console.log(`${A(v.action)} ${ee(o,v.path)} (${v.detail})`),v.action==="refused"&&(process.exitCode=1)}let P=un(o);console.log(`${A(P.action)} ${ee(o,P.path)} (${P.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=Xt(o,p,g,h);if(console.log(`${A(m.action)} ${ee(o,m.path)} (${m.detail})`),m.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 P=Ft(o,i,u);console.log(`${A(P.action)} ${ee(o,P.path)} (${P.detail})`),P.action==="refused"?process.exitCode=1:k.push("codex")}}for(let m of Y){if(!n.includes(m))continue;if(!l.ok){console.log(`[info] ${J[m].label}: not wired -- the key did not verify, and it would be written into the host config`);continue}let P=_t(m,{homeDir:Hn(),env:process.env,apiUrl:i,apiKey:u});console.log(`${A(P.action)} ${P.path} (${J[m].label}: ${P.detail})`),P.action==="refused"?process.exitCode=1:k.push(m);let v=zt(m,{homeDir:Hn(),env:process.env,runner:h});console.log(`${A(v.action)} ${v.path} (${J[m].label}: ${v.detail})`),v.action==="refused"?process.exitCode=1:y=!0}if(l.ok)console.log(`${A("verified")} connection to ${i} (GET /api/workspaces -> 200)`);else{let m=l.status?`HTTP ${l.status}`:l.error??"unknown error";console.log(`${A("failed")} connection to ${i} (${m}) -- check your API key and --url`),(l.status===401||l.status===403)&&(process.exitCode=1)}if(f&&!e.embedKey&&f.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 ${_()} 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=Zt(o,g,p);console.log(`${A(m.action)} ${ee(o,m.path)} (${m.detail})`)}console.log("");let x={"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"},b=n.filter(m=>m!=="desktop"&&k.includes(m)).map(m=>x[m]).join(" / start ");b?console.log(`Done. Open ${b} 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."))}s(yi,"runInit");function wi(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]}}s(wi,"parseSkillsSyncArgs");function ki(e){let t=R(),n=e.url||t.apiUrl||process.env.CONTEXT_API_URL||process.env.CTQ_API_URL||q,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}}s(ki,"resolveApiCreds");async function xi(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}s(xi,"fetchSkillsFromApi");async function bi(e){console.log("contextq skills sync"),console.log("=====================");let{apiUrl:t,apiKey:n}=ki(e);console.log(`[info] syncing to ${e.dir}${e.workspace?` (workspace: ${e.workspace})`:""}`);let o=await hn({dir:e.dir,fetchSkills:s(()=>xi(t,n,e.workspace),"fetchSkills")});for(let r of o.written)console.log(`${A("created")} ${r}/SKILL.md`);for(let r of o.updated)console.log(`${A("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)`)}}s(bi,"runSkillsSync");function vi(e){let t="claude",n=!1,o,r;for(let i=0;i<e.length;i++){let c=e[i],a=e[i+1];if(c==="--format"&&a){if(!st.includes(a))throw new Error(`--format must be one of: ${st.join(", ")}`);t=a,i++}else if(c==="--agent"&&a)o=a,i++;else if(c==="--workspace"&&a)r=a,i++;else if(c==="--require-config")n=!0;else throw new Error(`unknown "hook boot" argument "${c}"`)}return{format:t,agent:o,workspace:r,requireConfig:n}}s(vi,"parseHookBootArgs");async function Si(e){try{let t=vi(e),n=await cn({cwd:process.env.CLAUDE_PROJECT_DIR||process.cwd(),env:process.env,stdinText:await ln(),...t});n&&process.stdout.write(`${n}
133
+ `)}catch(t){console.error(`contextq hook boot: ${t.message}`)}}s(Si,"runHookBootCommand");async function $i(e){let t={url:q,restart:!1,leaveDenied:!1,abort:!1,keyStdin:!1,ping:void 0};for(let i=0;i<e.length;i++){let c=e[i];if(c==="--url")t.url=e[++i]??"";else if(c==="--restart")t.restart=!0;else if(c==="--leave-denied")t.leaveDenied=!0;else if(c==="--abort")t.abort=!0;else if(c==="--key-stdin")t.keyStdin=!0;else if(c==="--ping")t.ping=!0;else if(c==="--no-ping")t.ping=!1;else{console.error(`contextq: unknown option "${c}" 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(()=>(Kn(),Ln)),{gitRoot:o}=await Promise.resolve().then(()=>(de(),le)),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 c=[];for await(let d of process.stdin)c.push(d);let a=Buffer.concat(c).toString("utf8").trim();if(!T.test(a))throw new Error("--key-stdin expects one sk_live_ key on stdin");return a}:()=>xt(c=>ve({apiUrl:t.url,signal:c,onPrompt:kt}))});let i=await n.attachSwitch({cloudUrl:t.url,repoRoot:r,pingConsent:t.ping});console.log(""),console.log(`Attached: ${Object.keys(i.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(i){console.error(`contextq: ${i.message}`),process.exitCode=1}}s($i,"runAttachCommand");async function Ci(e){let t=await Promise.resolve().then(()=>(Le(),Cn)),n=e[0],o={};for(let r=1;r<e.length;r++)if(e[r]==="--port"){let i=e[++r]??"",c=/^\d+$/.test(i)?Number(i):NaN;if(!Number.isInteger(c)||c<1||c>65535){console.error(`contextq: invalid port "${i}"; use a number from 1 to 65535`),process.exitCode=1;return}o.port=c}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}}s(Ci,"runLocalCommand");async function _i(){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 Si(e.slice(2));return}if(e[0]==="attach"){await $i(e.slice(1));return}if(e[0]==="local"){await Ci(e.slice(1));return}if(e[0]==="skills"){if(e[1]==="-h"||e[1]==="--help"){he();return}if(e[1]!=="sync"){console.error(`contextq: unknown "skills" subcommand "${e[1]??""}" -- expected "sync"`),he(),process.exitCode=1;return}try{let n=wi(e.slice(2));await bi(n)}catch(n){console.error(`contextq: ${n.message}`),process.exitCode=1}return}let t;try{t=mi(e)}catch(n){console.error(`contextq: ${n.message}`),he(),process.exitCode=1;return}if(t.help||!t.command){he();return}if(t.command==="login"){try{await hi(t)}catch(n){console.error(`contextq: ${n.message}`),process.exitCode=n instanceof S&&n.code==="cancelled"?130:1}return}if(t.command!=="init"){console.error(`contextq: unknown command "${t.command}"`),he(),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(()=>(de(),le));await n({cwd:process.cwd(),workspace:t.workspace,agent:t.agent,runtime:t.port?{port:t.port}:void 0});return}await yi(t)}catch(n){console.error(`contextq: ${n.message}`),process.exitCode=1}}s(_i,"main");_i();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contextq/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.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": {