@thinkermd/cli 0.1.2 → 0.1.4
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/tk.js +33 -3
- package/package.json +6 -2
package/dist/tk.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as
|
|
2
|
+
import{Command as Ro}from"commander";import ke from"chalk";import{join as it}from"path";import{writeFile as Yt}from"fs/promises";import k from"chalk";import je from"ora";import{homedir as Mt}from"os";import{join as G}from"path";import{readFile as Lt,writeFile as Ut,mkdir as be}from"fs/promises";import{existsSync as Bt}from"fs";var y=".thinker.md",A="config.json",tt="structure.json",O="CONTEXT.md",bt="contexts",St="thinker.md.tar.gz",$t="metadata.json",vt="latest",et="repos",ot="branches";var It="thinkermd-meta-repos",_t="1.0",mt="full",Nt="incremental",At=["node_modules",".git","dist","build",".next",".nuxt","coverage","__pycache__",".pytest_cache","venv",".venv","vendor","target",y],Ot=3,Ft=1e3,Dt="[Describe the purpose",jt=100;var K=G(Mt(),".thinkermd","config.json"),J="https://app.thinkermd.dev";function ut(){return process.env.META_REPO_ID&&process.env.JOB_TOKEN?"sandbox":"local"}function Se(){return{mode:"sandbox",metaRepoId:process.env.META_REPO_ID,branch:process.env.BRANCH,commitSha:process.env.COMMIT_SHA,previousCommitSha:process.env.PREVIOUS_COMMIT_SHA||void 0,jobId:process.env.JOB_ID,callbackUrl:process.env.CALLBACK_URL,jobToken:process.env.JOB_TOKEN,r2Endpoint:process.env.R2_ENDPOINT,r2AccessKeyId:process.env.R2_ACCESS_KEY_ID,r2SecretAccessKey:process.env.R2_SECRET_ACCESS_KEY,r2Bucket:process.env.R2_BUCKET}}async function v(){try{if(Bt(K)){let e=await Lt(K,"utf-8");return JSON.parse(e)}}catch{}return{}}async function gt(e){let t=G(Mt(),".thinkermd");await be(t,{recursive:!0,mode:448}),await Ut(K,JSON.stringify(e,null,2),{mode:384})}async function S(){return ut()==="sandbox"?Se():{mode:"local",...await v()}}var z={version:_t,ignore:[...At]};async function F(e){let t=G(e,y,A);try{if(Bt(t)){let o=await Lt(t,"utf-8");return JSON.parse(o)}}catch{}return null}async function rt(e,t){let o=G(e,y,A);await Ut(o,JSON.stringify(t,null,2))}function Kt(){let t=["META_REPO_ID","BRANCH","COMMIT_SHA","JOB_ID","CALLBACK_URL","JOB_TOKEN","R2_ENDPOINT","R2_ACCESS_KEY_ID","R2_SECRET_ACCESS_KEY","R2_BUCKET"].filter(o=>!process.env[o]);if(t.length>0)throw new Error(`Missing required environment variables: ${t.join(", ")}`)}function I(e){return G(e,y)}function q(e){return G(e,y,bt)}import{execFile as $e}from"child_process";import{promisify as ve}from"util";var Ie=ve($e);async function H(e,t="."){let{stdout:o}=await Ie("git",e,{cwd:t});return o.trim()}async function $(e="."){try{return await H(["branch","--show-current"],e)}catch(t){throw new Error(`Failed to get current branch: ${t}`)}}async function N(e="."){try{return await H(["rev-parse","HEAD"],e)}catch(t){throw new Error(`Failed to get current commit: ${t}`)}}async function T(e="."){try{return await H(["rev-parse","--show-toplevel"],e)}catch(t){throw new Error(`Not a git repository: ${t}`)}}async function _(e="."){try{return await H(["rev-parse","--git-dir"],e),!0}catch{return!1}}async function Gt(e=".",t){try{return await H(["log","-1","--format=%s",t||"HEAD"],e)}catch(o){throw new Error(`Failed to get commit message: ${o}`)}}async function ft(e="."){try{return await H(["remote","get-url","origin"],e)}catch{return null}}async function X(e="."){let t=await ft(e);if(!t)return null;let o=t.match(/[/:]([\w-]+)\/([\w.-]+?)(\.git)?$/);return o?`${o[1]}/${o[2]}`:null}import{join as L,relative as _e,dirname as Ne}from"path";import{readFile as Ae}from"fs/promises";import{existsSync as Jt}from"fs";async function Oe(){let e=await import("ignore");return(e.default??e)()}var Fe=[".git",".thinker.md","node_modules","__pycache__",".pytest_cache","venv",".venv",".next",".nuxt","dist","build","coverage","target",".idea",".vscode","*.log","*.lock","package-lock.json","yarn.lock","bun.lockb","pnpm-lock.yaml","Cargo.lock","poetry.lock","Gemfile.lock","*.pyc","*.pyo","*.class","*.o","*.so","*.dylib","*.dll","*.exe"];async function De(e){let t=await Oe();t.add(Fe),t.add(z.ignore||[]);try{let o=L(e,".gitignore");if(Jt(o)){let r=await Ae(o,"utf-8");t.add(r)}}catch{}return t}async function nt(e){let t=await De(e),o=new Set,r={total:0};await Ht(e,e,t,o,r);let n=Array.from(o).sort();return{version:"1.0",generatedAt:new Date().toISOString(),directories:n,directoryCount:n.length,fileCount:r.total}}async function Ht(e,t,o,r,n){let{readdir:i,stat:s}=await import("fs/promises"),a;try{a=await i(t)}catch{return}for(let c of a){let d=L(t,c),l=_e(e,d);if(!(o.ignores(l)||o.ignores(l+"/")))try{let u=await s(d);u.isDirectory()?(r.add(l),await Ht(e,d,o,r,n)):u.isFile()&&n.total++}catch{}}}async function Xt(e){let t=L(e,".thinker.md","contexts"),o=[];return Jt(L(e,".thinker.md","CONTEXT.md"))&&o.push(""),await Vt(t,"",o),o}async function Vt(e,t,o){let{readdir:r,stat:n}=await import("fs/promises"),i=t?L(e,t):e,s;try{s=await r(i)}catch{return}for(let a of s){let c=L(i,a),d=t?L(t,a):a;try{(await n(c)).isDirectory()?await Vt(e,d,o):a==="CONTEXT.md"&&o.push(Ne(d))}catch{}}}async function f(e,t,o){let r=await S();if(r.mode!=="sandbox"||!r.callbackUrl||!r.jobToken)return;let n=r.callbackUrl.replace("/callback","/progress"),i={type:"progress",status:"processing",step:e,progress:t,message:o};try{let s=await fetch(n,{method:"POST",headers:{Authorization:`Bearer ${r.jobToken}`,"Content-Type":"application/json"},body:JSON.stringify(i)});s.ok||console.warn(`[Progress] Update failed: ${s.status} ${s.statusText}`)}catch(s){console.warn("[Progress] Update error:",s)}}async function zt(e,t){let o=await S();if(o.mode!=="sandbox"||!o.callbackUrl||!o.jobToken)return;let r={jobId:o.jobId,status:"completed",artifactUrl:e,stats:t};try{let n=await fetch(o.callbackUrl,{method:"POST",headers:{Authorization:`Bearer ${o.jobToken}`,"Content-Type":"application/json"},body:JSON.stringify(r)});if(!n.ok)throw console.error(`[Callback] Completion failed: ${n.status} ${n.statusText}`),new Error(`Callback failed: ${n.status}`);console.log("[Callback] Job completed successfully")}catch(n){throw console.error("[Callback] Completion error:",n),n}}async function qt(e,t){let r=`# ${t||"Project Root"}
|
|
3
3
|
|
|
4
4
|
<!--
|
|
5
5
|
This context file should describe:
|
|
@@ -24,5 +24,35 @@ This context file should describe:
|
|
|
24
24
|
## Dependencies
|
|
25
25
|
|
|
26
26
|
[Note any important dependencies or relationships]
|
|
27
|
-
`;await
|
|
28
|
-
This appears to be the first generation. Use 'tk init' to create initial structure.`),process.exit(0)),t.succeed(`Found version: ${a.slice(0,7)}`)}await m("pull",30,`Found version: ${a.slice(0,7)}`),t.start("Downloading thinker.md.tar.gz...");let d=await Kt(i,s,a);d||(t.fail("Archive not found"),console.error(N.red(`No archive found for commit ${a.slice(0,7)}`)),process.exit(1)),t.succeed(`Downloaded archive: ${d.length} bytes`),await m("pull",60,"Extracting archive...");let g=Gt(r,".thinker.md.tar.gz");await Ro(g,d),t.start("Extracting to .thinker.md/...");let l=P(r),{rm:p}=await import("fs/promises");try{await p(l,{recursive:!0,force:!0})}catch{}await Vt.extract({file:g,cwd:r}),await p(g,{force:!0}),t.succeed("Extracted .thinker.md/"),await m("pull",80,"Updating config...");let h=await I(r);h&&(h.pulledFrom=a,h.pulledAt=new Date().toISOString(),await tt(r,h));let w=await So(l);await m("pull",100,`Pulled ${w} context files`),console.log(),console.log(N.green(`\u2713 Pulled ${w} context files`)),console.log(),console.log(`From: ${N.cyan(`${s}@${a.slice(0,7)}`)}`),console.log(),console.log("Next steps:"),console.log(" 1. Review and update context files as needed"),console.log(" 2. Run "+N.cyan("tk validate")+" to check structure"),console.log(" 3. Run "+N.cyan("tk push")+" to upload changes")}catch(r){t.fail("Pull failed"),console.error(N.red(`Error: ${r}`)),process.exit(1)}}import{join as zt}from"path";import T from"chalk";import bo from"ora";import*as Ht from"tar";async function vo(o){let{readdir:t,stat:e}=await import("fs/promises"),r=0,n=0;async function i(s){let a;try{a=await t(s)}catch{return}for(let d of a){let g=zt(s,d);try{(await e(g)).isDirectory()?await i(g):(n++,d===_&&r++)}catch{}}}return await i(o),{contextFiles:r,totalFiles:n}}async function Yt(o){let t=bo(),e=process.cwd();try{if(it()==="sandbox")try{bt()}catch(E){console.error(T.red(`Sandbox environment error: ${E}`)),process.exit(1)}await b(e)||(console.error(T.red("Error: Not a git repository")),process.exit(1));let r=await S(e),n=await $(),i=P(r),{stat:s,rm:a}=await import("fs/promises");try{await s(i)}catch{console.error(T.red(`Error: ${f}/ not found`)),console.log("Run 'tk init' first to create the structure."),process.exit(1)}if(await m("push",0,"Validating structure..."),!o.skipValidation){t.start(`Validating ${f}/ structure...`);let E=await ct(r);if(!E.valid){t.fail("Validation failed"),console.log(),console.log(T.red("Fix the following errors before pushing:"));for(let Y of E.errors)console.log(T.red(` \u2022 ${Y.path}: ${Y.message}`));process.exit(1)}t.succeed("Validation passed"),E.warnings.length>0&&console.log(T.yellow(` ${E.warnings.length} warning(s)`))}await m("push",20,"Validation passed");let d;if(n.mode==="sandbox"&&n.metaRepoId)d=n.metaRepoId;else{let E=await I(r);E?.projectId?d=E.projectId:(console.error(T.red("Error: No project ID found. Set META_REPO_ID environment variable.")),process.exit(1))}let g=n.branch||await A(r),l=n.commitSha||await F(r),p=await vt(r,l);await m("push",30,"Packaging .thinker.md/..."),t.start(`Packaging ${f}/...`);let h=zt(r,`${f}.tar.gz`);await Ht.create({file:h,cwd:r,gzip:!0},[f]);let{readFile:w}=await import("fs/promises"),c=await w(h);await a(h,{force:!0}),t.succeed(`Packaged archive: ${c.length} bytes`),await m("push",60,"Uploading to storage...");let x=await vo(i),L=await I(r),C=o.forceFull?nt:L?.pulledFrom?wt:nt,gt={branch:g,commitSha:l,commitMessage:p,mode:C,parentCommitSha:L?.pulledFrom,generatedAt:new Date().toISOString(),jobId:n.jobId||`local-${Date.now()}`,tokensUsed:0,filesAnalyzed:x.totalFiles,contextFilesGenerated:x.contextFiles};t.start("Uploading to storage...");try{let E=await Jt(d,g,l,c,gt);if(t.succeed("Uploaded to storage"),await m("push",90,"Finalizing..."),n.mode==="sandbox"&&n.callbackUrl){let Y={filesAnalyzed:x.totalFiles,contextFilesGenerated:x.contextFiles,tokensUsed:0,mode:C};await Ot(E,Y)}await m("push",100,"Done!"),console.log(),console.log(T.green("\u2713 Successfully pushed to storage")),console.log(),console.log(`Branch: ${T.cyan(g)}`),console.log(`Commit: ${T.cyan(l.slice(0,7))}`),console.log(`Mode: ${T.cyan(C)}`),console.log(`Context files: ${T.cyan(x.contextFiles)}`),n.mode==="sandbox"&&(console.log(),console.log(T.green("\u2713 Job completed")))}catch(E){t.fail("Upload failed"),E instanceof Error&&E.message.includes("R2 credentials")?(console.error(T.red(E.message)),console.log(),console.log("Required environment variables:"),console.log(" R2_ENDPOINT"),console.log(" R2_ACCESS_KEY_ID"),console.log(" R2_SECRET_ACCESS_KEY"),console.log(" R2_BUCKET")):console.error(T.red(`Error: ${E}`)),process.exit(1)}}catch(r){t.fail("Push failed"),console.error(T.red(`Error: ${r}`)),process.exit(1)}}import O from"chalk";import qt from"ora";async function _o(o,t){try{let e=await fetch(`${o}/api/auth/validate`,{method:"GET",headers:{Authorization:`Bearer ${t}`}});return e.ok?{valid:!0,userId:(await e.json()).userId}:e.status===401?{valid:!1,error:"Invalid or expired token"}:{valid:!1,error:`API error: ${e.status} ${e.statusText}`}}catch(e){return{valid:!1,error:`Connection failed: ${e}`}}}async function Wt(o){let t=qt(),e=o.apiUrl||Z;if(o.token){t.start("Validating token...");let n=await _o(e,o.token);n.valid||(t.fail("Authentication failed"),console.error(O.red(`Error: ${n.error}`)),process.exit(1));let i={apiToken:o.token,apiUrl:e,userId:n.userId};await st(i),t.succeed("Authenticated successfully"),console.log(),console.log(`Config saved to: ${O.cyan(M)}`);return}console.log(O.bold("ThinkerMD Login")),console.log(),console.log("To authenticate, you need an API token from ThinkerMD."),console.log(),console.log("Get your token at: "+O.cyan(`${e}/settings/tokens`)),console.log(),console.log("Then run:"),console.log(O.cyan(" tk login --token YOUR_TOKEN")),console.log();let r=await J();r.apiToken&&(console.log(O.green("You are already logged in.")),console.log(`User ID: ${O.cyan(r.userId||"unknown")}`),console.log(),console.log("To re-authenticate, run:"),console.log(O.cyan(" tk login --token NEW_TOKEN")))}async function Qt(){let o=qt();o.start("Logging out..."),await st({}),o.succeed("Logged out successfully"),console.log(),console.log(`Config cleared from: ${O.cyan(M)}`)}import{join as Zt}from"path";import u from"chalk";import Io from"ora";async function Ao(o){let{readFile:t}=await import("fs/promises"),{existsSync:e}=await import("fs"),r=Zt(P(o),"structure.json");try{if(e(r)){let n=await t(r,"utf-8");return JSON.parse(n)}}catch{}return null}async function No(o){let{readdir:t,stat:e}=await import("fs/promises"),r=0;async function n(i){let s;try{s=await t(i)}catch{return}for(let a of s){let d=Zt(i,a);try{(await e(d)).isDirectory()?await n(d):a==="CONTEXT.md"&&r++}catch{}}}return await n(o),r}async function to(){let o=Io(),t=process.cwd();try{await b(t)||(console.error(u.red("Error: Not a git repository")),process.exit(1));let e=await S(t),r=await $(),n=P(e);console.log(u.bold("ThinkerMD Status")),console.log();let i=await K(e),s=r.branch||await A(e),a=r.commitSha||await F(e);console.log(u.dim("Repository:")),console.log(` Name: ${u.cyan(i||"unknown")}`),console.log(` Branch: ${u.cyan(s)}`),console.log(` Commit: ${u.cyan(a.slice(0,7))}`),console.log();let{stat:d}=await import("fs/promises"),g=!1;try{await d(n),g=!0}catch{}if(!g){console.log(u.dim("Local Context:")),console.log(` Status: ${u.yellow("Not initialized")}`),console.log(),console.log("Run "+u.cyan("tk init")+" to create .thinker.md/");return}console.log(u.dim("Local Context:"));let l=await I(e),p=await Ao(e),h=await No(n);if(console.log(` Status: ${u.green("Initialized")}`),console.log(` Project ID: ${u.cyan(l?.projectId||"unknown")}`),console.log(` Context files: ${u.cyan(h)}`),p&&(console.log(` Directories: ${u.cyan(p.directoryCount)}`),console.log(` Files tracked: ${u.cyan(p.fileCount)}`)),l?.generatedAt){let c=new Date(l.generatedAt);console.log(` Generated: ${u.cyan(c.toLocaleString())}`)}l?.pulledFrom&&console.log(` Pulled from: ${u.cyan(l.pulledFrom.slice(0,7))}`),console.log();let w=r.metaRepoId||l?.projectId;if(w&&r.r2Endpoint){console.log(u.dim("Remote Storage:")),o.start("Checking remote...");try{let c=await rt(w,s);c?(o.stop(),console.log(` Latest commit: ${u.cyan(c.slice(0,7))}`),c===a?console.log(` Status: ${u.green("Up to date")}`):l?.pulledFrom===c?console.log(` Status: ${u.yellow("Local changes not pushed")}`):(console.log(` Status: ${u.yellow("Remote has newer version")}`),console.log(),console.log("Run "+u.cyan("tk pull")+" to download latest context"))):(o.stop(),console.log(` Status: ${u.yellow("No remote context found")}`),console.log(),console.log("Run "+u.cyan("tk push")+" to upload context"))}catch{o.stop(),console.log(` Status: ${u.dim("Unable to check (no credentials)")}`)}}console.log(),console.log(u.dim("Mode:")),console.log(` ${r.mode==="sandbox"?u.cyan("Sandbox"):u.cyan("Local")}`),r.mode==="sandbox"&&console.log(` Job ID: ${u.cyan(r.jobId||"unknown")}`)}catch(e){o.fail("Failed to get status"),console.error(u.red(`Error: ${e}`)),process.exit(1)}}import y from"chalk";import Oo from"ora";async function oo(o){let t=Oo(),e=process.cwd();try{await b(e)||(console.error(y.red("Error: Not a git repository")),process.exit(1));let r=await S(e);if((await $()).mode==="sandbox"){console.error(y.yellow("Note: In sandbox mode, generation is triggered automatically.")),console.log("Use 'tk init' to create structure and 'tk push' when done.");return}let i=await J();i.apiToken||(console.error(y.red("Error: Not logged in")),console.log(),console.log("Please authenticate first:"),console.log(y.cyan(" tk login --token YOUR_TOKEN")),process.exit(1));let s=i.apiUrl||Z,a=await K(r),d=o.branch||await A(r),g=await F(r);a||(console.error(y.red("Error: Unable to determine repository name")),console.log("Make sure this repository has a remote origin configured."),process.exit(1)),console.log(y.bold("Trigger Context Generation")),console.log(),console.log(`Repository: ${y.cyan(a)}`),console.log(`Branch: ${y.cyan(d)}`),console.log(`Commit: ${y.cyan(g.slice(0,7))}`),console.log(),t.start("Looking up meta-repository...");let l=await fetch(`${s}/api/meta-repositories/lookup?repo=${encodeURIComponent(a)}`,{headers:{Authorization:`Bearer ${i.apiToken}`}});l.ok||(l.status===404&&(t.fail("Meta-repository not found"),console.log(),console.log("This repository is not connected to ThinkerMD. Please add it at:"),console.log(y.cyan(`${s}/dashboard`)),process.exit(1)),t.fail("API error"),console.error(y.red(`Error: ${l.status} ${l.statusText}`)),process.exit(1));let p=await l.json();t.succeed(`Found meta-repository: ${p.id}`),t.start("Triggering generation job...");let h=await fetch(`${s}/api/jobs/generate`,{method:"POST",headers:{Authorization:`Bearer ${i.apiToken}`,"Content-Type":"application/json"},body:JSON.stringify({metaRepositoryId:p.id,commitSha:g,branch:d,priority:"normal"})});if(!h.ok){t.fail("Failed to trigger generation");let c=await h.json().catch(()=>({}));console.error(y.red(`Error: ${c.error||h.statusText}`)),process.exit(1)}let w=await h.json();if(t.succeed("Generation job queued"),console.log(),console.log(`Job ID: ${y.cyan(w.jobId)}`),console.log(`Status: ${y.cyan(w.status)}`),w.estimatedPosition&&console.log(`Queue position: ${y.cyan(w.estimatedPosition)}`),console.log(),console.log("Track progress at:"),console.log(y.cyan(`${s}/dashboard/repositories/${p.id}/jobs/${w.jobId}`)),o.wait){console.log(),t.start("Waiting for completion...");let c=!1,x="";for(;!c;){await new Promise(C=>setTimeout(C,5e3));let L=await fetch(`${s}/api/jobs/${w.jobId}`,{headers:{Authorization:`Bearer ${i.apiToken}`}});if(L.ok){let C=await L.json();C.status!==x&&(x=C.status,t.text=`Status: ${C.status} (${C.progress||0}%)`),(C.status==="completed"||C.status==="failed")&&(c=!0,C.status==="completed"?(t.succeed("Generation completed!"),console.log(),console.log("Run "+y.cyan("tk pull")+" to download the generated context")):(t.fail("Generation failed"),C.errorMessage&&console.error(y.red(`Error: ${C.errorMessage}`))))}}}}catch(r){t.fail("Failed to trigger generation"),console.error(y.red(`Error: ${r}`)),process.exit(1)}}var R=new Fo;R.name("tk").description("ThinkerMD CLI - Manage .thinker.md/ context files").version("0.1.0");R.command("init").description("Create .thinker.md/ structure with empty CONTEXT.md files").option("-f, --force","Reinitialize even if .thinker.md/ exists").option("-m, --merge","Only add missing CONTEXT.md files, keep existing ones").action(o=>jt(o));R.command("validate").description("Verify .thinker.md/ structure matches repository").option("-q, --quiet","Only output errors, no summary").action(o=>Lt(o));R.command("pull").description("Download previous context from storage").option("-b, --branch <branch>","Specify branch to pull from").option("-c, --commit <sha>","Pull specific commit version").action(o=>Xt(o));R.command("push").description("Validate and upload .thinker.md/ to storage").option("--skip-validation","Skip validation before push").option("--force-full","Force full generation mode (ignore previous context)").action(o=>Yt(o));R.command("login").description("Authenticate with ThinkerMD API").option("-t, --token <token>","API token").option("--api-url <url>","Custom API URL").action(o=>Wt(o));R.command("logout").description("Remove saved authentication").action(()=>Qt());R.command("status").description("Show status of .thinker.md/ context").action(()=>to());R.command("generate").description("Trigger context generation via API").option("-b, --branch <branch>","Specify branch to generate for").option("-w, --wait","Wait for generation to complete").action(o=>oo(o));R.on("command:*",()=>{console.error(eo.red(`Unknown command: ${R.args.join(" ")}`)),console.log(),R.help()});async function ro(){try{await R.parseAsync(process.argv)}catch(o){o instanceof Error&&console.error(eo.red(`Error: ${o.message}`)),process.exit(1)}}ro().catch(o=>{console.error("Fatal error:",o),process.exit(1)});
|
|
27
|
+
`;await Yt(e,r)}async function Me(e,t,o=!1){let{existsSync:r}=await import("fs"),n=q(e),i=0,s=0,a=it(I(e),"CONTEXT.md");o&&r(a)?s++:(await qt(a,""),i++);for(let c of t){let d=it(n,c),l=it(d,"CONTEXT.md"),{mkdir:u}=await import("fs/promises");if(await u(d,{recursive:!0}),o&&r(l)){s++;continue}await qt(l,c),i++}return{created:i,skipped:s}}async function Wt(e){let t=je(),o=process.cwd();try{await _(o)||(console.error(k.red("Error: Not a git repository")),console.log("Please run this command from the root of a git repository."),process.exit(1));let r=await T(o),n=I(r),{stat:i}=await import("fs/promises");try{await i(n),!e.force&&!e.merge&&(console.error(k.yellow("Warning: .thinker.md/ already exists")),console.log("Use --force to reinitialize or --merge to add missing contexts"),process.exit(1)),e.merge?console.log(k.cyan("Merging: adding missing CONTEXT.md files...")):console.log(k.yellow("Reinitializing .thinker.md/..."))}catch{}await f("init",0,"Scanning repository structure..."),t.start("Scanning repository structure...");let s=await nt(r);t.succeed(`Found ${s.directoryCount} directories, ${s.fileCount} files`),await f("init",30,`Found ${s.directoryCount} directories`);let a=await $(r),c=await N(r),d=await X(r)||"unknown";t.start("Creating .thinker.md/ structure...");let{mkdir:l}=await import("fs/promises");await l(n,{recursive:!0}),await l(q(r),{recursive:!0});let u=await S(),g={...z,version:"1.0",projectId:u.metaRepoId||`local-${Date.now()}`,name:d,ignore:z.ignore||[],generatedAt:new Date().toISOString(),generatedBy:"tk-cli"};await rt(r,g),t.succeed("Created config.json"),await f("init",50,"Created config.json");let h={...s,commitSha:c,branch:a};await Yt(it(n,"structure.json"),JSON.stringify(h,null,2)),t.succeed("Created structure.json"),await f("init",60,"Created structure.json"),t.start(e.merge?"Adding missing CONTEXT.md files...":"Creating empty CONTEXT.md files...");let{created:p,skipped:C}=await Me(r,s.directories,e.merge);e.merge?(t.succeed(`Added ${p} new CONTEXT.md files (${C} existing skipped)`),await f("init",100,`Added ${p} new CONTEXT.md files`)):(t.succeed(`Created ${p} empty CONTEXT.md files`),await f("init",100,`Created ${p} CONTEXT.md files`)),console.log(),e.merge?(console.log(k.green("\u2713 Merged .thinker.md/")),console.log(),console.log(`Added: ${k.cyan(`${p}`)} new CONTEXT.md files`),console.log(`Skipped: ${k.gray(`${C}`)} existing files`)):(console.log(k.green("\u2713 Initialized .thinker.md/")),console.log(),console.log("Created:"),console.log(` ${k.cyan("config.json")} - Project configuration`),console.log(` ${k.cyan("structure.json")} - Directory structure`),console.log(` ${k.cyan(`${p} CONTEXT.md`)} files - Ready for documentation`)),console.log(),console.log("Next steps:"),console.log(" 1. Fill in the CONTEXT.md files with documentation"),console.log(" 2. Run "+k.cyan("tk validate")+" to check structure"),console.log(" 3. Run "+k.cyan("tk push")+" to upload to storage")}catch(r){t.fail("Initialization failed"),console.error(k.red(`Error: ${r}`)),process.exit(1)}}import{join as ht}from"path";import M from"chalk";import Le from"ora";async function yt(e){let t=[],o=[],r=0,n=I(e),i=q(e),{stat:s,readFile:a}=await import("fs/promises");try{await s(n)}catch{return t.push({type:"structure_mismatch",path:y,message:`${y}/ directory not found. Run 'tk init' first.`}),{valid:!1,errors:t,warnings:o,validContextCount:0,directoryCount:0}}let c=await F(e);c?(c.version||t.push({type:"invalid_config",path:`${y}/${A}`,message:`${A} missing required field: version`}),c.projectId||t.push({type:"invalid_config",path:`${y}/${A}`,message:`${A} missing required field: projectId`})):t.push({type:"invalid_config",path:`${y}/${A}`,message:`${A} not found or invalid`});let d=null;try{let p=ht(n,tt),C=await a(p,"utf-8");d=JSON.parse(C)}catch{t.push({type:"structure_mismatch",path:`${y}/${tt}`,message:`${tt} not found or invalid`})}let l=await nt(e),u=ht(n,O);try{await s(u),r++}catch{t.push({type:"missing_context",path:O,message:`Root ${O} not found`})}for(let p of l.directories){let C=ht(i,p,O);try{if((await s(C)).isFile()){r++;let x=await a(C,"utf-8");(x.trim().length<jt||x.includes(Dt))&&o.push({type:"empty_context",path:`${p}/${O}`,message:"Context file appears to be empty or contains only template"})}}catch{t.push({type:"missing_context",path:`${p}/${O}`,message:`Missing context file for directory: ${p}`})}}let g=await Xt(e),h=new Set(l.directories);for(let p of g)p===""||p==="."||h.has(p)||o.push({type:"orphan_context",path:`${p}/${O}`,message:`Context exists for non-existent directory: ${p}`});return{valid:t.length===0,errors:t,warnings:o,validContextCount:r,directoryCount:l.directoryCount}}async function Qt(e){let t=Le(),o=process.cwd();try{await _(o)||(console.error(M.red("Error: Not a git repository")),process.exit(1));let r=await T(o);await f("validate",0,"Validating structure..."),t.start(`Validating ${y}/ structure...`);let n=await yt(r);if(t.stop(),await f("validate",100,"Validation complete"),!e.quiet){if(console.log(),n.errors.length>0){console.log(M.red(`\u2717 ${n.errors.length} error(s):`));for(let i of n.errors)console.log(M.red(` \u2022 ${i.path}: ${i.message}`));console.log()}if(n.warnings.length>0){console.log(M.yellow(`\u26A0 ${n.warnings.length} warning(s):`));for(let i of n.warnings)console.log(M.yellow(` \u2022 ${i.path}: ${i.message}`));console.log()}n.valid?console.log(M.green(`\u2713 Validation passed: ${n.validContextCount} contexts valid`)):console.log(M.red(`\u2717 Validation failed: ${n.errors.length} error(s), ${n.warnings.length} warning(s)`))}n.valid||process.exit(1)}catch(r){t.fail("Validation failed"),console.error(M.red(`Error: ${r}`)),process.exit(1)}}import{join as ne}from"path";import{writeFile as He}from"fs/promises";import D from"chalk";import Xe from"ora";import*as ie from"tar";import{S3Client as Ue,PutObjectCommand as xt,GetObjectCommand as Zt}from"@aws-sdk/client-s3";var wt=null;function Y(){if(!wt){let e=process.env.R2_ENDPOINT,t=process.env.R2_ACCESS_KEY_ID,o=process.env.R2_SECRET_ACCESS_KEY;if(!e||!t||!o)throw new Error("R2 credentials not configured. Required: R2_ENDPOINT, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY");wt=new Ue({region:"auto",endpoint:e,credentials:{accessKeyId:t,secretAccessKey:o}})}return wt}function W(){return process.env.R2_BUCKET||It}async function Q(e,t,o=Ot,r=Ft){let n;for(let i=1;i<=o;i++)try{return await e()}catch(s){if(n=s instanceof Error?s:new Error(String(s)),n.name==="NoSuchKey"||n.message.includes("credentials")||n.message.includes("AccessDenied"))throw n;if(i<o){let a=r*Math.pow(2,i-1);console.log(`[Storage] ${t} failed (attempt ${i}/${o}), retrying in ${a}ms...`),await new Promise(c=>setTimeout(c,a))}}throw n}function te(e,t,o){return`${et}/${e}/${ot}/${t}/${o}/${St}`}function Be(e,t,o){return`${et}/${e}/${ot}/${t}/${o}/${$t}`}function ee(e,t){return`${et}/${e}/${ot}/${t}/${vt}`}async function Ke(e,t,o,r){let n=Y(),i=te(e,t,o);return await Q(()=>n.send(new xt({Bucket:W(),Key:i,Body:r,ContentType:"application/gzip"})),"uploadArchive"),console.log(`[Storage] Uploaded archive to ${i}`),i}async function Ge(e,t,o,r){let n=Y(),i=Be(e,t,o);await Q(()=>n.send(new xt({Bucket:W(),Key:i,Body:JSON.stringify(r,null,2),ContentType:"application/json"})),"uploadMetadata"),console.log(`[Storage] Uploaded metadata to ${i}`)}async function Je(e,t,o){let r=Y(),n=ee(e,t);await Q(()=>r.send(new xt({Bucket:W(),Key:n,Body:o,ContentType:"text/plain"})),"updateLatestPointer"),console.log(`[Storage] Updated latest pointer for ${t} -> ${o}`)}async function st(e,t){let o=Y(),r=ee(e,t);try{return(await(await Q(()=>o.send(new Zt({Bucket:W(),Key:r})),"getLatestCommitSha")).Body?.transformToString())?.trim()||null}catch(n){if(n instanceof Error&&"name"in n&&n.name==="NoSuchKey")return null;throw n}}async function oe(e,t,o){let r=Y(),n=te(e,t,o);try{let s=await(await Q(()=>r.send(new Zt({Bucket:W(),Key:n})),"downloadArchive")).Body?.transformToByteArray();return s?Buffer.from(s):null}catch(i){if(i instanceof Error&&"name"in i&&i.name==="NoSuchKey")return null;throw i}}async function re(e,t,o,r,n){let i=await Ke(e,t,o,r);return await Ge(e,t,o,n),await Je(e,t,o),i}async function Ve(e){let{readdir:t,stat:o}=await import("fs/promises"),r=0;async function n(i){let s;try{s=await t(i)}catch{return}for(let a of s){let c=ne(i,a);try{(await o(c)).isDirectory()?await n(c):a==="CONTEXT.md"&&r++}catch{}}}return await n(e),r}async function se(e){let t=Xe(),o=process.cwd();try{await _(o)||(console.error(D.red("Error: Not a git repository")),process.exit(1));let r=await T(o),n=await S(),i;if(n.mode==="sandbox"&&n.metaRepoId)i=n.metaRepoId;else{let p=await F(r);p?.projectId?i=p.projectId:(console.error(D.red("Error: No project ID found. Run 'tk init' first or set META_REPO_ID.")),process.exit(1))}let s=e.branch||n.branch||await $(r);await f("pull",0,`Fetching latest context for branch '${s}'...`),console.log(`Fetching latest context for branch '${D.cyan(s)}'...`),t.start("Looking up latest version...");let a;if(e.commit)a=e.commit,t.succeed(`Using specified commit: ${a.slice(0,7)}`);else{try{a=await st(i,s)}catch(p){throw p instanceof Error&&p.message.includes("R2 credentials")&&(t.fail("R2 credentials not configured"),console.error(D.red(p.message)),process.exit(1)),p}a||(t.info("No previous context found for this branch"),console.log(`
|
|
28
|
+
This appears to be the first generation. Use 'tk init' to create initial structure.`),process.exit(0)),t.succeed(`Found version: ${a.slice(0,7)}`)}await f("pull",30,`Found version: ${a.slice(0,7)}`),t.start("Downloading thinker.md.tar.gz...");let c=await oe(i,s,a);c||(t.fail("Archive not found"),console.error(D.red(`No archive found for commit ${a.slice(0,7)}`)),process.exit(1)),t.succeed(`Downloaded archive: ${c.length} bytes`),await f("pull",60,"Extracting archive...");let d=ne(r,".thinker.md.tar.gz");await He(d,c),t.start("Extracting to .thinker.md/...");let l=I(r),{rm:u}=await import("fs/promises");try{await u(l,{recursive:!0,force:!0})}catch{}await ie.extract({file:d,cwd:r}),await u(d,{force:!0}),t.succeed("Extracted .thinker.md/"),await f("pull",80,"Updating config...");let g=await F(r);g&&(g.pulledFrom=a,g.pulledAt=new Date().toISOString(),await rt(r,g));let h=await Ve(l);await f("pull",100,`Pulled ${h} context files`),console.log(),console.log(D.green(`\u2713 Pulled ${h} context files`)),console.log(),console.log(`From: ${D.cyan(`${s}@${a.slice(0,7)}`)}`),console.log(),console.log("Next steps:"),console.log(" 1. Review and update context files as needed"),console.log(" 2. Run "+D.cyan("tk validate")+" to check structure"),console.log(" 3. Run "+D.cyan("tk push")+" to upload changes")}catch(r){t.fail("Pull failed"),console.error(D.red(`Error: ${r}`)),process.exit(1)}}import{join as ae}from"path";import R from"chalk";import ze from"ora";import*as ce from"tar";async function qe(e){let{readdir:t,stat:o}=await import("fs/promises"),r=0,n=0;async function i(s){let a;try{a=await t(s)}catch{return}for(let c of a){let d=ae(s,c);try{(await o(d)).isDirectory()?await i(d):(n++,c===O&&r++)}catch{}}}return await i(e),{contextFiles:r,totalFiles:n}}async function le(e){let t=ze(),o=process.cwd();try{if(ut()==="sandbox")try{Kt()}catch(E){console.error(R.red(`Sandbox environment error: ${E}`)),process.exit(1)}await _(o)||(console.error(R.red("Error: Not a git repository")),process.exit(1));let r=await T(o),n=await S(),i=I(r),{stat:s,rm:a}=await import("fs/promises");try{await s(i)}catch{console.error(R.red(`Error: ${y}/ not found`)),console.log("Run 'tk init' first to create the structure."),process.exit(1)}if(await f("push",0,"Validating structure..."),!e.skipValidation){t.start(`Validating ${y}/ structure...`);let E=await yt(r);if(!E.valid){t.fail("Validation failed"),console.log(),console.log(R.red("Fix the following errors before pushing:"));for(let Z of E.errors)console.log(R.red(` \u2022 ${Z.path}: ${Z.message}`));process.exit(1)}t.succeed("Validation passed"),E.warnings.length>0&&console.log(R.yellow(` ${E.warnings.length} warning(s)`))}await f("push",20,"Validation passed");let c;if(n.mode==="sandbox"&&n.metaRepoId)c=n.metaRepoId;else{let E=await F(r);E?.projectId?c=E.projectId:(console.error(R.red("Error: No project ID found. Set META_REPO_ID environment variable.")),process.exit(1))}let d=n.branch||await $(r),l=n.commitSha||await N(r),u=await Gt(r,l);await f("push",30,"Packaging .thinker.md/..."),t.start(`Packaging ${y}/...`);let g=ae(r,`${y}.tar.gz`);await ce.create({file:g,cwd:r,gzip:!0},[y]);let{readFile:h}=await import("fs/promises"),p=await h(g);await a(g,{force:!0}),t.succeed(`Packaged archive: ${p.length} bytes`),await f("push",60,"Uploading to storage...");let C=await qe(i),B=await F(r),x=e.forceFull?mt:B?.pulledFrom?Nt:mt,Pt={branch:d,commitSha:l,commitMessage:u,mode:x,parentCommitSha:B?.pulledFrom,generatedAt:new Date().toISOString(),jobId:n.jobId||`local-${Date.now()}`,tokensUsed:0,filesAnalyzed:C.totalFiles,contextFilesGenerated:C.contextFiles};t.start("Uploading to storage...");try{let E=await re(c,d,l,p,Pt);if(t.succeed("Uploaded to storage"),await f("push",90,"Finalizing..."),n.mode==="sandbox"&&n.callbackUrl){let Z={filesAnalyzed:C.totalFiles,contextFilesGenerated:C.contextFiles,tokensUsed:0,mode:x};await zt(E,Z)}await f("push",100,"Done!"),console.log(),console.log(R.green("\u2713 Successfully pushed to storage")),console.log(),console.log(`Branch: ${R.cyan(d)}`),console.log(`Commit: ${R.cyan(l.slice(0,7))}`),console.log(`Mode: ${R.cyan(x)}`),console.log(`Context files: ${R.cyan(C.contextFiles)}`),n.mode==="sandbox"&&(console.log(),console.log(R.green("\u2713 Job completed")))}catch(E){t.fail("Upload failed"),E instanceof Error&&E.message.includes("R2 credentials")?(console.error(R.red(E.message)),console.log(),console.log("Required environment variables:"),console.log(" R2_ENDPOINT"),console.log(" R2_ACCESS_KEY_ID"),console.log(" R2_SECRET_ACCESS_KEY"),console.log(" R2_BUCKET")):console.error(R.red(`Error: ${E}`)),process.exit(1)}}catch(r){t.fail("Push failed"),console.error(R.red(`Error: ${r}`)),process.exit(1)}}import j from"chalk";import de from"ora";async function Ye(e,t){try{let o=await fetch(`${e}/api/auth/validate`,{method:"GET",headers:{Authorization:`Bearer ${t}`}});return o.ok?{valid:!0,userId:(await o.json()).userId}:o.status===401?{valid:!1,error:"Invalid or expired token"}:{valid:!1,error:`API error: ${o.status} ${o.statusText}`}}catch(o){return{valid:!1,error:`Connection failed: ${o}`}}}async function pe(e){let t=de(),o=e.apiUrl||J;if(e.token){t.start("Validating token...");let n=await Ye(o,e.token);n.valid||(t.fail("Authentication failed"),console.error(j.red(`Error: ${n.error}`)),process.exit(1));let i={apiToken:e.token,apiUrl:o,userId:n.userId};await gt(i),t.succeed("Authenticated successfully"),console.log(),console.log(`Config saved to: ${j.cyan(K)}`);return}console.log(j.bold("ThinkerMD Login")),console.log(),console.log("To authenticate, you need an API token from ThinkerMD."),console.log(),console.log("Get your token at: "+j.cyan(`${o}/settings/tokens`)),console.log(),console.log("Then run:"),console.log(j.cyan(" tk login --token YOUR_TOKEN")),console.log();let r=await v();r.apiToken&&(console.log(j.green("You are already logged in.")),console.log(`User ID: ${j.cyan(r.userId||"unknown")}`),console.log(),console.log("To re-authenticate, run:"),console.log(j.cyan(" tk login --token NEW_TOKEN")))}async function me(){let e=de();e.start("Logging out..."),await gt({}),e.succeed("Logged out successfully"),console.log(),console.log(`Config cleared from: ${j.cyan(K)}`)}import{join as ue}from"path";import m from"chalk";import We from"ora";async function Qe(e){let{readFile:t}=await import("fs/promises"),{existsSync:o}=await import("fs"),r=ue(I(e),"structure.json");try{if(o(r)){let n=await t(r,"utf-8");return JSON.parse(n)}}catch{}return null}async function Ze(e){let{readdir:t,stat:o}=await import("fs/promises"),r=0;async function n(i){let s;try{s=await t(i)}catch{return}for(let a of s){let c=ue(i,a);try{(await o(c)).isDirectory()?await n(c):a==="CONTEXT.md"&&r++}catch{}}}return await n(e),r}async function ge(){let e=We(),t=process.cwd();try{await _(t)||(console.error(m.red("Error: Not a git repository")),process.exit(1));let o=await T(t),r=await S(),n=I(o);console.log(m.bold("ThinkerMD Status")),console.log();let i=await X(o),s=r.branch||await $(o),a=r.commitSha||await N(o);console.log(m.dim("Repository:")),console.log(` Name: ${m.cyan(i||"unknown")}`),console.log(` Branch: ${m.cyan(s)}`),console.log(` Commit: ${m.cyan(a.slice(0,7))}`),console.log();let{stat:c}=await import("fs/promises"),d=!1;try{await c(n),d=!0}catch{}if(!d){console.log(m.dim("Local Context:")),console.log(` Status: ${m.yellow("Not initialized")}`),console.log(),console.log("Run "+m.cyan("tk init")+" to create .thinker.md/");return}console.log(m.dim("Local Context:"));let l=await F(o),u=await Qe(o),g=await Ze(n);if(console.log(` Status: ${m.green("Initialized")}`),console.log(` Project ID: ${m.cyan(l?.projectId||"unknown")}`),console.log(` Context files: ${m.cyan(g)}`),u&&(console.log(` Directories: ${m.cyan(u.directoryCount)}`),console.log(` Files tracked: ${m.cyan(u.fileCount)}`)),l?.generatedAt){let p=new Date(l.generatedAt);console.log(` Generated: ${m.cyan(p.toLocaleString())}`)}l?.pulledFrom&&console.log(` Pulled from: ${m.cyan(l.pulledFrom.slice(0,7))}`),console.log();let h=r.metaRepoId||l?.projectId;if(h&&r.r2Endpoint){console.log(m.dim("Remote Storage:")),e.start("Checking remote...");try{let p=await st(h,s);p?(e.stop(),console.log(` Latest commit: ${m.cyan(p.slice(0,7))}`),p===a?console.log(` Status: ${m.green("Up to date")}`):l?.pulledFrom===p?console.log(` Status: ${m.yellow("Local changes not pushed")}`):(console.log(` Status: ${m.yellow("Remote has newer version")}`),console.log(),console.log("Run "+m.cyan("tk pull")+" to download latest context"))):(e.stop(),console.log(` Status: ${m.yellow("No remote context found")}`),console.log(),console.log("Run "+m.cyan("tk push")+" to upload context"))}catch{e.stop(),console.log(` Status: ${m.dim("Unable to check (no credentials)")}`)}}console.log(),console.log(m.dim("Mode:")),console.log(` ${r.mode==="sandbox"?m.cyan("Sandbox"):m.cyan("Local")}`),r.mode==="sandbox"&&console.log(` Job ID: ${m.cyan(r.jobId||"unknown")}`)}catch(o){e.fail("Failed to get status"),console.error(m.red(`Error: ${o}`)),process.exit(1)}}import w from"chalk";import to from"ora";async function fe(e){let t=to(),o=process.cwd();try{await _(o)||(console.error(w.red("Error: Not a git repository")),process.exit(1));let r=await T(o);if((await S()).mode==="sandbox"){console.error(w.yellow("Note: In sandbox mode, generation is triggered automatically.")),console.log("Use 'tk init' to create structure and 'tk push' when done.");return}let i=await v();i.apiToken||(console.error(w.red("Error: Not logged in")),console.log(),console.log("Please authenticate first:"),console.log(w.cyan(" tk login --token YOUR_TOKEN")),process.exit(1));let s=i.apiUrl||J,a=await X(r),c=e.branch||await $(r),d=await N(r);a||(console.error(w.red("Error: Unable to determine repository name")),console.log("Make sure this repository has a remote origin configured."),process.exit(1)),console.log(w.bold("Trigger Context Generation")),console.log(),console.log(`Repository: ${w.cyan(a)}`),console.log(`Branch: ${w.cyan(c)}`),console.log(`Commit: ${w.cyan(d.slice(0,7))}`),console.log(),t.start("Looking up meta-repository...");let l=await fetch(`${s}/api/meta-repositories/lookup?repo=${encodeURIComponent(a)}`,{headers:{Authorization:`Bearer ${i.apiToken}`}});l.ok||(l.status===404&&(t.fail("Meta-repository not found"),console.log(),console.log("This repository is not connected to ThinkerMD. Please add it at:"),console.log(w.cyan(`${s}/dashboard`)),process.exit(1)),t.fail("API error"),console.error(w.red(`Error: ${l.status} ${l.statusText}`)),process.exit(1));let u=await l.json();t.succeed(`Found meta-repository: ${u.id}`),t.start("Triggering generation job...");let g=await fetch(`${s}/api/jobs/generate`,{method:"POST",headers:{Authorization:`Bearer ${i.apiToken}`,"Content-Type":"application/json"},body:JSON.stringify({metaRepositoryId:u.id,commitSha:d,branch:c,priority:"normal"})});if(!g.ok){t.fail("Failed to trigger generation");let p=await g.json().catch(()=>({}));console.error(w.red(`Error: ${p.error||g.statusText}`)),process.exit(1)}let h=await g.json();if(t.succeed("Generation job queued"),console.log(),console.log(`Job ID: ${w.cyan(h.jobId)}`),console.log(`Status: ${w.cyan(h.status)}`),h.estimatedPosition&&console.log(`Queue position: ${w.cyan(h.estimatedPosition)}`),console.log(),console.log("Track progress at:"),console.log(w.cyan(`${s}/dashboard/repositories/${u.id}/jobs/${h.jobId}`)),e.wait){console.log(),t.start("Waiting for completion...");let p=!1,C="";for(;!p;){await new Promise(x=>setTimeout(x,5e3));let B=await fetch(`${s}/api/jobs/${h.jobId}`,{headers:{Authorization:`Bearer ${i.apiToken}`}});if(B.ok){let x=await B.json();x.status!==C&&(C=x.status,t.text=`Status: ${x.status} (${x.progress||0}%)`),(x.status==="completed"||x.status==="failed")&&(p=!0,x.status==="completed"?(t.succeed("Generation completed!"),console.log(),console.log("Run "+w.cyan("tk pull")+" to download the generated context")):(t.fail("Generation failed"),x.errorMessage&&console.error(w.red(`Error: ${x.errorMessage}`))))}}}}catch(r){t.fail("Failed to trigger generation"),console.error(w.red(`Error: ${r}`)),process.exit(1)}}import Tt from"chalk";import{Hono as uo}from"hono";import P from"chalk";import{createServer as go}from"http";import{homedir as xe}from"os";import{join as Ce}from"path";import{readFile as ro,writeFile as no,mkdir as io}from"fs/promises";import{existsSync as so}from"fs";async function eo(){return(await v()).apiUrl||process.env.THINKERMD_API_URL||J}async function oo(){return(await v()).apiToken||process.env.THINKERMD_API_TOKEN||null}async function at(e,t={}){let o=await eo(),r=await oo();if(!r)throw new Error("Not authenticated. Run `tk login` first.");let{method:n="GET",body:i,params:s}=t,a=`${o}${e}`;if(s){let d=new URLSearchParams(s);a+=`?${d.toString()}`}let c=await fetch(a,{method:n,headers:{Authorization:`Bearer ${r}`,"Content-Type":"application/json"},body:i?JSON.stringify(i):void 0});if(!c.ok){let d=await c.json().catch(()=>({}));throw new Error(d.error||`API request failed: ${c.status} ${c.statusText}`)}return c.json()}async function he(e,t,o={}){return at("/api/mcp/search",{method:"POST",body:{repositoryId:e,query:t,branch:o.branch,limit:o.limit||10}})}async function Ct(e,t,o={}){return at("/api/mcp/read",{params:{repositoryId:e,filePath:t,...o.branch&&{branch:o.branch}}})}async function ye(e,t={}){return at("/api/mcp/files",{params:{repositoryId:e,...t.branch&&{branch:t.branch}}})}async function we(e){try{let t=await at("/api/user/repositories",{params:{remoteUrl:e}});return t.repositories&&t.repositories.length>0?t.repositories[0]:null}catch{return null}}var Et=Ce(xe(),".thinkermd","cache","repo-mapping.json"),ao=24*60*60*1e3;async function co(){try{if(so(Et)){let e=await ro(Et,"utf-8");return JSON.parse(e)}}catch{}return{mappings:{}}}async function lo(e){let t=Ce(xe(),".thinkermd","cache");await io(t,{recursive:!0}),await no(Et,JSON.stringify(e,null,2))}async function V(e){let t;try{t=await T(e)}catch{return console.error(`[RepoResolver] Not a git repository: ${e}`),null}let o=await ft(t);if(!o)return console.error(`[RepoResolver] No git remote found for: ${t}`),null;let r=po(o),n=await co(),i=n.mappings[r];if(i&&Date.now()-i.cachedAt<ao){console.log(`[RepoResolver] Cache hit for: ${r}`);let d=await $(t),l=await N(t);return{repositoryId:i.repositoryId,fullName:i.fullName,remoteUrl:r,gitRoot:t,branch:d||"main",commit:l||"HEAD"}}console.log(`[RepoResolver] Querying API for: ${r}`);let s=await we(r);if(!s)return console.error(`[RepoResolver] Repository not found in ThinkerMD: ${r}`),null;n.mappings[r]={remoteUrl:r,repositoryId:s.id,fullName:s.fullName,cachedAt:Date.now()},await lo(n);let a=await $(t),c=await N(t);return{repositoryId:s.id,fullName:s.fullName,remoteUrl:r,gitRoot:t,branch:a||"main",commit:c||"HEAD"}}function po(e){return e=e.replace(/\.git$/,""),e.startsWith("git@")&&(e=e.replace(/^git@/,"https://").replace(/:([^/])/,"/$1")),e.startsWith("http://")&&(e=e.replace("http://","https://")),e}async function ct(e){let{path:t,query:o,limit:r=10}=e,n=await V(t);if(!n)throw new Error(`Could not resolve repository for path: ${t}`);return{...await he(n.repositoryId,o,{branch:n.branch,limit:r}),repositoryId:n.repositoryId,fullName:n.fullName}}async function lt(e){let{path:t,filePath:o}=e,r=await V(t);if(!r)throw new Error(`Could not resolve repository for path: ${t}`);return{...await Ct(r.repositoryId,o,{branch:r.branch}),repositoryId:r.repositoryId,fullName:r.fullName}}async function dt(e){let{path:t}=e,o=await V(t);if(!o)throw new Error(`Could not resolve repository for path: ${t}`);let r=t;t.startsWith(o.gitRoot)&&(r=t.slice(o.gitRoot.length).replace(/^\//,""));let n=r?`.thinker.md/contexts/${r}/CONTEXT.md`:".thinker.md/contexts/CONTEXT.md";return{...await Ct(o.repositoryId,n,{branch:o.branch}),repositoryId:o.repositoryId,fullName:o.fullName}}async function pt(e){let{path:t}=e,o=await V(t);if(!o)throw new Error(`Could not resolve repository for path: ${t}`);return{...await ye(o.repositoryId,{branch:o.branch}),repositoryId:o.repositoryId,fullName:o.fullName}}async function Ee(e){return V(e.path)}import{McpServer as mo}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as U}from"zod";function Re(){let e=new mo({name:"thinkermd",version:"0.1.0"});return e.tool("thinkermd_search","Semantic search across context files in the repository. Use this to find relevant documentation and context about the codebase.",{query:U.string().describe("Search query - what you're looking for"),path:U.string().optional().describe("Path to directory (defaults to current working directory)"),limit:U.number().optional().default(10).describe("Maximum number of results to return")},async({query:t,path:o,limit:r})=>{try{let n=o||process.cwd(),i=await ct({path:n,query:t,limit:r});return{content:[{type:"text",text:JSON.stringify(i,null,2)}]}}catch(n){return{content:[{type:"text",text:`Error: ${n instanceof Error?n.message:"Search failed"}`}],isError:!0}}}),e.tool("thinkermd_read","Read a specific context file from the repository. Use this to get the full content of a CONTEXT.md file.",{filePath:U.string().describe("Path to the context file relative to repository root"),path:U.string().optional().describe("Path to repository directory (defaults to current working directory)")},async({filePath:t,path:o})=>{try{let r=o||process.cwd();return{content:[{type:"text",text:(await lt({path:r,filePath:t})).content}]}}catch(r){return{content:[{type:"text",text:`Error: ${r instanceof Error?r.message:"Read failed"}`}],isError:!0}}}),e.tool("thinkermd_get_context","Get the CONTEXT.md file for a specific directory. This provides contextual documentation about that part of the codebase.",{path:U.string().describe("Path to the directory to get context for")},async({path:t})=>{try{return{content:[{type:"text",text:(await dt({path:t})).content}]}}catch(o){return{content:[{type:"text",text:`Error: ${o instanceof Error?o.message:"Get context failed"}`}],isError:!0}}}),e.tool("thinkermd_list_files","List all CONTEXT.md files available in the repository. Use this to discover what documentation is available.",{path:U.string().optional().describe("Path to repository directory (defaults to current working directory)")},async({path:t})=>{try{let o=t||process.cwd(),r=await pt({path:o});return{content:[{type:"text",text:JSON.stringify(r,null,2)}]}}catch(o){return{content:[{type:"text",text:`Error: ${o instanceof Error?o.message:"List files failed"}`}],isError:!0}}}),e}import{SSEServerTransport as fo}from"@modelcontextprotocol/sdk/server/sse.js";var Rt=new Map,ho=Re();async function yo(e,t){console.log("[MCP] New SSE connection");let o=new fo("/mcp/messages",t),r=crypto.randomUUID();Rt.set(r,o),t.setHeader("X-MCP-Session-Id",r),await ho.connect(o),t.on("close",()=>{Rt.delete(r),console.log("[MCP] SSE connection closed")})}async function wo(e,t){let o=e.headers["x-mcp-session-id"];if(!o){t.writeHead(400,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Missing X-MCP-Session-Id header"}));return}let r=Rt.get(o);if(!r){t.writeHead(400,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Invalid session"}));return}try{await r.handlePostMessage(e,t)}catch(n){console.error("[MCP] Message handling error:",n),t.headersSent||(t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Failed to process message"})))}}function xo(){let e=new uo;return e.get("/health",t=>t.json({status:"ok",version:"0.1.0"})),e.post("/mcp/search",async t=>{try{let o=await t.req.json(),{path:r,query:n,limit:i}=o;if(!r||!n)return t.json({error:"path and query are required"},400);let s=await ct({path:r,query:n,limit:i});return t.json(s)}catch(o){return console.error("[MCP] Search error:",o),t.json({error:o instanceof Error?o.message:"Search failed"},500)}}),e.get("/mcp/read",async t=>{try{let o=t.req.query("path"),r=t.req.query("filePath");if(!o||!r)return t.json({error:"path and filePath are required"},400);let n=await lt({path:o,filePath:r});return t.json(n)}catch(o){return console.error("[MCP] Read error:",o),t.json({error:o instanceof Error?o.message:"Read failed"},500)}}),e.get("/mcp/context",async t=>{try{let o=t.req.query("path");if(!o)return t.json({error:"path is required"},400);let r=await dt({path:o});return t.json(r)}catch(o){return console.error("[MCP] Get context error:",o),t.json({error:o instanceof Error?o.message:"Get context failed"},500)}}),e.get("/mcp/files",async t=>{try{let o=t.req.query("path");if(!o)return t.json({error:"path is required"},400);let r=await pt({path:o});return t.json(r)}catch(o){return console.error("[MCP] List files error:",o),t.json({error:o instanceof Error?o.message:"List files failed"},500)}}),e.get("/mcp/repo",async t=>{try{let o=t.req.query("path");if(!o)return t.json({error:"path is required"},400);let r=await Ee({path:o});return r?t.json({connected:!0,...r}):t.json({connected:!1,message:"Repository not found in ThinkerMD"})}catch(o){return console.error("[MCP] Get repo info error:",o),t.json({error:o instanceof Error?o.message:"Get repo info failed"},500)}}),e}function Co(e,t){let o=`http://localhost:${e}/mcp`;console.log(P.cyan(`
|
|
29
|
+
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
30
|
+
\u2551 \u2551
|
|
31
|
+
\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2551
|
|
32
|
+
\u2551 \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2551
|
|
33
|
+
\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2551
|
|
34
|
+
\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2551
|
|
35
|
+
\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2551
|
|
36
|
+
\u2551 \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u2551
|
|
37
|
+
\u2551 .md \u2551
|
|
38
|
+
\u2551 \u2551
|
|
39
|
+
\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
|
|
40
|
+
\u2551 \u2551
|
|
41
|
+
\u2551 ${P.green("\u2713")} Logged in as: ${P.white(t.padEnd(42))}\u2551
|
|
42
|
+
\u2551 ${P.green("\u2713")} Daemon running on: ${P.white(o.padEnd(38))}\u2551
|
|
43
|
+
\u2551 \u2551
|
|
44
|
+
\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
|
|
45
|
+
\u2551 Add to your AI tool config: \u2551
|
|
46
|
+
\u2551 \u2551
|
|
47
|
+
\u2551 ${P.dim("{")} \u2551
|
|
48
|
+
\u2551 ${P.dim('"mcpServers"')}: { \u2551
|
|
49
|
+
\u2551 ${P.dim('"thinkermd"')}: { \u2551
|
|
50
|
+
\u2551 ${P.dim('"type"')}: ${P.green('"http"')}, \u2551
|
|
51
|
+
\u2551 ${P.dim('"url"')}: ${P.green(`"${o}"`).padEnd(40)}\u2551
|
|
52
|
+
\u2551 } \u2551
|
|
53
|
+
\u2551 } \u2551
|
|
54
|
+
\u2551 ${P.dim("}")} \u2551
|
|
55
|
+
\u2551 \u2551
|
|
56
|
+
\u2551 ${P.yellow("Press Ctrl+C to stop")} \u2551
|
|
57
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
58
|
+
`))}async function Te(e){let{port:t}=e,o=xo(),n=(await v()).userId||"authenticated user";go(async(s,a)=>{let c=new URL(s.url||"/",`http://localhost:${t}`);if(c.pathname==="/mcp"&&s.method==="GET"){await yo(s,a);return}if(c.pathname==="/mcp/messages"&&s.method==="POST"){await wo(s,a);return}let d={};for(let[u,g]of Object.entries(s.headers))typeof g=="string"?d[u]=g:Array.isArray(g)&&(d[u]=g.join(", "));let l=await o.fetch(new Request(c.toString(),{method:s.method,headers:d,body:s.method!=="GET"&&s.method!=="HEAD"?s:void 0,duplex:"half"}));if(a.writeHead(l.status,Object.fromEntries(l.headers.entries())),l.body){let u=l.body.getReader();try{for(;;){let{done:g,value:h}=await u.read();if(g)break;a.write(h)}}finally{u.releaseLock()}}a.end()}).listen(t,()=>{Co(t,n)}),await new Promise(()=>{})}var Eo=3456;async function kt(e){(await v()).apiToken||(console.error(Tt.red("Not authenticated. Run `tk login` first.")),process.exit(1));let o=e.port?parseInt(e.port,10):Eo;(isNaN(o)||o<1||o>65535)&&(console.error(Tt.red(`Invalid port: ${e.port}`)),process.exit(1));try{await Te({port:o})}catch(r){console.error(Tt.red("Failed to start daemon:"),r),process.exit(1)}}var b=new Ro;b.name("tk").description("ThinkerMD CLI - Manage .thinker.md/ context files").version("0.1.0");b.command("init").description("Create .thinker.md/ structure with empty CONTEXT.md files").option("-f, --force","Reinitialize even if .thinker.md/ exists").option("-m, --merge","Only add missing CONTEXT.md files, keep existing ones").action(e=>Wt(e));b.command("validate").description("Verify .thinker.md/ structure matches repository").option("-q, --quiet","Only output errors, no summary").action(e=>Qt(e));b.command("pull").description("Download previous context from storage").option("-b, --branch <branch>","Specify branch to pull from").option("-c, --commit <sha>","Pull specific commit version").action(e=>se(e));b.command("push").description("Validate and upload .thinker.md/ to storage").option("--skip-validation","Skip validation before push").option("--force-full","Force full generation mode (ignore previous context)").action(e=>le(e));b.command("login").description("Authenticate with ThinkerMD API").option("-t, --token <token>","API token").option("--api-url <url>","Custom API URL").action(e=>pe(e));b.command("logout").description("Remove saved authentication").action(()=>me());b.command("status").description("Show status of .thinker.md/ context").action(()=>ge());b.command("generate").description("Trigger context generation via API").option("-b, --branch <branch>","Specify branch to generate for").option("-w, --wait","Wait for generation to complete").action(e=>fe(e));b.command("serve").description("Start MCP daemon for AI assistant integration").option("-p, --port <port>","Port to listen on","3456").action(e=>kt(e));b.on("command:*",()=>{console.error(ke.red(`Unknown command: ${b.args.join(" ")}`)),console.log(),b.help()});async function Pe(){try{if(process.argv.length===2){await kt({port:"3456"});return}await b.parseAsync(process.argv)}catch(e){e instanceof Error&&console.error(ke.red(`Error: ${e.message}`)),process.exit(1)}}Pe().catch(e=>{console.error("Fatal error:",e),process.exit(1)});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thinkermd/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "ThinkerMD CLI for managing .thinker.md/ context files",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,11 +16,15 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@aws-sdk/client-s3": "^3.705.0",
|
|
19
|
+
"@hono/node-server": "^1.13.7",
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
19
21
|
"chalk": "^5.3.0",
|
|
20
22
|
"commander": "^12.0.0",
|
|
23
|
+
"hono": "^4.6.14",
|
|
21
24
|
"ignore": "^5.3.0",
|
|
22
25
|
"ora": "^8.0.0",
|
|
23
|
-
"tar": "^7.0.0"
|
|
26
|
+
"tar": "^7.0.0",
|
|
27
|
+
"zod": "^4.3.5"
|
|
24
28
|
},
|
|
25
29
|
"devDependencies": {
|
|
26
30
|
"@types/node": "^20.0.0",
|