document360-engine 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,28 +1,28 @@
1
- import{readdirSync as Hr,readFileSync as xt,existsSync as De,statSync as Wr}from"node:fs";import{join as Re}from"node:path";import{query as zr}from"@anthropic-ai/claude-agent-sdk";import{existsSync as Et}from"node:fs";import{isAbsolute as It,relative as $t,resolve as Oe}from"node:path";var Ue=new Set(["Edit","Write","MultiEdit","NotebookEdit"]);function le(e,t,r){let n=It(t)?t:Oe(e,t),o=$t(e,n).replace(/\\/g,"/");return o===""||o.startsWith("..")?!1:/\.(md|markdown)$/i.test(o)||o===".d360-writer.json"||o==="d360-category-map.json"||o===".d360-writer"||o.startsWith(".d360-writer/")?!0:[r?.captureDir,r?.outputDir].filter(i=>typeof i=="string"&&i.length>0).map(i=>i.replace(/\\/g,"/").replace(/\/+$/,"")).some(i=>o===i||o.startsWith(`${i}/`))}var Ot=/^(\/dev\/null|nul|null)$/i,Q=e=>e.replace(/^['"]/,"").replace(/['"]$/,""),Ut=new Set(["rm","rmdir","mv","truncate","touch","mkdir","chmod","chown"]),Mt=new Set(["cp","ln","install"]);function Me(e,t,r){let n=i=>{let c=Q(i);return!c||c.startsWith("&")||c.startsWith("-")||Ot.test(c)?!1:!le(t,c,r)},o=/(?:^|[^0-9>])>>?\s*("[^"]+"|'[^']+'|[^\s|&;<>]+)/g,s;for(;s=o.exec(e);)if(n(s[1]))return Q(s[1]);for(let i of e.split(/&&|\|\||[;|]/)){let c=i.trim().split(/\s+/).filter(Boolean);if(c.length===0)continue;let d=c[0].replace(/.*[/\\]/,""),u=c.slice(1),l=u.filter(f=>!f.startsWith("-")),p=[];Ut.has(d)?p=l:Mt.has(d)?p=l.slice(-1):d==="tee"?p=l:d==="dd"?p=u.filter(f=>f.startsWith("of=")).map(f=>f.slice(3)):(d==="sed"||d==="perl")&&u.some(f=>/^-i/.test(f)||f==="--in-place")&&(p=l.filter(f=>Et(Oe(t,Q(f)))));for(let f of p)if(n(f))return Q(f)}return null}function de(e){return`Writer mode: "${e}" is outside the documentation scope (markdown, capture specs, d360 config). Note the needed change for the engineering team instead of making it. Repos that want the agent to modify source code set "mode": "engineer" in .d360-writer.json.`}import{homedir as Nt}from"node:os";import{join as R,dirname as qe}from"node:path";import{fileURLToPath as qt}from"node:url";import{existsSync as Be,mkdirSync as Bt}from"node:fs";var Lt=qt(import.meta.url),Ne=qe(Lt);function Ft(){let e=Ne;for(let t=0;t<4;t++){if(Be(R(e,"package.json")))return e;e=qe(e)}return R(Ne,"..","..")}function Le(){return R(Ft(),"skills")}function j(){return R(Nt(),".document360-writer")}function ue(){return R(j(),"config.json")}function pe(){return R(j(),"mcp.json")}function fe(){return R(j(),"sessions.json")}function ge(e=process.cwd()){return R(e,".d360-writer.json")}function Fe(e=process.cwd()){return R(e,".d360-writer","skills")}function T(e){Be(e)||Bt(e,{recursive:!0})}import{readFileSync as Z,writeFileSync as me,existsSync as he}from"node:fs";import{homedir as Ht}from"node:os";import{join as Wt}from"node:path";var V=class extends Error{};function He(e,t){if(!e?.profiles||Object.keys(e.profiles).length===0)throw new V("No connection profiles in .d360-writer.json. This version requires a `profiles` map + `defaultProfile` (the old d360.environment/projectId shape is no longer supported). Run `d360-writer init` to scaffold the new shape.");let r=t??e.defaultProfile;if(!r)throw new V(`No profile specified and no defaultProfile set. Available: ${Object.keys(e.profiles).join(", ")}`);let n=e.profiles[r];if(!n)throw new V(`Unknown profile "${r}". Available: ${Object.keys(e.profiles).join(", ")}`);return{name:r,profile:n}}function O(e=process.cwd()){let t=ge(e);return he(t)?JSON.parse(Z(t,"utf8")):null}function We(e,t=process.cwd()){me(ge(t),JSON.stringify(e,null,2)+`
2
- `,"utf8")}function ye(){let e=ue();if(!he(e))return{};try{return JSON.parse(Z(e,"utf8"))}catch{return{}}}function Un(e){T(j()),me(ue(),JSON.stringify(e,null,2)+`
3
- `,"utf8")}function zt(e,t,r,n){return e?{model:e,source:"project"}:t?{model:t,source:"user"}:r?{model:r,source:"env"}:n?{model:n,source:"claude-settings"}:{model:null,source:"claude-default"}}function Jt(){try{let e=JSON.parse(Z(Wt(Ht(),".claude","settings.json"),"utf8"));return typeof e.model=="string"&&e.model?e.model:void 0}catch{return}}function Mn(e=process.cwd()){return zt(O(e)?.defaultModel,ye().defaultModel,process.env.ANTHROPIC_MODEL,Jt())}function ze(){let e=pe();if(!he(e))return{servers:{}};try{return JSON.parse(Z(e,"utf8"))}catch{return{servers:{}}}}function Nn(e){T(j()),me(pe(),JSON.stringify(e,null,2)+`
4
- `,"utf8")}var X=class{resolvers=[];buffer=[];closed=!1;push(t){if(this.closed)return;let r={type:"user",message:{role:"user",content:t},parent_tool_use_id:null},n=this.resolvers.shift();n?n(r):this.buffer.push(r)}close(){this.closed=!0;for(let t of this.resolvers)t(null);this.resolvers=[]}async*[Symbol.asyncIterator](){for(;;){if(this.buffer.length>0){yield this.buffer.shift();continue}if(this.closed)return;let t=await new Promise(r=>this.resolvers.push(r));if(t===null)return;yield t}}};import{existsSync as qr,readFileSync as wt}from"node:fs";import{basename as Br,isAbsolute as Lr,resolve as _t}from"node:path";import{createSdkMcpServer as Fr,tool as b}from"@anthropic-ai/claude-agent-sdk";import{z as g}from"zod";import{createHash as Vt,randomBytes as Je}from"node:crypto";import{createServer as Gt}from"node:http";import{spawn as we}from"node:child_process";var Ve=600*1e3;function _e(e){return e.toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function Kt(e){let t=_e(Je(32)),r=_e(Vt("sha256").update(t).digest()),n=_e(Je(16)),o=new URL(e.authorizationUrl);return o.searchParams.set("client_id",e.clientId),o.searchParams.set("response_type","code"),o.searchParams.set("redirect_uri",e.redirectUri),o.searchParams.set("scope",e.scopes.join(" ")),o.searchParams.set("state",n),o.searchParams.set("code_challenge",r),o.searchParams.set("code_challenge_method","S256"),e.acrValues&&o.searchParams.set("acr_values",e.acrValues),e.prompt&&o.searchParams.set("prompt",e.prompt),{url:o.toString(),verifier:t,state:n}}function Yt(e){if(process.env.D360_NO_BROWSER)return;let t=process.platform;if(t==="win32"){let r=e.replace(/'/g,"''");we("powershell",["-NoProfile","-NonInteractive","-Command",`Start-Process '${r}'`],{detached:!0,stdio:"ignore"}).unref()}else t==="darwin"?we("open",[e],{detached:!0,stdio:"ignore"}).unref():we("xdg-open",[e],{detached:!0,stdio:"ignore"}).unref()}function Qt(e,t){let r=new URL(e),n,o=new Promise((s,i)=>{let c=setTimeout(()=>{i(new Error(`Timed out after ${Ve/6e4} minutes waiting for the browser login.`)),n.close()},Ve);n=Gt((d,u)=>{let l=new URL(d.url??"/",`http://${r.host}`);if(l.pathname!==r.pathname){u.writeHead(404).end();return}let p=l.searchParams.get("error"),f=l.searchParams.get("code"),h=l.searchParams.get("state"),k=S=>{u.writeHead(400,{"content-type":"text/html"}),u.end(`<html><body><h3>Login failed</h3><p>${S}</p><p>Return to the terminal.</p></body></html>`),clearTimeout(c),i(new Error(S)),n.close()};if(p)return k(`${p}: ${l.searchParams.get("error_description")??""}`);if(!f)return k("No authorization code in the callback.");if(h!==t)return k("State mismatch \u2014 possible CSRF; try again.");u.writeHead(200,{"content-type":"text/html"}),u.end("<html><body><h3>\u2713 Document360 login complete</h3><p>You can close this tab and return to the terminal.</p></body></html>"),clearTimeout(c),s(f),n.close()}),n.on("error",d=>{clearTimeout(c),i(new Error(`Could not listen on ${r.host} (${d.message}). Another process may hold the port \u2014 set D360_REDIRECT_URI to a free port and retry.`))}),n.listen(Number(r.port)||80,r.hostname)});return{server:n,result:o}}async function Ke(e,t){let r=await fetch(e.tokenUrl,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:t}),n=await r.text();if(!r.ok)throw new Error(`Token endpoint ${r.status}: ${n.slice(0,400)}`);return JSON.parse(n)}async function Ge(e,t,r){return Ke(e,new URLSearchParams({grant_type:"authorization_code",code:t,redirect_uri:e.redirectUri,client_id:e.clientId,code_verifier:r}))}async function Ye(e,t){return Ke(e,new URLSearchParams({grant_type:"refresh_token",refresh_token:t,client_id:e.clientId}))}function Qe(e,t){let r=Date.now();return{profile:e,accessToken:t.access_token,refreshToken:t.refresh_token,idToken:t.id_token,scope:t.scope,obtainedAt:new Date(r).toISOString(),expiresAt:new Date(r+(t.expires_in??3600)*1e3).toISOString()}}async function Wn(e,t,r){let{url:n,verifier:o,state:s}=Kt(e);if(t.manual){r("Open this URL in a browser and sign in:"),r(n);let d=await t.promptForRedirect("Paste the full URL you were redirected to (it contains ?code=...):"),u=new URL(d.trim()),l=u.searchParams.get("code"),p=u.searchParams.get("state");if(!l)throw new Error("No code parameter found in the pasted URL.");if(p!==s)throw new Error("State mismatch in the pasted URL \u2014 restart the login.");return Ge(e,l,o)}let{result:i}=Qt(e.redirectUri,s);r("Opening your browser for Document360 sign-in\u2026"),r(`If it did not open, visit:
5
- ${n}`),Yt(n);let c=await i;return Ge(e,c,o)}import{existsSync as Ze,readFileSync as Zt,unlinkSync as Xt,writeFileSync as er}from"node:fs";import{join as Xe}from"node:path";function et(){return Xe(j(),"auth")}function ke(e){return Xe(et(),`${e}.json`)}function tt(e){T(et()),er(ke(e.profile),JSON.stringify(e,null,2))}function be(e){let t=ke(e);if(!Ze(t))return null;try{return JSON.parse(Zt(t,"utf8"))}catch{return null}}function Kn(e){let t=ke(e);return Ze(t)?(Xt(t),!0):!1}function rt(e,t=60){return Date.now()>=new Date(e.expiresAt).getTime()-t*1e3}function nt(e){if(!e)return null;let t=e.split(".");if(t.length!==3)return null;try{let r=Buffer.from(t[1].replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8");return JSON.parse(r)}catch{return null}}import{appendFileSync as tr,readdirSync as rr,unlinkSync as nr}from"node:fs";import{join as ve}from"node:path";var or=14,sr=4096;function ir(){return ve(j(),"logs")}function ar(e){return e.replace(/Bearer\s+[A-Za-z0-9._~+/=-]{8,}/g,"Bearer [redacted]").replace(/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]+/g,"[redacted-jwt]")}function cr(e,t=sr){return e.length<=t?e:`${e.slice(0,t)}\u2026 [+${e.length-t} chars]`}function ee(e){return e===void 0?void 0:cr(ar(e))}var Se=()=>process.env.D360_LOG_BODIES==="1",lr=/^d360-api-(\d{4}-\d{2}-\d{2})\.jsonl$/;function dr(e){let t=new Date(Date.now()-or*24*60*60*1e3).toISOString().slice(0,10);for(let r of rr(e)){let n=r.match(lr);if(n&&n[1]<t)try{nr(ve(e,r))}catch{}}}var ot=!1;function xe(e){try{let t=ir();T(t),ot||(ot=!0,dr(t));let r=ve(t,`d360-api-${e.ts.slice(0,10)}.jsonl`);tr(r,JSON.stringify(e)+`
6
- `,"utf8")}catch{}}var A=class extends Error{},te=class extends Error{constructor(r,n,o){super(r);this.status=n;this.requestId=o}status;requestId};async function st(e){let t=be(e.profile);if(!t)throw new A(`Not logged in to Document360 (profile "${e.profile}"). Run /login (or: d360-writer login --profile ${e.profile})`);if(!rt(t))return t.accessToken;if(!t.refreshToken)throw new A(`Document360 session expired (profile "${e.profile}"). Run /login (or: d360-writer login --profile ${e.profile})`);try{let r=Qe(e.profile,await Ye(e.connection,t.refreshToken));return tt(r),r.accessToken}catch(r){throw new A(`Document360 session expired and refresh failed (${r.message.slice(0,120)}). Run /login (or: d360-writer login --profile ${e.profile})`)}}function N(e,t){if(t)return t;let r=be(e.profile),o=(r?nt(r.accessToken)??{}:{}).doc360_project_id;if(typeof o=="string"&&o)return o;throw new A("No Document360 project resolved. Log in (the project is chosen during login) or set the profile's project.projectId in .d360-writer.json.")}async function re(e,t,r,n={}){let o=await st(e),s=new URL(r.startsWith("http")?r:`${e.connection.apiUrl}${r}`);for(let[f,h]of Object.entries(n.query??{}))h!==void 0&&s.searchParams.set(f,String(h));let i=n.body!==void 0?JSON.stringify(n.body):void 0,c=Date.now(),d=f=>xe({ts:new Date().toISOString(),profile:e.profile,method:t,url:s.toString(),ms:Date.now()-c,ok:f.ok,status:f.status,requestId:f.requestId,error:f.error,...f.withBodies?{requestBody:ee(i),responseBody:ee(f.responseBody)}:{}}),u;try{u=await fetch(s,{method:t,headers:{authorization:`Bearer ${o}`,accept:"application/json",...i!==void 0?{"content-type":"application/json"}:{}},body:i})}catch(f){throw d({ok:!1,error:`network: ${f.message}`,withBodies:!0}),f}let l=await u.text(),p={};if(l)try{p=JSON.parse(l)}catch{}if(!u.ok||p.success===!1){let f=p.errors?.map(k=>k.message??k.code).filter(Boolean).join("; ")||l.slice(0,300)||u.statusText;if(d({ok:!1,status:u.status,requestId:p.request_id,error:f,responseBody:l,withBodies:!0}),u.status===401)throw new A(`Document360 rejected the token (401). Run /login (or: d360-writer login --profile ${e.profile})`);let h=p.request_id?` [request_id: ${p.request_id}]`:"";throw new te(`Document360 API ${u.status}: ${f}${h}`,u.status,p.request_id)}return d({ok:!0,status:u.status,requestId:p.request_id,responseBody:l,withBodies:Se()}),p}async function E(e,t,r){return(await re(e,"GET",t,r)).data}async function q(e,t,r){return(await re(e,"POST",t,r)).data}async function it(e,t,r){return(await re(e,"PATCH",t,r)).data}async function at(e,t,r){let n=await st(e),o=`${e.connection.apiUrl}${t}`,s=Date.now(),i=l=>xe({ts:new Date().toISOString(),profile:e.profile,method:"POST",url:o,ms:Date.now()-s,ok:l.ok,status:l.status,requestId:l.requestId,error:l.error,...l.withBodies?{requestBody:"[multipart form-data]",responseBody:ee(l.responseBody)}:{}}),c;try{c=await fetch(o,{method:"POST",headers:{authorization:`Bearer ${n}`,accept:"application/json"},body:r})}catch(l){throw i({ok:!1,error:`network: ${l.message}`,withBodies:!0}),l}let d=await c.text(),u={};if(d)try{u=JSON.parse(d)}catch{}if(!c.ok||u.success===!1){let l=u.errors?.map(f=>f.message??f.code).filter(Boolean).join("; ")||d.slice(0,300)||c.statusText;if(i({ok:!1,status:c.status,requestId:u.request_id,error:l,responseBody:d,withBodies:!0}),c.status===401)throw new A(`Document360 rejected the token (401). Run /login (or: d360-writer login --profile ${e.profile})`);let p=u.request_id?` [request_id: ${u.request_id}]`:"";throw new te(`Document360 upload ${c.status}: ${l}${p}`,c.status,u.request_id)}return i({ok:!0,status:c.status,requestId:u.request_id,responseBody:d,withBodies:Se()}),u.data}async function I(e,t,r={}){let n=[],o;for(let s=0;s<50;s++){let i=await re(e,"GET",t,{...r,query:{...r.query,cursor:o}});if(Array.isArray(i.data)&&n.push(...i.data),!i.pagination?.has_more||!i.pagination.next_cursor)break;o=i.pagination.next_cursor}return n}async function oo(e,t){return I(e,`/v3/projects/${t}/workspaces`)}async function so(e,t,r){return E(e,`/v3/projects/${t}/articles/${r}`,{query:{content_mode:"raw"}})}import{existsSync as ur,readFileSync as pr,writeFileSync as fr}from"node:fs";import{join as gr}from"node:path";function ct(e){return gr(e,"d360-category-map.json")}function lt(e){let t=ct(e);if(!ur(t))return null;try{return JSON.parse(pr(t,"utf8"))}catch{return null}}function dt(e){return typeof e=="string"?{id:e}:{...e}}function H(e,t){let n=lt(e)?.[t];if(!n)return null;let o={};for(let[s,i]of Object.entries(n.articles??{}))o[s]=dt(i);return{environment:n.environment,projectId:n.projectId,workspaceId:n.workspaceId,lastAnalyzedCommit:n.lastAnalyzedCommit,categories:{...n.categories??{}},articles:o}}function ut(e,t){for(let[r,n]of Object.entries(e.articles))if(n.id===t)return r;return null}function ne(e,t,r,n){let o=lt(e),s=o?.[t];if(!o||!s)return!1;s.articles??={};let i=s.articles[r],c=i!==void 0?dt(i):null;return s.articles[r]={...c??{},...n,lastSyncedAt:new Date().toISOString()},fr(ct(e),JSON.stringify(o,null,2)+`
7
- `,"utf8"),!0}import{createHash as mr}from"node:crypto";function hr(e){let t=e.replace(/^\uFEFF/,"").replace(/\r\n/g,`
1
+ import{readdirSync as Jr,readFileSync as jt,existsSync as Ae,statSync as Vr}from"node:fs";import{join as Ee}from"node:path";import{query as Gr}from"@anthropic-ai/claude-agent-sdk";import{existsSync as $t}from"node:fs";import{isAbsolute as Ot,relative as Mt,resolve as Ue}from"node:path";var Ne=new Set(["Edit","Write","MultiEdit","NotebookEdit"]);function de(e,t,r){let n=Ot(t)?t:Ue(e,t),o=Mt(e,n).replace(/\\/g,"/");return o===""||o.startsWith("..")?!1:/\.(md|markdown)$/i.test(o)||o===".d360-writer.json"||o==="d360-category-map.json"||o===".d360-writer"||o.startsWith(".d360-writer/")?!0:[r?.captureDir,r?.outputDir].filter(i=>typeof i=="string"&&i.length>0).map(i=>i.replace(/\\/g,"/").replace(/\/+$/,"")).some(i=>o===i||o.startsWith(`${i}/`))}var Ut=/^(\/dev\/null|nul|null)$/i,X=e=>e.replace(/^['"]/,"").replace(/['"]$/,""),Nt=new Set(["rm","rmdir","mv","truncate","touch","mkdir","chmod","chown"]),qt=new Set(["cp","ln","install"]);function qe(e,t,r){let n=i=>{let c=X(i);return!c||c.startsWith("&")||c.startsWith("-")||Ut.test(c)?!1:!de(t,c,r)},o=/(?:^|[^0-9>])>>?\s*("[^"]+"|'[^']+'|[^\s|&;<>]+)/g,s;for(;s=o.exec(e);)if(n(s[1]))return X(s[1]);for(let i of e.split(/&&|\|\||[;|]/)){let c=i.trim().split(/\s+/).filter(Boolean);if(c.length===0)continue;let u=c[0].replace(/.*[/\\]/,""),d=c.slice(1),l=d.filter(f=>!f.startsWith("-")),p=[];Nt.has(u)?p=l:qt.has(u)?p=l.slice(-1):u==="tee"?p=l:u==="dd"?p=d.filter(f=>f.startsWith("of=")).map(f=>f.slice(3)):(u==="sed"||u==="perl")&&d.some(f=>/^-i/.test(f)||f==="--in-place")&&(p=l.filter(f=>$t(Ue(t,X(f)))));for(let f of p)if(n(f))return X(f)}return null}function ue(e){return`Writer mode: "${e}" is outside the documentation scope (markdown, capture specs, d360 config). Note the needed change for the engineering team instead of making it. Repos that want the agent to modify source code set "mode": "engineer" in .d360-writer.json.`}import{homedir as Lt}from"node:os";import{join as A,dirname as Be}from"node:path";import{fileURLToPath as Bt}from"node:url";import{existsSync as Fe,mkdirSync as Ft}from"node:fs";var Ht=Bt(import.meta.url),Le=Be(Ht);function Wt(){let e=Le;for(let t=0;t<4;t++){if(Fe(A(e,"package.json")))return e;e=Be(e)}return A(Le,"..","..")}function He(){return A(Wt(),"skills")}function j(){return A(Lt(),".document360-writer")}function pe(){return A(j(),"config.json")}function fe(){return A(j(),"mcp.json")}function ge(){return A(j(),"sessions.json")}function me(e=process.cwd()){return A(e,".d360-writer.json")}function We(e=process.cwd()){return A(e,".d360-writer","skills")}function E(e){Fe(e)||Ft(e,{recursive:!0})}import{readFileSync as ee,writeFileSync as he,existsSync as ye}from"node:fs";import{homedir as zt}from"node:os";import{join as Jt}from"node:path";var Y=class extends Error{};function ze(e,t){if(!e?.profiles||Object.keys(e.profiles).length===0)throw new Y("No connection profiles in .d360-writer.json. This version requires a `profiles` map + `defaultProfile` (the old d360.environment/projectId shape is no longer supported). Run `d360-writer init` to scaffold the new shape.");let r=t??e.defaultProfile;if(!r)throw new Y(`No profile specified and no defaultProfile set. Available: ${Object.keys(e.profiles).join(", ")}`);let n=e.profiles[r];if(!n)throw new Y(`Unknown profile "${r}". Available: ${Object.keys(e.profiles).join(", ")}`);return{name:r,profile:n}}function N(e=process.cwd()){let t=me(e);return ye(t)?JSON.parse(ee(t,"utf8")):null}function Je(e,t=process.cwd()){he(me(t),JSON.stringify(e,null,2)+`
2
+ `,"utf8")}function we(){let e=pe();if(!ye(e))return{};try{return JSON.parse(ee(e,"utf8"))}catch{return{}}}function qn(e){E(j()),he(pe(),JSON.stringify(e,null,2)+`
3
+ `,"utf8")}function Vt(e,t,r,n){return e?{model:e,source:"project"}:t?{model:t,source:"user"}:r?{model:r,source:"env"}:n?{model:n,source:"claude-settings"}:{model:null,source:"claude-default"}}function Gt(){try{let e=JSON.parse(ee(Jt(zt(),".claude","settings.json"),"utf8"));return typeof e.model=="string"&&e.model?e.model:void 0}catch{return}}function Ln(e=process.cwd()){return Vt(N(e)?.defaultModel,we().defaultModel,process.env.ANTHROPIC_MODEL,Gt())}function Ve(){let e=fe();if(!ye(e))return{servers:{}};try{return JSON.parse(ee(e,"utf8"))}catch{return{servers:{}}}}function Bn(e){E(j()),he(fe(),JSON.stringify(e,null,2)+`
4
+ `,"utf8")}var te=class{resolvers=[];buffer=[];closed=!1;push(t){if(this.closed)return;let r={type:"user",message:{role:"user",content:t},parent_tool_use_id:null},n=this.resolvers.shift();n?n(r):this.buffer.push(r)}close(){this.closed=!0;for(let t of this.resolvers)t(null);this.resolvers=[]}async*[Symbol.asyncIterator](){for(;;){if(this.buffer.length>0){yield this.buffer.shift();continue}if(this.closed)return;let t=await new Promise(r=>this.resolvers.push(r));if(t===null)return;yield t}}};import{existsSync as Fr,readFileSync as _t}from"node:fs";import{basename as Hr,isAbsolute as Wr,resolve as vt}from"node:path";import{createSdkMcpServer as zr,tool as b}from"@anthropic-ai/claude-agent-sdk";import{z as g}from"zod";import{createHash as Kt,randomBytes as Ge}from"node:crypto";import{createServer as Yt}from"node:http";import{spawn as ke}from"node:child_process";var Ke=600*1e3;function _e(e){return e.toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function Qt(e){let t=_e(Ge(32)),r=_e(Kt("sha256").update(t).digest()),n=_e(Ge(16)),o=new URL(e.authorizationUrl);return o.searchParams.set("client_id",e.clientId),o.searchParams.set("response_type","code"),o.searchParams.set("redirect_uri",e.redirectUri),o.searchParams.set("scope",e.scopes.join(" ")),o.searchParams.set("state",n),o.searchParams.set("code_challenge",r),o.searchParams.set("code_challenge_method","S256"),e.acrValues&&o.searchParams.set("acr_values",e.acrValues),e.prompt&&o.searchParams.set("prompt",e.prompt),{url:o.toString(),verifier:t,state:n}}function Zt(e){if(process.env.D360_NO_BROWSER)return;let t=process.platform;if(t==="win32"){let r=e.replace(/'/g,"''");ke("powershell",["-NoProfile","-NonInteractive","-Command",`Start-Process '${r}'`],{detached:!0,stdio:"ignore"}).unref()}else t==="darwin"?ke("open",[e],{detached:!0,stdio:"ignore"}).unref():ke("xdg-open",[e],{detached:!0,stdio:"ignore"}).unref()}function Xt(e,t){let r=new URL(e),n,o=new Promise((s,i)=>{let c=setTimeout(()=>{i(new Error(`Timed out after ${Ke/6e4} minutes waiting for the browser login.`)),n.close()},Ke);n=Yt((u,d)=>{let l=new URL(u.url??"/",`http://${r.host}`);if(l.pathname!==r.pathname){d.writeHead(404).end();return}let p=l.searchParams.get("error"),f=l.searchParams.get("code"),h=l.searchParams.get("state"),_=x=>{d.writeHead(400,{"content-type":"text/html"}),d.end(`<html><body><h3>Login failed</h3><p>${x}</p><p>Return to the terminal.</p></body></html>`),clearTimeout(c),i(new Error(x)),n.close()};if(p)return _(`${p}: ${l.searchParams.get("error_description")??""}`);if(!f)return _("No authorization code in the callback.");if(h!==t)return _("State mismatch \u2014 possible CSRF; try again.");d.writeHead(200,{"content-type":"text/html"}),d.end("<html><body><h3>\u2713 Document360 login complete</h3><p>You can close this tab and return to the terminal.</p></body></html>"),clearTimeout(c),s(f),n.close()}),n.on("error",u=>{clearTimeout(c),i(new Error(`Could not listen on ${r.host} (${u.message}). Another process may hold the port \u2014 set D360_REDIRECT_URI to a free port and retry.`))}),n.listen(Number(r.port)||80,r.hostname)});return{server:n,result:o}}async function Qe(e,t){let r=await fetch(e.tokenUrl,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:t}),n=await r.text();if(!r.ok)throw new Error(`Token endpoint ${r.status}: ${n.slice(0,400)}`);return JSON.parse(n)}async function Ye(e,t,r){return Qe(e,new URLSearchParams({grant_type:"authorization_code",code:t,redirect_uri:e.redirectUri,client_id:e.clientId,code_verifier:r}))}async function Ze(e,t){return Qe(e,new URLSearchParams({grant_type:"refresh_token",refresh_token:t,client_id:e.clientId}))}function Xe(e,t){let r=Date.now();return{profile:e,accessToken:t.access_token,refreshToken:t.refresh_token,idToken:t.id_token,scope:t.scope,obtainedAt:new Date(r).toISOString(),expiresAt:new Date(r+(t.expires_in??3600)*1e3).toISOString()}}async function Vn(e,t,r){let{url:n,verifier:o,state:s}=Qt(e);if(t.manual){r("Open this URL in a browser and sign in:"),r(n);let u=await t.promptForRedirect("Paste the full URL you were redirected to (it contains ?code=...):"),d=new URL(u.trim()),l=d.searchParams.get("code"),p=d.searchParams.get("state");if(!l)throw new Error("No code parameter found in the pasted URL.");if(p!==s)throw new Error("State mismatch in the pasted URL \u2014 restart the login.");return Ye(e,l,o)}let{result:i}=Xt(e.redirectUri,s);r("Opening your browser for Document360 sign-in\u2026"),r(`If it did not open, visit:
5
+ ${n}`),Zt(n);let c=await i;return Ye(e,c,o)}import{existsSync as et,readFileSync as er,unlinkSync as tr,writeFileSync as rr}from"node:fs";import{join as tt}from"node:path";function rt(){return tt(j(),"auth")}function ve(e){return tt(rt(),`${e}.json`)}function nt(e){E(rt()),rr(ve(e.profile),JSON.stringify(e,null,2))}function be(e){let t=ve(e);if(!et(t))return null;try{return JSON.parse(er(t,"utf8"))}catch{return null}}function Zn(e){let t=ve(e);return et(t)?(tr(t),!0):!1}function ot(e,t=60){return Date.now()>=new Date(e.expiresAt).getTime()-t*1e3}function st(e){if(!e)return null;let t=e.split(".");if(t.length!==3)return null;try{let r=Buffer.from(t[1].replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8");return JSON.parse(r)}catch{return null}}import{appendFileSync as nr,readdirSync as or,unlinkSync as sr}from"node:fs";import{join as Se}from"node:path";var ir=14,ar=4096;function cr(){return Se(j(),"logs")}function lr(e){return e.replace(/Bearer\s+[A-Za-z0-9._~+/=-]{8,}/g,"Bearer [redacted]").replace(/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]+/g,"[redacted-jwt]")}function dr(e,t=ar){return e.length<=t?e:`${e.slice(0,t)}\u2026 [+${e.length-t} chars]`}function re(e){return e===void 0?void 0:dr(lr(e))}var xe=()=>process.env.D360_LOG_BODIES==="1",ur=/^d360-api-(\d{4}-\d{2}-\d{2})\.jsonl$/;function pr(e){let t=new Date(Date.now()-ir*24*60*60*1e3).toISOString().slice(0,10);for(let r of or(e)){let n=r.match(ur);if(n&&n[1]<t)try{sr(Se(e,r))}catch{}}}var it=!1;function Pe(e){try{let t=cr();E(t),it||(it=!0,pr(t));let r=Se(t,`d360-api-${e.ts.slice(0,10)}.jsonl`);nr(r,JSON.stringify(e)+`
6
+ `,"utf8")}catch{}}var I=class extends Error{},ne=class extends Error{constructor(r,n,o){super(r);this.status=n;this.requestId=o}status;requestId};async function at(e){let t=be(e.profile);if(!t)throw new I(`Not logged in to Document360 (profile "${e.profile}"). Run /login (or: d360-writer login --profile ${e.profile})`);if(!ot(t))return t.accessToken;if(!t.refreshToken)throw new I(`Document360 session expired (profile "${e.profile}"). Run /login (or: d360-writer login --profile ${e.profile})`);try{let r=Xe(e.profile,await Ze(e.connection,t.refreshToken));return nt(r),r.accessToken}catch(r){throw new I(`Document360 session expired and refresh failed (${r.message.slice(0,120)}). Run /login (or: d360-writer login --profile ${e.profile})`)}}function B(e,t){if(t)return t;let r=be(e.profile),o=(r?st(r.accessToken)??{}:{}).doc360_project_id;if(typeof o=="string"&&o)return o;throw new I("No Document360 project resolved. Log in (the project is chosen during login) or set the profile's project.projectId in .d360-writer.json.")}async function oe(e,t,r,n={}){let o=await at(e),s=new URL(r.startsWith("http")?r:`${e.connection.apiUrl}${r}`);for(let[f,h]of Object.entries(n.query??{}))h!==void 0&&s.searchParams.set(f,String(h));let i=n.body!==void 0?JSON.stringify(n.body):void 0,c=Date.now(),u=f=>Pe({ts:new Date().toISOString(),profile:e.profile,method:t,url:s.toString(),ms:Date.now()-c,ok:f.ok,status:f.status,requestId:f.requestId,error:f.error,...f.withBodies?{requestBody:re(i),responseBody:re(f.responseBody)}:{}}),d;try{d=await fetch(s,{method:t,headers:{authorization:`Bearer ${o}`,accept:"application/json",...i!==void 0?{"content-type":"application/json"}:{}},body:i})}catch(f){throw u({ok:!1,error:`network: ${f.message}`,withBodies:!0}),f}let l=await d.text(),p={};if(l)try{p=JSON.parse(l)}catch{}if(!d.ok||p.success===!1){let f=p.errors?.map(_=>_.message??_.code).filter(Boolean).join("; ")||l.slice(0,300)||d.statusText;if(u({ok:!1,status:d.status,requestId:p.request_id,error:f,responseBody:l,withBodies:!0}),d.status===401)throw new I(`Document360 rejected the token (401). Run /login (or: d360-writer login --profile ${e.profile})`);let h=p.request_id?` [request_id: ${p.request_id}]`:"";throw new ne(`Document360 API ${d.status}: ${f}${h}`,d.status,p.request_id)}return u({ok:!0,status:d.status,requestId:p.request_id,responseBody:l,withBodies:xe()}),p}async function R(e,t,r){return(await oe(e,"GET",t,r)).data}async function F(e,t,r){return(await oe(e,"POST",t,r)).data}async function ct(e,t,r){return(await oe(e,"PATCH",t,r)).data}async function lt(e,t,r){let n=await at(e),o=`${e.connection.apiUrl}${t}`,s=Date.now(),i=l=>Pe({ts:new Date().toISOString(),profile:e.profile,method:"POST",url:o,ms:Date.now()-s,ok:l.ok,status:l.status,requestId:l.requestId,error:l.error,...l.withBodies?{requestBody:"[multipart form-data]",responseBody:re(l.responseBody)}:{}}),c;try{c=await fetch(o,{method:"POST",headers:{authorization:`Bearer ${n}`,accept:"application/json"},body:r})}catch(l){throw i({ok:!1,error:`network: ${l.message}`,withBodies:!0}),l}let u=await c.text(),d={};if(u)try{d=JSON.parse(u)}catch{}if(!c.ok||d.success===!1){let l=d.errors?.map(f=>f.message??f.code).filter(Boolean).join("; ")||u.slice(0,300)||c.statusText;if(i({ok:!1,status:c.status,requestId:d.request_id,error:l,responseBody:u,withBodies:!0}),c.status===401)throw new I(`Document360 rejected the token (401). Run /login (or: d360-writer login --profile ${e.profile})`);let p=d.request_id?` [request_id: ${d.request_id}]`:"";throw new ne(`Document360 upload ${c.status}: ${l}${p}`,c.status,d.request_id)}return i({ok:!0,status:c.status,requestId:d.request_id,responseBody:u,withBodies:xe()}),d.data}async function $(e,t,r={}){let n=[],o;for(let s=0;s<50;s++){let i=await oe(e,"GET",t,{...r,query:{...r.query,cursor:o}});if(Array.isArray(i.data)&&n.push(...i.data),!i.pagination?.has_more||!i.pagination.next_cursor)break;o=i.pagination.next_cursor}return n}async function ao(e,t){return $(e,`/v3/projects/${t}/workspaces`)}async function co(e,t,r){return R(e,`/v3/projects/${t}/articles/${r}`,{query:{content_mode:"raw"}})}import{existsSync as fr,readFileSync as gr,writeFileSync as mr}from"node:fs";import{join as hr}from"node:path";function dt(e){return hr(e,"d360-category-map.json")}function ut(e){let t=dt(e);if(!fr(t))return null;try{return JSON.parse(gr(t,"utf8"))}catch{return null}}function pt(e){return typeof e=="string"?{id:e}:{...e}}function H(e,t){let n=ut(e)?.[t];if(!n)return null;let o={};for(let[s,i]of Object.entries(n.articles??{}))o[s]=pt(i);return{environment:n.environment,projectId:n.projectId,workspaceId:n.workspaceId,lastAnalyzedCommit:n.lastAnalyzedCommit,categories:{...n.categories??{}},articles:o}}function ft(e,t){for(let[r,n]of Object.entries(e.articles))if(n.id===t)return r;return null}function se(e,t,r,n){let o=ut(e),s=o?.[t];if(!o||!s)return!1;s.articles??={};let i=s.articles[r],c=i!==void 0?pt(i):null;return s.articles[r]={...c??{},...n,lastSyncedAt:new Date().toISOString()},mr(dt(e),JSON.stringify(o,null,2)+`
7
+ `,"utf8"),!0}import{createHash as yr}from"node:crypto";function wr(e){let t=e.replace(/^\uFEFF/,"").replace(/\r\n/g,`
8
8
  `);return t=t.replace(/\n*$/,""),t===""?"":t+`
9
- `}function U(e){return"sha256:"+mr("sha256").update(hr(e),"utf8").digest("hex")}function B(e,t){return U(`${e??""}
10
- ${t??""}`)}import{existsSync as _r,readdirSync as kr,readFileSync as br,statSync as vr}from"node:fs";import{join as Pe}from"node:path";var pt={berlin:{apiUrl:"https://apihub.berlin.document360.net",portalUrl:"https://portal.berlin.document360.net",authorizationUrl:"https://identity.berlin.document360.net/connect/authorize",tokenUrl:"https://identity.berlin.document360.net/connect/token",clientId:"d360WriterAgentClient",scopes:["openid","profile","email","customerApi","offline_access"],acrValues:"project_select",redirectUri:"http://127.0.0.1:3223/callback",prompt:"login"},sharjah:{apiUrl:"https://apihub.sharjah.document360.net",portalUrl:"https://portal.sharjah.document360.net",authorizationUrl:"https://identity.sharjah.document360.net/connect/authorize",tokenUrl:"https://identity.sharjah.document360.net/connect/token",clientId:"d360WriterAgentClient",scopes:["openid","profile","email","customerApi","offline_access"],acrValues:"project_select",redirectUri:"http://127.0.0.1:3223/callback",prompt:"login"}};function yr(){return Object.keys(pt)}function fo(e="berlin"){return oe({environment:e})}function oe(e){let t=e.environment??"berlin",r=pt[t];if(!r)throw new Error(`Unknown Document360 environment "${t}". Known: ${yr().join(", ")}`);let n=process.env.D360_SCOPES?.split(/[\s,]+/).filter(Boolean);return{name:t,apiUrl:process.env.D360_API_URL??e.apiUrl??r.apiUrl,portalUrl:process.env.D360_PORTAL_URL??e.portalUrl??r.portalUrl,authorizationUrl:process.env.D360_AUTHORIZATION_URL??e.authorizationUrl??r.authorizationUrl,tokenUrl:process.env.D360_TOKEN_URL??e.tokenUrl??r.tokenUrl,clientId:process.env.D360_CLIENT_ID??e.clientId??r.clientId,scopes:n??e.scopes??r.scopes,acrValues:process.env.D360_ACR_VALUES??e.acrValues??r.acrValues,redirectUri:process.env.D360_REDIRECT_URI??e.redirectUri??r.redirectUri,prompt:process.env.D360_PROMPT??e.prompt??r.prompt}}function L(e,t){let r=O(e);if(r===null){let s=t??"berlin";return{name:s,connection:oe({environment:s}),project:{},production:!1}}let{name:n,profile:o}=He(r,t);return{name:n,connection:oe(o.connection),project:o.project??{},production:o.production===!0}}function yo(e,t,r){let n=O(e);n?.profiles?.[t]&&(n.profiles[t].project={...n.profiles[t].project,...r},We(n,e))}function ft(e,t){if(e?.docsDir)return e.docsDir.replace(/\\/g,"/").replace(/\/+$/,"");let r=new Set(Object.keys(t?.articles??{}).map(n=>n.split("/")[0]).filter(Boolean));if(r.size===1)return[...r][0];throw new Error(r.size===0?'Cannot locate the docs folder: no mapped articles yet. Set "docsDir" in .d360-writer.json.':`Cannot locate the docs folder: mapped articles span multiple roots (${[...r].join(", ")}). Set "docsDir" in .d360-writer.json.`)}var wr=4;function gt(e,t,r,n={getAll:I,get:E}){let o=`/v3/projects/${t}`;return{async fetch(s){let[i,c]=await Promise.all([n.getAll(e,`${o}/workspaces/${r}/articles`),n.getAll(e,`${o}/workspaces/${r}/categories`)]),d=new Map;for(let h of i)d.set(h.id,{id:h.id,title:h.title,categoryId:h.category_id,hidden:h.hidden});let u=new Map;for(let h of c)u.set(h.id,{id:h.id,name:h.name,parentId:h.parent_category_id});let l=s.filter(h=>d.has(h)),p=0,f=async()=>{for(;p<l.length;){let h=l[p++],k=await n.get(e,`${o}/articles/${h}`,{query:{content_mode:"raw"}}),S=d.get(h);S.title=k.title??S.title,S.content=k.content,S.latestVersion=k.latest_version,S.modifiedAt=k.modified_at,S.contentHash=B(k.title??S.title,k.content)}};return await Promise.all(Array.from({length:Math.min(wr,l.length)},f)),{articles:d,categories:u}}}}function Sr(e){if(!e.mapped)return e.localExists?"untracked-local":"untracked-remote";if(!e.localExists&&!e.remoteExists)return"orphaned";if(!e.localExists)return"deleted-local";if(!e.remoteExists)return"deleted-remote";if(!e.hasBase)return"unknown-base";let t=e.localHashNow!==e.baseLocalHash,r=e.remoteContentHashNow!==e.baseRemoteContentHash;return t&&r?"conflict":t?"local-ahead":r?"remote-ahead":"in-sync"}function xr(e,t){let r=Pe(e,t);if(!_r(r))return[];let n=[],o=(s,i)=>{for(let c of kr(s)){let d=Pe(s,c),u=i?`${i}/${c}`:c;vr(d).isDirectory()?o(d,u):c.endsWith(".md")&&n.push(u)}};return o(r,t.replace(/\\/g,"/")),n}function Pr(e,t){let r=new Set(Object.values(e.categories)),n=new Map;for(let i of t.categories.values()){if(!i.parentId)continue;let c=n.get(i.parentId)??[];c.push(i.id),n.set(i.parentId,c)}let o=new Set,s=[...r];for(;s.length>0;){let i=s.shift();o.has(i)||(o.add(i),s.push(...n.get(i)??[]))}return o}async function mt(e){let t=L(e.cwd,e.profileName),r=O(e.cwd),n=H(e.cwd,t.name);if(!n)throw new Error(`No d360-category-map.json section for profile "${t.name}". Publish at least one article first (/publish).`);let o={profile:t.name,connection:t.connection},s=n.projectId??t.project.projectId??N(o),i=n.workspaceId??t.project.workspaceId;if(!i)throw new Error(`No workspace recorded for profile "${t.name}". Run /workspace to select one.`);let c=ft(r,n),d=Object.values(n.articles).map(_=>_.id),l=await(e.provider??gt(o,s,i)).fetch(d),p=new Set(xr(e.cwd,c)),f=[];for(let[_,$]of Object.entries(n.articles)){let G=p.has(_);p.delete(_);let D=l.articles.get($.id),ae=!!($.localHash&&$.remoteContentHash),z=Sr({mapped:!0,localExists:G,remoteExists:D!==void 0,hasBase:ae,localHashNow:G?U(br(Pe(e.cwd,_),"utf8")):void 0,baseLocalHash:$.localHash,remoteContentHashNow:D?.contentHash,baseRemoteContentHash:$.remoteContentHash}),F=[];if(z==="remote-ahead"||z==="conflict"){let K=D?.modifiedAt?.slice(0,10);F.push(`remote edited${K?` ${K}`:""}${D?.latestVersion?` (v${D.latestVersion})`:""}`)}D?.hidden&&F.push("hidden on D360"),f.push({path:_,articleId:$.id,title:D?.title,status:z,detail:F.length>0?F.join("; "):void 0})}for(let _ of[...p].sort())f.push({path:_,articleId:null,status:"untracked-local"});let h=new Set(d),k=Pr(n,l);for(let _ of l.articles.values())h.has(_.id)||!_.categoryId||!k.has(_.categoryId)||f.push({path:null,articleId:_.id,title:_.title,status:"untracked-remote",detail:_.hidden?"hidden on D360":void 0});let S={};for(let _ of f)S[_.status]=(S[_.status]??0)+1;return{profile:t.name,projectId:s,workspaceId:i,docsRoot:c,entries:f,counts:S,generatedAt:new Date().toISOString()}}import{readdirSync as jr}from"node:fs";import{join as se}from"node:path";var Cr=new Set([".git","node_modules","dist","build","bin","obj","out","target",".vs",".vscode",".idea",".next","coverage",".turbo",".cache","packages"]),Dr=40,Rr=3,je=5e3,Tr=/\.(csproj|vbproj|fsproj)$|^(package\.json|go\.mod|Cargo\.toml|pyproject\.toml|setup\.py|pom\.xml|build\.gradle(\.kts)?)$/i,Ar=/\.sln$|^(Directory\.(Build|Packages)\.props|global\.json|go\.work)$/i;function Er(e){return/\.(csproj|vbproj|fsproj|sln)$/i.test(e)||/^(Directory\.(Build|Packages)\.props|global\.json)$/i.test(e)?".NET":/^(package\.json|angular\.json|tsconfig\.json)$/i.test(e)?"JS/TS":/^(pyproject\.toml|requirements\.txt|setup\.py)$/i.test(e)?"Python":/^(go\.mod|go\.work)$/i.test(e)?"Go":/^Cargo\.toml$/i.test(e)?"Rust":/^(pom\.xml|build\.gradle(\.kts)?)$/i.test(e)?"Java":null}var Ir=/(?:^|[.\-_])(web|portal|ui|app|apps|client|site|widget|admin|dashboard|frontend|api)(?:$|[.\-_])/i,$r=/(?:^|[.\-_])(tests?|specs?|helm|docker|migrations?|functions?|ci|builds?|scripts|tools|proxy|coverage|release|libs?|redis|signalr|websocket|dataaccess|nuget|sdk|infra|infrastructure|deploy|deployment|e2e)(?:$|[.\-_])/i;function Ce(e){let t=[],r=[];try{for(let n of jr(e,{withFileTypes:!0}))n.isDirectory()?!n.name.startsWith(".")&&!Cr.has(n.name)&&t.push(n.name):n.isFile()&&r.push(n.name)}catch{}return{dirs:t,files:r}}function Or(e){let t=new Set;for(let r of e){let n=Er(r);n&&t.add(n)}return t}var ht=e=>e.some(t=>Tr.test(t)),Ur=e=>e.some(t=>Ar.test(t));function Mr(e){let t=0,r=n=>{if(t>=je)return;let{dirs:o,files:s}=Ce(n);t+=s.length;for(let i of o){if(t>=je)return;r(se(n,i))}};return r(e),Math.min(t,je)}function Nr(e,t){let r=e.split("/").pop()??e,n=t.size?[...t].join("+"):"";return $r.test(r)?{recommended:!1,reason:`tests/infrastructure${n?` \xB7 ${n}`:""}`}:Ir.test(r)?{recommended:!0,reason:`user-facing surface${n?` \xB7 ${n}`:""}`}:{recommended:!1,reason:n?`${n} project`:"source folder"}}function yt(e){let t=[],r=(s,i)=>{let c=Or(i.files),{recommended:d,reason:u}=Nr(s,c);t.push({path:s,fileCount:Mr(se(e,s)),stacks:[...c],recommended:d,reason:u})},n=(s,i)=>{let c=s?se(e,s):e,d=Ce(c);if(s&&ht(d.files)){r(s,d);return}if(s&&d.dirs.length===0){r(s,d);return}let u=p=>ht(Ce(se(c,p)).files);if((Ur(d.files)||d.dirs.some(u)||s==="")&&i<Rr){for(let p of d.dirs)n(s?`${s}/${p}`:p,i+1);return}s&&r(s,d)};n("",0),t.sort((s,i)=>Number(i.recommended)-Number(s.recommended)||s.path.localeCompare(i.path));let o=t.filter(s=>s.recommended);return t.slice(0,Math.max(Dr,o.length))}var kt="/v3/projects",x=e=>`${kt}/${e}`;function v(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)??'{"success":true}'}]}}function w(e){return{content:[{type:"text",text:e instanceof Error?e.message:String(e)}],isError:!0}}function bt(e,t){let r={profile:e.name,connection:e.connection},n=e.project.projectId,o=e.project.workspaceId,s=()=>N(r,n),i=a=>{let m=a??o;if(!m)throw new Error("No workspace_id given and none in the profile. Call d360_list_workspaces first.");return m},c=(a,m)=>{if(t.cwd)try{let y=_t(t.cwd,a);ne(t.cwd,e.name,a.replace(/\\/g,"/"),{...m,...qr(y)?{localHash:U(wt(y,"utf8"))}:{}})}catch{}},d=()=>t.writesAllowed?null:w(`Refusing to write to PRODUCTION profile "${e.name}". Authorize this session first: run /allow-prod in the REPL, or pass --yes in one-shot/CI.`),u=b("d360_context","Report the active Document360 connection: profile name, environment, project/workspace id, and whether it is a production profile. Call this before maintaining d360-category-map.json so you scope IDs to the right profile.",{},async()=>{try{return v({profile:e.name,environment:e.connection.name,production:e.production,projectId:n??N(r),workspaceId:o??null})}catch(a){return w(a)}}),l=b("d360_list_projects","List all Document360 projects the signed-in user can access (id, name, sub-domain, status).",{},async()=>{try{return v(await I(r,kt))}catch(a){return w(a)}}),p=b("d360_list_workspaces","List workspaces (project versions) for the current project. Each has id, name, slug, is_default, workspace_type.",{project_id:g.string().optional().describe("Defaults to the logged-in/config project.")},async a=>{try{return v(await I(r,`${x(a.project_id??s())}/workspaces`))}catch(m){return w(m)}}),f=b("d360_list_categories","List categories in a workspace (the folder structure for articles).",{workspace_id:g.string().optional().describe("Defaults to d360.workspaceId in config."),project_id:g.string().optional()},async a=>{try{let m=a.workspace_id??o;return m?v(await I(r,`${x(a.project_id??s())}/workspaces/${m}/categories`)):w("No workspace_id given and none in config. Call d360_list_workspaces first.")}catch(m){return w(m)}}),h=b("d360_list_articles","List articles in a workspace (id, title, status, category). Use to see existing docs before writing.",{workspace_id:g.string().optional().describe("Defaults to d360.workspaceId in config."),project_id:g.string().optional()},async a=>{try{let m=a.workspace_id??o;return m?v(await I(r,`${x(a.project_id??s())}/workspaces/${m}/articles`)):w("No workspace_id given and none in config. Call d360_list_workspaces first.")}catch(m){return w(m)}}),k=b("d360_get_article","Get a single article including its content. content_mode=raw returns the stored markdown source (best for editing); display returns processed content.",{article_id:g.string(),content_mode:g.enum(["raw","display"]).optional().describe("raw = stored markdown source (default), display = processed."),published:g.boolean().optional().describe("Read the published version instead of the latest draft."),project_id:g.string().optional()},async a=>{try{return v(await E(r,`${x(a.project_id??s())}/articles/${a.article_id}`,{query:{content_mode:a.content_mode,published:a.published}}))}catch(m){return w(m)}}),S=b("d360_ai_query","Ask Document360's AI search over a workspace's published content. Returns an answer grounded in existing articles \u2014 use to check what's already documented.",{query:g.string(),workspace_id:g.string().optional().describe("Defaults to d360.workspaceId in config."),project_id:g.string().optional()},async a=>{try{let m=a.workspace_id??o;return m?v(await q(r,`${x(a.project_id??s())}/workspaces/ai/query`,{body:{query:a.query,workspace_id:m}})):w("No workspace_id given and none in config. Call d360_list_workspaces first.")}catch(m){return w(m)}}),_=b("d360_sync_status","Deterministic drift report: local docs tree vs Document360, classified against the sync bases in d360-category-map.json. Call this BEFORE any docs gap/coverage analysis \u2014 it proves whether the local docs tree can be trusted as the Document360 inventory (no need to fetch article content). Statuses: local-ahead (push pending), remote-ahead (portal edit to pull), conflict, untracked-local/remote, deleted-local/remote, unknown-base (no base recorded yet).",{},async()=>{try{if(!t.cwd)return w("Sync status needs a repo context (no cwd configured for this session).");let a=await mt({cwd:t.cwd,profileName:e.name});return v({profile:a.profile,docsRoot:a.docsRoot,generatedAt:a.generatedAt,counts:a.counts,attention:a.entries.filter(m=>m.status!=="in-sync")})}catch(a){return w(a)}}),$=b("d360_repo_inventory","Deterministic inventory of candidate source folders for documentation scope (no file reads of your own needed). Returns project-level folders with: path, fileCount, detected stacks, a `recommended` flag (user-facing surfaces pre-recommended; tests/infrastructure not), and a one-line reason. Use this when analyzing a large/multi-project repo to decide which folders back the docs \u2014 it is the same data the /scope picker pre-ticks, so your recommendation and the picker agree. Cheap; safe to call before reading any source.",{},async()=>{try{return t.cwd?v(yt(t.cwd)):w("Repo inventory needs a repo context (no cwd configured for this session).")}catch(a){return w(a)}}),G=b("d360_create_category","Create a category (a docs folder). Returns the new category id.",{name:g.string(),workspace_id:g.string().optional(),parent_category_id:g.string().optional().describe("Omit for a top-level category."),content:g.string().optional(),slug:g.string().optional(),order:g.number().optional(),project_id:g.string().optional()},async a=>{let m=d();if(m)return m;try{return v(await q(r,`${x(a.project_id??s())}/categories`,{body:{name:a.name,workspace_id:i(a.workspace_id),parent_category_id:a.parent_category_id,content:a.content,slug:a.slug,order:a.order,content_type:"markdown"}}))}catch(y){return w(y)}}),D=b("d360_create_article","Create a DRAFT article in a category. The body is always Markdown (product rule \u2014 we never create WYSIWYG/Block articles). Returns the new article id. Does not publish.",{title:g.string(),category_id:g.string(),content:g.string().optional().describe("Markdown body."),workspace_id:g.string().optional(),slug:g.string().optional(),order:g.number().optional(),project_id:g.string().optional(),local_path:g.string().optional().describe("Repo-relative path of the local .md this article mirrors (e.g. user-docs/01-intro/01-a.md). When given, the new article id + sync base are recorded into d360-category-map.json automatically \u2014 do not edit the articles map yourself.")},async a=>{let m=d();if(m)return m;try{let y=await q(r,`${x(a.project_id??s())}/articles/bulk`,{body:{articles:[{title:a.title,category_id:a.category_id,workspace_id:i(a.workspace_id),content:a.content,slug:a.slug,order:a.order,content_type:"markdown"}]}}),M=Array.isArray(y)?y[0]:void 0;return a.local_path&&typeof M?.id=="string"&&c(a.local_path,{id:M.id,remoteContentHash:B(a.title,a.content),remoteVersion:1}),v(y)}catch(y){return w(y)}}),ae=b("d360_update_article","Update an article's title/content/category. Edits the latest draft by default; set auto_fork to safely edit a published article (creates a new draft version).",{article_id:g.string(),title:g.string().optional(),content:g.string().optional().describe("Markdown body."),category_id:g.string().optional(),hidden:g.boolean().optional(),version_number:g.number().optional(),auto_fork:g.boolean().optional().describe("If the target version is published, fork a new draft instead of erroring."),project_id:g.string().optional()},async a=>{let m=d();if(m)return m;try{let{article_id:y,project_id:M,...P}=a,Y=M??s(),C=null;if(typeof P.content=="string"&&t.onUiEvent)try{C=await E(r,`${x(Y)}/articles/${y}`,{query:{content_mode:"raw"}})}catch{}let ce=e.project.languageCode??C?.lang_code,Ae=await it(r,`${x(Y)}/articles/${y}`,{body:P,...ce?{query:{lang_code:ce}}:{}});if(C&&typeof P.content=="string"&&(C.content??"")!==P.content&&t.onUiEvent?.({type:"article_diff",articleId:y,title:C.title??null,oldContent:C.content??"",newContent:P.content}),typeof P.content=="string"&&t.cwd)try{let Ee=H(t.cwd,e.name),Ie=Ee?ut(Ee,y):null,J=Ae,$e=P.title??J?.title??C?.title;Ie&&typeof $e=="string"&&c(Ie,{id:y,remoteContentHash:B($e,P.content),...typeof J?.latest_version=="number"?{remoteVersion:J.latest_version}:{},...typeof J?.modified_at=="string"?{remoteModifiedAt:J.modified_at}:{}})}catch{}return v(Ae)}catch(y){return w(y)}}),z=b("d360_fork_article","Fork a published article into a new draft version \u2014 the safe way to start editing live content.",{article_id:g.string(),project_id:g.string().optional()},async a=>{let m=d();if(m)return m;try{return v(await q(r,`${x(a.project_id??s())}/articles/${a.article_id}/fork`))}catch(y){return w(y)}}),F=b("d360_publish_article","Publish an article version. This makes the draft live to readers \u2014 only call when the user explicitly asks to publish.",{article_id:g.string(),version_number:g.number(),workspace_id:g.string().optional(),message:g.string().optional(),project_id:g.string().optional()},async a=>{let m=d();if(m)return m;try{return v(await q(r,`${x(a.project_id??s())}/articles/${a.article_id}/publish`,{body:{workspace_id:i(a.workspace_id),version_number:a.version_number,message:a.message}}))}catch(y){return w(y)}}),K=b("d360_unpublish_article","Unpublish an article version, reverting it to draft (removes it from readers).",{article_id:g.string(),version_number:g.number().describe("The published version to unpublish (see d360_get_article)."),workspace_id:g.string().optional(),project_id:g.string().optional()},async a=>{let m=d();if(m)return m;try{return v(await q(r,`${x(a.project_id??s())}/articles/${a.article_id}/unpublish`,{body:{workspace_id:i(a.workspace_id),version_number:a.version_number}}))}catch(y){return w(y)}}),Tt=b("d360_upload_drive_file","Upload a local file (e.g. a captured screenshot PNG) to Drive and return its URL for embedding in an article. Uploads to the default folder unless folder_id is given.",{file_path:g.string().describe("Local path to the file (absolute, or relative to the repo)."),folder_id:g.string().optional(),title:g.string().optional(),project_id:g.string().optional()},async a=>{let m=d();if(m)return m;try{let y=a.project_id??s(),M=Lr(a.file_path)?a.file_path:_t(process.cwd(),a.file_path),P=a.folder_id;if(!P&&(P=(await E(r,`${x(y)}/drive/folders/default`))?.id,!P))return w("Could not resolve the default Drive folder.");let Y=wt(M),C=new FormData;return C.append("file",new Blob([Y]),a.title??Br(M)),v(await at(r,`${x(y)}/drive/folders/${P}/files`,C))}catch(y){return w(y)}}),At=b("d360_search_drive","Search Drive files (e.g. to reuse an already-uploaded screenshot instead of uploading a duplicate).",{search_keyword:g.string().optional(),allow_images_only:g.boolean().optional(),project_id:g.string().optional()},async a=>{try{return v(await I(r,`${x(a.project_id??s())}/drive/search`,{query:{search_keyword:a.search_keyword,allow_images_only:a.allow_images_only}}))}catch(m){return w(m)}});return Fr({name:"document360",version:"0.2.0",instructions:"First-party Document360 tools. The signed-in user's permissions apply server-side. Project/workspace default from the active profile; pass ids explicitly to override. Create articles as DRAFTS; only call d360_publish_article when the user explicitly asks to publish. All articles are Markdown \u2014 the tools enforce this; never attempt WYSIWYG/Block content. To edit a published article, fork it (d360_fork_article) or update with auto_fork. Before any docs gap/coverage analysis, call d360_sync_status to verify the local docs tree matches Document360. When scoping a large repo (which folders back the docs), call d360_repo_inventory for the candidate folders. Auth errors mean the session expired \u2014 tell the user to run /login (works inside this session).",tools:[u,l,p,f,h,k,S,_,$,G,D,ae,z,F,K,Tt,At]})}var Pt="CLAUDE.md";function vt(e){if(!De(e))return[];let t=[];for(let r of Hr(e)){let n=Re(e,r);if(r===Pt||!Wr(n).isDirectory())continue;let o=Re(n,"SKILL.md");De(o)&&t.push({name:r,body:xt(o,"utf8")})}return t}function St(e){let t=Re(e,Pt);return De(t)?xt(t,"utf8"):null}function Jr(e,t){let r=Le(),n=Fe(e),o=St(r),s=St(n),i=vt(r),c=vt(n),d=new Set(c.map(p=>p.name)),u=[...i.filter(p=>!d.has(p.name)),...c],l=[];if(o&&l.push(o),s&&l.push(`# Project addendum
9
+ `}function q(e){return"sha256:"+yr("sha256").update(wr(e),"utf8").digest("hex")}function W(e,t){return q(`${e??""}
10
+ ${t??""}`)}import{existsSync as vr,readdirSync as br,readFileSync as Sr,statSync as xr}from"node:fs";import{join as je}from"node:path";var gt={berlin:{apiUrl:"https://apihub.berlin.document360.net",portalUrl:"https://portal.berlin.document360.net",authorizationUrl:"https://identity.berlin.document360.net/connect/authorize",tokenUrl:"https://identity.berlin.document360.net/connect/token",clientId:"d360WriterAgentClient",scopes:["openid","profile","email","customerApi","offline_access"],acrValues:"project_select",redirectUri:"http://127.0.0.1:3223/callback",prompt:"login"},sharjah:{apiUrl:"https://apihub.sharjah.document360.net",portalUrl:"https://portal.sharjah.document360.net",authorizationUrl:"https://identity.sharjah.document360.net/connect/authorize",tokenUrl:"https://identity.sharjah.document360.net/connect/token",clientId:"d360WriterAgentClient",scopes:["openid","profile","email","customerApi","offline_access"],acrValues:"project_select",redirectUri:"http://127.0.0.1:3223/callback",prompt:"login"}};function kr(){return Object.keys(gt)}function ho(e="berlin"){return ie({environment:e})}function ie(e){let t=e.environment??"berlin",r=gt[t];if(!r)throw new Error(`Unknown Document360 environment "${t}". Known: ${kr().join(", ")}`);let n=process.env.D360_SCOPES?.split(/[\s,]+/).filter(Boolean);return{name:t,apiUrl:process.env.D360_API_URL??e.apiUrl??r.apiUrl,portalUrl:process.env.D360_PORTAL_URL??e.portalUrl??r.portalUrl,authorizationUrl:process.env.D360_AUTHORIZATION_URL??e.authorizationUrl??r.authorizationUrl,tokenUrl:process.env.D360_TOKEN_URL??e.tokenUrl??r.tokenUrl,clientId:process.env.D360_CLIENT_ID??e.clientId??r.clientId,scopes:n??e.scopes??r.scopes,acrValues:process.env.D360_ACR_VALUES??e.acrValues??r.acrValues,redirectUri:process.env.D360_REDIRECT_URI??e.redirectUri??r.redirectUri,prompt:process.env.D360_PROMPT??e.prompt??r.prompt}}function z(e,t){let r=N(e);if(r===null){let s=t??"berlin";return{name:s,connection:ie({environment:s}),project:{},production:!1}}let{name:n,profile:o}=ze(r,t);return{name:n,connection:ie(o.connection),project:o.project??{},production:o.production===!0}}function _o(e,t,r){let n=N(e);n?.profiles?.[t]&&(n.profiles[t].project={...n.profiles[t].project,...r},Je(n,e))}function mt(e,t){if(e?.docsDir)return e.docsDir.replace(/\\/g,"/").replace(/\/+$/,"");let r=new Set(Object.keys(t?.articles??{}).map(n=>n.split("/")[0]).filter(Boolean));if(r.size===1)return[...r][0];throw new Error(r.size===0?'Cannot locate the docs folder: no mapped articles yet. Set "docsDir" in .d360-writer.json.':`Cannot locate the docs folder: mapped articles span multiple roots (${[...r].join(", ")}). Set "docsDir" in .d360-writer.json.`)}var _r=4;function ht(e,t,r,n={getAll:$,get:R}){let o=`/v3/projects/${t}`;return{async fetch(s){let[i,c]=await Promise.all([n.getAll(e,`${o}/workspaces/${r}/articles`),n.getAll(e,`${o}/workspaces/${r}/categories`)]),u=new Map;for(let h of i)u.set(h.id,{id:h.id,title:h.title,categoryId:h.category_id,hidden:h.hidden});let d=new Map;for(let h of c)d.set(h.id,{id:h.id,name:h.name,parentId:h.parent_category_id});let l=s.filter(h=>u.has(h)),p=0,f=async()=>{for(;p<l.length;){let h=l[p++],_=await n.get(e,`${o}/articles/${h}`,{query:{content_mode:"raw"}}),x=u.get(h);x.title=_.title??x.title,x.content=_.content,x.latestVersion=_.latest_version,x.modifiedAt=_.modified_at,x.contentHash=W(_.title??x.title,_.content)}};return await Promise.all(Array.from({length:Math.min(_r,l.length)},f)),{articles:u,categories:d}}}}function Pr(e){if(!e.mapped)return e.localExists?"untracked-local":"untracked-remote";if(!e.localExists&&!e.remoteExists)return"orphaned";if(!e.localExists)return"deleted-local";if(!e.remoteExists)return"deleted-remote";if(!e.hasBase)return"unknown-base";let t=e.localHashNow!==e.baseLocalHash,r=e.remoteContentHashNow!==e.baseRemoteContentHash;return t&&r?"conflict":t?"local-ahead":r?"remote-ahead":"in-sync"}function jr(e,t){let r=je(e,t);if(!vr(r))return[];let n=[],o=(s,i)=>{for(let c of br(s)){let u=je(s,c),d=i?`${i}/${c}`:c;xr(u).isDirectory()?o(u,d):c.endsWith(".md")&&n.push(d)}};return o(r,t.replace(/\\/g,"/")),n}function Cr(e,t){let r=new Set(Object.values(e.categories)),n=new Map;for(let i of t.categories.values()){if(!i.parentId)continue;let c=n.get(i.parentId)??[];c.push(i.id),n.set(i.parentId,c)}let o=new Set,s=[...r];for(;s.length>0;){let i=s.shift();o.has(i)||(o.add(i),s.push(...n.get(i)??[]))}return o}async function yt(e){let t=z(e.cwd,e.profileName),r=N(e.cwd),n=H(e.cwd,t.name);if(!n)throw new Error(`No d360-category-map.json section for profile "${t.name}". Publish at least one article first (/publish).`);let o={profile:t.name,connection:t.connection},s=n.projectId??t.project.projectId??B(o),i=n.workspaceId??t.project.workspaceId;if(!i)throw new Error(`No workspace recorded for profile "${t.name}". Run /workspace to select one.`);let c=mt(r,n),u=Object.values(n.articles).map(v=>v.id),l=await(e.provider??ht(o,s,i)).fetch(u),p=new Set(jr(e.cwd,c)),f=[];for(let[v,O]of Object.entries(n.articles)){let Q=p.has(v);p.delete(v);let T=l.articles.get(O.id),le=!!(O.localHash&&O.remoteContentHash),G=Pr({mapped:!0,localExists:Q,remoteExists:T!==void 0,hasBase:le,localHashNow:Q?q(Sr(je(e.cwd,v),"utf8")):void 0,baseLocalHash:O.localHash,remoteContentHashNow:T?.contentHash,baseRemoteContentHash:O.remoteContentHash}),J=[];if(G==="remote-ahead"||G==="conflict"){let Z=T?.modifiedAt?.slice(0,10);J.push(`remote edited${Z?` ${Z}`:""}${T?.latestVersion?` (v${T.latestVersion})`:""}`)}T?.hidden&&J.push("hidden on D360"),f.push({path:v,articleId:O.id,title:T?.title,status:G,detail:J.length>0?J.join("; "):void 0})}for(let v of[...p].sort())f.push({path:v,articleId:null,status:"untracked-local"});let h=new Set(u),_=Cr(n,l);for(let v of l.articles.values())h.has(v.id)||!v.categoryId||!_.has(v.categoryId)||f.push({path:null,articleId:v.id,title:v.title,status:"untracked-remote",detail:v.hidden?"hidden on D360":void 0});let x={};for(let v of f)x[v.status]=(x[v.status]??0)+1;return{profile:t.name,projectId:s,workspaceId:i,docsRoot:c,entries:f,counts:x,generatedAt:new Date().toISOString()}}import{posix as Ce}from"node:path";var Dr=/\[([^\]]+)\]\(([^)\s]+)\)/g;function De(e,t,r){if(!r)return{content:e,resolved:0,unresolved:0};let n=Ce.dirname(t.replace(/\\/g,"/")),o=0,s=0;return{content:e.replace(Dr,(c,u,d)=>{if(/^(https?:|mailto:|#|\/)/i.test(d)||!/\.(md|markdown)(#[^)]*)?$/i.test(d))return c;let l=d.indexOf("#"),p=l===-1?d:d.slice(0,l),f=l===-1?"":d.slice(l),h=Ce.normalize(Ce.join(n,p)).replace(/^\.\//,""),_=r.articles[h]?.url;return _?(o++,`[${u}](${_}${f})`):(s++,u)}),resolved:o,unresolved:s}}import{readdirSync as Rr}from"node:fs";import{join as ae}from"node:path";var Tr=new Set([".git","node_modules","dist","build","bin","obj","out","target",".vs",".vscode",".idea",".next","coverage",".turbo",".cache","packages"]),Ar=40,Er=3,Re=5e3,Ir=/\.(csproj|vbproj|fsproj)$|^(package\.json|go\.mod|Cargo\.toml|pyproject\.toml|setup\.py|pom\.xml|build\.gradle(\.kts)?)$/i,$r=/\.sln$|^(Directory\.(Build|Packages)\.props|global\.json|go\.work)$/i;function Or(e){return/\.(csproj|vbproj|fsproj|sln)$/i.test(e)||/^(Directory\.(Build|Packages)\.props|global\.json)$/i.test(e)?".NET":/^(package\.json|angular\.json|tsconfig\.json)$/i.test(e)?"JS/TS":/^(pyproject\.toml|requirements\.txt|setup\.py)$/i.test(e)?"Python":/^(go\.mod|go\.work)$/i.test(e)?"Go":/^Cargo\.toml$/i.test(e)?"Rust":/^(pom\.xml|build\.gradle(\.kts)?)$/i.test(e)?"Java":null}var Mr=/(?:^|[.\-_])(web|portal|ui|app|apps|client|site|widget|admin|dashboard|frontend|api)(?:$|[.\-_])/i,Ur=/(?:^|[.\-_])(tests?|specs?|helm|docker|migrations?|functions?|ci|builds?|scripts|tools|proxy|coverage|release|libs?|redis|signalr|websocket|dataaccess|nuget|sdk|infra|infrastructure|deploy|deployment|e2e)(?:$|[.\-_])/i;function Te(e){let t=[],r=[];try{for(let n of Rr(e,{withFileTypes:!0}))n.isDirectory()?!n.name.startsWith(".")&&!Tr.has(n.name)&&t.push(n.name):n.isFile()&&r.push(n.name)}catch{}return{dirs:t,files:r}}function Nr(e){let t=new Set;for(let r of e){let n=Or(r);n&&t.add(n)}return t}var wt=e=>e.some(t=>Ir.test(t)),qr=e=>e.some(t=>$r.test(t));function Lr(e){let t=0,r=n=>{if(t>=Re)return;let{dirs:o,files:s}=Te(n);t+=s.length;for(let i of o){if(t>=Re)return;r(ae(n,i))}};return r(e),Math.min(t,Re)}function Br(e,t){let r=e.split("/").pop()??e,n=t.size?[...t].join("+"):"";return Ur.test(r)?{recommended:!1,reason:`tests/infrastructure${n?` \xB7 ${n}`:""}`}:Mr.test(r)?{recommended:!0,reason:`user-facing surface${n?` \xB7 ${n}`:""}`}:{recommended:!1,reason:n?`${n} project`:"source folder"}}function kt(e){let t=[],r=(s,i)=>{let c=Nr(i.files),{recommended:u,reason:d}=Br(s,c);t.push({path:s,fileCount:Lr(ae(e,s)),stacks:[...c],recommended:u,reason:d})},n=(s,i)=>{let c=s?ae(e,s):e,u=Te(c);if(s&&wt(u.files)){r(s,u);return}if(s&&u.dirs.length===0){r(s,u);return}let d=p=>wt(Te(ae(c,p)).files);if((qr(u.files)||u.dirs.some(d)||s==="")&&i<Er){for(let p of u.dirs)n(s?`${s}/${p}`:p,i+1);return}s&&r(s,u)};n("",0),t.sort((s,i)=>Number(i.recommended)-Number(s.recommended)||s.path.localeCompare(i.path));let o=t.filter(s=>s.recommended);return t.slice(0,Math.max(Ar,o.length))}var bt="/v3/projects",P=e=>`${bt}/${e}`;function S(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)??'{"success":true}'}]}}function w(e){return{content:[{type:"text",text:e instanceof Error?e.message:String(e)}],isError:!0}}function St(e,t){let r={profile:e.name,connection:e.connection},n=e.project.projectId,o=e.project.workspaceId,s=()=>B(r,n),i=a=>{let m=a??o;if(!m)throw new Error("No workspace_id given and none in the profile. Call d360_list_workspaces first.");return m},c=(a,m)=>{if(t.cwd)try{let y=vt(t.cwd,a);se(t.cwd,e.name,a.replace(/\\/g,"/"),{...m,...Fr(y)?{localHash:q(_t(y,"utf8"))}:{}})}catch{}},u=()=>t.writesAllowed?null:w(`Refusing to write to PRODUCTION profile "${e.name}". Authorize this session first: run /allow-prod in the REPL, or pass --yes in one-shot/CI.`),d=b("d360_context","Report the active Document360 connection: profile name, environment, project/workspace id, and whether it is a production profile. Call this before maintaining d360-category-map.json so you scope IDs to the right profile.",{},async()=>{try{return S({profile:e.name,environment:e.connection.name,production:e.production,projectId:n??B(r),workspaceId:o??null})}catch(a){return w(a)}}),l=b("d360_list_projects","List all Document360 projects the signed-in user can access (id, name, sub-domain, status).",{},async()=>{try{return S(await $(r,bt))}catch(a){return w(a)}}),p=b("d360_list_workspaces","List workspaces (project versions) for the current project. Each has id, name, slug, is_default, workspace_type.",{project_id:g.string().optional().describe("Defaults to the logged-in/config project.")},async a=>{try{return S(await $(r,`${P(a.project_id??s())}/workspaces`))}catch(m){return w(m)}}),f=b("d360_list_categories","List categories in a workspace (the folder structure for articles).",{workspace_id:g.string().optional().describe("Defaults to d360.workspaceId in config."),project_id:g.string().optional()},async a=>{try{let m=a.workspace_id??o;return m?S(await $(r,`${P(a.project_id??s())}/workspaces/${m}/categories`)):w("No workspace_id given and none in config. Call d360_list_workspaces first.")}catch(m){return w(m)}}),h=b("d360_list_articles","List articles in a workspace (id, title, status, category). Use to see existing docs before writing.",{workspace_id:g.string().optional().describe("Defaults to d360.workspaceId in config."),project_id:g.string().optional()},async a=>{try{let m=a.workspace_id??o;return m?S(await $(r,`${P(a.project_id??s())}/workspaces/${m}/articles`)):w("No workspace_id given and none in config. Call d360_list_workspaces first.")}catch(m){return w(m)}}),_=b("d360_get_article","Get a single article including its content. content_mode=raw returns the stored markdown source (best for editing); display returns processed content.",{article_id:g.string(),content_mode:g.enum(["raw","display"]).optional().describe("raw = stored markdown source (default), display = processed."),published:g.boolean().optional().describe("Read the published version instead of the latest draft."),project_id:g.string().optional()},async a=>{try{return S(await R(r,`${P(a.project_id??s())}/articles/${a.article_id}`,{query:{content_mode:a.content_mode,published:a.published}}))}catch(m){return w(m)}}),x=b("d360_ai_query","Ask Document360's AI search over a workspace's published content. Returns an answer grounded in existing articles \u2014 use to check what's already documented.",{query:g.string(),workspace_id:g.string().optional().describe("Defaults to d360.workspaceId in config."),project_id:g.string().optional()},async a=>{try{let m=a.workspace_id??o;return m?S(await F(r,`${P(a.project_id??s())}/workspaces/ai/query`,{body:{query:a.query,workspace_id:m}})):w("No workspace_id given and none in config. Call d360_list_workspaces first.")}catch(m){return w(m)}}),v=b("d360_sync_status","Deterministic drift report: local docs tree vs Document360, classified against the sync bases in d360-category-map.json. Call this BEFORE any docs gap/coverage analysis \u2014 it proves whether the local docs tree can be trusted as the Document360 inventory (no need to fetch article content). Statuses: local-ahead (push pending), remote-ahead (portal edit to pull), conflict, untracked-local/remote, deleted-local/remote, unknown-base (no base recorded yet).",{},async()=>{try{if(!t.cwd)return w("Sync status needs a repo context (no cwd configured for this session).");let a=await yt({cwd:t.cwd,profileName:e.name});return S({profile:a.profile,docsRoot:a.docsRoot,generatedAt:a.generatedAt,counts:a.counts,attention:a.entries.filter(m=>m.status!=="in-sync")})}catch(a){return w(a)}}),O=b("d360_repo_inventory","Deterministic inventory of candidate source folders for documentation scope (no file reads of your own needed). Returns project-level folders with: path, fileCount, detected stacks, a `recommended` flag (user-facing surfaces pre-recommended; tests/infrastructure not), and a one-line reason. Use this when analyzing a large/multi-project repo to decide which folders back the docs \u2014 it is the same data the /scope picker pre-ticks, so your recommendation and the picker agree. Cheap; safe to call before reading any source.",{},async()=>{try{return t.cwd?S(kt(t.cwd)):w("Repo inventory needs a repo context (no cwd configured for this session).")}catch(a){return w(a)}}),Q=b("d360_create_category","Create a category (a docs folder). Returns the new category id.",{name:g.string(),workspace_id:g.string().optional(),parent_category_id:g.string().optional().describe("Omit for a top-level category."),content:g.string().optional(),slug:g.string().optional(),order:g.number().optional(),project_id:g.string().optional()},async a=>{let m=u();if(m)return m;try{return S(await F(r,`${P(a.project_id??s())}/categories`,{body:{name:a.name,workspace_id:i(a.workspace_id),parent_category_id:a.parent_category_id,content:a.content,slug:a.slug,order:a.order,content_type:"markdown"}}))}catch(y){return w(y)}}),T=b("d360_create_article","Create a DRAFT article in a category. The body is always Markdown (product rule \u2014 we never create WYSIWYG/Block articles). Returns the new article id. Does not publish.",{title:g.string(),category_id:g.string(),content:g.string().optional().describe("Markdown body."),workspace_id:g.string().optional(),slug:g.string().optional(),order:g.number().optional(),project_id:g.string().optional(),local_path:g.string().optional().describe("Repo-relative path of the local .md this article mirrors (e.g. user-docs/01-intro/01-a.md). When given, the new article id + sync base are recorded into d360-category-map.json automatically \u2014 do not edit the articles map yourself.")},async a=>{let m=u();if(m)return m;try{let y=a.project_id??s(),D=a.content;typeof D=="string"&&a.local_path&&t.cwd&&(D=De(D,a.local_path,H(t.cwd,e.name)).content);let k=await F(r,`${P(y)}/articles/bulk`,{body:{articles:[{title:a.title,category_id:a.category_id,workspace_id:i(a.workspace_id),content:D,slug:a.slug,order:a.order,content_type:"markdown"}]}}),M=Array.isArray(k)?k[0]:void 0;if(a.local_path&&typeof M?.id=="string"){let C;try{let U=await R(r,`${P(y)}/articles/${M.id}`,{});C=typeof U?.url=="string"?U.url:void 0}catch{}c(a.local_path,{id:M.id,remoteContentHash:W(a.title,D),remoteVersion:1,...C?{url:C}:{}})}return S(k)}catch(y){return w(y)}}),le=b("d360_update_article","Update an article's title/content/category. Edits the latest draft by default; set auto_fork to safely edit a published article (creates a new draft version).",{article_id:g.string(),title:g.string().optional(),content:g.string().optional().describe("Markdown body."),category_id:g.string().optional(),hidden:g.boolean().optional(),version_number:g.number().optional(),auto_fork:g.boolean().optional().describe("If the target version is published, fork a new draft instead of erroring."),project_id:g.string().optional()},async a=>{let m=u();if(m)return m;try{let{article_id:y,project_id:D,...k}=a,M=D??s(),C=typeof k.content=="string"&&t.cwd?H(t.cwd,e.name):null,U=C?ft(C,y):null;typeof k.content=="string"&&U&&C&&(k.content=De(k.content,U,C).content);let L=null;if(typeof k.content=="string"&&t.onUiEvent)try{L=await R(r,`${P(M)}/articles/${y}`,{query:{content_mode:"raw"}})}catch{}let $e=e.project.languageCode??L?.lang_code,Oe=await ct(r,`${P(M)}/articles/${y}`,{body:k,...$e?{query:{lang_code:$e}}:{}});if(L&&typeof k.content=="string"&&(L.content??"")!==k.content&&t.onUiEvent?.({type:"article_diff",articleId:y,title:L.title??null,oldContent:L.content??"",newContent:k.content}),typeof k.content=="string"&&U)try{let K=Oe,Me=k.title??K?.title??L?.title;typeof Me=="string"&&c(U,{id:y,remoteContentHash:W(Me,k.content),...typeof K?.latest_version=="number"?{remoteVersion:K.latest_version}:{},...typeof K?.modified_at=="string"?{remoteModifiedAt:K.modified_at}:{}})}catch{}return S(Oe)}catch(y){return w(y)}}),G=b("d360_fork_article","Fork a published article into a new draft version \u2014 the safe way to start editing live content.",{article_id:g.string(),project_id:g.string().optional()},async a=>{let m=u();if(m)return m;try{return S(await F(r,`${P(a.project_id??s())}/articles/${a.article_id}/fork`))}catch(y){return w(y)}}),J=b("d360_publish_article","Publish an article version. This makes the draft live to readers \u2014 only call when the user explicitly asks to publish.",{article_id:g.string(),version_number:g.number(),workspace_id:g.string().optional(),message:g.string().optional(),project_id:g.string().optional()},async a=>{let m=u();if(m)return m;try{return S(await F(r,`${P(a.project_id??s())}/articles/${a.article_id}/publish`,{body:{workspace_id:i(a.workspace_id),version_number:a.version_number,message:a.message}}))}catch(y){return w(y)}}),Z=b("d360_unpublish_article","Unpublish an article version, reverting it to draft (removes it from readers).",{article_id:g.string(),version_number:g.number().describe("The published version to unpublish (see d360_get_article)."),workspace_id:g.string().optional(),project_id:g.string().optional()},async a=>{let m=u();if(m)return m;try{return S(await F(r,`${P(a.project_id??s())}/articles/${a.article_id}/unpublish`,{body:{workspace_id:i(a.workspace_id),version_number:a.version_number}}))}catch(y){return w(y)}}),Et=b("d360_upload_drive_file","Upload a local file (e.g. a captured screenshot PNG) to Drive and return its URL for embedding in an article. Uploads to the default folder unless folder_id is given.",{file_path:g.string().describe("Local path to the file (absolute, or relative to the repo)."),folder_id:g.string().optional(),title:g.string().optional(),project_id:g.string().optional()},async a=>{let m=u();if(m)return m;try{let y=a.project_id??s(),D=Wr(a.file_path)?a.file_path:vt(process.cwd(),a.file_path),k=a.folder_id;if(!k&&(k=(await R(r,`${P(y)}/drive/folders/default`))?.id,!k))return w("Could not resolve the default Drive folder.");let M=_t(D),C=new FormData;return C.append("file",new Blob([M]),a.title??Hr(D)),S(await lt(r,`${P(y)}/drive/folders/${k}/files`,C))}catch(y){return w(y)}}),It=b("d360_search_drive","Search Drive files (e.g. to reuse an already-uploaded screenshot instead of uploading a duplicate).",{search_keyword:g.string().optional(),allow_images_only:g.boolean().optional(),project_id:g.string().optional()},async a=>{try{return S(await $(r,`${P(a.project_id??s())}/drive/search`,{query:{search_keyword:a.search_keyword,allow_images_only:a.allow_images_only}}))}catch(m){return w(m)}});return zr({name:"document360",version:"0.2.0",instructions:"First-party Document360 tools. The signed-in user's permissions apply server-side. Project/workspace default from the active profile; pass ids explicitly to override. Create articles as DRAFTS; only call d360_publish_article when the user explicitly asks to publish. All articles are Markdown \u2014 the tools enforce this; never attempt WYSIWYG/Block content. To edit a published article, fork it (d360_fork_article) or update with auto_fork. Before any docs gap/coverage analysis, call d360_sync_status to verify the local docs tree matches Document360. When scoping a large repo (which folders back the docs), call d360_repo_inventory for the candidate folders. Auth errors mean the session expired \u2014 tell the user to run /login (works inside this session).",tools:[d,l,p,f,h,_,x,v,O,Q,T,le,G,J,Z,Et,It]})}var Ct="CLAUDE.md";function xt(e){if(!Ae(e))return[];let t=[];for(let r of Jr(e)){let n=Ee(e,r);if(r===Ct||!Vr(n).isDirectory())continue;let o=Ee(n,"SKILL.md");Ae(o)&&t.push({name:r,body:jt(o,"utf8")})}return t}function Pt(e){let t=Ee(e,Ct);return Ae(t)?jt(t,"utf8"):null}function Kr(e,t){let r=He(),n=We(e),o=Pt(r),s=Pt(n),i=xt(r),c=xt(n),u=new Set(c.map(p=>p.name)),d=[...i.filter(p=>!u.has(p.name)),...c],l=[];if(o&&l.push(o),s&&l.push(`# Project addendum
11
11
 
12
- ${s}`),t&&(l.push("# Project configuration"),l.push("```json"),l.push(JSON.stringify(t,null,2)),l.push("```")),u.length>0){l.push("# Capabilities"),l.push('You have the following specialized documentation skills. Activate the one whose preconditions match the user request. If multiple match, run them in order. These skills are INLINE INSTRUCTIONS in this prompt \u2014 "activating" one means following its steps directly. They are NOT registered with any Skill tool; never attempt to invoke them via a Skill/skill-runner tool.');for(let p of u)l.push(`## Skill: ${p.name}`),l.push(p.body)}return l.join(`
12
+ ${s}`),t&&(l.push("# Project configuration"),l.push("```json"),l.push(JSON.stringify(t,null,2)),l.push("```")),d.length>0){l.push("# Capabilities"),l.push('You have the following specialized documentation skills. Activate the one whose preconditions match the user request. If multiple match, run them in order. These skills are INLINE INSTRUCTIONS in this prompt \u2014 "activating" one means following its steps directly. They are NOT registered with any Skill tool; never attempt to invoke them via a Skill/skill-runner tool.');for(let p of d)l.push(`## Skill: ${p.name}`),l.push(p.body)}return l.join(`
13
13
 
14
- `)}function Vr(){let e=ze(),t={};for(let[r,n]of Object.entries(e.servers))n.type==="stdio"?t[r]={type:"stdio",command:n.command,args:n.args,env:n.env}:n.type==="http"?t[r]={type:"http",url:n.url,headers:n.headers}:n.type==="sse"&&(t[r]={type:"sse",url:n.url,headers:n.headers});return t}var Gr=/auth|api key|login|credential|401|forbidden/i,Te=class{queue;iterator;sdkQuery;uiEvents=[];sessionId=null;constructor(t={}){let r=t.cwd??process.cwd(),n=O(r),o=Jr(r,n),s=Vr();try{let l=L(r,t.profileName),p=!l.production||t.allowProdWrites===!0;s.document360=bt(l,{writesAllowed:p,cwd:r,onUiEvent:f=>this.uiEvents.push(f)})}catch{}this.queue=new X;let i=async(l,p)=>{if(Ue.has(l)){let f=p.file_path??p.notebook_path;if(f&&!le(r,f,n))return{behavior:"deny",message:de(f)}}if(l==="Bash"||l==="PowerShell"){let f=p.command;if(typeof f=="string"){let h=Me(f,r,n);if(h)return{behavior:"deny",message:de(h)}}}return{behavior:"allow",updatedInput:p}},c=n?.mode==="engineer",d={cwd:r,systemPrompt:o,mcpServers:s,...c?{permissionMode:"bypassPermissions",allowDangerouslySkipPermissions:!0}:{permissionMode:"default",canUseTool:i},disallowedTools:["Skill"],includePartialMessages:!0,settingSources:[],model:n?.defaultModel??ye().defaultModel,resume:t.resume},u=zr({prompt:this.queue,options:d});this.sdkQuery=u,this.iterator=u[Symbol.asyncIterator]()}async interrupt(){try{await this.sdkQuery.interrupt()}catch{}}async setModel(t){await this.sdkQuery.setModel(t)}async*send(t){for(this.queue.push(t);;){let r;try{r=await this.iterator.next()}catch(s){let i=s.message;yield{type:"error",kind:Gr.test(i)?"auth":"agent",message:i};return}if(r.done)return;for(;this.uiEvents.length>0;)yield this.uiEvents.shift();let n=r.value?.session_id;n&&!this.sessionId&&(this.sessionId=n,yield{type:"session",sessionId:n});let o=Yr(r.value);if(o){for(let s of o)yield s;if(o.some(s=>s.type==="result"))return}}}close(){this.queue.close()}};function ts(e={}){return new Te(e)}function Kr(e){return typeof e=="string"?e:Array.isArray(e)?e.map(t=>{let r=t;return r.type==="text"&&typeof r.text=="string"?r.text:""}).filter(Boolean).join(`
15
- `):""}function Yr(e){if(!e||typeof e!="object")return null;let t=e;if(t.type==="stream_event"){let r=t.event;return r?.delta&&typeof r.delta.text=="string"?[{type:"text",delta:r.delta.text}]:null}if(t.type==="assistant"){let r=t.message,n=[];for(let o of r?.content??[]){let s=o;s.type==="tool_use"&&n.push({type:"tool",id:s.id??"",name:s.name??"tool",input:s.input??{}})}return n.length>0?n:null}if(t.type==="user"){let r=t.message;if(!Array.isArray(r?.content))return null;let n=[];for(let o of r.content){let s=o;s.type==="tool_result"&&n.push({type:"tool_result",id:s.tool_use_id??"",output:Kr(s.content),isError:s.is_error===!0})}return n.length>0?n:null}if(t.type==="result"){let r=t;return[{type:"result",inputTokens:r.usage?.input_tokens??0,outputTokens:r.usage?.output_tokens??0,ok:r.subtype==="success"}]}return null}import{existsSync as Qr}from"node:fs";import{homedir as Zr}from"node:os";import{join as Xr}from"node:path";var is=["auto","api","subscription"];function jt(){return process.env.CLAUDE_CODE_OAUTH_TOKEN?!0:Qr(Xr(Zr(),".claude",".credentials.json"))}function as(e){let t=!!process.env.ANTHROPIC_API_KEY;return e==="api"?t?{kind:"api"}:{kind:"none"}:e==="subscription"?(delete process.env.ANTHROPIC_API_KEY,{kind:"subscription",stored:jt()}):t?{kind:"api"}:{kind:"subscription",stored:jt()}}import{existsSync as en,readFileSync as tn,writeFileSync as rn}from"node:fs";var nn=100;function W(){let e=fe();if(!en(e))return[];try{let t=JSON.parse(tn(e,"utf8"));return Array.isArray(t)?t:[]}catch{return[]}}function ie(e){T(j());let t=[...e].sort((r,n)=>n.updatedAt.localeCompare(r.updatedAt)).slice(0,nn);rn(fe(),JSON.stringify(t,null,2))}function us(e){let t=W().filter(r=>r.uuid!==e.uuid);t.push(e),ie(t)}function ps(e){return W().find(t=>t.uuid===e)}function on(e){return W().filter(t=>t.cwd===e).sort((t,r)=>r.updatedAt.localeCompare(t.updatedAt))}function fs(e,t){let r=on(e),n=t.toLowerCase(),o=r.find(c=>c.name.toLowerCase()===n);if(o)return o;let s=r.filter(c=>c.name.toLowerCase().startsWith(n));if(s.length===1)return s[0];let i=r.filter(c=>c.uuid.startsWith(t));if(i.length===1)return i[0]}function gs(e){let t=W(),r=t.find(n=>n.uuid===e);r&&(r.updatedAt=new Date().toISOString(),ie(t))}function ms(e,t){let r=W(),n=r.find(o=>o.uuid===e);return n?(n.name=t,n.renamed=!0,n.updatedAt=new Date().toISOString(),ie(r),!0):!1}function hs(e,t){let r=W(),n=r.find(o=>o.uuid===e);!n||n.renamed||(n.name=t,n.titled=!0,ie(r))}function ys(e,t=6){return e.toLowerCase().replace(/[^a-z0-9\s-]/g,"").split(/\s+/).filter(Boolean).slice(0,t).join("-")||"session"}function ws(e){let t=Date.now()-new Date(e).getTime(),r=Math.floor(t/6e4);if(r<1)return"just now";if(r<60)return`${r}m ago`;let n=Math.floor(r/60);return n<24?`${n}h ago`:`${Math.floor(n/24)}d ago`}import{query as sn}from"@anthropic-ai/claude-agent-sdk";var an=6e4;async function bs(e,t){try{return await Promise.race([cn(e,t),ln()])}catch{return null}}async function cn(e,t){let r=sn({prompt:["Generate a short session title for a documentation-writing session that started with this request:","",e.slice(0,500),"","Reply with ONLY the title: 3-6 words, lowercase, kebab-case, no quotes, no punctuation."].join(`
16
- `),options:{cwd:t,model:"haiku",maxTurns:1,settingSources:[],systemPrompt:"You generate terse kebab-case session titles. Output only the title, nothing else.",allowedTools:[]}});for await(let n of r){let o=n;if(o.type==="result")return o.subtype==="success"&&typeof o.result=="string"?dn(o.result):null}return null}function ln(){return new Promise(e=>setTimeout(()=>e(null),an).unref())}function dn(e){let t=e.trim().toLowerCase().replace(/[^a-z0-9\s-]/g,"").replace(/\s+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").split("-").slice(0,8).join("-").slice(0,60);return t.length>=3?t:null}import{query as un}from"@anthropic-ai/claude-agent-sdk";var pn=3e4;async function xs(e,t,r){try{return await Promise.race([fn(e,t,r),gn()])}catch{return null}}async function fn(e,t,r){let n=un({prompt:["In a documentation-writing session, the user asked:","",e.slice(0,800),"","The assistant's reply ended with:","",t.slice(-4e3),"","Suggest the user's most likely next prompt. Reply with ONLY that prompt:","imperative, specific, at most 15 words, no quotes \u2014 a complete, grammatical","sentence the user could send verbatim. If the assistant asked a question,",'answer it. If the assistant offered options AND recommended one ("the right','approach", "recommended", "I suggest"), your suggestion must accept the',"RECOMMENDED option \u2014 never an alternative the assistant advised against or","deferred. If there is no clear next action, reply with exactly: NONE"].join(`
17
- `),options:{cwd:r,model:"haiku",maxTurns:1,settingSources:[],systemPrompt:"You propose the next user prompt for a docs-writing CLI. Output only the prompt, or NONE.",allowedTools:[]}});for await(let o of n){let s=o;if(s.type==="result")return s.subtype==="success"&&typeof s.result=="string"?mn(s.result):null}return null}function gn(){return new Promise(e=>setTimeout(()=>e(null),pn).unref())}function mn(e){let t=e.trim().split(`
18
- `)[0].replace(/^["'`]+|["'`]+$/g,"").trim();return!t||/^none\b/i.test(t)||t.length<4||t.length>100?null:t}var hn=/^---\r?\n[\s\S]*?\r?\n---\r?\n/;function Ct(e){let t=e.match(hn);return t?{frontmatter:t[0],body:e.slice(t[0].length)}:{frontmatter:null,body:e}}import{existsSync as yn,mkdirSync as wn,readFileSync as _n,writeFileSync as kn}from"node:fs";import{dirname as bn,join as Dt}from"node:path";var Rt=/^\[Screenshot: [^\]]+\]\s*$/;function vn(e){let t=e.replace(/\r\n/g,`
14
+ `)}function Yr(){let e=Ve(),t={};for(let[r,n]of Object.entries(e.servers))n.type==="stdio"?t[r]={type:"stdio",command:n.command,args:n.args,env:n.env}:n.type==="http"?t[r]={type:"http",url:n.url,headers:n.headers}:n.type==="sse"&&(t[r]={type:"sse",url:n.url,headers:n.headers});return t}var Qr=/auth|api key|login|credential|401|forbidden/i,Ie=class{queue;iterator;sdkQuery;uiEvents=[];sessionId=null;constructor(t={}){let r=t.cwd??process.cwd(),n=N(r),o=Kr(r,n),s=Yr();try{let l=z(r,t.profileName),p=!l.production||t.allowProdWrites===!0;s.document360=St(l,{writesAllowed:p,cwd:r,onUiEvent:f=>this.uiEvents.push(f)})}catch{}this.queue=new te;let i=async(l,p)=>{if(Ne.has(l)){let f=p.file_path??p.notebook_path;if(f&&!de(r,f,n))return{behavior:"deny",message:ue(f)}}if(l==="Bash"||l==="PowerShell"){let f=p.command;if(typeof f=="string"){let h=qe(f,r,n);if(h)return{behavior:"deny",message:ue(h)}}}return{behavior:"allow",updatedInput:p}},c=n?.mode==="engineer",u={cwd:r,systemPrompt:o,mcpServers:s,...c?{permissionMode:"bypassPermissions",allowDangerouslySkipPermissions:!0}:{permissionMode:"default",canUseTool:i},disallowedTools:["Skill"],includePartialMessages:!0,settingSources:[],model:n?.defaultModel??we().defaultModel,resume:t.resume},d=Gr({prompt:this.queue,options:u});this.sdkQuery=d,this.iterator=d[Symbol.asyncIterator]()}async interrupt(){try{await this.sdkQuery.interrupt()}catch{}}async setModel(t){await this.sdkQuery.setModel(t)}async*send(t){for(this.queue.push(t);;){let r;try{r=await this.iterator.next()}catch(s){let i=s.message;yield{type:"error",kind:Qr.test(i)?"auth":"agent",message:i};return}if(r.done)return;for(;this.uiEvents.length>0;)yield this.uiEvents.shift();let n=r.value?.session_id;n&&!this.sessionId&&(this.sessionId=n,yield{type:"session",sessionId:n});let o=Xr(r.value);if(o){for(let s of o)yield s;if(o.some(s=>s.type==="result"))return}}}close(){this.queue.close()}};function as(e={}){return new Ie(e)}function Zr(e){return typeof e=="string"?e:Array.isArray(e)?e.map(t=>{let r=t;return r.type==="text"&&typeof r.text=="string"?r.text:""}).filter(Boolean).join(`
15
+ `):""}function Xr(e){if(!e||typeof e!="object")return null;let t=e;if(t.type==="stream_event"){let r=t.event;return r?.delta&&typeof r.delta.text=="string"?[{type:"text",delta:r.delta.text}]:null}if(t.type==="assistant"){let r=t.message,n=[];for(let o of r?.content??[]){let s=o;s.type==="tool_use"&&n.push({type:"tool",id:s.id??"",name:s.name??"tool",input:s.input??{}})}return n.length>0?n:null}if(t.type==="user"){let r=t.message;if(!Array.isArray(r?.content))return null;let n=[];for(let o of r.content){let s=o;s.type==="tool_result"&&n.push({type:"tool_result",id:s.tool_use_id??"",output:Zr(s.content),isError:s.is_error===!0})}return n.length>0?n:null}if(t.type==="result"){let r=t;return[{type:"result",inputTokens:r.usage?.input_tokens??0,outputTokens:r.usage?.output_tokens??0,ok:r.subtype==="success"}]}return null}import{existsSync as en}from"node:fs";import{homedir as tn}from"node:os";import{join as rn}from"node:path";var ps=["auto","api","subscription"];function Dt(){return process.env.CLAUDE_CODE_OAUTH_TOKEN?!0:en(rn(tn(),".claude",".credentials.json"))}function fs(e){let t=!!process.env.ANTHROPIC_API_KEY;return e==="api"?t?{kind:"api"}:{kind:"none"}:e==="subscription"?(delete process.env.ANTHROPIC_API_KEY,{kind:"subscription",stored:Dt()}):t?{kind:"api"}:{kind:"subscription",stored:Dt()}}import{existsSync as nn,readFileSync as on,writeFileSync as sn}from"node:fs";var an=100;function V(){let e=ge();if(!nn(e))return[];try{let t=JSON.parse(on(e,"utf8"));return Array.isArray(t)?t:[]}catch{return[]}}function ce(e){E(j());let t=[...e].sort((r,n)=>n.updatedAt.localeCompare(r.updatedAt)).slice(0,an);sn(ge(),JSON.stringify(t,null,2))}function ys(e){let t=V().filter(r=>r.uuid!==e.uuid);t.push(e),ce(t)}function ws(e){return V().find(t=>t.uuid===e)}function cn(e){return V().filter(t=>t.cwd===e).sort((t,r)=>r.updatedAt.localeCompare(t.updatedAt))}function ks(e,t){let r=cn(e),n=t.toLowerCase(),o=r.find(c=>c.name.toLowerCase()===n);if(o)return o;let s=r.filter(c=>c.name.toLowerCase().startsWith(n));if(s.length===1)return s[0];let i=r.filter(c=>c.uuid.startsWith(t));if(i.length===1)return i[0]}function _s(e){let t=V(),r=t.find(n=>n.uuid===e);r&&(r.updatedAt=new Date().toISOString(),ce(t))}function vs(e,t){let r=V(),n=r.find(o=>o.uuid===e);return n?(n.name=t,n.renamed=!0,n.updatedAt=new Date().toISOString(),ce(r),!0):!1}function bs(e,t){let r=V(),n=r.find(o=>o.uuid===e);!n||n.renamed||(n.name=t,n.titled=!0,ce(r))}function Ss(e,t=6){return e.toLowerCase().replace(/[^a-z0-9\s-]/g,"").split(/\s+/).filter(Boolean).slice(0,t).join("-")||"session"}function xs(e){let t=Date.now()-new Date(e).getTime(),r=Math.floor(t/6e4);if(r<1)return"just now";if(r<60)return`${r}m ago`;let n=Math.floor(r/60);return n<24?`${n}h ago`:`${Math.floor(n/24)}d ago`}import{query as ln}from"@anthropic-ai/claude-agent-sdk";var dn=6e4;async function Cs(e,t){try{return await Promise.race([un(e,t),pn()])}catch{return null}}async function un(e,t){let r=ln({prompt:["Generate a short session title for a documentation-writing session that started with this request:","",e.slice(0,500),"","Reply with ONLY the title: 3-6 words, lowercase, kebab-case, no quotes, no punctuation."].join(`
16
+ `),options:{cwd:t,model:"haiku",maxTurns:1,settingSources:[],systemPrompt:"You generate terse kebab-case session titles. Output only the title, nothing else.",allowedTools:[]}});for await(let n of r){let o=n;if(o.type==="result")return o.subtype==="success"&&typeof o.result=="string"?fn(o.result):null}return null}function pn(){return new Promise(e=>setTimeout(()=>e(null),dn).unref())}function fn(e){let t=e.trim().toLowerCase().replace(/[^a-z0-9\s-]/g,"").replace(/\s+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").split("-").slice(0,8).join("-").slice(0,60);return t.length>=3?t:null}import{query as gn}from"@anthropic-ai/claude-agent-sdk";var mn=3e4;async function Ts(e,t,r){try{return await Promise.race([hn(e,t,r),yn()])}catch{return null}}async function hn(e,t,r){let n=gn({prompt:["In a documentation-writing session, the user asked:","",e.slice(0,800),"","The assistant's reply ended with:","",t.slice(-4e3),"","Suggest the user's most likely next prompt. Reply with ONLY that prompt:","imperative, specific, at most 15 words, no quotes \u2014 a complete, grammatical","sentence the user could send verbatim. If the assistant asked a question,",'answer it. If the assistant offered options AND recommended one ("the right','approach", "recommended", "I suggest"), your suggestion must accept the',"RECOMMENDED option \u2014 never an alternative the assistant advised against or","deferred. If there is no clear next action, reply with exactly: NONE"].join(`
17
+ `),options:{cwd:r,model:"haiku",maxTurns:1,settingSources:[],systemPrompt:"You propose the next user prompt for a docs-writing CLI. Output only the prompt, or NONE.",allowedTools:[]}});for await(let o of n){let s=o;if(s.type==="result")return s.subtype==="success"&&typeof s.result=="string"?wn(s.result):null}return null}function yn(){return new Promise(e=>setTimeout(()=>e(null),mn).unref())}function wn(e){let t=e.trim().split(`
18
+ `)[0].replace(/^["'`]+|["'`]+$/g,"").trim();return!t||/^none\b/i.test(t)||t.length<4||t.length>100?null:t}var kn=/^---\r?\n[\s\S]*?\r?\n---\r?\n/;function Rt(e){let t=e.match(kn);return t?{frontmatter:t[0],body:e.slice(t[0].length)}:{frontmatter:null,body:e}}import{existsSync as _n,mkdirSync as vn,readFileSync as bn,writeFileSync as Sn}from"node:fs";import{dirname as xn,join as Tt}from"node:path";var At=/^\[Screenshot: [^\]]+\]\s*$/;function Pn(e){let t=e.replace(/\r\n/g,`
19
19
  `).split(`
20
- `),r=new Map;for(let n=0;n<t.length;n++){if(!t[n].startsWith("<!-- SCREENSHOT"))continue;let o=n;for(;n<t.length&&!t[n].includes("-->");)n++;if(n>=t.length)break;let s=t.slice(o,n+1),i=n+1;for(;i<t.length&&t[i].trim()==="";)i++;i<t.length&&Rt.test(t[i])&&r.set(t[i].trim(),[...s,...t.slice(n+1,i)])}return r}function Sn(e,t,r){let n=[],o=t.replace(/\r\n/g,`
21
- `).replace(/^\n+/,""),s=r?Ct(r).frontmatter:null,i=o,c=0;if(r){let u=vn(r);u.size>0&&(i=o.split(`
22
- `).flatMap(l=>{let p=u.get(l.trim());return p&&Rt.test(l)?(c++,[...p,l]):[l]}).join(`
20
+ `),r=new Map;for(let n=0;n<t.length;n++){if(!t[n].startsWith("<!-- SCREENSHOT"))continue;let o=n;for(;n<t.length&&!t[n].includes("-->");)n++;if(n>=t.length)break;let s=t.slice(o,n+1),i=n+1;for(;i<t.length&&t[i].trim()==="";)i++;i<t.length&&At.test(t[i])&&r.set(t[i].trim(),[...s,...t.slice(n+1,i)])}return r}function jn(e,t,r){let n=[],o=t.replace(/\r\n/g,`
21
+ `).replace(/^\n+/,""),s=r?Rt(r).frontmatter:null,i=o,c=0;if(r){let d=Pn(r);d.size>0&&(i=o.split(`
22
+ `).flatMap(l=>{let p=d.get(l.trim());return p&&At.test(l)?(c++,[...p,l]):[l]}).join(`
23
23
  `))}return c>0&&n.push(`${c} screenshot spec comment(s) re-attached from the existing local file.`),r&&/\]\((\.\.?\/)[^)]+\.md\)/.test(r)&&n.push("The existing local file has relative cross-article links; publishing flattened them to plain text on Document360, so this pull does not restore them."),/!\[[^\]]*\]\(https?:\/\/[^)]*\)/.test(o)&&n.push("Remote content embeds uploaded images (Drive URLs); they are kept as-is locally."),s&&n.push("Existing frontmatter preserved."),{content:`${s??""}# ${e}
24
24
 
25
25
  ${i}`.replace(/\n*$/,`
26
- `),notes:n}}async function $s(e,t=E){let r=L(e.cwd,e.profileName),n=H(e.cwd,r.name),o=n?.articles[e.relPath];if(!n||!o)throw new Error(`"${e.relPath}" is not in d360-category-map.json for profile "${r.name}". Run /sync to see tracked articles.`);let s={profile:r.name,connection:r.connection},i=n.projectId??r.project.projectId??N(s),c=await t(s,`/v3/projects/${i}/articles/${o.id}`,{query:{content_mode:"raw"}}),d=Dt(e.cwd,e.relPath),u=yn(d)?_n(d,"utf8"):null,l=c.title??"",{content:p,notes:f}=Sn(l,c.content??"",u);return{path:e.relPath,articleId:o.id,title:l,oldContent:u??"",newContent:p,remoteContentHash:B(c.title,c.content),latestVersion:c.latest_version,modifiedAt:c.modified_at,overwritesLocalChanges:u!==null&&o.localHash!==void 0&&U(u)!==o.localHash,notes:f}}function Os(e,t){let r=L(e.cwd,e.profileName),n=Dt(e.cwd,t.path),o=t.oldContent.includes(`\r
26
+ `),notes:n}}async function Ls(e,t=R){let r=z(e.cwd,e.profileName),n=H(e.cwd,r.name),o=n?.articles[e.relPath];if(!n||!o)throw new Error(`"${e.relPath}" is not in d360-category-map.json for profile "${r.name}". Run /sync to see tracked articles.`);let s={profile:r.name,connection:r.connection},i=n.projectId??r.project.projectId??B(s),c=await t(s,`/v3/projects/${i}/articles/${o.id}`,{query:{content_mode:"raw"}}),u=Tt(e.cwd,e.relPath),d=_n(u)?bn(u,"utf8"):null,l=c.title??"",{content:p,notes:f}=jn(l,c.content??"",d);return{path:e.relPath,articleId:o.id,title:l,oldContent:d??"",newContent:p,remoteContentHash:W(c.title,c.content),latestVersion:c.latest_version,modifiedAt:c.modified_at,overwritesLocalChanges:d!==null&&o.localHash!==void 0&&q(d)!==o.localHash,notes:f}}function Bs(e,t){let r=z(e.cwd,e.profileName),n=Tt(e.cwd,t.path),o=t.oldContent.includes(`\r
27
27
  `)?t.newContent.replace(/\n/g,`\r
28
- `):t.newContent;wn(bn(n),{recursive:!0}),kn(n,o,"utf8"),ne(e.cwd,r.name,t.path,{id:t.articleId,localHash:U(o),remoteContentHash:t.remoteContentHash,remoteVersion:t.latestVersion,remoteModifiedAt:t.modifiedAt})}export{is as AUTH_MODES,te as D360ApiError,A as D360AuthError,Te as EngineSession,V as ProfileConfigError,ir as apiLogDir,Os as applyPull,Kt as buildAuthorizationUrl,ct as categoryMapPath,Sr as classify,Kn as clearTokens,mt as computeSyncStatus,ts as createSession,gt as createV3RemoteProvider,E as d360Get,I as d360GetAll,it as d360Patch,q as d360Post,at as d360Upload,nt as decodeJwtClaims,T as ensureDir,Ge as exchangeCode,fs as findByName,ut as findPathByArticleId,bs as generateTitle,st as getAccessToken,so as getArticle,ps as getSession,U as hashContent,B as hashRemote,yt as inventoryRepo,rt as isExpired,yr as knownEnvironments,on as listSessions,oo as listWorkspaces,H as loadProfileMap,W as loadSessions,be as loadTokens,xe as logApiCall,Se as logBodiesAlways,ee as loggableBody,Wn as loginPkce,hr as normalizeMarkdown,Ft as packageRoot,Le as packageSkillsDir,zt as pickModel,$s as planPull,ge as projectConfigPath,Fe as projectSkillsDir,ze as readMcpConfig,O as readProjectConfig,ye as readUserConfig,Sn as reconstructLocalMarkdown,ne as recordSyncBase,ar as redactSecrets,Ye as refreshTokens,ws as relativeTime,ms as renameSession,L as resolveActiveProfile,as as resolveAuth,oe as resolveConnection,ft as resolveDocsRoot,fo as resolveEnvironment,Mn as resolveModelSetting,He as resolveProfile,N as resolveProjectId,tt as saveTokens,fe as sessionsFilePath,yo as setProfileProject,hs as setTitle,ys as slugify,Ct as splitFrontmatter,xs as suggestNextAction,Qe as toStoredTokens,gs as touchSession,cr as truncateBody,us as upsertSession,ue as userConfigPath,j as userDir,pe as userMcpConfigPath,Nn as writeMcpConfig,We as writeProjectConfig,Un as writeUserConfig};
28
+ `):t.newContent;vn(xn(n),{recursive:!0}),Sn(n,o,"utf8"),se(e.cwd,r.name,t.path,{id:t.articleId,localHash:q(o),remoteContentHash:t.remoteContentHash,remoteVersion:t.latestVersion,remoteModifiedAt:t.modifiedAt})}export{ps as AUTH_MODES,ne as D360ApiError,I as D360AuthError,Ie as EngineSession,Y as ProfileConfigError,cr as apiLogDir,Bs as applyPull,Qt as buildAuthorizationUrl,dt as categoryMapPath,Pr as classify,Zn as clearTokens,yt as computeSyncStatus,as as createSession,ht as createV3RemoteProvider,R as d360Get,$ as d360GetAll,ct as d360Patch,F as d360Post,lt as d360Upload,st as decodeJwtClaims,E as ensureDir,Ye as exchangeCode,ks as findByName,ft as findPathByArticleId,Cs as generateTitle,at as getAccessToken,co as getArticle,ws as getSession,q as hashContent,W as hashRemote,kt as inventoryRepo,ot as isExpired,kr as knownEnvironments,cn as listSessions,ao as listWorkspaces,H as loadProfileMap,V as loadSessions,be as loadTokens,Pe as logApiCall,xe as logBodiesAlways,re as loggableBody,Vn as loginPkce,wr as normalizeMarkdown,Wt as packageRoot,He as packageSkillsDir,Vt as pickModel,Ls as planPull,me as projectConfigPath,We as projectSkillsDir,Ve as readMcpConfig,N as readProjectConfig,we as readUserConfig,jn as reconstructLocalMarkdown,se as recordSyncBase,lr as redactSecrets,Ze as refreshTokens,xs as relativeTime,vs as renameSession,z as resolveActiveProfile,fs as resolveAuth,ie as resolveConnection,De as resolveCrossLinks,mt as resolveDocsRoot,ho as resolveEnvironment,Ln as resolveModelSetting,ze as resolveProfile,B as resolveProjectId,nt as saveTokens,ge as sessionsFilePath,_o as setProfileProject,bs as setTitle,Ss as slugify,Rt as splitFrontmatter,Ts as suggestNextAction,Xe as toStoredTokens,_s as touchSession,dr as truncateBody,ys as upsertSession,pe as userConfigPath,j as userDir,fe as userMcpConfigPath,Bn as writeMcpConfig,Je as writeProjectConfig,qn as writeUserConfig};
@@ -10,6 +10,9 @@ export type ArticleSyncEntry = {
10
10
  remoteVersion?: number;
11
11
  remoteModifiedAt?: string;
12
12
  lastSyncedAt?: string;
13
+ /** Public Document360 URL of the article — used to resolve inline cross-article
14
+ links at publish (see sync/crossLinks.ts). Captured when the article is created. */
15
+ url?: string;
13
16
  };
14
17
  export type ProfileMap = {
15
18
  environment?: string;
@@ -0,0 +1,9 @@
1
+ import type { ProfileMap } from './categoryMap.js';
2
+ export type CrossLinkResult = {
3
+ content: string;
4
+ resolved: number;
5
+ unresolved: number;
6
+ };
7
+ /** `fromRepoPath` is the repo-relative path of the article being published (so
8
+ relative targets resolve against it). `map` supplies target path → url. */
9
+ export declare function resolveCrossLinks(content: string, fromRepoPath: string, map: ProfileMap | null): CrossLinkResult;
@@ -1,6 +1,7 @@
1
1
  export * from './types.js';
2
2
  export * from './hash.js';
3
3
  export * from './frontmatter.js';
4
+ export * from './crossLinks.js';
4
5
  export * from './categoryMap.js';
5
6
  export * from './docsRoot.js';
6
7
  export * from './remote.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document360-engine",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Headless documentation agent engine for document360-writer / -desktop. Emits a typed event stream; no UI.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/skills/CLAUDE.md CHANGED
@@ -70,7 +70,7 @@ When publishing (only when explicitly asked, via `publish-to-d360` skill), trans
70
70
  - Strip the YAML frontmatter block (`---` ... `---` at the top). It is local-only metadata.
71
71
  - Strip the H1 line. Document360 stores the title separately and renders it itself.
72
72
  - Strip every `<!-- SCREENSHOT ... -->` HTML comment block. Keep the visible `[Screenshot: ...]` line.
73
- - Convert relative cross-article links (e.g. `[Foo](../03-foo.md)`) to plain-text references like `See "Foo" "Intro"`. Document360 handles cross-linking differently.
73
+ - Leave relative cross-article links (e.g. `[Foo](../03-foo.md)`) AS-IS in the body — do NOT convert them to plain text. The publish tools (`d360_create_article` / `d360_update_article`) resolve each to the target's live Document360 URL automatically via the category map; a target not yet published degrades to plain text on its own and resolves on a later publish. (Provide `local_path` on create so the resolver knows the source article's location.)
74
74
  - Everything else passes through verbatim.
75
75
 
76
76
  ## Terminology
@@ -126,3 +126,10 @@ Suggestion quality rules:
126
126
  - **Only suggest commands that currently apply.** After a run that synced everything, `/publish <path>` is a no-op — don't offer it. Think: what would actually be the user's next move?
127
127
  - **After a bulk operation, suggest the bulk form** (`/publish --all`, `/sync pull --all`) — never one arbitrary example article out of thirty.
128
128
  - **Suggestions must be consistent with the state you just reported.** If you said "everything is in sync", `/sync pull --all` cannot be the next move. Re-read your own findings before offering commands; an inapplicable suggestion is worse than none.
129
+
130
+ When the choices are **courses of action, not slash commands** (e.g. "convert all callouts to DFM" vs "convert the FAQ too" vs "leave as GFM"), the auto-numbered quick-run buttons do NOT apply — those only fire on standalone command lines. So number the options yourself and tell the user how to pick:
131
+
132
+ - Present them as a numbered list — `1.`, `2.`, `3.` — one course of action per item, each a short imperative phrase the user can scan.
133
+ - Close with a single prompt line, e.g. `Reply with the number and I'll action it.` Keep it to one line; no preamble.
134
+ - A bare number in the user's next message refers to the option you numbered — treat "1" as "do option 1", and act on it without re-asking. If your offer has changed since (new turn, new options), renumber and re-present rather than guessing.
135
+ - Cap it at a handful of options (≤4). If there are more, that's a sign the choice is too open — narrow it or recommend one.
@@ -20,7 +20,7 @@
20
20
  - Strip the YAML frontmatter block (`---` ... `---` at the top, e.g. `sources:`) — local-only metadata, never published.
21
21
  - Strip the H1 line (its text becomes the article `title`).
22
22
  - Strip every `<!-- SCREENSHOT ... -->` block. Keep the visible `[Screenshot: ...]` line — unless the screenshot has been captured and uploaded (see Screenshots below), in which case replace it with the markdown image.
23
- - Convert relative `[link](../foo/bar.md)` to plain text like `See "foo" "bar"`.
23
+ - Leave relative `[link](../foo/bar.md)` cross-article links AS-IS — the create/update tools resolve them to live Document360 URLs via the category map (forward refs to unpublished targets degrade to plain text automatically). Always pass `local_path` on create so the resolver knows the source article's location.
24
24
  3. Look up the article path in the active profile's `articles` map.
25
25
  4. **Update path** (found): call `d360_update_article` with `article_id`, `title`, `content` (publish form). If the mapped article is already published, set `auto_fork: true` (or call `d360_fork_article` first) so you edit a fresh draft rather than erroring.
26
26
  5. **Create path** (not found): resolve the `category_id` from the path's parent folder via the `categories` table (create the category first with `d360_create_category` if it doesn't exist yet, recording its id). Call `d360_create_article` with `title`, `category_id`, `content` (content_type defaults to markdown), and `local_path` set to the repo-relative `.md` path — the tool records the new article id + sync base into `d360-category-map.json` itself. Do NOT edit the `articles` map by hand (the `categories` map is still yours to maintain; make sure the profile section exists BEFORE creating articles, or the automatic recording has nowhere to write).