@mod-computer/mod 0.2.2 → 0.2.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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # mod-cli
1
+ # mod
2
2
 
3
3
  Spec-driven development with traceability. Connect your specs, code, and tests so reviews show the full picture.
4
4
 
@@ -6,24 +6,25 @@ Spec-driven development with traceability. Connect your specs, code, and tests s
6
6
 
7
7
  ```bash
8
8
  # Install
9
- npm install -g @mod-computer/cli
9
+ npm install -g @mod-computer/mod
10
10
 
11
- # Initialize workspace (installs /mod skill for Claude Code)
11
+ # Initialize workspace (creates .mod/traces/, glassware.toml, installs mod skill)
12
12
  mod init
13
- mod auth login
14
13
 
15
14
  # Start working
16
15
  git checkout -b feat/user-auth
17
16
  ```
18
17
 
18
+ No account needed. No server connection. Just install and go.
19
+
19
20
  ## The Idea
20
21
 
21
22
  You write specs. Agents implement them. But at review time, how do you know what requirement each function addresses? What's tested? What changed but isn't connected to anything?
22
23
 
23
- mod-cli builds a trace graph as you work:
24
+ mod builds a trace graph as you work:
24
25
 
25
26
  ```
26
- Spec requirement → ImplementationTests
27
+ requirement → specificationimplementation → test
27
28
  ```
28
29
 
29
30
  At review, `mod trace report` shows what's connected. `mod trace diff` catches what isn't.
@@ -72,56 +73,101 @@ mod trace unmet # Requirements without implementations
72
73
  ### Setup
73
74
 
74
75
  ```bash
75
- mod init # Initialize workspace, install /mod skill
76
- mod auth login # Authenticate
77
- mod auth logout # Sign out
78
- mod auth status # Show auth state
76
+ mod init # Initialize workspace, install mod skill
79
77
  ```
80
78
 
81
- ### Traces
79
+ ### Add Traces
82
80
 
83
81
  ```bash
84
- # Add traces
82
+ # Single trace
85
83
  mod trace add <file>:<line> --type=<type> ["description"]
86
84
  mod trace add <file>:<line> --type=<type> --link=<trace-id>
87
85
 
88
- # Link traces
86
+ # Bulk add (agent-friendly)
87
+ mod trace add-bulk --json '[
88
+ {"file": "specs/auth.md", "line": 15, "type": "requirement"},
89
+ {"file": "specs/auth.md", "line": 22, "type": "requirement"}
90
+ ]'
91
+ ```
92
+
93
+ ### Link Traces
94
+
95
+ ```bash
96
+ # Single link
89
97
  mod trace link <source-id> <target-id>
90
98
 
91
- # View traces
99
+ # Bulk link
100
+ mod trace link-bulk --json '[
101
+ {"source": "impl-login--d0e6f3a4", "target": "req-login--a7f3d2c1"}
102
+ ]'
103
+ ```
104
+
105
+ ### Unlink and Delete
106
+
107
+ ```bash
108
+ mod trace unlink <source-id> <target-id>
109
+ mod trace delete <trace-id> --force
110
+ ```
111
+
112
+ ### Bulk Update
113
+
114
+ ```bash
115
+ mod trace update-bulk --json '[
116
+ {"id": "req-login--a7f3d2c1", "nodeType": "specification"}
117
+ ]'
118
+ ```
119
+
120
+ ### View Traces
121
+
122
+ ```bash
92
123
  mod trace list # All traces
93
124
  mod trace list --type=requirement # Filter by type
94
125
  mod trace list --file=<path> # Filter by file
95
126
  mod trace get <trace-id> # Get trace details
127
+ ```
128
+
129
+ ### Reports
96
130
 
97
- # Reports
131
+ ```bash
98
132
  mod trace report <file> # Per-document coverage
99
133
  mod trace coverage # Workspace-wide stats
134
+ ```
135
+
136
+ ### Find Gaps
100
137
 
101
- # Find gaps
138
+ ```bash
102
139
  mod trace diff # Untraced files on branch
103
140
  mod trace diff main..HEAD # Explicit git range
104
141
  mod trace unmet # Requirements without implementations
105
142
  ```
106
143
 
107
- ### Comments
144
+ ## Schema
108
145
 
109
- ```bash
110
- mod comment add <file>:<line> "text"
111
- mod comment list [file]
112
- ```
146
+ Define your trace graph in `glassware.toml`. The default schema:
147
+
148
+ ```toml
149
+ node_types = ["requirement", "specification", "implementation", "test"]
113
150
 
114
- ## Trace Types
151
+ [edge_types.specifies]
152
+ from = "specification"
153
+ to = "requirement"
154
+ attribute = "requirements"
155
+ label = "Specified"
156
+
157
+ [edge_types.implements]
158
+ from = "implementation"
159
+ to = "specification"
160
+ attribute = "specifications"
161
+ label = "Implemented"
162
+
163
+ [edge_types.tests]
164
+ from = "test"
165
+ to = "implementation"
166
+ attribute = "implementations"
167
+ label = "Tested"
168
+ ```
115
169
 
116
- | Type | Use For |
117
- |------|---------|
118
- | `requirement` | Specs, user stories, acceptance criteria |
119
- | `specification` | Detailed technical specs |
120
- | `implementation` | Code that builds something |
121
- | `test` | Code that verifies something |
122
- | `design` | Design docs, architecture notes |
123
- | `decision` | ADRs, decision records |
124
- | `utility` | Helpers that don't need tracing |
170
+ Add custom node types and edges to match your workflow (releases, RFCs, ADRs, etc.).
125
171
 
126
172
  ## CI Integration
127
173
 
@@ -763,7 +763,7 @@ Run without --dry-run to delete.`)}import gst from"fs/promises";import BL from"p
763
763
  `+r:""),l=Math.floor(o.length/s)-this.cursorPos.rows+(r?oAr(r):0);l>0&&(a+=Ist(l)),a+=_st(this.cursorPos.cols),this.write(Cst(this.extraLinesUnderPrompt)+kst(this.height)+a),this.extraLinesUnderPrompt=l,this.height=oAr(a)}checkCursorPos(){let t=this.rl.getCursorPos();t.cols!==this.cursorPos.cols&&(this.write(_st(t.cols)),this.cursorPos=t)}done({clearContent:t}){this.rl.setPrompt("");let r=Cst(this.extraLinesUnderPrompt);r+=t?kst(this.height):`
764
764
  `,r+=nAr,this.write(r),this.rl.close()}};var dke=class extends Promise{static withResolver(){let t,r;return{promise:new Promise((o,i)=>{t=o,r=i}),resolve:t,reject:r}}};function Seo(){let e=Error.prepareStackTrace,t=[];try{Error.prepareStackTrace=(r,n)=>{let o=n.slice(1);return t=o,o},new Error().stack}catch{return t}return Error.prepareStackTrace=e,t}function UL(e){let t=Seo();return(n,o={})=>{let{input:i=process.stdin,signal:s}=o,a=new Set,c=new sAr.default;c.pipe(o.output??process.stdout);let l=iAr.createInterface({terminal:!0,input:i,output:c}),d=new pie(l),{promise:m,resolve:h,reject:A}=dke.withResolver(),b=()=>A(new lke);if(s){let _=()=>A(new cke({cause:s.reason}));if(s.aborted)return _(),Object.assign(m,{cancel:b});s.addEventListener("abort",_),a.add(()=>s.removeEventListener("abort",_))}a.add(K_e((_,D)=>{A(new uie(`User force closed the prompt with ${_} ${D}`))}));let w=()=>A(new uie("User force closed the prompt with SIGINT"));l.on("SIGINT",w),a.add(()=>l.removeListener("SIGINT",w));let v=()=>d.checkCursorPos();return l.input.on("keypress",v),a.add(()=>l.input.removeListener("keypress",v)),zgr(l,_=>{let D=weo.bind(()=>NL.clearAll());return l.on("close",D),a.add(()=>l.removeListener("close",D)),_(()=>{var R;try{let M=e(n,F=>{setImmediate(()=>h(F))});if(M===void 0){let F=(R=t[1])==null?void 0:R.getFileName();throw new Error(`Prompt functions must return a string.
765
765
  at ${F}`)}let[P,U]=typeof M=="string"?[M]:M;d.render(P,U),NL.run()}catch(M){A(M)}}),Object.assign(m.then(R=>(NL.clearAll(),R),R=>{throw NL.clearAll(),R}).finally(()=>{a.forEach(R=>R()),d.done({clearContent:!!o.clearPromptOnDone}),c.end()}).then(()=>m),{cancel:b})})}}import{styleText as Ieo}from"node:util";var mT=class{separator=Ieo("dim",Array.from({length:15}).join(f5.line));type="separator";constructor(t){t&&(this.separator=t)}static isSeparator(t){return!!(t&&typeof t=="object"&&"type"in t&&t.type==="separator")}};function aAr(e,t){let r=t!==!1;return/^(y|yes)/i.test(e)?r=!0:/^(n|no)/i.test(e)&&(r=!1),r}function cAr(e){return e?"Yes":"No"}var Tst=UL((e,t)=>{let{transformer:r=cAr}=e,[n,o]=Up("idle"),[i,s]=Up(""),a=hT(e.theme),c=PL({status:n,theme:a});LL((h,A)=>{if(n==="idle")if(ML(h)){let b=aAr(i,e.default);s(r(b)),o("done"),t(b)}else if(lie(h)){let b=cAr(!aAr(i,e.default));A.clearLine(0),A.write(b),s(b)}else s(A.line)});let l=i,d="";n==="done"?l=a.style.answer(i):d=` ${a.style.defaultAnswer(e.default===!1?"y/N":"Y/n")}`;let m=a.style.message(e.message,n);return`${c} ${m}${d} ${l}`});var Ceo={validationFailureMode:"keep"},Dst=UL((e,t)=>{let{prefill:r="tab"}=e,n=hT(Ceo,e.theme),[o,i]=Up("idle"),[s="",a]=Up(e.default),[c,l]=Up(),[d,m]=Up(""),h=PL({status:o,theme:n});async function A(D){let{required:R,pattern:M,patternError:P="Invalid input"}=e;return R&&!D?"You must provide a value":M&&!M.test(D)?P:typeof e.validate=="function"?await e.validate(D)||"You must provide a valid value":!0}LL(async(D,R)=>{if(o==="idle")if(ML(D)){let M=d||s;i("loading");let P=await A(M);P===!0?(m(M),i("done"),t(M)):(n.validationFailureMode==="clear"?m(""):R.write(d),l(P),i("idle"))}else cie(D)&&!d?a(void 0):lie(D)&&!d?(a(void 0),R.clearLine(0),R.write(s),m(s)):(m(R.line),l(void 0))}),pT(D=>{r==="editable"&&s&&(D.write(s),m(s))},[]);let b=n.style.message(e.message,o),w=d;typeof e.transformer=="function"?w=e.transformer(d,{isFinal:o==="done"}):o==="done"&&(w=n.style.answer(d));let v;s&&o!=="done"&&!d&&(v=n.style.defaultAnswer(s));let _="";return c&&(_=n.style.error(c)),[[h,b,v,w].filter(D=>D!==void 0).join(" "),_]});import{styleText as hie}from"node:util";var _eo={icon:{cursor:f5.pointer},style:{disabled:e=>hie("dim",`- ${e}`),description:e=>hie("cyan",e),keysHelpTip:e=>e.map(([t,r])=>`${hie("bold",t)} ${hie("dim",r)}`).join(hie("dim"," \u2022 "))},indexMode:"hidden",keybindings:[]};function d5(e){return!mT.isSeparator(e)&&!e.disabled}function keo(e){return e.map(t=>{if(mT.isSeparator(t))return t;if(typeof t!="object"||t===null||!("value"in t)){let o=String(t);return{value:t,name:o,short:o,disabled:!1}}let r=t.name??String(t.value),n={value:t.value,name:r,short:t.short??r,disabled:t.disabled??!1};return t.description&&(n.description=t.description),n})}var Rst=UL((e,t)=>{let{loop:r=!0,pageSize:n=7}=e,o=hT(_eo,e.theme),{keybindings:i}=o,[s,a]=Up("idle"),c=PL({status:s,theme:o}),l=QL(),d=!i.includes("vim"),m=fie(()=>keo(e.choices),[e.choices]),h=fie(()=>{let F=m.findIndex(d5),L=m.findLastIndex(d5);if(F===-1)throw new l5("[select prompt] No selectable choices. All choices are disabled.");return{first:F,last:L}},[m]),A=fie(()=>"default"in e?m.findIndex(F=>d5(F)&&F.value===e.default):-1,[e.default,m]),[b,w]=Up(A===-1?h.first:A),v=m[b];LL((F,L)=>{if(clearTimeout(l.current),ML(F))a("done"),t(v.value);else if(aie(F,i)||ake(F,i)){if(L.clearLine(0),r||aie(F,i)&&b!==h.first||ake(F,i)&&b!==h.last){let j=aie(F,i)?-1:1,G=b;do G=(G+j+m.length)%m.length;while(!d5(m[G]));w(G)}}else if(bst(F)&&!Number.isNaN(Number(L.line))){let j=Number(L.line)-1,G=-1,ee=m.findIndex(re=>mT.isSeparator(re)?!1:(G++,G===j)),Z=m[ee];Z!=null&&d5(Z)&&w(ee),l.current=setTimeout(()=>{L.clearLine(0)},700)}else if(cie(F))L.clearLine(0);else if(d){let j=L.line.toLowerCase(),G=m.findIndex(ee=>mT.isSeparator(ee)||!d5(ee)?!1:ee.name.toLowerCase().startsWith(j));G!==-1&&w(G),l.current=setTimeout(()=>{L.clearLine(0)},700)}}),pT(()=>()=>{clearTimeout(l.current)},[]);let _=o.style.message(e.message,s),D=o.style.keysHelpTip([["\u2191\u2193","navigate"],["\u23CE","select"]]),R=0,M=wst({items:m,active:b,renderItem({item:F,isActive:L,index:j}){if(mT.isSeparator(F))return R++,` ${F.separator}`;let G=o.indexMode==="number"?`${j+1-R}. `:"";if(F.disabled){let re=typeof F.disabled=="string"?F.disabled:"(disabled)";return o.style.disabled(`${G}${F.name} ${re}`)}let ee=L?o.style.highlight:re=>re,Z=L?o.icon.cursor:" ";return ee(`${Z} ${G}${F.name}`)},pageSize:n,loop:r});if(s==="done")return[c,_,o.style.answer(v.short)].filter(Boolean).join(" ");let{description:P}=v;return`${[[c,_].filter(Boolean).join(" "),M," ",P?o.style.description(P):"",D].filter(Boolean).join(`
766
- `).trimEnd()}${rAr}`});async function gT(e,t){return await Rst({message:e,choices:t.map(n=>({name:n.label,value:n.value,description:n.description}))})}async function pke(e,t){return await Dst({message:e,default:t==null?void 0:t.default,validate:t!=null&&t.validate?n=>t.validate(n)??!0:void 0})}async function Ost(e,t){return await Tst({message:e,default:(t==null?void 0:t.default)??!0})}function p5(e){return!e||e.trim().length===0?"Workspace name cannot be empty":e.length>100?"Workspace name must be 100 characters or less":/^[a-zA-Z0-9][a-zA-Z0-9-_ ]*[a-zA-Z0-9]$|^[a-zA-Z0-9]$/.test(e.trim())?null:"Workspace name must start and end with alphanumeric characters"}Xu();tie();Xu();async function Bst(e,t){let r=(t==null?void 0:t.timeout)??3e4,n=(t==null?void 0:t.verbose)??!1,o=n?console.log.bind(console):()=>{};o("[fetch] Creating temporary network repo...");let i=E7(),s=nle(),a=new Set;a.add(e);let c=new NM({storage:new R3(i),network:[new CD(s)],sharePolicy:async(l,d)=>d?a.has(d):!1});o("[fetch] Waiting for WebSocket connection..."),await new Promise(l=>setTimeout(l,500));try{o(`[fetch] Fetching workspace document: ${e}`);let l=await c.find(e);await Promise.race([l.whenReady(),new Promise((_,D)=>setTimeout(()=>D(new Error("Timeout waiting for workspace document")),r))]);let d=l.doc();if(!d)throw new Error("Workspace document is empty after fetch");let m=d.title||d.name||"Untitled";o(`[fetch] Workspace loaded: ${m}`);let h=d.fileRefs||[],A=d.folderRefs||[];o(`[fetch] Found ${h.length} files, ${A.length} folders`);let b=h.map(_=>_.id||_.documentId).filter(Boolean),w=A.map(_=>_.id||_.documentId).filter(Boolean);for(let _ of[...b,...w])a.add(_);o(`[fetch] Fetching ${b.length} file documents...`);let v=0;for(let _ of b)try{let D=await c.find(_);await Promise.race([D.whenReady(),new Promise((R,M)=>setTimeout(()=>M(new Error(`Timeout fetching file ${_}`)),1e4))]),v++,n&&v%10===0&&o(`[fetch] Fetched ${v}/${b.length} files...`)}catch(D){o(`[fetch] Warning: Could not fetch file ${_}: ${D}`)}o(`[fetch] Fetching ${w.length} folder documents...`);for(let _ of w)try{let D=await c.find(_);await Promise.race([D.whenReady(),new Promise((R,M)=>setTimeout(()=>M(new Error(`Timeout fetching folder ${_}`)),1e4))])}catch(D){o(`[fetch] Warning: Could not fetch folder ${_}: ${D}`)}return o("[fetch] Waiting for storage flush..."),await new Promise(_=>setTimeout(_,1e3)),TL(e),Jit([...b,...w]),o(`[fetch] Fetch complete: ${v} files, ${w.length} folders`),{workspaceId:e,workspaceName:m,fileCount:v,folderCount:w.length}}finally{o("[fetch] Shutting down temporary repo...");try{await new Promise(l=>setTimeout(l,500))}catch(l){o(`[fetch] Warning during shutdown: ${l}`)}}}async function uAr(e,t){let r=e.includes("--force"),n=process.cwd();if(!qW(qn.AUTH))return lAr(n,r);let o=e.indexOf("--workspace"),i=e.indexOf("--create"),s=o!==-1?e[o+1]:null,a=i!==-1?e[i+1]:null;try{oF();let c=is(n);if(c&&!r){console.log("Already initialized"),console.log(`Workspace: ${c.workspaceName}`),console.log("");let h=await gT("What would you like to do?",[{label:"Resume file import (if interrupted)",value:"resume"},{label:"Start syncing",value:"sync"},{label:"Reinitialize with different workspace",value:"force"}]);if(h==="resume"){console.log("Checking for files to import...");try{await mke(t,{id:c.workspaceId}),console.log("Resume complete")}catch(A){console.error("Resume failed:",A instanceof Error?A.message:A),process.exit(1)}}else h==="sync"?console.log("Run `mod sync start` to begin syncing"):h==="force"&&console.log("Reinitializing...");h!=="force"&&process.exit(0)}let l=sa();Cx.existsSync(y7())||QE(l);let d=!!l.auth;if(!d&&!a&&!s)return lAr(n,r);let m;d?m=await Teo(t,l.auth.email,{workspaceName:s,createName:a}):m=await Deo(t,{createName:a}),m.lastSyncedAt=new Date().toISOString(),w_(n,m),fAr(),console.log("Installed /mod skill to .claude/skills/mod/"),Beo(m,d),process.exit(0)}catch(c){console.error("Initialization failed:",c.message),process.exit(1)}}async function Teo(e,t,r={}){var l,d,m;console.log(`Signed in as ${t}`);let n=sa(),o=[];try{let h=(l=n.auth)==null?void 0:l.userDocId;if(!h)console.warn("User document not found. Creating local workspace.");else{let A=await e.find(h);await A.whenReady();let b=A.doc();if(b){let w=b.workspaceIds||[];for(let v of w)try{let _=await e.find(v);await _.whenReady();let D=_.doc();o.push({id:v,name:(D==null?void 0:D.title)||(D==null?void 0:D.name)||"Untitled"})}catch{o.push({id:v,name:`Workspace ${String(v).slice(0,8)}`})}}}}catch{console.warn("Could not load cloud workspaces. Creating local workspace.")}if(r.createName)return console.log(`Creating workspace: ${r.createName}`),await hke(e,r.createName);if(r.workspaceName){let h=o.find(_=>_.name.toLowerCase()===r.workspaceName.toLowerCase());if(!h)throw new Error(`Workspace "${r.workspaceName}" not found. Available: ${o.map(_=>_.name).join(", ")||"(none)"}`);console.log(`Selected workspace: ${h.name}`),console.log("Fetching workspace from server...");try{let _=await Bst(h.id,{verbose:!1});console.log(`Fetched ${_.fileCount} files, ${_.folderCount} folders`)}catch(_){console.warn(`Warning: Could not fetch workspace from server: ${_ instanceof Error?_.message:_}`),console.warn("Workspace will sync when daemon starts.")}let A={type:"existing",id:h.id,name:h.name};TL(A.id);let b=process.cwd(),w={path:b,workspaceId:A.id,workspaceName:A.name,connectedAt:new Date().toISOString(),lastSyncedAt:new Date().toISOString()};w_(b,w),console.log("Workspace connection saved");let v=sa();if((d=v.auth)!=null&&d.userDocId)try{await Sx(e).addWorkspace(v.auth.userDocId,A.id)}catch{}return await mke(e,{id:A.id}),w}let i=[...o.map(h=>({label:h.name,value:{type:"existing",id:h.id,name:h.name}})),{label:"+ Create new workspace",value:{type:"create",id:"",name:""}}],s=await gT("Select workspace:",i);if(s.type==="create")return await hke(e);console.log("Fetching workspace from server...");try{let h=await Bst(s.id,{verbose:!1});console.log(`Fetched ${h.fileCount} files, ${h.folderCount} folders`)}catch(h){console.warn(`Warning: Could not fetch workspace from server: ${h instanceof Error?h.message:h}`),console.warn("Workspace will sync when daemon starts.")}TL(s.id);let a=process.cwd(),c={path:a,workspaceId:s.id,workspaceName:s.name,connectedAt:new Date().toISOString(),lastSyncedAt:new Date().toISOString()};if(w_(a,c),console.log("Workspace connection saved"),(m=n.auth)!=null&&m.userDocId)try{await Sx(e).addWorkspace(n.auth.userDocId,s.id)}catch{}return await mke(e,{id:s.id}),c}async function Deo(e,t={}){return t.createName?(console.log(`Creating workspace: ${t.createName}`),await hke(e,t.createName)):(await gT("Select option:",[{label:"Create local workspace",value:"create",description:"Work offline, sync later"},{label:"Sign in to sync with team",value:"signin",description:"Access cloud workspaces"}])==="signin"&&(console.log(""),console.log("Please run `mod auth login` to sign in, then run `mod init` again."),process.exit(0)),await hke(e))}async function hke(e,t){var d;let r=$L.basename(process.cwd()),n=process.cwd(),o=r.charAt(0).toUpperCase()+r.slice(1),i;if(t){let m=p5(t);if(m)throw new Error(`Invalid workspace name: ${m}`);i=t}else i=await pke("Workspace name",{default:o,validate:p5});console.log("Creating workspace...");let a=await gs(e).createWorkspace({name:i});TL(a.id);let c={path:n,workspaceId:a.id,workspaceName:a.name,connectedAt:new Date().toISOString(),lastSyncedAt:new Date().toISOString()};w_(n,c),console.log("Workspace connection saved"),console.log("Syncing workspace..."),await new Promise(m=>setTimeout(m,2e3));let l=sa();if((d=l.auth)!=null&&d.userDocId)try{await Sx(e).addWorkspace(l.auth.userDocId,a.id),await new Promise(h=>setTimeout(h,1e3)),console.log("Workspace registered for sync")}catch{console.warn("Note: Could not register workspace for cross-device sync")}return await mke(e,a),console.log("Workspace synced"),c}async function mke(e,t){let n=await new s5(e).execute({paths:["."]},i=>{i.phase==="scanning"?process.stdout.write(`\rScanning... ${i.total} files found`):i.phase==="adding"&&process.stdout.write(`\rImporting... ${i.current}/${i.total} files`)});if(process.stdout.write("\r"+" ".repeat(60)+"\r"),n.summary.totalFiles===0){console.log("No files to import");return}let o=[];n.summary.created>0&&o.push(`${n.summary.created} created`),n.summary.unchanged>0&&o.push(`${n.summary.unchanged} unchanged`),n.summary.skipped>0&&o.push(`${n.summary.skipped} skipped`),n.summary.errors>0&&o.push(`${n.summary.errors} errors`),console.log(`Imported ${n.summary.totalFiles} files (${o.join(", ")})`)}function lAr(e,t){let r=$L.join(e,".mod","traces");!t&&Cx.existsSync(r)&&(console.log("Already initialized for local development."),console.log("Run `mod init --force` to reinitialize."),process.exit(0)),Cx.mkdirSync(r,{recursive:!0});let n=$L.join(e,"glassware.toml");Cx.existsSync(n)||(Cx.writeFileSync(n,`# Trace schema configuration
766
+ `).trimEnd()}${rAr}`});async function gT(e,t){return await Rst({message:e,choices:t.map(n=>({name:n.label,value:n.value,description:n.description}))})}async function pke(e,t){return await Dst({message:e,default:t==null?void 0:t.default,validate:t!=null&&t.validate?n=>t.validate(n)??!0:void 0})}async function Ost(e,t){return await Tst({message:e,default:(t==null?void 0:t.default)??!0})}function p5(e){return!e||e.trim().length===0?"Workspace name cannot be empty":e.length>100?"Workspace name must be 100 characters or less":/^[a-zA-Z0-9][a-zA-Z0-9-_ ]*[a-zA-Z0-9]$|^[a-zA-Z0-9]$/.test(e.trim())?null:"Workspace name must start and end with alphanumeric characters"}Xu();tie();Xu();async function Bst(e,t){let r=(t==null?void 0:t.timeout)??3e4,n=(t==null?void 0:t.verbose)??!1,o=n?console.log.bind(console):()=>{};o("[fetch] Creating temporary network repo...");let i=E7(),s=nle(),a=new Set;a.add(e);let c=new NM({storage:new R3(i),network:[new CD(s)],sharePolicy:async(l,d)=>d?a.has(d):!1});o("[fetch] Waiting for WebSocket connection..."),await new Promise(l=>setTimeout(l,500));try{o(`[fetch] Fetching workspace document: ${e}`);let l=await c.find(e);await Promise.race([l.whenReady(),new Promise((_,D)=>setTimeout(()=>D(new Error("Timeout waiting for workspace document")),r))]);let d=l.doc();if(!d)throw new Error("Workspace document is empty after fetch");let m=d.title||d.name||"Untitled";o(`[fetch] Workspace loaded: ${m}`);let h=d.fileRefs||[],A=d.folderRefs||[];o(`[fetch] Found ${h.length} files, ${A.length} folders`);let b=h.map(_=>_.id||_.documentId).filter(Boolean),w=A.map(_=>_.id||_.documentId).filter(Boolean);for(let _ of[...b,...w])a.add(_);o(`[fetch] Fetching ${b.length} file documents...`);let v=0;for(let _ of b)try{let D=await c.find(_);await Promise.race([D.whenReady(),new Promise((R,M)=>setTimeout(()=>M(new Error(`Timeout fetching file ${_}`)),1e4))]),v++,n&&v%10===0&&o(`[fetch] Fetched ${v}/${b.length} files...`)}catch(D){o(`[fetch] Warning: Could not fetch file ${_}: ${D}`)}o(`[fetch] Fetching ${w.length} folder documents...`);for(let _ of w)try{let D=await c.find(_);await Promise.race([D.whenReady(),new Promise((R,M)=>setTimeout(()=>M(new Error(`Timeout fetching folder ${_}`)),1e4))])}catch(D){o(`[fetch] Warning: Could not fetch folder ${_}: ${D}`)}return o("[fetch] Waiting for storage flush..."),await new Promise(_=>setTimeout(_,1e3)),TL(e),Jit([...b,...w]),o(`[fetch] Fetch complete: ${v} files, ${w.length} folders`),{workspaceId:e,workspaceName:m,fileCount:v,folderCount:w.length}}finally{o("[fetch] Shutting down temporary repo...");try{await new Promise(l=>setTimeout(l,500))}catch(l){o(`[fetch] Warning during shutdown: ${l}`)}}}async function uAr(e,t){let r=e.includes("--force"),n=process.cwd();if(!qW(qn.AUTH))return lAr(n,r);let o=e.indexOf("--workspace"),i=e.indexOf("--create"),s=o!==-1?e[o+1]:null,a=i!==-1?e[i+1]:null;try{oF();let c=is(n);if(c&&!r){console.log("Already initialized"),console.log(`Workspace: ${c.workspaceName}`),console.log("");let h=await gT("What would you like to do?",[{label:"Resume file import (if interrupted)",value:"resume"},{label:"Start syncing",value:"sync"},{label:"Reinitialize with different workspace",value:"force"}]);if(h==="resume"){console.log("Checking for files to import...");try{await mke(t,{id:c.workspaceId}),console.log("Resume complete")}catch(A){console.error("Resume failed:",A instanceof Error?A.message:A),process.exit(1)}}else h==="sync"?console.log("Run `mod sync start` to begin syncing"):h==="force"&&console.log("Reinitializing...");h!=="force"&&process.exit(0)}let l=sa();Cx.existsSync(y7())||QE(l);let d=!!l.auth;if(!d&&!a&&!s)return lAr(n,r);let m;d?m=await Teo(t,l.auth.email,{workspaceName:s,createName:a}):m=await Deo(t,{createName:a}),m.lastSyncedAt=new Date().toISOString(),w_(n,m),fAr(),console.log("Installed mod skill to .claude/skills/mod/"),Beo(m,d),process.exit(0)}catch(c){console.error("Initialization failed:",c.message),process.exit(1)}}async function Teo(e,t,r={}){var l,d,m;console.log(`Signed in as ${t}`);let n=sa(),o=[];try{let h=(l=n.auth)==null?void 0:l.userDocId;if(!h)console.warn("User document not found. Creating local workspace.");else{let A=await e.find(h);await A.whenReady();let b=A.doc();if(b){let w=b.workspaceIds||[];for(let v of w)try{let _=await e.find(v);await _.whenReady();let D=_.doc();o.push({id:v,name:(D==null?void 0:D.title)||(D==null?void 0:D.name)||"Untitled"})}catch{o.push({id:v,name:`Workspace ${String(v).slice(0,8)}`})}}}}catch{console.warn("Could not load cloud workspaces. Creating local workspace.")}if(r.createName)return console.log(`Creating workspace: ${r.createName}`),await hke(e,r.createName);if(r.workspaceName){let h=o.find(_=>_.name.toLowerCase()===r.workspaceName.toLowerCase());if(!h)throw new Error(`Workspace "${r.workspaceName}" not found. Available: ${o.map(_=>_.name).join(", ")||"(none)"}`);console.log(`Selected workspace: ${h.name}`),console.log("Fetching workspace from server...");try{let _=await Bst(h.id,{verbose:!1});console.log(`Fetched ${_.fileCount} files, ${_.folderCount} folders`)}catch(_){console.warn(`Warning: Could not fetch workspace from server: ${_ instanceof Error?_.message:_}`),console.warn("Workspace will sync when daemon starts.")}let A={type:"existing",id:h.id,name:h.name};TL(A.id);let b=process.cwd(),w={path:b,workspaceId:A.id,workspaceName:A.name,connectedAt:new Date().toISOString(),lastSyncedAt:new Date().toISOString()};w_(b,w),console.log("Workspace connection saved");let v=sa();if((d=v.auth)!=null&&d.userDocId)try{await Sx(e).addWorkspace(v.auth.userDocId,A.id)}catch{}return await mke(e,{id:A.id}),w}let i=[...o.map(h=>({label:h.name,value:{type:"existing",id:h.id,name:h.name}})),{label:"+ Create new workspace",value:{type:"create",id:"",name:""}}],s=await gT("Select workspace:",i);if(s.type==="create")return await hke(e);console.log("Fetching workspace from server...");try{let h=await Bst(s.id,{verbose:!1});console.log(`Fetched ${h.fileCount} files, ${h.folderCount} folders`)}catch(h){console.warn(`Warning: Could not fetch workspace from server: ${h instanceof Error?h.message:h}`),console.warn("Workspace will sync when daemon starts.")}TL(s.id);let a=process.cwd(),c={path:a,workspaceId:s.id,workspaceName:s.name,connectedAt:new Date().toISOString(),lastSyncedAt:new Date().toISOString()};if(w_(a,c),console.log("Workspace connection saved"),(m=n.auth)!=null&&m.userDocId)try{await Sx(e).addWorkspace(n.auth.userDocId,s.id)}catch{}return await mke(e,{id:s.id}),c}async function Deo(e,t={}){return t.createName?(console.log(`Creating workspace: ${t.createName}`),await hke(e,t.createName)):(await gT("Select option:",[{label:"Create local workspace",value:"create",description:"Work offline, sync later"},{label:"Sign in to sync with team",value:"signin",description:"Access cloud workspaces"}])==="signin"&&(console.log(""),console.log("Please run `mod auth login` to sign in, then run `mod init` again."),process.exit(0)),await hke(e))}async function hke(e,t){var d;let r=$L.basename(process.cwd()),n=process.cwd(),o=r.charAt(0).toUpperCase()+r.slice(1),i;if(t){let m=p5(t);if(m)throw new Error(`Invalid workspace name: ${m}`);i=t}else i=await pke("Workspace name",{default:o,validate:p5});console.log("Creating workspace...");let a=await gs(e).createWorkspace({name:i});TL(a.id);let c={path:n,workspaceId:a.id,workspaceName:a.name,connectedAt:new Date().toISOString(),lastSyncedAt:new Date().toISOString()};w_(n,c),console.log("Workspace connection saved"),console.log("Syncing workspace..."),await new Promise(m=>setTimeout(m,2e3));let l=sa();if((d=l.auth)!=null&&d.userDocId)try{await Sx(e).addWorkspace(l.auth.userDocId,a.id),await new Promise(h=>setTimeout(h,1e3)),console.log("Workspace registered for sync")}catch{console.warn("Note: Could not register workspace for cross-device sync")}return await mke(e,a),console.log("Workspace synced"),c}async function mke(e,t){let n=await new s5(e).execute({paths:["."]},i=>{i.phase==="scanning"?process.stdout.write(`\rScanning... ${i.total} files found`):i.phase==="adding"&&process.stdout.write(`\rImporting... ${i.current}/${i.total} files`)});if(process.stdout.write("\r"+" ".repeat(60)+"\r"),n.summary.totalFiles===0){console.log("No files to import");return}let o=[];n.summary.created>0&&o.push(`${n.summary.created} created`),n.summary.unchanged>0&&o.push(`${n.summary.unchanged} unchanged`),n.summary.skipped>0&&o.push(`${n.summary.skipped} skipped`),n.summary.errors>0&&o.push(`${n.summary.errors} errors`),console.log(`Imported ${n.summary.totalFiles} files (${o.join(", ")})`)}function lAr(e,t){let r=$L.join(e,".mod","traces");!t&&Cx.existsSync(r)&&(console.log("Already initialized for local development."),console.log("Run `mod init --force` to reinitialize."),process.exit(0)),Cx.mkdirSync(r,{recursive:!0});let n=$L.join(e,"glassware.toml");Cx.existsSync(n)||(Cx.writeFileSync(n,`# Trace schema configuration
767
767
  # See: https://docs.mod.computer/glassware
768
768
 
769
769
  node_types = ["requirement", "specification", "implementation", "test"]
@@ -782,9 +782,9 @@ to = "specification"
782
782
  attribute = "implementations"
783
783
  from = "test"
784
784
  to = "implementation"
785
- `,"utf-8"),console.log("Created glassware.toml")),fAr(),console.log(""),console.log("Initialized for local spec-driven development:"),console.log(" .mod/traces/ \u2014 Local trace storage (committable to git)"),console.log(" glassware.toml \u2014 Trace schema configuration"),console.log(" .claude/skills/ \u2014 /mod skill for AI agents"),console.log(""),console.log("Get started:"),console.log(" mod trace add <file>:<line> --type=implementation"),console.log(" mod trace list"),console.log(" mod trace report <file>"),process.exit(0)}function fAr(){let e=process.cwd(),t=$L.join(e,".claude","skills","mod"),r=$L.join(t,"references");Cx.existsSync(t)||Cx.mkdirSync(t,{recursive:!0}),Cx.existsSync(r)||Cx.mkdirSync(r,{recursive:!0});let n=$L.join(t,"SKILL.md"),o=$L.join(r,"commands.md");Cx.existsSync(n)||Cx.writeFileSync(n,Reo),Cx.existsSync(o)||Cx.writeFileSync(o,Oeo)}var Reo=`---
785
+ `,"utf-8"),console.log("Created glassware.toml")),fAr(),console.log(""),console.log("Initialized for local spec-driven development:"),console.log(" .mod/traces/ Local trace storage (committable to git)"),console.log(" glassware.toml Trace schema configuration"),console.log(" .claude/skills/ Mod skill for AI agents"),console.log(""),console.log("Get started:"),console.log(" mod trace add <file>:<line> --type=implementation"),console.log(" mod trace list"),console.log(" mod trace report <file>"),process.exit(0)}function fAr(){let e=process.cwd(),t=$L.join(e,".claude","skills","mod"),r=$L.join(t,"references");Cx.existsSync(t)||Cx.mkdirSync(t,{recursive:!0}),Cx.existsSync(r)||Cx.mkdirSync(r,{recursive:!0});let n=$L.join(t,"SKILL.md"),o=$L.join(r,"commands.md");Cx.existsSync(n)||Cx.writeFileSync(n,Reo),Cx.existsSync(o)||Cx.writeFileSync(o,Oeo)}var Reo=`---
786
786
  name: mod
787
- description: Spec-driven development with traceability. Implements specs, adds traces, verifies coverage. Use when implementing from specifications, adding requirement traceability, or checking trace coverage before merging.
787
+ description: Spec-driven development with traceability. Implements specs, adds traces, verifies coverage.
788
788
  ---
789
789
 
790
790
  # Mod: Spec-Driven Development
@@ -795,21 +795,27 @@ Use this skill when the user asks to implement from specs, add traceability, or
795
795
 
796
796
  - "implement this spec"
797
797
  - "implement specs/*.md"
798
- - "/mod implement <file>"
798
+ - "implement <file>"
799
799
  - "add tests for this spec"
800
+ - "add traces"
800
801
  - "check trace coverage"
802
+ - "check coverage"
803
+ - "trace report"
801
804
 
802
805
  ## Workflow
803
806
 
804
807
  ### 1. Setup (if needed)
805
808
 
809
+ Check if workspace exists:
810
+ \`\`\`bash
811
+ mod status
812
+ \`\`\`
813
+
806
814
  If not initialized:
807
815
  \`\`\`bash
808
816
  mod init
809
817
  \`\`\`
810
818
 
811
- This creates \`.mod/traces/\` for local trace storage and \`glassware.toml\` for schema configuration. Traces are stored as JSON files committable to git.
812
-
813
819
  ### 2. Read the Spec
814
820
 
815
821
  Parse the spec file for requirements. Requirements may be marked with glassware annotations or be plain markdown sections.
@@ -864,7 +870,7 @@ Only report completion when both commands exit 0 (all files traced, all requirem
864
870
 
865
871
  | Command | Purpose |
866
872
  |---------|---------|
867
- | \`mod init\` | Initialize for local spec-driven development |
873
+ | \`mod init\` | Initialize workspace |
868
874
  | \`mod trace add <file>:<line> --type=<type> [--link=<id>]\` | Add trace |
869
875
  | \`mod trace link <source> <target>\` | Link two traces |
870
876
  | \`mod trace report <file>\` | Show trace coverage for spec |
@@ -880,6 +886,26 @@ Only report completion when both commands exit 0 (all files traced, all requirem
880
886
  - \`test\` - Code that verifies
881
887
  - \`utility\` - Intentionally untraced helpers
882
888
 
889
+ ## Keeping Specs Up to Date
890
+
891
+ Specs are living documents. As you implement, update the spec to reflect reality:
892
+
893
+ - If behavior diverges from the spec during implementation, update the spec to match.
894
+ - If you add new capabilities not covered by the spec, add spec entries with traces.
895
+ - If requirements change or are removed, update the spec accordingly.
896
+ - Run \`mod trace report <spec-file>\` after spec edits to confirm trace integrity.
897
+
898
+ ## Protecting Traced Content
899
+
900
+ Traces reference spec content by file path, line number, and quoted text. Rewriting or restructuring a spec file can orphan traces - the trace still exists in \`.mod/traces/\` but points to content that moved or disappeared.
901
+
902
+ **When editing spec files:**
903
+ - Avoid bulk-rewriting entire spec files. Prefer targeted edits that preserve line structure.
904
+ - If you must restructure a spec, run \`mod trace report <spec-file>\` before and after to confirm no traces were orphaned.
905
+ - When removing a spec item, delete its traces too: \`mod trace delete <trace-id> --force\`.
906
+ - When moving content to a different line, the trace's \`quotedText\` will still match on re-scan, but line numbers will drift. Re-run \`mod trace report\` to verify the chain is intact.
907
+ - If adding new sections to a spec, add corresponding traces with \`mod trace add\`.
908
+
883
909
  ## Important
884
910
 
885
911
  - Always verify coverage before completing
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@mod-computer/cli",
3
+ "version": "0.2.6",
4
+ "license": "MIT",
5
+ "description": "Mod CLI - Spec-driven development with traceability",
6
+ "bin": {
7
+ "mod": "cli.bundle.js"
8
+ },
9
+ "type": "module",
10
+ "engines": {
11
+ "node": ">=16"
12
+ },
13
+ "keywords": [
14
+ "cli",
15
+ "mod",
16
+ "traceability",
17
+ "spec-driven"
18
+ ]
19
+ }
package/package.json CHANGED
@@ -1,23 +1,75 @@
1
1
  {
2
- "name": "@mod-computer/mod",
3
- "version": "0.2.2",
4
- "license": "MIT",
5
- "description": "Mod CLI - Spec-driven development with traceability",
6
- "bin": {
7
- "mod": "cli.bundle.js"
8
- },
9
- "type": "module",
10
- "engines": {
11
- "node": ">=16"
12
- },
13
- "repository": {
14
- "type": "git",
15
- "url": "https://github.com/modcomputer/mod-monorepo"
16
- },
17
- "keywords": [
18
- "cli",
19
- "mod",
20
- "traceability",
21
- "spec-driven"
22
- ]
23
- }
2
+ "name": "@mod-computer/mod",
3
+ "repository": {
4
+ "type": "git",
5
+ "url": "https://github.com/modcomputer/mod-monorepo"
6
+ },
7
+ "version": "0.2.4",
8
+ "license": "MIT",
9
+ "bin": {
10
+ "mod": "dist/cli.bundle.js"
11
+ },
12
+ "type": "module",
13
+ "engines": {
14
+ "node": ">=16"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc && node fix-imports.js && cp -r source/config/release-profiles dist/config/",
18
+ "build:bundle": "node build-bundle.js",
19
+ "prepublishOnly": "pnpm build:bundle",
20
+ "dev": "tsc --watch",
21
+ "test": "vitest run",
22
+ "package": "node build-for-docs.js"
23
+ },
24
+ "files": [
25
+ "dist/cli.bundle.js",
26
+ "dist/package.json"
27
+ ],
28
+ "dependencies": {
29
+ "@ai-sdk/anthropic": "2.0.0-beta.3",
30
+ "@ai-sdk/openai": "2.0.0-beta.5",
31
+ "@automerge/automerge": "^3.1.2",
32
+ "@automerge/automerge-repo": "^2.3.1",
33
+ "@automerge/automerge-repo-network-websocket": "^2.3.1",
34
+ "@automerge/automerge-repo-storage-nodefs": "^2.3.1",
35
+ "@inquirer/prompts": "^8.1.0",
36
+ "ai": "5.0.0-beta.11",
37
+ "chokidar": "^4.0.3",
38
+ "dotenv": "^17.1.0",
39
+ "ink": "^6.0.1",
40
+ "ink-select-input": "^6.2.0",
41
+ "ink-text-input": "^6.0.0",
42
+ "meow": "^11.0.0",
43
+ "ora": "^9.0.0",
44
+ "react": "19.1.0",
45
+ "react-dom": "19.1.0",
46
+ "zustand": "^5.0.6"
47
+ },
48
+ "devDependencies": {
49
+ "@babel/cli": "^7.21.0",
50
+ "@babel/preset-react": "^7.18.6",
51
+ "@mod/mod-core": "workspace:*",
52
+ "@types/ink": "^2.0.3",
53
+ "@types/ink-select-input": "^3.0.5",
54
+ "@types/ink-text-input": "^2.0.5",
55
+ "@types/meow": "^5.0.0",
56
+ "@types/node": "^24.0.10",
57
+ "@types/react": "^19.1.8",
58
+ "@vdemedes/prettier-config": "^2.0.1",
59
+ "@vitest/coverage-v8": "^1.6.1",
60
+ "chalk": "^5.2.0",
61
+ "esbuild": "^0.24.0",
62
+ "eslint-config-xo-react": "^0.27.0",
63
+ "eslint-plugin-react": "^7.32.2",
64
+ "eslint-plugin-react-hooks": "^4.6.0",
65
+ "fast-check": "^4.5.3",
66
+ "import-jsx": "^5.0.0",
67
+ "ink-testing-library": "^3.0.0",
68
+ "prettier": "^2.8.7",
69
+ "react-test-renderer": "^19.1.0",
70
+ "ts-node": "^10.9.2",
71
+ "typescript": "^5.8.3",
72
+ "vitest": "^1.6.1"
73
+ },
74
+ "prettier": "@vdemedes/prettier-config"
75
+ }
Binary file
package/glassware DELETED
Binary file