@lmctl-ai/lmctl 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2362,16 +2362,14 @@ copy, in that order.`});var b_={};me(b_,{runLs:()=>AR});import{resolve as kc}fro
2362
2362
  [--recursive] [--all] [--format table|teamfile|json] [--json]
2363
2363
  lmctl ls --runs [--limit N] [--format table|json] [--json]
2364
2364
 
2365
- List native provider sessions in scope, or list recent managed runs with --runs.`,bR=50,S_=500});import{spawnSync as Oc}from"node:child_process";import{existsSync as R_,readFileSync as N_}from"node:fs";import{homedir as L_}from"node:os";import{dirname as hB,resolve as Lc}from"node:path";function nn(t,e){return e.code==="ETIMEDOUT"?`${t} did not respond within lint's own spawn timeout (${e.message}) \u2014 this does not mean the provider itself is unreachable, only that this specific check didn't finish in time.`:`${t} could not be run: ${e.message}`}function CR(){let t=Oc("agy",["models"],{encoding:"utf-8",timeout:LR});return{status:t.status,stdout:t.stdout??"",error:t.error}}function OR(t){let e=[];for(let r of t.split(/\r?\n/)){let n=r.trim(),s=n.indexOf(" ");if(s<0)continue;let i=n.slice(s+1).trim();i&&e.push(i)}return e}function DR(t){if(!t&&xc)return xc;let e=s=>(t||(xc=s),s),r;try{r=(t??CR)()}catch(s){return e({available:!1,models:[],reason:s instanceof Error?nn("agy models",s):`agy models could not be run: ${String(s)}`})}if(r.error)return e({available:!1,models:[],reason:nn("agy models",r.error)});if(r.status!==0)return e({available:!1,models:[],reason:`agy models exited with status ${r.status??"unknown"}`});let n=OR(r.stdout);return n.length===0?e({available:!1,models:[],reason:"agy models returned no models"}):e({available:!0,models:n})}function $R(){let t=Oc("codex",["debug","models"],{encoding:"utf-8",timeout:MR});return{status:t.status,stdout:t.stdout??"",error:t.error}}function FR(t){let e;try{e=JSON.parse(t)}catch{return}let r=e.models;if(!Array.isArray(r))return;let n=[];for(let s of r){if(!s||typeof s!="object")continue;let{slug:i,visibility:o}=s;typeof i=="string"&&o==="list"&&n.push(i)}return n}function PR(t){if(!t&&Rc)return Rc;let e=s=>(t||(Rc=s),s),r;try{r=(t??$R)()}catch(s){return e({available:!1,models:[],reason:s instanceof Error?nn("codex debug models",s):`codex debug models could not be run: ${String(s)}`})}if(r.error)return e({available:!1,models:[],reason:nn("codex debug models",r.error)});if(r.status!==0)return e({available:!1,models:[],reason:`codex debug models exited with status ${r.status??"unknown"}`});let n=FR(r.stdout);return n===void 0?e({available:!1,models:[],reason:"codex debug models did not return parseable JSON"}):n.length===0?e({available:!1,models:[],reason:"codex debug models returned no user-visible models"}):e({available:!0,models:n})}function jR(t){let e=Oc("opencode",["models",t],{encoding:"utf-8",timeout:UR});return{status:e.status,stdout:e.stdout??"",error:e.error}}function BR(t,e){if(!e&&Nc.has(t))return Nc.get(t);let r=i=>(e||Nc.set(t,i),i),n;try{n=(e??jR)(t)}catch(i){return r({available:!1,models:[],reason:i instanceof Error?nn(`opencode models ${t}`,i):`opencode models ${t} could not be run: ${String(i)}`})}if(n.error)return r({available:!1,models:[],reason:nn(`opencode models ${t}`,n.error)});if(n.status!==0)return r({available:!1,models:[],reason:`opencode models ${t} exited with status ${n.status??"unknown"}`});let s=n.stdout.split(/\r?\n/).map(i=>i.trim()).filter(i=>i.length>0);return s.length===0?r({available:!1,models:[],reason:`opencode models ${t} returned no models`}):r({available:!0,models:s})}function Dc(t,e={}){let r=[];for(let n of t){let s=n.provider.trim().toLowerCase();if(n.effort&&s!=="opencode"&&s!=="lmplayer"&&s!=="qwen"&&r.push(`[${n.alias}] effort="${n.effort}" is ignored: effort selection is only supported for opencode and lmplayer.`),s==="qwen"&&n.effort&&r.push(`[${n.alias}] effort="${n.effort}" is not a qwen CLI flag: Qwen Code does not expose reasoning-effort selection. Use a thinking/effort model id in model= instead.`),!n.model&&NR.has(s)&&r.push(`[${n.alias}] provider=${s} has no model= \u2014 ${s} will use its own ambient default, which cannot be linted or priced and may not be the intended one. Set model= explicitly.`),s==="copilot"){if(!n.model)continue;r.push(`[${n.alias}] model="${n.model}" is ignored: copilot does not support lmctl model selection.`);continue}if(s==="lmplayer"&&!n.model){r.push(`[${n.alias}] provider=lmplayer has no model= \u2014 lmplayer will choose its own default model, which may not be visible in the teamfile. Set model= explicitly (e.g. github-copilot/gpt-5.5).`);continue}if(s!=="lmplayer"){if(s==="opencode"){if(n.effort&&!n.model&&r.push(`[${n.alias}] effort="${n.effort}" has no effect without model= on an opencode member.`),!n.model){r.push(`[${n.alias}] provider=opencode has no model= \u2014 opencode will choose its own default model, which may not be servable and isn't visible in the teamfile. Set model= explicitly (e.g. github-copilot/gpt-5.5).`);continue}let i=XR(e.opencodeConfigPath);if(!i.available){r.push(`[${n.alias}] model="${n.model}" cannot be verified for opencode: ${i.reason}.`);continue}if(i.models.includes(n.model)||/^github-copilot\/(gpt|gemini|o[0-9])/.test(n.model))continue;if(!n.model.includes("/")){let l=i.models.filter(c=>c.endsWith(`/${n.model}`));r.push(l.length>0?`[${n.alias}] model="${n.model}" has no provider prefix \u2014 opencode model ids are <provider>/<model>. Your opencode.json declares it as ${l.join(" / ")}; set model=${l[0]}.`:`[${n.alias}] model="${n.model}" has no provider prefix \u2014 opencode model ids are <provider>/<model> (e.g. github-copilot/${n.model}). Declare it in ~/.config/opencode/opencode.json (see https://lmctl.com/examples/opencode.json).`);continue}let o=n.model.slice(0,n.model.indexOf("/")),a=BR(o,e.opencodeModelsRunner);if(a.available&&a.models.includes(n.model))continue;if(a.available&&!a.models.includes(n.model)){r.push(`[${n.alias}] model="${n.model}" was not found in opencode's LOCALLY-VISIBLE live catalog for "${o}" (checked \`opencode models ${o}\`) -- this is unresolved, not a confirmed unavailability: it could mean opencode genuinely doesn't serve this model, OR that your local catalog/auth for "${o}" is stale/incomplete. Declaring it in opencode.json may or may not help; if it doesn't, check whether a non-opencode provider serves this model directly (e.g. \`lmctl lint\` with model= against provider=codex/claude/gemini/agy).`);continue}r.push(`[${n.alias}] model="${n.model}" is not declared in your opencode.json. opencode resolves github-copilot GPT/Gemini config-free, but copilot Claude and custom providers must be declared \u2014 copy https://lmctl.com/examples/opencode.json to ~/.config/opencode/opencode.json and adjust for your account.`);continue}if(s==="kimi"){if(!n.model)continue;let i=WR(e.kimiConfigPath);i.available?i.models.includes(n.model)||r.push(`[${n.alias}] model="${n.model}" is not declared in Kimi's [models] config. Valid configured models: ${is(i.models)}`):r.push(`[${n.alias}] model="${n.model}" cannot be verified for kimi: ${i.reason}.`);continue}if(s==="agy"){if(!n.model)continue;let i=DR(e.agyModelsRunner);i.available?i.models.includes(n.model)||r.push(`[${n.alias}] model="${n.model}" is not in agy's live model catalog (\`agy models\`). Valid models: ${is(i.models)}`):r.push(`[${n.alias}] model="${n.model}" cannot be verified for agy: ${i.reason}.`);continue}if(s==="dsh"){if(!n.model||k_.includes(n.model))continue;A_.includes(n.model)||r.push(`[${n.alias}] model="${n.model}" is not in dsh's built-in catalog. Valid pinned models: ${is(A_)}. Valid server-resolved aliases (DeepSeek resolves these to its current model server-side -- pinning to a specific version instead is a DOWNGRADE, not a safer choice): ${is(k_)}. The catalog is patchable via dsh profile overlays \u2014 a model added that way warns here even though it may resolve.`);continue}if(s==="hermes"){if(!n.model)continue;let i=n.model.indexOf(":");(i<=0||i===n.model.length-1)&&r.push(`[${n.alias}] model="${n.model}" should use <provider>:<model> from Hermes's ACP catalog. lmctl checks the shape only; provider and model availability are resolved by Hermes.`);continue}if(s!=="pi"){if(s==="codex"){if(!n.model)continue;n.model===x_&&r.push(`[${n.alias}] model="${x_}" is banned as a build model (operator, 2026-09-06) -- it's a fast/cheap classification model, not intended for build work, and there's no cost reason left to reach for it. Use model=gpt-5.6-sol (or leave model= unset) for build work; if this member genuinely does non-build/classification work, this warning can be ignored.`);let i=PR(e.codexModelsRunner);if(!i.available){r.push(`[${n.alias}] model="${n.model}" cannot be verified for codex: ${i.reason}.`);continue}i.models.includes(n.model)||r.push(`[${n.alias}] model="${n.model}" is not in codex's live model catalog (\`codex debug models\`). Valid models: ${is(i.models)}`);continue}n.model&&(C_.has(s)||r.push(`[${n.alias}] model="${n.model}" cannot be verified for provider "${s}": no model validation path exists for this provider.`))}}}return r}async function O_(t,e={}){let r=t.filter(i=>i.model!==void 0&&C_.has(i.provider.trim().toLowerCase()));if(r.length===0)return[];let n=e.snapshot??await Yi();if(n.catalog===null)return[];let s=[];for(let i of r){let o=i.provider.trim().toLowerCase(),a=i.model,l=Zi(o,a,n.catalog,n.aliases);if(l.kind==="unknown"){let c=T_(n.catalog,o)??0;s.push(`[${i.alias}] model "${a}" is unknown to the lmprice catalog for provider "${o}" (${c} known) -- not necessarily wrong: may be a model too new for lmprice to have published yet, or a typo. A matching lmprice catalog/alias entry would resolve this; lint cannot otherwise confirm whether ${o} accepts it.`)}else l.kind==="known-unpriced"&&s.push(`[${i.alias}] model "${a}" is recognized in the lmprice catalog but has no price information \u2014 cost will be unavailable`);if(o==="claude"&&(l.kind==="priced"||l.kind==="known-unpriced"||l.kind==="estimated")&&RR.test(a)){let c=a.replace(/(\d)\.(?=\d)/g,"$1-");s.push(`[${i.alias}] model="${a}" matches the lmprice catalog only through its OpenRouter-style spelling ("." between version numbers); claude's own CLI expects "-" there and does an exact match, so it may reject this string verbatim even though lint recognizes it. If seed/chat fails with "issue with the selected model," try model="${c}" instead.`)}}return s}function WR(t){let e=process.env.KIMI_CODE_HOME||Lc(L_(),".kimi-code"),r=t??Lc(e,"config.toml");if(!R_(r))return{available:!1,models:[],reason:`${r} not found`};let n;try{n=N_(r,"utf-8")}catch(i){return{available:!1,models:[],reason:`${r} could not be read: ${i instanceof Error?i.message:String(i)}`}}let s=new Set;for(let i of n.split(/\r?\n/)){if(/^\s*\[/.test(i)&&!/^\s*\[\s*(.+?)\s*\]\s*(?:#.*)?$/.test(i))return{available:!1,models:[],reason:`${r} has an invalid TOML table header`};let o=/^\s*\[\s*(.+?)\s*\]\s*(?:#.*)?$/.exec(i);if(!o)continue;let a=HR(o[1]);if(o[1].trim().startsWith("models")&&a===null)return{available:!1,models:[],reason:`${r} has an invalid [models] table header`};a?.[0]==="models"&&a.length>=2&&s.add(a[1])}return s.size===0?{available:!1,models:[],reason:`${r} has no [models] entries`}:{available:!0,models:[...s].sort()}}function HR(t){let e=[],r=0;for(;r<t.length;){for(;/\s/.test(t[r]??"");)r++;if(r>=t.length)break;let n="",s=t[r]==='"'||t[r]==="'"?t[r++]:void 0;if(s){for(;r<t.length&&t[r]!==s;)s==='"'&&t[r]==="\\"&&r+1<t.length&&r++,n+=t[r++];if(t[r]!==s)return null;r++}else{for(;r<t.length&&t[r]!==".";)n+=t[r++];n=n.trim()}if(!n)return null;for(e.push(n);/\s/.test(t[r]??"");)r++;if(r>=t.length)break;if(t[r]!==".")return null;r++}return e.length>0?e:null}function XR(t){let e=t??Lc(L_(),".config","opencode","opencode.json");if(!R_(e))return{available:!0,models:[]};let r;try{r=JSON.parse(N_(e,"utf-8"))}catch(n){return{available:!1,models:[],reason:`${e} could not be parsed: ${n instanceof Error?n.message:String(n)}`}}return{available:!0,models:qR(r)}}function qR(t){if(!Cc(t))return[];let e=to(t.provider)??to(t.providers);if(!e)return[];let r=new Set;for(let[n,s]of Object.entries(e)){if(!Cc(s))continue;let i=to(s.models)??to(s.model);if(i)for(let o of Object.keys(i))r.add(o.startsWith(`${n}/`)?o:`${n}/${o}`)}return[...r].sort()}function to(t){return Cc(t)?t:null}function Cc(t){return t!==null&&typeof t=="object"&&!Array.isArray(t)}function is(t){return t.length>0?t.join(", "):"(none)"}var C_,RR,NR,A_,k_,x_,LR,xc,MR,Rc,UR,Nc,D_=k(()=>{"use strict";vc();Ac();C_=new Set(Object.keys(Qi).filter(t=>t!=="codex"&&t!=="kimi")),RR=/\d\.(?=\d)/,NR=new Set(["claude","codex","gemini","qwen","kimi","agy"]),A_=["deepseek-v4-flash","deepseek-v4-pro","deepseek-v4-flash-vision-exp"],k_=["deepseek-flash","deepseek-pro"],x_="gpt-5.3-codex-spark",LR=15e3;xc=null;MR=5e3;Rc=null;UR=5e3;Nc=new Map});import{homedir as GR}from"node:os";import{basename as JR}from"node:path";function M_(t,e){let r=new Set;return{text:t.replace(KR,(s,i)=>{let o=i.trim(),a=VR[o];return a===void 0?(r.add(o),s):a(e)}),unknownNames:[...r].sort()}}function $_(t){return{workdir:t.workdir,teamfile:t.teamfile,date:YR(t.date)}}function YR(t){return t.toISOString().slice(0,10)}var VR,KR,F_=k(()=>{"use strict";VR={workdir:t=>t.workdir,teamfile:t=>t.teamfile,teamname:t=>{let e=JR(t.teamfile);return e.endsWith(".lmctl")?e.slice(0,-6):e},home:()=>GR(),date:t=>t.date},KR=/{{\s*([A-Za-z0-9_-]+)\s*}}/g});var Xc={};me(Xc,{GEMINI_PROVIDER_NOTICE:()=>X_,_setAgyModelsRunnerForTests:()=>nN,_setCatalogSnapshotForTests:()=>rN,_setCodexModelsRunnerForTests:()=>sN,_setLintOpenCodeConfigProbeForTests:()=>aN,_setLintSessiondirFsOpsForTests:()=>oN,_setTeamfileSeedForTests:()=>iN,buildRootTeamRegistrationRow:()=>K_,registerRootTeam:()=>Y_,runLint:()=>Wc,runSeed:()=>Hc,sha256Text:()=>V_,validateMemberSpec:()=>xt});import{createHash as zR}from"node:crypto";import{accessSync as QR,constants as P_,existsSync as ZR,readFileSync as W_,realpathSync as $c,statSync as eN,writeFileSync as U_}from"node:fs";import{dirname as wr,resolve as H_}from"node:path";function rN(t){Pc=t??void 0}function J_(t){return O_(t,Pc!==void 0?{snapshot:Pc}:{})}function nN(t){jc=t??void 0}function sN(t){Bc=t??void 0}function iN(t){q_=t??(async e=>{let{seed:r}=await Promise.resolve().then(()=>(bt(),Bl)),n=await r(e);return n.success?{sessionId:n.sessionId,warnings:n.warnings}:{error:n.error??"provider seed failed"}})}function oN(t){Tr=t===null?Fc:{...Fc,...t}}function aN(t){G_=t??Cn}async function lN(t,e,r){let n=t.filter(i=>i.teamfile===void 0&&i.provider.trim().toLowerCase()==="opencode"&&i.model!==void 0&&i.model.length>0);return(await Promise.all(n.map(async i=>({member:i,result:await G_({cwd:i.sessiondir??e,model:i.model,...i.effort!==void 0?{effort:i.effort}:{},...r!==void 0?{env:r}:{}})})))).flatMap(({member:i,result:o})=>o.status==="rejected"?[`[${i.alias}] ${o.option}="${o.value}" rejected by opencode: ${o.error}`]:[])}function cN(t,e){let r=wr(e),n=new Set,s=[];for(let i of t){if(i.teamfile!==void 0)continue;let o=i.sessiondir??r,a=H_(o);if(n.has(a))continue;let l=dN(a);l!==void 0&&(n.add(a),s.push(`${i.alias}: sessiondir ${a} is not writable (seed would fail: ${l}) \u2014 fix permissions or set a writable sessiondir`))}return s}function dN(t){try{if(Tr.existsSync(t)){if(!Tr.statSync(t).isDirectory())return"ENOTDIR";Tr.accessSync(t,P_.W_OK);return}let e=mN(t);if(e===void 0)return;if(!Tr.statSync(e).isDirectory())return"ENOTDIR";Tr.accessSync(e,P_.W_OK);return}catch(e){return fN(e)??"permission check failed"}}function uN(t){let e=H_(t);try{return $c(e)}catch{return e}}function mN(t){let e=t;for(;;){try{if(Tr.existsSync(e))return e}catch{return}let r=wr(e);if(r===e)return;e=r}}function fN(t){if(typeof t=="object"&&t!==null&&"code"in t){let e=t.code;if(typeof e=="string"&&e.length>0)return e}}async function Wc(t,e={stdout:process.stdout,stderr:process.stderr}){if(t[0]==="--help"||t[0]==="-h"||t[0]==="help")return e.stdout.write(tN),0;let r=ie(t,[],["--strict"]);if(r.error!==void 0)return e.stderr.write(`error: ${r.error}
2366
- `),2;if(r.positionals.length!==1)return e.stderr.write(`usage: lmctl lint [--strict] <teamfile.lmctl>
2367
- `),2;let n=r.flags["--strict"]===!0,s=r.positionals[0],i;try{i=await C(e,_=>pe(s,_))}catch(_){return sn(e,`${s}: ${_ instanceof Error?_.message:String(_)}`)}let o;try{o=Ie(i)}catch(_){return sn(e,`${s}: ${_ instanceof Error?_.message:String(_)}`)}let a=Fo(o,wr(i));e.stderr.write(`notice: checking teamfile and models ...
2368
- `);let l=Dc(o.members,{agyModelsRunner:jc,codexModelsRunner:Bc}),c=[...l],u=await J_(o.members);l.push(...u);let d=W_(i,"utf8"),f=M_(d,$_({workdir:wr(i),teamfile:i,date:new Date})).unknownNames;if(f.length>0){let _=`unknown template placeholder${f.length===1?"":"s"}: ${f.map(p=>`{{${p}}}`).join(", ")}`;l.push(_),c.push(_)}let m=ga(o.members,wr(i));l.push(...m),c.push(...m);let g=cN(o.members,i);l.push(...g),c.push(...g);let h=await lN(o.members,wr(i),e.env);l.push(...h),c.push(...h);for(let _ of a)e.stderr.write(`error: ${_.field}: ${_.message}
2369
- `);for(let _ of o.members)if(Uc([_],e),_.session!==void 0&&_.session.length<os){let p=`${_.alias}: sessionid is shorter than ${os} chars; run lmctl seed to refresh it`;l.push(p),c.push(p)}for(let _ of l)e.stderr.write(`warning: ${_}
2370
- `);if(e.stderr.write(`notice: ${a.length} error${a.length===1?"":"s"}, ${l.length} warning${l.length===1?"":"s"}
2371
- `),a.length>0)return 1;if(n&&c.length>0){e.stderr.write(`notice: --strict: promoting ${c.length} warning${c.length===1?"":"s"} to failure (excludes ${u.length} catalog-model warning${u.length===1?"":"s"}, which never promote):
2372
- `);for(let _ of c)e.stderr.write(` - ${_}
2373
- `);return 1}return e.stdout.write(`ok
2374
- `),0}async function Hc(t,e={stdout:process.stdout,stderr:process.stderr}){if(t[0]==="--help"||t[0]==="-h"||t[0]==="help")return e.stdout.write(Mc),0;let r=ie(t,[],[]);return r.error!==void 0?B_(e,r.error,Mc):r.positionals.length!==1?B_(e,"expected <teamfile.lmctl>",Mc):pN(r.positionals[0],e)}async function pN(t,e){let r;try{r=await C(e,h=>pe(t,h))}catch(h){return sn(e,`${t}: ${h instanceof Error?h.message:String(h)}`)}let n;try{n=$c(r)}catch(h){return sn(e,`${t}: ${h instanceof Error?h.message:String(h)}`)}let s=wr(r),i;try{i=Ie(r)}catch(h){return sn(e,`${t}: ${h instanceof Error?h.message:String(h)}`)}let a=W_(r,"utf8").split(`
2365
+ List native provider sessions in scope, or list recent managed runs with --runs.`,bR=50,S_=500});import{spawnSync as Oc}from"node:child_process";import{existsSync as R_,readFileSync as N_}from"node:fs";import{homedir as L_}from"node:os";import{dirname as hB,resolve as Lc}from"node:path";function nn(t,e){return e.code==="ETIMEDOUT"?`${t} did not respond within lint's own spawn timeout (${e.message}) \u2014 this does not mean the provider itself is unreachable, only that this specific check didn't finish in time.`:`${t} could not be run: ${e.message}`}function CR(){let t=Oc("agy",["models"],{encoding:"utf-8",timeout:LR});return{status:t.status,stdout:t.stdout??"",error:t.error}}function OR(t){let e=[];for(let r of t.split(/\r?\n/)){let n=r.trim(),s=n.indexOf(" ");if(s<0)continue;let i=n.slice(s+1).trim();i&&e.push(i)}return e}function DR(t){if(!t&&xc)return xc;let e=s=>(t||(xc=s),s),r;try{r=(t??CR)()}catch(s){return e({available:!1,models:[],reason:s instanceof Error?nn("agy models",s):`agy models could not be run: ${String(s)}`})}if(r.error)return e({available:!1,models:[],reason:nn("agy models",r.error)});if(r.status!==0)return e({available:!1,models:[],reason:`agy models exited with status ${r.status??"unknown"}`});let n=OR(r.stdout);return n.length===0?e({available:!1,models:[],reason:"agy models returned no models"}):e({available:!0,models:n})}function $R(){let t=Oc("codex",["debug","models"],{encoding:"utf-8",timeout:MR});return{status:t.status,stdout:t.stdout??"",error:t.error}}function FR(t){let e;try{e=JSON.parse(t)}catch{return}let r=e.models;if(!Array.isArray(r))return;let n=[];for(let s of r){if(!s||typeof s!="object")continue;let{slug:i,visibility:o}=s;typeof i=="string"&&o==="list"&&n.push(i)}return n}function PR(t){if(!t&&Rc)return Rc;let e=s=>(t||(Rc=s),s),r;try{r=(t??$R)()}catch(s){return e({available:!1,models:[],reason:s instanceof Error?nn("codex debug models",s):`codex debug models could not be run: ${String(s)}`})}if(r.error)return e({available:!1,models:[],reason:nn("codex debug models",r.error)});if(r.status!==0)return e({available:!1,models:[],reason:`codex debug models exited with status ${r.status??"unknown"}`});let n=FR(r.stdout);return n===void 0?e({available:!1,models:[],reason:"codex debug models did not return parseable JSON"}):n.length===0?e({available:!1,models:[],reason:"codex debug models returned no user-visible models"}):e({available:!0,models:n})}function jR(t){let e=Oc("opencode",["models",t],{encoding:"utf-8",timeout:UR});return{status:e.status,stdout:e.stdout??"",error:e.error}}function BR(t,e){if(!e&&Nc.has(t))return Nc.get(t);let r=i=>(e||Nc.set(t,i),i),n;try{n=(e??jR)(t)}catch(i){return r({available:!1,models:[],reason:i instanceof Error?nn(`opencode models ${t}`,i):`opencode models ${t} could not be run: ${String(i)}`})}if(n.error)return r({available:!1,models:[],reason:nn(`opencode models ${t}`,n.error)});if(n.status!==0)return r({available:!1,models:[],reason:`opencode models ${t} exited with status ${n.status??"unknown"}`});let s=n.stdout.split(/\r?\n/).map(i=>i.trim()).filter(i=>i.length>0);return s.length===0?r({available:!1,models:[],reason:`opencode models ${t} returned no models`}):r({available:!0,models:s})}function Dc(t,e={}){let r=[];for(let n of t){let s=n.provider.trim().toLowerCase();if(n.effort&&s!=="opencode"&&s!=="lmplayer"&&s!=="qwen"&&r.push(`[${n.alias}] effort="${n.effort}" is ignored: effort selection is only supported for opencode and lmplayer.`),s==="qwen"&&n.effort&&r.push(`[${n.alias}] effort="${n.effort}" is not a qwen CLI flag: Qwen Code does not expose reasoning-effort selection. Use a thinking/effort model id in model= instead.`),!n.model&&NR.has(s)&&r.push(`[${n.alias}] provider=${s} has no model= \u2014 ${s} will use its own ambient default, which cannot be linted or priced and may not be the intended one. Set model= explicitly.`),s==="copilot"){if(!n.model)continue;r.push(`[${n.alias}] model="${n.model}" is ignored: copilot does not support lmctl model selection.`);continue}if(s==="lmplayer"&&!n.model){r.push(`[${n.alias}] provider=lmplayer has no model= \u2014 lmplayer will choose its own default model, which may not be visible in the teamfile. Set model= explicitly (e.g. github-copilot/gpt-5.5).`);continue}if(s!=="lmplayer"){if(s==="opencode"){if(n.effort&&!n.model&&r.push(`[${n.alias}] effort="${n.effort}" has no effect without model= on an opencode member.`),!n.model){r.push(`[${n.alias}] provider=opencode has no model= \u2014 opencode will choose its own default model, which may not be servable and isn't visible in the teamfile. Set model= explicitly (e.g. github-copilot/gpt-5.5).`);continue}let i=XR(e.opencodeConfigPath);if(!i.available){r.push(`[${n.alias}] model="${n.model}" cannot be verified for opencode: ${i.reason}.`);continue}if(i.models.includes(n.model)||/^github-copilot\/(gpt|gemini|o[0-9])/.test(n.model))continue;if(!n.model.includes("/")){let l=i.models.filter(c=>c.endsWith(`/${n.model}`));r.push(l.length>0?`[${n.alias}] model="${n.model}" has no provider prefix \u2014 opencode model ids are <provider>/<model>. Your opencode.json declares it as ${l.join(" / ")}; set model=${l[0]}.`:`[${n.alias}] model="${n.model}" has no provider prefix \u2014 opencode model ids are <provider>/<model> (e.g. github-copilot/${n.model}). Declare it in ~/.config/opencode/opencode.json (see https://lmctl.com/examples/opencode.json).`);continue}let o=n.model.slice(0,n.model.indexOf("/")),a=BR(o,e.opencodeModelsRunner);if(a.available&&a.models.includes(n.model))continue;if(a.available&&!a.models.includes(n.model)){r.push(`[${n.alias}] model="${n.model}" was not found in opencode's LOCALLY-VISIBLE live catalog for "${o}" (checked \`opencode models ${o}\`) -- this is unresolved, not a confirmed unavailability: it could mean opencode genuinely doesn't serve this model, OR that your local catalog/auth for "${o}" is stale/incomplete. Declaring it in opencode.json may or may not help; if it doesn't, check whether a non-opencode provider serves this model directly (e.g. \`lmctl lint\` with model= against provider=codex/claude/gemini/agy).`);continue}r.push(`[${n.alias}] model="${n.model}" is not declared in your opencode.json. opencode resolves github-copilot GPT/Gemini config-free, but copilot Claude and custom providers must be declared \u2014 copy https://lmctl.com/examples/opencode.json to ~/.config/opencode/opencode.json and adjust for your account.`);continue}if(s==="kimi"){if(!n.model)continue;let i=WR(e.kimiConfigPath);i.available?i.models.includes(n.model)||r.push(`[${n.alias}] model="${n.model}" is not declared in Kimi's [models] config. Valid configured models: ${is(i.models)}`):r.push(`[${n.alias}] model="${n.model}" cannot be verified for kimi: ${i.reason}.`);continue}if(s==="agy"){if(!n.model)continue;let i=DR(e.agyModelsRunner);i.available?i.models.includes(n.model)||r.push(`[${n.alias}] model="${n.model}" is not in agy's live model catalog (\`agy models\`). Valid models: ${is(i.models)}`):r.push(`[${n.alias}] model="${n.model}" cannot be verified for agy: ${i.reason}.`);continue}if(s==="dsh"){if(!n.model||k_.includes(n.model))continue;A_.includes(n.model)||r.push(`[${n.alias}] model="${n.model}" is not in dsh's built-in catalog. Valid pinned models: ${is(A_)}. Valid server-resolved aliases (DeepSeek resolves these to its current model server-side -- pinning to a specific version instead is a DOWNGRADE, not a safer choice): ${is(k_)}. The catalog is patchable via dsh profile overlays \u2014 a model added that way warns here even though it may resolve.`);continue}if(s==="hermes"){if(!n.model)continue;let i=n.model.indexOf(":");(i<=0||i===n.model.length-1)&&r.push(`[${n.alias}] model="${n.model}" should use <provider>:<model> from Hermes's ACP catalog. lmctl checks the shape only; provider and model availability are resolved by Hermes.`);continue}if(s!=="pi"){if(s==="codex"){if(!n.model)continue;n.model===x_&&r.push(`[${n.alias}] model="${x_}" is banned as a build model (operator, 2026-09-06) -- it's a fast/cheap classification model, not intended for build work, and there's no cost reason left to reach for it. Use model=gpt-5.6-sol (or leave model= unset) for build work; if this member genuinely does non-build/classification work, this warning can be ignored.`);let i=PR(e.codexModelsRunner);if(!i.available){r.push(`[${n.alias}] model="${n.model}" cannot be verified for codex: ${i.reason}.`);continue}i.models.includes(n.model)||r.push(`[${n.alias}] model="${n.model}" is not in codex's live model catalog (\`codex debug models\`). Valid models: ${is(i.models)}`);continue}n.model&&(C_.has(s)||r.push(`[${n.alias}] model="${n.model}" cannot be verified for provider "${s}": no model validation path exists for this provider.`))}}}return r}async function O_(t,e={}){let r=t.filter(i=>i.model!==void 0&&C_.has(i.provider.trim().toLowerCase()));if(r.length===0)return[];let n=e.snapshot??await Yi();if(n.catalog===null)return[];let s=[];for(let i of r){let o=i.provider.trim().toLowerCase(),a=i.model,l=Zi(o,a,n.catalog,n.aliases);if(l.kind==="unknown"){let c=T_(n.catalog,o)??0;s.push(`[${i.alias}] model "${a}" is unknown to the lmprice catalog for provider "${o}" (${c} known) -- not necessarily wrong: may be a model too new for lmprice to have published yet, or a typo. A matching lmprice catalog/alias entry would resolve this; lint cannot otherwise confirm whether ${o} accepts it.`)}else l.kind==="known-unpriced"&&s.push(`[${i.alias}] model "${a}" is recognized in the lmprice catalog but has no price information \u2014 cost will be unavailable`);if(o==="claude"&&(l.kind==="priced"||l.kind==="known-unpriced"||l.kind==="estimated")&&RR.test(a)){let c=a.replace(/(\d)\.(?=\d)/g,"$1-");s.push(`[${i.alias}] model="${a}" matches the lmprice catalog only through its OpenRouter-style spelling ("." between version numbers); claude's own CLI expects "-" there and does an exact match, so it may reject this string verbatim even though lint recognizes it. If seed/chat fails with "issue with the selected model," try model="${c}" instead.`)}}return s}function WR(t){let e=process.env.KIMI_CODE_HOME||Lc(L_(),".kimi-code"),r=t??Lc(e,"config.toml");if(!R_(r))return{available:!1,models:[],reason:`${r} not found`};let n;try{n=N_(r,"utf-8")}catch(i){return{available:!1,models:[],reason:`${r} could not be read: ${i instanceof Error?i.message:String(i)}`}}let s=new Set;for(let i of n.split(/\r?\n/)){if(/^\s*\[/.test(i)&&!/^\s*\[\s*(.+?)\s*\]\s*(?:#.*)?$/.test(i))return{available:!1,models:[],reason:`${r} has an invalid TOML table header`};let o=/^\s*\[\s*(.+?)\s*\]\s*(?:#.*)?$/.exec(i);if(!o)continue;let a=HR(o[1]);if(o[1].trim().startsWith("models")&&a===null)return{available:!1,models:[],reason:`${r} has an invalid [models] table header`};a?.[0]==="models"&&a.length>=2&&s.add(a[1])}return s.size===0?{available:!1,models:[],reason:`${r} has no [models] entries`}:{available:!0,models:[...s].sort()}}function HR(t){let e=[],r=0;for(;r<t.length;){for(;/\s/.test(t[r]??"");)r++;if(r>=t.length)break;let n="",s=t[r]==='"'||t[r]==="'"?t[r++]:void 0;if(s){for(;r<t.length&&t[r]!==s;)s==='"'&&t[r]==="\\"&&r+1<t.length&&r++,n+=t[r++];if(t[r]!==s)return null;r++}else{for(;r<t.length&&t[r]!==".";)n+=t[r++];n=n.trim()}if(!n)return null;for(e.push(n);/\s/.test(t[r]??"");)r++;if(r>=t.length)break;if(t[r]!==".")return null;r++}return e.length>0?e:null}function XR(t){let e=t??Lc(L_(),".config","opencode","opencode.json");if(!R_(e))return{available:!0,models:[]};let r;try{r=JSON.parse(N_(e,"utf-8"))}catch(n){return{available:!1,models:[],reason:`${e} could not be parsed: ${n instanceof Error?n.message:String(n)}`}}return{available:!0,models:qR(r)}}function qR(t){if(!Cc(t))return[];let e=to(t.provider)??to(t.providers);if(!e)return[];let r=new Set;for(let[n,s]of Object.entries(e)){if(!Cc(s))continue;let i=to(s.models)??to(s.model);if(i)for(let o of Object.keys(i))r.add(o.startsWith(`${n}/`)?o:`${n}/${o}`)}return[...r].sort()}function to(t){return Cc(t)?t:null}function Cc(t){return t!==null&&typeof t=="object"&&!Array.isArray(t)}function is(t){return t.length>0?t.join(", "):"(none)"}var C_,RR,NR,A_,k_,x_,LR,xc,MR,Rc,UR,Nc,D_=k(()=>{"use strict";vc();Ac();C_=new Set(Object.keys(Qi).filter(t=>t!=="codex"&&t!=="kimi")),RR=/\d\.(?=\d)/,NR=new Set(["claude","codex","gemini","qwen","kimi","agy"]),A_=["deepseek-v4-flash","deepseek-v4-pro","deepseek-v4-flash-vision-exp"],k_=["deepseek-flash","deepseek-pro"],x_="gpt-5.3-codex-spark",LR=15e3;xc=null;MR=5e3;Rc=null;UR=5e3;Nc=new Map});import{homedir as GR}from"node:os";import{basename as JR}from"node:path";function M_(t,e){let r=new Set;return{text:t.replace(KR,(s,i)=>{let o=i.trim(),a=VR[o];return a===void 0?(r.add(o),s):a(e)}),unknownNames:[...r].sort()}}function $_(t){return{workdir:t.workdir,teamfile:t.teamfile,date:YR(t.date)}}function YR(t){return t.toISOString().slice(0,10)}var VR,KR,F_=k(()=>{"use strict";VR={workdir:t=>t.workdir,teamfile:t=>t.teamfile,teamname:t=>{let e=JR(t.teamfile);return e.endsWith(".lmctl")?e.slice(0,-6):e},home:()=>GR(),date:t=>t.date},KR=/{{\s*([A-Za-z0-9_-]+)\s*}}/g});var Xc={};me(Xc,{GEMINI_PROVIDER_NOTICE:()=>X_,_setAgyModelsRunnerForTests:()=>nN,_setCatalogSnapshotForTests:()=>rN,_setCodexModelsRunnerForTests:()=>sN,_setLintOpenCodeConfigProbeForTests:()=>aN,_setLintSessiondirFsOpsForTests:()=>oN,_setTeamfileSeedForTests:()=>iN,buildRootTeamRegistrationRow:()=>K_,registerRootTeam:()=>Y_,runLint:()=>Wc,runSeed:()=>Hc,sha256Text:()=>V_,validateMemberSpec:()=>xt});import{createHash as zR}from"node:crypto";import{accessSync as QR,constants as P_,existsSync as ZR,readFileSync as W_,realpathSync as $c,statSync as eN,writeFileSync as U_}from"node:fs";import{dirname as wr,resolve as H_}from"node:path";function rN(t){Pc=t??void 0}function J_(t){return O_(t,Pc!==void 0?{snapshot:Pc}:{})}function nN(t){jc=t??void 0}function sN(t){Bc=t??void 0}function iN(t){q_=t??(async e=>{let{seed:r}=await Promise.resolve().then(()=>(bt(),Bl)),n=await r(e);return n.success?{sessionId:n.sessionId,warnings:n.warnings}:{error:n.error??"provider seed failed"}})}function oN(t){Tr=t===null?Fc:{...Fc,...t}}function aN(t){G_=t??Cn}async function lN(t,e,r){let n=t.filter(i=>i.teamfile===void 0&&i.provider.trim().toLowerCase()==="opencode"&&i.model!==void 0&&i.model.length>0);return(await Promise.all(n.map(async i=>({member:i,result:await G_({cwd:i.sessiondir??e,model:i.model,...i.effort!==void 0?{effort:i.effort}:{},...r!==void 0?{env:r}:{}})})))).flatMap(({member:i,result:o})=>o.status==="rejected"?[`[${i.alias}] ${o.option}="${o.value}" rejected by opencode: ${o.error}`]:[])}function cN(t,e){let r=wr(e),n=new Set,s=[];for(let i of t){if(i.teamfile!==void 0)continue;let o=i.sessiondir??r,a=H_(o);if(n.has(a))continue;let l=dN(a);l!==void 0&&(n.add(a),s.push(`${i.alias}: sessiondir ${a} is not writable (seed would fail: ${l}) \u2014 fix permissions or set a writable sessiondir`))}return s}function dN(t){try{if(Tr.existsSync(t)){if(!Tr.statSync(t).isDirectory())return"ENOTDIR";Tr.accessSync(t,P_.W_OK);return}let e=mN(t);if(e===void 0)return;if(!Tr.statSync(e).isDirectory())return"ENOTDIR";Tr.accessSync(e,P_.W_OK);return}catch(e){return fN(e)??"permission check failed"}}function uN(t){let e=H_(t);try{return $c(e)}catch{return e}}function mN(t){let e=t;for(;;){try{if(Tr.existsSync(e))return e}catch{return}let r=wr(e);if(r===e)return;e=r}}function fN(t){if(typeof t=="object"&&t!==null&&"code"in t){let e=t.code;if(typeof e=="string"&&e.length>0)return e}}async function Wc(t,e={stdout:process.stdout,stderr:process.stderr}){if(t[0]==="--help"||t[0]==="-h"||t[0]==="help")return e.stdout.write(tN),0;let r=ie(t,[],[]);if(r.error!==void 0)return e.stderr.write(`error: ${r.error}
2366
+ `),2;if(r.positionals.length!==1)return e.stderr.write(`usage: lmctl lint <teamfile.lmctl>
2367
+ `),2;let n=r.positionals[0],s;try{s=await C(e,u=>pe(n,u))}catch(u){return sn(e,`${n}: ${u instanceof Error?u.message:String(u)}`)}let i;try{i=Ie(s)}catch(u){return sn(e,`${n}: ${u instanceof Error?u.message:String(u)}`)}let o=Fo(i,wr(s));e.stderr.write(`notice: checking teamfile and models ...
2368
+ `);let a=Dc(i.members,{agyModelsRunner:jc,codexModelsRunner:Bc});a.push(...await J_(i.members));let l=W_(s,"utf8"),c=M_(l,$_({workdir:wr(s),teamfile:s,date:new Date})).unknownNames;c.length>0&&a.push(`unknown template placeholder${c.length===1?"":"s"}: ${c.map(u=>`{{${u}}}`).join(", ")}`),a.push(...ga(i.members,wr(s))),a.push(...cN(i.members,s)),a.push(...await lN(i.members,wr(s),e.env));for(let u of o)e.stderr.write(`error: ${u.field}: ${u.message}
2369
+ `);for(let u of i.members)Uc([u],e),u.session!==void 0&&u.session.length<os&&a.push(`${u.alias}: sessionid is shorter than ${os} chars; run lmctl seed to refresh it`);for(let u of a)e.stderr.write(`warning: ${u}
2370
+ `);return e.stderr.write(`notice: ${o.length} error${o.length===1?"":"s"}, ${a.length} warning${a.length===1?"":"s"}
2371
+ `),o.length>0?1:(e.stdout.write(`ok
2372
+ `),0)}async function Hc(t,e={stdout:process.stdout,stderr:process.stderr}){if(t[0]==="--help"||t[0]==="-h"||t[0]==="help")return e.stdout.write(Mc),0;let r=ie(t,[],[]);return r.error!==void 0?B_(e,r.error,Mc):r.positionals.length!==1?B_(e,"expected <teamfile.lmctl>",Mc):pN(r.positionals[0],e)}async function pN(t,e){let r;try{r=await C(e,h=>pe(t,h))}catch(h){return sn(e,`${t}: ${h instanceof Error?h.message:String(h)}`)}let n;try{n=$c(r)}catch(h){return sn(e,`${t}: ${h instanceof Error?h.message:String(h)}`)}let s=wr(r),i;try{i=Ie(r)}catch(h){return sn(e,`${t}: ${h instanceof Error?h.message:String(h)}`)}let a=W_(r,"utf8").split(`
2375
2373
  `),l=_N(a),c=Fo(i,s);if(c.length>0){Uc(i.members,e);for(let h of c)e.stderr.write(`error: ${h.field}: ${h.message}
2376
2374
  `);return 1}try{xr(e,i,r)}catch{}Uc(i.members,e),e.stderr.write(`notice: checking teamfile and models ...
2377
2375
  `);for(let h of Dc(i.members,{agyModelsRunner:jc,codexModelsRunner:Bc}))e.stderr.write(`warning: ${h}
@@ -2400,23 +2398,19 @@ ${t}`:`${r}
2400
2398
  `),1}function B_(t,e,r){return t.stderr.write(`error: ${e}
2401
2399
  ${r}`),2}function Uc(t,e){for(let r of t)r.teamfile!==void 0&&e.stderr.write(`warning: member "${r.alias}" uses deprecated member-level teamfile=; cross-team calls now work at runtime without _CONNECT_
2402
2400
  `),r.provider==="gemini"&&e.stderr.write(`${X_.replace("%s",r.alias)}
2403
- `)}var os,tN,Mc,X_,Fc,q_,Tr,G_,jc,Bc,Pc,on=k(()=>{"use strict";Et();D_();_a();ka();he();Tt();F_();Rs();os=5,tN=`usage: lmctl lint [--strict] <teamfile.lmctl>
2401
+ `)}var os,tN,Mc,X_,Fc,q_,Tr,G_,jc,Bc,Pc,on=k(()=>{"use strict";Et();D_();_a();ka();he();Tt();F_();Rs();os=5,tN=`usage: lmctl lint <teamfile.lmctl>
2404
2402
 
2405
2403
  Validate an lmctl teamfile and warn about stale or placeholder session ids.
2406
2404
  For opencode members with model=, lint also asks the local ACP provider to
2407
2405
  validate model= and effort= without sending a prompt.
2408
2406
 
2409
- --strict Exit 1 if there are any warnings, not just validation errors --
2410
- EXCEPT lmprice catalog-model warnings (operator ruling, 2026-09-29:
2411
- the lmprice table is incomplete by construction, so an "unknown"
2412
- model there can never be asserted as an error; it always warns and
2413
- lint always exits 0 for it, --strict or not). Without --strict, all
2414
- other warnings are printed to stderr but never affect the exit code
2415
- -- a real fleet incident (2026-09-15: 194 codex members fell through
2416
- to a model the subscription had removed) showed lint's own catalog
2417
- can be stale, so a caller relying only on the exit code got false
2418
- confidence. Use --strict in anything that gates on lint (a
2419
- pre-dispatch check, CI, etc).
2407
+ lint has exactly two outcomes: ok, or warnings printed to stderr (operator
2408
+ ruling, 2026-09-29: "no need to give users the choice with no obvious
2409
+ value" -- there used to be a --strict flag that could promote warnings to
2410
+ a failing exit code; it has been removed, lmctl decides the scoping and the
2411
+ exit code is always 0 for warnings-only runs). Exit 1 is reserved for
2412
+ genuine validation errors -- a malformed teamfile, an unparseable member
2413
+ line -- never for a warning, no matter how many.
2420
2414
 
2421
2415
  lint prints a "checking teamfile and models ..." note on stderr while the
2422
2416
  checks run -- the catalog check can pause on a network fetch when the local
@@ -36,8 +36,10 @@ export declare function modelSelectionWarnings(members: MemberConfig[], opts?: {
36
36
  * - unresolved -> silent (deliberate non-binding in lmprice's alias
37
37
  * table, not lint's claim to second-guess)
38
38
  * - no-catalog / catalog unavailable -> skip silently
39
- * The returned warnings wire into lint/seed's standard warnings array, so
40
- * `lint --strict` promotes them exactly like every other warning.
39
+ * The returned warnings wire into lint/seed's standard warnings array
40
+ * (operator ruling, 2026-09-29: lint has exactly two outcomes, ok/warning --
41
+ * never an error, and the `--strict` flag that used to promote warnings to
42
+ * a failing exit code has been removed entirely).
41
43
  */
42
44
  export declare function catalogModelWarnings(members: MemberConfig[], opts?: {
43
45
  snapshot?: CatalogSnapshot;
@@ -10,15 +10,20 @@ For `.lmctl` teamfile compatibility mode:
10
10
  This REPLACED the static tested-model lists that used to live in this
11
11
  document (2026-09-24) — hand-copied lists contradicted the catalog.
12
12
 
13
- **Best-effort contract (operator, 2026-09-25): lint is advisory and is NOT
14
- claimed to be 100% up to date.** Checks can lag provider reality in BOTH
15
- directions — a model lint warns about may have just been added upstream, and
16
- a model lint accepts may have just been retired. Every model check is a
17
- warning, never an error (`lint --strict` is the operator's explicit
18
- promotion, not the default). The runtime stance for anything unrecognized
19
- or out of date is always: "not recognized — could still be valid; no cost
20
- information," never a failure. Cost is equally best-effort (n/a with a
21
- reason, never a guessed price).
13
+ **Best-effort contract (operator, 2026-09-25, hardened 2026-09-29): lint is
14
+ advisory and is NOT claimed to be 100% up to date.** Checks can lag provider
15
+ reality in BOTH directions — a model lint warns about may have just been
16
+ added upstream, and a model lint accepts may have just been retired. Every
17
+ model check is a warning, never an error, full stop: lint has exactly two
18
+ outcomes (ok / warning) and always exits 0 on warnings-only. There used to be
19
+ a `--strict` flag that could promote warnings to a failing exit code; the
20
+ operator removed it entirely (2026-09-29: "no need to give users the choice
21
+ with no obvious value") — lmctl decides the scoping, not the caller. Exit 1
22
+ is reserved for genuine validation errors (a malformed teamfile, an
23
+ unparseable member line), never for a warning. The runtime stance for
24
+ anything unrecognized or out of date is always: "not recognized — could
25
+ still be valid; no cost information," never a failure. Cost is equally
26
+ best-effort (n/a with a reason, never a guessed price).
22
27
  - `codex` and `agy` model validation queries each provider's own live
23
28
  catalog command (`codex debug models`, `agy models`) instead of a
24
29
  hand-copied list — see their `## Codex` / `## Antigravity` sections
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lmctl-ai/lmctl",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "A provider-agnostic control plane for teams of AI coding agents — across providers, with independent review and durable memory.",
5
5
  "type": "module",
6
6
  "homepage": "https://lmctl.com",