@inkeep/open-knowledge 0.0.0-dev-20260424071819 → 0.0.0-dev-20260424150009

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 (69) hide show
  1. package/dist/assets/skills/open-knowledge/SKILL.md +228 -0
  2. package/dist/{banner-cxbl_Ely.mjs → banner-CI3--er0.mjs} +2 -2
  3. package/dist/{chokidar-C6yIgAJZ.mjs → chokidar-Z9-8o-jG.mjs} +1 -1
  4. package/dist/cli.mjs +348 -274
  5. package/dist/colors-Cmha5EZJ.mjs +2 -0
  6. package/dist/colors-CxLEz90E.mjs +1 -0
  7. package/dist/constants-QFqPbiZ7.mjs +2 -0
  8. package/dist/{execAsync-DPeftusp.mjs → execAsync-CUbOIvpN.mjs} +1 -1
  9. package/dist/{execAsync-DhDvXMWQ.mjs → execAsync-CjzNx_Wg.mjs} +1 -1
  10. package/dist/{execAsync-RJZP-H-A.mjs → execAsync-xymjJFLm.mjs} +1 -1
  11. package/dist/{getMachineId-bsd-DcLiB1Fb.mjs → getMachineId-bsd-B_JlAoG9.mjs} +2 -2
  12. package/dist/{getMachineId-bsd-B-D2bOSl.mjs → getMachineId-bsd-CMAUw13R.mjs} +2 -2
  13. package/dist/{getMachineId-bsd-Dl-xGqjV.mjs → getMachineId-bsd-DrOrzTD2.mjs} +2 -2
  14. package/dist/{getMachineId-darwin-A7Xy885j.mjs → getMachineId-darwin-BQcf5peQ.mjs} +2 -2
  15. package/dist/{getMachineId-darwin-BQro2RrT.mjs → getMachineId-darwin-DC2qb-xd.mjs} +2 -2
  16. package/dist/{getMachineId-darwin-DPoYqu2E.mjs → getMachineId-darwin-DTGIdpu7.mjs} +2 -2
  17. package/dist/{getMachineId-linux-BwDCbiIf.mjs → getMachineId-linux-C3A6DqVu.mjs} +1 -1
  18. package/dist/{getMachineId-linux-DLyT3DBf.mjs → getMachineId-linux-CMta7vmz.mjs} +1 -1
  19. package/dist/{getMachineId-linux-B6DWkXLm.mjs → getMachineId-linux-nez-pYOw.mjs} +1 -1
  20. package/dist/{getMachineId-unsupported-CnUw0N-q.mjs → getMachineId-unsupported-BrxgPAkN.mjs} +1 -1
  21. package/dist/{getMachineId-unsupported-VfwZ1yF9.mjs → getMachineId-unsupported-gFGrtRr7.mjs} +1 -1
  22. package/dist/{getMachineId-unsupported-CQbaY6vv.mjs → getMachineId-unsupported-j-7WDfTT.mjs} +1 -1
  23. package/dist/{getMachineId-win-Bmzw0GhO.mjs → getMachineId-win-CfF1zVez.mjs} +2 -2
  24. package/dist/{getMachineId-win-DHI6zfnJ.mjs → getMachineId-win-D8Js6D_D.mjs} +2 -2
  25. package/dist/{getMachineId-win-BYymgu0m.mjs → getMachineId-win-DsIEIIis.mjs} +2 -2
  26. package/dist/index.d.mts +0 -7
  27. package/dist/index.mjs +1 -1
  28. package/dist/init-Bw4LWW1o.mjs +1 -0
  29. package/dist/init-D0FupYPY.mjs +1 -0
  30. package/dist/init-D2rQPwU1.mjs +120 -0
  31. package/dist/init-DUIYyxaK.mjs +5 -0
  32. package/dist/{is-object-DVVYT5oa.mjs → is-object-7k010cEq.mjs} +1 -1
  33. package/dist/{keepalive-BqSeN73F.mjs → keepalive-CfaIYv9f.mjs} +1 -1
  34. package/dist/{loader-C-EaZX2e.mjs → loader-C3Wwbx8H.mjs} +2 -2
  35. package/dist/loader-DNeW8gS5.mjs +1 -0
  36. package/dist/{paths-BPoLusOz.mjs → paths-Ba3tNHQr.mjs} +2 -2
  37. package/dist/paths-ORyeejxG.mjs +1 -0
  38. package/dist/preview-BR34uHnQ.mjs +1 -0
  39. package/dist/{preview-T_RVYqtF.mjs → preview-QLsD0vVK.mjs} +2 -2
  40. package/dist/public/assets/McpConsentDialogBody-BpxsNx0A.js +1 -0
  41. package/dist/public/assets/index-Bnp3jcJM.js +27 -0
  42. package/dist/public/assets/index-tR5M38z1.css +1 -0
  43. package/dist/public/index.html +2 -2
  44. package/dist/src-CeiyJxyd.mjs +1 -0
  45. package/dist/{src-DQq7h88V.mjs → src-DIoA15kL.mjs} +25 -8
  46. package/dist/src-lFQj9tkB.mjs +1 -0
  47. package/dist/start-B9LXLMio.mjs +1 -0
  48. package/dist/{start-B84Wdu83.mjs → start-C79L5QTe.mjs} +2 -2
  49. package/dist/{wrapper-DhAZwCvI.mjs → wrapper-BlhyhsXg.mjs} +1 -1
  50. package/package.json +6 -2
  51. package/scripts/postinstall.mjs +53 -0
  52. package/scripts/probe-exec.ts +100 -0
  53. package/scripts/probe-read-document.ts +111 -0
  54. package/dist/colors-CDmFQ1jq.mjs +0 -1
  55. package/dist/colors-CEQEtnHQ.mjs +0 -2
  56. package/dist/constants-CXDXntb9.mjs +0 -2
  57. package/dist/init-BBVwEA2N.mjs +0 -5
  58. package/dist/init-DsXM9hPp.mjs +0 -1
  59. package/dist/init-DvES_bXI.mjs +0 -1
  60. package/dist/init-QQvPB0Au.mjs +0 -230
  61. package/dist/loader-Ck-HQgHI.mjs +0 -1
  62. package/dist/paths-B483looM.mjs +0 -1
  63. package/dist/preview-C5sfPOVh.mjs +0 -1
  64. package/dist/public/assets/McpConsentDialogBody-Dg7gdIrj.js +0 -1
  65. package/dist/public/assets/index-B33JcCrk.js +0 -27
  66. package/dist/public/assets/index-Cj5uVVs3.css +0 -1
  67. package/dist/src-DN3CCqaD.mjs +0 -1
  68. package/dist/src-DuGm387u.mjs +0 -1
  69. package/dist/start-h-f1SVhW.mjs +0 -1
package/dist/cli.mjs CHANGED
@@ -1,19 +1,48 @@
1
1
  #!/usr/bin/env node
2
- import{a as e,i as t}from"./constants-CXDXntb9.mjs";import{O as n,c as r,k as i}from"./src-CB-sXsMW.mjs";import{n as a,t as o}from"./paths-BPoLusOz.mjs";import{ct as s,lt as c,m as l,nt as u,st as d,ut as f}from"./src-DQq7h88V.mjs";import{c as p,f as m,r as h}from"./server-lock-4vgF5yho.mjs";import{i as g}from"./init-QQvPB0Au.mjs";import{t as _}from"./is-object-DVVYT5oa.mjs";import{r as v}from"./init-BBVwEA2N.mjs";import{i as y,n as ee,t as b}from"./loader-C-EaZX2e.mjs";import{o as x,s as S}from"./start-B84Wdu83.mjs";import"./src-DN3CCqaD.mjs";import{Command as C}from"commander";import{appendFileSync as w,closeSync as T,existsSync as E,mkdirSync as D,openSync as te,readFileSync as O,readdirSync as ne,realpathSync as re,statSync as ie,unlinkSync as ae,writeFileSync as oe}from"node:fs";import{homedir as se,hostname as ce}from"node:os";import{basename as le,dirname as ue,isAbsolute as de,join as fe,relative as pe,resolve as k}from"node:path";import{parse as me,stringify as he}from"yaml";import{createOAuthDeviceAuth as ge}from"@octokit/auth-oauth-device";import _e from"@inquirer/password";import{Octokit as ve}from"@octokit/rest";import{fileURLToPath as ye}from"node:url";import{randomUUID as be}from"node:crypto";import{execFileSync as xe,spawn as Se}from"node:child_process";import Ce from"simple-git";import{readFile as we,readdir as Te,stat as Ee}from"node:fs/promises";import{createServer as De,request as Oe}from"node:http";import ke from"picomatch";import{z as A}from"zod";import{McpServer as Ae}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as je}from"@modelcontextprotocol/sdk/server/stdio.js";import{RootsListChangedNotificationSchema as Me}from"@modelcontextprotocol/sdk/types.js";import{AsyncLocalStorage as Ne}from"node:async_hooks";import{Bash as Pe,ReadWriteFs as Fe}from"just-bash";import Ie from"shell-quote";const Le=`open-knowledge`;var Re=class{backend=`keyring`;async get(e){let{Entry:t}=await import(`@napi-rs/keyring`);try{let n=new t(Le,e).getPassword();return n==null?null:JSON.parse(n)}catch{return null}}async set(e,t,n,r){let{Entry:i}=await import(`@napi-rs/keyring`),a=new i(Le,e),o={login:t,token:n,...r};a.setPassword(JSON.stringify(o))}async clear(e){let{Entry:t}=await import(`@napi-rs/keyring`);try{new t(Le,e).deletePassword()}catch{}}},ze=class{backend=`file`;authFile;constructor(e){this.authFile=e??fe(se(),`.open-knowledge`,`auth.yml`)}read(){if(!E(this.authFile))return{};try{return me(O(this.authFile,`utf-8`))??{}}catch(e){let t=e instanceof Error?e.message:`unknown error`;return process.stderr.write(`[auth] Failed to parse ${this.authFile}: ${t}. Starting with empty credentials.\n`),{}}}write(e){let t=ue(this.authFile);E(t)||D(t,{recursive:!0,mode:448}),oe(this.authFile,he(e),{mode:384})}async get(e){return this.read()[e]??null}async set(e,t,n,r){let i=this.read();i[e]={login:t,token:n,...r},this.write(i)}async clear(e){let t=this.read();delete t[e],this.write(t)}};async function Be(e){try{let{Entry:e}=await import(`@napi-rs/keyring`);return new e(Le,`__probe__`),process.stderr.write(`[auth] token storage: OS keychain
3
- `),new Re}catch{return process.stderr.write(`[auth] token storage: file (~/.open-knowledge/auth.yml)
4
- `),new ze(e)}}async function Ve(e,t,n){let r=He(await Ue(e)).host??``;if(!r)return 1;let i=await n.get(r);if(i==null)return 1;let a=e=>e.replace(/[\r\n]/g,``);return t.write(`username=${a(i.login)}\npassword=${a(i.token)}\n`),0}function He(e){let t={};for(let n of e.split(`
5
- `)){let e=n.trim();if(e===``)continue;let r=e.indexOf(`=`);r!==-1&&(t[e.slice(0,r)]=e.slice(r+1))}return t}function Ue(e){return new Promise((t,n)=>{let r=[];e.on(`data`,e=>r.push(e)),e.on(`end`,()=>t(Buffer.concat(r).toString(`utf-8`))),e.on(`error`,n)})}function We(e){let t=new C(`git-credential`);return t.description(`Git credential helper (git credential-helper protocol)`),t.command(`get`).description(`Lookup credentials from TokenStore (called by git)`).action(async()=>{let t=await e(),n=await Ve(process.stdin,process.stdout,t);process.exit(n)}),t}async function Ge(e){let{clientId:t,scopes:n=[`repo`,`read:user`,`user:email`],onVerification:r,host:i}=e,a=i&&i!==`github.com`?`https://${i}/api/v3`:`https://api.github.com`,o=ge({clientType:`oauth-app`,clientId:t,scopes:n,onVerification:async e=>{await r({verificationUri:e.verification_uri,userCode:e.user_code,expiresIn:e.expires_in,interval:e.interval})},request:a===`https://api.github.com`?void 0:(await import(`@octokit/request`)).request.defaults({baseUrl:a})}),s;try{s=await o({type:`oauth`})}catch(e){if(e instanceof Error){let t=e.message.toLowerCase();throw t.includes(`access_denied`)?Error(`Device-flow authorization was denied.`):t.includes(`expired_token`)||t.includes(`timeout`)||t.includes(`timed out`)?Error(`Device-flow code expired before authorization — please try again.`):Error(`GitHub sign-in failed: ${e.message}`)}throw e}return{token:s.token,tokenType:s.tokenType,scopes:s.scopes??[]}}function Ke(e){return process.env.OPEN_KNOWLEDGE_GITHUB_CLIENT_ID??e?.github?.oauthAppClientId??`Ov23liqlSd0V1MwR6rhI`}const qe=new Set([`gitlab.com`,`bitbucket.org`,`codeberg.org`,`gitea.com`,`sr.ht`,`sourcehut.org`]);function Je(e){let t=e.toLowerCase().replace(/:\d+$/,``);qe.has(t)&&(process.stderr.write(`Error: ${e} is not a GitHub host. Only GitHub and GitHub Enterprise Server are supported.\n`),process.exit(1))}function Ye(e,t){e&&process.stdout.write(`${JSON.stringify(t)}\n`)}async function Xe(e,t,n,r=Ge){let i=Ke(n),{host:a,json:o}=e;Je(a),o||process.stderr.write(`Logging in to ${a}…\n`);let s=await r({clientId:i,host:a===`github.com`?void 0:a,onVerification:e=>{e.userCode,e.verificationUri,o?Ye(!0,{type:`verification`,user_code:e.userCode,verification_uri:e.verificationUri,expires_in:e.expiresIn}):process.stderr.write(`Open: ${e.verificationUri}\nEnter code: ${e.userCode}\n`)}}),c=`unknown`,l,u;try{let e=a===`github.com`?`https://api.github.com`:`https://${a}/api/v3`,t=await fetch(`${e}/user`,{headers:{Authorization:`Bearer ${s.token}`,"User-Agent":`open-knowledge-cli`,Accept:`application/vnd.github+json`}});if(t.ok){let e=await t.json();c=e.login??c,l=e.name??void 0,u=e.email??void 0}}catch{}await t.set(a,c,s.token,{gitProtocol:`https`,name:l,email:u}),o?Ye(!0,{type:`complete`,host:a,login:c}):process.stderr.write(`✓ Logged in as ${c} on ${a}\n`)}function Ze(e,t){return new C(`login`).description(`Authenticate with GitHub via Device Flow`).option(`--host <host>`,`GitHub or GitHub Enterprise hostname`,`github.com`).option(`--json`,`Output JSONL progress events`,!1).action(async n=>{await Xe(n,await t(),e())})}async function Qe(e,t,n){let{host:r,json:i}=e;Je(r);let a=await(n??(()=>_e({message:`Enter PAT:`})))();a||(process.stderr.write(`No token provided
6
- `),process.exit(1));let o=r===`github.com`?void 0:`https://${r}/api/v3`,s=new ve({auth:a,...o?{baseUrl:o}:{}}),c=`unknown`,l,u;try{let{data:e}=await s.users.getAuthenticated();c=e.login,l=e.name??void 0,u=e.email??void 0}catch{process.stderr.write(`Token validation failed
7
- `),process.exit(1)}await t.set(r,c,a,{gitProtocol:`https`,name:l,email:u}),i?process.stdout.write(`${JSON.stringify({type:`complete`,host:r,login:c})}\n`):process.stderr.write(`✓ PAT stored for ${c} on ${r}\n`)}function $e(e){return new C(`pat`).description(`Store a Personal Access Token`).option(`--host <host>`,`GitHub or GitHub Enterprise hostname`,`github.com`).option(`--json`,`Output JSON`,!1).action(async t=>{await Qe(t,await e())})}async function et(e,t){let{host:n,json:r}=e;Je(n);let i=await t.get(n);i??(process.stderr.write(`Not logged in to ${n}\n`),process.exit(1));let a=n===`github.com`?void 0:`https://${n}/api/v3`,o=new ve({auth:i.token,...a?{baseUrl:a}:{}}),s=[];for await(let e of o.paginate.iterator(o.repos.listForAuthenticatedUser,{per_page:100,sort:`updated`}))for(let t of e.data)s.push({full_name:t.full_name,clone_url:t.clone_url,private:t.private});if(r)process.stdout.write(`${JSON.stringify({type:`repos`,host:n,repos:s})}\n`);else for(let e of s)process.stdout.write(`${e.full_name} ${e.clone_url}\n`)}function tt(e){return new C(`repos`).description(`List accessible repositories`).option(`--host <host>`,`GitHub or GitHub Enterprise hostname`,`github.com`).option(`--json`,`Output JSON`,!1).action(async t=>{await et(t,await e())})}async function nt(e,t){let{host:n}=e;await t.clear(n),process.stderr.write(`✓ Signed out from ${n}\n`)}function rt(e){return new C(`signout`).description(`Remove stored credentials`).option(`--host <host>`,`GitHub hostname`,`github.com`).action(async t=>{await nt(t,await e())})}async function it(e,t){let{host:n,json:r}=e;Je(n);let i=await t.get(n);i??(r?process.stdout.write(`${JSON.stringify({type:`status`,host:n,authenticated:!1})}\n`):process.stderr.write(`Not logged in to ${n}\n`),process.exit(1));let a=n===`github.com`?void 0:`https://${n}/api/v3`,o=new ve({auth:i.token,...a?{baseUrl:a}:{}});try{let{data:e}=await o.users.getAuthenticated();r?process.stdout.write(`${JSON.stringify({type:`status`,host:n,authenticated:!0,login:e.login,name:e.name,email:e.email})}\n`):process.stderr.write(`✓ Logged in as ${e.login} on ${n}\n`)}catch{r?process.stdout.write(JSON.stringify({type:`status`,host:n,authenticated:!1,error:`token invalid`})+`
8
- `):process.stderr.write(`✗ Token invalid for ${n}\n`),process.exit(1)}}function at(e){return new C(`status`).description(`Show authentication status`).option(`--host <host>`,`GitHub or GitHub Enterprise hostname`,`github.com`).option(`--json`,`Output JSON`,!1).action(async t=>{await it(t,await e())})}function ot(e){let t=new C(`auth`);t.description(`GitHub authentication management`);let n=()=>Be(),r=e??(()=>({}));return t.addCommand(Ze(r,n)),t.addCommand(at(n)),t.addCommand(tt(n)),t.addCommand(rt(n)),t.addCommand($e(n)),t.addCommand(We(n)),t}function st(e,t,n={}){let r=p(e,t);if(!E(r))return{status:`missing`,lockPath:r};let i;try{i=JSON.parse(O(r,`utf-8`))}catch{return{status:`corrupt`,lockPath:r}}if(!i||typeof i!=`object`||typeof i.pid!=`number`)return{status:`corrupt`,lockPath:r};let a=i,o=n.host??ce();return a.hostname===o?(n.isAlive??m)(a.pid)?{status:`alive`,lockPath:r,lock:a}:{status:`dead-pid`,lockPath:r,lock:a}:{status:`foreign-host`,lockPath:r,lock:a}}function ct(e,t){let n=[];for(let[r,i]of[[`server`,e],[`ui`,t]])(i.status===`dead-pid`||i.status===`corrupt`)&&n.push({name:r,lockPath:i.lockPath,reason:i.status});return{prune:n}}function lt(e){let t=e.inspect??(t=>st(e.lockDir,t)),n=e.unlink??(e=>ae(e)),r=e.log??(e=>console.log(e)),i=e.error??(e=>console.error(e)),a=ct(t(`server`),t(`ui`));if(a.prune.length===0)return r(`No stale locks.`),{pruned:[],failed:[]};let o=[],s=[];for(let e of a.prune)try{n(e.lockPath),o.push(e)}catch(t){s.push({target:e,error:t instanceof Error?t.message:String(t)})}if(o.length>0){let e=o.map(e=>`${e.name} (${e.reason})`).join(`, `);r(`Pruned ${o.length} stale lock${o.length===1?``:`s`}: ${e}`)}return s.length>0&&i(`Failed to prune: ${s.map(({target:e,error:t})=>`${e.name} (${e.lockPath}): ${t}`).join(`; `)}`),{pruned:o,failed:s}}function ut(e){return new C(`clean`).description(`Prune stale / corrupt open-knowledge lock files (never touches live locks)`).action(()=>{lt({lockDir:a(o(e(),process.cwd()))}).failed.length>0&&(process.exitCode=1)})}function dt(){try{let e=xe(`gh`,[`auth`,`token`],{encoding:`utf-8`,stdio:[`ignore`,`pipe`,`pipe`],timeout:5e3}).trim();return e.length===0?{available:!1}:{available:!0,token:e}}catch{return{available:!1}}}async function ft(e,t,n={},r=dt){if(!n.skipGhDetect&&r().available)return{tier:`A`,credentialArgs:[`-c`,`credential.helper=!gh auth git-credential`]};let i=await t.get(e);return i==null?{tier:`none`,credentialArgs:[]}:{tier:i.gitProtocol===`ssh`?`C`:`B`,credentialArgs:[`-c`,`credential.helper=!open-knowledge auth git-credential`]}}function pt(e){return e.replace(/:\d+$/,``)}function mt(e){let t=e.trim();if(!t)return null;{let e=/^https?:\/\/([^/?#]+)\/([\w.\-~%]+)\/([\w.\-~%]+?)(?:\.git)?\/?$/.exec(t);if(e)return{protocol:`https`,hostname:pt(e[1]),owner:e[2],name:e[3]}}{let e=/^ssh:\/\/(?:[\w.-]+@)?([^/?#]+)\/([\w.\-~%]+)\/([\w.\-~%]+?)(?:\.git)?\/?$/.exec(t);if(e)return{protocol:`ssh`,hostname:pt(e[1]),owner:e[2],name:e[3]}}{let e=/^git:\/\/([^/?#]+)\/([\w.\-~%]+)\/([\w.\-~%]+?)(?:\.git)?\/?$/.exec(t);if(e)return{protocol:`git`,hostname:pt(e[1]),owner:e[2],name:e[3]}}{let e=/^(?:[\w.-]+@)?([\w.-]+):([\w.\-~%]+)\/([\w.\-~%]+?)(?:\.git)?$/.exec(t);if(e?.[1].includes(`.`)||e&&t.startsWith(`git@`))return{protocol:`ssh`,hostname:e[1],owner:e[2],name:e[3]}}{let e=/^git:([\w.-]+)\/([\w.\-~%]+)\/([\w.\-~%]+?)(?:\.git)?\/?$/.exec(t);if(e)return{protocol:`git`,hostname:e[1],owner:e[2],name:e[3]}}if(!t.includes(`://`)&&!t.includes(`@`)&&!t.startsWith(`/`)){let e=/^([\w.-]+)\/([\w.\-~%]+?)(?:\.git)?$/.exec(t);if(e)return{protocol:`https`,hostname:`github.com`,owner:e[1],name:e[2]}}return null}const ht=[[`count`,0,10],[`compress`,10,20],[`receiv`,20,60],[`resolv`,60,100]];function gt(e){let t=/^([\w ]+):\s+(\d+)%/.exec(e.trim());if(!t)return null;let n=t[1].toLowerCase(),r=Number(t[2]);for(let[e,i,a]of ht)if(n.includes(e))return{stage:t[1],pct:Math.round(i+r/100*(a-i))};return null}function _t(e,t){e&&process.stdout.write(`${JSON.stringify(t)}\n`)}async function vt(e,t,n,r=process.cwd()){let i=mt(e);if(!i)throw Error(`Invalid git URL: ${e}`);let a=t.dir?k(r,t.dir):k(r,i.name);if(E(a)&&ne(a).length>0)throw Error(`Target directory is not empty: ${a}`);let o=await Be(),s=await ft(i.hostname,o,{}),c=Ce({baseDir:r,config:s.credentialArgs.length>=2?[s.credentialArgs[1]]:[],unsafe:{allowUnsafeCredentialHelper:!0}}).env({GIT_TERMINAL_PROMPT:`0`}),l=-1;if(c.outputHandler((e,n,r)=>{r.on(`data`,e=>{let n=e.toString(`utf-8`);for(let e of n.split(`
9
- `)){let n=gt(e);n&&n.pct!==l&&(l=n.pct,_t(t.json,{type:`progress`,pct:n.pct,stage:n.stage}),t.json||process.stderr.write(`\r Cloning… ${n.pct}%`))}})}),await c.clone(e,a,[`--progress`]),t.json||process.stderr.write(`
10
- `),!E(k(a,`.open-knowledge`)))try{let[{runInit:e},{ensureOkGitignoredAtRoot:t}]=await Promise.all([import(`./init-DvES_bXI.mjs`),import(`./init-DsXM9hPp.mjs`)]);await e({cwd:a,mcp:!1});try{t(a)}catch{}}catch{}return a}function yt(e){return new C(`clone`).description(`Clone a git repository and open it`).argument(`<url>`,`Repository URL or owner/repo shorthand`).argument(`[dir]`,`Target directory (default: ./<repo-name>)`).option(`--json`,`Output JSONL progress events`,!1).action(async(t,n,r)=>{let i=e();try{let a=await vt(t,{json:r.json,dir:n},i);if(r.json)_t(!0,{type:`complete`,dir:a});else{process.stderr.write(`✓ Cloned to ${a}\n`),process.chdir(a);let{startCommand:t}=await import(`./start-h-f1SVhW.mjs`);await t(e).parseAsync([],{from:`user`})}}catch(e){let t=e instanceof Error?e.message:String(e);r.json?_t(!0,{type:`error`,message:t}):process.stderr.write(`✗ ${t}\n`),process.exitCode=1}})}const bt=new Ne;var xt=class e{sessionId;corrId;component;constructor(e=`mcp`,t){this.sessionId=t??be().slice(0,12),this.corrId=be().slice(0,8),this.component=e}info(e,t={}){this.emit(`info`,e,t)}warn(e,t={}){this.emit(`warn`,e,t)}error(e,t,n={}){let r=t?{error:t instanceof Error?t.message:String(t),...n}:n;this.emit(`error`,e,r)}debug(e,t={}){(process.env.MCP_DEBUG===`1`||process.env.DEBUG?.includes(`mcp`))&&this.emit(`debug`,e,t)}child(t){return new e(t??this.component,this.sessionId)}asCallback(){return e=>this.info(e)}emit(e,t,n){let r={ts:new Date().toISOString(),level:e,sessionId:this.sessionId,corrId:this.corrId,component:this.component,msg:t,...n},i=`${JSON.stringify(r)}\n`;process.stderr.write(i);let a=process.env.OK_LOG_FILE;if(a)try{w(a,i)}catch(e){console.warn(`[mcp-logger] Failed to write to OK_LOG_FILE: ${e instanceof Error?e.message:e}`)}}};function St(e=`mcp`){return new xt(e)}function Ct(e,t){return bt.run(e,t)}function wt(){return bt.getStore()}const Tt=new Set([`find`,`markdown`,`replace`]),Et=[`backlinks`,`deadLinks`,`documents`,`enrichedPaths`,`entries`,`forwardLinks`,`hints`,`hubs`,`orphans`,`results`],Dt=[`checkpointRef`,`cwd`,`fileCount`,`matchCount`,`ok`,`query`,`stdoutTruncated`,`truncated`];function j(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Ot(e){return j(e)&&`requestId`in e}function kt(e,t){if(Tt.has(e))return{redacted:!0,type:`string`,length:t.length,lines:t.length===0?0:t.split(`
11
- `).length};let n=e===`command`?240:120;return t.length<=n?t:{type:`string`,length:t.length,preview:`${t.slice(0,n)}...`}}function At(e,t){if(t===null)return null;if(typeof t==`string`)return kt(e,t);if(typeof t==`number`||typeof t==`boolean`)return t;if(Array.isArray(t))return{type:`array`,length:t.length};if(j(t)){let e=Object.keys(t).sort();return{type:`object`,keyCount:e.length,keys:e.slice(0,12)}}if(t!==void 0)return{type:typeof t}}function jt(e,t){return j(t)?Object.fromEntries(Object.entries(t).map(([e,t])=>[e,At(e,t)])):At(e,t)}function Mt(e){let t={structuredKeys:Object.keys(e).sort()};for(let n of Dt)n in e&&(t[n]=e[n]);for(let n of Et){let r=e[n];Array.isArray(r)&&(t[`${n}Count`]=r.length)}return`previewUrl`in e&&(t.previewUrl=e.previewUrl??null),`previewUrlSource`in e&&(t.previewUrlSource=e.previewUrlSource),typeof e.stdout==`string`&&(t.stdoutChars=e.stdout.length),Array.isArray(e.warnings)&&(t.warningsCount=e.warnings.length),j(e.warning)&&(t.warning=!0,`previewUrl`in e.warning&&(t.warningPreviewUrl=e.warning.previewUrl??null)),j(e.ui)&&(t.uiBaseUrl=typeof e.ui.baseUrl==`string`||e.ui.baseUrl===null?e.ui.baseUrl:void 0,t.uiPort=typeof e.ui.port==`number`||e.ui.port===null?e.ui.port:void 0),t}function Nt(e){if(!j(e))return{resultType:typeof e};let t={isError:e.isError===!0};return Array.isArray(e.content)&&(t.contentItems=e.content.length,t.contentTextChars=e.content.reduce((e,t)=>j(t)&&typeof t.text==`string`?e+t.text.length:e,0)),j(e.structuredContent)&&Object.assign(t,Mt(e.structuredContent)),t}function Pt(e){return{connectionId:e.connectionId.slice(0,8),displayName:e.displayName,...e.clientInfo?.name?{clientName:e.clientInfo.name}:{},...e.label?{label:e.label}:{}}}function Ft(e){let t=e.at(-1);return Ot(t)?{toolArgs:e.length>1?e[0]:void 0,extra:t}:{toolArgs:e[0],extra:void 0}}function It(e,t,n){let r=n.logger;return r?async(...i)=>{let a=r.child(),o=Date.now(),{toolArgs:s,extra:c}=Ft(i);a.info(`tool start`,{tool:e,...c?.requestId===void 0?{}:{requestId:c.requestId},...c?.sessionId?{transportSessionId:c.sessionId}:{},...n.identityRef?.current?{agent:Pt(n.identityRef.current)}:{},...s===void 0?{}:{args:jt(e,s)}});try{let n=await Ct(a,async()=>await t(...i));return a.info(`tool finish`,{tool:e,...c?.requestId===void 0?{}:{requestId:c.requestId},durationMs:Date.now()-o,result:Nt(n)}),n}catch(t){throw a.error(`tool error`,t,{tool:e,...c?.requestId===void 0?{}:{requestId:c.requestId},durationMs:Date.now()-o}),t}}:t}function Lt(e,t){if(!t.logger)return e;let n=e.tool.bind(e),r=Object.create(e);return r.tool=((...e)=>{let r=String(e[0]),i=e.at(-1);if(typeof i!=`function`)return n(...e);let a=[...e];return a[a.length-1]=It(r,i,t),n(...a)}),r}const M="Absolute host path to resolve the request against. Defaults only when the MCP client advertises exactly one root; otherwise pass `cwd` explicitly.",Rt=A.string().max(200).optional().describe(`Optional one-line user-outcome description (≤80 chars). Appears as a bullet in the timeline.`);function N(e,t){return{content:[{type:`text`,text:e}],...t?{isError:!0}:{}}}function P(e,t,n){return{content:[{type:`text`,text:e}],structuredContent:t,...n?{isError:!0}:{}}}const F="Error: Hocuspocus server is not running. Start it with `open-knowledge start`, then retry.\nFor disk-only writes without real-time sync, use your native Edit tool directly.";async function zt(e,t){return typeof e==`function`?await e(t):e}async function Bt(e,t){return typeof e==`function`?await e(t):e}async function I(e,t,n){let r;try{r=await e(n)}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}try{let e=await Bt(t,r);return{ok:!0,cwd:r,config:e}}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}}async function L(e,t,n,r){let i=await I(e,t,r);if(!i.ok)return i;let{cwd:a,config:o}=i;try{return{ok:!0,cwd:a,config:o,url:await zt(n,a)}}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}}function R(e){let t=e.toLowerCase();return t.endsWith(`.md`)?{ok:!0,docName:e.slice(0,-3)}:t.endsWith(`.mdx`)?{ok:!0,docName:e.slice(0,-4)}:t.endsWith(`.markdown`)?{ok:!1,error:`Error: docName "${e}" ends in ".markdown", which is not a supported extension. Use ".md" or ".mdx", or strip the extension to let the server auto-detect.`}:{ok:!0,docName:e}}async function z(e,t){let n;try{n=await fetch(`${e}${t}`,{signal:AbortSignal.timeout(3e4)})}catch(e){return{ok:!1,error:`Server unreachable: ${e instanceof Error?e.message:e}`}}try{return await n.json()}catch{return{ok:!1,error:`Server returned HTTP ${n.status} with non-JSON body`}}}async function B(e,t,n){let r;try{r=await fetch(`${e}${t}`,{method:`POST`,headers:{"Content-Type":`application/json`},body:n?JSON.stringify(n):void 0,signal:AbortSignal.timeout(3e4)})}catch(e){return{ok:!1,error:`Server unreachable: ${e instanceof Error?e.message:e}`}}try{return await r.json()}catch{return{ok:!1,error:`Server returned HTTP ${r.status} with non-JSON body`}}}function Vt(e,t){return`Promote existing research on this topic into a canonical article inside the project content directory. **Canonical, not provisional** the output is the source of truth for future agents.
2
+ import{i as e,r as t}from"./constants-QFqPbiZ7.mjs";import{O as n,c as r,k as i}from"./src-CB-sXsMW.mjs";import{n as a,t as o}from"./paths-Ba3tNHQr.mjs";import{C as s,_t as c,gt as l,h as u,ht as d,ut as f,vt as p,w as m,x as h}from"./src-DIoA15kL.mjs";import{c as g,f as _,r as v}from"./server-lock-4vgF5yho.mjs";import{i as y,n as b,o as x,r as S,s as C,t as w}from"./colors-Cmha5EZJ.mjs";import{t as T}from"./is-object-7k010cEq.mjs";import{r as E}from"./init-DUIYyxaK.mjs";import{i as D,n as O,t as ee}from"./loader-C3Wwbx8H.mjs";import{o as k,s as te}from"./start-C79L5QTe.mjs";import"./src-CeiyJxyd.mjs";import{Command as A}from"commander";import{appendFileSync as ne,closeSync as re,existsSync as j,mkdirSync as ie,openSync as ae,readFileSync as oe,readdirSync as se,realpathSync as ce,statSync as le,unlinkSync as ue,writeFileSync as de}from"node:fs";import{homedir as fe,hostname as pe}from"node:os";import{basename as me,dirname as he,isAbsolute as ge,join as _e,relative as ve,resolve as M}from"node:path";import{parse as ye,stringify as be}from"yaml";import{createOAuthDeviceAuth as xe}from"@octokit/auth-oauth-device";import Se from"@inquirer/password";import{Octokit as Ce}from"@octokit/rest";import{fileURLToPath as we}from"node:url";import{randomUUID as Te}from"node:crypto";import{execFileSync as Ee,spawn as De}from"node:child_process";import N from"simple-git";import{readFile as Oe,readdir as ke,stat as Ae}from"node:fs/promises";import{createServer as je,request as Me}from"node:http";import Ne from"picomatch";import{z as P}from"zod";import{McpServer as Pe}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as Fe}from"@modelcontextprotocol/sdk/server/stdio.js";import{RootsListChangedNotificationSchema as Ie}from"@modelcontextprotocol/sdk/types.js";import{AsyncLocalStorage as Le}from"node:async_hooks";import{Bash as Re,ReadWriteFs as ze}from"just-bash";import Be from"shell-quote";import{createInterface as Ve}from"node:readline/promises";const He=`open-knowledge`;var Ue=class{backend=`keyring`;async get(e){let{Entry:t}=await import(`@napi-rs/keyring`);try{let n=new t(He,e).getPassword();return n==null?null:JSON.parse(n)}catch{return null}}async set(e,t,n,r){let{Entry:i}=await import(`@napi-rs/keyring`),a=new i(He,e),o={login:t,token:n,...r};a.setPassword(JSON.stringify(o))}async clear(e){let{Entry:t}=await import(`@napi-rs/keyring`);try{new t(He,e).deletePassword()}catch{}}},We=class{backend=`file`;authFile;constructor(e){this.authFile=e??_e(fe(),`.open-knowledge`,`auth.yml`)}read(){if(!j(this.authFile))return{};try{return ye(oe(this.authFile,`utf-8`))??{}}catch(e){let t=e instanceof Error?e.message:`unknown error`;return process.stderr.write(`[auth] Failed to parse ${this.authFile}: ${t}. Starting with empty credentials.\n`),{}}}write(e){let t=he(this.authFile);j(t)||ie(t,{recursive:!0,mode:448}),de(this.authFile,be(e),{mode:384})}async get(e){return this.read()[e]??null}async set(e,t,n,r){let i=this.read();i[e]={login:t,token:n,...r},this.write(i)}async clear(e){let t=this.read();delete t[e],this.write(t)}};async function Ge(e){try{let{Entry:e}=await import(`@napi-rs/keyring`);return new e(He,`__probe__`),process.stderr.write(`[auth] token storage: OS keychain
3
+ `),new Ue}catch{return process.stderr.write(`[auth] token storage: file (~/.open-knowledge/auth.yml)
4
+ `),new We(e)}}async function Ke(e,t,n){let r=qe(await Je(e)).host??``;if(!r)return 1;let i=await n.get(r);if(i==null)return 1;let a=e=>e.replace(/[\r\n]/g,``);return t.write(`username=${a(i.login)}\npassword=${a(i.token)}\n`),0}function qe(e){let t={};for(let n of e.split(`
5
+ `)){let e=n.trim();if(e===``)continue;let r=e.indexOf(`=`);r!==-1&&(t[e.slice(0,r)]=e.slice(r+1))}return t}function Je(e){return new Promise((t,n)=>{let r=[];e.on(`data`,e=>r.push(e)),e.on(`end`,()=>t(Buffer.concat(r).toString(`utf-8`))),e.on(`error`,n)})}function Ye(e){let t=new A(`git-credential`);return t.description(`Git credential helper (git credential-helper protocol)`),t.command(`get`).description(`Lookup credentials from TokenStore (called by git)`).action(async()=>{let t=await e(),n=await Ke(process.stdin,process.stdout,t);process.exit(n)}),t}async function Xe(e){let{clientId:t,scopes:n=[`repo`,`read:user`,`user:email`],onVerification:r,host:i}=e,a=i&&i!==`github.com`?`https://${i}/api/v3`:`https://api.github.com`,o=xe({clientType:`oauth-app`,clientId:t,scopes:n,onVerification:async e=>{await r({verificationUri:e.verification_uri,userCode:e.user_code,expiresIn:e.expires_in,interval:e.interval})},request:a===`https://api.github.com`?void 0:(await import(`@octokit/request`)).request.defaults({baseUrl:a})}),s;try{s=await o({type:`oauth`})}catch(e){if(e instanceof Error){let t=e.message.toLowerCase();throw t.includes(`access_denied`)?Error(`Device-flow authorization was denied.`):t.includes(`expired_token`)||t.includes(`timeout`)||t.includes(`timed out`)?Error(`Device-flow code expired before authorization — please try again.`):Error(`GitHub sign-in failed: ${e.message}`)}throw e}return{token:s.token,tokenType:s.tokenType,scopes:s.scopes??[]}}function Ze(e){return process.env.OPEN_KNOWLEDGE_GITHUB_CLIENT_ID??e?.github?.oauthAppClientId??`Ov23liqlSd0V1MwR6rhI`}const Qe=new Set([`gitlab.com`,`bitbucket.org`,`codeberg.org`,`gitea.com`,`sr.ht`,`sourcehut.org`]);function $e(e){let t=e.toLowerCase().replace(/:\d+$/,``);Qe.has(t)&&(process.stderr.write(`Error: ${e} is not a GitHub host. Only GitHub and GitHub Enterprise Server are supported.\n`),process.exit(1))}function et(e,t){e&&process.stdout.write(`${JSON.stringify(t)}\n`)}async function tt(e,t,n,r=Xe){let i=Ze(n),{host:a,json:o}=e;$e(a),o||process.stderr.write(`Logging in to ${a}…\n`);let s=await r({clientId:i,host:a===`github.com`?void 0:a,onVerification:e=>{e.userCode,e.verificationUri,o?et(!0,{type:`verification`,user_code:e.userCode,verification_uri:e.verificationUri,expires_in:e.expiresIn}):process.stderr.write(`Open: ${e.verificationUri}\nEnter code: ${e.userCode}\n`)}}),c=`unknown`,l,u;try{let e=a===`github.com`?`https://api.github.com`:`https://${a}/api/v3`,t=await fetch(`${e}/user`,{headers:{Authorization:`Bearer ${s.token}`,"User-Agent":`open-knowledge-cli`,Accept:`application/vnd.github+json`}});if(t.ok){let e=await t.json();c=e.login??c,l=e.name??void 0,u=e.email??void 0}}catch{}await t.set(a,c,s.token,{gitProtocol:`https`,name:l,email:u}),o?et(!0,{type:`complete`,host:a,login:c}):process.stderr.write(`✓ Logged in as ${c} on ${a}\n`)}function nt(e,t){return new A(`login`).description(`Authenticate with GitHub via Device Flow`).option(`--host <host>`,`GitHub or GitHub Enterprise hostname`,`github.com`).option(`--json`,`Output JSONL progress events`,!1).action(async n=>{await tt(n,await t(),e())})}async function rt(e,t,n){let{host:r,json:i}=e;$e(r);let a=await(n??(()=>Se({message:`Enter PAT:`})))();a||(process.stderr.write(`No token provided
6
+ `),process.exit(1));let o=r===`github.com`?void 0:`https://${r}/api/v3`,s=new Ce({auth:a,...o?{baseUrl:o}:{}}),c=`unknown`,l,u;try{let{data:e}=await s.users.getAuthenticated();c=e.login,l=e.name??void 0,u=e.email??void 0}catch{process.stderr.write(`Token validation failed
7
+ `),process.exit(1)}await t.set(r,c,a,{gitProtocol:`https`,name:l,email:u}),i?process.stdout.write(`${JSON.stringify({type:`complete`,host:r,login:c})}\n`):process.stderr.write(`✓ PAT stored for ${c} on ${r}\n`)}function it(e){return new A(`pat`).description(`Store a Personal Access Token`).option(`--host <host>`,`GitHub or GitHub Enterprise hostname`,`github.com`).option(`--json`,`Output JSON`,!1).action(async t=>{await rt(t,await e())})}async function at(e,t){let{host:n,json:r}=e;$e(n);let i=await t.get(n);i??(process.stderr.write(`Not logged in to ${n}\n`),process.exit(1));let a=n===`github.com`?void 0:`https://${n}/api/v3`,o=new Ce({auth:i.token,...a?{baseUrl:a}:{}}),s=[];for await(let e of o.paginate.iterator(o.repos.listForAuthenticatedUser,{per_page:100,sort:`updated`}))for(let t of e.data)s.push({full_name:t.full_name,clone_url:t.clone_url,private:t.private});if(r)process.stdout.write(`${JSON.stringify({type:`repos`,host:n,repos:s})}\n`);else for(let e of s)process.stdout.write(`${e.full_name} ${e.clone_url}\n`)}function ot(e){return new A(`repos`).description(`List accessible repositories`).option(`--host <host>`,`GitHub or GitHub Enterprise hostname`,`github.com`).option(`--json`,`Output JSON`,!1).action(async t=>{await at(t,await e())})}async function st(e,t){let{host:n}=e;await t.clear(n),process.stderr.write(`✓ Signed out from ${n}\n`)}function ct(e){return new A(`signout`).description(`Remove stored credentials`).option(`--host <host>`,`GitHub hostname`,`github.com`).action(async t=>{await st(t,await e())})}async function lt(e,t){let{host:n,json:r}=e;$e(n);let i=await t.get(n);i??(r?process.stdout.write(`${JSON.stringify({type:`status`,host:n,authenticated:!1})}\n`):process.stderr.write(`Not logged in to ${n}\n`),process.exit(1));let a=n===`github.com`?void 0:`https://${n}/api/v3`,o=new Ce({auth:i.token,...a?{baseUrl:a}:{}});try{let{data:e}=await o.users.getAuthenticated();r?process.stdout.write(`${JSON.stringify({type:`status`,host:n,authenticated:!0,login:e.login,name:e.name,email:e.email})}\n`):process.stderr.write(`✓ Logged in as ${e.login} on ${n}\n`)}catch{r?process.stdout.write(JSON.stringify({type:`status`,host:n,authenticated:!1,error:`token invalid`})+`
8
+ `):process.stderr.write(`✗ Token invalid for ${n}\n`),process.exit(1)}}function ut(e){return new A(`status`).description(`Show authentication status`).option(`--host <host>`,`GitHub or GitHub Enterprise hostname`,`github.com`).option(`--json`,`Output JSON`,!1).action(async t=>{await lt(t,await e())})}function dt(e){let t=new A(`auth`);t.description(`GitHub authentication management`);let n=()=>Ge(),r=e??(()=>({}));return t.addCommand(nt(r,n)),t.addCommand(ut(n)),t.addCommand(ot(n)),t.addCommand(ct(n)),t.addCommand(it(n)),t.addCommand(Ye(n)),t}function ft(e,t,n={}){let r=g(e,t);if(!j(r))return{status:`missing`,lockPath:r};let i;try{i=JSON.parse(oe(r,`utf-8`))}catch{return{status:`corrupt`,lockPath:r}}if(!i||typeof i!=`object`||typeof i.pid!=`number`)return{status:`corrupt`,lockPath:r};let a=i,o=n.host??pe();return a.hostname===o?(n.isAlive??_)(a.pid)?{status:`alive`,lockPath:r,lock:a}:{status:`dead-pid`,lockPath:r,lock:a}:{status:`foreign-host`,lockPath:r,lock:a}}function pt(e,t){let n=[];for(let[r,i]of[[`server`,e],[`ui`,t]])(i.status===`dead-pid`||i.status===`corrupt`)&&n.push({name:r,lockPath:i.lockPath,reason:i.status});return{prune:n}}function mt(e){let t=e.inspect??(t=>ft(e.lockDir,t)),n=e.unlink??(e=>ue(e)),r=e.log??(e=>console.log(e)),i=e.error??(e=>console.error(e)),a=pt(t(`server`),t(`ui`));if(a.prune.length===0)return r(`No stale locks.`),{pruned:[],failed:[]};let o=[],s=[];for(let e of a.prune)try{n(e.lockPath),o.push(e)}catch(t){s.push({target:e,error:t instanceof Error?t.message:String(t)})}if(o.length>0){let e=o.map(e=>`${e.name} (${e.reason})`).join(`, `);r(`Pruned ${o.length} stale lock${o.length===1?``:`s`}: ${e}`)}return s.length>0&&i(`Failed to prune: ${s.map(({target:e,error:t})=>`${e.name} (${e.lockPath}): ${t}`).join(`; `)}`),{pruned:o,failed:s}}function ht(e){return new A(`clean`).description(`Prune stale / corrupt open-knowledge lock files (never touches live locks)`).action(()=>{mt({lockDir:a(o(e(),process.cwd()))}).failed.length>0&&(process.exitCode=1)})}function gt(){try{let e=Ee(`gh`,[`auth`,`token`],{encoding:`utf-8`,stdio:[`ignore`,`pipe`,`pipe`],timeout:5e3}).trim();return e.length===0?{available:!1}:{available:!0,token:e}}catch{return{available:!1}}}async function _t(e,t,n={},r=gt){if(!n.skipGhDetect&&r().available)return{tier:`A`,credentialArgs:[`-c`,`credential.helper=!gh auth git-credential`]};let i=await t.get(e);return i==null?{tier:`none`,credentialArgs:[]}:{tier:i.gitProtocol===`ssh`?`C`:`B`,credentialArgs:[`-c`,`credential.helper=!open-knowledge auth git-credential`]}}function vt(e){return e.replace(/:\d+$/,``)}function yt(e){let t=e.trim();if(!t)return null;{let e=/^https?:\/\/([^/?#]+)\/([\w.\-~%]+)\/([\w.\-~%]+?)(?:\.git)?\/?$/.exec(t);if(e)return{protocol:`https`,hostname:vt(e[1]),owner:e[2],name:e[3]}}{let e=/^ssh:\/\/(?:[\w.-]+@)?([^/?#]+)\/([\w.\-~%]+)\/([\w.\-~%]+?)(?:\.git)?\/?$/.exec(t);if(e)return{protocol:`ssh`,hostname:vt(e[1]),owner:e[2],name:e[3]}}{let e=/^git:\/\/([^/?#]+)\/([\w.\-~%]+)\/([\w.\-~%]+?)(?:\.git)?\/?$/.exec(t);if(e)return{protocol:`git`,hostname:vt(e[1]),owner:e[2],name:e[3]}}{let e=/^(?:[\w.-]+@)?([\w.-]+):([\w.\-~%]+)\/([\w.\-~%]+?)(?:\.git)?$/.exec(t);if(e?.[1].includes(`.`)||e&&t.startsWith(`git@`))return{protocol:`ssh`,hostname:e[1],owner:e[2],name:e[3]}}{let e=/^git:([\w.-]+)\/([\w.\-~%]+)\/([\w.\-~%]+?)(?:\.git)?\/?$/.exec(t);if(e)return{protocol:`git`,hostname:e[1],owner:e[2],name:e[3]}}if(!t.includes(`://`)&&!t.includes(`@`)&&!t.startsWith(`/`)){let e=/^([\w.-]+)\/([\w.\-~%]+?)(?:\.git)?$/.exec(t);if(e)return{protocol:`https`,hostname:`github.com`,owner:e[1],name:e[2]}}return null}const bt=[[`count`,0,10],[`compress`,10,20],[`receiv`,20,60],[`resolv`,60,100]];function xt(e){let t=/^([\w ]+):\s+(\d+)%/.exec(e.trim());if(!t)return null;let n=t[1].toLowerCase(),r=Number(t[2]);for(let[e,i,a]of bt)if(n.includes(e))return{stage:t[1],pct:Math.round(i+r/100*(a-i))};return null}function St(e,t){e&&process.stdout.write(`${JSON.stringify(t)}\n`)}async function Ct(e,t,n,r=process.cwd()){let i=yt(e);if(!i)throw Error(`Invalid git URL: ${e}`);let a=t.dir?M(r,t.dir):M(r,i.name);if(j(a)&&se(a).length>0)throw Error(`Target directory is not empty: ${a}`);let o=await Ge(),s=await _t(i.hostname,o,{}),c=N({baseDir:r,config:s.credentialArgs.length>=2?[s.credentialArgs[1]]:[],unsafe:{allowUnsafeCredentialHelper:!0}}).env({GIT_TERMINAL_PROMPT:`0`}),l=-1;if(c.outputHandler((e,n,r)=>{r.on(`data`,e=>{let n=e.toString(`utf-8`);for(let e of n.split(`
9
+ `)){let n=xt(e);n&&n.pct!==l&&(l=n.pct,St(t.json,{type:`progress`,pct:n.pct,stage:n.stage}),t.json||process.stderr.write(`\r Cloning… ${n.pct}%`))}})}),await c.clone(e,a,[`--progress`]),t.json||process.stderr.write(`
10
+ `),!j(M(a,`.open-knowledge`)))try{let[{runInit:e},{ensureOkGitignoredAtRoot:t}]=await Promise.all([import(`./init-Bw4LWW1o.mjs`),import(`./init-D0FupYPY.mjs`)]);await e({cwd:a,mcp:!1});try{t(a)}catch{}}catch{}return a}function wt(e){return new A(`clone`).description(`Clone a git repository and open it`).argument(`<url>`,`Repository URL or owner/repo shorthand`).argument(`[dir]`,`Target directory (default: ./<repo-name>)`).option(`--json`,`Output JSONL progress events`,!1).action(async(t,n,r)=>{let i=e();try{let a=await Ct(t,{json:r.json,dir:n},i);if(r.json)St(!0,{type:`complete`,dir:a});else{process.stderr.write(`✓ Cloned to ${a}\n`),process.chdir(a);let{startCommand:t}=await import(`./start-B9LXLMio.mjs`);await t(e).parseAsync([],{from:`user`})}}catch(e){let t=e instanceof Error?e.message:String(e);r.json?St(!0,{type:`error`,message:t}):process.stderr.write(`✗ ${t}\n`),process.exitCode=1}})}const Tt=new Le;var Et=class e{sessionId;corrId;component;constructor(e=`mcp`,t){this.sessionId=t??Te().slice(0,12),this.corrId=Te().slice(0,8),this.component=e}info(e,t={}){this.emit(`info`,e,t)}warn(e,t={}){this.emit(`warn`,e,t)}error(e,t,n={}){let r=t?{error:t instanceof Error?t.message:String(t),...n}:n;this.emit(`error`,e,r)}debug(e,t={}){(process.env.MCP_DEBUG===`1`||process.env.DEBUG?.includes(`mcp`))&&this.emit(`debug`,e,t)}child(t){return new e(t??this.component,this.sessionId)}asCallback(){return e=>this.info(e)}emit(e,t,n){let r={ts:new Date().toISOString(),level:e,sessionId:this.sessionId,corrId:this.corrId,component:this.component,msg:t,...n},i=`${JSON.stringify(r)}\n`;process.stderr.write(i);let a=process.env.OK_LOG_FILE;if(a)try{ne(a,i)}catch(e){console.warn(`[mcp-logger] Failed to write to OK_LOG_FILE: ${e instanceof Error?e.message:e}`)}}};function Dt(e=`mcp`){return new Et(e)}function Ot(e,t){return Tt.run(e,t)}function kt(){return Tt.getStore()}const At=new Set([`find`,`markdown`,`replace`]),jt=[`backlinks`,`deadLinks`,`documents`,`enrichedPaths`,`entries`,`forwardLinks`,`hints`,`hubs`,`orphans`,`results`],Mt=[`checkpointRef`,`cwd`,`fileCount`,`matchCount`,`ok`,`query`,`stdoutTruncated`,`truncated`];function F(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Nt(e){return F(e)&&`requestId`in e}function Pt(e,t){if(At.has(e))return{redacted:!0,type:`string`,length:t.length,lines:t.length===0?0:t.split(`
11
+ `).length};let n=e===`command`?240:120;return t.length<=n?t:{type:`string`,length:t.length,preview:`${t.slice(0,n)}...`}}function Ft(e,t){if(t===null)return null;if(typeof t==`string`)return Pt(e,t);if(typeof t==`number`||typeof t==`boolean`)return t;if(Array.isArray(t))return{type:`array`,length:t.length};if(F(t)){let e=Object.keys(t).sort();return{type:`object`,keyCount:e.length,keys:e.slice(0,12)}}if(t!==void 0)return{type:typeof t}}function It(e,t){return F(t)?Object.fromEntries(Object.entries(t).map(([e,t])=>[e,Ft(e,t)])):Ft(e,t)}function Lt(e){let t={structuredKeys:Object.keys(e).sort()};for(let n of Mt)n in e&&(t[n]=e[n]);for(let n of jt){let r=e[n];Array.isArray(r)&&(t[`${n}Count`]=r.length)}return`previewUrl`in e&&(t.previewUrl=e.previewUrl??null),`previewUrlSource`in e&&(t.previewUrlSource=e.previewUrlSource),typeof e.stdout==`string`&&(t.stdoutChars=e.stdout.length),Array.isArray(e.warnings)&&(t.warningsCount=e.warnings.length),F(e.warning)&&(t.warning=!0,`previewUrl`in e.warning&&(t.warningPreviewUrl=e.warning.previewUrl??null)),F(e.ui)&&(t.uiBaseUrl=typeof e.ui.baseUrl==`string`||e.ui.baseUrl===null?e.ui.baseUrl:void 0,t.uiPort=typeof e.ui.port==`number`||e.ui.port===null?e.ui.port:void 0),t}function Rt(e){if(!F(e))return{resultType:typeof e};let t={isError:e.isError===!0};return Array.isArray(e.content)&&(t.contentItems=e.content.length,t.contentTextChars=e.content.reduce((e,t)=>F(t)&&typeof t.text==`string`?e+t.text.length:e,0)),F(e.structuredContent)&&Object.assign(t,Lt(e.structuredContent)),t}function zt(e){return{connectionId:e.connectionId.slice(0,8),displayName:e.displayName,...e.clientInfo?.name?{clientName:e.clientInfo.name}:{},...e.label?{label:e.label}:{}}}function Bt(e){let t=e.at(-1);return Nt(t)?{toolArgs:e.length>1?e[0]:void 0,extra:t}:{toolArgs:e[0],extra:void 0}}function Vt(e,t,n){let r=n.logger;return r?async(...i)=>{let a=r.child(),o=Date.now(),{toolArgs:s,extra:c}=Bt(i);a.info(`tool start`,{tool:e,...c?.requestId===void 0?{}:{requestId:c.requestId},...c?.sessionId?{transportSessionId:c.sessionId}:{},...n.identityRef?.current?{agent:zt(n.identityRef.current)}:{},...s===void 0?{}:{args:It(e,s)}});try{let n=await Ot(a,async()=>await t(...i));return a.info(`tool finish`,{tool:e,...c?.requestId===void 0?{}:{requestId:c.requestId},durationMs:Date.now()-o,result:Rt(n)}),n}catch(t){throw a.error(`tool error`,t,{tool:e,...c?.requestId===void 0?{}:{requestId:c.requestId},durationMs:Date.now()-o}),t}}:t}function Ht(e,t){if(!t.logger)return e;let n=e.tool.bind(e),r=Object.create(e);return r.tool=((...e)=>{let r=String(e[0]),i=e.at(-1);if(typeof i!=`function`)return n(...e);let a=[...e];return a[a.length-1]=Vt(r,i,t),n(...a)}),r}const I="Absolute host path to resolve the request against. Defaults only when the MCP client advertises exactly one root; otherwise pass `cwd` explicitly.",Ut=P.string().max(200).optional().describe(`Optional one-line user-outcome description (≤80 chars). Appears as a bullet in the timeline.`);function L(e,t){return{content:[{type:`text`,text:e}],...t?{isError:!0}:{}}}function R(e,t,n){return{content:[{type:`text`,text:e}],structuredContent:t,...n?{isError:!0}:{}}}const z="Error: Hocuspocus server is not running. Start it with `open-knowledge start`, then retry.\nFor disk-only writes without real-time sync, use your native Edit tool directly.",Wt={ingest:`raw-sources layer (preserve external material, no analysis)`,research:`wiki layer, provisional (synthesize findings that can still change)`,consolidate:`wiki layer, canonical (promote stabilized research to source-of-truth)`},Gt={ingest:"user shares a URL or file they want preserved, or `research` needs raw sources",research:"`ingest` has captured the relevant sources (or the user points at one)",consolidate:"`research` has produced a provisional article AND a decision has actually been made"},Kt={ingest:"often `research` on the same topic or just stop; raw preservation is frequently enough on its own",research:"usually stop (research lives as provisional indefinitely) or `consolidate` once a decision lands",consolidate:"update 2–3 neighbor docs to link the new canonical article; research articles it supersedes gain a `superseded_by` pointer"};function qt(e){return`## Where this fits
12
+
13
+ Open Knowledge accretes a persistent wiki through three workflow tools, mapped to [Karpathy's three-layer knowledge-base pattern](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f):
14
+
15
+ - **Raw sources** (immutable) — \`ingest\`
16
+ - **Wiki, provisional** — \`research\`
17
+ - **Wiki, canonical** — \`consolidate\`
18
+
19
+ (Project-level folder structure + \`config.yml\` scaffolding is handled by the \`ok seed\` CLI, not by an MCP tool.)
20
+
21
+ **This tool operates in the ${Wt[e]}.**
22
+
23
+ - **Before this:** ${Gt[e]}
24
+ - **After this:** ${Kt[e]}
25
+
26
+ Karpathy's insight: "The tedious part of maintaining a knowledge base is not the reading or the thinking — it's the bookkeeping." Humans abandon wikis because maintenance costs exceed perceived value. These tools exist so an agent can do the bookkeeping (fetching, summarizing, cross-linking, superseding) without fatigue. Follow the steps below faithfully — skipping the cross-linking, supersedes chains, or raw-source preservation is what turns a useful wiki back into an abandoned one.
27
+
28
+ `}async function Jt(e,t){return typeof e==`function`?await e(t):e}async function Yt(e,t){return typeof e==`function`?await e(t):e}async function B(e,t,n){let r;try{r=await e(n)}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}try{let e=await Yt(t,r);return{ok:!0,cwd:r,config:e}}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}}async function V(e,t,n,r){let i=await B(e,t,r);if(!i.ok)return i;let{cwd:a,config:o}=i;try{return{ok:!0,cwd:a,config:o,url:await Jt(n,a)}}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}}function H(e){let t=e.toLowerCase();return t.endsWith(`.md`)?{ok:!0,docName:e.slice(0,-3)}:t.endsWith(`.mdx`)?{ok:!0,docName:e.slice(0,-4)}:t.endsWith(`.markdown`)?{ok:!1,error:`Error: docName "${e}" ends in ".markdown", which is not a supported extension. Use ".md" or ".mdx", or strip the extension to let the server auto-detect.`}:{ok:!0,docName:e}}async function U(e,t){let n;try{n=await fetch(`${e}${t}`,{signal:AbortSignal.timeout(3e4)})}catch(e){return{ok:!1,error:`Server unreachable: ${e instanceof Error?e.message:e}`}}try{return await n.json()}catch{return{ok:!1,error:`Server returned HTTP ${n.status} with non-JSON body`}}}async function Xt(e,t,n){let r;try{r=await fetch(`${e}${t}`,{method:`POST`,headers:{"Content-Type":`application/json`},body:n?JSON.stringify(n):void 0,signal:AbortSignal.timeout(3e4)})}catch(e){return{ok:!1,error:`Server unreachable: ${e instanceof Error?e.message:e}`}}try{return await r.json()}catch{return{ok:!1,error:`Server returned HTTP ${r.status} with non-JSON body`}}}function Zt(e,t){return`${qt(`consolidate`)}Promote existing research on this topic into a canonical article inside the project content directory. **Canonical, not provisional** — the output is the source of truth for future agents.
12
29
 
13
30
  Topic: ${e}
14
31
 
15
32
  The content directory for this project is **\`${t}\`** (from \`${n}/config.yml\`).
16
33
 
34
+ ## STOP gate: has a decision actually been made?
35
+
36
+ Consolidation is **promotion, not creation**. If the team hasn't decided, the resulting "canonical" article lies about the team's state of understanding — future agents read it, act on it, and the false certainty compounds.
37
+
38
+ Before any write, confirm out loud with the user:
39
+
40
+ - **What is the actual decision?** (e.g., "We chose Yjs for CRDT" — not "Yjs is one option")
41
+ - **What alternatives were considered and rejected?** (these go in "Alternatives considered," not as equals)
42
+ - **What's the rationale the team used?** (not your reconstruction from sources)
43
+
44
+ If the decision is still open, **do not consolidate**. Return and tell the user: "The research is still provisional. When the team decides, re-invoke \`consolidate\` with the outcome." Then stop.
45
+
17
46
  ## When to use this workflow
18
47
 
19
48
  - A team has made a decision after research and wants the outcome committed as canonical knowledge
@@ -47,15 +76,9 @@ Locate research articles on this topic:
47
76
 
48
77
  If there is no research to consolidate, stop. Consolidation is promotion, not creation. Run \`research\` first.
49
78
 
50
- ### 2. Confirm the decision
79
+ ### 2. Re-confirm the decision (you already ran the STOP gate above)
51
80
 
52
- Before writing, confirm with the developer:
53
-
54
- - **What is the actual decision?** (e.g., "We chose Yjs for CRDT" — not "Yjs is one option")
55
- - **What alternatives were considered and rejected?** (these get mentioned in trade-offs, not as equal options)
56
- - **What's the rationale the team actually used?** (not your reconstruction from sources — ask if unclear)
57
-
58
- If the decision is not yet made, **do not consolidate**. Return and tell the developer to either (a) make the decision first, or (b) keep the research as provisional.
81
+ You already confirmed the decision at the STOP gate at the top of this workflow. This step is a brief re-check after loading the research in Step 1 — occasionally the research surfaces something that makes the "decision" look less decided than the user initially claimed (e.g., an un-rebutted open question, an alternative they forgot about). If the loaded research reveals that, pause and re-confirm with the user before writing.
59
82
 
60
83
  ### 3. Write the canonical article
61
84
 
@@ -149,35 +172,44 @@ superseded_by: <path-to-new-canonical-article>.md
149
172
  - **Don't delete research articles** — they are the trail; keep them with a \`superseded_by\` marker
150
173
  - **Don't rewrite research prose verbatim** — canonical articles have a different voice (direct, decided) than research (exploratory, provisional)
151
174
  - **Don't skip the supersedes / superseded_by links** — the audit trail matters for future readers
152
-
153
- Full convention: read \`${n}/AGENTS.md\`.`}const Ht=[`Promote research into a canonical article inside the project content directory. Canonical, not provisional the output is the source of truth for future agents.`,``,`**Use when:**`,`- A team has made a decision after research and wants the outcome committed as canonical knowledge`,`- Compacting several provisional research notes into one authoritative article`,`- A developer asks to "consolidate" or "finalize" knowledge on a topic`,``,`**Triggers on:**`,`- "consolidate", "finalize", "promote to canonical", "make this official"`,`- User says the team has decided and wants the outcome written as canonical`,`- Research has stabilized and a destination article is needed`].join(`
154
- `);function Ut(e,t){e.tool(`consolidate`,Ht,{topic:A.string().describe(`The topic to consolidate into a canonical article`),cwd:A.string().optional().describe(M)},async e=>{let n=await I(t.resolveCwd,t.config,e.cwd);return n.ok?P(Vt(e.topic,n.config.content.dir),{previewUrl:null}):N(`Error: ${n.error}`,!0)})}function Wt(e){return e.split(`/`).map(encodeURIComponent).join(`/`)}function Gt(e){return e.endsWith(`/`)?e.slice(0,-1):e}function Kt(e){try{return new URL(e),!0}catch{return!1}}async function V(e,t,n){let r=n??await t.resolveCwd(),i=await Bt(t.config,r),s=o(i,r);return U(e,{config:i,lockDir:a(s),contentDir:s})}function qt(e){try{let t=u(e.lockDir);if(t&&t.port>0)return{baseUrl:`http://localhost:${t.port}`,port:t.port}}catch(t){process.stderr.write(`[preview-url] readUiLock failed at ${e.lockDir} while building ui block: ${t instanceof Error?t.message:String(t)}\n`)}return{baseUrl:null,port:null}}async function H(e,t){let n=t??await e.resolveCwd(),r=await Bt(e.config,n),i=o(r,n),s={config:r,lockDir:a(i),contentDir:i};return{resolve:e=>U(e,s),ui:qt(s)}}function Jt(e){let t=e.toLowerCase();return t.endsWith(`.md`)?e.slice(0,-3):t.endsWith(`.mdx`)?e.slice(0,-4):e}function U(e,t){let n=`/#/${Wt(e)}`;if(process.env.OK_ELECTRON_PROTOCOL_HOST===`1`&&t.contentDir)try{let n=re(t.contentDir);return{url:`openknowledge://open?project=${encodeURIComponent(n)}&doc=${encodeURIComponent(e)}`,source:`electron-protocol`}}catch(e){process.stderr.write(`[preview-url] realpathSync failed for ${t.contentDir}, falling through to http sources: ${e instanceof Error?e.message:String(e)}\n`)}let r=process.env.OPEN_KNOWLEDGE_PREVIEW_BASE_URL;if(r&&Kt(r))return{url:`${Gt(r)}${n}`,source:`env`};try{let e=u(t.lockDir);if(e&&e.port>0)return{url:`http://localhost:${e.port}${n}`,source:`lock`}}catch(e){process.stderr.write(`[preview-url] readUiLock failed at ${t.lockDir}, falling through to config: ${e instanceof Error?e.message:String(e)}\n`)}let i=t.config.preview?.baseUrl;return i&&Kt(i)?{url:`${Gt(i)}${n}`,source:`config`}:null}const Yt=["**IMPORTANT: Before calling this tool, you MUST first call `get_preview_url` and navigate to the returned URL in your preview browser. If `get_preview_url` returns null, start the server first (`open-knowledge start` or `preview_start`), then call `get_preview_url` again. Do NOT call this tool without the preview open. NEVER manually construct the URL.**",``,`[Requires: Hocuspocus server] Find-and-replace on a live document via the CRDT layer.`,`The patch is applied through Hocuspocus and propagated to all connected editors in real-time.`,"Use `offset` when you need to patch an exact occurrence; omit it to preserve first-match behavior.",``,"**When rewriting prose, add `[[wiki-links]]` aggressively.** If the replacement mentions other documents or entities that should have their own page, link them as `[[Page Name]]`. Over-linking is the goal; underlinked documents lose their value in backlink-driven navigation.",``,`**Parameters:**`,"- `docName` — Document name, typically without extension. A trailing `.md` or `.mdx` is stripped automatically.","- `find` — Text to find (exact match)","- `replace` Replacement text","- `offset` (optional) — Exact occurrence to patch, as a JavaScript string offset in the current markdown. If the document changed and the text no longer matches there, the server returns a stale-target error; re-run `suggest_links` to get fresh offsets.",'- `summary` — Optional one-line user-outcome description of this edit (≤80 chars). Appears as a bullet in the document timeline so readers can scan intent without opening every diff. Prefer outcome phrasing ("Fixed token-refresh race") over structural ("Changed 1 line"). Avoid including secrets or PII — summaries are persisted to git history.'].join(`
155
- `);function Xt(e,t){e.tool(`edit_document`,Yt,{docName:A.string().describe(`Document name to edit`),find:A.string().describe(`Text to find (exact match)`),replace:A.string().describe(`Replacement text`),offset:A.number().int().min(0).optional().describe(`Exact occurrence to patch, as a JavaScript string offset in the current markdown`),summary:Rt,cwd:A.string().optional().describe(M)},async e=>{let n=await L(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return N(`Error: ${n.error}`,!0);let{cwd:r,config:i,url:s}=n;if(!s)return N(F,!0);let c=R(e.docName);if(!c.ok)return N(c.error,!0);let l=t.identityRef?.current,u=await B(s,`/api/agent-patch`,{docName:c.docName,find:e.find,replace:e.replace,offset:e.offset,...e.summary===void 0?{}:{summary:e.summary},...l?{agentId:l.connectionId,agentName:l.displayName,clientName:l.clientInfo?.name,colorSeed:l.colorSeed}:{}});if(!u.ok)return N(`Error: ${u.error}`,!0);let d=a(o(i,r)),f=U(c.docName,{config:i,lockDir:d}),p=(typeof u.subscriberCount==`number`?u.subscriberCount:void 0)===0,m=u.summary&&typeof u.summary==`object`?u.summary:void 0,h=typeof m?.hint==`string`?m.hint:void 0,g=[`Edit applied successfully.`];f&&g.push(`Preview: ${f.url}`),p&&g.push(f?`Warning: no preview is currently attached to "${c.docName}". Open ${f.url} to watch future edits live.`:`Warning: no preview is currently attached to "${c.docName}".`),h&&g.push(h);let _=g.join(`
156
- `);if(!f&&!p&&!m)return N(_);let v={};return f&&(v.previewUrl=f.url,v.previewUrlSource=f.source),p&&(v.warning={message:`No preview attached to ${c.docName}.`,previewUrl:f?.url??null}),m&&(v.summary=m),P(_,v)})}const Zt=new Set([`cat`,`ls`,`grep`,`find`]),Qt=/\b[\w./-]+\.(md|mdx)\b/g;function W(e){return/\.(md|mdx)$/.test(e)}function G(e){let t=e.trim();return t?(t=t.replace(/\/+/g,`/`),t.startsWith(`./`)&&(t=t.slice(2)),t.endsWith(`/`)&&(t=t.slice(0,-1)),t):``}function K(e){return e.args.slice(1)}function q(e){return e.filter(e=>!e.startsWith(`-`))}function $t(e){return q(K(e)).filter(W)}function en(e,t){let n=q(K(t)),r=n.length>0?n[n.length-1]:``,i=r&&r!==`.`?G(r):``,a=[];i&&a.push(i);for(let t of e.split(`
157
- `)){let e=t.trim();if(!e||/\.[a-z0-9]+$/i.test(e)&&!W(e))continue;let n=i?`${i}/${e}`:e;a.push(n)}return a}function tn(e){let t=[];for(let n of e.split(`
158
- `)){if(!n)continue;let e=n.indexOf(`:`);if(e<0)continue;let r=G(n.slice(0,e));W(r)&&t.push(r)}return t}function nn(e){let t=[];for(let n of e.split(`
159
- `)){let e=G(n);e&&W(e)&&t.push(e)}return t}function rn(e){return q(K(e)).filter(W)}function an(e){return q(K(e)).length>0}function on(e){let t=[],n=e.matchAll(Qt);for(let e of n)t.push(G(e[0]));return t}function sn(e,t){let n=null;for(let e=t.length-1;e>=0;e--){let r=t[e];if(Zt.has(r.command)){n=r;break}if((r.command===`head`||r.command===`tail`)&&an(r)){n=r;break}}let r;if(!n)r=on(e);else{switch(n.command){case`cat`:r=$t(n);break;case`ls`:r=en(e,n);break;case`grep`:r=tn(e);break;case`find`:r=nn(e);break;case`head`:case`tail`:r=rn(n);break;default:r=on(e)}r.length===0&&(r=on(e))}let i=new Set,a=[];for(let e of r){let t=G(e);!t||i.has(t)||(i.add(t),a.push(t))}return a}function J(e){return e===``?`''`:/^[\w.\-/]+$/.test(e)?e:`'${e.replace(/'/g,`'\\''`)}'`}const cn=16*1024*1024;var ln=class extends Error{limitBytes;actualBytes;partial;constructor(e,t,n){super(`Output exceeded ${e} byte buffer (got ${t}); narrow the command`),this.name=`StdoutOverflowError`,this.limitBytes=e,this.actualBytes=t,this.partial=n}};function un(e){if(!de(e))throw Error(`createBashInstance: cwd must be absolute (got: ${e})`);return new Pe({cwd:`/`,fs:new Fe({root:k(e),allowSymlinks:!1})})}async function dn(e,t){let n=await e.exec(t);if(n.stdout.length>cn)throw new ln(cn,n.stdout.length,{stdout:n.stdout.slice(0,cn),stderr:n.stderr,exitCode:n.exitCode});return{stdout:n.stdout,stderr:n.stderr,exitCode:n.exitCode}}function fn(e){return e.startsWith(`**/`)?e.slice(3):e}async function pn(e,t,n={}){let r=un(t),i=[`-rn`,`-F`];(n.caseInsensitive??!0)&&i.push(`-i`);for(let e of n.include??[])i.push(`--include=${J(fn(e))}`);for(let e of n.exclude??[])i.push(`--exclude=${J(fn(e))}`),i.push(`--exclude-dir=${J(fn(e))}`);let a=n.paths?.length?n.paths.map(J):[`.`],o=`grep ${i.join(` `)} ${J(e)} ${a.join(` `)}`,s;try{s=await dn(r,o)}catch(e){if(e instanceof ln)s=e.partial;else throw e}if(s.exitCode===1&&!s.stdout)return[];if(s.exitCode!==0&&s.exitCode!==1&&!s.stdout)throw Error(`grep exited ${s.exitCode}: ${s.stderr}`);let c=[],l=n.maxResults??1/0;for(let e of s.stdout.split(`
160
- `)){if(!e)continue;if(c.length>=l)break;let t=e.indexOf(`:`);if(t===-1)continue;let n=e.indexOf(`:`,t+1);if(n===-1)continue;let r=e.slice(0,t),i=e.slice(t+1,n),a=e.slice(n+1),o=Number.parseInt(i,10);Number.isFinite(o)&&c.push({path:r,line:o,text:a})}return c}const mn=new Set([`.git`,n,`node_modules`,`.changeset`,`.claude`,`.agents`,`dist`,`build`]);async function hn(e){let t=k(e),n=new Map,r=0,i=!1;async function a(e){if(i)return;let o;try{o=await Te(e,{withFileTypes:!0})}catch{return}for(let s of o){if(i)return;if(s.isDirectory()&&mn.has(s.name))continue;let o=k(e,s.name);if(s.isDirectory()){await a(o);continue}if(s.isFile()){if(r>=1e3){i=!0;return}try{let e=await Ee(o);n.set(pe(t,o),e.mtimeMs),r++}catch{}}}}return await a(t),{snapshot:n,truncated:i}}function gn(e,t){let n=[];for(let[r,i]of t){let t=e.get(r);(t===void 0||t!==i)&&n.push(r)}for(let[r]of e)t.has(r)||n.push(r);return{changed:n}}const _n=[`node_modules`,`.git`,`dist`,`build`,`.next`,`.turbo`,`.nuxt`,`coverage`,`.cache`,`.parcel-cache`,`.vercel`,n,`.claude`];function vn(e){return e===`--recursive`||e===`--dereference-recursive`?!0:e.startsWith(`--`)||!e.startsWith(`-`)?!1:/[rR]/.test(e.slice(1))}const yn=[{command:`grep`,applies:e=>e.slice(1).some(vn),hasUserExcludes:e=>e.some(e=>e===`--exclude-dir`||e.startsWith(`--exclude-dir=`)),buildExcludeArgs:e=>e.map(e=>`--exclude-dir=${e}`),insertionIndex:()=>1},{command:`find`,applies:()=>!0,hasUserExcludes:e=>e.slice(1).some(e=>e===`-not`||e===`!`||e===`-prune`),buildExcludeArgs:e=>{let t=[];for(let n of e)t.push(`-not`,`-path`,`*/${n}/*`);return t},insertionIndex:e=>{for(let t=1;t<e.length;t++)if(e[t].startsWith(`-`))return t;return e.length}}];function bn(e){return e.map(e=>{let t=yn.find(t=>t.command===e.command);if(!t||!t.applies(e.args)||t.hasUserExcludes(e.args))return e;let n=t.buildExcludeArgs(_n),r=t.insertionIndex(e.args);return{command:e.command,args:[...e.args.slice(0,r),...n,...e.args.slice(r)]}})}function xn(e){return e.map(e=>e.args.map(J).join(` `)).join(` | `)}const Sn=new Set([`cat`,`ls`,`grep`,`find`,`head`,`tail`,`wc`,`sort`,`uniq`,`cut`]),Cn=new Set([`>`,`>>`,`<`,`>&`,`<&`,`|&`]),wn=new Set([`&`,`;`,`;;`,`&&`,`||`,`(`,`)`,`<(`,`>(`,`<<`,`<<-`]),Tn=new Set([`-o`,`--output-file`,`--output`]),En=[`-o=`,`--output-file=`,`--output=`],Dn=new Set([`-exec`,`-execdir`,`-delete`,`-fprint`,`-fprintf`,`-fprint0`,`-ok`,`-okdir`]),On=/[`]|\$\(|\$\{|\$'/;function kn(e){return typeof e==`object`&&!!e&&`op`in e}function An(e){let t=typeof e.op==`string`?e.op:`(unknown)`;return Cn.has(t)?{category:`write_blocked`,message:`Write operation blocked: '${t}'. exec is read-only. For document changes, use write_document or edit_document.`}:wn.has(t)?{category:`shell_construct_blocked`,message:`Shell construct '${t}' is not supported. Only pipes (|) are allowed between allowlisted stages.`}:{category:`shell_construct_blocked`,message:`Operator '${t}' is not supported.`}}function jn(e){let t=[];for(let n of e){if(typeof n==`string`){if(On.test(n))return{error:{category:`shell_construct_blocked`,message:`Argument '${n}' contains a shell-injection pattern (backtick, $(), or \${}); not supported.`}};t.push(n);continue}if(!kn(n))return{error:{category:`shell_construct_blocked`,message:`Unrecognized token shape.`}};if(n.op===`glob`&&typeof n.pattern==`string`){t.push(n.pattern);continue}return typeof n.comment==`string`?{error:{category:`shell_construct_blocked`,message:`Comments are not allowed in exec commands.`}}:{error:An(n)}}return{args:t}}function Mn(e){if(!Sn.has(e.command))return{category:`unknown_command`,message:`Command '${e.command}' is not in the allowlist. For pattern matching try 'grep'; for file listing try 'ls' or 'find'. Allowlist: cat, ls, grep, find, head, tail, wc, sort, uniq, cut.`};for(let t of e.args.slice(1)){if(Tn.has(t)||En.some(e=>t.startsWith(e)))return{category:`write_blocked`,message:`Write operation blocked: '${t}'. exec is read-only. For document changes, use write_document or edit_document.`};if(e.command===`find`&&Dn.has(t))return{category:`write_blocked`,message:`find flag '${t}' is blocked (executes commands or deletes files). Use exec for read-only discovery; chain with another allowlisted tool via '|' if you need to transform output.`}}return null}function Nn(e){let t=e.trim();if(!t)return{error:{category:`unknown_command`,message:`Empty command.`}};let n;try{n=Ie.parse(t)}catch{return{error:{category:`shell_construct_blocked`,message:`Failed to parse command — likely malformed quoting or an unsupported construct.`}}}let r=[],i=[];for(let e of n){if(kn(e)&&e.op===`|`){r.push(i),i=[];continue}i.push(e)}r.push(i);let a=[];for(let e of r){let t=jn(e);if(`error`in t)return t;if(t.args.length===0)return{error:{category:`shell_construct_blocked`,message:`Empty pipeline stage (trailing pipe or leading pipe).`}};let n={command:t.args[0],args:t.args},r=Mn(n);if(r)return{error:r};a.push(n)}return{stages:a}}const Pn=/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;function Fn(e,t){let n=e.match(Pn);if(!n)return null;try{let e=me(n[1]);if(_(e)){if(t){let n=t.safeParse(e);return n.success?n.data:null}return e}}catch{}return null}const In=new WeakMap;function Ln(e){let t=In.get(e);if(t)return t;let n=e.map(e=>ke(e.match,{dot:!0}));return In.set(e,n),n}function Rn(e,t){if(e.length===0)return{};let n=Ln(e),r={},i=[],a=!1;for(let o=0;o<e.length;o++){if(!n[o](t))continue;a=!0;let s=e[o].frontmatter;if(s.title!==void 0&&(r.title=s.title),s.description!==void 0&&(r.description=s.description),s.tags!==void 0)for(let e of s.tags)i.includes(e)||i.push(e)}return a?(i.length>0&&(r.tags=i),r):{}}function zn(e){try{return ie(k(e,`.git`)).isDirectory()}catch{return!1}}function Bn(e){return Ce({baseDir:k(e),timeout:{block:5e3}})}async function Vn(e,t,n=5){if(!zn(e))return{commits:[],source:`git-absent`};let r=Bn(e),i=``;try{i=await r.raw(`log`,`-${Math.max(1,n)}`,`--format=%H|%aI|%an|%s`,`--follow`,`--`,t)}catch{return{commits:[],source:`git`}}let a=[];for(let e of i.split(`
161
- `)){if(!e)continue;let t=e.indexOf(`|`);if(t<0)continue;let n=e.indexOf(`|`,t+1);if(n<0)continue;let r=e.indexOf(`|`,n+1);r<0||a.push({hash:e.slice(0,t),date:e.slice(t+1,n),authorName:e.slice(n+1,r),subject:e.slice(r+1)})}return{commits:a,source:`git`}}const Hn=5e3;async function Un(e){try{let t=(await Ce({baseDir:e,timeout:{block:Hn}}).revparse([`--abbrev-ref`,`HEAD`])).trim();return t&&t!==`HEAD`?t:null}catch{return null}}function Wn(e,t){return Ce({baseDir:t,timeout:{block:Hn}}).env({GIT_DIR:e,GIT_WORK_TREE:t})}function Gn(e,t){let n=s(t);return e.startsWith(n)?e.slice(n.length):e}async function Kn(e,t,n,r,i){let a=``;try{a=await e.raw(`log`,t,`-${Math.max(1,i*2)}`,`--format=%H%x00%aI%x00%an%x00%s%x00%B%x1e`,`--`,n)}catch{return[]}let o=Gn(t,r),s=c(o),l=[];for(let e of a.split(``)){let t=e.trimStart();if(!t)continue;let[n=``,i=``,a=``,c=``,u=``]=t.split(`\0`),d=n.trim();d.length===40&&l.push({hash:d,date:i,writerName:a,message:c,contributors:f(u),writerId:o,isAgent:s.isAgent,writerClassification:s.classification,branch:r})}return l}async function qn(e,t,n=5){let r=d(e);if(!r)return{commits:[],source:`shadow-repo-absent`};let i=await Un(e);if(!i)return{commits:[],source:`shadow-repo`};let a=Wn(r,k(e)),o=``;try{o=await a.raw(`for-each-ref`,s(i),`--format=%(refname)`)}catch{return{commits:[],source:`shadow-repo`}}let c=o.split(`
162
- `).map(e=>e.trim()).filter(Boolean);return c.length===0?{commits:[],source:`shadow-repo`}:{commits:(await Promise.all(c.map(e=>Kn(a,e,t,i,n)))).flat().sort((e,t)=>t.date.localeCompare(e.date)).slice(0,n),source:`shadow-repo`}}const Jn=1e3,Yn=new Set([`.git`,n,`node_modules`,`.changeset`,`.claude`,`.agents`,`dist`,`build`]),Xn=/\.(md|mdx)$/i,Zn=A.object({title:A.string().optional(),description:A.string().optional(),tags:A.array(A.string()).default([])});function Qn(e){return e.replace(/\.md$/,``).replace(/\.mdx$/,``)}async function $n(e){try{let t=Fn(await we(e,`utf-8`),Zn);return t?{title:t.title,description:t.description,tags:t.tags??[]}:{tags:[]}}catch{return null}}async function er(e,t){if(!e)return null;let n=await z(e,`/api/backlinks?docName=${encodeURIComponent(t)}`);if(!n.ok)return null;let r=n.backlinks??n.results??n.links;if(!Array.isArray(r))return[];let i=[];for(let e of r){if(typeof e!=`object`||!e)continue;let t=e,n=typeof t.docName==`string`?t.docName:typeof t.source==`string`?t.source:typeof t.page==`string`?t.page:void 0;n&&i.push({source:n,title:typeof t.title==`string`?t.title:void 0,snippet:typeof t.snippet==`string`?t.snippet:null})}return i}async function tr(e,t){if(!e||t.length===0)return null;let n=[...new Set(t)],r=[];for(let e=0;e<n.length;e+=100)r.push(n.slice(e,e+100));let i=await Promise.all(r.map(async t=>{let n=await z(e,`/api/backlink-counts?docNames=${encodeURIComponent(t.join(`,`))}`);return n.ok?n.counts??{}:null})),a=new Map,o=!1;for(let e of i)if(e){o=!0;for(let[t,n]of Object.entries(e))typeof n==`number`&&Number.isFinite(n)&&a.set(t,n)}return o?a:null}async function nr(e,t){if(!e)return null;let n=await z(e,`/api/forward-links?docName=${encodeURIComponent(t)}`);if(!n.ok)return null;let r=n.forwardLinks??n.links??n.results;if(!Array.isArray(r))return[];let i=[];for(let e of r){if(typeof e!=`object`||!e)continue;let t=e;if(t.kind===`external`&&typeof t.url==`string`){i.push({kind:`external`,url:t.url,title:typeof t.title==`string`?t.title:void 0,snippet:typeof t.snippet==`string`?t.snippet:null});continue}let n=typeof t.docName==`string`?t.docName:void 0;n&&i.push({kind:`doc`,docName:n,title:typeof t.title==`string`?t.title:void 0,snippet:typeof t.snippet==`string`?t.snippet:null})}return i}function rr(e,t,n){let r=t??[],i=r.length===0?{}:Rn(r,n),a=e?.title??i.title,o=e?.description??i.description,s=e?.tags??[],c=i.tags??[],l;if(c.length===0)l=s;else{let e=new Set;l=[];for(let t of c)e.has(t)||(e.add(t),l.push(t));for(let t of s)e.has(t)||(e.add(t),l.push(t))}return{title:a,description:o,tags:l}}async function ir(e,t,n={}){let r=e.replace(/^\.\//,``).replace(/^\/+/,``),i=k(t.projectDir,r),a=t.historyDepth??5,o=n.includeRichFields===!0,s=$n(i);if(!o){let e=rr(await s,t.folderRules,r);return{path:r,title:e.title,description:e.description,tags:e.tags,backlinkCount:null,backlinks:null,forwardLinkCount:null,forwardLinks:null,history:null,historySource:null,projectHistory:null,projectHistorySource:null}}let[c,l,u,d,f]=await Promise.all([s,er(t.serverUrl,Qn(r)).catch(()=>null),nr(t.serverUrl,Qn(r)).catch(()=>null),qn(t.projectDir,r,a).catch(()=>({commits:[],source:`shadow-repo`})),Vn(t.projectDir,r,a).catch(()=>({commits:[],source:`git`}))]),p=rr(c,t.folderRules,r);return{path:r,title:p.title,description:p.description,tags:p.tags,backlinkCount:l?.length??null,backlinks:l,forwardLinkCount:u?.length??null,forwardLinks:u,history:d.commits,historySource:d.source,projectHistory:f.commits,projectHistorySource:f.source}}async function ar(e,t){let n={directMdCount:0,recursiveMdCount:0,childDirCount:0,mostRecent:null,truncated:!1},r=0,i=[{path:e,depth:0}];for(;i.length>0;){let e=i.shift();if(!e)break;if(r>=Jn){n.truncated=!0;break}let a;try{a=await Te(e.path,{withFileTypes:!0})}catch{continue}for(let o of a){if(r>=Jn){n.truncated=!0;break}r++;let a=o.name;if(o.isDirectory()){if(Yn.has(a)||a.startsWith(`.`))continue;e.depth===0&&n.childDirCount++,i.push({path:`${e.path}/${a}`,depth:e.depth+1})}else if(o.isFile()&&Xn.test(a)){n.recursiveMdCount++,e.depth===0&&n.directMdCount++;let r=`${e.path}/${a}`;try{let e=await Ee(r);(!n.mostRecent||e.mtimeMs>n.mostRecent.mtimeMs)&&(n.mostRecent={absPath:r,relPath:pe(t,r).split(/[\\/]/).filter(Boolean).join(`/`),mtimeMs:e.mtimeMs})}catch{}}}}return n}async function or(e,t){let n=e.replace(/^\.\//,``).replace(/^\/+/,``).replace(/\/+$/,``),r=await ar(k(t.projectDir,n),t.projectDir),i;if(r.mostRecent){let e=await $n(r.mostRecent.absPath);i={path:r.mostRecent.relPath,title:e?.title??le(r.mostRecent.relPath),updatedAt:new Date(r.mostRecent.mtimeMs).toISOString()}}let a={path:n,type:`directory`,directMdCount:r.directMdCount,recursiveMdCount:r.recursiveMdCount,childDirCount:r.childDirCount,mostRecentMd:i,truncated:r.truncated},o=t.folderRules??[];if(o.length>0){let e=Rn(o,n);e.title!==void 0&&(a.title=e.title),e.description!==void 0&&(a.description=e.description),e.tags!==void 0&&e.tags.length>0&&(a.tags=e.tags)}return a}const sr=50*1024,cr=/\.(png|jpe?g|gif|webp|svg|pdf|zip|tar|gz|tgz|mp4|mov|mp3|wav|ico|bmp)$/i,lr=[`Run a read-only bash-like command against the project content directory. Returns raw stdout plus enriched metadata for every wiki file referenced (frontmatter, backlink/forward-link counts, shadow-repo activity with agent/human attribution).`,``,`Allowlist: cat, ls, grep, find, head, tail, wc, sort, uniq, cut. Pipes (|) work between stages. Redirections, subshells, and writes are rejected.`,``,"cwd: the command runs in the explicit absolute `cwd` you pass, or in the MCP client's only advertised root when there is exactly one. If the client has zero or multiple roots, pass `cwd` explicitly. Paths inside the command resolve relative to that cwd; traversal above it is rejected.",``,"Stdout provenance headers (GNU-style): `ls <dir>/` prepends `<dir>/:`, single-file `cat`/`head`/`tail` prepends `==> <path> <==`, so the subject of the command is visible in raw output. Multi-file `cat a b` emits no header — the `enrichedPaths` array still lists every file. `head`/`tail` used as pipe trimmers (no file arg) defer to the upstream producer.",``,`Examples:`,'- `exec({ command: "cat articles/auth.md" })` — file contents + full enrichment','- `exec({ command: "ls articles/" })` — listing + per-file enrichment (slim)','- `exec({ command: "grep -rn oauth articles/ | head -5" })` — pipe with enrichment on matched files','- `exec({ command: "ls", cwd: "/abs/path/to/other-repo" })` — run in a different project'].join(`
163
- `);function ur(e){let t=e.split(`
164
- `),n=t[t.length-1]===``?t.length-1:t.length;if(n<=500&&e.length<=sr)return{text:e,truncated:!1,omittedLines:0};let r=Math.min(n,500),i=0,a=0;for(let e=0;e<r;e++){let n=t[e];if(i+=n.length+1,i>sr)break;a++}let o=t.slice(0,a).join(`
165
- `),s=n-a;return{text:`${o}\n<truncated: ${s} more lines re-run with a more-specific query>`,truncated:!0,omittedLines:s}}function dr(e){let t=[];for(let n of e)if(n.command===`cat`)for(let e of n.args.slice(1))e.startsWith(`-`)||cr.test(e)&&t.push(e);return t}function fr(e){for(let t=1;t<e.length;t++){let n=e[t],r=n.match(/^--lines=(\d+)$/);if(r)return Number(r[1]);if(n===`--lines`&&t+1<e.length){let n=Number(e[t+1]);if(Number.isFinite(n))return n}if(n===`-n`&&t+1<e.length){let n=Number(e[t+1]);if(Number.isFinite(n))return n}let i=n.match(/^-n(\d+)$/);if(i)return Number(i[1]);let a=n.match(/^-(\d+)$/);if(a)return Number(a[1])}return 10}function pr(e,t){if(e.length<2)return null;let n=e[e.length-1];if(n.command!==`head`&&n.command!==`tail`)return null;let r=fr(n.args),i=t.split(`
166
- `),a=i[i.length-1]===``?i.length-1:i.length;if(a<r)return null;let o=i.slice(0,a),s=new Set(o.map(e=>{let t=e.indexOf(`:`);return t>0?e.slice(0,t):e})).size,c=e.slice(0,-1).map(e=>e.command).join(` | `);return{banner:`Output hit \`${n.command} -${r}\` cap (${a} lines, ${s} unique file${s===1?``:`s`}). The \`${c}\` stage may have had more matches that never reached stdout. For existence checks across many files, prefer \`grep -rl PATTERN <dir>\` (list files only, no head). For enumeration, drop the \`| ${n.command}\` or widen the cap.`}}function mr(e){return e.type===`directory`}function hr(e){let t=[e.title?`**${e.title}** (${e.path}/)`:`**${e.path}/** (directory)`];e.description&&t.push(e.description),e.tags&&e.tags.length>0&&t.push(`tags: ${e.tags.join(`, `)}`);let n=[];return n.push(`${e.recursiveMdCount} md file${e.recursiveMdCount===1?``:`s`}`),e.childDirCount>0&&n.push(`${e.childDirCount} subdir${e.childDirCount===1?``:`s`}`),t.push(n.join(`, `)),e.mostRecentMd&&t.push(`most recent: ${e.mostRecentMd.title??e.mostRecentMd.path} (${e.mostRecentMd.path})`),e.truncated&&t.push(`scan truncated`),`- ${t.join(` — `)}`}function gr(e){let t=[`**${e.title??e.path}** (${e.path})`];if(e.description&&t.push(e.description),e.tags.length>0&&t.push(`tags: ${e.tags.join(`, `)}`),e.backlinkCount!==null&&t.push(`backlinks: ${e.backlinkCount}`),e.forwardLinkCount!==null&&t.push(`forward links: ${e.forwardLinkCount}`),e.history&&e.history.length>0){let n=e.history.map(e=>{let t=e.writerClassification===`agent`?`agent: ${e.writerName}`:e.writerClassification===`principal`?`human: ${e.writerName}`:`${e.writerClassification}: ${e.writerName}`;return`${e.hash.slice(0,7)} [${t}] ${e.message}`});t.push(`OK edits: ${n.join(` · `)}`)}if(e.projectHistory&&e.projectHistory.length>0){let n=e.projectHistory.map(e=>`${e.hash.slice(0,7)} ${e.authorName}: ${e.subject}`);t.push(`commits: ${n.join(` · `)}`)}return`- ${t.join(` — `)}`}function _r(e,t,n){let r=null;for(let t=e.length-1;t>=0;t--){let n=e[t],i=n.command;if(i===`ls`||i===`cat`){r=n;break}if((i===`head`||i===`tail`)&&q(K(n)).length>0){r=n;break}}if(!r)return``;let i=q(K(r));if(r.command===`ls`){let e=i[i.length-1];if(!e||e===`.`)return``;let n=e.replace(/\/+/g,`/`);return n.startsWith(`./`)&&(n=n.slice(2)),n.endsWith(`/`)&&(n=n.slice(0,-1)),!n||!t.has(n)?``:`${n}/:\n`}let a=i.filter(e=>/\.(md|mdx)$/.test(e)&&n.has(e));return a.length===1?`==> ${a[0]} <==\n`:``}function vr(e){if(e.length===0)return``;let t=[``,`### Referenced files`,``];for(let n of e)t.push(mr(n)?hr(n):gr(n));return t.join(`
167
- `)}async function yr(e,t){let n=[],r=[];return await Promise.all(t.map(async t=>{try{let i=await Ee(k(e,t));i.isDirectory()?r.push(t):i.isFile()&&n.push(t)}catch{/\.(md|mdx)$/i.test(t)&&n.push(t)}})),{files:n,dirs:r}}function Y(e,t){return P(t,{enrichedPaths:[],error:{category:e,message:t}},!0)}function br(e,t){return e.map(e=>{let n=t(Jt(e.path));return{...e,previewUrl:n?.url??null,...n?{previewUrlSource:n.source}:{}}})}async function xr(e,t){let n=await L(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return Y(`shell_construct_blocked`,`exec failed: ${n.error}`);let{cwd:r,config:i,url:a}=n,o=Nn(e.command);if(`error`in o)return Y(o.error.category,o.error.message);let s=bn(o.stages),c=xn(s),l=await hn(r),u=un(r),d=``,f=``;try{let e=await dn(u,c);d=e.stdout,f=e.stderr}catch(e){return e instanceof ln?Y(`output_overflow`,`Output exceeded 16 MB buffer. Narrow the command (e.g., add a more specific grep pattern, use head, restrict the path).`):Y(`shell_construct_blocked`,`exec failed: ${e instanceof Error?e.message:String(e)}`)}let p=await hn(r),m=gn(l.snapshot,p.snapshot);if(m.changed.length>0)return Y(`security_invariant_violation`,`Security invariant violated: file(s) in the content directory were modified during a read-only exec call: ${m.changed.join(`, `)}. This indicates a parser bug; the command has been logged.`);let h=ur(d),g=sn(d,s),{files:_,dirs:v}=await yr(r,g),y=s.length===1&&s[0].command===`cat`&&_.length===1,ee=i.folders,b=await Promise.all(_.map(e=>ir(e,{projectDir:r,serverUrl:a,folderRules:ee},{includeRichFields:y}).catch(()=>({path:e,tags:[],backlinkCount:null,backlinks:null,forwardLinkCount:null,forwardLinks:null,history:null,historySource:null,projectHistory:null,projectHistorySource:null})))),x=await Promise.all(v.map(e=>or(e,{projectDir:r,folderRules:ee}).catch(()=>({path:e,type:`directory`,directMdCount:0,recursiveMdCount:0,childDirCount:0,truncated:!1}))));if(!y&&a&&b.length>0){let e=await tr(a,b.map(e=>Qn(e.path))).catch(()=>null);if(e)for(let t of b){let n=e.get(Qn(t.path));typeof n==`number`&&(t.backlinkCount=n)}}let S=new Map(b.map(e=>[e.path,e])),C=new Map(x.map(e=>[e.path,e])),w=[];for(let e of g){let t=S.get(e);if(t){w.push(t);continue}let n=C.get(e);n&&w.push(n)}let T=dr(s),E=[];T.length>0&&E.push(`File${T.length>1?`s`:``} ${T.join(`, `)} appear${T.length===1?`s`:``} to be binary (image/PDF/etc.) exec returns text only (NG8). For binary retrieval, use native Read.`);let D=pr(s,d);D&&E.push(D.banner),f&&E.push(`stderr: ${f.trim()}`);let te=E.length>0?`${E.join(`
168
- `)}\n\n`:``,O=_r(s,C,S)+h.text,ne=`${te}${O}${vr(w)}`,{resolve:re,ui:ie}=await H({config:i,resolveCwd:async()=>r}),ae=br(w,re),oe=ie;return P(ne,{enrichedPaths:ae,stdout:O,stdoutTruncated:h.truncated,cwd:r,...oe?{ui:oe}:{},...E.length>0?{warnings:E}:{}})}function Sr(e,t){e.tool(`exec`,lr,{command:A.string().describe(`Read-only bash command (allowlist: cat, ls, grep, find, head, tail, wc, sort, uniq, cut; pipes OK)`),cwd:A.string().optional().describe("Absolute host path to run the command from. Defaults only when the MCP client advertises exactly one root; otherwise pass `cwd` explicitly.")},async e=>{try{return await xr(e,t)}catch(e){return Y(`shell_construct_blocked`,`exec handler error: ${e instanceof Error?e.message:String(e)}`)}})}const Cr=[`[Requires: Hocuspocus server] Find all pages that link to a given page.`,`Returns source page names, resolved titles, and context snippets as JSON.`,``,`**Parameters:**`,'- `docName` — Target page docName, typically without extension (for example, "articles/project-alpha"). A trailing `.md` or `.mdx` is stripped automatically.'].join(`
169
- `);function wr(e,t){e.tool(`get_backlinks`,Cr,{docName:A.string().describe(`Target page docName`),cwd:A.string().optional().describe(M)},async e=>{let n=await L(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return N(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return N(F,!0);let a=R(e.docName);if(!a.ok)return N(a.error,!0);let o=await z(i,`/api/backlinks?docName=${encodeURIComponent(a.docName)}`);if(!o.ok)return N(`Error: ${o.error}`,!0);let{ok:s,...c}=o,l=c,{resolve:u,ui:d}=await H(t,r),f=(l.backlinks??[]).map(e=>{let t=typeof e.source==`string`?e.source:null,n=t?u(t):null;return{...e,previewUrl:n?.url??null,...n?{previewUrlSource:n.source}:{}}}),p={...l,backlinks:f,ui:d,cwd:r};return P(JSON.stringify(p,null,2),p)})}const Tr=[`[Requires: Hocuspocus server] Find missing internal page targets across the corpus.`,`Returns grouped dead links keyed by missing target with source-doc rows as JSON.`,``,`**Parameters:**`,"- `sourceDocNames` (optional) Referring source docs to narrow the audit with OR semantics"].join(`
170
- `);function Er(e,t){e.tool(`get_dead_links`,Tr,{sourceDocNames:A.array(A.string()).optional().describe(`Referring source docs to narrow the audit with OR semantics`),cwd:A.string().optional().describe(M)},async e=>{let n=await L(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return N(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return N(F,!0);let a=new URLSearchParams;for(let t of e.sourceDocNames??[]){let e=R(t);if(!e.ok)return N(e.error,!0);a.append(`sourceDocName`,e.docName)}let o=a.toString(),s=await z(i,`/api/dead-links${o?`?${o}`:``}`);if(!s.ok)return N(`Error: ${s.error}`,!0);let{ok:c,...l}=s,u=l,{resolve:d,ui:f}=await H(t,r),p=(u.deadLinks??[]).map(e=>{let t=typeof e.target==`string`?e.target:null,n=t?d(t):null,r=(e.sources??[]).map(e=>{let t=typeof e.source==`string`?e.source:null,n=t?d(t):null;return{...e,previewUrl:n?.url??null,...n?{previewUrlSource:n.source}:{}}});return{...e,sources:r,previewUrl:n?.url??null,...n?{previewUrlSource:n.source}:{}}}),m={...u,deadLinks:p,ui:f,cwd:r};return P(JSON.stringify(m,null,2),m)})}const Dr=[`[Requires: Hocuspocus server] Find all pages that a given page links to.`,`Returns forward links as JSON.`,``,`**Parameters:**`,"- `docName` — Source page docName, typically without extension. A trailing `.md` or `.mdx` is stripped automatically."].join(`
171
- `);function Or(e,t){e.tool(`get_forward_links`,Dr,{docName:A.string().describe(`Source page docName`),cwd:A.string().optional().describe(M)},async e=>{let n=await L(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return N(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return N(F,!0);let a=R(e.docName);if(!a.ok)return N(a.error,!0);let o=await z(i,`/api/forward-links?docName=${encodeURIComponent(a.docName)}`);if(!o.ok)return N(`Error: ${o.error}`,!0);let{ok:s,...c}=o,l=c,{resolve:u,ui:d}=await H(t,r),f=(l.forwardLinks??[]).map(e=>{let t=e.kind===`doc`&&typeof e.docName==`string`?e.docName:null,n=t?u(t):null;return{...e,previewUrl:n?.url??null,...n?{previewUrlSource:n.source}:{}}}),p={...l,forwardLinks:f,ui:d,cwd:r};return P(JSON.stringify(p,null,2),p)})}const kr=[`[Requires: Hocuspocus server] List version history for a document.`,`Returns timeline entries from the shadow repo, sorted by timestamp descending.`,"Each entry includes a commit SHA that can be passed to `rollback_to_version`.",``,`**Parameters:**`,"- `docName` — Document name to query history for, typically without extension. A trailing `.md` or `.mdx` is stripped automatically.","- `branch` (optional) — Branch name (default: current branch)","- `limit` (optional) — Maximum entries to return (default 50, max 200)","- `offset` (optional) — Number of entries to skip for pagination (default 0)",'- `type` (optional) — Filter by entry type: "checkpoint", "upstream", or "wip"',"- `author` (optional) — Filter to entries by this author name or email","- `excludeAuthor` (optional) — Exclude entries by this author name or email"].join(`
172
- `);function Ar(e,t){e.tool(`get_history`,kr,{docName:A.string().describe(`Document name to query history for`),branch:A.string().optional().describe(`Branch name (default: current branch)`),limit:A.number().int().min(1).max(200).optional().describe(`Maximum entries to return (default 50, max 200)`),offset:A.number().int().min(0).optional().describe(`Number of entries to skip for pagination (default 0)`),type:A.enum([`checkpoint`,`upstream`,`wip`]).optional().describe(`Filter by entry type`),author:A.string().optional().describe(`Filter to entries by this author name or email`),excludeAuthor:A.string().optional().describe(`Exclude entries by this author name or email`),cwd:A.string().optional().describe(M)},async e=>{let n=await L(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return N(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return N(F,!0);let a=R(e.docName);if(!a.ok)return N(a.error,!0);let o=new URLSearchParams;o.set(`docName`,a.docName),e.branch&&o.set(`branch`,e.branch),e.limit!=null&&o.set(`limit`,String(e.limit)),e.offset!=null&&o.set(`offset`,String(e.offset)),e.type&&o.set(`type`,e.type),e.author&&o.set(`author`,e.author),e.excludeAuthor&&o.set(`excludeAuthor`,e.excludeAuthor);let s=await z(i,`/api/history?${o.toString()}`);if(!s.ok)return N(`Error: ${s.error}`,!0);let{ok:c,...l}=s,u=await V(a.docName,{config:t.config,resolveCwd:t.resolveCwd},r);return P(JSON.stringify(l,null,2),{...l,previewUrl:u?.url??null,...u?{previewUrlSource:u.source}:{}})})}const jr=[`[Requires: Hocuspocus server] Find the most-linked pages in the knowledge graph.`,`Returns hub pages ordered by inbound link count as JSON.`,``,`**Parameters:**`,"- `limit` (optional) — Maximum number of hubs to return (default 20)"].join(`
173
- `);function Mr(e,t){e.tool(`get_hubs`,jr,{limit:A.number().int().positive().optional().describe(`Maximum number of hubs to return`),cwd:A.string().optional().describe(M)},async e=>{let n=await L(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return N(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return N(F,!0);let a=await z(i,`/api/hubs${e.limit?`?limit=${encodeURIComponent(String(e.limit))}`:``}`);if(!a.ok)return N(`Error: ${a.error}`,!0);let{ok:o,...s}=a,c=s,{resolve:l,ui:u}=await H(t,r),d=(c.hubs??[]).map(e=>{let t=typeof e.docName==`string`?e.docName:null,n=t?l(t):null;return{...e,previewUrl:n?.url??null,...n?{previewUrlSource:n.source}:{}}}),f={...c,hubs:d,ui:u,cwd:r};return P(JSON.stringify(f,null,2),f)})}const Nr=[`[Requires: Hocuspocus server] Find disconnected pages in the knowledge graph.`,`Returns orphaned pages as JSON.`,``,`**Parameters:**`,"- `mode` (optional) Orphan lens: `incoming`, `outgoing`, or `both` (default `both`)"].join(`
174
- `);function Pr(e,t){e.tool(`get_orphans`,Nr,{mode:A.enum(i).optional().describe(`Filter which type of graph disconnection to surface`),cwd:A.string().optional().describe(M)},async e=>{let n=await L(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return N(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return N(F,!0);let a=await z(i,`/api/orphans${e.mode?`?mode=${encodeURIComponent(e.mode)}`:``}`);if(!a.ok)return N(`Error: ${a.error}`,!0);let{ok:o,...s}=a,c=s,{resolve:l,ui:u}=await H(t,r),d=(c.orphans??[]).map(e=>{let t=typeof e.docName==`string`?e.docName:null,n=t?l(t):null;return{...e,previewUrl:n?.url??null,...n?{previewUrlSource:n.source}:{}}}),f={...c,orphans:d,ui:u,cwd:r};return P(JSON.stringify(f,null,2),f)})}const Fr=["Return a browser URL for the given wiki docName. Agents should call this IMMEDIATELY BEFORE `write_document` / `edit_document` so they can navigate the preview browser to the doc first and watch the CRDT edit land live.",``,`**Parameters:**`,"- `docName` Wiki doc name, typically without extension.",``,"Returns `{ previewUrl, previewUrlSource }` (source: `env` / `lock` / `config`). When no source is configured, returns `{ previewUrl: null }` and the agent may proceed without navigation."].join(`
175
- `);async function Ir(e,t){let n=R(e.docName);if(!n.ok)return{ok:!1,error:n.error};let r=n.docName,i=await I(t.resolveCwd,t.config,e.cwd);if(!i.ok)return i;let{cwd:s,config:c}=i,u=o(c,s),d;try{d=l({projectDir:s,contentDir:u,includePatterns:c.content.include,excludePatterns:c.content.exclude})}catch(e){return{ok:!1,error:`Cannot evaluate content filter: ${e instanceof Error?e.message:String(e)}`}}if(![`${r}.md`,`${r}.mdx`].some(e=>!d.isExcluded(e)))return{ok:!1,error:`Error: docName "${r}" is not inside content.include globs (${c.content.include.join(`, `)}). This tool only returns URLs for docs that match those globs.`};let f=U(r,{config:c,lockDir:a(u),contentDir:u});return f?{ok:!0,result:{previewUrl:f.url,previewUrlSource:f.source},text:`Preview URL for "${r}" (source: ${f.source}):\n${f.url}`}:{ok:!0,result:{previewUrl:null},text:`No preview URL resolvable for "${r}". The server is likely not running yet. Start it with \`open-knowledge start\` (or \`preview_start\`), then **call \`get_preview_url\` again** — the server writes a lock file that this tool reads to resolve the URL. NEVER guess or manually construct the preview URL. Alternatively, set \`OPEN_KNOWLEDGE_PREVIEW_BASE_URL\` or add \`preview.baseUrl\` to .open-knowledge/config.yml.`}}function Lr(e,t){e.tool(`get_preview_url`,Fr,{docName:A.string().min(1),cwd:A.string().optional().describe(M)},async e=>{let n=await Ir(e,t);return n.ok?P(n.text,n.result):N(n.error,!0)})}function Rr(e,t){return`Capture this external source into the project knowledge base as raw reference material. **Raw preservation only** — no summary, no analysis, no interpretation. Summarizing is the job of the \`research\` tool later.
175
+ `}const Qt=[`Promote research into a canonical article inside the project content directory. Canonical, not provisional — the output is the source of truth for future agents.`,``,`**Use when:**`,`- A team has made a decision after research and wants the outcome committed as canonical knowledge`,`- Compacting several provisional research notes into one authoritative article`,`- A developer asks to "consolidate" or "finalize" knowledge on a topic`,``,`**Triggers on:**`,`- "consolidate", "finalize", "promote to canonical", "make this official"`,`- User says the team has decided and wants the outcome written as canonical`,`- Research has stabilized and a destination article is needed`].join(`
176
+ `);function $t(e,t){e.tool(`consolidate`,Qt,{topic:P.string().describe(`The topic to consolidate into a canonical article`),cwd:P.string().optional().describe(I)},async e=>{let n=await B(t.resolveCwd,t.config,e.cwd);return n.ok?R(Zt(e.topic,n.config.content.dir),{previewUrl:null}):L(`Error: ${n.error}`,!0)})}function en(e){return e.split(`/`).map(encodeURIComponent).join(`/`)}function tn(e){return e.endsWith(`/`)?e.slice(0,-1):e}function nn(e){try{return new URL(e),!0}catch{return!1}}async function W(e,t,n){let r=n??await t.resolveCwd(),i=await Yt(t.config,r),s=o(i,r);return on(e,{config:i,lockDir:a(s),contentDir:s})}function rn(e){try{let t=f(e.lockDir);if(t&&t.port>0)return{baseUrl:`http://localhost:${t.port}`,port:t.port}}catch(t){process.stderr.write(`[preview-url] readUiLock failed at ${e.lockDir} while building ui block: ${t instanceof Error?t.message:String(t)}\n`)}return{baseUrl:null,port:null}}async function G(e,t){let n=t??await e.resolveCwd(),r=await Yt(e.config,n),i=o(r,n),s={config:r,lockDir:a(i),contentDir:i};return{resolve:e=>on(e,s),ui:rn(s)}}function an(e){let t=e.toLowerCase();return t.endsWith(`.md`)?e.slice(0,-3):t.endsWith(`.mdx`)?e.slice(0,-4):e}function on(e,t){let n=`/#/${en(e)}`;if(process.env.OK_ELECTRON_PROTOCOL_HOST===`1`&&t.contentDir)try{let n=ce(t.contentDir);return{url:`openknowledge://open?project=${encodeURIComponent(n)}&doc=${encodeURIComponent(e)}`,source:`electron-protocol`}}catch(e){process.stderr.write(`[preview-url] realpathSync failed for ${t.contentDir}, falling through to http sources: ${e instanceof Error?e.message:String(e)}\n`)}let r=process.env.OPEN_KNOWLEDGE_PREVIEW_BASE_URL;if(r&&nn(r))return{url:`${tn(r)}${n}`,source:`env`};try{let e=f(t.lockDir);if(e&&e.port>0)return{url:`http://localhost:${e.port}${n}`,source:`lock`}}catch(e){process.stderr.write(`[preview-url] readUiLock failed at ${t.lockDir}, falling through to config: ${e instanceof Error?e.message:String(e)}\n`)}let i=t.config.preview?.baseUrl;return i&&nn(i)?{url:`${tn(i)}${n}`,source:`config`}:null}const sn=["**IMPORTANT: Before calling this tool, you MUST first call `get_preview_url` and navigate to the returned URL in your preview browser. If `get_preview_url` returns null, start the server first (`open-knowledge start` or `preview_start`), then call `get_preview_url` again. Do NOT call this tool without the preview open. NEVER manually construct the URL.**",``,`[Requires: Hocuspocus server] Find-and-replace on a live document via the CRDT layer.`,`The patch is applied through Hocuspocus and propagated to all connected editors in real-time.`,"Use `offset` when you need to patch an exact occurrence; omit it to preserve first-match behavior.",``,"**When rewriting prose, add `[[wiki-links]]` aggressively.** If the replacement mentions other documents or entities that should have their own page, link them as `[[Page Name]]`. Over-linking is the goal; underlinked documents lose their value in backlink-driven navigation.",``,`**Parameters:**`,"- `docName` — Document name, typically without extension. A trailing `.md` or `.mdx` is stripped automatically.","- `find` — Text to find (exact match)","- `replace` — Replacement text","- `offset` (optional) — Exact occurrence to patch, as a JavaScript string offset in the current markdown. If the document changed and the text no longer matches there, the server returns a stale-target error; re-run `suggest_links` to get fresh offsets.",'- `summary` — Optional one-line user-outcome description of this edit (≤80 chars). Appears as a bullet in the document timeline so readers can scan intent without opening every diff. Prefer outcome phrasing ("Fixed token-refresh race") over structural ("Changed 1 line"). Avoid including secrets or PII — summaries are persisted to git history.'].join(`
177
+ `);function cn(e,t){e.tool(`edit_document`,sn,{docName:P.string().describe(`Document name to edit`),find:P.string().describe(`Text to find (exact match)`),replace:P.string().describe(`Replacement text`),offset:P.number().int().min(0).optional().describe(`Exact occurrence to patch, as a JavaScript string offset in the current markdown`),summary:Ut,cwd:P.string().optional().describe(I)},async e=>{let n=await V(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return L(`Error: ${n.error}`,!0);let{cwd:r,config:i,url:s}=n;if(!s)return L(z,!0);let c=H(e.docName);if(!c.ok)return L(c.error,!0);let l=t.identityRef?.current,u=await Xt(s,`/api/agent-patch`,{docName:c.docName,find:e.find,replace:e.replace,offset:e.offset,...e.summary===void 0?{}:{summary:e.summary},...l?{agentId:l.connectionId,agentName:l.displayName,clientName:l.clientInfo?.name,colorSeed:l.colorSeed}:{}});if(!u.ok)return L(`Error: ${u.error}`,!0);let d=a(o(i,r)),f=on(c.docName,{config:i,lockDir:d}),p=(typeof u.subscriberCount==`number`?u.subscriberCount:void 0)===0,m=u.summary&&typeof u.summary==`object`?u.summary:void 0,h=typeof m?.hint==`string`?m.hint:void 0,g=[`Edit applied successfully.`];f&&g.push(`Preview: ${f.url}`),p&&g.push(f?`Warning: no preview is currently attached to "${c.docName}". Open ${f.url} to watch future edits live.`:`Warning: no preview is currently attached to "${c.docName}".`),h&&g.push(h);let _=g.join(`
178
+ `);if(!f&&!p&&!m)return L(_);let v={};return f&&(v.previewUrl=f.url,v.previewUrlSource=f.source),p&&(v.warning={message:`No preview attached to ${c.docName}.`,previewUrl:f?.url??null}),m&&(v.summary=m),R(_,v)})}const ln=new Set([`cat`,`ls`,`grep`,`find`]),un=/\b[\w./-]+\.(md|mdx)\b/g;function dn(e){return/\.(md|mdx)$/.test(e)}function fn(e){let t=e.trim();return t?(t=t.replace(/\/+/g,`/`),t.startsWith(`./`)&&(t=t.slice(2)),t.endsWith(`/`)&&(t=t.slice(0,-1)),t):``}function K(e){return e.args.slice(1)}function q(e){return e.filter(e=>!e.startsWith(`-`))}function pn(e){return q(K(e)).filter(dn)}function mn(e,t){let n=q(K(t)),r=n.length>0?n[n.length-1]:``,i=r&&r!==`.`?fn(r):``,a=[];i&&a.push(i);for(let t of e.split(`
179
+ `)){let e=t.trim();if(!e||/\.[a-z0-9]+$/i.test(e)&&!dn(e))continue;let n=i?`${i}/${e}`:e;a.push(n)}return a}function hn(e){let t=[];for(let n of e.split(`
180
+ `)){if(!n)continue;let e=n.indexOf(`:`);if(e<0)continue;let r=fn(n.slice(0,e));dn(r)&&t.push(r)}return t}function gn(e){let t=[];for(let n of e.split(`
181
+ `)){let e=fn(n);e&&dn(e)&&t.push(e)}return t}function _n(e){return q(K(e)).filter(dn)}function vn(e){return q(K(e)).length>0}function yn(e){let t=[],n=e.matchAll(un);for(let e of n)t.push(fn(e[0]));return t}function bn(e,t){let n=null;for(let e=t.length-1;e>=0;e--){let r=t[e];if(ln.has(r.command)){n=r;break}if((r.command===`head`||r.command===`tail`)&&vn(r)){n=r;break}}let r;if(!n)r=yn(e);else{switch(n.command){case`cat`:r=pn(n);break;case`ls`:r=mn(e,n);break;case`grep`:r=hn(e);break;case`find`:r=gn(e);break;case`head`:case`tail`:r=_n(n);break;default:r=yn(e)}r.length===0&&(r=yn(e))}let i=new Set,a=[];for(let e of r){let t=fn(e);!t||i.has(t)||(i.add(t),a.push(t))}return a}function J(e){return e===``?`''`:/^[\w.\-/]+$/.test(e)?e:`'${e.replace(/'/g,`'\\''`)}'`}const xn=16*1024*1024;var Sn=class extends Error{limitBytes;actualBytes;partial;constructor(e,t,n){super(`Output exceeded ${e} byte buffer (got ${t}); narrow the command`),this.name=`StdoutOverflowError`,this.limitBytes=e,this.actualBytes=t,this.partial=n}};function Cn(e){if(!ge(e))throw Error(`createBashInstance: cwd must be absolute (got: ${e})`);return new Re({cwd:`/`,fs:new ze({root:M(e),allowSymlinks:!1})})}async function wn(e,t){let n=await e.exec(t);if(n.stdout.length>xn)throw new Sn(xn,n.stdout.length,{stdout:n.stdout.slice(0,xn),stderr:n.stderr,exitCode:n.exitCode});return{stdout:n.stdout,stderr:n.stderr,exitCode:n.exitCode}}function Tn(e){return e.startsWith(`**/`)?e.slice(3):e}async function En(e,t,n={}){let r=Cn(t),i=[`-rn`,`-F`];(n.caseInsensitive??!0)&&i.push(`-i`);for(let e of n.include??[])i.push(`--include=${J(Tn(e))}`);for(let e of n.exclude??[])i.push(`--exclude=${J(Tn(e))}`),i.push(`--exclude-dir=${J(Tn(e))}`);let a=n.paths?.length?n.paths.map(J):[`.`],o=`grep ${i.join(` `)} ${J(e)} ${a.join(` `)}`,s;try{s=await wn(r,o)}catch(e){if(e instanceof Sn)s=e.partial;else throw e}if(s.exitCode===1&&!s.stdout)return[];if(s.exitCode!==0&&s.exitCode!==1&&!s.stdout)throw Error(`grep exited ${s.exitCode}: ${s.stderr}`);let c=[],l=n.maxResults??1/0;for(let e of s.stdout.split(`
182
+ `)){if(!e)continue;if(c.length>=l)break;let t=e.indexOf(`:`);if(t===-1)continue;let n=e.indexOf(`:`,t+1);if(n===-1)continue;let r=e.slice(0,t),i=e.slice(t+1,n),a=e.slice(n+1),o=Number.parseInt(i,10);Number.isFinite(o)&&c.push({path:r,line:o,text:a})}return c}const Dn=new Set([`.git`,n,`node_modules`,`.changeset`,`.claude`,`.agents`,`dist`,`build`]);async function On(e){let t=M(e),n=new Map,r=0,i=!1;async function a(e){if(i)return;let o;try{o=await ke(e,{withFileTypes:!0})}catch{return}for(let s of o){if(i)return;if(s.isDirectory()&&Dn.has(s.name))continue;let o=M(e,s.name);if(s.isDirectory()){await a(o);continue}if(s.isFile()){if(r>=1e3){i=!0;return}try{let e=await Ae(o);n.set(ve(t,o),e.mtimeMs),r++}catch{}}}}return await a(t),{snapshot:n,truncated:i}}function kn(e,t){let n=[];for(let[r,i]of t){let t=e.get(r);(t===void 0||t!==i)&&n.push(r)}for(let[r]of e)t.has(r)||n.push(r);return{changed:n}}const An=[`node_modules`,`.git`,`dist`,`build`,`.next`,`.turbo`,`.nuxt`,`coverage`,`.cache`,`.parcel-cache`,`.vercel`,n,`.claude`];function jn(e){return e===`--recursive`||e===`--dereference-recursive`?!0:e.startsWith(`--`)||!e.startsWith(`-`)?!1:/[rR]/.test(e.slice(1))}const Mn=[{command:`grep`,applies:e=>e.slice(1).some(jn),hasUserExcludes:e=>e.some(e=>e===`--exclude-dir`||e.startsWith(`--exclude-dir=`)),buildExcludeArgs:e=>e.map(e=>`--exclude-dir=${e}`),insertionIndex:()=>1},{command:`find`,applies:()=>!0,hasUserExcludes:e=>e.slice(1).some(e=>e===`-not`||e===`!`||e===`-prune`),buildExcludeArgs:e=>{let t=[];for(let n of e)t.push(`-not`,`-path`,`*/${n}/*`);return t},insertionIndex:e=>{for(let t=1;t<e.length;t++)if(e[t].startsWith(`-`))return t;return e.length}}];function Nn(e){return e.map(e=>{let t=Mn.find(t=>t.command===e.command);if(!t||!t.applies(e.args)||t.hasUserExcludes(e.args))return e;let n=t.buildExcludeArgs(An),r=t.insertionIndex(e.args);return{command:e.command,args:[...e.args.slice(0,r),...n,...e.args.slice(r)]}})}function Pn(e){return e.map(e=>e.args.map(J).join(` `)).join(` | `)}const Fn=new Set([`cat`,`ls`,`grep`,`find`,`head`,`tail`,`wc`,`sort`,`uniq`,`cut`]),In=new Set([`>`,`>>`,`<`,`>&`,`<&`,`|&`]),Ln=new Set([`&`,`;`,`;;`,`&&`,`||`,`(`,`)`,`<(`,`>(`,`<<`,`<<-`]),Rn=new Set([`-o`,`--output-file`,`--output`]),zn=[`-o=`,`--output-file=`,`--output=`],Bn=new Set([`-exec`,`-execdir`,`-delete`,`-fprint`,`-fprintf`,`-fprint0`,`-ok`,`-okdir`]),Vn=/[`]|\$\(|\$\{|\$'/;function Hn(e){return typeof e==`object`&&!!e&&`op`in e}function Un(e){let t=typeof e.op==`string`?e.op:`(unknown)`;return In.has(t)?{category:`write_blocked`,message:`Write operation blocked: '${t}'. exec is read-only. For document changes, use write_document or edit_document.`}:Ln.has(t)?{category:`shell_construct_blocked`,message:`Shell construct '${t}' is not supported. Only pipes (|) are allowed between allowlisted stages.`}:{category:`shell_construct_blocked`,message:`Operator '${t}' is not supported.`}}function Wn(e){let t=[];for(let n of e){if(typeof n==`string`){if(Vn.test(n))return{error:{category:`shell_construct_blocked`,message:`Argument '${n}' contains a shell-injection pattern (backtick, $(), or \${}); not supported.`}};t.push(n);continue}if(!Hn(n))return{error:{category:`shell_construct_blocked`,message:`Unrecognized token shape.`}};if(n.op===`glob`&&typeof n.pattern==`string`){t.push(n.pattern);continue}return typeof n.comment==`string`?{error:{category:`shell_construct_blocked`,message:`Comments are not allowed in exec commands.`}}:{error:Un(n)}}return{args:t}}function Gn(e){if(!Fn.has(e.command))return{category:`unknown_command`,message:`Command '${e.command}' is not in the allowlist. For pattern matching try 'grep'; for file listing try 'ls' or 'find'. Allowlist: cat, ls, grep, find, head, tail, wc, sort, uniq, cut.`};for(let t of e.args.slice(1)){if(Rn.has(t)||zn.some(e=>t.startsWith(e)))return{category:`write_blocked`,message:`Write operation blocked: '${t}'. exec is read-only. For document changes, use write_document or edit_document.`};if(e.command===`find`&&Bn.has(t))return{category:`write_blocked`,message:`find flag '${t}' is blocked (executes commands or deletes files). Use exec for read-only discovery; chain with another allowlisted tool via '|' if you need to transform output.`}}return null}function Kn(e){let t=e.trim();if(!t)return{error:{category:`unknown_command`,message:`Empty command.`}};let n;try{n=Be.parse(t)}catch{return{error:{category:`shell_construct_blocked`,message:`Failed to parse command — likely malformed quoting or an unsupported construct.`}}}let r=[],i=[];for(let e of n){if(Hn(e)&&e.op===`|`){r.push(i),i=[];continue}i.push(e)}r.push(i);let a=[];for(let e of r){let t=Wn(e);if(`error`in t)return t;if(t.args.length===0)return{error:{category:`shell_construct_blocked`,message:`Empty pipeline stage (trailing pipe or leading pipe).`}};let n={command:t.args[0],args:t.args},r=Gn(n);if(r)return{error:r};a.push(n)}return{stages:a}}const qn=/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;function Jn(e,t){let n=e.match(qn);if(!n)return null;try{let e=ye(n[1]);if(T(e)){if(t){let n=t.safeParse(e);return n.success?n.data:null}return e}}catch{}return null}const Yn=new WeakMap;function Xn(e){let t=Yn.get(e);if(t)return t;let n=e.map(e=>Ne(e.match,{dot:!0}));return Yn.set(e,n),n}function Zn(e,t){if(e.length===0)return{};let n=Xn(e),r={},i=[],a=!1;for(let o=0;o<e.length;o++){if(!n[o](t))continue;a=!0;let s=e[o].frontmatter;if(s.title!==void 0&&(r.title=s.title),s.description!==void 0&&(r.description=s.description),s.tags!==void 0)for(let e of s.tags)i.includes(e)||i.push(e)}return a?(i.length>0&&(r.tags=i),r):{}}function Qn(e){try{return le(M(e,`.git`)).isDirectory()}catch{return!1}}function $n(e){return N({baseDir:M(e),timeout:{block:5e3}})}async function er(e,t,n=5){if(!Qn(e))return{commits:[],source:`git-absent`};let r=$n(e),i=``;try{i=await r.raw(`log`,`-${Math.max(1,n)}`,`--format=%H|%aI|%an|%s`,`--follow`,`--`,t)}catch{return{commits:[],source:`git`}}let a=[];for(let e of i.split(`
183
+ `)){if(!e)continue;let t=e.indexOf(`|`);if(t<0)continue;let n=e.indexOf(`|`,t+1);if(n<0)continue;let r=e.indexOf(`|`,n+1);r<0||a.push({hash:e.slice(0,t),date:e.slice(t+1,n),authorName:e.slice(n+1,r),subject:e.slice(r+1)})}return{commits:a,source:`git`}}const tr=5e3;async function nr(e){try{let t=(await N({baseDir:e,timeout:{block:tr}}).revparse([`--abbrev-ref`,`HEAD`])).trim();return t&&t!==`HEAD`?t:null}catch{return null}}function rr(e,t){return N({baseDir:t,timeout:{block:tr}}).env({GIT_DIR:e,GIT_WORK_TREE:t})}function ir(e,t){let n=l(t);return e.startsWith(n)?e.slice(n.length):e}async function ar(e,t,n,r,i){let a=``;try{a=await e.raw(`log`,t,`-${Math.max(1,i*2)}`,`--format=%H%x00%aI%x00%an%x00%s%x00%B%x1e`,`--`,n)}catch{return[]}let o=ir(t,r),s=c(o),l=[];for(let e of a.split(``)){let t=e.trimStart();if(!t)continue;let[n=``,i=``,a=``,c=``,u=``]=t.split(`\0`),d=n.trim();d.length===40&&l.push({hash:d,date:i,writerName:a,message:c,contributors:p(u),writerId:o,isAgent:s.isAgent,writerClassification:s.classification,branch:r})}return l}async function or(e,t,n=5){let r=d(e);if(!r)return{commits:[],source:`shadow-repo-absent`};let i=await nr(e);if(!i)return{commits:[],source:`shadow-repo`};let a=rr(r,M(e)),o=``;try{o=await a.raw(`for-each-ref`,l(i),`--format=%(refname)`)}catch{return{commits:[],source:`shadow-repo`}}let s=o.split(`
184
+ `).map(e=>e.trim()).filter(Boolean);return s.length===0?{commits:[],source:`shadow-repo`}:{commits:(await Promise.all(s.map(e=>ar(a,e,t,i,n)))).flat().sort((e,t)=>t.date.localeCompare(e.date)).slice(0,n),source:`shadow-repo`}}const sr=1e3,cr=new Set([`.git`,n,`node_modules`,`.changeset`,`.claude`,`.agents`,`dist`,`build`]),lr=/\.(md|mdx)$/i,ur=P.object({title:P.string().optional(),description:P.string().optional(),tags:P.array(P.string()).default([])});function dr(e){return e.replace(/\.md$/,``).replace(/\.mdx$/,``)}async function fr(e){try{let t=Jn(await Oe(e,`utf-8`),ur);return t?{title:t.title,description:t.description,tags:t.tags??[]}:{tags:[]}}catch{return null}}async function pr(e,t){if(!e)return null;let n=await U(e,`/api/backlinks?docName=${encodeURIComponent(t)}`);if(!n.ok)return null;let r=n.backlinks??n.results??n.links;if(!Array.isArray(r))return[];let i=[];for(let e of r){if(typeof e!=`object`||!e)continue;let t=e,n=typeof t.docName==`string`?t.docName:typeof t.source==`string`?t.source:typeof t.page==`string`?t.page:void 0;n&&i.push({source:n,title:typeof t.title==`string`?t.title:void 0,snippet:typeof t.snippet==`string`?t.snippet:null})}return i}async function mr(e,t){if(!e||t.length===0)return null;let n=[...new Set(t)],r=[];for(let e=0;e<n.length;e+=100)r.push(n.slice(e,e+100));let i=await Promise.all(r.map(async t=>{let n=await U(e,`/api/backlink-counts?docNames=${encodeURIComponent(t.join(`,`))}`);return n.ok?n.counts??{}:null})),a=new Map,o=!1;for(let e of i)if(e){o=!0;for(let[t,n]of Object.entries(e))typeof n==`number`&&Number.isFinite(n)&&a.set(t,n)}return o?a:null}async function hr(e,t){if(!e)return null;let n=await U(e,`/api/forward-links?docName=${encodeURIComponent(t)}`);if(!n.ok)return null;let r=n.forwardLinks??n.links??n.results;if(!Array.isArray(r))return[];let i=[];for(let e of r){if(typeof e!=`object`||!e)continue;let t=e;if(t.kind===`external`&&typeof t.url==`string`){i.push({kind:`external`,url:t.url,title:typeof t.title==`string`?t.title:void 0,snippet:typeof t.snippet==`string`?t.snippet:null});continue}let n=typeof t.docName==`string`?t.docName:void 0;n&&i.push({kind:`doc`,docName:n,title:typeof t.title==`string`?t.title:void 0,snippet:typeof t.snippet==`string`?t.snippet:null})}return i}function gr(e,t,n){let r=t??[],i=r.length===0?{}:Zn(r,n),a=e?.title??i.title,o=e?.description??i.description,s=e?.tags??[],c=i.tags??[],l;if(c.length===0)l=s;else{let e=new Set;l=[];for(let t of c)e.has(t)||(e.add(t),l.push(t));for(let t of s)e.has(t)||(e.add(t),l.push(t))}return{title:a,description:o,tags:l}}async function _r(e,t,n={}){let r=e.replace(/^\.\//,``).replace(/^\/+/,``),i=M(t.projectDir,r),a=t.historyDepth??5,o=n.includeRichFields===!0,s=fr(i);if(!o){let e=gr(await s,t.folderRules,r);return{path:r,title:e.title,description:e.description,tags:e.tags,backlinkCount:null,backlinks:null,forwardLinkCount:null,forwardLinks:null,history:null,historySource:null,projectHistory:null,projectHistorySource:null}}let[c,l,u,d,f]=await Promise.all([s,pr(t.serverUrl,dr(r)).catch(()=>null),hr(t.serverUrl,dr(r)).catch(()=>null),or(t.projectDir,r,a).catch(()=>({commits:[],source:`shadow-repo`})),er(t.projectDir,r,a).catch(()=>({commits:[],source:`git`}))]),p=gr(c,t.folderRules,r);return{path:r,title:p.title,description:p.description,tags:p.tags,backlinkCount:l?.length??null,backlinks:l,forwardLinkCount:u?.length??null,forwardLinks:u,history:d.commits,historySource:d.source,projectHistory:f.commits,projectHistorySource:f.source}}async function vr(e,t){let n={directMdCount:0,recursiveMdCount:0,childDirCount:0,mostRecent:null,truncated:!1},r=0,i=[{path:e,depth:0}];for(;i.length>0;){let e=i.shift();if(!e)break;if(r>=sr){n.truncated=!0;break}let a;try{a=await ke(e.path,{withFileTypes:!0})}catch{continue}for(let o of a){if(r>=sr){n.truncated=!0;break}r++;let a=o.name;if(o.isDirectory()){if(cr.has(a)||a.startsWith(`.`))continue;e.depth===0&&n.childDirCount++,i.push({path:`${e.path}/${a}`,depth:e.depth+1})}else if(o.isFile()&&lr.test(a)){n.recursiveMdCount++,e.depth===0&&n.directMdCount++;let r=`${e.path}/${a}`;try{let e=await Ae(r);(!n.mostRecent||e.mtimeMs>n.mostRecent.mtimeMs)&&(n.mostRecent={absPath:r,relPath:ve(t,r).split(/[\\/]/).filter(Boolean).join(`/`),mtimeMs:e.mtimeMs})}catch{}}}}return n}async function yr(e,t){let n=e.replace(/^\.\//,``).replace(/^\/+/,``).replace(/\/+$/,``),r=await vr(M(t.projectDir,n),t.projectDir),i;if(r.mostRecent){let e=await fr(r.mostRecent.absPath);i={path:r.mostRecent.relPath,title:e?.title??me(r.mostRecent.relPath),updatedAt:new Date(r.mostRecent.mtimeMs).toISOString()}}let a={path:n,type:`directory`,directMdCount:r.directMdCount,recursiveMdCount:r.recursiveMdCount,childDirCount:r.childDirCount,mostRecentMd:i,truncated:r.truncated},o=t.folderRules??[];if(o.length>0){let e=Zn(o,n);e.title!==void 0&&(a.title=e.title),e.description!==void 0&&(a.description=e.description),e.tags!==void 0&&e.tags.length>0&&(a.tags=e.tags)}return a}const br=50*1024,xr=/\.(png|jpe?g|gif|webp|svg|pdf|zip|tar|gz|tgz|mp4|mov|mp3|wav|ico|bmp)$/i,Sr=["**STOP — native tools on in-scope markdown.** Do NOT use your host's native `Read`, `Grep`, or `Glob` on `.md` / `.mdx` paths inside OK's content directory — use `exec` (this tool) instead. Native file tools skip frontmatter, backlinks, shadow-repo activity, and project git history that `exec` returns for every matched wiki file. Reserve native `Read`/`Grep`/`Glob` for source code and non-markdown paths only.",``,`Run a read-only bash-like command against the project content directory. Returns raw stdout plus enriched metadata for every wiki file referenced (frontmatter, backlink/forward-link counts, shadow-repo activity with agent/human attribution).`,``,`Allowlist: cat, ls, grep, find, head, tail, wc, sort, uniq, cut. Pipes (|) work between stages. Redirections, subshells, and writes are rejected.`,``,"cwd: the command runs in the explicit absolute `cwd` you pass, or in the MCP client's only advertised root when there is exactly one. If the client has zero or multiple roots, pass `cwd` explicitly. Paths inside the command resolve relative to that cwd; traversal above it is rejected.",``,"Stdout provenance headers (GNU-style): `ls <dir>/` prepends `<dir>/:`, single-file `cat`/`head`/`tail` prepends `==> <path> <==`, so the subject of the command is visible in raw output. Multi-file `cat a b` emits no header — the `enrichedPaths` array still lists every file. `head`/`tail` used as pipe trimmers (no file arg) defer to the upstream producer.",``,`Examples:`,'- `exec({ command: "cat articles/auth.md" })` — file contents + full enrichment','- `exec({ command: "ls articles/" })` — listing + per-file enrichment (slim)','- `exec({ command: "grep -rn oauth articles/ | head -5" })` — pipe with enrichment on matched files','- `exec({ command: "ls", cwd: "/abs/path/to/other-repo" })` — run in a different project'].join(`
185
+ `);function Cr(e){let t=e.split(`
186
+ `),n=t[t.length-1]===``?t.length-1:t.length;if(n<=500&&e.length<=br)return{text:e,truncated:!1,omittedLines:0};let r=Math.min(n,500),i=0,a=0;for(let e=0;e<r;e++){let n=t[e];if(i+=n.length+1,i>br)break;a++}let o=t.slice(0,a).join(`
187
+ `),s=n-a;return{text:`${o}\n<truncated: ${s} more lines — re-run with a more-specific query>`,truncated:!0,omittedLines:s}}function wr(e){let t=[];for(let n of e)if(n.command===`cat`)for(let e of n.args.slice(1))e.startsWith(`-`)||xr.test(e)&&t.push(e);return t}function Tr(e){for(let t=1;t<e.length;t++){let n=e[t],r=n.match(/^--lines=(\d+)$/);if(r)return Number(r[1]);if(n===`--lines`&&t+1<e.length){let n=Number(e[t+1]);if(Number.isFinite(n))return n}if(n===`-n`&&t+1<e.length){let n=Number(e[t+1]);if(Number.isFinite(n))return n}let i=n.match(/^-n(\d+)$/);if(i)return Number(i[1]);let a=n.match(/^-(\d+)$/);if(a)return Number(a[1])}return 10}function Er(e,t){if(e.length<2)return null;let n=e[e.length-1];if(n.command!==`head`&&n.command!==`tail`)return null;let r=Tr(n.args),i=t.split(`
188
+ `),a=i[i.length-1]===``?i.length-1:i.length;if(a<r)return null;let o=i.slice(0,a),s=new Set(o.map(e=>{let t=e.indexOf(`:`);return t>0?e.slice(0,t):e})).size,c=e.slice(0,-1).map(e=>e.command).join(` | `);return{banner:`Output hit \`${n.command} -${r}\` cap (${a} lines, ${s} unique file${s===1?``:`s`}). The \`${c}\` stage may have had more matches that never reached stdout. For existence checks across many files, prefer \`grep -rl PATTERN <dir>\` (list files only, no head). For enumeration, drop the \`| ${n.command}\` or widen the cap.`}}function Dr(e){return e.type===`directory`}function Or(e){let t=[e.title?`**${e.title}** (${e.path}/)`:`**${e.path}/** (directory)`];e.description&&t.push(e.description),e.tags&&e.tags.length>0&&t.push(`tags: ${e.tags.join(`, `)}`);let n=[];return n.push(`${e.recursiveMdCount} md file${e.recursiveMdCount===1?``:`s`}`),e.childDirCount>0&&n.push(`${e.childDirCount} subdir${e.childDirCount===1?``:`s`}`),t.push(n.join(`, `)),e.mostRecentMd&&t.push(`most recent: ${e.mostRecentMd.title??e.mostRecentMd.path} (${e.mostRecentMd.path})`),e.truncated&&t.push(`scan truncated`),`- ${t.join(` — `)}`}function kr(e){let t=[`**${e.title??e.path}** (${e.path})`];if(e.description&&t.push(e.description),e.tags.length>0&&t.push(`tags: ${e.tags.join(`, `)}`),e.backlinkCount!==null&&t.push(`backlinks: ${e.backlinkCount}`),e.forwardLinkCount!==null&&t.push(`forward links: ${e.forwardLinkCount}`),e.history&&e.history.length>0){let n=e.history.map(e=>{let t=e.writerClassification===`agent`?`agent: ${e.writerName}`:e.writerClassification===`principal`?`human: ${e.writerName}`:`${e.writerClassification}: ${e.writerName}`;return`${e.hash.slice(0,7)} [${t}] ${e.message}`});t.push(`OK edits: ${n.join(` · `)}`)}if(e.projectHistory&&e.projectHistory.length>0){let n=e.projectHistory.map(e=>`${e.hash.slice(0,7)} ${e.authorName}: ${e.subject}`);t.push(`commits: ${n.join(` · `)}`)}return`- ${t.join(` — `)}`}function Ar(e,t,n){let r=null;for(let t=e.length-1;t>=0;t--){let n=e[t],i=n.command;if(i===`ls`||i===`cat`){r=n;break}if((i===`head`||i===`tail`)&&q(K(n)).length>0){r=n;break}}if(!r)return``;let i=q(K(r));if(r.command===`ls`){let e=i[i.length-1];if(!e||e===`.`)return``;let n=e.replace(/\/+/g,`/`);return n.startsWith(`./`)&&(n=n.slice(2)),n.endsWith(`/`)&&(n=n.slice(0,-1)),!n||!t.has(n)?``:`${n}/:\n`}let a=i.filter(e=>/\.(md|mdx)$/.test(e)&&n.has(e));return a.length===1?`==> ${a[0]} <==\n`:``}function jr(e){if(e.length===0)return``;let t=[``,`### Referenced files`,``];for(let n of e)t.push(Dr(n)?Or(n):kr(n));return t.join(`
189
+ `)}async function Mr(e,t){let n=[],r=[];return await Promise.all(t.map(async t=>{try{let i=await Ae(M(e,t));i.isDirectory()?r.push(t):i.isFile()&&n.push(t)}catch{/\.(md|mdx)$/i.test(t)&&n.push(t)}})),{files:n,dirs:r}}function Y(e,t){return R(t,{enrichedPaths:[],error:{category:e,message:t}},!0)}function Nr(e,t){return e.map(e=>{let n=t(an(e.path));return{...e,previewUrl:n?.url??null,...n?{previewUrlSource:n.source}:{}}})}async function Pr(e,t){let n=await V(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return Y(`shell_construct_blocked`,`exec failed: ${n.error}`);let{cwd:r,config:i,url:a}=n,o=Kn(e.command);if(`error`in o)return Y(o.error.category,o.error.message);let s=Nn(o.stages),c=Pn(s),l=await On(r),u=Cn(r),d=``,f=``;try{let e=await wn(u,c);d=e.stdout,f=e.stderr}catch(e){return e instanceof Sn?Y(`output_overflow`,`Output exceeded 16 MB buffer. Narrow the command (e.g., add a more specific grep pattern, use head, restrict the path).`):Y(`shell_construct_blocked`,`exec failed: ${e instanceof Error?e.message:String(e)}`)}let p=await On(r),m=kn(l.snapshot,p.snapshot);if(m.changed.length>0)return Y(`security_invariant_violation`,`Security invariant violated: file(s) in the content directory were modified during a read-only exec call: ${m.changed.join(`, `)}. This indicates a parser bug; the command has been logged.`);let h=Cr(d),g=bn(d,s),{files:_,dirs:v}=await Mr(r,g),y=s.length===1&&s[0].command===`cat`&&_.length===1,b=i.folders,x=await Promise.all(_.map(e=>_r(e,{projectDir:r,serverUrl:a,folderRules:b},{includeRichFields:y}).catch(()=>({path:e,tags:[],backlinkCount:null,backlinks:null,forwardLinkCount:null,forwardLinks:null,history:null,historySource:null,projectHistory:null,projectHistorySource:null})))),S=await Promise.all(v.map(e=>yr(e,{projectDir:r,folderRules:b}).catch(()=>({path:e,type:`directory`,directMdCount:0,recursiveMdCount:0,childDirCount:0,truncated:!1}))));if(!y&&a&&x.length>0){let e=await mr(a,x.map(e=>dr(e.path))).catch(()=>null);if(e)for(let t of x){let n=e.get(dr(t.path));typeof n==`number`&&(t.backlinkCount=n)}}let C=new Map(x.map(e=>[e.path,e])),w=new Map(S.map(e=>[e.path,e])),T=[];for(let e of g){let t=C.get(e);if(t){T.push(t);continue}let n=w.get(e);n&&T.push(n)}let E=wr(s),D=[];E.length>0&&D.push(`File${E.length>1?`s`:``} ${E.join(`, `)} appear${E.length===1?`s`:``} to be binary (image/PDF/etc.) exec returns text only (NG8). For binary retrieval, use native Read.`);let O=Er(s,d);O&&D.push(O.banner),f&&D.push(`stderr: ${f.trim()}`);let ee=D.length>0?`${D.join(`
190
+ `)}\n\n`:``,k=Ar(s,w,C)+h.text,te=`${ee}${k}${jr(T)}`,{resolve:A,ui:ne}=await G({config:i,resolveCwd:async()=>r}),re=Nr(T,A),j=ne;return R(te,{enrichedPaths:re,stdout:k,stdoutTruncated:h.truncated,cwd:r,...j?{ui:j}:{},...D.length>0?{warnings:D}:{}})}function Fr(e,t){e.tool(`exec`,Sr,{command:P.string().describe(`Read-only bash command (allowlist: cat, ls, grep, find, head, tail, wc, sort, uniq, cut; pipes OK)`),cwd:P.string().optional().describe("Absolute host path to run the command from. Defaults only when the MCP client advertises exactly one root; otherwise pass `cwd` explicitly.")},async e=>{try{return await Pr(e,t)}catch(e){return Y(`shell_construct_blocked`,`exec handler error: ${e instanceof Error?e.message:String(e)}`)}})}const Ir=[`[Requires: Hocuspocus server] Find all pages that link to a given page.`,`Returns source page names, resolved titles, and context snippets as JSON.`,``,`**Parameters:**`,'- `docName` Target page docName, typically without extension (for example, "articles/project-alpha"). A trailing `.md` or `.mdx` is stripped automatically.'].join(`
191
+ `);function Lr(e,t){e.tool(`get_backlinks`,Ir,{docName:P.string().describe(`Target page docName`),cwd:P.string().optional().describe(I)},async e=>{let n=await V(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return L(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return L(z,!0);let a=H(e.docName);if(!a.ok)return L(a.error,!0);let o=await U(i,`/api/backlinks?docName=${encodeURIComponent(a.docName)}`);if(!o.ok)return L(`Error: ${o.error}`,!0);let{ok:s,...c}=o,l=c,{resolve:u,ui:d}=await G(t,r),f=(l.backlinks??[]).map(e=>{let t=typeof e.source==`string`?e.source:null,n=t?u(t):null;return{...e,previewUrl:n?.url??null,...n?{previewUrlSource:n.source}:{}}}),p={...l,backlinks:f,ui:d,cwd:r};return R(JSON.stringify(p,null,2),p)})}const Rr=[`[Requires: Hocuspocus server] Find missing internal page targets across the corpus.`,`Returns grouped dead links keyed by missing target with source-doc rows as JSON.`,``,`**Parameters:**`,"- `sourceDocNames` (optional) Referring source docs to narrow the audit with OR semantics"].join(`
192
+ `);function zr(e,t){e.tool(`get_dead_links`,Rr,{sourceDocNames:P.array(P.string()).optional().describe(`Referring source docs to narrow the audit with OR semantics`),cwd:P.string().optional().describe(I)},async e=>{let n=await V(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return L(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return L(z,!0);let a=new URLSearchParams;for(let t of e.sourceDocNames??[]){let e=H(t);if(!e.ok)return L(e.error,!0);a.append(`sourceDocName`,e.docName)}let o=a.toString(),s=await U(i,`/api/dead-links${o?`?${o}`:``}`);if(!s.ok)return L(`Error: ${s.error}`,!0);let{ok:c,...l}=s,u=l,{resolve:d,ui:f}=await G(t,r),p=(u.deadLinks??[]).map(e=>{let t=typeof e.target==`string`?e.target:null,n=t?d(t):null,r=(e.sources??[]).map(e=>{let t=typeof e.source==`string`?e.source:null,n=t?d(t):null;return{...e,previewUrl:n?.url??null,...n?{previewUrlSource:n.source}:{}}});return{...e,sources:r,previewUrl:n?.url??null,...n?{previewUrlSource:n.source}:{}}}),m={...u,deadLinks:p,ui:f,cwd:r};return R(JSON.stringify(m,null,2),m)})}const Br=[`[Requires: Hocuspocus server] Find all pages that a given page links to.`,`Returns forward links as JSON.`,``,`**Parameters:**`,"- `docName` — Source page docName, typically without extension. A trailing `.md` or `.mdx` is stripped automatically."].join(`
193
+ `);function Vr(e,t){e.tool(`get_forward_links`,Br,{docName:P.string().describe(`Source page docName`),cwd:P.string().optional().describe(I)},async e=>{let n=await V(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return L(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return L(z,!0);let a=H(e.docName);if(!a.ok)return L(a.error,!0);let o=await U(i,`/api/forward-links?docName=${encodeURIComponent(a.docName)}`);if(!o.ok)return L(`Error: ${o.error}`,!0);let{ok:s,...c}=o,l=c,{resolve:u,ui:d}=await G(t,r),f=(l.forwardLinks??[]).map(e=>{let t=e.kind===`doc`&&typeof e.docName==`string`?e.docName:null,n=t?u(t):null;return{...e,previewUrl:n?.url??null,...n?{previewUrlSource:n.source}:{}}}),p={...l,forwardLinks:f,ui:d,cwd:r};return R(JSON.stringify(p,null,2),p)})}const Hr=[`[Requires: Hocuspocus server] List version history for a document.`,`Returns timeline entries from the shadow repo, sorted by timestamp descending.`,"Each entry includes a commit SHA that can be passed to `rollback_to_version`.",``,`**Parameters:**`,"- `docName` — Document name to query history for, typically without extension. A trailing `.md` or `.mdx` is stripped automatically.","- `branch` (optional) — Branch name (default: current branch)","- `limit` (optional) — Maximum entries to return (default 50, max 200)","- `offset` (optional) — Number of entries to skip for pagination (default 0)",'- `type` (optional) — Filter by entry type: "checkpoint", "upstream", or "wip"',"- `author` (optional) — Filter to entries by this author name or email","- `excludeAuthor` (optional) — Exclude entries by this author name or email"].join(`
194
+ `);function Ur(e,t){e.tool(`get_history`,Hr,{docName:P.string().describe(`Document name to query history for`),branch:P.string().optional().describe(`Branch name (default: current branch)`),limit:P.number().int().min(1).max(200).optional().describe(`Maximum entries to return (default 50, max 200)`),offset:P.number().int().min(0).optional().describe(`Number of entries to skip for pagination (default 0)`),type:P.enum([`checkpoint`,`upstream`,`wip`]).optional().describe(`Filter by entry type`),author:P.string().optional().describe(`Filter to entries by this author name or email`),excludeAuthor:P.string().optional().describe(`Exclude entries by this author name or email`),cwd:P.string().optional().describe(I)},async e=>{let n=await V(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return L(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return L(z,!0);let a=H(e.docName);if(!a.ok)return L(a.error,!0);let o=new URLSearchParams;o.set(`docName`,a.docName),e.branch&&o.set(`branch`,e.branch),e.limit!=null&&o.set(`limit`,String(e.limit)),e.offset!=null&&o.set(`offset`,String(e.offset)),e.type&&o.set(`type`,e.type),e.author&&o.set(`author`,e.author),e.excludeAuthor&&o.set(`excludeAuthor`,e.excludeAuthor);let s=await U(i,`/api/history?${o.toString()}`);if(!s.ok)return L(`Error: ${s.error}`,!0);let{ok:c,...l}=s,u=await W(a.docName,{config:t.config,resolveCwd:t.resolveCwd},r);return R(JSON.stringify(l,null,2),{...l,previewUrl:u?.url??null,...u?{previewUrlSource:u.source}:{}})})}const Wr=[`[Requires: Hocuspocus server] Find the most-linked pages in the knowledge graph.`,`Returns hub pages ordered by inbound link count as JSON.`,``,`**Parameters:**`,"- `limit` (optional) — Maximum number of hubs to return (default 20)"].join(`
195
+ `);function Gr(e,t){e.tool(`get_hubs`,Wr,{limit:P.number().int().positive().optional().describe(`Maximum number of hubs to return`),cwd:P.string().optional().describe(I)},async e=>{let n=await V(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return L(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return L(z,!0);let a=await U(i,`/api/hubs${e.limit?`?limit=${encodeURIComponent(String(e.limit))}`:``}`);if(!a.ok)return L(`Error: ${a.error}`,!0);let{ok:o,...s}=a,c=s,{resolve:l,ui:u}=await G(t,r),d=(c.hubs??[]).map(e=>{let t=typeof e.docName==`string`?e.docName:null,n=t?l(t):null;return{...e,previewUrl:n?.url??null,...n?{previewUrlSource:n.source}:{}}}),f={...c,hubs:d,ui:u,cwd:r};return R(JSON.stringify(f,null,2),f)})}const Kr=[`[Requires: Hocuspocus server] Find disconnected pages in the knowledge graph.`,`Returns orphaned pages as JSON.`,``,`**Parameters:**`,"- `mode` (optional) — Orphan lens: `incoming`, `outgoing`, or `both` (default `both`)"].join(`
196
+ `);function qr(e,t){e.tool(`get_orphans`,Kr,{mode:P.enum(i).optional().describe(`Filter which type of graph disconnection to surface`),cwd:P.string().optional().describe(I)},async e=>{let n=await V(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return L(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return L(z,!0);let a=await U(i,`/api/orphans${e.mode?`?mode=${encodeURIComponent(e.mode)}`:``}`);if(!a.ok)return L(`Error: ${a.error}`,!0);let{ok:o,...s}=a,c=s,{resolve:l,ui:u}=await G(t,r),d=(c.orphans??[]).map(e=>{let t=typeof e.docName==`string`?e.docName:null,n=t?l(t):null;return{...e,previewUrl:n?.url??null,...n?{previewUrlSource:n.source}:{}}}),f={...c,orphans:d,ui:u,cwd:r};return R(JSON.stringify(f,null,2),f)})}const Jr=["Return a browser URL for the given wiki docName. Agents should call this IMMEDIATELY BEFORE `write_document` / `edit_document` so they can navigate the preview browser to the doc first and watch the CRDT edit land live.",``,`**Parameters:**`,"- `docName` — Wiki doc name, typically without extension.",``,"Returns `{ previewUrl, previewUrlSource }` (source: `env` / `lock` / `config`). When no source is configured, returns `{ previewUrl: null }` and the agent may proceed without navigation."].join(`
197
+ `);async function Yr(e,t){let n=H(e.docName);if(!n.ok)return{ok:!1,error:n.error};let r=n.docName,i=await B(t.resolveCwd,t.config,e.cwd);if(!i.ok)return i;let{cwd:s,config:c}=i,l=o(c,s),d;try{d=u({projectDir:s,contentDir:l,includePatterns:c.content.include,excludePatterns:c.content.exclude})}catch(e){return{ok:!1,error:`Cannot evaluate content filter: ${e instanceof Error?e.message:String(e)}`}}if(![`${r}.md`,`${r}.mdx`].some(e=>!d.isExcluded(e)))return{ok:!1,error:`Error: docName "${r}" is not inside content.include globs (${c.content.include.join(`, `)}). This tool only returns URLs for docs that match those globs.`};let f=on(r,{config:c,lockDir:a(l),contentDir:l});return f?{ok:!0,result:{previewUrl:f.url,previewUrlSource:f.source},text:`Preview URL for "${r}" (source: ${f.source}):\n${f.url}`}:{ok:!0,result:{previewUrl:null},text:`No preview URL resolvable for "${r}". The server is likely not running yet. Start it with \`open-knowledge start\` (or \`preview_start\`), then **call \`get_preview_url\` again** the server writes a lock file that this tool reads to resolve the URL. NEVER guess or manually construct the preview URL. Alternatively, set \`OPEN_KNOWLEDGE_PREVIEW_BASE_URL\` or add \`preview.baseUrl\` to .open-knowledge/config.yml.`}}function Xr(e,t){e.tool(`get_preview_url`,Jr,{docName:P.string().min(1),cwd:P.string().optional().describe(I)},async e=>{let n=await Yr(e,t);return n.ok?R(n.text,n.result):L(n.error,!0)})}function Zr(e,t){return`${qt(`ingest`)}Capture this external source into the project knowledge base as raw reference material. **Raw preservation only** no summary, no analysis, no interpretation. Summarizing is the job of the \`research\` tool later.
176
198
 
177
199
  Source: ${e}
178
200
 
179
201
  The content directory for this project is **\`${t}\`** (from \`${n}/config.yml\`).
180
202
 
203
+ ## Step 0: Is this source worth preserving?
204
+
205
+ Before fetching anything, sanity-check:
206
+
207
+ - **Is it in scope?** If the source is unrelated to what this knowledge base is accumulating, ingest is pollution. Check the existing layout: \`exec("ls ${t}")\` to see what topics are already covered.
208
+ - **Is it already ingested?** \`exec("grep -rln <source-url-or-title-slug> ${t}")\` — if the same source is already present with current content, stop and reuse. Re-ingest is appropriate when the source has changed materially (new version, significant edits) — note the reason in frontmatter's \`description\`.
209
+ - **Is the user's intent actually \`ingest\` (preserve) or \`research\` (analyze)?** If they want findings synthesized rather than raw bytes archived, redirect: "\`research\` on this topic will pull sources via \`ingest\` as needed. Use \`research\` instead." Don't pre-ingest speculatively when the user wants analysis.
210
+
211
+ If all three checks pass, proceed.
212
+
181
213
  ## Step 1: Fetch the content
182
214
 
183
215
  - **URL** → use your available web fetch tool.
@@ -221,183 +253,248 @@ tags:
221
253
  - Valid frontmatter (at minimum \`title\`, \`description\`, and either \`source_url\` or \`source_path\`)
222
254
  - \`exec("ls <dir>")\` should list the file with enrichment
223
255
 
256
+ ## Step 5: Discuss takeaways with the user (no file write)
257
+
258
+ After preservation, briefly surface back to the user what the source actually contains — in **chat**, not in the raw file. This is Karpathy's "discussing takeaways" step: the raw file stays verbatim, but the human collaborator gets a quick orientation.
259
+
260
+ - 3–5 bullet points capturing the source's main claims, with no editorializing.
261
+ - Note any **tensions** with existing knowledge-base docs you already surfaced in Step 0 — agents that ingest in isolation miss the "wait, this contradicts \`[[prior-article]]\`" signal.
262
+ - Offer next steps: "Shall I \`research\` this topic now, or is preservation enough?" Don't silently chain into \`research\` — the user may have just wanted the archive.
263
+
264
+ ## Step 6 (optional): Update neighbor docs to link the new source
265
+
266
+ If the source is directly relevant to an existing article or research doc, update that doc to link the new raw source. A preserved source that no doc points at is an island. Limit this to 1–3 high-signal neighbors — don't touch everything tangentially related.
267
+
268
+ - Follow the \`write_document\` / \`edit_document\` contract from the skill (preview-before-edit).
269
+ - Use a markdown link: \`[Source title](./${t?`relative/path/to/source.md`:`raw/source.md`})\`.
270
+ - Do NOT mass-update every neighbor. Karpathy's pattern rewards focused cross-linking; noisy neighbor-pings degrade the signal.
271
+
224
272
  ## Non-goals
225
273
 
226
274
  - **No analysis** — don't interpret, compare, or critique the source
227
275
  - **No promotion to a canonical article** — that's the \`consolidate\` tool's job, later
228
- - **No deduplication** if the same source is ingested twice, let it happen; cleanup is a separate concern
276
+ - **No silent chaining into research** ingest completes on its own; the user explicitly opts into \`research\`
277
+ - **No synthesis inside the raw file** — the takeaways live in chat or a separate summary doc, never mixed into the preserved source
278
+ `}const Qr=[`Fetch an external source (URL or local file) and save raw content as reference material in the project content directory.`,`Raw preservation only — no analysis or interpretation.`,``,`**Use when:**`,`- Capturing reference material for the project knowledge base`,`- Saving a URL or document for later research`,`- Archiving an external source alongside the codebase`,`- The user shares a URL or document they want preserved`,``,`**Triggers on:**`,`- "ingest", "save this source", "capture this URL", "add to external sources"`,`- User shares a URL, article, or document to preserve in the knowledge base`,`- Research workflow needs raw sources before analysis`].join(`
279
+ `);function $r(e,t){e.tool(`ingest`,Qr,{source:P.string().describe(`URL, file path, or identifier of the source to ingest`),cwd:P.string().optional().describe(I)},async e=>{let n=await B(t.resolveCwd,t.config,e.cwd);return n.ok?R(Zr(e.source,n.config.content.dir),{previewUrl:null}):L(`Error: ${n.error}`,!0)})}const ei=[`[Requires: Hocuspocus server] List available documents from the Hocuspocus server.`,`Returns document names, optionally filtered by directory.`,``,`**Parameters:**`,"- `dir` (optional) — Filter to documents in this directory"].join(`
280
+ `);function ti(e,t){e.tool(`list_documents`,ei,{dir:P.string().optional().describe(`Optional directory to filter documents`),cwd:P.string().optional().describe(I)},async e=>{let n=await V(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return L(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return L(z,!0);let a=await U(i,`/api/documents${e.dir?`?dir=${encodeURIComponent(e.dir)}`:``}`);if(!a.ok)return L(`Error: ${a.error}`,!0);let{ok:o,...s}=a,c=s,{resolve:l,ui:u}=await G(t,r),d=(c.documents??[]).map(e=>{let t=typeof e.docName==`string`?e.docName:null,n=t?l(t):null;return{...e,previewUrl:n?.url??null,...n?{previewUrlSource:n.source}:{}}}),f={...c,documents:d,ui:u,cwd:r};return R(JSON.stringify(f,null,2),f)})}const ni=[`Read a wiki file with enriched context: contents + frontmatter metadata + recent shadow-repo activity (agent vs human attribution) + backlink/forward-link context.`,``,`**Use when:**`,`- Loading an article for context`,`- Understanding who changed a file recently and whether it was an agent or human`,`- Seeing how this page links out and what links back to it`,``,"Prefer this over your native `Read` for wiki files — one call returns what otherwise takes 3-4.",``,`**Parameters:**`,"- `path` — Project-root-relative path to the file, including extension (e.g. `articles/auth/sso.md`). To pass this document to `edit_document` / `write_document` / `get_backlinks`, strip the extension (they take extension-less `docName`).","- `since` (reserved) — Reserved for shadow-log since-filter; currently unused."].join(`
281
+ `);function ri(e){if(!e||e.length===0)return``;let t=[``,`### Recent activity (OK edits)`,``];for(let n of e){let e=n.writerClassification===`agent`?`agent: ${n.writerName}`:n.writerClassification===`principal`?`human: ${n.writerName}`:`${n.writerClassification}: ${n.writerName}`,r=n.hash.slice(0,7);t.push(`- ${r} ${n.date} [${e}] ${n.message}`)}return t.join(`
282
+ `)}function ii(e){if(!e||e.length===0)return``;let t=[``,`### Commit history (project git)`,``];for(let n of e){let e=n.hash.slice(0,7);t.push(`- ${e} ${n.date} ${n.authorName} — ${n.subject}`)}return t.join(`
283
+ `)}function ai(e){if(!e||e.length===0)return``;let t=[``,`### Backlinks (${e.length})`,``];for(let n of e){let e=n.title?` — "${n.title}"`:``,r=n.snippet?` — "${n.snippet}"`:``;t.push(`- ${n.source}${e}${r}`)}return t.join(`
284
+ `)}function oi(e){if(!e||e.length===0)return``;let t=[``,`### Forward links (${e.length})`,``];for(let n of e){if(n.kind===`external`){let e=n.title?` — "${n.title}"`:``,r=n.snippet?` — "${n.snippet}"`:``;t.push(`- ${n.url}${e}${r}`);continue}let e=n.title?` — "${n.title}"`:``,r=n.snippet?` — "${n.snippet}"`:``;t.push(`- ${n.docName}${e}${r}`)}return t.join(`
285
+ `)}function si(e){return e.replace(/^\.\//,``).replace(/^\/+/,``)}function ci(e){return e.replace(/\.(md|mdx)$/i,``)}async function li(e,t){let n=await V(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)throw Error(n.error);let{cwd:r,config:i,url:a}=n,o=si(e.path),s=M(r,o),c=i.mcp.tools.read_document.historyDepth,[l,u]=await Promise.all([Oe(s,`utf-8`),_r(o,{projectDir:r,serverUrl:a,historyDepth:c,folderRules:i.folders},{includeRichFields:!0})]),d=o.split(`/`).pop()?.replace(/\.md$/,``).replace(/\.mdx$/,``)??o,f=u.title??d,p=u.description??``,m=u.tags,h=[];h.push(`## ${f}`),p&&h.push(`**Description:** ${p}`),m.length>0&&h.push(`**Tags:** ${m.join(`, `)}`),h.push(`**Path:** ${o}`);let g=ri(u.history);g&&h.push(g);let _=ii(u.projectHistory);_&&h.push(_);let v=ai(u.backlinks);v&&h.push(v);let y=oi(u.forwardLinks);return y&&h.push(y),h.push(``,`### Content`,``,l),h.join(`
286
+ `)}function ui(e,t){e.tool(`read_document`,ni,{path:P.string().describe(`Project-root-relative path to the file`),since:P.string().optional().describe(`Reserved; currently unused (§15 Future Work)`),cwd:P.string().optional().describe("Absolute host path to resolve `path` against. Defaults only when the MCP client advertises exactly one root; otherwise pass `cwd` explicitly.")},async e=>{try{let n=await li(e,t),r=await W(ci(si(e.path)),{config:t.config,resolveCwd:t.resolveCwd},await t.resolveCwd(e.cwd));return r?R(n,{previewUrl:r.url,previewUrlSource:r.source}):R(n,{previewUrl:null})}catch(e){return L(`Error: ${e instanceof Error?e.message:String(e)}`,!0)}})}const di=["[Requires: Hocuspocus server] Rename a document through the managed rename flow at `POST /api/rename`.",`Renames the target document and rewrites inbound wiki-links plus supported internal inline Markdown links in affected docs.`,``,`**Parameters:**`,"- `docName` — Current document name, typically without extension. A trailing `.md` or `.mdx` is stripped automatically.","- `newDocName` — New document name, typically without extension. A trailing `.md` or `.mdx` is stripped automatically.",'- `summary` — Optional one-line user-outcome description (≤80 chars). Appears as a bullet in the timeline. If omitted, a default like "Renamed X → Y" is generated. Provide your own summary to explain the why. Avoid including secrets or PII — summaries are persisted to git history.'].join(`
287
+ `);function fi(e){return Array.isArray(e)?e.flatMap(e=>{if(!e||typeof e!=`object`)return[];let{fromDocName:t,toDocName:n}=e;return typeof t==`string`&&typeof n==`string`?[{fromDocName:t,toDocName:n}]:[]}):[]}function pi(e){return Array.isArray(e)?e.flatMap(e=>{if(!e||typeof e!=`object`)return[];let{docName:t,rewrites:n}=e;return typeof t==`string`&&typeof n==`number`?[{docName:t,rewrites:n}]:[]}):[]}function mi(e,t,n=`${t}s`){return e===1?t:n}function hi(e,t){e.tool(`rename_document`,di,{docName:P.string().describe(`Current document name`),newDocName:P.string().describe(`New document name`),summary:P.string().max(200).optional().describe(`Optional one-line user-outcome description (≤80 chars). Defaults to "Renamed X → Y" when omitted.`),cwd:P.string().optional().describe(I)},async e=>{let n=await V(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return L(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return L(z,!0);let a=H(e.docName);if(!a.ok)return L(a.error,!0);let o=H(e.newDocName);if(!o.ok)return L(o.error,!0);let s=t.identityRef?.current,c=await Xt(i,`/api/rename`,{docName:a.docName,newDocName:o.docName,...e.summary===void 0?{}:{summary:e.summary},...s?{agentId:s.connectionId,agentName:s.displayName,clientName:s.clientInfo?.name,colorSeed:s.colorSeed}:{}});if(!c.ok){let e=typeof c.error==`string`?c.error:`Rename failed`,t={ok:!1,error:e};return R(`Error: ${e}`,t,!0)}let l=fi(c.renamed),u=pi(c.rewrittenDocs),d=l.map(({fromDocName:e,toDocName:t})=>`${e} -> ${t}`).join(`, `)||`${a.docName} -> ${o.docName}`,f=u.length===0?`No inbound links required updates.`:`Rewrote ${u.length} ${mi(u.length,`document`)}.`,p={config:t.config,resolveCwd:t.resolveCwd},m=await W(o.docName,p,r),h=await W(a.docName,p,r),g=c.summary&&typeof c.summary==`object`?c.summary:void 0,_=typeof g?.hint==`string`?g.hint:void 0,v={ok:!0,renamed:l,rewrittenDocs:u,previewUrl:m?.url??null,...m?{previewUrlSource:m.source}:{},...h?{previousPreviewUrl:h.url}:{},...g?{summary:g}:{}},y=[`Renamed ${d}. ${f}`];return _&&y.push(_),R(y.join(`
288
+ `),v)})}function gi(e,t){return`${qt(`research`)}Conduct **evidence-driven research** on this topic and produce a provisional research article in the Open Knowledge content directory. This workflow mirrors the discipline of the \`eng:research\` skill, scoped to Open Knowledge's wiki-provisional layer.
229
289
 
230
- Full convention: read \`${n}/AGENTS.md\`.`}const zr=[`Fetch an external source (URL or local file) and save raw content as reference material in the project content directory.`,`Raw preservation only — no analysis or interpretation.`,``,`**Use when:**`,`- Capturing reference material for the project knowledge base`,`- Saving a URL or document for later research`,`- Archiving an external source alongside the codebase`,`- The user shares a URL or document they want preserved`,``,`**Triggers on:**`,`- "ingest", "save this source", "capture this URL", "add to external sources"`,`- User shares a URL, article, or document to preserve in the knowledge base`,`- Research workflow needs raw sources before analysis`].join(`
231
- `);function Br(e,t){e.tool(`ingest`,zr,{source:A.string().describe(`URL, file path, or identifier of the source to ingest`),cwd:A.string().optional().describe(M)},async e=>{let n=await I(t.resolveCwd,t.config,e.cwd);return n.ok?P(Rr(e.source,n.config.content.dir),{previewUrl:null}):N(`Error: ${n.error}`,!0)})}function Vr(e){return`Initialize a project knowledge base at \`${e}\` for this repository.
232
-
233
- The content directory for this project is **\`${e}\`** (from \`${n}/config.yml\`).
290
+ Topic: ${e}
291
+ Content directory: \`${t}\` (from \`${n}/config.yml\`)
234
292
 
235
- ## When to use
293
+ ## Three paths
236
294
 
237
- - First time setting up a knowledge base in a repo where \`${n}/\` does not exist, or where the content directory has no articles yet
238
- - When onboarding to a new codebase and you want to capture initial understanding for future agent sessions
295
+ - **Path A Research article (DEFAULT):** Persistent provisional article under the content directory with an inline \`sources:\` frontmatter list pointing at raw sources captured via \`ingest\`. This is the default unless the user explicitly opts out.
296
+ - **Path B Direct answer:** Findings delivered in conversation only. **Requires explicit user request** (e.g., "just tell me", "no doc needed", "quick answer").
297
+ - **Path C — Update existing research:** Surgical additions/corrections to an existing research article. Triggered when the user references an existing research doc or says "update/refresh/extend."
239
298
 
240
- ## Steps
299
+ Path A is the default because wiki-provisional articles compound over time; spoken answers do not.
241
300
 
242
- ### 1. Verify the structure exists
301
+ ## Autonomy mode
243
302
 
244
- If \`${n}/\` does not already exist, scaffold it from a terminal (not from within this MCP session — scaffolding is a CLI operation, not a tool call):
303
+ | Mode | Behavior | How entered |
304
+ |---|---|---|
305
+ | **Supervised** (default) | Stop at the scoping gate for user rubric confirmation. Route coverage decisions interactively. | Default when a user drives the session. |
306
+ | **Headless** | Auto-confirm rubric after proposing it. Auto-select routing decisions. Skip interactive prompts. All other gates (scan, analysis, validation, grounding) still enforced. | Explicit "don't wait for me", "just proceed", "run headless" — or non-interactive container environments. |
245
307
 
246
- \`\`\`bash
247
- open-knowledge init
248
- # or: npx @inkeep/open-knowledge init
249
- \`\`\`
308
+ In headless mode, propose the rubric AND proceed immediately. Mark the Scoping task completed after proposing.
250
309
 
251
- That creates \`${n}/\` with \`config.yml\`, \`AGENTS.md\`, \`.gitignore\`, and wires this MCP server into your selected editors' MCP config files (user-scoped by default). It does **not** scaffold content subdirectories — knowledge lives wherever \`content.dir\` points (currently \`${e}\`). After scaffolding, reconnect the MCP client so the server picks up the new config.
310
+ ---
252
311
 
253
- If you have \`Bash\` tool access, you can shell out: \`bash\` → \`npx @inkeep/open-knowledge init\`, then prompt the user to reconnect.
312
+ ## Mandatory execution order
254
313
 
255
- ### 2. Read the codebase systematically
314
+ **Hard gates — do NOT skip ahead.** If you find yourself about to run a \`WebFetch\` or \`WebSearch\` without completing Steps 0-2, STOP you skipped a gate.
256
315
 
257
- Explore the project to build understanding before writing anything:
316
+ 1. **Step 0: Create workflow checkpoint tasks** ALWAYS the first action.
317
+ 2. ⛔ **Step 1: Scan existing coverage + route** — scan the content directory for prior work; classify coverage; present options before new research begins.
318
+ 3. ⛔ **Step 2: Collaborative scoping** — propose a research rubric. In Supervised mode, STOP and WAIT for user confirmation before any external fetch.
319
+ 4. **Step 3: Capture raw sources via \`ingest\`** — preserve before analyzing.
320
+ 5. **Step 4: Read + analyze** — 3P-external by default; 1P-codebase only when user explicitly requests.
321
+ 6. **Step 5: Write the research article** — Path A only.
322
+ 7. **Step 6: Link aggressively + file valuable Q&A back**.
323
+ 8. **Step 7: Validate** — frontmatter, dead-links, sources alignment.
324
+ 9. **Step 8: Recap + follow-up directions**.
258
325
 
259
- 1. **Start broad** Read \`README.md\`, \`CLAUDE.md\` or \`AGENTS.md\`, \`package.json\` (or equivalent manifest), and any existing prose documentation
260
- 2. **Map the structure** — Use \`exec("ls <dir>")\` for directories under \`content.dir\` that match \`content.include\` (returns folder metadata — file counts, subdirs, most-recent md) and native \`Glob\`/\`ls\` for source code
261
- 3. **Read key files** — Entry points, config files, core modules, type definitions, schema files
262
- 4. **Check existing docs** — \`specs/\`, \`docs/\`, \`ARCHITECTURE.md\`, or any prose dirs: use \`exec\` for every \`.md\` / \`.mdx\` that matches \`content.include\` (under shipped defaults, that is essentially **all** markdown in the repo). Use native \`Read\`/\`Glob\` only for source code / non-markdown, or when MCP is unavailable
263
- 5. **Review recent history** — \`git log --oneline -30\` for recent decisions and direction
326
+ **Path B shortcut:** If the user explicitly requested a direct answer in Step 2, skip Steps 5, 7. Steps 0, 1, 3, 4, 6, 8 still apply (evidence discipline doesn't relax just because output is conversational).
264
327
 
265
- Don't rush this phase. The quality of articles depends on the quality of understanding.
328
+ ---
266
329
 
267
- ### 3. Synthesize knowledge articles
330
+ ## Report framing default: external / third-party sources
268
331
 
269
- Write articles inside the content directory (\`${e}\`). Organization is up to the project no enforced structure:
332
+ Research articles default to **3P/external framing** — investigating third-party topics, technologies, concepts, public repos, papers, official docs. **Do NOT mix the user's own codebase analysis into the research article unless the user explicitly asks.** Mixing drifts findings from factual synthesis toward opinion-forming applied to the company, reducing factual fidelity.
270
333
 
271
- - If the project already has a docs layout (\`docs/\`, \`guides/\`, topic-grouped subfolders), follow it
272
- - If starting fresh, group by topic (e.g., \`architecture/\`, \`auth/\`, \`data-model/\`)create subfolders as needed; no scaffolded directories exist by default
273
- - **One topic per article** — keep articles focused (e.g., "Auth Architecture", not "Everything About The Backend")
274
- - **Add proper frontmatter**:
334
+ - **Default:** external sources (web, OSS repos, papers, official APIs).
335
+ - **Exception:** if user asks "research how our X compares to Y" or "include our codebase," include it but clearly separate 1P observations from 3P findings in the article so a reader can distinguish externally-verifiable facts from company-specific takes.
275
336
 
276
- \`\`\`yaml
277
- ---
278
- title: Article Title
279
- description: One-line summary
280
- tags:
281
- - relevant
282
- - tags
283
337
  ---
338
+
339
+ ## Step 0: Create workflow checkpoint tasks
340
+
341
+ ⛔ **ALWAYS THE FIRST ACTION.** Before any read, any scan, any fetch — create tasks. They persist across context compaction, make skipped steps immediately visible, and show progress to the user.
342
+
343
+ Create these tasks via your host's task system (\`TaskCreate\` in Claude Code; equivalent elsewhere):
344
+
345
+ \`\`\`
346
+ TaskCreate: "Research: Scan existing coverage + route" → start as in_progress
347
+ TaskCreate: "Research: Collaborative scoping — rubric gate" → pending, blocked by #1
348
+ TaskCreate: "Research: Capture sources via ingest" → pending, blocked by #2
349
+ TaskCreate: "Research: Read + analyze" → pending, blocked by #3
350
+ TaskCreate: "Research: Write the research article" → pending, blocked by #4
351
+ TaskCreate: "Research: Link aggressively + file Q&A back" → pending, blocked by #5
352
+ TaskCreate: "Research: Validate (frontmatter + dead-links)" → pending, blocked by #6
353
+ TaskCreate: "Research: Recap + follow-up directions" → pending, blocked by #7
284
354
  \`\`\`
285
355
 
286
- - **Write for future agents** Explain the *why* and *how things connect*, not just *what exists*. Source code already says what exists.
287
- - **Keep articles concise** — 100-300 lines is a good target. Split larger topics into multiple articles.
288
- - **Link to source code** by file path when helpful, but don't duplicate code into articles.
356
+ Use \`addBlockedBy\` to enforce ordering. As you complete each step, mark the task \`completed\` and the next task \`in_progress\`.
289
357
 
290
- ### 4. Link aggressively
358
+ **Path B variant:** If scoping determines Path B (direct answer), mark tasks #5 and #7 as \`deleted\` — they don't apply.
291
359
 
292
- This is the single highest-leverage step for a new knowledge base. Articles that don't link each other are isolated documents; articles that cross-link form a navigable graph.
360
+ **Path C variant:** If Step 1 routes to Path C (update existing), mark tasks #3 and #5 as \`deleted\` (ingest is usually unnecessary and no new article is created) and rename task #4 to "Research: Read existing article + diff deltas."
293
361
 
294
- - **Every noun-phrase that names another article is a \`[[Page Name]]\` link.** Write links inline as you draft — don't save linking for a second pass. Prefer \`[[Page]]\` over Markdown \`[text](./page.md)\` since only wiki-links participate in the backlinks index.
295
- - **Redlinks are fine — write them eagerly.** If you're drafting "Auth Architecture" and mention "session tokens", write \`[[Session Tokens]]\` even if that page doesn't exist yet. The redlink is a to-do list for the next pass.
296
- - **Build hub articles.** Pick 2–3 broad topics (e.g., "Architecture Overview", "Data Model") and have them link out to the specific articles below them. Hubs are what agents discover first — their outbound links are how everything else becomes findable.
297
- - **Cross-link siblings.** In each subfolder, 2–3 closely-related articles should link each other under a "See also" section or inline.
298
- - **After writing a batch of articles, verify link density:** \`exec("cat <article>.md")\` on a sample and confirm the rendered output shows a healthy backlinks list. An article with zero backlinks is an island — link back to it from somewhere.
362
+ Why tasks: the observed failure mode is the agent jumping straight to \`WebFetch\` without scanning or scoping. Tasks make the skipped gates obvious to the user mid-session.
299
363
 
300
- ### 5. Suggested starting topics
364
+ ---
301
365
 
302
- Depending on the project, consider articles covering:
366
+ ## Step 1: Scan existing coverage + route
303
367
 
304
- - **Architecture overview** High-level system design, key components, how they connect
305
- - **Data model** — Core entities, relationships, database schema
306
- - **API surface** — Endpoints, protocols, authentication model
307
- - **Deploy & infrastructure** — How to deploy, CI/CD, environments
308
- - **Development workflow** — How to run locally, test conventions, contribution flow
309
- - **Key decisions** — Architecture decisions and their rationale (the "why")
310
- - **Domain concepts** — Business domain terms and their meaning in code
368
+ **MANDATORY FIRST RESEARCH STEP.** Before any external fetch, scan what the knowledge base already holds.
311
369
 
312
- ### 6. Verify
370
+ ### Phase 1: Check existing knowledge
313
371
 
314
- - \`exec("ls ${e}")\` shows the articles you wrote, each with title/description/tags enrichment
315
- - \`exec("grep -rn <common-codebase-term> ${e}")\` finds the expected articles
316
- - \`exec("cat <article>.md")\` on a sample shows the article plus its backlinks section — if the backlinks list is empty, go back to step 4 and link from somewhere
317
- - Every article has frontmatter with at minimum \`title\` and \`description\`
372
+ **If the user explicitly references an existing research article** (names it, links it, says "update/refresh/extend"):
373
+ Skip the scan. Go directly to **Path C**.
318
374
 
319
- ## Non-goals
375
+ **Otherwise, always scan first:**
320
376
 
321
- - **Don't produce a file-by-file code index** the agent reads source code directly when needed
322
- - **Don't copy source code into articles** link by path
323
- - **Don't write articles for things that change often** (dependency versions, file counts); focus on stable understanding
324
- - **Don't create scaffolded subfolders you won't fill** — empty \`articles/\`/\`research/\`/\`external-sources/\` folders are clutter; organize as you actually need
325
-
326
- Full convention: read \`${n}/AGENTS.md\`.`}const Hr=[`Bootstrap the project knowledge base by reading the codebase and writing initial knowledge articles grouped by topic.`,``,`**Use when:**`,`- Setting up a knowledge base for the first time in a repo`,`- Onboarding to a new codebase and capturing initial understanding`,`- The content directory is empty or sparse`,``,`**Triggers on:**`,`- "init content", "bootstrap knowledge base", "populate articles", "set up project knowledge"`,`- User asks to document or catalog the codebase`].join(`
327
- `);function Ur(e,t){e.tool(`init-content`,Hr,{cwd:A.string().optional().describe(M)},async(e={})=>{let n=await I(t.resolveCwd,t.config,e.cwd);if(!n.ok)return N(`Error: ${n.error}`,!0);let{cwd:r,config:i}=n;return P(Vr(i.content.dir),{ui:qt({config:i,lockDir:a(o(i,r))})})})}const Wr=[`[Requires: Hocuspocus server] List available documents from the Hocuspocus server.`,`Returns document names, optionally filtered by directory.`,``,`**Parameters:**`,"- `dir` (optional) — Filter to documents in this directory"].join(`
328
- `);function Gr(e,t){e.tool(`list_documents`,Wr,{dir:A.string().optional().describe(`Optional directory to filter documents`),cwd:A.string().optional().describe(M)},async e=>{let n=await L(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return N(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return N(F,!0);let a=await z(i,`/api/documents${e.dir?`?dir=${encodeURIComponent(e.dir)}`:``}`);if(!a.ok)return N(`Error: ${a.error}`,!0);let{ok:o,...s}=a,c=s,{resolve:l,ui:u}=await H(t,r),d=(c.documents??[]).map(e=>{let t=typeof e.docName==`string`?e.docName:null,n=t?l(t):null;return{...e,previewUrl:n?.url??null,...n?{previewUrlSource:n.source}:{}}}),f={...c,documents:d,ui:u,cwd:r};return P(JSON.stringify(f,null,2),f)})}const Kr=[`Read a wiki file with enriched context: contents + frontmatter metadata + recent shadow-repo activity (agent vs human attribution) + backlink/forward-link context.`,``,`**Use when:**`,`- Loading an article for context`,`- Understanding who changed a file recently and whether it was an agent or human`,`- Seeing how this page links out and what links back to it`,``,"Prefer this over your native `Read` for wiki files — one call returns what otherwise takes 3-4.",``,`**Parameters:**`,"- `path` — Project-root-relative path to the file, including extension (e.g. `articles/auth/sso.md`). To pass this document to `edit_document` / `write_document` / `get_backlinks`, strip the extension (they take extension-less `docName`).","- `since` (reserved) — Reserved for shadow-log since-filter; currently unused."].join(`
329
- `);function qr(e){if(!e||e.length===0)return``;let t=[``,`### Recent activity (OK edits)`,``];for(let n of e){let e=n.writerClassification===`agent`?`agent: ${n.writerName}`:n.writerClassification===`principal`?`human: ${n.writerName}`:`${n.writerClassification}: ${n.writerName}`,r=n.hash.slice(0,7);t.push(`- ${r} ${n.date} [${e}] ${n.message}`)}return t.join(`
330
- `)}function Jr(e){if(!e||e.length===0)return``;let t=[``,`### Commit history (project git)`,``];for(let n of e){let e=n.hash.slice(0,7);t.push(`- ${e} ${n.date} ${n.authorName} — ${n.subject}`)}return t.join(`
331
- `)}function Yr(e){if(!e||e.length===0)return``;let t=[``,`### Backlinks (${e.length})`,``];for(let n of e){let e=n.title?` — "${n.title}"`:``,r=n.snippet?` — "${n.snippet}"`:``;t.push(`- ${n.source}${e}${r}`)}return t.join(`
332
- `)}function Xr(e){if(!e||e.length===0)return``;let t=[``,`### Forward links (${e.length})`,``];for(let n of e){if(n.kind===`external`){let e=n.title?` — "${n.title}"`:``,r=n.snippet?` — "${n.snippet}"`:``;t.push(`- ${n.url}${e}${r}`);continue}let e=n.title?` — "${n.title}"`:``,r=n.snippet?` — "${n.snippet}"`:``;t.push(`- ${n.docName}${e}${r}`)}return t.join(`
333
- `)}function Zr(e){return e.replace(/^\.\//,``).replace(/^\/+/,``)}function Qr(e){return e.replace(/\.(md|mdx)$/i,``)}async function $r(e,t){let n=await L(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)throw Error(n.error);let{cwd:r,config:i,url:a}=n,o=Zr(e.path),s=k(r,o),c=i.mcp.tools.read_document.historyDepth,[l,u]=await Promise.all([we(s,`utf-8`),ir(o,{projectDir:r,serverUrl:a,historyDepth:c,folderRules:i.folders},{includeRichFields:!0})]),d=o.split(`/`).pop()?.replace(/\.md$/,``).replace(/\.mdx$/,``)??o,f=u.title??d,p=u.description??``,m=u.tags,h=[];h.push(`## ${f}`),p&&h.push(`**Description:** ${p}`),m.length>0&&h.push(`**Tags:** ${m.join(`, `)}`),h.push(`**Path:** ${o}`);let g=qr(u.history);g&&h.push(g);let _=Jr(u.projectHistory);_&&h.push(_);let v=Yr(u.backlinks);v&&h.push(v);let y=Xr(u.forwardLinks);return y&&h.push(y),h.push(``,`### Content`,``,l),h.join(`
334
- `)}function ei(e,t){e.tool(`read_document`,Kr,{path:A.string().describe(`Project-root-relative path to the file`),since:A.string().optional().describe(`Reserved; currently unused (§15 Future Work)`),cwd:A.string().optional().describe("Absolute host path to resolve `path` against. Defaults only when the MCP client advertises exactly one root; otherwise pass `cwd` explicitly.")},async e=>{try{let n=await $r(e,t),r=await V(Qr(Zr(e.path)),{config:t.config,resolveCwd:t.resolveCwd},await t.resolveCwd(e.cwd));return r?P(n,{previewUrl:r.url,previewUrlSource:r.source}):P(n,{previewUrl:null})}catch(e){return N(`Error: ${e instanceof Error?e.message:String(e)}`,!0)}})}const ti=["[Requires: Hocuspocus server] Rename a document through the managed rename flow at `POST /api/rename`.",`Renames the target document and rewrites inbound wiki-links plus supported internal inline Markdown links in affected docs.`,``,`**Parameters:**`,"- `docName` — Current document name, typically without extension. A trailing `.md` or `.mdx` is stripped automatically.","- `newDocName` — New document name, typically without extension. A trailing `.md` or `.mdx` is stripped automatically.",'- `summary` — Optional one-line user-outcome description (≤80 chars). Appears as a bullet in the timeline. If omitted, a default like "Renamed X → Y" is generated. Provide your own summary to explain the why. Avoid including secrets or PII — summaries are persisted to git history.'].join(`
335
- `);function ni(e){return Array.isArray(e)?e.flatMap(e=>{if(!e||typeof e!=`object`)return[];let{fromDocName:t,toDocName:n}=e;return typeof t==`string`&&typeof n==`string`?[{fromDocName:t,toDocName:n}]:[]}):[]}function ri(e){return Array.isArray(e)?e.flatMap(e=>{if(!e||typeof e!=`object`)return[];let{docName:t,rewrites:n}=e;return typeof t==`string`&&typeof n==`number`?[{docName:t,rewrites:n}]:[]}):[]}function ii(e,t,n=`${t}s`){return e===1?t:n}function ai(e,t){e.tool(`rename_document`,ti,{docName:A.string().describe(`Current document name`),newDocName:A.string().describe(`New document name`),summary:A.string().max(200).optional().describe(`Optional one-line user-outcome description (≤80 chars). Defaults to "Renamed X → Y" when omitted.`),cwd:A.string().optional().describe(M)},async e=>{let n=await L(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return N(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return N(F,!0);let a=R(e.docName);if(!a.ok)return N(a.error,!0);let o=R(e.newDocName);if(!o.ok)return N(o.error,!0);let s=t.identityRef?.current,c=await B(i,`/api/rename`,{docName:a.docName,newDocName:o.docName,...e.summary===void 0?{}:{summary:e.summary},...s?{agentId:s.connectionId,agentName:s.displayName,clientName:s.clientInfo?.name,colorSeed:s.colorSeed}:{}});if(!c.ok){let e=typeof c.error==`string`?c.error:`Rename failed`,t={ok:!1,error:e};return P(`Error: ${e}`,t,!0)}let l=ni(c.renamed),u=ri(c.rewrittenDocs),d=l.map(({fromDocName:e,toDocName:t})=>`${e} -> ${t}`).join(`, `)||`${a.docName} -> ${o.docName}`,f=u.length===0?`No inbound links required updates.`:`Rewrote ${u.length} ${ii(u.length,`document`)}.`,p={config:t.config,resolveCwd:t.resolveCwd},m=await V(o.docName,p,r),h=await V(a.docName,p,r),g=c.summary&&typeof c.summary==`object`?c.summary:void 0,_=typeof g?.hint==`string`?g.hint:void 0,v={ok:!0,renamed:l,rewrittenDocs:u,previewUrl:m?.url??null,...m?{previewUrlSource:m.source}:{},...h?{previousPreviewUrl:h.url}:{},...g?{summary:g}:{}},y=[`Renamed ${d}. ${f}`];return _&&y.push(_),P(y.join(`
336
- `),v)})}function oi(e,t){return`Research this topic and write provisional findings inside the project content directory. Research is **provisional, not canonical** — it captures findings, trade-offs, and open questions at a point in time. Promoting to canonical articles is a deliberate later step (via the \`consolidate\` tool).
377
+ 1. \`exec("grep -rln <topic-keyword> ${t}")\` returns matching files with frontmatter enrichment so you can judge relevance without opening each.
378
+ 2. \`exec("ls ${t}")\` surfaces folder layout and most-recent-updated doc per subdir.
379
+ 3. For the **1–3 most promising candidates**, \`exec("cat <path>")\` returns full doc + frontmatter + backlinks + recent shadow-repo activity.
337
380
 
338
- Topic: ${e}
381
+ Classify:
339
382
 
340
- The content directory for this project is **\`${t}\`** (from \`${n}/config.yml\`).
383
+ | Coverage | What it means | Route to |
384
+ |---|---|---|
385
+ | **Fully covered** | An existing article directly answers the question with evidence | Present findings; offer to elaborate, verify, extend, or explicitly new-report |
386
+ | **Partially covered** | Related research exists; the specific question is a natural extension | Offer: (1) extend existing via Path C, (2) new article via Path A |
387
+ | **Not covered** | No meaningful overlap | Proceed to Path A (default) or Path B |
341
388
 
342
- ## When to use this workflow
389
+ ### Phase 2: Present routing options (Supervised mode)
343
390
 
344
- - A developer asks you to research a topic (e.g., "research CRDT alternatives for our editor")
345
- - You're exploring a decision space before committing to an approach
346
- - Spec conversations and exploratory work that isn't ready to be canonical yet
347
- - You need to synthesize multiple sources into a structured analysis
391
+ **Fully covered:**
348
392
 
349
- ## Principle: provisional, not canonical
393
+ > "We already have research on this in \`<path>\`. Here's what it found: [2–4 key findings]. Options: (1) use as-is, (2) verify / refresh (article is from [date]), (3) extend on [specific dimension], (4) new angle if this is a different framing."
350
394
 
351
- Research articles are **provisional**. They capture what you found at a point in time. They are not the source of truth. When decisions solidify, research gets promoted to canonical articles via the \`consolidate\` tool (or manually). Until then, research is a place where uncertainty lives.
395
+ Let the user choose. Do NOT start new research when existing research already answers the question.
352
396
 
353
- ## Steps
397
+ **Partially covered:**
398
+
399
+ > "We have related research in \`<path>\` covering [scope]. Your question about [topic] isn't directly answered but it's a natural extension. Options: (1) extend existing via Path C, (2) start new article via Path A. I'd recommend [1 or 2] because [reason]."
354
400
 
355
- ### 1. Scope the research
401
+ **Not covered:**
356
402
 
357
- Understand what the developer is actually asking:
403
+ Proceed to Step 2 (scoping). If the user asked for a quick answer, flag that Path B may apply and confirm in Step 2's scoping exchange.
358
404
 
359
- - **What specific question needs answering?** If the prompt was vague, narrow it before gathering sources.
360
- - **What's the decision this research will inform?** Research without a decision context tends to meander.
361
- - **What's already known?** Use \`exec("grep -rn <topic-keyword> <content-dir>")\` to find prior work — grep results come with per-file enrichment (title, description, tags) so you can judge relevance without opening each. If prior work exists, use \`exec("cat <path>")\` to load it with full rich context (frontmatter + shadow-repo activity + project git history + backlinks) — you may be iterating on an existing research doc rather than creating a new one.
405
+ **Headless mode:** auto-select fully-covered proceed to new article on the specific angle the caller requested; partially-covered start new article; not-covered → Path A.
362
406
 
363
- If the topic is itself a URL, treat that URL as the anchor source and widen from there. If it's a question, figure out 3–8 sources that could plausibly inform it.
407
+ ### Scan discipline
364
408
 
365
- ### 2. Gather sources via \`ingest\`
409
+ - **Do not skip the scan.** Even 30 seconds of grep + cat prevents duplicate research AND gives the user context on what's already known.
410
+ - **Bias toward extending (Path C)** when topics are semantically coherent — one comprehensive article beats two overlapping ones.
411
+ - **Bias toward new (Path A)** when framing, audience, or primary question differs materially.
366
412
 
367
- Invoke the \`ingest\` tool for each relevant URL, paper, or document. **Typical research pulls 3–8 sources.** Too few and the synthesis is thin; too many and you'll be reading for the rest of the session.
413
+ ---
414
+
415
+ ## Step 2: Collaborative scoping (Supervised STOP gate)
368
416
 
369
- **Don't skip \`ingest\`.** Raw sources must be preserved before analysis it separates capture from interpretation and makes the research reproducible. A research article without preserved sources is just opinion; a research article with preserved sources is a trail someone else can follow.
417
+ **HARD GATE (Supervised mode).** Do NOT start external research until the user confirms the rubric. After proposing it, **STOP and WAIT for user response.** Only then mark the Scoping task completed.
370
418
 
371
- If a fetch fails for a source you specifically need, stop and ask the user to paste it — don't silently drop it.
419
+ **In headless mode:** propose the rubric AND proceed. Mark the task completed after proposing.
372
420
 
373
- ### 3. Read and analyze
421
+ ### Propose a rubric
374
422
 
375
- Read each ingested source carefully. Also read:
423
+ Return this structure to the user:
376
424
 
377
- - **Existing canonical articles** on the topic — use \`exec("cat <path>")\` (rich enrichment: frontmatter + shadow-repo activity + project git history + backlinks in one call)
378
- - **Prior research** on adjacent topics — same: \`exec("cat <path>")\` for Open Knowledge markdown
379
- - **Relevant source code** for projects where research is grounded in the codebase (read entry points, core modules, and any specs that touch the topic) — native \`Read\` is fine for \`.ts\` / \`.js\` / etc.; use \`exec\` for \`.md\` / \`.mdx\` under \`content.include\`
380
- - **Project context** — project-root docs, \`specs/\`, \`reports/\`, or wherever the project organizes design material
425
+ \`\`\`
426
+ ## Proposed research rubric
381
427
 
382
- Take notes on:
428
+ **Question:** [narrowed from the original topic — concrete, answerable, bounded]
383
429
 
384
- - **Key claims** and their evidence
430
+ **Dimensions to investigate:** [3–7 facets]
431
+ 1. [Dimension 1]
432
+ 2. [Dimension 2]
433
+ ...
434
+
435
+ **Candidate sources:** [3–8 initial guesses]
436
+ - [Source 1 — why it's relevant]
437
+ - [Source 2 — why it's relevant]
438
+ ...
439
+
440
+ **Success criteria:** [2–3 concrete outcomes — "the article cites X authoritative sources", "open questions are marked explicitly", etc.]
441
+
442
+ **Output format:** Path A (article) | Path B (direct answer) | Path C (update \`<existing-article>\`)
443
+ \`\`\`
444
+
445
+ ### Scoping discipline
446
+
447
+ - If the original topic is vague ("research LLM agents"), narrow it before fetching: "What specific agents? For what decision? Over what time horizon?"
448
+ - If the topic is itself a URL, treat that URL as the anchor and widen to 2–4 adjacent authoritative sources.
449
+ - Name the **decision** this research informs. Research without a decision context meanders.
450
+ - Do not over-specify the rubric — the user can adjust. Propose, don't prescribe.
451
+
452
+ ---
453
+
454
+ ## Step 3: Capture raw sources via \`ingest\`
455
+
456
+ For each relevant URL, paper, or document in the confirmed rubric, invoke the \`ingest\` tool. **Typical research pulls 3–8 sources.** Too few → thin synthesis. Too many → you'll be reading for the rest of the session.
457
+
458
+ - **Don't skip \`ingest\`.** Raw preservation separates capture from interpretation and makes research reproducible. An article without preserved sources is just opinion; an article with preserved sources is a trail someone else can follow.
459
+ - If a fetch fails for a source you specifically need, **stop and ask the user to paste it** — don't silently drop it. Write-time fabrication of missing evidence is the biggest failure mode.
460
+ - If \`ingest\` returns an obvious *summary* instead of the raw bytes (some LLM-backed fetch tools do this), note it and try a raw alternative (\`curl -sL <url>\`, or ask the user to paste).
461
+
462
+ ---
463
+
464
+ ## Step 4: Read + analyze
465
+
466
+ Read each ingested source carefully. Also load:
467
+
468
+ - **Existing canonical articles** on the topic — \`exec("cat <path>")\` (returns frontmatter + backlinks + shadow-repo activity).
469
+ - **Prior research** on adjacent topics — same: \`exec("cat <path>")\` for Open Knowledge markdown.
470
+ - **Relevant source code** — ONLY if the user asked for 1P analysis. Use native \`Read\` for \`.ts\` / \`.js\` / etc.; \`exec\` for in-scope \`.md\` / \`.mdx\`.
471
+ - **Project context** — \`specs/\`, \`reports/\`, or wherever the project keeps design material.
472
+
473
+ Take structured notes:
474
+
475
+ - **Key claims** and their evidence — every claim needs a source you can point at
385
476
  - **Trade-offs** between options
386
- - **Contradictions** between sources
387
- - **Unknowns** and open questions
477
+ - **Contradictions** between sources — these are often the most valuable part of the article
478
+ - **Unknowns** and open questions — the boundary of what you know
388
479
  - **Relevance** to the specific decision at hand
389
480
 
390
- ### 4. Write the research article
481
+ ### Grounding discipline
391
482
 
392
- Save the file as a markdown document inside the content directory. The path convention depends on the project:
483
+ Every factual claim in the article must cite its source inline. No unsourced speculation. If you don't have evidence: (a) run another search and cite it, (b) mark inline \`(TODO: needs source)\`, or (c) don't write the claim. Never fabricate.
393
484
 
394
- - If the project has adopted the three-tier lifecycle (external-sources → research → articles), save under a \`research/\` folder relative to the content dir (\`<content-dir>/research/<slug>.md\`)
395
- - If the project has an existing docs/reports/specs layout, save alongside that layout in a location that matches the project's conventions
396
- - When a research topic is large enough to warrant a subfolder, create one (\`research/<topic>/<subtopic>.md\`)
485
+ ---
397
486
 
398
- Use descriptive kebab-case filenames: \`crdt-alternatives-for-editor.md\`, \`llm-maintained-wikis-pattern.md\`.
487
+ ## Step 5: Write the research article (Path A only)
399
488
 
400
- Frontmatter:
489
+ Save a markdown document inside the content directory. Path convention:
490
+
491
+ - If the project adopted the three-tier lifecycle (external-sources → research → articles), save under \`<content-dir>/research/<slug>.md\`.
492
+ - If the project has an existing docs/reports/specs layout, match it.
493
+ - Large topics warrant a subfolder: \`research/<topic>/<subtopic>.md\`.
494
+
495
+ Filename: descriptive, kebab-case (\`crdt-alternatives-for-editor.md\`, \`llm-wikis-karpathy-pattern.md\`). No dates — dates go in frontmatter.
496
+
497
+ ### Frontmatter
401
498
 
402
499
  \`\`\`yaml
403
500
  ---
@@ -407,19 +504,19 @@ status: provisional
407
504
  date: YYYY-MM-DD
408
505
  tags:
409
506
  - research
410
- - topic-tag
507
+ - <topic-tag>
411
508
  sources:
412
509
  - <path-to-ingested-source-1>.md
413
510
  - <path-to-ingested-source-2>.md
414
511
  ---
415
512
  \`\`\`
416
513
 
417
- Structure:
514
+ ### Structure
418
515
 
419
516
  \`\`\`markdown
420
517
  ## Question
421
518
 
422
- [What specific question is this research answering? Be precise.]
519
+ [What specific question does this research answer? Be precise.]
423
520
 
424
521
  ## Context
425
522
 
@@ -427,15 +524,15 @@ Structure:
427
524
 
428
525
  ## Findings
429
526
 
430
- [Main findings organized by theme, option, or criterion. Cite sources by path.]
527
+ [Main findings organized by theme, option, or criterion. Every claim cites a source inline.]
431
528
 
432
- ### Option A / Theme 1
529
+ ### Theme / Option 1
433
530
 
434
- - Pros
435
- - Cons
436
- - Evidence (with source links)
531
+ - Pros — with evidence links
532
+ - Cons — with evidence links
533
+ - Evidence: [Source A](./external-sources/source-a.md), [Source B](./external-sources/source-b.md)
437
534
 
438
- ### Option B / Theme 2
535
+ ### Theme / Option 2
439
536
 
440
537
  ...
441
538
 
@@ -445,154 +542,131 @@ Structure:
445
542
 
446
543
  ## Open questions
447
544
 
448
- [What you still don't know — candidates for further research, prototyping, or decisions that need human judgment.]
545
+ [What you still don't know — candidates for further research, prototyping, or human-judgment decisions.]
449
546
 
450
547
  ## Tentative recommendation
451
548
 
452
549
  [Your best guess, clearly marked as tentative. Explain the reasoning so a future reader can re-evaluate when new information arrives.]
453
- \`\`\`
454
-
455
- ### 5. Link aggressively
456
-
457
- Research articles are discovery surfaces — they should link out to **every** related document (sources, sibling research, prior canonical articles, adjacent topics). Under-linked research becomes an island that nobody finds.
458
-
459
- - Every noun-phrase that names another document should be a \`[[Page Name]]\` link, not plain prose. Prefer \`[[Page]]\` over Markdown \`[text](./page.md)\` — only wiki-links participate in the backlinks index.
460
- - Link sources inline where you cite them, not just in the \`sources:\` frontmatter list. "According to \`[[llm-agents-dust-tt]]\`..." is stronger than a bare path.
461
- - Cross-link sibling research: if an adjacent topic has its own research doc, link it in "Open questions" or inline. Readers following one thread should find the others.
462
- - **Redlinks are fine.** If the research surfaces a concept that needs its own page later, \`[[name it now]]\` — the redlink is a breadcrumb for future work.
463
- - Update 1–2 closely-related existing pages to link back to this research (usually under "Further reading" or "See also").
464
-
465
- ### 6. Mark it provisional
466
-
467
- - Set \`status: provisional\` in frontmatter
468
- - Use language like "tentative", "initial findings", "based on current understanding"
469
- - Do NOT write research articles as if they were canonical — that's misleading to future readers
470
- - If you're uncertain, say so explicitly. Research is the layer where uncertainty is allowed to live.
471
-
472
- ### 7. Verify
473
-
474
- - File exists at the chosen path under the content directory
475
- - Has frontmatter with \`title\`, \`description\`, \`status: provisional\`, \`date\`, and a \`sources\` list
476
- - \`exec("ls <dir>")\` should list the file with enrichment
477
- - Linked source files from step 2 exist — broken source links mean something went wrong in \`ingest\`
478
-
479
- ## Non-goals
480
550
 
481
- - **Don't promote to a canonical article** — that's the \`consolidate\` tool's job after the team actually decides
482
- - **Don't hide uncertainty** — research is where uncertainty lives; be explicit about what you don't know
483
- - **Don't skip \`ingest\`** — always capture raw sources first, then analyze
484
- - **Don't overwrite existing research** — if the topic was researched before, either iterate on the existing file or create a clearly-named successor (e.g., \`crdt-alternatives-2.md\`) and mark the old one as superseded
485
-
486
- Full convention: read \`${n}/AGENTS.md\`.`}const si=[`Analyze a topic by gathering sources via ingest and writing provisional findings into the project content directory.`,`Provisional, not canonical — findings live here until decisions solidify.`,``,`**Use when:**`,`- Researching a topic before committing to an approach`,`- Exploring a decision space or comparing alternatives`,`- Synthesizing multiple sources into structured analysis`,`- Spec conversations and exploratory work that is not yet canonical`,``,`**Triggers on:**`,`- "research", "investigate", "compare options for", "analyze alternatives"`,`- User asks to explore trade-offs, gather evidence, or evaluate approaches`,`- A decision needs structured analysis grounded in external sources`].join(`
487
- `);function ci(e,t){e.tool(`research`,si,{topic:A.string().describe(`The topic, question, or anchor URL to research`),cwd:A.string().optional().describe(M)},async e=>{let n=await I(t.resolveCwd,t.config,e.cwd);return n.ok?P(oi(e.topic,n.config.content.dir),{previewUrl:null}):N(`Error: ${n.error}`,!0)})}const li=[`[Requires: Hocuspocus server] Restore a document to a historical version via the CRDT layer.`,`The restore is append-only — it creates a new version with the old content,`,`preserving all history. All connected editors see the change in real-time.`,``,`**Parameters:**`,"- `docName` — Document name to restore, typically without extension. A trailing `.md` or `.mdx` is stripped automatically.","- `commitSha` — The 40-character SHA of the shadow repo commit to restore to."," Use `get_history` to find available versions.",'- `summary` — Optional one-line user-outcome description (≤80 chars). Appears as a bullet in the timeline. If omitted, a default like "Restored to <sha-short>" is generated. Provide your own summary to explain the why. Avoid including secrets or PII — summaries are persisted to git history.'].join(`
488
- `);function ui(e,t){e.tool(`rollback_to_version`,li,{docName:A.string().describe(`Document name to restore`),commitSha:A.string().length(40).regex(/^[0-9a-f]+$/i).describe(`40-character commit SHA from the shadow repo timeline`),summary:A.string().max(200).optional().describe(`Optional one-line user-outcome description (≤80 chars). Defaults to "Restored to <sha-short>" when omitted.`),cwd:A.string().optional().describe(M)},async e=>{let n=await L(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return N(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return N(F,!0);let a=R(e.docName);if(!a.ok)return N(a.error,!0);let o=a.docName,s=await z(i,`/api/history/${e.commitSha}?docName=${encodeURIComponent(o)}`);if(!s.ok)return N(`Error: ${s.error??`Version not found`}`,!0);let c=t.identityRef?.current,l=await B(i,`/api/rollback`,{docName:o,commitSha:e.commitSha,...e.summary===void 0?{}:{summary:e.summary},...c?{agentId:c.connectionId,agentName:c.displayName,clientName:c.clientInfo?.name,colorSeed:c.colorSeed}:{}});if(!l.ok)return N(`Error: ${l.error}`,!0);let u=l.summary&&typeof l.summary==`object`?l.summary:void 0,d=typeof u?.hint==`string`?u.hint:void 0,f=[`Restored "${o}" to version ${e.commitSha.slice(0,8)} (${s.author}, ${s.timestamp}). The change has been applied to all connected editors.`];d&&f.push(d);let p=await V(o,{config:t.config,resolveCwd:t.resolveCwd},r);return P(f.join(`
489
- `),{previewUrl:p?.url??null,...p?{previewUrlSource:p.source}:{},...u?{summary:u}:{}})})}const di=[`[Requires: Hocuspocus server] Save a version checkpoint of all documents.`,`Creates a checkpoint commit in the shadow repo and project repo,`,`preserving the current state of all documents. The checkpoint can later`,"be found via `get_history` and restored via `rollback_to_version`."].join(`
490
- `);function fi(e,t,n,r,i){e.tool(`save_version`,di,{cwd:A.string().optional().describe(M)},async(e={})=>{let a=await L(r,t,n,e.cwd);if(!a.ok)return N(`Error: ${a.error}`,!0);if(!a.url)return N(F,!0);let{url:o}=a,s=i?.current,c=await B(o,`/api/save-version`,{...s?{writers:[{id:`agent-${s.connectionId}`,name:s.displayName,email:`agent-${s.connectionId}@openknowledge.local`}]}:{}});return c.ok?P(`Checkpoint saved. Checkpoint ref: ${c.checkpointRef}`,{checkpointRef:c.checkpointRef,previewUrl:null}):N(`Error: ${c.error}`,!0)})}const pi=[`Search wiki content with metadata-enriched results. Matches are grouped by file; each file is annotated with its title, description, and tags so you can judge relevance without opening it first.`,``,`**Use when:**`,`- Finding all articles mentioning a topic`,`- Locating a specific term across the wiki before deciding which file to read`,``,"Prefer this over your native `Grep` for wiki search — results include article metadata so you can skip irrelevant matches without extra reads.",``,`**Parameters:**`,"- `query` — Literal text to search for (fixed-string match, no regex)","- `case_sensitive` (optional, default false) — case-sensitive match"].join(`
491
- `);function mi(e){let t=new Map;for(let n of e){let e=t.get(n.path);e?e.push(n):t.set(n.path,[n])}return[...t.entries()].map(([e,t])=>({path:e,matches:t}))}async function hi(e,t){let r=await L(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!r.ok)throw Error(r.error);let{cwd:i,config:a,url:o}=r,s=a.mcp.tools.search.maxResults,c=a.content.include,l=a.content.exclude,u=await pn(e.query,i,{caseInsensitive:!(e.case_sensitive??!1),include:c,exclude:[...l,`node_modules`,`.git`,`.claude`,`.changeset`,n],maxResults:s+1}),d=u.length>s,f=d?u.slice(0,s):u,{resolve:p,ui:m}=await H({config:a,resolveCwd:async()=>i},i);if(f.length===0)return{text:`No matches for "${e.query}".`,structured:{query:e.query,matchCount:0,fileCount:0,truncated:!1,results:[],ui:m,cwd:i}};let h=mi(f),g=new Map,_=a.folders;await Promise.all(h.map(async e=>{try{let t=await ir(e.path,{projectDir:i,serverUrl:o,folderRules:_});g.set(e.path,t)}catch{}}));let v=[];v.push(`## Search results for "${e.query}" (${f.length} match${f.length===1?``:`es`} in ${h.length} file${h.length===1?``:`s`})`,``);let y=[];for(let e of h){let t=g.get(e.path),n=t?.title??e.path;v.push(`### ${n} (${e.path})`),t?.tags?.length&&v.push(`Tags: ${t.tags.join(`, `)}`),t?.description&&v.push(`${t.description}`);for(let t of e.matches)v.push(`- Line ${t.line}: ${t.text}`);v.push(``);let r=Jt(e.path),i=p(r);y.push({path:e.path,docName:r,title:t?.title??null,description:t?.description??null,tags:t?.tags??[],matches:e.matches.map(e=>({line:e.line,text:e.text})),previewUrl:i?.url??null,...i?{previewUrlSource:i.source}:{}})}return d&&v.push(`_${f.length} of ${u.length}+ matches shown. Raise \`mcp.tools.search.maxResults\` in config.yml to see more._`),{text:v.join(`
492
- `),structured:{query:e.query,matchCount:f.length,fileCount:h.length,truncated:d,results:y,ui:m,cwd:i}}}function gi(e,t){e.tool(`search`,pi,{query:A.string().describe(`Literal text to search for`),case_sensitive:A.boolean().optional().describe(`Case-sensitive search (default false)`),cwd:A.string().optional().describe("Absolute host path to search in. Defaults only when the MCP client advertises exactly one root; otherwise pass `cwd` explicitly.")},async e=>{try{let{text:n,structured:r}=await hi(e,t);return r?P(n,r):N(n)}catch(e){return N(`Error: ${e instanceof Error?e.message:String(e)}`,!0)}})}const _i=[`[Requires: Hocuspocus server] Find missing link candidates for a target page.`,"Returns JSON with structure: `{ target: { docName, title, aliases }, mentions: [{ source, excerpt, offset }], truncated }`.","Each mention includes an `offset` you can pass to `edit_document` for precision patching.","When `truncated` is true, the scan hit its time budget before reading every admitted document.",``,`**Parameters:**`,'- `docName` — Target page docName, typically without extension (for example, "articles/project-alpha"). A trailing `.md` or `.mdx` is stripped automatically.'].join(`
493
- `);function vi(e,t){e.tool(`suggest_links`,_i,{docName:A.string().describe(`Target page docName`),cwd:A.string().optional().describe(M)},async e=>{let n=await L(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return N(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return N(F,!0);let a=R(e.docName);if(!a.ok)return N(a.error,!0);let o=await z(i,`/api/suggest-links?docName=${encodeURIComponent(a.docName)}`);if(!o.ok)return N(`Error: ${o.error}`,!0);let{ok:s,...c}=o,l=await V(a.docName,{config:t.config,resolveCwd:t.resolveCwd},r);return P(JSON.stringify(c,null,2),{...c,previewUrl:l?.url??null,...l?{previewUrlSource:l.source}:{}})})}const yi=["**IMPORTANT: Before calling this tool, you MUST first call `get_preview_url` and navigate to the returned URL in your preview browser. If `get_preview_url` returns null, start the server first (`open-knowledge start` or `preview_start`), then call `get_preview_url` again. Do NOT call this tool without the preview open. NEVER manually construct the URL.**",``,`[Requires: Hocuspocus server] Write markdown content to a document via the CRDT layer.`,`Content is applied through Hocuspocus and propagated to all connected editors in real-time.`,``,'**Link liberally.** Every noun-phrase that names another document in this knowledge base should be a `[[wiki-link]]`, not plain prose. Backlinks are the primary navigation surface — underlinked documents become islands. Redlinks (links to pages that don\'t exist yet) are fine; they signal "this should exist." Prefer `[[Page Name]]` over Markdown `[text](./page.md)` — only wiki-links participate in the backlinks index.',``,`**Parameters:**`,'- `docName` — Document name, typically without extension (e.g., "my-doc" or "notes/meeting"). A trailing `.md` or `.mdx` is stripped automatically. New documents are created as `.md` by default; to create a `.mdx` file, first place it on disk, then use this tool for edits.',"- `markdown` — Markdown content to write",'- `position` — Where to insert: "append", "prepend", or "replace"','- `summary` — Optional one-line user-outcome description of this edit (≤80 chars). Appears as a bullet in the document timeline so readers can scan intent without opening every diff. Prefer outcome phrasing ("Fixed token-refresh race") over structural ("Added 3 lines"). Avoid including secrets or PII — summaries are persisted to git history.'].join(`
494
- `);function bi(e,t){e.tool(`write_document`,yi,{docName:A.string().describe(`Document name to write to`),markdown:A.string().describe(`Markdown content to write`),position:A.enum([`append`,`prepend`,`replace`]).describe(`Where to insert the content`),summary:Rt,cwd:A.string().optional().describe(M)},async e=>{let n=await L(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return N(`Error: ${n.error}`,!0);let{cwd:r,config:i,url:s}=n;if(!s)return N(F,!0);let c=R(e.docName);if(!c.ok)return N(c.error,!0);let l=t.identityRef?.current,u=await B(s,`/api/agent-write-md`,{docName:c.docName,markdown:e.markdown,position:e.position,...e.summary===void 0?{}:{summary:e.summary},...l?{agentId:l.connectionId,agentName:l.displayName,clientName:l.clientInfo?.name,colorSeed:l.colorSeed}:{}});if(!u.ok)return N(`Error: ${u.error}`,!0);let d=a(o(i,r)),f=U(c.docName,{config:i,lockDir:d}),p=(typeof u.subscriberCount==`number`?u.subscriberCount:void 0)===0,m=Array.isArray(u.hints)?u.hints:void 0,h=u.summary&&typeof u.summary==`object`?u.summary:void 0,g=typeof h?.hint==`string`?h.hint:void 0,_=[`Written successfully (${e.position}).`];if(f&&_.push(`Preview: ${f.url}`),p&&_.push(f?`Warning: no preview is currently attached to "${c.docName}". Open ${f.url} to watch future edits live.`:`Warning: no preview is currently attached to "${c.docName}".`),g&&_.push(g),m)for(let e of m)e.message&&_.push(e.message);let v=_.join(`
495
- `);if(!f&&!p&&!m&&!h)return N(v);let y={};return f&&(y.previewUrl=f.url,y.previewUrlSource=f.source),p&&(y.warning={message:`No preview attached to ${c.docName}.`,previewUrl:f?.url??null}),m&&(y.hints=m),h&&(y.summary=h),P(v,y)})}const xi={exec:lr,"init-content":Hr,ingest:zr,research:si,consolidate:Ht,read_document:Kr,rename_document:ti,search:pi,suggest_links:_i,write_document:yi,edit_document:Yt,get_history:kr,save_version:di,rollback_to_version:li,list_documents:Wr,get_backlinks:Cr,get_forward_links:Dr,get_orphans:Nr,get_hubs:jr,get_dead_links:Tr,get_preview_url:Fr};function Si(e,t){let n=t.logger,r=Lt(e,{logger:t.logger,identityRef:t.identityRef}),i=e=>async r=>{try{let i=await t.resolveCwd(r);return(wt()??n)?.debug(`tool cwd resolved`,{tool:e,cwd:i,...r?{explicit:r}:{}}),i}catch(t){throw(wt()??n)?.warn(`tool call failed`,{tool:e,error:t instanceof Error?t.message:String(t),...r?{explicit:r}:{}}),t}};Sr(r,{resolveCwd:i(`exec`),serverUrl:t.serverUrl,config:t.config}),Ur(r,{config:t.config,resolveCwd:i(`init-content`)}),Br(r,{config:t.config,resolveCwd:i(`ingest`)}),ci(r,{config:t.config,resolveCwd:i(`research`)}),Ut(r,{config:t.config,resolveCwd:i(`consolidate`)}),ei(r,{resolveCwd:i(`read_document`),config:t.config,serverUrl:t.serverUrl}),gi(r,{resolveCwd:i(`search`),config:t.config,serverUrl:t.serverUrl}),vi(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`suggest_links`)}),bi(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`write_document`),identityRef:t.identityRef}),Xt(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`edit_document`),identityRef:t.identityRef}),ai(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`rename_document`),identityRef:t.identityRef}),Ar(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`get_history`)}),fi(r,t.config,t.serverUrl,i(`save_version`),t.identityRef),ui(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`rollback_to_version`),identityRef:t.identityRef}),Gr(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`list_documents`)}),wr(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`get_backlinks`)}),Or(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`get_forward_links`)}),Pr(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`get_orphans`)}),Mr(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`get_hubs`)}),Er(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`get_dead_links`)}),Lr(r,{resolveCwd:i(`get_preview_url`),config:t.config})}function Ci(e){return e&&typeof e==`object`&&`code`in e&&typeof e.code==`string`?e.code:e instanceof Error&&e.name?e.name:typeof e}var wi=class extends Error{};function Ti(e){let t=y(e.startupCwd),n=null,r=null,i=async()=>n===null?(r||=(async()=>{let t=await e.listRoots(),r=await Promise.all(t.roots.map(async e=>e.uri.startsWith(`file://`)?await y(ye(e.uri)):null)),i=[...new Set(r.filter(e=>e!==null))];return n=i,e.logger?.info(`roots resolved`,{roots:i,count:i.length}),i})().finally(()=>{r=null}),await r):n;return{async resolveCwd(n){if(n){let t=await y(n);return e.logger?.debug(`cwd resolved`,{cwd:t,routing:`explicit`}),t}if(e.bypassProjectSelection){let n=await t;return e.logger?.debug(`cwd resolved`,{cwd:n,routing:`bypass`}),n}let r;try{r=await i()}catch(t){throw e.logger?.warn(`roots/list unavailable`,{error:t instanceof Error?t.message:String(t),errorType:Ci(t)}),new wi(`Client roots unavailable; pass cwd explicitly.`)}if(r.length===0)throw new wi(`No client roots available; pass cwd explicitly.`);if(r.length>1)throw new wi(`Multiple roots available; pass cwd explicitly.`);return e.logger?.debug(`cwd resolved`,{cwd:r[0],routing:`single-root`}),r[0]},invalidateRoots(){n=null,r=null,e.logger?.info(`roots cache invalidated`)}}}function Ei(e){let t=y(e.startupCwd),n;return{async resolveCwdForTools(t){let r=await e.resolveCwd(t);return n=r,r},async getKeepaliveCwd(){return e.bypassProjectSelection?await t:n}}}let X;function Di(e,t){let{dir:n,include:r,exclude:i}=e.content,a=i.length>0?i.map(e=>`\`${e}\``).join(`, `):`(none)`,o=t?.dynamicConfig?`## Startup Project Content Layout (Informational)`:`## This project's content layout (live config)`,s=t?.dynamicConfig?"**Multi-project note:** tool calls resolve `.open-knowledge/config.yml` from the **effective cwd** for that invocation (explicit tool `cwd` → exactly one client root → error). The values below describe the MCP process startup project only; they are not a routing fallback.":"**Path contract (`config.yml`):** `.open-knowledge/config.yml` (plus optional `~/.open-knowledge/config.yml`, with CLI/env overrides) owns the `content` keys. The table above is **this MCP session's resolved view** of that contract — same rules, no guessing from folder names. A file is an Open Knowledge document iff it lives under **Content directory**, matches at least one **Include glob**, and is not removed by **Exclude globs** or `.gitignore`.";return`# MCP Instructions v2 — exec-primary (2026-04-13)
496
-
497
- ${o}
498
-
499
- - **Content directory:** \`${n}\`
500
- - **Include globs:** ${r.map(e=>`\`${e}\``).join(`, `)}
501
- - **Exclude globs:** ${a}
502
-
503
- ${s}
551
+ ## Further reading
504
552
 
505
- Paths in \`exec\` commands are resolved relative to the content directory. The sandbox prevents paths escaping it.
553
+ [Links to the ingested sources + adjacent research + any canonical articles on the topic.]
554
+ \`\`\`
506
555
 
507
- **Default rule:** with the usual \`**/*.md\` + \`**/*.mdx\` globs from repo root, **every** such file under the content directory is an Open Knowledge document unless \`.gitignore\` or \`content.exclude\` drops it. Folder names (\`specs/\`, \`reports/\`, …) do not matter. If \`content.include\` was narrowed, only matching paths use \`exec\`.
556
+ ### Voice
508
557
 
509
- ## Navigation — \`exec\` is **mandatory** for Open Knowledge \`.md\` / \`.mdx\` (this server is registered)
558
+ - **Provisional, not canonical.** Use "tentative", "initial findings", "based on current understanding."
559
+ - **Do NOT write as if it were canonical** — that's misleading. Canonicality is \`consolidate\`'s job, after decisions land.
560
+ - **Explicit about uncertainty.** Research is the layer where uncertainty is allowed to live.
510
561
 
511
- **STOP:** Do not use the host IDE's \`Read\`, \`Grep\`, or \`Glob\` on in-scope \`.md\` / \`.mdx\` — use \`exec\` (or typed \`read_document\` / \`search\` when you need fixed \`structuredContent\`). Native file tools skip frontmatter, backlinks, shadow-repo activity, and project git history; reserve them for **source code and non-markdown** paths.
562
+ ---
512
563
 
513
- **MCP clients differ:** Your agent host may expose these tools directly or only through its MCP integration (server name from \`tools/list\`, user-chosen label). **That still counts as this server being available.** Invoke \`exec\` / \`search\` / \`read_document\` the way **your product's docs** describe — not native \`Grep\` on in-scope markdown. Missing a top-level symbol named \`exec\` is not an excuse.
564
+ ## Step 6: Link aggressively + file valuable Q&A back
514
565
 
515
- \`exec\` provides the same enrichment as the typed tools plus bash composability (pipes, \`head\`, \`find\`). One tool covers reading, listing, grepping, and combining them. **Escape hatch:** only if this MCP server is **not** registered for the workspace, or after an MCP **call** failed — say \`Open Knowledge MCP unavailable:\`. Never use the hatch to skip trying MCP first.
566
+ Research articles are discovery surfaces. Under-linked research becomes an island nobody finds.
516
567
 
517
- For paths **outside** the include globs above, use native tools.
568
+ ### Link discipline
518
569
 
519
- Examples:
570
+ - Every noun-phrase that names another document is a link. Use standard markdown: \`[text](./relative/path.md)\`. (Wiki-link syntax \`[[Page]]\` is still parsed for legacy content but no longer the default; matches the OK skill guidance.)
571
+ - Link sources inline where you cite them, not just in the frontmatter \`sources:\` list: "According to [LLM Agents (Dust)](./external-sources/llm-agents-dust.md)..." is stronger than a bare \`sources:\` entry.
572
+ - Cross-link sibling research: if an adjacent topic has its own research doc, link it under "Open questions" or inline. Readers following one thread should find the others.
573
+ - After writing, update 1–2 closely-related existing pages to link back to this research (usually under "Further reading" or "See also"). This is how the research becomes discoverable via backlinks.
574
+ - Never wrap links in backticks; never use HTML anchors — matches the OK skill's linking rules.
520
575
 
521
- - Read a file: \`exec("cat <path>.md")\` — returns file contents + rich enrichment
522
- - List a directory: \`exec("ls <dir>")\` — each result comes with per-file enrichment in \`structuredContent.enrichedPaths\`
523
- - Search: \`exec("grep -rn <term> <dir>")\` — matches + enrichment per matched file
524
- - Combine: \`exec("grep -rn <term> <dir> | head -5")\` — top 5 matches with full enrichment
576
+ ### File valuable Q&A back (Karpathy's "query" step)
525
577
 
526
- Allowlist (read-only): \`cat\`, \`ls\`, \`grep\`, \`find\`, \`head\`, \`tail\`, \`wc\`, \`sort\`, \`uniq\`, \`cut\`. Pipes (\`|\`) work between stages. Redirections, subshells, and writes are rejected with a category-specific error telling you the next step.
578
+ If the user asked a specific question during the research session that produced a citable answer, capture it as its own short page alongside the research — not just as chat. Concrete questions with sourced answers are the highest-signal unit of knowledge you can produce.
527
579
 
528
- ### Scope searches \`grep\` and \`find\` can be slow if unscoped
580
+ Karpathy: *"Search wiki pages, synthesize answers with citations, file valuable outputs back as new pages."*
529
581
 
530
- Recursive \`grep -r\` / \`find\` walk every file under the path, which on a real repo includes source code, build output, and dependencies. For reads inside the content tree, scope deliberately:
582
+ - Short filename: \`what-does-X-mean.md\`, \`how-does-Y-work.md\`
583
+ - Include the same \`sources:\` frontmatter
584
+ - Link the answer from this research doc under "Further reading"
585
+ - Answers too small to justify a separate file stay in chat; don't fragment
531
586
 
532
- - **Filter to markdown:** \`grep -rn TERM --include="*.md" <dir>\` — skips every non-md file.
533
- - **Scope to a known knowledge dir:** \`grep -rn TERM reports/ specs/\` (or whatever folders the project uses) beats \`grep -rn TERM .\`.
534
- - **Bail early:** pipe through \`| head -20\` for bounded output. The server waits for the pipeline to finish before returning, so unscoped commands block on the slowest stage.
535
- - **Existence vs. enumeration:** "does X exist in any tracked doc?" is \`grep -rl PATTERN <dir>\` (list matching files, unbounded) — NOT \`grep -rn PATTERN <dir> | head -N\`. When \`head\` truncates, alphabetically-earlier files dominate the output and later files silently go missing. The server surfaces a banner when \`head\` / \`tail\` hits its cap, but the fix is to pick the right command up front.
536
- - **Auto-prune (built in):** the server transparently adds \`--exclude-dir=\` for \`node_modules\`, \`.git\`, \`dist\`, \`build\`, \`.next\`, \`.turbo\`, \`coverage\`, \`.claude\`, etc. on recursive \`grep\`, and \`-not -path\` equivalents on \`find\`. This saves you from remembering them — but explicit scoping via \`--include\` or a narrower path is still dramatically faster on monorepos.
587
+ ---
537
588
 
538
- ### Why \`exec\` over typed tools
589
+ ## Step 7: Validate
539
590
 
540
- \`exec\` is the default because it subsumes \`read_document\` and \`search\` enrichment paths (same shared helper under the hood) and adds bash composition. The typed tools remain registered as **Typed call sites (advanced)** — present for callers that consume \`structuredContent\` with fixed shapes — but they're not recommended for common agent reads.
591
+ Run this checklist before marking complete:
541
592
 
542
- ## Writing
593
+ - [ ] File exists at the chosen path under the content directory
594
+ - [ ] Frontmatter has \`title\`, \`description\`, \`status: provisional\`, \`date\`, and a \`sources:\` list
595
+ - [ ] \`exec("ls <dir>")\` lists the new file with frontmatter enrichment
596
+ - [ ] \`get_dead_links({ sourceDocNames: ['<path-without-ext>'] })\` returns clean (or each redlink is explicitly justified as a forward-reference)
597
+ - [ ] Every factual claim in Findings cites a source inline
598
+ - [ ] Linked source files from Step 3 all exist (broken source links → \`ingest\` went wrong somewhere)
599
+ - [ ] At least 1–2 neighbor docs now link to this research (per Step 6's "After writing, update ..." rule)
543
600
 
544
- Agent writes to in-scope \`.md\` / \`.mdx\` (paths under \`content.include\`) **must** go through the \`write_document\` / \`edit_document\` MCP tools — never \`exec\` (which is read-only) and never native \`Edit\` / \`sed\`. Routing writes through the server is what captures agent-vs-human attribution in the shadow repo. Writes via other paths land as anonymous \`upstream\` imports and lose attribution.
601
+ ---
545
602
 
546
- ${g}
603
+ ## Step 8: Recap + follow-up directions
547
604
 
548
- ## Linking lean on \`[[wiki-links]]\` aggressively
605
+ Close the loop with the user in conversation:
549
606
 
550
- **When writing or editing any document, link liberally to every other document it relates to.** Open Knowledge's value compounds with link density: backlinks surface cross-document context in every \`exec("cat X.md")\` read, \`get_hubs\` / \`get_orphans\` reveal structure, and agents (you, next session) navigate the knowledge base by following links the way you'd navigate a wiki. A document with no outbound links is an island; an island in a knowledge base is worse than no document at all.
607
+ \`\`\`
608
+ ## Recap
551
609
 
552
- **Defaults when writing:**
610
+ - [Finding 1 — with source]
611
+ - [Finding 2 — with source]
612
+ - [Key trade-off / contradiction surfaced]
613
+ - [1–2 open questions that remain]
553
614
 
554
- - **Every noun-phrase that names another document is a link.** If you mention a concept, project, decision, or entity that has (or should have) its own page, write it as \`[[Page Title]]\` instead of plain prose. Don't stop to check whether the target exists first — a redlink signals "this should exist" to future work. Over-linking is the goal, not the failure mode.
555
- - **Cross-link siblings.** When you create a document in a folder, skim the siblings (\`exec("ls <folder>")\`) and link to the 2–3 most related ones. A "See also" section at the bottom is fine; inline links woven through the prose are better.
556
- - **Link back to sources.** If a document is derived from research, spec decisions, external sources, or prior reports, link to them — don't re-summarize. The reader can follow.
557
- - **Prefer \`[[Page]]\` over Markdown \`[text](./page.md)\`.** Wiki-links resolve by docName (file path minus \`.md\`) and participate in the backlinks index. Markdown links to other wiki files don't.
558
- - **Update both sides when possible.** If you add an important link from A → B, consider whether B should link back to A or to a landing page that lists documents like A.
615
+ **Tentative recommendation:** [state it in one sentence]
559
616
 
560
- **Rule of thumb:** if a human reader would want to click a term to learn more, make it a link. Err on the side of too many links.
617
+ **Follow-up research directions** (Path A candidates for later):
618
+ 1. [Direction 1 — what would it investigate?]
619
+ 2. [Direction 2 — what would it investigate?]
620
+ 3. [Direction 3 — what would it investigate?]
621
+ \`\`\`
561
622
 
562
- ## Cadencemaintain hubs as you create children
623
+ Follow-ups should be **external-source investigations** not actions on the user's codebase (those belong in a spec, not more research). Each direction should be a standalone topic someone could later invoke \`research\` on.
563
624
 
564
- When you create or meaningfully edit a doc inside a folder that has a hub doc (\`INDEX.md\`, \`README.md\`, \`REPORT.md\`, \`SPEC.md\`, or a file whose name matches the folder name — e.g. \`reports/r1/r1.md\`), update the hub to reflect the change before moving to the next child. Write one child → update hub → write next child. Don't batch five children and then the hub.
625
+ In headless mode, write the recap into the research article's "Further reading" section rather than prompting interactively.
565
626
 
566
- **Why:** the browser follows your focus in real time via push-nav on every write. Hub-as-you-go makes your work legible to the human watching — each pulse is a complete thought (child → hub → child → hub), and the hub doc itself functions as the live progress bar. Batched writes make the nav flicker, flatten the narrative, and hide the structure you're building.
627
+ ---
567
628
 
568
- When \`write_document\` creates a doc with zero incoming backlinks and a hub candidate exists in the folder tree, the response includes a \`hints: [{type: 'orphan', parentCandidates: [...], message: ...}]\` entry — that's the soft nudge to interleave the hub update next. Pair with the link-as-you-write discipline above.
629
+ ## Non-goals
569
630
 
570
- ## Frontmatter conventions
631
+ - **Don't promote to a canonical article.** That's \`consolidate\`'s job after a decision actually lands. Premature canonicalization buries uncertainty and misleads future readers.
632
+ - **Don't hide uncertainty.** Research is the layer where "we don't know yet" is acceptable prose. Say it explicitly.
633
+ - **Don't skip \`ingest\`.** Always capture raw sources first, then analyze. An article without preserved sources is opinion.
634
+ - **Don't skip Step 1 scan.** Duplicate research wastes the user's time AND misses chances to extend prior work.
635
+ - **Don't skip the scoping gate in Supervised mode.** The user's rubric shapes everything downstream; you cannot recover a wrong-scope article cheaply.
636
+ - **Don't mix 1P codebase analysis into the article unless asked.** Findings drift from factual synthesis to opinion when you do.
637
+ - **Don't overwrite existing research silently.** If the topic was researched before, either iterate (Path C) or create a clearly-named successor (\`crdt-alternatives-2.md\`) and mark the old one as superseded.
638
+ `}const _i=[`Analyze a topic by gathering sources via ingest and writing provisional findings into the project content directory.`,`Provisional, not canonical — findings live here until decisions solidify.`,``,`**Use when:**`,`- Researching a topic before committing to an approach`,`- Exploring a decision space or comparing alternatives`,`- Synthesizing multiple sources into structured analysis`,`- Spec conversations and exploratory work that is not yet canonical`,``,`**Triggers on:**`,`- "research", "investigate", "compare options for", "analyze alternatives"`,`- User asks to explore trade-offs, gather evidence, or evaluate approaches`,`- A decision needs structured analysis grounded in external sources`].join(`
639
+ `);function vi(e,t){e.tool(`research`,_i,{topic:P.string().describe(`The topic, question, or anchor URL to research`),cwd:P.string().optional().describe(I)},async e=>{let n=await B(t.resolveCwd,t.config,e.cwd);return n.ok?R(gi(e.topic,n.config.content.dir),{previewUrl:null}):L(`Error: ${n.error}`,!0)})}const yi=[`[Requires: Hocuspocus server] Restore a document to a historical version via the CRDT layer.`,`The restore is append-only — it creates a new version with the old content,`,`preserving all history. All connected editors see the change in real-time.`,``,`**Parameters:**`,"- `docName` — Document name to restore, typically without extension. A trailing `.md` or `.mdx` is stripped automatically.","- `commitSha` — The 40-character SHA of the shadow repo commit to restore to."," Use `get_history` to find available versions.",'- `summary` — Optional one-line user-outcome description (≤80 chars). Appears as a bullet in the timeline. If omitted, a default like "Restored to <sha-short>" is generated. Provide your own summary to explain the why. Avoid including secrets or PII — summaries are persisted to git history.'].join(`
640
+ `);function bi(e,t){e.tool(`rollback_to_version`,yi,{docName:P.string().describe(`Document name to restore`),commitSha:P.string().length(40).regex(/^[0-9a-f]+$/i).describe(`40-character commit SHA from the shadow repo timeline`),summary:P.string().max(200).optional().describe(`Optional one-line user-outcome description (≤80 chars). Defaults to "Restored to <sha-short>" when omitted.`),cwd:P.string().optional().describe(I)},async e=>{let n=await V(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return L(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return L(z,!0);let a=H(e.docName);if(!a.ok)return L(a.error,!0);let o=a.docName,s=await U(i,`/api/history/${e.commitSha}?docName=${encodeURIComponent(o)}`);if(!s.ok)return L(`Error: ${s.error??`Version not found`}`,!0);let c=t.identityRef?.current,l=await Xt(i,`/api/rollback`,{docName:o,commitSha:e.commitSha,...e.summary===void 0?{}:{summary:e.summary},...c?{agentId:c.connectionId,agentName:c.displayName,clientName:c.clientInfo?.name,colorSeed:c.colorSeed}:{}});if(!l.ok)return L(`Error: ${l.error}`,!0);let u=l.summary&&typeof l.summary==`object`?l.summary:void 0,d=typeof u?.hint==`string`?u.hint:void 0,f=[`Restored "${o}" to version ${e.commitSha.slice(0,8)} (${s.author}, ${s.timestamp}). The change has been applied to all connected editors.`];d&&f.push(d);let p=await W(o,{config:t.config,resolveCwd:t.resolveCwd},r);return R(f.join(`
641
+ `),{previewUrl:p?.url??null,...p?{previewUrlSource:p.source}:{},...u?{summary:u}:{}})})}const xi=[`[Requires: Hocuspocus server] Save a version checkpoint of all documents.`,`Creates a checkpoint commit in the shadow repo and project repo,`,`preserving the current state of all documents. The checkpoint can later`,"be found via `get_history` and restored via `rollback_to_version`."].join(`
642
+ `);function Si(e,t,n,r,i){e.tool(`save_version`,xi,{cwd:P.string().optional().describe(I)},async(e={})=>{let a=await V(r,t,n,e.cwd);if(!a.ok)return L(`Error: ${a.error}`,!0);if(!a.url)return L(z,!0);let{url:o}=a,s=i?.current,c=await Xt(o,`/api/save-version`,{...s?{writers:[{id:`agent-${s.connectionId}`,name:s.displayName,email:`agent-${s.connectionId}@openknowledge.local`}]}:{}});return c.ok?R(`Checkpoint saved. Checkpoint ref: ${c.checkpointRef}`,{checkpointRef:c.checkpointRef,previewUrl:null}):L(`Error: ${c.error}`,!0)})}const Ci=[`Search wiki content with metadata-enriched results. Matches are grouped by file; each file is annotated with its title, description, and tags so you can judge relevance without opening it first.`,``,`**Use when:**`,`- Finding all articles mentioning a topic`,`- Locating a specific term across the wiki before deciding which file to read`,``,"Prefer this over your native `Grep` for wiki search — results include article metadata so you can skip irrelevant matches without extra reads.",``,`**Parameters:**`,"- `query` — Literal text to search for (fixed-string match, no regex)","- `case_sensitive` (optional, default false) — case-sensitive match"].join(`
643
+ `);function wi(e){let t=new Map;for(let n of e){let e=t.get(n.path);e?e.push(n):t.set(n.path,[n])}return[...t.entries()].map(([e,t])=>({path:e,matches:t}))}async function Ti(e,t){let r=await V(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!r.ok)throw Error(r.error);let{cwd:i,config:a,url:o}=r,s=a.mcp.tools.search.maxResults,c=a.content.include,l=a.content.exclude,u=await En(e.query,i,{caseInsensitive:!(e.case_sensitive??!1),include:c,exclude:[...l,`node_modules`,`.git`,`.claude`,`.changeset`,n],maxResults:s+1}),d=u.length>s,f=d?u.slice(0,s):u,{resolve:p,ui:m}=await G({config:a,resolveCwd:async()=>i},i);if(f.length===0)return{text:`No matches for "${e.query}".`,structured:{query:e.query,matchCount:0,fileCount:0,truncated:!1,results:[],ui:m,cwd:i}};let h=wi(f),g=new Map,_=a.folders;await Promise.all(h.map(async e=>{try{let t=await _r(e.path,{projectDir:i,serverUrl:o,folderRules:_});g.set(e.path,t)}catch{}}));let v=[];v.push(`## Search results for "${e.query}" (${f.length} match${f.length===1?``:`es`} in ${h.length} file${h.length===1?``:`s`})`,``);let y=[];for(let e of h){let t=g.get(e.path),n=t?.title??e.path;v.push(`### ${n} (${e.path})`),t?.tags?.length&&v.push(`Tags: ${t.tags.join(`, `)}`),t?.description&&v.push(`${t.description}`);for(let t of e.matches)v.push(`- Line ${t.line}: ${t.text}`);v.push(``);let r=an(e.path),i=p(r);y.push({path:e.path,docName:r,title:t?.title??null,description:t?.description??null,tags:t?.tags??[],matches:e.matches.map(e=>({line:e.line,text:e.text})),previewUrl:i?.url??null,...i?{previewUrlSource:i.source}:{}})}return d&&v.push(`_${f.length} of ${u.length}+ matches shown. Raise \`mcp.tools.search.maxResults\` in config.yml to see more._`),{text:v.join(`
644
+ `),structured:{query:e.query,matchCount:f.length,fileCount:h.length,truncated:d,results:y,ui:m,cwd:i}}}function Ei(e,t){e.tool(`search`,Ci,{query:P.string().describe(`Literal text to search for`),case_sensitive:P.boolean().optional().describe(`Case-sensitive search (default false)`),cwd:P.string().optional().describe("Absolute host path to search in. Defaults only when the MCP client advertises exactly one root; otherwise pass `cwd` explicitly.")},async e=>{try{let{text:n,structured:r}=await Ti(e,t);return r?R(n,r):L(n)}catch(e){return L(`Error: ${e instanceof Error?e.message:String(e)}`,!0)}})}const Di=[`[Requires: Hocuspocus server] Find missing link candidates for a target page.`,"Returns JSON with structure: `{ target: { docName, title, aliases }, mentions: [{ source, excerpt, offset }], truncated }`.","Each mention includes an `offset` you can pass to `edit_document` for precision patching.","When `truncated` is true, the scan hit its time budget before reading every admitted document.",``,`**Parameters:**`,'- `docName` — Target page docName, typically without extension (for example, "articles/project-alpha"). A trailing `.md` or `.mdx` is stripped automatically.'].join(`
645
+ `);function Oi(e,t){e.tool(`suggest_links`,Di,{docName:P.string().describe(`Target page docName`),cwd:P.string().optional().describe(I)},async e=>{let n=await V(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return L(`Error: ${n.error}`,!0);let{cwd:r,url:i}=n;if(!i)return L(z,!0);let a=H(e.docName);if(!a.ok)return L(a.error,!0);let o=await U(i,`/api/suggest-links?docName=${encodeURIComponent(a.docName)}`);if(!o.ok)return L(`Error: ${o.error}`,!0);let{ok:s,...c}=o,l=await W(a.docName,{config:t.config,resolveCwd:t.resolveCwd},r);return R(JSON.stringify(c,null,2),{...c,previewUrl:l?.url??null,...l?{previewUrlSource:l.source}:{}})})}const ki=["**IMPORTANT: Before calling this tool, you MUST first call `get_preview_url` and navigate to the returned URL in your preview browser. If `get_preview_url` returns null, start the server first (`open-knowledge start` or `preview_start`), then call `get_preview_url` again. Do NOT call this tool without the preview open. NEVER manually construct the URL.**",``,`[Requires: Hocuspocus server] Write markdown content to a document via the CRDT layer.`,`Content is applied through Hocuspocus and propagated to all connected editors in real-time.`,``,'**Link liberally.** Every noun-phrase that names another document in this knowledge base should be a `[[wiki-link]]`, not plain prose. Backlinks are the primary navigation surface — underlinked documents become islands. Redlinks (links to pages that don\'t exist yet) are fine; they signal "this should exist." Prefer `[[Page Name]]` over Markdown `[text](./page.md)` — only wiki-links participate in the backlinks index.',``,`**Parameters:**`,'- `docName` — Document name, typically without extension (e.g., "my-doc" or "notes/meeting"). A trailing `.md` or `.mdx` is stripped automatically. New documents are created as `.md` by default; to create a `.mdx` file, first place it on disk, then use this tool for edits.',"- `markdown` — Markdown content to write",'- `position` — Where to insert: "append", "prepend", or "replace"','- `summary` — Optional one-line user-outcome description of this edit (≤80 chars). Appears as a bullet in the document timeline so readers can scan intent without opening every diff. Prefer outcome phrasing ("Fixed token-refresh race") over structural ("Added 3 lines"). Avoid including secrets or PII — summaries are persisted to git history.'].join(`
646
+ `);function Ai(e,t){e.tool(`write_document`,ki,{docName:P.string().describe(`Document name to write to`),markdown:P.string().describe(`Markdown content to write`),position:P.enum([`append`,`prepend`,`replace`]).describe(`Where to insert the content`),summary:Ut,cwd:P.string().optional().describe(I)},async e=>{let n=await V(t.resolveCwd,t.config,t.serverUrl,e.cwd);if(!n.ok)return L(`Error: ${n.error}`,!0);let{cwd:r,config:i,url:s}=n;if(!s)return L(z,!0);let c=H(e.docName);if(!c.ok)return L(c.error,!0);let l=t.identityRef?.current,u=await Xt(s,`/api/agent-write-md`,{docName:c.docName,markdown:e.markdown,position:e.position,...e.summary===void 0?{}:{summary:e.summary},...l?{agentId:l.connectionId,agentName:l.displayName,clientName:l.clientInfo?.name,colorSeed:l.colorSeed}:{}});if(!u.ok)return L(`Error: ${u.error}`,!0);let d=a(o(i,r)),f=on(c.docName,{config:i,lockDir:d}),p=(typeof u.subscriberCount==`number`?u.subscriberCount:void 0)===0,m=Array.isArray(u.hints)?u.hints:void 0,h=u.summary&&typeof u.summary==`object`?u.summary:void 0,g=typeof h?.hint==`string`?h.hint:void 0,_=[`Written successfully (${e.position}).`];if(f&&_.push(`Preview: ${f.url}`),p&&_.push(f?`Warning: no preview is currently attached to "${c.docName}". Open ${f.url} to watch future edits live.`:`Warning: no preview is currently attached to "${c.docName}".`),g&&_.push(g),m)for(let e of m)e.message&&_.push(e.message);let v=_.join(`
647
+ `);if(!f&&!p&&!m&&!h)return L(v);let y={};return f&&(y.previewUrl=f.url,y.previewUrlSource=f.source),p&&(y.warning={message:`No preview attached to ${c.docName}.`,previewUrl:f?.url??null}),m&&(y.hints=m),h&&(y.summary=h),R(v,y)})}function ji(e,t){let n=t.logger,r=Ht(e,{logger:t.logger,identityRef:t.identityRef}),i=e=>async r=>{try{let i=await t.resolveCwd(r);return(kt()??n)?.debug(`tool cwd resolved`,{tool:e,cwd:i,...r?{explicit:r}:{}}),i}catch(t){throw(kt()??n)?.warn(`tool call failed`,{tool:e,error:t instanceof Error?t.message:String(t),...r?{explicit:r}:{}}),t}};Fr(r,{resolveCwd:i(`exec`),serverUrl:t.serverUrl,config:t.config}),$r(r,{config:t.config,resolveCwd:i(`ingest`)}),vi(r,{config:t.config,resolveCwd:i(`research`)}),$t(r,{config:t.config,resolveCwd:i(`consolidate`)}),ui(r,{resolveCwd:i(`read_document`),config:t.config,serverUrl:t.serverUrl}),Ei(r,{resolveCwd:i(`search`),config:t.config,serverUrl:t.serverUrl}),Oi(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`suggest_links`)}),Ai(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`write_document`),identityRef:t.identityRef}),cn(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`edit_document`),identityRef:t.identityRef}),hi(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`rename_document`),identityRef:t.identityRef}),Ur(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`get_history`)}),Si(r,t.config,t.serverUrl,i(`save_version`),t.identityRef),bi(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`rollback_to_version`),identityRef:t.identityRef}),ti(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`list_documents`)}),Lr(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`get_backlinks`)}),Vr(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`get_forward_links`)}),qr(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`get_orphans`)}),Gr(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`get_hubs`)}),zr(r,{serverUrl:t.serverUrl,config:t.config,resolveCwd:i(`get_dead_links`)}),Xr(r,{resolveCwd:i(`get_preview_url`),config:t.config})}function Mi(e){return e&&typeof e==`object`&&`code`in e&&typeof e.code==`string`?e.code:e instanceof Error&&e.name?e.name:typeof e}var Ni=class extends Error{};function Pi(e){let t=D(e.startupCwd),n=null,r=null,i=async()=>n===null?(r||=(async()=>{let t=await e.listRoots(),r=await Promise.all(t.roots.map(async e=>e.uri.startsWith(`file://`)?await D(we(e.uri)):null)),i=[...new Set(r.filter(e=>e!==null))];return n=i,e.logger?.info(`roots resolved`,{roots:i,count:i.length}),i})().finally(()=>{r=null}),await r):n;return{async resolveCwd(n){if(n){let t=await D(n);return e.logger?.debug(`cwd resolved`,{cwd:t,routing:`explicit`}),t}if(e.bypassProjectSelection){let n=await t;return e.logger?.debug(`cwd resolved`,{cwd:n,routing:`bypass`}),n}let r;try{r=await i()}catch(t){throw e.logger?.warn(`roots/list unavailable`,{error:t instanceof Error?t.message:String(t),errorType:Mi(t)}),new Ni(`Client roots unavailable; pass cwd explicitly.`)}if(r.length===0)throw new Ni(`No client roots available; pass cwd explicitly.`);if(r.length>1)throw new Ni(`Multiple roots available; pass cwd explicitly.`);return e.logger?.debug(`cwd resolved`,{cwd:r[0],routing:`single-root`}),r[0]},invalidateRoots(){n=null,r=null,e.logger?.info(`roots cache invalidated`)}}}function Fi(e){let t=D(e.startupCwd),n;return{async resolveCwdForTools(t){let r=await e.resolveCwd(t);return n=r,r},async getKeepaliveCwd(){return e.bypassProjectSelection?await t:n}}}let X;function Ii(e,t){let{dir:n,include:r,exclude:i}=e.content,a=i.length>0?i.map(e=>`\`${e}\``).join(`, `):`(none)`;return`# Open Knowledge (OK) — collaborative markdown via MCP
571
648
 
572
- Open Knowledge has two metadata surfaces that merge at read time:
649
+ **STOP native tools on in-scope \`.md\` / \`.mdx\`.** Do NOT use host-native \`Read\`, \`Grep\`, \`Glob\`, \`Edit\`, \`Write\` on markdown inside the content dir. Reads: \`exec\` / \`read_document\` / \`search\`. Writes: \`write_document\` / \`edit_document\` ONLY, each preceded by \`get_preview_url(docName)\` + browser open.
573
650
 
574
- 1. **Per-file frontmatter** YAML at the top of each \`.md\` / \`.mdx\`: \`title\` (required), \`description\` (required), \`tags\` (recommended). This is where a file's own identity lives.
575
- 2. **Folder-level defaults via \`.open-knowledge/config.yml\` \`folders:\`** — declare \`title\` / \`description\` / \`tags\` defaults keyed by glob \`match:\`. Rules apply in declaration order; later matches override earlier scalars. Tags concatenate across ALL matching rules (in declaration order), with file tags appended last, and first-occurrence preserved on dedup. The file's own frontmatter wins per-scalar; folder defaults fill in blanks.
651
+ Content dir: ${n}. Include: ${r.map(e=>`\`${e}\``).join(`, `)}. Exclude: ${a}.
576
652
 
577
- Folder metadata lives in \`config.yml\`, **not** in content files — this is intentionally different from the rejected \`INDEX.md\`-inside-content pattern. The merge happens on every \`exec\` / \`read_document\` / \`search\` call and is never written back to disk.
653
+ ## Reads
578
654
 
579
- ## Tools
655
+ \`exec("cat <path>.md")\` / \`exec("ls <dir>")\` / \`exec("grep -rn <term> <dir>")\` — primary; returns contents + enrichment. Typed \`read_document\` / \`search\` when you need \`structuredContent\`.
580
656
 
581
- **Primary:**
582
- - \`exec\` — read-only bash with enriched output (see above).
657
+ ## Preview before every write (REQUIRED)
583
658
 
584
- **Workflow (instructional tools):**
585
- - \`init-content\`, \`ingest\`, \`research\`, \`consolidate\` — each returns structured instructions you follow. Output text includes the live \`content.dir\` value (${n}) so you don't need to re-read the config.
659
+ Every \`write_document\` / \`edit_document\` MUST be preceded by \`get_preview_url(docName)\` → open returned URL in your preview browser → call write tool.
586
660
 
587
- **Writes:**
588
- - \`write_document\`, \`edit_document\`, \`rename_document\`, \`undo_agent_edit\`, \`redo_agent_edit\` — mutate the CRDT through the server; attribution captured.
661
+ If \`get_preview_url\` returns \`null\`, start the UI (\`open-knowledge ui\`, or \`preview_start("open-knowledge-ui")\` in Claude Code). Native \`Edit\` / \`sed\` on in-scope markdown bypasses the CRDT and loses agent attribution.
589
662
 
590
- **Typed call sites (advanced) — prefer \`exec\` for common reads:**
591
- - \`read_document\`, \`search\`, \`list_documents\`, \`get_backlinks\`, \`get_forward_links\`, \`get_orphans\`, \`get_hubs\`.
663
+ ## Full guidance
592
664
 
593
- ${Object.entries(xi).map(([e,t])=>`### \`${e}\`\n${t}`).join(`
665
+ Detailed conventions (wiki-link authoring, frontmatter, anti-patterns) live in the installed \`open-knowledge\` Agent Skill. If missing, run \`npx @inkeep/open-knowledge init\`.
594
666
 
595
- `)}
596
- `}async function Oi(e,t){try{let t=e.replace(`ws://`,`http://`).replace(`wss://`,`https://`);return(await fetch(`${t}/api/document`,{signal:AbortSignal.timeout(2e3)})).ok}catch(n){return t.warn(`Hocuspocus probe failed`,{serverUrl:e,error:n instanceof Error?n.message:String(n)}),!1}}async function ki(n){let{projectDir:r,serverUrl:i,config:a,startupConfig:o,bypassProjectSelection:s=!1}=n;if(X=St(),X.info(`MCP server starting`,{startupCwd:r,bypassProjectSelection:s,serverUrlType:typeof i==`string`?`explicit`:`lazy`}),typeof i==`string`){let e=await Oi(i,X);X.info(`Hocuspocus detection complete`,{serverUrl:i,available:e})}else X.info(`server discovery is lazy per effective cwd`);let c=new Ae({name:t,version:e},{instructions:Di(o,{dynamicConfig:typeof a==`function`&&!s})}),l=Ti({startupCwd:r,bypassProjectSelection:s,listRoots:()=>c.server.listRoots(),logger:X}),u=Ei({startupCwd:r,resolveCwd:l.resolveCwd,bypassProjectSelection:s}),d=u.resolveCwdForTools;c.server.setNotificationHandler(Me,async()=>{l.invalidateRoots()});let f=async e=>{if(typeof i==`string`)return i.replace(`ws://`,`http://`).replace(`wss://`,`https://`);let t=e??await d();return(typeof i==`function`?await i(t):i)?.replace(`ws://`,`http://`).replace(`wss://`,`https://`)},p=be(),m=process.env.AGENT_LABEL||void 0,h={current:{connectionId:p,label:m,displayName:m??`Agent`,colorSeed:m??p}};c.server.oninitialized=()=>{let e=c.server.getClientVersion();h.current={connectionId:p,clientInfo:e?{name:e.name,version:e.version}:void 0,label:m,displayName:m??e?.name??`Agent`,colorSeed:m??e?.name??p},X?.info(`agent identity established`,{displayName:h.current.displayName,connectionId:p.slice(0,8),clientName:e?.name})},Si(c,{serverUrl:f,resolveCwd:d,config:a,identityRef:h,logger:X});let g=new je;await c.connect(g),X.info(`MCP server running on stdio`);let{startKeepalive:_}=await import(`./keepalive-BqSeN73F.mjs`),v=_({resolveWsUrl:async()=>{let e=await u.getKeepaliveCwd();if(!e)return;let t=await f(e);if(t)return t.replace(/^http:/,`ws:`).replace(/^https:/,`wss:`)},connectionId:`agent-${p}`,logger:X.child(`keepalive`)}),y=e=>{X?.info(`MCP server shutting down`,{signal:e});try{v.close()}catch{}process.exit(0)};process.on(`SIGINT`,()=>y(`SIGINT`)),process.on(`SIGTERM`,()=>y(`SIGTERM`))}function Ai(e){if(e===void 0||e===``)return;let t=Number.parseInt(e,10);if(!(Number.isNaN(t)||t<=0))return t}function ji(e){if(e.portOverride!==void 0){let t=Number.parseInt(e.portOverride,10);if(Number.isNaN(t))return{action:`disk-only`,message:`invalid --port value '${e.portOverride}' — disk-only mode`};if(t>0){let n=`ws://${e.host}:${t}`;return{action:`connect`,url:n,message:`using --port override, connecting to ${n}`}}return{action:`disk-only`,message:`--port=0 — disk-only mode`}}let t=e.readLock();if(t&&t.port>0&&e.isAlive(t.pid)){let e=`ws://localhost:${t.port}`;return{action:`connect`,url:e,message:`connected to running instance at ${e} (pid ${t.pid})`}}return e.envAutoStart===`0`?{action:`disk-only`,message:`auto-spawn disabled via OK_MCP_AUTOSTART=0 — disk-only mode`}:e.configAutoStart?t?{action:`spawn`,message:`existing lock is not usable (port=${t.port}, pid=${t.pid}) — spawning ok start`}:{action:`spawn`,message:`no running instance — spawning ok start`}:{action:`disk-only`,message:`auto-spawn disabled via config.mcp.autoStart=false — disk-only mode`}}async function Mi(e){let t=e.readLock??(()=>h(e.lockDir)),n=e.isAlive??m,r=e.sleep??(e=>new Promise(t=>setTimeout(t,e))),i=e.spawn??Se,a=e.readErrorLog??(e=>E(e)?O(e,`utf-8`).trim():``),o=e.openErrorLog??(e=>te(e,`w`)),s=e.closeFd??(e=>T(e)),c=e.timeoutMs??5e3,l=e.pollIntervalMs??100,u=ji({host:e.host,portOverride:e.portOverride,envAutoStart:e.envAutoStart,configAutoStart:e.configAutoStart,readLock:t,isAlive:n});if(e.logger?.info(`auto-start decision`,{action:u.action,message:u.message,contentDir:e.contentDir}),u.action===`connect`)return{serverUrl:u.url,message:u.message};if(u.action===`disk-only`)return{serverUrl:void 0,message:u.message};E(e.lockDir)||D(e.lockDir,{recursive:!0});let d=fe(e.lockDir,`last-spawn-error.log`),f=o(d),p,g,_=S();e.logger?.info(`spawning server`,{command:_.command,cwd:e.contentDir,timeoutMs:c});try{try{p=i(_.command,[..._.prefixArgs,`start`],{detached:!0,stdio:[`ignore`,`ignore`,f],cwd:e.contentDir}),p.on(`error`,e=>{g=e instanceof Error?e.message:String(e)}),p.unref()}catch(e){g=e instanceof Error?e.message:String(e)}}finally{try{s(f)}catch{}}let v=Date.now()+c;for(;Date.now()<v;){if(g){let t=a(d);throw e.logger?.error(`spawn failed`,void 0,{error:g,stderr:t}),Error(`Error: spawn failed: ${g}${t?` stderr:\n${t}`:``}`)}await r(l);let i=t();if(i&&i.port>0&&n(i.pid)){let t=`ws://localhost:${i.port}`;return e.logger?.info(`server ready after spawn`,{url:t,pid:i.pid}),{serverUrl:t,message:`spawned ok start; connected at ${t} (pid ${i.pid})`}}}if(g){let t=a(d);throw e.logger?.error(`spawn failed (post-deadline)`,void 0,{error:g,stderr:t}),Error(`Error: spawn failed: ${g}${t?` stderr:\n${t}`:``}`)}let y=a(d),ee=(c/1e3).toFixed(c%1e3==0?0:2),b=p?.pid,x=``;throw typeof b==`number`&&(x=n(b)?` child pid=${b} is still running — raise OK_MCP_SPAWN_TIMEOUT_MS if this is a slow boot.`:` child pid=${b} exited — check last-spawn-error.log.`),e.logger?.error(`spawn poll timeout`,void 0,{timeoutMs:c,childPid:b,childAlive:typeof b==`number`?n(b):void 0,stderr:y||void 0}),Error(`Error: server did not start within ${ee}s.${x}${y?` stderr:\n${y}`:``}`)}function Ni(e){if(e.portOverride!==void 0){let t=Number.parseInt(e.portOverride,10);if(Number.isNaN(t)||t<=0)return async()=>void 0;let n=`ws://${e.host}:${t}`;return async()=>n}let t=e.ensureServerRunningFn??Mi,n=e.cacheMs??1e3,r=new Map,i=new Map;return async s=>{let c=await y(s??e.startupCwd),l=Date.now(),u=r.get(c);if(u&&u.expiresAt>l)return e.logger?.debug(`server url cache hit`,{cwd:c,url:u.url}),u.url;let d=i.get(c);if(d)return e.logger?.debug(`server url resolution pending`,{cwd:c}),await d;e.logger?.debug(`server url cache miss`,{cwd:c});let f=(async()=>{let i=await e.resolveConfig(c),s=o(i,c),l=a(s),u=e.readLock,d=u?()=>u(l):void 0,f=await t({lockDir:l,contentDir:s,host:i.server.host,portOverride:void 0,envAutoStart:e.envAutoStart,configAutoStart:i.mcp.autoStart,logger:e.logger,timeoutMs:e.timeoutMs,pollIntervalMs:e.pollIntervalMs,spawn:e.spawn,readLock:d,isAlive:e.isAlive,sleep:e.sleep,readErrorLog:e.readErrorLog,openErrorLog:e.openErrorLog,closeFd:e.closeFd});return r.set(c,{url:f.serverUrl,expiresAt:Date.now()+n}),f.serverUrl})();i.set(c,f);try{return await f}finally{i.delete(c)}}}function Pi(e){return new C(`mcp`).description(`Start MCP stdio server for project knowledge base`).option(`-p, --port <port>`,`Override port discovery and connect to this port (0 = disk-only)`,void 0).action(async t=>{try{let n=e(),r=process.cwd(),i=b({startupCwd:r,startupConfig:n}),a=Ai(process.env.OK_MCP_SPAWN_TIMEOUT_MS),o,s;if(t.port!==void 0){let e=Number.parseInt(t.port,10);Number.isNaN(e)?(o=void 0,s=`invalid --port value '${t.port}' — disk-only mode`):e>0?(o=`ws://${n.server.host}:${e}`,s=`using --port override, connecting to ${o}`):(o=void 0,s=`--port=0 — disk-only mode`)}else o=Ni({startupCwd:r,resolveConfig:i,host:n.server.host,portOverride:void 0,envAutoStart:process.env.OK_MCP_AUTOSTART,timeoutMs:a}),s=`project server discovery/autostart is lazy per effective cwd`;process.stderr.write(`[mcp] ${s}\n`),await ki({projectDir:r,serverUrl:o,config:i,startupConfig:n,bypassProjectSelection:t.port!==void 0})}catch(e){process.stderr.write(`MCP server failed to start: ${e instanceof Error?e.message:String(e)}\n`),process.exitCode=1}})}function Fi(e){return new C(`preview`).description(`Show what content the watcher will track (read-only)`).action(async()=>{let{previewContent:t,formatPreviewBlock:n}=await import(`./preview-C5sfPOVh.mjs`),r=e(),i=process.cwd(),a=o(r,i),s;try{s=t({projectDir:i,contentDir:a,include:r.content.include,exclude:r.content.exclude})}catch(e){console.error(`Content preview failed: ${e instanceof Error?e.message:String(e)}`),process.exitCode=1;return}process.stdout.write(`${n(s,i)}\n`),s.totalCount===0&&s.warnings.length>0&&(process.exitCode=1)})}function Z(e,t){e&&process.stdout.write(`${JSON.stringify(t)}\n`)}async function Ii(e,t,n=process.cwd()){let r=e.op??`sync`,i=h(a(o(t,n)));if(i&&i.port>0){let t=`http://127.0.0.1:${i.port}/api/sync/trigger`;e.json||process.stderr.write(`Triggering ${r} via running server (port ${i.port})…\n`);try{let n=await fetch(t,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({op:r})});if(!n.ok){let e=await n.json().catch(()=>({}));throw Error(e.error??`Server responded with ${n.status}`)}Z(e.json,{type:`triggered`,op:r,port:i.port}),e.json||process.stderr.write(`✓ ${r} triggered\n`);return}catch(t){let n=t instanceof Error?t.message:String(t);e.json||process.stderr.write(`Server trigger failed (${n}), running directly…\n`)}}e.json||process.stderr.write(`Running ${r} directly (no live server)…\n`);let s=Ce({baseDir:n});if(r===`sync`||r===`pull`){Z(e.json,{type:`step`,step:`pull`});let t=await s.pull();Z(e.json,{type:`pull`,summary:t.summary}),e.json||process.stderr.write(` pull: ${t.summary.changes} changes\n`)}(r===`sync`||r===`push`)&&(Z(e.json,{type:`step`,step:`push`}),await s.push(),Z(e.json,{type:`push`,ok:!0}),e.json||process.stderr.write(` push: ok
597
- `)),Z(e.json,{type:`complete`,op:r}),e.json||process.stderr.write(`✓ ${r} complete\n`)}function Li(e){return new C(`sync`).description(`Commit, pull, and push to the remote`).option(`--json`,`Output JSONL progress events`,!1).action(async t=>{try{await Ii({json:t.json,op:`sync`},e())}catch(e){let n=e instanceof Error?e.message:String(e);t.json?process.stdout.write(`${JSON.stringify({type:`error`,message:n})}\n`):process.stderr.write(`✗ sync failed: ${n}\n`),process.exit(1)}})}function Ri(e){return new C(`pull`).description(`Pull changes from the remote`).option(`--json`,`Output JSONL progress events`,!1).action(async t=>{try{await Ii({json:t.json,op:`pull`},e())}catch(e){let n=e instanceof Error?e.message:String(e);t.json?process.stdout.write(`${JSON.stringify({type:`error`,message:n})}\n`):process.stderr.write(`✗ pull failed: ${n}\n`),process.exit(1)}})}function zi(e){return new C(`push`).description(`Push commits to the remote`).option(`--json`,`Output JSONL progress events`,!1).action(async t=>{try{await Ii({json:t.json,op:`push`},e())}catch(e){let n=e instanceof Error?e.message:String(e);t.json?process.stdout.write(`${JSON.stringify({type:`error`,message:n})}\n`):process.stderr.write(`✗ push failed: ${n}\n`),process.exit(1)}})}function Bi(e,t){return{server:Vi(`server`,e),ui:Vi(`ui`,t)}}function Vi(e,t){switch(t.status){case`missing`:return{name:e,state:`missing`,alive:!1};case`corrupt`:return{name:e,state:`corrupt`,alive:!1};case`foreign-host`:return{name:e,state:`foreign-host`,pid:t.lock.pid,port:t.lock.port,startedAt:t.lock.startedAt,host:t.lock.hostname,alive:`unknown`};case`dead-pid`:return{name:e,state:`dead-pid`,pid:t.lock.pid,port:t.lock.port,startedAt:t.lock.startedAt,host:t.lock.hostname,alive:!1};case`alive`:return{name:e,state:`alive`,pid:t.lock.pid,port:t.lock.port,startedAt:t.lock.startedAt,host:t.lock.hostname,alive:!0}}}function Hi(e){return`${Ui(e.server)}\n${Ui(e.ui)}`}function Ui(e){let t=e.name===`server`?`server`:`ui `;return e.state===`missing`?`${t} not running`:e.state===`corrupt`?`${t} lock file corrupt — run \`ok clean\``:e.state===`foreign-host`?`${t} foreign host (${e.host}) pid=${e.pid} port=${e.port}`:e.state===`dead-pid`?`${t} stale (dead pid=${e.pid}) — run \`ok clean\``:`${t} alive pid=${e.pid} port=${e.port} started=${e.startedAt}`}function Wi(e){let t=e.inspect??(t=>st(e.lockDir,t)),n=e.log??(e=>console.log(e)),r=Bi(t(`server`),t(`ui`));return e.json?n(JSON.stringify(r,null,2)):n(Hi(r)),r}function Gi(e){return new C(`status`).description(`Show live state of the server + ui lockfiles for this project`).option(`--json`,`Emit structured JSON instead of formatted text`).action(t=>{Wi({lockDir:a(o(e(),process.cwd())),json:t.json===!0})})}function Ki(e,t){let n=[];return e.status===`alive`&&n.push({name:`server`,pid:e.lock.pid,port:e.lock.port}),t.status===`alive`&&n.push({name:`ui`,pid:t.lock.pid,port:t.lock.port}),{targets:n}}function qi(e){let t=e.inspect??(t=>st(e.lockDir,t)),n=e.kill??((e,t)=>process.kill(e,t)),r=e.log??(e=>console.log(e)),i=e.error??(e=>console.error(e)),a=Ki(t(`server`),t(`ui`));if(a.targets.length===0)return r(`No running open-knowledge processes.`),{stopped:[],failed:[],hadTargets:!1};let o=[],s=[];for(let e of a.targets)try{n(e.pid,`SIGTERM`),o.push(e)}catch(t){s.push({target:e,error:t instanceof Error?t.message:String(t)})}return o.length>0&&r(`Stopped: ${o.map(e=>`${e.name} (pid=${e.pid}, port=${e.port})`).join(`, `)}`),s.length>0&&i(`Failed to stop: ${s.map(({target:e,error:t})=>`${e.name} (pid=${e.pid}): ${t}`).join(`; `)}`),{stopped:o,failed:s,hadTargets:!0}}function Ji(e){return new C(`stop`).description(`Stop the running open-knowledge server and UI (live only)`).action(()=>{qi({lockDir:a(o(e(),process.cwd()))}).failed.length>0&&(process.exitCode=1)})}const Yi=1e4,Xi=[`connection`,`keep-alive`,`proxy-authenticate`,`proxy-authorization`,`te`,`trailer`,`transfer-encoding`,`upgrade`,`cookie`,`set-cookie`];async function Zi(e){let t=e.upstreamTimeoutMs??Yi,n=De((n,r)=>{$i(n,r,e.upstreamHost,e.upstreamPort,t)});await new Promise((t,r)=>{let i=e=>r(e);n.once(`error`,i),n.listen(e.listenPort,e.host,()=>{n.off(`error`,i),t()})});let r=n.address();return{httpServer:n,port:typeof r==`object`&&r?r.port:e.listenPort,close:()=>new Promise(e=>{n.close(()=>e())})}}function Qi(e,t,n){$i(e,t,n.upstreamHost,n.upstreamPort,n.upstreamTimeoutMs??Yi)}function $i(e,t,n,r,i){let a={...e.headers};delete a.host;for(let e of Xi)delete a[e];e.setTimeout(3e4,()=>{if(t.headersSent)try{t.end()}catch{}else try{t.writeHead(408,{"Content-Type":`text/plain`}),t.end(`Request Timeout`)}catch{}try{e.socket?.destroy()}catch{}});let o=Oe({host:n,port:r,method:e.method,path:e.url,headers:{...a,host:`${n}:${r}`}},e=>{let n={...e.headers};for(let e of Xi)delete n[e];t.writeHead(e.statusCode??502,n),e.pipe(t),e.once(`error`,()=>{try{t.end()}catch{}})});i>0&&o.setTimeout(i,()=>{if(!t.headersSent)t.writeHead(504,{"Content-Type":`text/plain`}),t.end(`Gateway Timeout`);else try{t.end()}catch{}o.destroy()}),o.on(`error`,()=>{if(!t.headersSent)t.writeHead(502,{"Content-Type":`text/plain`}),t.end(`Bad Gateway`);else try{t.end()}catch{}}),e.on(`error`,()=>{o.destroy()}),e.pipe(o)}async function ea(e){await Promise.all(e.map(e=>new Promise(t=>{e.close(()=>t())})))}async function ta(e){let{existsSync:t}=await import(`node:fs`),{createServer:n}=await import(`node:http`),{resolve:i}=await import(`node:path`),{acquireUiLock:a,readServerLock:o,releaseUiLock:s,updateUiLockPort:c}=await import(`./src-DuGm387u.mjs`),{default:l}=await import(`sirv`),{resolveContentDir:u,resolveLockDir:d}=await import(`./paths-B483looM.mjs`),f=u(e.config,e.cwd),p=d(f);a(p,{port:0,worktreeRoot:e.cwd});let m=import.meta.dirname??new URL(`.`,import.meta.url).pathname,h=[i(m,`public`),i(m,`../../app/dist`),i(m,`../../../app/dist`)].find(e=>t(e)),g=h?l(h,{single:!0,gzip:!0,immutable:!0}):null,_=t(f)?l(f,{dotfiles:!1,dev:!0}):null,v=e.port,y=null,ee=(e,t)=>{let n=e.url?.split(`?`)[0];if(n===`/api/config`&&(e.method===`GET`||e.method===`HEAD`)){y?.();let n=o(p),r=n&&n.port>0?`ws://localhost:${n.port}/collab`:null,i=JSON.stringify({collabUrl:r,previewUrl:null,port:v});t.setHeader(`Content-Type`,`application/json`),t.setHeader(`Cache-Control`,`no-store`),t.setHeader(`X-Content-Type-Options`,`nosniff`),t.statusCode=200,e.method===`HEAD`?t.end():t.end(i);return}if(n?.startsWith(`/api/`)){y?.();let r=o(p);if(!r||r.port<=0){t.writeHead(503,{"Content-Type":`application/json`,"Cache-Control":`no-store`}),t.end(JSON.stringify({error:"Collab server not running. Start `ok start` or run `ok status`.",path:n}));return}Qi(e,t,{upstreamHost:`localhost`,upstreamPort:r.port});return}if(decodeURIComponent(n?.replace(/^\//,``)??``)&&_){t.setHeader(`X-Content-Type-Options`,`nosniff`),_(e,t,()=>{g?g(e,t):na(t)});return}if(g){g(e,t);return}na(t)},b=e.host===void 0?[`::1`,`127.0.0.1`]:[e.host],x=[],S=e.port;try{for(let e of b){let t=n(ee);x.push(t),await new Promise((n,r)=>{let i=e=>r(e);t.once(`error`,i),t.listen(S,e,()=>{t.off(`error`,i);let e=t.address();typeof e==`object`&&e&&(S=e.port),n()})})}}catch(e){await Promise.all(x.map(e=>new Promise(t=>{try{e.close(()=>t())}catch{t()}})));try{s(p)}catch{}throw e}let C=S;v=C,c(p,C);let w=e.scheduler??r,T=e.safetyNetMs??432e5,E=null,D=!1,te=!1,O=()=>{D||(D=!0,E!==null&&(w.clearTimeout(E),E=null))},ne=()=>{if(O(),!te){te=!0;try{s(p)}catch{}}},re=()=>{D||T<=0||(E!==null&&(w.clearTimeout(E),E=null),E=w.setTimeout(()=>{E=null,console.warn(`[ui] safety-net (${T}ms) reached — shutting down (D-025 backstop)`);try{e.onSafetyNet?.()}catch{}for(let e of x)try{e.close()}catch{}ne()},T))},ie=()=>{D||T<=0||re()};return y=ie,re(),{httpServers:x,port:C,release:ne,detachSafetyNet:O,nudgeSafetyNet:ie}}function na(e){e.writeHead(404),e.end(`Not found`)}function ra(e,t){if(e!==void 0){let t=Number.parseInt(e,10);if(Number.isNaN(t)||t<0||t>65535)throw Error(`Invalid --port value '${e}'`);return t}if(t!==void 0&&t!==``){let e=Number.parseInt(t,10);if(Number.isNaN(e)||e<0||e>65535)throw Error(`Invalid PORT env value '${t}'`);return e}return 0}async function ia(e){let t=e.readLock??(async()=>{let{readUiLock:t}=await import(`./src-DuGm387u.mjs`);return t(e.lockDir)}),n=await t();if(!n)throw Error(`UI lock collision reported but the lock disappeared before handling — retry acquiring.`);if(n.port===e.requestedPort&&n.port>0)return{mode:`already-running`,port:n.port};let r=n.port;if(r===0){let n=Date.now()+(e.pollDeadlineMs??2e3),i=e.pollIntervalMs??100;for(;Date.now()<n;){await new Promise(e=>{setTimeout(e,i)});let e=await t();if(e&&e.port>0){r=e.port;break}}if(r===0)throw Error("UI did not bind within 2s; run `ok clean`");if(r===e.requestedPort)return{mode:`already-running`,port:r}}return{mode:`proxy`,handle:await Zi({listenPort:e.requestedPort,host:e.host,upstreamHost:`localhost`,upstreamPort:r}),upstreamPort:r}}function aa(e){return new C(`ui`).description(`Serve the Open Knowledge React editor UI`).option(`-p, --port <port>`,`UI port (default: $PORT env or 0 / kernel-allocated)`).option(`-H, --host <host>`,"UI host. Default: two-socket loopback bind (`[::1]` + `127.0.0.1`) so cross-family collisions fail loud (D-033). Pass an explicit host (e.g. `127.0.0.1`, `0.0.0.0`) to bind a single socket on that host.").action(async t=>{let{dim:n}=await import(`./colors-CDmFQ1jq.mjs`),{UiLockCollisionError:r}=await import(`./src-DuGm387u.mjs`),{resolveContentDir:i,resolveLockDir:a}=await import(`./paths-B483looM.mjs`),o=e(),s=t.host,c;try{c=ra(t.port,process.env.PORT)}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exitCode=1;return}try{let e=await ta({config:o,cwd:process.cwd(),port:c,host:s}),t=s===void 0||s===`::`||s===`0.0.0.0`?`localhost`:s;console.log(`${n(`[ui]`)} listening on http://${t}:${e.port}`);let r=!1,i=t=>{if(r)return;r=!0,console.log(n(`\n[ui] Shutting down (${t})...`)),e.detachSafetyNet();let i=()=>{try{e.release()}finally{process.exit(process.exitCode??0)}};ea(e.httpServers).then(i,i),setTimeout(i,2e3).unref()};process.once(`SIGINT`,()=>i(`SIGINT`)),process.once(`SIGTERM`,()=>i(`SIGTERM`));return}catch(e){if(!(e instanceof r))throw e;let t=a(i(o,process.cwd())),l=s??`localhost`,u;try{u=await ia({requestedPort:c,host:l,lockDir:t})}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exit(1)}u.mode===`already-running`&&(console.log(`UI already running at http://${l}:${u.port}`),process.exit(0)),console.log(`UI running at http://${l}:${u.upstreamPort}; acting as HTTP proxy on port ${u.handle.port}`);let d=!1,f=e=>{d||(d=!0,console.log(n(`\n[ui-proxy] Shutting down (${e})...`)),u.handle.close().finally(()=>process.exit(process.exitCode??0)),setTimeout(()=>process.exit(process.exitCode??0),2e3).unref())};process.once(`SIGINT`,()=>f(`SIGINT`)),process.once(`SIGTERM`,()=>f(`SIGTERM`))}})}process.argv.includes(`--no-color`)?(process.env.NO_COLOR=`1`,delete process.env.FORCE_COLOR):process.argv.includes(`--color`)&&(process.env.FORCE_COLOR=`1`,delete process.env.NO_COLOR);const Q=new C;let $;Q.name(`open-knowledge`).description(`Local-first knowledge base with CRDT collaboration`).version(e).option(`--cwd <path>`,`Working directory`).option(`--log-level <level>`,`Log level`,`info`).option(`--no-color`,`Disable color output`).option(`--color`,`Force color output`).hook(`preAction`,e=>{let t=e.opts(),n=t.cwd;n!==void 0&&process.chdir(n);let{config:r}=ee(n),i=e.args.length===0?t:e.commands[0]?.opts()??{};i.port!==void 0&&(r.server.port=Number(i.port)),i.host!==void 0&&(r.server.host=i.host),process.env.PORT&&(r.server.port=Number(process.env.PORT)),process.env.HOST&&(r.server.host=process.env.HOST),$=r});const oa=x(()=>$);Q.addCommand(oa,{isDefault:!0});const sa=Pi(()=>$);Q.addCommand(sa),Q.addCommand(v());const ca=Fi(()=>$);Q.addCommand(ca);const la=aa(()=>$);Q.addCommand(la),Q.addCommand(Ji(()=>$)),Q.addCommand(ut(()=>$)),Q.addCommand(Gi(()=>$)),Q.addCommand(ot(()=>$)),Q.addCommand(yt(()=>$)),Q.addCommand(Li(()=>$)),Q.addCommand(zi(()=>$)),Q.addCommand(Ri(()=>$)),await Q.parseAsync();export{};
667
+ **Escape hatch.** Native \`Read\`/\`Grep\`/\`Glob\` on \`.md\` is allowed ONLY when no OK MCP is registered, or immediately after an OK MCP call failed — then begin your sentence with \`Open Knowledge MCP unavailable:\`. Non-markdown: native tools always.
668
+ `}async function Li(e,t){try{let t=e.replace(`ws://`,`http://`).replace(`wss://`,`https://`);return(await fetch(`${t}/api/document`,{signal:AbortSignal.timeout(2e3)})).ok}catch(n){return t.warn(`Hocuspocus probe failed`,{serverUrl:e,error:n instanceof Error?n.message:String(n)}),!1}}async function Ri(n){let{projectDir:r,serverUrl:i,config:a,startupConfig:o,bypassProjectSelection:s=!1}=n;if(X=Dt(),X.info(`MCP server starting`,{startupCwd:r,bypassProjectSelection:s,serverUrlType:typeof i==`string`?`explicit`:`lazy`}),typeof i==`string`){let e=await Li(i,X);X.info(`Hocuspocus detection complete`,{serverUrl:i,available:e})}else X.info(`server discovery is lazy per effective cwd`);let c=new Pe({name:t,version:e},{instructions:Ii(o,{dynamicConfig:typeof a==`function`&&!s})}),l=Pi({startupCwd:r,bypassProjectSelection:s,listRoots:()=>c.server.listRoots(),logger:X}),u=Fi({startupCwd:r,resolveCwd:l.resolveCwd,bypassProjectSelection:s}),d=u.resolveCwdForTools;c.server.setNotificationHandler(Ie,async()=>{l.invalidateRoots()});let f=async e=>{if(typeof i==`string`)return i.replace(`ws://`,`http://`).replace(`wss://`,`https://`);let t=e??await d();return(typeof i==`function`?await i(t):i)?.replace(`ws://`,`http://`).replace(`wss://`,`https://`)},p=Te(),m=process.env.AGENT_LABEL||void 0,h={current:{connectionId:p,label:m,displayName:m??`Agent`,colorSeed:m??p}};c.server.oninitialized=()=>{let e=c.server.getClientVersion();h.current={connectionId:p,clientInfo:e?{name:e.name,version:e.version}:void 0,label:m,displayName:m??e?.name??`Agent`,colorSeed:m??e?.name??p},X?.info(`agent identity established`,{displayName:h.current.displayName,connectionId:p.slice(0,8),clientName:e?.name})},ji(c,{serverUrl:f,resolveCwd:d,config:a,identityRef:h,logger:X});let g=new Fe;await c.connect(g),X.info(`MCP server running on stdio`);let{startKeepalive:_}=await import(`./keepalive-CfaIYv9f.mjs`),v=_({resolveWsUrl:async()=>{let e=await u.getKeepaliveCwd();if(!e)return;let t=await f(e);if(t)return t.replace(/^http:/,`ws:`).replace(/^https:/,`wss:`)},connectionId:`agent-${p}`,logger:X.child(`keepalive`)}),y=e=>{X?.info(`MCP server shutting down`,{signal:e});try{v.close()}catch{}process.exit(0)};process.on(`SIGINT`,()=>y(`SIGINT`)),process.on(`SIGTERM`,()=>y(`SIGTERM`))}function zi(e){if(e===void 0||e===``)return;let t=Number.parseInt(e,10);if(!(Number.isNaN(t)||t<=0))return t}function Bi(e){if(e.portOverride!==void 0){let t=Number.parseInt(e.portOverride,10);if(Number.isNaN(t))return{action:`disk-only`,message:`invalid --port value '${e.portOverride}' — disk-only mode`};if(t>0){let n=`ws://${e.host}:${t}`;return{action:`connect`,url:n,message:`using --port override, connecting to ${n}`}}return{action:`disk-only`,message:`--port=0 — disk-only mode`}}let t=e.readLock();if(t&&t.port>0&&e.isAlive(t.pid)){let e=`ws://localhost:${t.port}`;return{action:`connect`,url:e,message:`connected to running instance at ${e} (pid ${t.pid})`}}return e.envAutoStart===`0`?{action:`disk-only`,message:`auto-spawn disabled via OK_MCP_AUTOSTART=0 — disk-only mode`}:e.configAutoStart?t?{action:`spawn`,message:`existing lock is not usable (port=${t.port}, pid=${t.pid}) — spawning ok start`}:{action:`spawn`,message:`no running instance — spawning ok start`}:{action:`disk-only`,message:`auto-spawn disabled via config.mcp.autoStart=false — disk-only mode`}}async function Vi(e){let t=e.readLock??(()=>v(e.lockDir)),n=e.isAlive??_,r=e.sleep??(e=>new Promise(t=>setTimeout(t,e))),i=e.spawn??De,a=e.readErrorLog??(e=>j(e)?oe(e,`utf-8`).trim():``),o=e.openErrorLog??(e=>ae(e,`w`)),s=e.closeFd??(e=>re(e)),c=e.timeoutMs??5e3,l=e.pollIntervalMs??100,u=Bi({host:e.host,portOverride:e.portOverride,envAutoStart:e.envAutoStart,configAutoStart:e.configAutoStart,readLock:t,isAlive:n});if(e.logger?.info(`auto-start decision`,{action:u.action,message:u.message,contentDir:e.contentDir}),u.action===`connect`)return{serverUrl:u.url,message:u.message};if(u.action===`disk-only`)return{serverUrl:void 0,message:u.message};j(e.lockDir)||ie(e.lockDir,{recursive:!0});let d=_e(e.lockDir,`last-spawn-error.log`),f=o(d),p,m,h=te();e.logger?.info(`spawning server`,{command:h.command,cwd:e.contentDir,timeoutMs:c});try{try{p=i(h.command,[...h.prefixArgs,`start`],{detached:!0,stdio:[`ignore`,`ignore`,f],cwd:e.contentDir}),p.on(`error`,e=>{m=e instanceof Error?e.message:String(e)}),p.unref()}catch(e){m=e instanceof Error?e.message:String(e)}}finally{try{s(f)}catch{}}let g=Date.now()+c;for(;Date.now()<g;){if(m){let t=a(d);throw e.logger?.error(`spawn failed`,void 0,{error:m,stderr:t}),Error(`Error: spawn failed: ${m}${t?` stderr:\n${t}`:``}`)}await r(l);let i=t();if(i&&i.port>0&&n(i.pid)){let t=`ws://localhost:${i.port}`;return e.logger?.info(`server ready after spawn`,{url:t,pid:i.pid}),{serverUrl:t,message:`spawned ok start; connected at ${t} (pid ${i.pid})`}}}if(m){let t=a(d);throw e.logger?.error(`spawn failed (post-deadline)`,void 0,{error:m,stderr:t}),Error(`Error: spawn failed: ${m}${t?` stderr:\n${t}`:``}`)}let y=a(d),b=(c/1e3).toFixed(c%1e3==0?0:2),x=p?.pid,S=``;throw typeof x==`number`&&(S=n(x)?` child pid=${x} is still running — raise OK_MCP_SPAWN_TIMEOUT_MS if this is a slow boot.`:` child pid=${x} exited — check last-spawn-error.log.`),e.logger?.error(`spawn poll timeout`,void 0,{timeoutMs:c,childPid:x,childAlive:typeof x==`number`?n(x):void 0,stderr:y||void 0}),Error(`Error: server did not start within ${b}s.${S}${y?` stderr:\n${y}`:``}`)}function Hi(e){if(e.portOverride!==void 0){let t=Number.parseInt(e.portOverride,10);if(Number.isNaN(t)||t<=0)return async()=>void 0;let n=`ws://${e.host}:${t}`;return async()=>n}let t=e.ensureServerRunningFn??Vi,n=e.cacheMs??1e3,r=new Map,i=new Map;return async s=>{let c=await D(s??e.startupCwd),l=Date.now(),u=r.get(c);if(u&&u.expiresAt>l)return e.logger?.debug(`server url cache hit`,{cwd:c,url:u.url}),u.url;let d=i.get(c);if(d)return e.logger?.debug(`server url resolution pending`,{cwd:c}),await d;e.logger?.debug(`server url cache miss`,{cwd:c});let f=(async()=>{let i=await e.resolveConfig(c),s=o(i,c),l=a(s),u=e.readLock,d=u?()=>u(l):void 0,f=await t({lockDir:l,contentDir:s,host:i.server.host,portOverride:void 0,envAutoStart:e.envAutoStart,configAutoStart:i.mcp.autoStart,logger:e.logger,timeoutMs:e.timeoutMs,pollIntervalMs:e.pollIntervalMs,spawn:e.spawn,readLock:d,isAlive:e.isAlive,sleep:e.sleep,readErrorLog:e.readErrorLog,openErrorLog:e.openErrorLog,closeFd:e.closeFd});return r.set(c,{url:f.serverUrl,expiresAt:Date.now()+n}),f.serverUrl})();i.set(c,f);try{return await f}finally{i.delete(c)}}}function Ui(e){return new A(`mcp`).description(`Start MCP stdio server for project knowledge base`).option(`-p, --port <port>`,`Override port discovery and connect to this port (0 = disk-only)`,void 0).action(async t=>{try{let n=e(),r=process.cwd(),i=ee({startupCwd:r,startupConfig:n}),a=zi(process.env.OK_MCP_SPAWN_TIMEOUT_MS),o,s;if(t.port!==void 0){let e=Number.parseInt(t.port,10);Number.isNaN(e)?(o=void 0,s=`invalid --port value '${t.port}' — disk-only mode`):e>0?(o=`ws://${n.server.host}:${e}`,s=`using --port override, connecting to ${o}`):(o=void 0,s=`--port=0 — disk-only mode`)}else o=Hi({startupCwd:r,resolveConfig:i,host:n.server.host,portOverride:void 0,envAutoStart:process.env.OK_MCP_AUTOSTART,timeoutMs:a}),s=`project server discovery/autostart is lazy per effective cwd`;process.stderr.write(`[mcp] ${s}\n`),await Ri({projectDir:r,serverUrl:o,config:i,startupConfig:n,bypassProjectSelection:t.port!==void 0})}catch(e){process.stderr.write(`MCP server failed to start: ${e instanceof Error?e.message:String(e)}\n`),process.exitCode=1}})}function Wi(e){return new A(`preview`).description(`Show what content the watcher will track (read-only)`).action(async()=>{let{previewContent:t,formatPreviewBlock:n}=await import(`./preview-BR34uHnQ.mjs`),r=e(),i=process.cwd(),a=o(r,i),s;try{s=t({projectDir:i,contentDir:a,include:r.content.include,exclude:r.content.exclude})}catch(e){console.error(`Content preview failed: ${e instanceof Error?e.message:String(e)}`),process.exitCode=1;return}process.stdout.write(`${n(s,i)}\n`),s.totalCount===0&&s.warnings.length>0&&(process.exitCode=1)})}function Z(e,t){e&&process.stdout.write(`${JSON.stringify(t)}\n`)}async function Gi(e,t,n=process.cwd()){let r=e.op??`sync`,i=v(a(o(t,n)));if(i&&i.port>0){let t=`http://127.0.0.1:${i.port}/api/sync/trigger`;e.json||process.stderr.write(`Triggering ${r} via running server (port ${i.port})…\n`);try{let n=await fetch(t,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({op:r})});if(!n.ok){let e=await n.json().catch(()=>({}));throw Error(e.error??`Server responded with ${n.status}`)}Z(e.json,{type:`triggered`,op:r,port:i.port}),e.json||process.stderr.write(`✓ ${r} triggered\n`);return}catch(t){let n=t instanceof Error?t.message:String(t);e.json||process.stderr.write(`Server trigger failed (${n}), running directly…\n`)}}e.json||process.stderr.write(`Running ${r} directly (no live server)…\n`);let s=N({baseDir:n});if(r===`sync`||r===`pull`){Z(e.json,{type:`step`,step:`pull`});let t=await s.pull();Z(e.json,{type:`pull`,summary:t.summary}),e.json||process.stderr.write(` pull: ${t.summary.changes} changes\n`)}(r===`sync`||r===`push`)&&(Z(e.json,{type:`step`,step:`push`}),await s.push(),Z(e.json,{type:`push`,ok:!0}),e.json||process.stderr.write(` push: ok
669
+ `)),Z(e.json,{type:`complete`,op:r}),e.json||process.stderr.write(`✓ ${r} complete\n`)}function Ki(e){return new A(`sync`).description(`Commit, pull, and push to the remote`).option(`--json`,`Output JSONL progress events`,!1).action(async t=>{try{await Gi({json:t.json,op:`sync`},e())}catch(e){let n=e instanceof Error?e.message:String(e);t.json?process.stdout.write(`${JSON.stringify({type:`error`,message:n})}\n`):process.stderr.write(`✗ sync failed: ${n}\n`),process.exit(1)}})}function qi(e){return new A(`pull`).description(`Pull changes from the remote`).option(`--json`,`Output JSONL progress events`,!1).action(async t=>{try{await Gi({json:t.json,op:`pull`},e())}catch(e){let n=e instanceof Error?e.message:String(e);t.json?process.stdout.write(`${JSON.stringify({type:`error`,message:n})}\n`):process.stderr.write(`✗ pull failed: ${n}\n`),process.exit(1)}})}function Ji(e){return new A(`push`).description(`Push commits to the remote`).option(`--json`,`Output JSONL progress events`,!1).action(async t=>{try{await Gi({json:t.json,op:`push`},e())}catch(e){let n=e instanceof Error?e.message:String(e);t.json?process.stdout.write(`${JSON.stringify({type:`error`,message:n})}\n`):process.stderr.write(`✗ push failed: ${n}\n`),process.exit(1)}})}async function Yi(e={}){let t=M(e.cwd??process.cwd()),n;try{n=await h({projectDir:t})}catch(e){return e instanceof s?{status:`prerequisite-missing`,message:`${S(`Error:`)} ${e.message}`,exitCode:1}:{status:`failed`,message:`${S(`Error:`)} ${e instanceof Error?e.message:String(e)}`,exitCode:1}}if(n.created.length===0&&n.configEdits.length===0)return{status:`no-op`,message:`${x(`Your knowledge base is already seeded.`)}\n${b(`Nothing to do.`)}`,plan:n,exitCode:0};if(e.dryRun)return{status:`dry-run`,message:`${w(`Plan (dry-run — no changes made):`)}\n\n${Xi(n,t)}`,plan:n,exitCode:0};if(!e.yes&&!await Zi(`${w(`Plan:`)}\n\n${Xi(n,t)}\n\n${w(`Apply?`)} ${b(`[Y/n] `)}`,e.confirmStream))return{status:`cancelled`,message:b(`Cancelled.`),plan:n,exitCode:0};let r=await m(n,{projectDir:t});if(r.errors.length>0){let e=r.errors.map(e=>` ${S(`✗`)} ${e.path}: ${e.error}`);return{status:`failed`,message:[`${C(`Applied`)} ${r.applied} entries, ${C(String(r.errors.length))} error(s):`,...e].join(`
670
+ `),plan:n,exitCode:1}}return{status:`applied`,message:`${x(`✓ Seeded knowledge base`)} ${b(`(${r.applied} entries, ${r.durationMs}ms)`)}`,plan:n,exitCode:0}}function Xi(e,t){let n=[],r=e.created.filter(e=>e.kind===`folder`),i=e.created.filter(e=>e.kind===`file`);if(r.length>0){n.push(w(`Folders to create:`));for(let e of r)n.push(` ${x(`+`)} ${y(ve(t,M(t,e.path))||e.path)}${b(`/`)}`)}if(i.length>0){n.length>0&&n.push(``),n.push(w(`Files to create:`));for(let e of i)n.push(` ${x(`+`)} ${y(ve(t,M(t,e.path))||e.path)}`)}if(e.configEdits.length>0){n.length>0&&n.push(``),n.push(w(`config.yml folders: entries to add:`));for(let t of e.configEdits)n.push(` ${x(`+`)} ${y(t.folderMatch)} ${b(`—`)} ${t.entry.frontmatter.title??``}`)}if(e.skipped.length>0){n.length>0&&n.push(``),n.push(b(`Already present (skipped):`));for(let t of e.skipped)n.push(` ${b(`· ${t.path} (${t.reason})`)}`)}if(e.warnings.length>0){n.length>0&&n.push(``),n.push(C(`Warnings:`));for(let t of e.warnings)n.push(` ${C(`!`)} ${t}`)}return n.join(`
671
+ `)}async function Zi(e,t){let n=Ve({input:t??process.stdin,output:process.stdout});try{let t=(await n.question(e)).trim().toLowerCase();return t===``||t===`y`||t===`yes`}finally{n.close()}}function Qi(){return new A(`seed`).description(`Scaffold the Karpathy three-layer knowledge-base structure (external-sources/, research/, articles/) + log.md + config.yml folders: entries`).argument(`[path]`,`Project directory (defaults to cwd)`).option(`-y, --yes`,`Skip confirmation prompt`).option(`--dry-run`,`Print the plan and exit without writing`).action(async(e,t)=>{let n=await Yi({cwd:e??process.cwd(),yes:t.yes,dryRun:t.dryRun});process.stdout.write(`${n.message}\n`),n.exitCode!==0&&(process.exitCode=n.exitCode)})}function $i(e,t){return{server:ea(`server`,e),ui:ea(`ui`,t)}}function ea(e,t){switch(t.status){case`missing`:return{name:e,state:`missing`,alive:!1};case`corrupt`:return{name:e,state:`corrupt`,alive:!1};case`foreign-host`:return{name:e,state:`foreign-host`,pid:t.lock.pid,port:t.lock.port,startedAt:t.lock.startedAt,host:t.lock.hostname,alive:`unknown`};case`dead-pid`:return{name:e,state:`dead-pid`,pid:t.lock.pid,port:t.lock.port,startedAt:t.lock.startedAt,host:t.lock.hostname,alive:!1};case`alive`:return{name:e,state:`alive`,pid:t.lock.pid,port:t.lock.port,startedAt:t.lock.startedAt,host:t.lock.hostname,alive:!0}}}function ta(e){return`${na(e.server)}\n${na(e.ui)}`}function na(e){let t=e.name===`server`?`server`:`ui `;return e.state===`missing`?`${t} not running`:e.state===`corrupt`?`${t} lock file corrupt — run \`ok clean\``:e.state===`foreign-host`?`${t} foreign host (${e.host}) pid=${e.pid} port=${e.port}`:e.state===`dead-pid`?`${t} stale (dead pid=${e.pid}) — run \`ok clean\``:`${t} alive pid=${e.pid} port=${e.port} started=${e.startedAt}`}function ra(e){let t=e.inspect??(t=>ft(e.lockDir,t)),n=e.log??(e=>console.log(e)),r=$i(t(`server`),t(`ui`));return e.json?n(JSON.stringify(r,null,2)):n(ta(r)),r}function ia(e){return new A(`status`).description(`Show live state of the server + ui lockfiles for this project`).option(`--json`,`Emit structured JSON instead of formatted text`).action(t=>{ra({lockDir:a(o(e(),process.cwd())),json:t.json===!0})})}function aa(e,t){let n=[];return e.status===`alive`&&n.push({name:`server`,pid:e.lock.pid,port:e.lock.port}),t.status===`alive`&&n.push({name:`ui`,pid:t.lock.pid,port:t.lock.port}),{targets:n}}function oa(e){let t=e.inspect??(t=>ft(e.lockDir,t)),n=e.kill??((e,t)=>process.kill(e,t)),r=e.log??(e=>console.log(e)),i=e.error??(e=>console.error(e)),a=aa(t(`server`),t(`ui`));if(a.targets.length===0)return r(`No running open-knowledge processes.`),{stopped:[],failed:[],hadTargets:!1};let o=[],s=[];for(let e of a.targets)try{n(e.pid,`SIGTERM`),o.push(e)}catch(t){s.push({target:e,error:t instanceof Error?t.message:String(t)})}return o.length>0&&r(`Stopped: ${o.map(e=>`${e.name} (pid=${e.pid}, port=${e.port})`).join(`, `)}`),s.length>0&&i(`Failed to stop: ${s.map(({target:e,error:t})=>`${e.name} (pid=${e.pid}): ${t}`).join(`; `)}`),{stopped:o,failed:s,hadTargets:!0}}function sa(e){return new A(`stop`).description(`Stop the running open-knowledge server and UI (live only)`).action(()=>{oa({lockDir:a(o(e(),process.cwd()))}).failed.length>0&&(process.exitCode=1)})}const ca=1e4,la=[`connection`,`keep-alive`,`proxy-authenticate`,`proxy-authorization`,`te`,`trailer`,`transfer-encoding`,`upgrade`,`cookie`,`set-cookie`];async function ua(e){let t=e.upstreamTimeoutMs??ca,n=je((n,r)=>{fa(n,r,e.upstreamHost,e.upstreamPort,t)});await new Promise((t,r)=>{let i=e=>r(e);n.once(`error`,i),n.listen(e.listenPort,e.host,()=>{n.off(`error`,i),t()})});let r=n.address();return{httpServer:n,port:typeof r==`object`&&r?r.port:e.listenPort,close:()=>new Promise(e=>{n.close(()=>e())})}}function da(e,t,n){fa(e,t,n.upstreamHost,n.upstreamPort,n.upstreamTimeoutMs??ca)}function fa(e,t,n,r,i){let a={...e.headers};delete a.host;for(let e of la)delete a[e];e.setTimeout(3e4,()=>{if(t.headersSent)try{t.end()}catch{}else try{t.writeHead(408,{"Content-Type":`text/plain`}),t.end(`Request Timeout`)}catch{}try{e.socket?.destroy()}catch{}});let o=Me({host:n,port:r,method:e.method,path:e.url,headers:{...a,host:`${n}:${r}`}},e=>{let n={...e.headers};for(let e of la)delete n[e];t.writeHead(e.statusCode??502,n),e.pipe(t),e.once(`error`,()=>{try{t.end()}catch{}})});i>0&&o.setTimeout(i,()=>{if(!t.headersSent)t.writeHead(504,{"Content-Type":`text/plain`}),t.end(`Gateway Timeout`);else try{t.end()}catch{}o.destroy()}),o.on(`error`,()=>{if(!t.headersSent)t.writeHead(502,{"Content-Type":`text/plain`}),t.end(`Bad Gateway`);else try{t.end()}catch{}}),e.on(`error`,()=>{o.destroy()}),e.pipe(o)}async function pa(e){await Promise.all(e.map(e=>new Promise(t=>{e.close(()=>t())})))}async function ma(e){let{existsSync:t}=await import(`node:fs`),{createServer:n}=await import(`node:http`),{resolve:i}=await import(`node:path`),{acquireUiLock:a,readServerLock:o,releaseUiLock:s,updateUiLockPort:c}=await import(`./src-lFQj9tkB.mjs`),{default:l}=await import(`sirv`),{resolveContentDir:u,resolveLockDir:d}=await import(`./paths-ORyeejxG.mjs`),f=u(e.config,e.cwd),p=d(f);a(p,{port:0,worktreeRoot:e.cwd});let m=import.meta.dirname??new URL(`.`,import.meta.url).pathname,h=[i(m,`public`),i(m,`../../app/dist`),i(m,`../../../app/dist`)].find(e=>t(e)),g=h?l(h,{single:!0,gzip:!0,immutable:!0}):null,_=t(f)?l(f,{dotfiles:!1,dev:!0}):null,v=e.port,y=null,b=(e,t)=>{let n=e.url?.split(`?`)[0];if(n===`/api/config`&&(e.method===`GET`||e.method===`HEAD`)){y?.();let n=o(p),r=n&&n.port>0?`ws://localhost:${n.port}/collab`:null,i=JSON.stringify({collabUrl:r,previewUrl:null,port:v});t.setHeader(`Content-Type`,`application/json`),t.setHeader(`Cache-Control`,`no-store`),t.setHeader(`X-Content-Type-Options`,`nosniff`),t.statusCode=200,e.method===`HEAD`?t.end():t.end(i);return}if(n?.startsWith(`/api/`)){y?.();let r=o(p);if(!r||r.port<=0){t.writeHead(503,{"Content-Type":`application/json`,"Cache-Control":`no-store`}),t.end(JSON.stringify({error:"Collab server not running. Start `ok start` or run `ok status`.",path:n}));return}da(e,t,{upstreamHost:`localhost`,upstreamPort:r.port});return}if(decodeURIComponent(n?.replace(/^\//,``)??``)&&_){t.setHeader(`X-Content-Type-Options`,`nosniff`),_(e,t,()=>{g?g(e,t):ha(t)});return}if(g){g(e,t);return}ha(t)},x=e.host===void 0?[`::1`,`127.0.0.1`]:[e.host],S=[],C=e.port;try{for(let e of x){let t=n(b);S.push(t),await new Promise((n,r)=>{let i=e=>r(e);t.once(`error`,i),t.listen(C,e,()=>{t.off(`error`,i);let e=t.address();typeof e==`object`&&e&&(C=e.port),n()})})}}catch(e){await Promise.all(S.map(e=>new Promise(t=>{try{e.close(()=>t())}catch{t()}})));try{s(p)}catch{}throw e}let w=C;v=w,c(p,w);let T=e.scheduler??r,E=e.safetyNetMs??432e5,D=null,O=!1,ee=!1,k=()=>{O||(O=!0,D!==null&&(T.clearTimeout(D),D=null))},te=()=>{if(k(),!ee){ee=!0;try{s(p)}catch{}}},A=()=>{O||E<=0||(D!==null&&(T.clearTimeout(D),D=null),D=T.setTimeout(()=>{D=null,console.warn(`[ui] safety-net (${E}ms) reached — shutting down (D-025 backstop)`);try{e.onSafetyNet?.()}catch{}for(let e of S)try{e.close()}catch{}te()},E))},ne=()=>{O||E<=0||A()};return y=ne,A(),{httpServers:S,port:w,release:te,detachSafetyNet:k,nudgeSafetyNet:ne}}function ha(e){e.writeHead(404),e.end(`Not found`)}function ga(e,t){if(e!==void 0){let t=Number.parseInt(e,10);if(Number.isNaN(t)||t<0||t>65535)throw Error(`Invalid --port value '${e}'`);return t}if(t!==void 0&&t!==``){let e=Number.parseInt(t,10);if(Number.isNaN(e)||e<0||e>65535)throw Error(`Invalid PORT env value '${t}'`);return e}return 0}async function _a(e){let t=e.readLock??(async()=>{let{readUiLock:t}=await import(`./src-lFQj9tkB.mjs`);return t(e.lockDir)}),n=await t();if(!n)throw Error(`UI lock collision reported but the lock disappeared before handling — retry acquiring.`);if(n.port===e.requestedPort&&n.port>0)return{mode:`already-running`,port:n.port};let r=n.port;if(r===0){let n=Date.now()+(e.pollDeadlineMs??2e3),i=e.pollIntervalMs??100;for(;Date.now()<n;){await new Promise(e=>{setTimeout(e,i)});let e=await t();if(e&&e.port>0){r=e.port;break}}if(r===0)throw Error("UI did not bind within 2s; run `ok clean`");if(r===e.requestedPort)return{mode:`already-running`,port:r}}return{mode:`proxy`,handle:await ua({listenPort:e.requestedPort,host:e.host,upstreamHost:`localhost`,upstreamPort:r}),upstreamPort:r}}function va(e){return new A(`ui`).description(`Serve the Open Knowledge React editor UI`).option(`-p, --port <port>`,`UI port (default: $PORT env or 0 / kernel-allocated)`).option(`-H, --host <host>`,"UI host. Default: two-socket loopback bind (`[::1]` + `127.0.0.1`) so cross-family collisions fail loud (D-033). Pass an explicit host (e.g. `127.0.0.1`, `0.0.0.0`) to bind a single socket on that host.").action(async t=>{let{dim:n}=await import(`./colors-CxLEz90E.mjs`),{UiLockCollisionError:r}=await import(`./src-lFQj9tkB.mjs`),{resolveContentDir:i,resolveLockDir:a}=await import(`./paths-ORyeejxG.mjs`),o=e(),s=t.host,c;try{c=ga(t.port,process.env.PORT)}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exitCode=1;return}try{let e=await ma({config:o,cwd:process.cwd(),port:c,host:s}),t=s===void 0||s===`::`||s===`0.0.0.0`?`localhost`:s;console.log(`${n(`[ui]`)} listening on http://${t}:${e.port}`);let r=!1,i=t=>{if(r)return;r=!0,console.log(n(`\n[ui] Shutting down (${t})...`)),e.detachSafetyNet();let i=()=>{try{e.release()}finally{process.exit(process.exitCode??0)}};pa(e.httpServers).then(i,i),setTimeout(i,2e3).unref()};process.once(`SIGINT`,()=>i(`SIGINT`)),process.once(`SIGTERM`,()=>i(`SIGTERM`));return}catch(e){if(!(e instanceof r))throw e;let t=a(i(o,process.cwd())),l=s??`localhost`,u;try{u=await _a({requestedPort:c,host:l,lockDir:t})}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exit(1)}u.mode===`already-running`&&(console.log(`UI already running at http://${l}:${u.port}`),process.exit(0)),console.log(`UI running at http://${l}:${u.upstreamPort}; acting as HTTP proxy on port ${u.handle.port}`);let d=!1,f=e=>{d||(d=!0,console.log(n(`\n[ui-proxy] Shutting down (${e})...`)),u.handle.close().finally(()=>process.exit(process.exitCode??0)),setTimeout(()=>process.exit(process.exitCode??0),2e3).unref())};process.once(`SIGINT`,()=>f(`SIGINT`)),process.once(`SIGTERM`,()=>f(`SIGTERM`))}})}process.argv.includes(`--no-color`)?(process.env.NO_COLOR=`1`,delete process.env.FORCE_COLOR):process.argv.includes(`--color`)&&(process.env.FORCE_COLOR=`1`,delete process.env.NO_COLOR);const Q=new A;let $;Q.name(`open-knowledge`).description(`Local-first knowledge base with CRDT collaboration`).version(e).option(`--cwd <path>`,`Working directory`).option(`--log-level <level>`,`Log level`,`info`).option(`--no-color`,`Disable color output`).option(`--color`,`Force color output`).hook(`preAction`,e=>{let t=e.opts(),n=t.cwd;n!==void 0&&process.chdir(n);let{config:r}=O(n),i=e.args.length===0?t:e.commands[0]?.opts()??{};i.port!==void 0&&(r.server.port=Number(i.port)),i.host!==void 0&&(r.server.host=i.host),process.env.PORT&&(r.server.port=Number(process.env.PORT)),process.env.HOST&&(r.server.host=process.env.HOST),$=r});const ya=k(()=>$);Q.addCommand(ya,{isDefault:!0});const ba=Ui(()=>$);Q.addCommand(ba),Q.addCommand(E()),Q.addCommand(Qi());const xa=Wi(()=>$);Q.addCommand(xa);const Sa=va(()=>$);Q.addCommand(Sa),Q.addCommand(sa(()=>$)),Q.addCommand(ht(()=>$)),Q.addCommand(ia(()=>$)),Q.addCommand(dt(()=>$)),Q.addCommand(wt(()=>$)),Q.addCommand(Ki(()=>$)),Q.addCommand(Ji(()=>$)),Q.addCommand(qi(()=>$)),await Q.parseAsync();export{};
598
672
  //# sourceMappingURL=cli.mjs.map