document360-writer 0.4.96 → 0.4.97

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/cli.js CHANGED
@@ -197,9 +197,9 @@ No MCP servers registered. Add one with /mcp add <name> <type> <ref>
197
197
  `+bc[e.tone](Nt(e.text));case"done":return`
198
198
  `+(e.ok?z.magenta("\u2736 "):z.red("\u2736 "))+z.gray(`Cooked for ${Mt(e.seconds)} \xB7 ${e.tokens} tokens`+(e.costUsd>0?` \xB7 ${le(e.costUsd)}`:""))}}function ni(e,t){return e.map(n=>Oo(n,t)).join(`
199
199
  `)}import{resolveActiveProfile as xc,loadTokens as $c,isExpired as vc,decodeJwtClaims as oi}from"document360-engine";function Cc(e){return e.length>12?`${e.slice(0,8)}\u2026`:e}function Se(e,t){try{let n=xc(e,t),o=$c(n.name);if(!o)return{ok:!1,reason:`Not signed in to Document360 (profile "${n.name}"). Run /login first.`};if(vc(o)&&!o.refreshToken)return{ok:!1,reason:`Your Document360 session expired (profile "${n.name}"). Run /login.`};let r=n.project.projectId;if(r){let c={...oi(o.idToken)??{},...oi(o.accessToken)??{}}.doc360_project_id;if(typeof c=="string"&&c&&c!==r)return{ok:!1,reason:`Your Document360 session is signed into a different project than profile "${n.name}" (this profile's project is ${Cc(r)}). Run /login and pick this project.`}}return{ok:!0}}catch{return{ok:!0}}}var Wt=3;function Mo(e){return[`Run the publish-to-d360 skill for the article at: ${e}`,"","Steps you must follow:","1. Read the article markdown \u2014 use the repo-relative path exactly as given above (it resolves against the working directory; do not reconstruct an absolute path).","2. Compute the D360 publish form (strip the YAML frontmatter and the H1 title, strip every <!-- SCREENSHOT ... --> block while keeping the visible [Screenshot: ...] line, normalize cross-article links).","3. Look up the article ID in <repo>/d360-category-map.json. If present, call the document360 MCP update-article tool. If absent, call create-article with local_path set to the repo-relative .md path (the tool records the new ID + sync base in the category map itself \u2014 do not edit the articles map by hand).","4. Report what changed and remind me to publish the draft manually in the Document360 portal."].join(`
200
- `)}var ri={"local-ahead":"modified locally","untracked-local":"new (never published)",conflict:"\u26A0 conflict \u2014 publishing overwrites the portal edit"};function Ft(e){return e.filter(t=>t.path!==null&&t.status in ri&&!t.adopt).map(t=>({path:t.path,label:ri[t.status]}))}function Rn(e){return e.filter(t=>t.adopt).length}function jc(e){let t=e.replace(/\\/g,"/").split("/").pop()??e,n=t.replace(/^\d+[-_]/,"").replace(/[-_]+/g," ").trim();return n?n.charAt(0).toUpperCase()+n.slice(1):t}function si(e){let t=[];e.length>1&&t.push({kind:"all",label:`Publish all ${e.length} articles`,paths:e.map(o=>o.path),indent:!1});let n=new Map;for(let o of e){let r=o.path.replace(/\\/g,"/").split("/").slice(0,-1).join("/"),s=n.get(r);s?s.push(o):n.set(r,[o])}for(let o of[...n.keys()].sort()){let r=n.get(o).sort((s,c)=>s.path.localeCompare(c.path));t.push({kind:"category",label:`${jc(o)} (${r.length})`,paths:r.map(s=>s.path),indent:!1});for(let s of r){let c=s.path.replace(/\\/g,"/").split("/").pop()??s.path;t.push({kind:"article",label:c,paths:[s.path],indent:!0,note:s.label})}}return t}function Dn(e){return[`Run the publish-to-d360 skill for ALL of these ${e.length} articles, one by one:`,"",...e.map(t=>`- ${t}`),"","Use the repo-relative paths exactly as listed (they resolve against the working directory; do not reconstruct absolute paths). Apply the normal per-article publish flow (frontmatter/H1/screenshot-comment strips, link normalization; update when mapped, create with local_path when not). If one article fails, note it and continue. Finish with a summary table (article | created/updated | failures) and remind me drafts need review in the portal."].join(`
201
- `)}function Wo(e,t,n){let o=k=>t[k.index]?.paths.length??0,r=e.filter(k=>k.ok),s=e.filter(k=>!k.ok),c=e.reduce((k,C)=>k+o(C),0),p=r.reduce((k,C)=>k+o(C),0),h=n==="api"?`${le(e.reduce((k,C)=>k+C.costUsd,0))} total`:Ue(e.reduce((k,C)=>k+C.outputTokens,0)),b=`${r.length}/${e.length} partition${e.length===1?"":"s"}`,w=k=>`${k} article${k===1?"":"s"}`,R=[];if(s.length===0)R.push(`Published ${w(c)} (${b}) successfully \xB7 ${h}.`),R.push("All are DRAFTS \u2014 review and publish them in the Document360 portal.");else{R.push(`Published ${p}/${w(c)} (${b}) \xB7 ${h}.`),R.push(`${s.length} partition${s.length===1?"":"s"} failed \u2014 re-run /publish --all to retry just the rest (published articles are skipped):`);for(let k of s)R.push(` \u2717 ${k.label}${k.error?` \u2014 ${k.error}`:""}`)}return R}async function Uo(e,t){let n=Tc(t),{model:o}=Rc(e.cwd,"standard");console.log(f(`Publishing ${t.length} article${t.length===1?"":"s"} across ${n.length} partition${n.length===1?"":"s"} (\u2264${Wt} agents at once) on ${o}\u2026`)),console.log(f(" (mid-run abort is TUI-only \u2014 Ctrl+C exits the REPL.)"));try{for await(let r of Dc({cwd:e.cwd,partitions:n,promptFor:s=>Dn(s.paths),concurrency:Wt,profileName:e.profileName,allowProdWrites:e.allowProdWrites(),model:o}))if(r.type==="partition_status")r.status==="running"?console.log(f(` \u25B8 ${r.label} \u2014 publishing\u2026`)):r.status==="done"?console.log(E(` \u2713 ${r.label}`)):console.log($(` \u2717 ${r.label}`));else if(r.type==="run_done"){console.log("");let s=process.env.ANTHROPIC_API_KEY?"api":"subscription";for(let c of Wo(r.results,n,s))console.log(r.ok?E(c):U(c))}}catch(r){console.log($(`Publish run failed: ${r.message}`))}return console.log(""),{kind:"continue"}}async function jn(e,t,n){let o=Sc(e,n);if(o.length===0)return[];let r=new Set(Ft((await Lo({cwd:e,profileName:t})).entries).map(s=>s.path));return o.filter(s=>r.has(s))}async function ii(e,t){let n=Se(t?.cwd??process.cwd(),t?.profileName);if(!n.ok)return console.log($(n.reason)),{kind:"continue"};let o=e[0];if(o==="--related"&&e[1]){if(!t)return{kind:"continue"};let r=await jn(t.cwd,t.profileName,e[1].replace(/\\/g,"/"));return r.length===0?(console.log(E("\u2713 All related articles are already published.")),{kind:"continue"}):await Uo(t,r)}if(o==="--all"){console.log(f("Checking what needs publishing\u2026"));try{let r=await Lo({cwd:t?.cwd??process.cwd()}),s=Ft(r.entries),c=Rn(r.entries);return c>0&&console.log(U(`\u26A0 ${c} article(s) already exist on Document360 but aren't linked \u2014 left out (publishing would duplicate them). Run /catch-up or ask me to adopt them first.`)),s.length===0?(console.log(E(c>0?"\u2713 Nothing else is ahead of Document360 to publish.":"\u2713 Nothing is ahead of Document360 \u2014 no publish candidates.")),{kind:"continue"}):t?await Uo(t,s.map(p=>p.path)):{kind:"forward-to-agent",prompt:Dn(s.map(p=>p.path)),display:"/publish --all"}}catch(r){return console.log($(`Could not compute sync status: ${r.message}`)),{kind:"continue"}}}if(!o){console.log(f("Checking what needs publishing\u2026"));let r;try{let s=await Lo({cwd:t?.cwd??process.cwd()});r=Ft(s.entries);let c=Rn(s.entries);c>0&&console.log(U(`\u26A0 ${c} article(s) already exist on Document360 but aren't linked \u2014 left out (publishing would duplicate them). Run /catch-up or ask me to adopt them first.`))}catch(s){return console.log($(`Could not compute sync status: ${s.message}`)),console.log(f("Publish a specific article: /publish <article-path>")),{kind:"continue"}}if(r.length===0)return console.log(E("\u2713 Nothing is ahead of Document360 \u2014 no publish candidates.")),console.log(f(" (Articles without a sync base are unverified \u2014 publish those by path if needed.)")),{kind:"continue"};if(!process.stdin.isTTY||!t){for(let s of r)console.log(` ${s.path} ${f(`(${s.label})`)}`);return console.log(f("Run: /publish <article-path>")),{kind:"continue"}}try{o=await t.withPausedInput(()=>Pc({message:"Publish which article?",choices:[...r.length>1?[{name:`All ${r.length} candidates`,value:"--all",description:"one agent run over every candidate"}]:[],...r.map(s=>({name:s.path,value:s.path,description:s.label}))]}))}catch{return console.log(f("Cancelled.")),{kind:"continue"}}if(o==="--all")return await Uo(t,r.map(s=>s.path))}return{kind:"forward-to-agent",prompt:Mo(o),display:`/publish ${o}`}}async function An(){return{kind:"forward-to-agent",prompt:["Run the gap-analysis skill against this repo.","","Follow its stages in order:","1. d360_sync_status first \u2014 stop and report if any article is in conflict or remote-ahead (the local copy cannot be trusted until resolved).","2. git diff since the lastAnalyzedCommit marker in d360-category-map.json (or last 30 days if no marker), filtered to the authoritativeSourceFiles paths.","2.5. d360_pending_hints \u2014 the source agent's own notes on what it shipped. Verify each (sources exist, commit is an ancestor of HEAD, source shows the surface) before trusting; fold verified ones into the table, flag the rest.","3. Map changes (diff + hints) to articles via their `sources:` frontmatter; read only implicated articles and changed source files. If articles are missing `sources:`, this is the one-time bootstrap pass \u2014 backfill them.","4. Output the proposal table: | Article (path or proposed title) | Action (create | update | retire | adopt) | Reason | Evidence | Scope |, then advance lastAnalyzedCommit and stamp consumed hints.","","Do not start writing or updating any article yet \u2014 the proposal table is the deliverable."].join(`
202
- `),display:"/audit"}}W();import{checkbox as Ac}from"@inquirer/prompts";import{inventoryRepo as Ec,readProjectConfig as ai,writeProjectConfig as Ic}from"document360-engine";function Fo(e,t){let n=ai(e);n&&(n.authoritativeSourceFiles=t,Ic(n,e))}function En(e){let t=[`${e.fileCount}${e.fileCount>=5e3?"+":""} files`];return e.stacks.length&&t.push(e.stacks.join("+")),t.push(e.reason),t.join(" \xB7 ")}async function li(e,t){let n=t?.cwd??process.cwd();if(!ai(n))return console.log($("No .d360-writer.json here. Run /init first.")),{kind:"continue"};let o=Ec(n);if(o.length===0)return console.log(f('No candidate source folders found. Set "authoritativeSourceFiles" in .d360-writer.json manually.')),{kind:"continue"};if(!process.stdin.isTTY||!t){console.log(""),console.log(I("Recommended documentation scope (run /scope in the REPL to choose):"));for(let s of o)console.log(` ${s.recommended?"\u25C9":"\u25CB"} ${s.path} ${f(En(s))}`);return console.log(""),{kind:"continue"}}let r;try{r=await t.withPausedInput(()=>Ac({message:"Which folders back the user docs? (space toggles, enter confirms)",choices:o.map(s=>({name:`${s.path} (${En(s)})`,value:s.path,checked:s.recommended})),pageSize:20}))}catch{return console.log(f("Cancelled.")),{kind:"continue"}}if(r.length===0)return console.log(f("Nothing selected \u2014 scope unchanged.")),{kind:"continue"};Fo(n,r),console.log(E(`\u2713 Scoped to ${r.length} folder(s) \u2014 written to .d360-writer.json`));for(let s of r)console.log(` ${s}`);return console.log(""),{kind:"continue"}}W();import{confirm as Lc}from"@inquirer/prompts";import{applyPull as fi,computeSyncStatus as mi,planPull as Mc,D360AuthError as Wc}from"document360-engine";W();var ci=[{status:"conflict",header:"Changed in both places \u2014 pick a direction (/sync pull <path> or /publish <path>):",paint:$,mark:"!"},{status:"local-ahead",header:"Changed here \u2014 push with /publish <path>:",paint:U,mark:"M"},{status:"remote-ahead",header:"Changed on Document360 \u2014 pull with /sync pull <path>:",paint:I,mark:"M"},{status:"deleted-local",header:"Removed locally \u2014 still on Document360 (pull to restore, or leave it):",paint:$,mark:"D"},{status:"deleted-remote",header:"Gone from Document360 \u2014 still here (publish to recreate, or delete the file):",paint:$,mark:"D"},{status:"orphaned",header:"Gone from both sides \u2014 these tracking entries point at nothing now:",paint:ee,mark:"X"},{status:"untracked-local",header:"New here \u2014 not on Document360 yet (publish to add them):",paint:f,mark:"?"},{status:"untracked-remote",header:"On Document360 \u2014 not in your local docs yet:",paint:f,mark:"?"},{status:"unknown-base",header:"Tracked, not yet compared \u2014 the next publish or pull will set a baseline:",paint:ee,mark:"\xB7"}];function Nc(e){return e.path?e.path:`${e.title??"(untitled)"} ${ee(`[${e.articleId}]`)}`}function In(e){let t=[],n=e.entries.filter(h=>h.adopt);if(n.length>0){t.push(""),t.push(xe("Already on Document360, just not linked \u2014 ask me to adopt these (a republish would duplicate them):"));for(let h of n)t.push(` ${U("~")} ${U(h.path??"")} ${ee(`\u2192 "${h.adopt.title}"`)}`)}for(let h of ci){let b=e.entries.filter(w=>w.status===h.status&&!(h.status==="untracked-local"&&w.adopt));if(b.length!==0){t.push(""),t.push(xe(h.header));for(let w of b)t.push(` ${h.paint(h.mark)} ${h.paint(Nc(w))}${w.detail?ee(` (${w.detail})`):""}`)}}let o=e.counts["in-sync"]??0,r=e.entries.length;t.push("");let s={"in-sync":"up to date","local-ahead":"changed here","remote-ahead":"changed on Document360",conflict:"changed both places","untracked-local":"new here","untracked-remote":"only on Document360","deleted-local":"removed locally","deleted-remote":"gone from Document360",orphaned:"gone both sides","unknown-base":"not yet compared"},c=n.length,p=r===o?E("\u2713 everything up to date"):[o>0?E(`\u2713 ${o} up to date`):null,c>0?`${c} to adopt`:null,...ci.map(h=>{let b=(e.counts[h.status]??0)-(h.status==="untracked-local"?c:0);return b>0?`${b} ${s[h.status]}`:null})].filter(Boolean).join(ee(" \xB7 "));return t.push(p+ee(` \xB7 ${r} tracked+seen \xB7 profile "${e.profile}" \xB7 docs root ${e.docsRoot}/`)),t}import{structuredPatch as _c}from"diff";import ui from"picocolors";var di=80,Oc="\x1B[48;2;74;28;28m",Uc="\x1B[48;2;24;66;24m",pi="\x1B[49m",Ho=e=>String(e).padStart(5);function Xe(e,t,n){let o=k=>{let C=k.replace(/\r\n/g,`
200
+ `)}var ri={"local-ahead":"modified locally","untracked-local":"new (never published)",conflict:"\u26A0 conflict \u2014 publishing overwrites the portal edit"};function Ft(e){return e.filter(t=>t.path!==null&&t.status in ri&&!t.link).map(t=>({path:t.path,label:ri[t.status]}))}function Rn(e){return e.filter(t=>t.link).length}function jc(e){let t=e.replace(/\\/g,"/").split("/").pop()??e,n=t.replace(/^\d+[-_]/,"").replace(/[-_]+/g," ").trim();return n?n.charAt(0).toUpperCase()+n.slice(1):t}function si(e){let t=[];e.length>1&&t.push({kind:"all",label:`Publish all ${e.length} articles`,paths:e.map(o=>o.path),indent:!1});let n=new Map;for(let o of e){let r=o.path.replace(/\\/g,"/").split("/").slice(0,-1).join("/"),s=n.get(r);s?s.push(o):n.set(r,[o])}for(let o of[...n.keys()].sort()){let r=n.get(o).sort((s,c)=>s.path.localeCompare(c.path));t.push({kind:"category",label:`${jc(o)} (${r.length})`,paths:r.map(s=>s.path),indent:!1});for(let s of r){let c=s.path.replace(/\\/g,"/").split("/").pop()??s.path;t.push({kind:"article",label:c,paths:[s.path],indent:!0,note:s.label})}}return t}function Dn(e){return[`Run the publish-to-d360 skill for ALL of these ${e.length} articles, one by one:`,"",...e.map(t=>`- ${t}`),"","Use the repo-relative paths exactly as listed (they resolve against the working directory; do not reconstruct absolute paths). Apply the normal per-article publish flow (frontmatter/H1/screenshot-comment strips, link normalization; update when mapped, create with local_path when not). If one article fails, note it and continue. Finish with a summary table (article | created/updated | failures) and remind me drafts need review in the portal."].join(`
201
+ `)}function Wo(e,t,n){let o=k=>t[k.index]?.paths.length??0,r=e.filter(k=>k.ok),s=e.filter(k=>!k.ok),c=e.reduce((k,C)=>k+o(C),0),p=r.reduce((k,C)=>k+o(C),0),h=n==="api"?`${le(e.reduce((k,C)=>k+C.costUsd,0))} total`:Ue(e.reduce((k,C)=>k+C.outputTokens,0)),b=`${r.length}/${e.length} partition${e.length===1?"":"s"}`,w=k=>`${k} article${k===1?"":"s"}`,R=[];if(s.length===0)R.push(`Published ${w(c)} (${b}) successfully \xB7 ${h}.`),R.push("All are DRAFTS \u2014 review and publish them in the Document360 portal.");else{R.push(`Published ${p}/${w(c)} (${b}) \xB7 ${h}.`),R.push(`${s.length} partition${s.length===1?"":"s"} failed \u2014 re-run /publish --all to retry just the rest (published articles are skipped):`);for(let k of s)R.push(` \u2717 ${k.label}${k.error?` \u2014 ${k.error}`:""}`)}return R}async function Uo(e,t){let n=Tc(t),{model:o}=Rc(e.cwd,"standard");console.log(f(`Publishing ${t.length} article${t.length===1?"":"s"} across ${n.length} partition${n.length===1?"":"s"} (\u2264${Wt} agents at once) on ${o}\u2026`)),console.log(f(" (mid-run abort is TUI-only \u2014 Ctrl+C exits the REPL.)"));try{for await(let r of Dc({cwd:e.cwd,partitions:n,promptFor:s=>Dn(s.paths),concurrency:Wt,profileName:e.profileName,allowProdWrites:e.allowProdWrites(),model:o}))if(r.type==="partition_status")r.status==="running"?console.log(f(` \u25B8 ${r.label} \u2014 publishing\u2026`)):r.status==="done"?console.log(E(` \u2713 ${r.label}`)):console.log($(` \u2717 ${r.label}`));else if(r.type==="run_done"){console.log("");let s=process.env.ANTHROPIC_API_KEY?"api":"subscription";for(let c of Wo(r.results,n,s))console.log(r.ok?E(c):U(c))}}catch(r){console.log($(`Publish run failed: ${r.message}`))}return console.log(""),{kind:"continue"}}async function jn(e,t,n){let o=Sc(e,n);if(o.length===0)return[];let r=new Set(Ft((await Lo({cwd:e,profileName:t})).entries).map(s=>s.path));return o.filter(s=>r.has(s))}async function ii(e,t){let n=Se(t?.cwd??process.cwd(),t?.profileName);if(!n.ok)return console.log($(n.reason)),{kind:"continue"};let o=e[0];if(o==="--related"&&e[1]){if(!t)return{kind:"continue"};let r=await jn(t.cwd,t.profileName,e[1].replace(/\\/g,"/"));return r.length===0?(console.log(E("\u2713 All related articles are already published.")),{kind:"continue"}):await Uo(t,r)}if(o==="--all"){console.log(f("Checking what needs publishing\u2026"));try{let r=await Lo({cwd:t?.cwd??process.cwd()}),s=Ft(r.entries),c=Rn(r.entries);return c>0&&console.log(U(`\u26A0 ${c} article(s) already exist on Document360 but aren't linked \u2014 left out (publishing would duplicate them). Run /catch-up or ask me to link them first.`)),s.length===0?(console.log(E(c>0?"\u2713 Nothing else is ahead of Document360 to publish.":"\u2713 Nothing is ahead of Document360 \u2014 no publish candidates.")),{kind:"continue"}):t?await Uo(t,s.map(p=>p.path)):{kind:"forward-to-agent",prompt:Dn(s.map(p=>p.path)),display:"/publish --all"}}catch(r){return console.log($(`Could not compute sync status: ${r.message}`)),{kind:"continue"}}}if(!o){console.log(f("Checking what needs publishing\u2026"));let r;try{let s=await Lo({cwd:t?.cwd??process.cwd()});r=Ft(s.entries);let c=Rn(s.entries);c>0&&console.log(U(`\u26A0 ${c} article(s) already exist on Document360 but aren't linked \u2014 left out (publishing would duplicate them). Run /catch-up or ask me to link them first.`))}catch(s){return console.log($(`Could not compute sync status: ${s.message}`)),console.log(f("Publish a specific article: /publish <article-path>")),{kind:"continue"}}if(r.length===0)return console.log(E("\u2713 Nothing is ahead of Document360 \u2014 no publish candidates.")),console.log(f(" (Articles without a sync base are unverified \u2014 publish those by path if needed.)")),{kind:"continue"};if(!process.stdin.isTTY||!t){for(let s of r)console.log(` ${s.path} ${f(`(${s.label})`)}`);return console.log(f("Run: /publish <article-path>")),{kind:"continue"}}try{o=await t.withPausedInput(()=>Pc({message:"Publish which article?",choices:[...r.length>1?[{name:`All ${r.length} candidates`,value:"--all",description:"one agent run over every candidate"}]:[],...r.map(s=>({name:s.path,value:s.path,description:s.label}))]}))}catch{return console.log(f("Cancelled.")),{kind:"continue"}}if(o==="--all")return await Uo(t,r.map(s=>s.path))}return{kind:"forward-to-agent",prompt:Mo(o),display:`/publish ${o}`}}async function An(){return{kind:"forward-to-agent",prompt:["Run the gap-analysis skill against this repo.","","Follow its stages in order:","1. d360_sync_status first \u2014 stop and report if any article is in conflict or remote-ahead (the local copy cannot be trusted until resolved).","2. git diff since the lastAnalyzedCommit marker in d360-category-map.json (or last 30 days if no marker), filtered to the authoritativeSourceFiles paths.","2.5. d360_pending_hints \u2014 the source agent's own notes on what it shipped. Verify each (sources exist, commit is an ancestor of HEAD, source shows the surface) before trusting; fold verified ones into the table, flag the rest.","3. Map changes (diff + hints) to articles via their `sources:` frontmatter; read only implicated articles and changed source files. If articles are missing `sources:`, this is the one-time bootstrap pass \u2014 backfill them.","4. Output the proposal table: | Article (path or proposed title) | Action (create | update | retire | link) | Reason | Evidence | Scope |, then advance lastAnalyzedCommit and stamp consumed hints.","","Do not start writing or updating any article yet \u2014 the proposal table is the deliverable."].join(`
202
+ `),display:"/audit"}}W();import{checkbox as Ac}from"@inquirer/prompts";import{inventoryRepo as Ec,readProjectConfig as ai,writeProjectConfig as Ic}from"document360-engine";function Fo(e,t){let n=ai(e);n&&(n.authoritativeSourceFiles=t,Ic(n,e))}function En(e){let t=[`${e.fileCount}${e.fileCount>=5e3?"+":""} files`];return e.stacks.length&&t.push(e.stacks.join("+")),t.push(e.reason),t.join(" \xB7 ")}async function li(e,t){let n=t?.cwd??process.cwd();if(!ai(n))return console.log($("No .d360-writer.json here. Run /init first.")),{kind:"continue"};let o=Ec(n);if(o.length===0)return console.log(f('No candidate source folders found. Set "authoritativeSourceFiles" in .d360-writer.json manually.')),{kind:"continue"};if(!process.stdin.isTTY||!t){console.log(""),console.log(I("Recommended documentation scope (run /scope in the REPL to choose):"));for(let s of o)console.log(` ${s.recommended?"\u25C9":"\u25CB"} ${s.path} ${f(En(s))}`);return console.log(""),{kind:"continue"}}let r;try{r=await t.withPausedInput(()=>Ac({message:"Which folders back the user docs? (space toggles, enter confirms)",choices:o.map(s=>({name:`${s.path} (${En(s)})`,value:s.path,checked:s.recommended})),pageSize:20}))}catch{return console.log(f("Cancelled.")),{kind:"continue"}}if(r.length===0)return console.log(f("Nothing selected \u2014 scope unchanged.")),{kind:"continue"};Fo(n,r),console.log(E(`\u2713 Scoped to ${r.length} folder(s) \u2014 written to .d360-writer.json`));for(let s of r)console.log(` ${s}`);return console.log(""),{kind:"continue"}}W();import{confirm as Lc}from"@inquirer/prompts";import{applyPull as fi,computeSyncStatus as mi,planPull as Mc,D360AuthError as Wc}from"document360-engine";W();var ci=[{status:"conflict",header:"Changed in both places \u2014 pick a direction (/sync pull <path> or /publish <path>):",paint:$,mark:"!"},{status:"local-ahead",header:"Changed here \u2014 push with /publish <path>:",paint:U,mark:"M"},{status:"remote-ahead",header:"Changed on Document360 \u2014 pull with /sync pull <path>:",paint:I,mark:"M"},{status:"deleted-local",header:"Removed locally \u2014 still on Document360 (pull to restore, or leave it):",paint:$,mark:"D"},{status:"deleted-remote",header:"Gone from Document360 \u2014 still here (publish to recreate, or delete the file):",paint:$,mark:"D"},{status:"orphaned",header:"Gone from both sides \u2014 these tracking entries point at nothing now:",paint:ee,mark:"X"},{status:"untracked-local",header:"New here \u2014 not on Document360 yet (publish to add them):",paint:f,mark:"?"},{status:"untracked-remote",header:"On Document360 \u2014 not in your local docs yet:",paint:f,mark:"?"},{status:"unknown-base",header:"Tracked, not yet compared \u2014 the next publish or pull will set a baseline:",paint:ee,mark:"\xB7"}];function Nc(e){return e.path?e.path:`${e.title??"(untitled)"} ${ee(`[${e.articleId}]`)}`}function In(e){let t=[],n=e.entries.filter(h=>h.link);if(n.length>0){t.push(""),t.push(xe("Already on Document360, just not linked \u2014 ask me to link these (a republish would duplicate them):"));for(let h of n)t.push(` ${U("~")} ${U(h.path??"")} ${ee(`\u2192 "${h.link.title}"`)}`)}for(let h of ci){let b=e.entries.filter(w=>w.status===h.status&&!(h.status==="untracked-local"&&w.link));if(b.length!==0){t.push(""),t.push(xe(h.header));for(let w of b)t.push(` ${h.paint(h.mark)} ${h.paint(Nc(w))}${w.detail?ee(` (${w.detail})`):""}`)}}let o=e.counts["in-sync"]??0,r=e.entries.length;t.push("");let s={"in-sync":"up to date","local-ahead":"changed here","remote-ahead":"changed on Document360",conflict:"changed both places","untracked-local":"new here","untracked-remote":"only on Document360","deleted-local":"removed locally","deleted-remote":"gone from Document360",orphaned:"gone both sides","unknown-base":"not yet compared"},c=n.length,p=r===o?E("\u2713 everything up to date"):[o>0?E(`\u2713 ${o} up to date`):null,c>0?`${c} to link`:null,...ci.map(h=>{let b=(e.counts[h.status]??0)-(h.status==="untracked-local"?c:0);return b>0?`${b} ${s[h.status]}`:null})].filter(Boolean).join(ee(" \xB7 "));return t.push(p+ee(` \xB7 ${r} tracked+seen \xB7 profile "${e.profile}" \xB7 docs root ${e.docsRoot}/`)),t}import{structuredPatch as _c}from"diff";import ui from"picocolors";var di=80,Oc="\x1B[48;2;74;28;28m",Uc="\x1B[48;2;24;66;24m",pi="\x1B[49m",Ho=e=>String(e).padStart(5);function Xe(e,t,n){let o=k=>{let C=k.replace(/\r\n/g,`
203
203
  `);return C.endsWith(`
204
204
  `)||C===""?C:C+`
205
205
  `},r=o(e),s=o(t);if(r===s)return null;let c=Math.max(20,n-10),p=_c("a","b",r,s,"","",{context:3}),h=0,b=0,w=[];p.hunks.forEach((k,C)=>{C>0&&w.push(ui.gray(" \u2026"));let P=k.oldStart,N=k.newStart;for(let O of k.lines){let q=O[0],te=O.slice(1).slice(0,c);q==="-"?(b++,w.push(`${Oc}${Ho(P++)} - ${te}${pi}`)):q==="+"?(h++,w.push(`${Uc}${Ho(N++)} + ${te}${pi}`)):(w.push(ui.gray(Ho(N))+" "+te),P++,N++)}});let R=w.slice(0,di);return{added:h,removed:b,lines:R,hidden:Math.max(0,w.length-di)}}async function hi(e,t){let n=Se(t.cwd,t.profileName);if(!n.ok)return console.log($(n.reason)),{kind:"continue"};let o=(e[0]??"status").toLowerCase();try{if(o==="status")return await Fc(t.cwd),{kind:"continue"};if(o==="pull")return await Hc(t,e.slice(1)),{kind:"continue"};console.log($(`Unknown subcommand: /sync ${o}`)),console.log(f("Usage: /sync drift report (local vs Document360)")),console.log(f(" /sync pull <path> pull portal edits into the local file")),console.log(f(" /sync pull --all pull every remote-ahead article"))}catch(r){r instanceof Wc?console.log($(r.message)):console.log($(`Sync failed: ${r.message}`))}return{kind:"continue"}}async function Fc(e){console.log(f("Checking Document360 for drift\u2026"));let t=await mi({cwd:e});for(let n of In(t))console.log(n);console.log("")}async function Hc(e,t){let n=t[0];if(!n){console.log($("Usage: /sync pull <article-path> | --all"));return}let o;if(n==="--all"){if(console.log(f("Checking Document360 for drift\u2026")),o=(await mi({cwd:e.cwd})).entries.filter(s=>s.status==="remote-ahead"&&s.path).map(s=>s.path),o.length===0){console.log(E("\u2713 Nothing is remote-ahead \u2014 no pulls needed.")),console.log(f(" (conflicts are never bulk-pulled; pull them one by one: /sync pull <path>)"));return}console.log(`${xe(String(o.length))} article(s) are remote-ahead.`)}else o=[n.replace(/\\/g,"/")];for(let r of o){let s=await Mc({cwd:e.cwd,relPath:r});console.log(""),console.log(`${I("\u25CF")} ${xe(s.title)} ${ee(`(${s.path})`)}`);for(let b of s.notes)console.log(U(` \u26A0 ${b}`));s.overwritesLocalChanges&&console.log(U(" \u26A0 This OVERWRITES local edits made since the last sync."));let c=Xe(s.oldContent,s.newContent,Math.max(40,(process.stdout.columns??80)-1));if(!c){console.log(f(" Local file already matches the remote content \u2014 advancing the sync base only.")),fi({cwd:e.cwd},s);continue}let p=b=>b===1?"":"s";console.log(ee(` \u23BF Added ${c.added} line${p(c.added)}, removed ${c.removed} line${p(c.removed)}`));for(let b of c.lines)console.log(b);if(c.hidden>0&&console.log(f(` \u2026 +${c.hidden} more diff lines`)),!await e.withPausedInput(()=>Lc({message:`Write ${s.path}?`,default:!s.overwritesLocalChanges}))){console.log(f(" Skipped."));continue}fi({cwd:e.cwd},s),console.log(E(` \u2713 Pulled ${s.path} (sync base advanced).`))}console.log("")}W();import{statSync as qc}from"node:fs";import{resolve as Bc}from"node:path";import{estimateBulkCost as Gc,planPartitions as Vc,readProjectConfig as Yc,resolveModelForOperation as zc,runPartitioned as Kc,trackedArticlePaths as Xc}from"document360-engine";var qo=3;function Bo(e){let t,n=!1;for(let r=0;r<e.length;r++){let s=e[r];s==="--run"||s==="--yes"?n=!0:s==="--scope"?t=e[++r]:s?.startsWith("--scope=")&&(t=s.slice(8))}return{scope:t?.replace(/\\/g,"/").replace(/\/+$/,"")||void 0,run:n}}function Go(e,t){return t?e.filter(n=>{let o=n.replace(/\\/g,"/");return o===t||o.startsWith(`${t}/`)}):e}function Vo(e,t){return t.map(n=>{let o=0;try{o=qc(Bc(e,n)).size}catch{o=0}return{path:n,bytes:o}})}function Yo(e){return`Convert each of these articles to canonical Document360 Flavored Markdown (DFM) and re-publish each as a DRAFT. This is a mechanical syntax conversion, not a rewrite \u2014 be efficient:
@@ -253,7 +253,7 @@ ${La}`}),B(l?["/doctor","/init"]:["/init"]);return}l&&B(["/doctor"]);try{let i=c
253
253
  `)}),ft(ze()),Dt.current=_e(e).status==="current",u({kind:"note",tone:"info",text:`No separate builder agent? Then just ask me now: "write the docs for this repo" \u2014 I'll read the code directly.`}))},[e,u,de,ft]),ln=Y(async l=>{let i;try{i=await It(e,l)}catch{u({kind:"note",tone:"info",text:"Next: pick a workspace \u2014 run /workspace."}),B(["/workspace"]);return}if(i.workspaces.length===0){u({kind:"note",tone:"warn",text:"This project has no workspaces yet. Create one in Document360, then run /workspace."});return}let a=i.workspaces.filter(d=>(d.workspace_type??"").toLowerCase()!=="apidocumentation"),g=a.length>0?a:i.workspaces;if(g.length===1){let d=g[0],m=i.workspaces.length-g.length;u({kind:"note",tone:"ok",text:`Selected the "${d.name??d.id}" workspace${m>0?" (skipped the API-documentation workspace)":""}.`}),an(i.profile,i.projectId,d);return}u({kind:"note",tone:"info",text:`Next: pick the workspace your articles publish to (${g.length} available).`}),B(["/workspace"])},[e,u,an]),os=Y((l,i)=>{$o(e,l,i.id,i.name),u({kind:"note",tone:"ok",text:`Project set to "${i.name??i.id}" (agent restarted).`}),de(),ln(l)},[e,u,de,ln]),rs=Y(async l=>{let i;try{i=ce(e,l)}catch{return}if(!i.project.workspaceId){if(i.project.projectId){if(!i.project.projectName)try{let{projects:a}=await gt(e,i.name),g=a.find(d=>d.id===i.project.projectId);g?.name&&Ep(e,i.name,{projectName:g.name})}catch{}await ln(i.name);return}u({kind:"note",tone:"info",text:"Next: pick the Document360 project."}),B(["/project"])}},[e,u,ln]),cn=Y(()=>{let l=Ae.current[0];if(!l)return;u({kind:"note",tone:"info",text:`\u25CF ${l.title} (${l.path})`});for(let a of l.notes)u({kind:"note",tone:"warn",text:`\u26A0 ${a}`});l.overwritesLocalChanges&&u({kind:"note",tone:"warn",text:"\u26A0 This OVERWRITES local edits made since the last sync."});let i=Xe(l.oldContent,l.newContent,Ie());u(i?{kind:"diff",added:i.added,removed:i.removed,lines:i.lines,hidden:i.hidden}:{kind:"note",tone:"info",text:"Local file already matches the remote content \u2014 applying only advances the sync base."}),u({kind:"note",tone:"info",text:`Write ${l.path}? (y/n \u2014 anything else cancels)`})},[u,Ie]),ss=Y(l=>{if(Ae.current.length===0)return!1;let i=l.trim().toLowerCase();if(i==="y"||i==="yes"){let a=Ae.current.shift();try{kp({cwd:e,profileName:s},a),u({kind:"note",tone:"ok",text:`\u2713 Pulled ${a.path} (sync base advanced).`}),Ae.current.length===0&&B(g=>g.length>0?g:["/sync"])}catch(g){u({kind:"note",tone:"error",text:`Pull failed: ${g.message}`})}}else if(i==="n"||i==="no"){let a=Ae.current.shift();u({kind:"note",tone:"info",text:`Skipped ${a.path}.`})}else{let a=Ae.current.length;return Ae.current=[],u({kind:"note",tone:"info",text:`Pull cancelled (${a} article(s) left untouched).`}),!0}return cn(),!0},[e,s,cn,u]),is=Y(l=>{let i=no.current;if(!i)return!1;if(no.current=null,se(null),Qe.current++,l.trim()!==i.repoName)return u({kind:"note",tone:"warn",text:"Reset cancelled \u2014 nothing deleted."}),!0;let{removed:a,failed:g}=Pr(e,i.targets);u({kind:"note",tone:g.length?"warn":"ok",text:`\u2713 Reset complete \u2014 removed ${a.length} item${a.length===1?"":"s"}. The repo is back to its original state.`});for(let d of g)u({kind:"note",tone:"error",text:` \u2717 ${d.path}: ${d.error}`});return i.hadDevhints&&(u({kind:"note",tone:"warn",text:Pn().join(`
254
254
  `)}),ft(Ro())),u({kind:"note",tone:"info",text:`Clean slate \u2014 set up d360-writer again whenever you're ready:
255
255
  ${La}`}),B(["/init"]),Xt(!1),!0},[e,u,ft]),as=Y(async l=>{let i=l.slice(1).trim().split(/\s+/),a=(i[0]??"").toLowerCase(),g=i.slice(1);switch(Xt(!0),a){case"help":u({kind:"note",tone:"info",text:kn().join(`
256
- `)});return;case"exit":case"quit":me.current?.close(),r();return;case"clear":de(),nt.current=[],Xt(!1),se(null),Qe.current++,u({kind:"note",tone:"info",text:"Conversation reset (the previous session is still resumable via /resume)."});return;case"login":{let d;try{d=ce(e,s)}catch(m){u({kind:"note",tone:"error",text:m.message});return}u({kind:"note",tone:"info",text:`Profile "${d.name}" \u2192 ${d.connection.name} (${d.connection.apiUrl})${d.production?" \u26A0 PRODUCTION":""}`}),te(!0);try{let m=await op(d.connection,{promptForRedirect:()=>Promise.reject(new Error("Manual login is CLI-only. Run: d360-writer login --manual"))},v=>u({kind:"note",tone:"info",text:v})),y=rp(d.name,m);if(sp(y),await vo(y,d.name,v=>u({kind:"note",tone:"info",text:v})),u({kind:"note",tone:"ok",text:`\u2713 Logged in to "${d.name}" as ${mn(y)}`}),await rs(s),Rt.current){let v=Rt.current;Rt.current=null;let D=Se(e,s);D.ok?(u({kind:"note",tone:"ok",text:`\u2713 Signed into the right project \u2014 press 1 to run ${v}.`}),B([v])):(u({kind:"note",tone:"warn",text:`${D.reason} (then ${v}.)`}),B(["/login"]),Rt.current=v)}else tn.current&&(se(tn.current),tn.current=null,u({kind:"note",tone:"info",text:"Press tab to re-send your last prompt."}))}catch(m){u({kind:"note",tone:"error",text:`Login failed: ${m.message}`})}finally{te(!1),re(m=>m+1)}return}case"allow-prod":{let d=!1;try{d=ce(e,s).production}catch{}if(!d){u({kind:"note",tone:"info",text:"Current profile is not production \u2014 writes are already allowed."});return}ot.current=!0,de(),u({kind:"note",tone:"warn",text:"\u26A0 Production writes authorized for this session."});return}case"rename":{let d=ur(g.join(" ")),m=Pe.current.uuid;if(!m){u({kind:"note",tone:"error",text:"Send a message first \u2014 sessions save once the agent replies."});return}if(!d){u({kind:"note",tone:"info",text:"Thinking of a name\u2026"});let y=Pe.current.firstPrompt??"";_a(y,e).then(v=>{v?(se(`/rename ${v}`),u({kind:"note",tone:"info",text:`Suggestion: "${v}" \u2014 press tab to accept, or type /rename <your name>.`})):u({kind:"note",tone:"info",text:"Usage: /rename <name>"})}).catch(()=>u({kind:"note",tone:"info",text:"Usage: /rename <name>"}));return}gp(m,d),rt(d),Gt(`${Er} d360-writer \xB7 ${d}`),u({kind:"note",tone:"ok",text:`Session renamed to "${d}".`});return}case"profile":{let d=g[0],m=Q(e);if(!d){let y=Object.entries(m?.profiles??{});if(y.length===0){u({kind:"note",tone:"info",text:"No profiles. Run /init first."});return}let v=y.map(([oe,K])=>({name:oe,env:K.connection?.environment??"custom",prod:K.production===!0,who:go(oe)})),D=s??m?.defaultProfile,j=Math.max(0,v.findIndex(oe=>oe.name===D));st({cursor:j,current:j,rows:v});return}if(d==="add"){let y=dr(e,g[1],g[2]);if(y){u({kind:"note",tone:"error",text:y});return}u({kind:"note",tone:"ok",text:`\u2713 Profile "${g[1]}" created (environment: ${g[2]??g[1]}).`}),wo(g[1],!1);return}if(!m?.profiles?.[d]){u({kind:"note",tone:"error",text:`Unknown profile "${d}". Create it: /profile add ${d} <environment>`});return}wo(d,!0);return}case"doctor":await qt(g,{cwd:e});return;case"mcp":await Sn(g);return;case"init":{if(Q(e)){u({kind:"note",tone:"info",text:"This repo is already set up \u2014 edit .d360-writer.json directly (or d360-writer init for the CLI wizard)."});return}let d=$p().map(m=>({name:m,apiUrl:vp(m).apiUrl}));it({cursor:0,rows:d});return}case"resume":{let d=g.join(" ").trim(),m=hp(e).filter(D=>D.uuid!==Pe.current.uuid);if(!d){if(!m.length){u({kind:"note",tone:"info",text:"No saved sessions for this repo yet."});return}Ve({query:"",cursor:0,sessions:m});return}let y=mp(e,d);if(!y){u({kind:"note",tone:"error",text:`No session matches "${d}".`});return}de(y.uuid),Pe.current={uuid:y.uuid,firstPrompt:y.firstPrompt,titleFired:!0},rt(y.name),Ur(y.uuid),nt.current=[];let v=Ar(e,y.uuid);for(let D of v)u(D);u({kind:"note",tone:"ok",text:v.length?`Resumed "${y.name}" \u2014 restored ${v.length} message(s); continue where you left off.`:`Resumed "${y.name}" (agent memory reconnected; no saved transcript to replay).`});return}case"workspace":{let d=g.join(" ").trim(),m;try{m=await It(e,s)}catch(v){u({kind:"note",tone:"error",text:`Could not list workspaces: ${v.message}`});return}if(!d){let v=m.workspaces.map(j=>({id:j.id,name:j.name??j.id,type:j.workspace_type}));if(v.length===0){u({kind:"note",tone:"info",text:"No workspaces in this project."});return}let D=Math.max(0,v.findIndex(j=>j.id===m.current));at({cursor:D,current:D,rows:v,profile:m.profile,projectId:m.projectId,projectName:m.projectName,environment:m.environment});return}let y=xo(m.workspaces,d);if(!y){u({kind:"note",tone:"error",text:`No workspace matches "${d}".`});return}an(m.profile,m.projectId,y);return}case"project":{let d=g.join(" ").trim(),m;try{m=await gt(e,s)}catch(v){u({kind:"note",tone:"error",text:`Could not list projects: ${v.message} (signed in? try /login)`});return}if(!d){let v=m.projects.map(j=>({id:j.id,name:j.name??j.id,sub:j.sub_domain}));if(v.length===0){u({kind:"note",tone:"info",text:"No projects found for this identity."});return}let D=v.findIndex(j=>j.id===m.current);lt({cursor:Math.max(0,D),current:D,rows:v,profile:m.profile,environment:m.environment});return}let y=hs(m.projects,d);if(!y){u({kind:"note",tone:"error",text:`No project matches "${d}". Available: ${m.projects.map(v=>v.name??v.id).join(", ")}`});return}os(m.profile,y);return}case"logout":{let d;try{d=ce(e,s).name}catch(y){u({kind:"note",tone:"error",text:y.message});return}let m=cp(d);up(e,d),de(),re(y=>y+1),u({kind:"note",tone:m?"ok":"info",text:m?`\u2713 Signed out of "${d}" and cleared its project/workspace selection.`:`Profile "${d}" was not signed in. Cleared any project/workspace selection.`}),u({kind:"note",tone:"info",text:"Run /login to sign in and pick a project."}),B(["/login"]);return}case"publish":{if(!co(l.trim()))return;if(g[0]==="--related"&&g[1]){let m=await jn(e,s,g[1].replace(/\\/g,"/"));if(m.length===0){u({kind:"note",tone:"ok",text:"\u2713 All related articles are already published."});return}await sn(m);return}if(g[0]&&g[0]!=="--all"){await ho(g[0]);return}let d=g[0]==="--all";u({kind:"note",tone:"info",text:"Checking what needs publishing\u2026"});try{let m=await Mr({cwd:e,profileName:s}),y=Ft(m.entries),v=Rn(m.entries);if(v>0&&u({kind:"note",tone:"warn",text:`\u26A0 ${v} article${v===1?"":"s"} already exist on Document360 but aren\u2019t linked \u2014 left OUT of publish (publishing would duplicate them). Run /catch-up (or ask me to adopt them) to link them first.`}),y.length===0){u({kind:"note",tone:"ok",text:v>0?"\u2713 Nothing else is ahead of Document360 to publish.":"\u2713 Nothing is ahead of Document360 \u2014 no publish candidates."});let D=m.counts["unknown-base"]??0;D>0&&u({kind:"note",tone:"info",text:`(${D} article(s) have no sync base yet \u2014 publish those by path if needed.)`});return}if(d){await sn(y.map(D=>D.path));return}ct({cursor:0,rows:si(y)})}catch(m){u({kind:"note",tone:"error",text:`Could not compute sync status: ${m.message}`}),u({kind:"note",tone:"info",text:"Publish a specific article: /publish <article-path>"})}return}case"preview":{let d=g.join(" ").trim();if(!d){let y=[];try{y=Object.keys(Lr(e,ce(e,s).name)?.articles??{})}catch{}if(y.length===0){u({kind:"note",tone:"info",text:"No tracked articles to pick from yet. Usage: /preview <path-to.md | article-id>"});return}Be({query:"",cursor:0,paths:y});return}let m=tp(d)?d:Kt(e,d);if(Yt(m)){try{u({kind:"preview",name:zt(m),text:Wa(Or(m,"utf8"))})}catch(y){u({kind:"note",tone:"error",text:`Could not read ${m}: ${y.message}`})}return}if(Lp.test(d)){try{let y=ce(e,s),v={profile:y.name,connection:y.connection},D=y.project.projectId??ap(v),j=await lp(v,D,d);u({kind:"preview",name:j.title??d,text:j.content??"*(article has no content)*"})}catch(y){u({kind:"note",tone:"error",text:`Could not fetch article: ${y.message}`})}return}u({kind:"note",tone:"error",text:`"${d}" is neither a file (relative to ${e}) nor an article id.`});return}case"model":{let d=g[0]?.trim();if(!d){let D=_n(Hr(e));He({cursor:D,current:D});return}let{lines:m,changed:y,effective:v}=Ht(e,d);for(let D of m)u({kind:"note",tone:D.startsWith("\u26A0")?"warn":D.startsWith("\u2713")?"ok":"info",text:D});y&&(Qr(D=>D+1),me.current?.setModel(v));return}case"convert":{if(!Q(e)){u({kind:"note",tone:"error",text:"No d360-writer config here. Run /init first."});return}let{scope:d,run:m}=Bo(g),y=Tp(e,s);if(y.length===0){u({kind:"note",tone:"error",text:"No tracked articles in d360-category-map.json. Publish some first (/publish), then /convert."});return}let v=Go(y,d);if(v.length===0){u({kind:"note",tone:"error",text:`No tracked articles under "${d}". (${y.length} are tracked overall.)`});return}let D=Wr(v),j=3,oe=`/convert${d?` --scope ${d}`:""} --run`,K=Qn(e,"light");if(!m){let x=Oa({files:Vo(e,v),op:"convert",model:K.model}),A=d?`Scope: ${d} (${v.length} of ${y.length} tracked)
256
+ `)});return;case"exit":case"quit":me.current?.close(),r();return;case"clear":de(),nt.current=[],Xt(!1),se(null),Qe.current++,u({kind:"note",tone:"info",text:"Conversation reset (the previous session is still resumable via /resume)."});return;case"login":{let d;try{d=ce(e,s)}catch(m){u({kind:"note",tone:"error",text:m.message});return}u({kind:"note",tone:"info",text:`Profile "${d.name}" \u2192 ${d.connection.name} (${d.connection.apiUrl})${d.production?" \u26A0 PRODUCTION":""}`}),te(!0);try{let m=await op(d.connection,{promptForRedirect:()=>Promise.reject(new Error("Manual login is CLI-only. Run: d360-writer login --manual"))},v=>u({kind:"note",tone:"info",text:v})),y=rp(d.name,m);if(sp(y),await vo(y,d.name,v=>u({kind:"note",tone:"info",text:v})),u({kind:"note",tone:"ok",text:`\u2713 Logged in to "${d.name}" as ${mn(y)}`}),await rs(s),Rt.current){let v=Rt.current;Rt.current=null;let D=Se(e,s);D.ok?(u({kind:"note",tone:"ok",text:`\u2713 Signed into the right project \u2014 press 1 to run ${v}.`}),B([v])):(u({kind:"note",tone:"warn",text:`${D.reason} (then ${v}.)`}),B(["/login"]),Rt.current=v)}else tn.current&&(se(tn.current),tn.current=null,u({kind:"note",tone:"info",text:"Press tab to re-send your last prompt."}))}catch(m){u({kind:"note",tone:"error",text:`Login failed: ${m.message}`})}finally{te(!1),re(m=>m+1)}return}case"allow-prod":{let d=!1;try{d=ce(e,s).production}catch{}if(!d){u({kind:"note",tone:"info",text:"Current profile is not production \u2014 writes are already allowed."});return}ot.current=!0,de(),u({kind:"note",tone:"warn",text:"\u26A0 Production writes authorized for this session."});return}case"rename":{let d=ur(g.join(" ")),m=Pe.current.uuid;if(!m){u({kind:"note",tone:"error",text:"Send a message first \u2014 sessions save once the agent replies."});return}if(!d){u({kind:"note",tone:"info",text:"Thinking of a name\u2026"});let y=Pe.current.firstPrompt??"";_a(y,e).then(v=>{v?(se(`/rename ${v}`),u({kind:"note",tone:"info",text:`Suggestion: "${v}" \u2014 press tab to accept, or type /rename <your name>.`})):u({kind:"note",tone:"info",text:"Usage: /rename <name>"})}).catch(()=>u({kind:"note",tone:"info",text:"Usage: /rename <name>"}));return}gp(m,d),rt(d),Gt(`${Er} d360-writer \xB7 ${d}`),u({kind:"note",tone:"ok",text:`Session renamed to "${d}".`});return}case"profile":{let d=g[0],m=Q(e);if(!d){let y=Object.entries(m?.profiles??{});if(y.length===0){u({kind:"note",tone:"info",text:"No profiles. Run /init first."});return}let v=y.map(([oe,K])=>({name:oe,env:K.connection?.environment??"custom",prod:K.production===!0,who:go(oe)})),D=s??m?.defaultProfile,j=Math.max(0,v.findIndex(oe=>oe.name===D));st({cursor:j,current:j,rows:v});return}if(d==="add"){let y=dr(e,g[1],g[2]);if(y){u({kind:"note",tone:"error",text:y});return}u({kind:"note",tone:"ok",text:`\u2713 Profile "${g[1]}" created (environment: ${g[2]??g[1]}).`}),wo(g[1],!1);return}if(!m?.profiles?.[d]){u({kind:"note",tone:"error",text:`Unknown profile "${d}". Create it: /profile add ${d} <environment>`});return}wo(d,!0);return}case"doctor":await qt(g,{cwd:e});return;case"mcp":await Sn(g);return;case"init":{if(Q(e)){u({kind:"note",tone:"info",text:"This repo is already set up \u2014 edit .d360-writer.json directly (or d360-writer init for the CLI wizard)."});return}let d=$p().map(m=>({name:m,apiUrl:vp(m).apiUrl}));it({cursor:0,rows:d});return}case"resume":{let d=g.join(" ").trim(),m=hp(e).filter(D=>D.uuid!==Pe.current.uuid);if(!d){if(!m.length){u({kind:"note",tone:"info",text:"No saved sessions for this repo yet."});return}Ve({query:"",cursor:0,sessions:m});return}let y=mp(e,d);if(!y){u({kind:"note",tone:"error",text:`No session matches "${d}".`});return}de(y.uuid),Pe.current={uuid:y.uuid,firstPrompt:y.firstPrompt,titleFired:!0},rt(y.name),Ur(y.uuid),nt.current=[];let v=Ar(e,y.uuid);for(let D of v)u(D);u({kind:"note",tone:"ok",text:v.length?`Resumed "${y.name}" \u2014 restored ${v.length} message(s); continue where you left off.`:`Resumed "${y.name}" (agent memory reconnected; no saved transcript to replay).`});return}case"workspace":{let d=g.join(" ").trim(),m;try{m=await It(e,s)}catch(v){u({kind:"note",tone:"error",text:`Could not list workspaces: ${v.message}`});return}if(!d){let v=m.workspaces.map(j=>({id:j.id,name:j.name??j.id,type:j.workspace_type}));if(v.length===0){u({kind:"note",tone:"info",text:"No workspaces in this project."});return}let D=Math.max(0,v.findIndex(j=>j.id===m.current));at({cursor:D,current:D,rows:v,profile:m.profile,projectId:m.projectId,projectName:m.projectName,environment:m.environment});return}let y=xo(m.workspaces,d);if(!y){u({kind:"note",tone:"error",text:`No workspace matches "${d}".`});return}an(m.profile,m.projectId,y);return}case"project":{let d=g.join(" ").trim(),m;try{m=await gt(e,s)}catch(v){u({kind:"note",tone:"error",text:`Could not list projects: ${v.message} (signed in? try /login)`});return}if(!d){let v=m.projects.map(j=>({id:j.id,name:j.name??j.id,sub:j.sub_domain}));if(v.length===0){u({kind:"note",tone:"info",text:"No projects found for this identity."});return}let D=v.findIndex(j=>j.id===m.current);lt({cursor:Math.max(0,D),current:D,rows:v,profile:m.profile,environment:m.environment});return}let y=hs(m.projects,d);if(!y){u({kind:"note",tone:"error",text:`No project matches "${d}". Available: ${m.projects.map(v=>v.name??v.id).join(", ")}`});return}os(m.profile,y);return}case"logout":{let d;try{d=ce(e,s).name}catch(y){u({kind:"note",tone:"error",text:y.message});return}let m=cp(d);up(e,d),de(),re(y=>y+1),u({kind:"note",tone:m?"ok":"info",text:m?`\u2713 Signed out of "${d}" and cleared its project/workspace selection.`:`Profile "${d}" was not signed in. Cleared any project/workspace selection.`}),u({kind:"note",tone:"info",text:"Run /login to sign in and pick a project."}),B(["/login"]);return}case"publish":{if(!co(l.trim()))return;if(g[0]==="--related"&&g[1]){let m=await jn(e,s,g[1].replace(/\\/g,"/"));if(m.length===0){u({kind:"note",tone:"ok",text:"\u2713 All related articles are already published."});return}await sn(m);return}if(g[0]&&g[0]!=="--all"){await ho(g[0]);return}let d=g[0]==="--all";u({kind:"note",tone:"info",text:"Checking what needs publishing\u2026"});try{let m=await Mr({cwd:e,profileName:s}),y=Ft(m.entries),v=Rn(m.entries);if(v>0&&u({kind:"note",tone:"warn",text:`\u26A0 ${v} article${v===1?"":"s"} already exist on Document360 but aren\u2019t linked \u2014 left OUT of publish (publishing would duplicate them). Run /catch-up (or ask me to link them) first.`}),y.length===0){u({kind:"note",tone:"ok",text:v>0?"\u2713 Nothing else is ahead of Document360 to publish.":"\u2713 Nothing is ahead of Document360 \u2014 no publish candidates."});let D=m.counts["unknown-base"]??0;D>0&&u({kind:"note",tone:"info",text:`(${D} article(s) have no sync base yet \u2014 publish those by path if needed.)`});return}if(d){await sn(y.map(D=>D.path));return}ct({cursor:0,rows:si(y)})}catch(m){u({kind:"note",tone:"error",text:`Could not compute sync status: ${m.message}`}),u({kind:"note",tone:"info",text:"Publish a specific article: /publish <article-path>"})}return}case"preview":{let d=g.join(" ").trim();if(!d){let y=[];try{y=Object.keys(Lr(e,ce(e,s).name)?.articles??{})}catch{}if(y.length===0){u({kind:"note",tone:"info",text:"No tracked articles to pick from yet. Usage: /preview <path-to.md | article-id>"});return}Be({query:"",cursor:0,paths:y});return}let m=tp(d)?d:Kt(e,d);if(Yt(m)){try{u({kind:"preview",name:zt(m),text:Wa(Or(m,"utf8"))})}catch(y){u({kind:"note",tone:"error",text:`Could not read ${m}: ${y.message}`})}return}if(Lp.test(d)){try{let y=ce(e,s),v={profile:y.name,connection:y.connection},D=y.project.projectId??ap(v),j=await lp(v,D,d);u({kind:"preview",name:j.title??d,text:j.content??"*(article has no content)*"})}catch(y){u({kind:"note",tone:"error",text:`Could not fetch article: ${y.message}`})}return}u({kind:"note",tone:"error",text:`"${d}" is neither a file (relative to ${e}) nor an article id.`});return}case"model":{let d=g[0]?.trim();if(!d){let D=_n(Hr(e));He({cursor:D,current:D});return}let{lines:m,changed:y,effective:v}=Ht(e,d);for(let D of m)u({kind:"note",tone:D.startsWith("\u26A0")?"warn":D.startsWith("\u2713")?"ok":"info",text:D});y&&(Qr(D=>D+1),me.current?.setModel(v));return}case"convert":{if(!Q(e)){u({kind:"note",tone:"error",text:"No d360-writer config here. Run /init first."});return}let{scope:d,run:m}=Bo(g),y=Tp(e,s);if(y.length===0){u({kind:"note",tone:"error",text:"No tracked articles in d360-category-map.json. Publish some first (/publish), then /convert."});return}let v=Go(y,d);if(v.length===0){u({kind:"note",tone:"error",text:`No tracked articles under "${d}". (${y.length} are tracked overall.)`});return}let D=Wr(v),j=3,oe=`/convert${d?` --scope ${d}`:""} --run`,K=Qn(e,"light");if(!m){let x=Oa({files:Vo(e,v),op:"convert",model:K.model}),A=d?`Scope: ${d} (${v.length} of ${y.length} tracked)
257
257
  `:"",M=`
258
258
  Model: ${K.model}${K.forced?" (forced)":" \u2014 mechanical work; /model to override"}`;u({kind:"note",tone:"info",text:A+zo(D,x,j).join(`
259
259
  `)+M}),B([oe]);return}We.current=Date.now(),Ze(0),O(!0);let _=new AbortController;we.current=_,J({verb:"Converting",total:D.length,done:0,active:[],rows:Vt(D),tools:0,chars:0,lastAt:Date.now(),startedAt:Date.now()}),u({kind:"note",tone:"info",text:`Converting ${v.length} articles across ${D.length} partitions (\u2264${j} agents at once) on ${K.model}\u2026 (esc to stop)`});try{for await(let x of Jn({cwd:e,partitions:D,promptFor:Yo,concurrency:j,profileName:s,allowProdWrites:ot.current,model:K.model,signal:_.signal}))if(x.type==="partition_status")x.status==="running"?J(A=>A&&{...A,active:[...A.active,x.label],rows:Re(A.rows,x.label,"active"),lastAt:Date.now()}):J(A=>A&&{...A,active:A.active.filter(M=>M!==x.label),done:A.done+1,rows:Re(A.rows,x.label,x.status==="done"?"done":"failed"),lastAt:Date.now()});else if(x.type==="partition_event")J(A=>{if(!A)return A;let M={...A,lastAt:Date.now()};return x.event.type==="tool"?M.tools=A.tools+1:x.event.type==="text"&&(M.chars=A.chars+x.event.delta.length),M});else if(x.type==="run_done"){pt(L=>L+x.totalCostUsd),dt(L=>L+x.results.reduce((G,ae)=>G+ae.outputTokens,0));let A=x.aborted?"Stopped. ":"",M=t.kind==="api"?"api":"subscription";u({kind:"note",tone:x.aborted?"warn":x.ok?"ok":"warn",text:A+Ko(x.results,D,M).join(`
@@ -10,9 +10,9 @@ export declare function publishCandidates(entries: SyncEntryState[]): {
10
10
  path: string;
11
11
  label: string;
12
12
  }[];
13
- /** Count of files that look already-published-but-unlinked (adopt candidates), so the /publish flows can
14
- tell the user why they were left out instead of silently dropping them. */
15
- export declare function adoptCandidateCount(entries: SyncEntryState[]): number;
13
+ /** Count of files that already exist on Document360 but aren't linked (link candidates), so the /publish
14
+ flows can tell the user why they were left out instead of silently dropping them. */
15
+ export declare function linkCandidateCount(entries: SyncEntryState[]): number;
16
16
  /** A row in the grouped /publish picker: "all", a category (publishes all its candidates), or one
17
17
  article. `paths` is what gets published when the row is chosen (one for an article, many otherwise). */
18
18
  export type PublishRow = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document360-writer",
3
- "version": "0.4.96",
3
+ "version": "0.4.97",
4
4
  "description": "Standalone documentation agent CLI. Reads your code, writes your docs. Specialized for Document360 publishing.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,7 +34,7 @@
34
34
  "@inquirer/prompts": "^8.4.3",
35
35
  "commander": "^14.0.3",
36
36
  "diff": "^8.0.4",
37
- "document360-engine": "^0.2.50",
37
+ "document360-engine": "^0.2.51",
38
38
  "ink": "^5.2.1",
39
39
  "picocolors": "^1.1.1",
40
40
  "react": "^18.3.1",