@lmctl-ai/lmctl 0.2.47 → 0.2.49

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/lmbee.js CHANGED
@@ -127,7 +127,7 @@ Appkey saved to ${p.appkeyPath} (0600)
127
127
  `),null;for(let[s,i]of[["peer_site",r],["team",n],["alias",o]])try{pn(i,s)}catch(a){return process.stderr.write(`teams-conf: skipping remote _TEAM_ line -- ${a instanceof Error?a.message:String(a)}: ${t}
128
128
  `),null}return{channel:"lmmail",name:n,peerSite:r,alias:o}}var WA=/^(`{3,}|~{3,})/;function ec(e){let t=[],r=null;for(let n of e.split(/\r?\n/)){let o=n.trim(),s=WA.exec(o);if(s){let a=s[1];r===null?r={char:a[0],len:a.length}:a[0]===r.char&&a.length>=r.len&&o.slice(a.length).trim()===""&&(r=null);continue}if(r!==null||!qA.test(o))continue;let i=HA(o);i&&t.push(i)}return t}function mm(e){return e.toLowerCase().endsWith(".md")}function GA(e,t){let r=new Set,n=[];for(let o of[e,...t])if(!(!o||r.has(o))&&(r.add(o),!!Za(o))){try{if(!FA(o).isFile())continue}catch{continue}n.push(o)}return n}function VA(e,t=[]){let r=GA(e,t),n=new Map,o=s=>{try{return fm(s,"utf8")}catch{return null}};for(let s of r){if(!mm(s))continue;let i=o(s);if(i!==null)for(let a of ec(i))n.has(a.name)||n.set(a.name,a)}for(let s of r){if(mm(s))continue;let i=o(s);if(i!==null)for(let a of _o(i))n.has(a.name)||n.set(a.name,a)}return[...n.values()]}function xs(e,t,r){e.prepare(`INSERT INTO sources(path, label, added_ts) VALUES (?, ?, ?)
129
129
  ON CONFLICT(path) DO UPDATE SET label = excluded.label`).run(t,r,new Date().toISOString())}function tc(e){return e.prepare("SELECT * FROM sources ORDER BY id").all()}function Rs(e,t){e.prepare("DELETE FROM sources WHERE path = ?").run(t)}function JA(e){let t=ks(e);return Za(t)?UA(t):null}function Q(e){let t=JA(dm(e.repoPath,`${e.name}.lmctl`));if(t)return t;throw Za(e.repoPath)?new Error(`resolveTeamfilePath: cannot resolve a teamfile for team "${e.name}" -- expected "${dm(e.repoPath,`${e.name}.lmctl`)}" to exist. teams.conf has no explicit teamfile-path column -- fix by naming the teamfile "<name>.lmctl" under repo_path.`):new Error(`resolveTeamfilePath: repo_path "${e.repoPath}" for team "${e.name}" does not exist`)}var KA=/^_MEMBER_(\s|$)/,zA=/(\w+)=(?:"([^"]*)"|([\S]*))/g;function jt(e,t){return nc(e).find(r=>r.alias===t)?.provider??null}function rc(e){return nc(e).filter(t=>t.sessionid!==void 0&&t.sessionid.length>0).length}function Vr(e){return nc(e).filter(t=>t.alias!==void 0).map(t=>({alias:t.alias,provider:t.provider??null}))}function nc(e){let t;try{t=fm(Q(e),"utf8")}catch{return[]}let r=[];for(let n of t.split(`
130
- `)){if(!KA.test(n.trim()))continue;let o={};for(let s of n.matchAll(zA)){let i=s[1];o[i]=s[2]!==void 0?s[2]:s[3]}r.push({alias:o.alias,provider:o.provider,sessionid:o.sessionid})}return r}function Jr(e,t){let r=tc(e).map(n=>n.path);return VA(t,r)}function gn(e){if(typeof e!="string")return null;let t=e.lastIndexOf(":");if(t===-1)return null;let r=e.slice(0,t),n=e.slice(t+1);return!r||!n?null:{teamfilePath:r,alias:n}}function _n(e,t){let r=gn(t);if(r===null||r.alias!==e.leadAlias)return!1;let n;try{n=Q(e)}catch{return!1}return n===r.teamfilePath}function Fe(e,t){return t.running.some(r=>_n(e,r.target))}function hm(e,t,r){return r.running.some(n=>{let o=gn(n.target);return o!==null&&o.alias===t&&o.teamfilePath===e})}function YA(e,t,r){let n=new Set(e.filter(u=>Fe(u,t)).map(u=>u.name)),o=new Set(r??[]),s=r===null,i=[...o].filter(u=>!n.has(u)).sort(),a=[...n].filter(u=>!o.has(u)).sort(),c=s||i.length>0,l=s?"first-run":i.length>0?`active->other: ${i.join(", ")}`:"nochange";return{first:s,active:[...n].sort(),wentIdle:i,newlyActive:a,wake:c,why:l}}function Ns(e){let t=e.prepare("SELECT v FROM kv WHERE k = ?").get("active");return t?JSON.parse(t.v):null}function QA(e,t){e.prepare("INSERT INTO kv(k, v) VALUES ('active', ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v").run(JSON.stringify(t))}function Ls(e,t,r){let n=YA(t,r,Ns(e));return QA(e,n.active),n}import{randomUUID as tf}from"node:crypto";import{existsSync as rf,mkdirSync as hk,readFileSync as qs,rmSync as gk,writeFileSync as nf}from"node:fs";import{homedir as _k}from"node:os";import{join as of,resolve as ef,sep as wk}from"node:path";var u1=64*1024;import{readFileSync as Is,writeFileSync as ZA}from"node:fs";function ek(e,t){return e.prepare("UPDATE queue SET status = 'sent', sent_ts = ? WHERE id = ? AND status = 'pending'").run(new Date().toISOString(),t).changes>0}function gm(e,t){let r=[],n=new Map;for(let s of t){if(s.dedupe_key!==null){r.push(s);continue}let i=JSON.stringify([s.from_id,s.to_id,s.escalation_only,s.to_alias]),a=n.get(i);a?a.push(s):n.set(i,[s])}let o=[...r];for(let s of n.values()){if(s.length===1){o.push(s[0]);continue}let[i,...a]=s,c;try{c=Is(i.prompt_file,"utf8")}catch{o.push(...s);continue}let l=[],u=!0;for(let d of a)try{l.push(`
130
+ `)){if(!KA.test(n.trim()))continue;let o={};for(let s of n.matchAll(zA)){let i=s[1];o[i]=s[2]!==void 0?s[2]:s[3]}r.push({alias:o.alias,provider:o.provider,sessionid:o.sessionid})}return r}function Jr(e,t){let r=tc(e).map(n=>n.path);return VA(t,r)}function gn(e){if(typeof e!="string")return null;let t=e.lastIndexOf(":");if(t===-1)return null;let r=e.slice(0,t),n=e.slice(t+1);return!r||!n?null:{teamfilePath:r,alias:n}}function _n(e,t){let r=gn(t);if(r===null||r.alias!==e.leadAlias)return!1;let n;try{n=Q(e)}catch{return!1}return n===r.teamfilePath}function Fe(e,t){return t.running.some(r=>_n(e,r.target))}function hm(e,t,r){return r.running.some(n=>{let o=gn(n.target);return o!==null&&o.alias===t&&o.teamfilePath===e})}function YA(e,t,r){let n=new Set(e.filter(u=>Fe(u,t)).map(u=>u.name)),o=new Set(r??[]),s=r===null,i=[...o].filter(u=>!n.has(u)).sort(),a=[...n].filter(u=>!o.has(u)).sort(),c=s||i.length>0,l=s?"first-run":i.length>0?`active->other: ${i.join(", ")}`:"nochange";return{first:s,active:[...n].sort(),wentIdle:i,newlyActive:a,wake:c,why:l}}function Ns(e){let t=e.prepare("SELECT v FROM kv WHERE k = ?").get("active");return t?JSON.parse(t.v):null}function QA(e,t){e.prepare("INSERT INTO kv(k, v) VALUES ('active', ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v").run(JSON.stringify(t))}function Ls(e,t,r){let n=YA(t,r,Ns(e));return QA(e,n.active),n}import{randomUUID as tf}from"node:crypto";import{existsSync as rf,mkdirSync as hk,readFileSync as qs,rmSync as gk,writeFileSync as nf}from"node:fs";import{homedir as _k}from"node:os";import{join as of,resolve as ef,sep as wk}from"node:path";var uj=64*1024;import{readFileSync as Is,writeFileSync as ZA}from"node:fs";function ek(e,t){return e.prepare("UPDATE queue SET status = 'sent', sent_ts = ? WHERE id = ? AND status = 'pending'").run(new Date().toISOString(),t).changes>0}function gm(e,t){let r=[],n=new Map;for(let s of t){if(s.dedupe_key!==null){r.push(s);continue}let i=JSON.stringify([s.from_id,s.to_id,s.escalation_only,s.to_alias]),a=n.get(i);a?a.push(s):n.set(i,[s])}let o=[...r];for(let s of n.values()){if(s.length===1){o.push(s[0]);continue}let[i,...a]=s,c;try{c=Is(i.prompt_file,"utf8")}catch{o.push(...s);continue}let l=[],u=!0;for(let d of a)try{l.push(`
131
131
 
132
132
  --- addendum, ${d.created_ts} ---
133
133
  ${Is(d.prompt_file,"utf8")}`)}catch{u=!1;break}if(!u){o.push(...s);continue}if(l.length>0){ZA(i.prompt_file,c+l.join(""));let d=e.prepare("UPDATE queue SET status = 'merged' WHERE id = ? AND status = 'pending'");for(let m of a)d.run(m.id)}o.push(i)}return o}function sc(e,t,r){let n=new Map(t.map(c=>[c.name,c])),o=r.teamName?e.prepare("SELECT * FROM queue WHERE status = 'pending' AND escalation_only = 0 AND to_id = ? ORDER BY id").all(r.teamName):e.prepare("SELECT * FROM queue WHERE status = 'pending' AND escalation_only = 0 ORDER BY id").all(),s=gm(e,o),i=new Set,a=0;for(let c of s){let l=c.to_alias===null?c.to_id:`${c.to_id}:${c.to_alias}`;if(i.has(l))continue;i.add(l);let u=n.get(c.to_id);if(!u||r.isTeamBusy(u))continue;if(r.minMemoryPercent!==void 0){let p=r.memoryCheck;if(p&&!p(r.minMemoryPercent))continue}try{Is(c.prompt_file,"utf8")}catch{continue}if(!ek(e,c.id))continue;let d;try{d=Is(c.prompt_file,"utf8")}catch(p){process.stderr.write(`[lmqueue] row #${c.id} claimed 'sent' but its prompt file became unreadable before dispatch -- nothing was actually sent: ${p instanceof Error?p.message:String(p)}
@@ -689,11 +689,11 @@ Core loop:
689
689
  (manual nudge and queue delivery are unaffected)
690
690
  lmbee auto-nudge-redirect set <team> --companion <team> [--worker-alias A] [--commit-threshold N] [--hours-threshold-ms N]
691
691
  lmbee auto-nudge-redirect clear <team> | status [<team>]
692
- epic #195: redirect team's auto-nudge to a cheap companion
692
+ redirect team's auto-nudge to a cheap companion
693
693
  team's Worker (manual dispatch/queue delivery unaffected)
694
694
  lmbee provider disable <name> [--reason TEXT] [--expires-at ISO]
695
695
  lmbee provider enable <name> | status [<name>]
696
- epic #197: quiet auto-nudge (dispatch + #195 review) for every
696
+ quiet auto-nudge (dispatch and redirect review) for every
697
697
  team on a disabled provider (manual dispatch/queue unaffected)
698
698
 
699
699
  Prompts and answers:
@@ -703,12 +703,11 @@ Prompts and answers:
703
703
  --in-reply-to: links this prompt to queue prompt <id> -- read/
704
704
  unread lifecycle only, never counts as owing a durable answer
705
705
  --literal: send an existing-on-disk path as literal TEXT, not
706
- @file content -- refused by default (math's own live incident,
707
- queue#14375/#14907: a bare path typo for @path silently sends
708
- the filename string, and the report never arrives)
706
+ @file content -- refused by default because a bare path typo
707
+ for @path sends the filename string instead of its contents
709
708
  lmbee nudge <team...>
710
709
  lmbee prompt <to> <prompt|@file> --from <id> [--new-task "title" [--task-description TEXT] | --task <id>] [--literal]
711
- NEW (operator design, 2026-09-21), PARALLEL to enqueue/queue --
710
+ PARALLEL to enqueue/queue --
712
711
  writes to the prompt_tasks/task_prompts ledger, never queue.
713
712
  Task-linked prompts do not create answers for queue prompts.
714
713
  --new-task to start a thread, or --task <id> to continue one --
@@ -722,7 +721,7 @@ Prompts and answers:
722
721
  A ruling made here does NOT close a task/epic flag (needs_review,
723
722
  blocked, etc.) -- write the closing note on that entity directly
724
723
  ("lmbee tasks note add <id>"), prompt is never read by task-ledger
725
- automation and never will be (2026-09-21 incident).
724
+ automation and never will be.
726
725
  lmbee prompt log <task-id> [--json] full prompt history for one prompt task
727
726
  lmbee prompt tasks <team> [--json] threads <team> has appeared in (either party), newest-active first --
728
727
  lmbee prompt reassign <prompt-id> --task <task-id> --by <id>
@@ -755,14 +754,14 @@ Prompts and answers:
755
754
  auto-scan, and escalation from claiming it), waits for its current
756
755
  invocation (if any) to end, then hands off an interactive
757
756
  lctl chat -- non-destructive cut-in-line, never kills the in-flight
758
- process (task #14614). Accepts lctl's own space-separated
757
+ process. Accepts lctl's own space-separated
759
758
  "<teamfile> <alias>" syntax too, .lmctl suffix and all.
760
759
 
761
760
  Teams & history:
762
761
  lmbee teams
763
762
  lmbee team <name> --timeline [--limit N]
764
763
  lmbee team register <name> --teamfile <path> [--alias <alias>]
765
- chat task #13, 2026-09-22: registers a real, existing teamfile
764
+ registers a real, existing teamfile
766
765
  as queue/chat-addressable without hand-editing teams.conf --
767
766
  writes to ~/.lmbee/on-demand-teams.conf, promptName=on-demand
768
767
  (queue-addressable, but never gets a standing auto-nudge).
@@ -773,18 +772,18 @@ Teams & history:
773
772
  lmbee attention [--team T] [--maintainer M] [--stuck-processing-all]
774
773
  lmbee in-flight [--exclude teamfile:alias[,teamfile:alias...]] pre-restart check: any other team's session running?
775
774
  lmbee dispatch-status capture --pid N | check --pid N [--start-ticks T] --artifacts P1,P2,...
776
- epic #198: did ONE specific dispatch finish, without a live watcher
775
+ check whether ONE specific dispatch finished, without a live watcher
777
776
  lmbee logical-name set <name> --teamfile PATH --alias ALIAS | get <name> | list | clear <name>
778
- task #163: name -> (teamfile, alias); "operator" is reserved
777
+ maps name -> (teamfile, alias); "operator" is reserved
779
778
  lmbee rate-limit status | log [--provider NAME] [--limit N]
780
- epic #206: latest lmratelimit readings/forecasts/arm state, and
779
+ latest lmratelimit readings/forecasts/arm state, and
781
780
  the durable armed/disarmed/fired/deferred policy decision log
782
781
  lmbee reliability-report [--json]
783
- task #218: per (provider, model), 7d/30d duration + completion
782
+ per (provider, model), 7d/30d duration + completion
784
783
  rate from tracked_invocation, joined to lmbi token totals --
785
784
  RELIABILITY only, never a quality/performance score
786
785
  lmbee subscriptions --config <path> [--json]
787
- task #119/epic 154 Phase B: thin read-only passthrough to
786
+ thin read-only passthrough to
788
787
  lmbi's own reviewed subscription-value report CLI -- prints
789
788
  the report verbatim, no reinterpretation (that schema is
790
789
  lmbi's domain). Requires LMBI_SUBSCRIPTION_CLI in the
@@ -1,3 +1,4 @@
1
+ import "lmctl";
1
2
  # batchscript.lms - Test-only comparison with procedural lmctl batch
2
3
  # Arguments: [teamfile, alias, batchfile, flags...]
3
4
 
@@ -28,7 +29,9 @@ for (flag_index = 3; flag_index < len(args); flag_index = flag_index + 1) {
28
29
  }
29
30
 
30
31
  # 1. Read prompts
31
- prompts = lm_read_prompts(batchfile, single);
32
+ readResult = lm_read_prompts(batchfile, single);
33
+ if (readResult.status != "ok") { lm_error(readResult.error); lm_exit(1); }
34
+ prompts = readResult.prompts;
32
35
 
33
36
  if (len(prompts) == 0) {
34
37
  lm_error("batch: " + batchfile + ": no prompts found");
@@ -36,16 +39,20 @@ if (len(prompts) == 0) {
36
39
  }
37
40
 
38
41
  # 2. Open batch session
39
- handle = lm_batch_open(teamfile, alias, batchfile, {
42
+ opened = lm_batch_open(teamfile, alias, batchfile, {
40
43
  promptCount: len(prompts),
41
44
  legacy: legacy,
42
45
  single: single
43
46
  });
44
47
 
45
- if (handle == null) {
48
+ if (opened.status != "ok") {
49
+ # The open no longer prints its own failure: the script reports it, as lmctl batch does.
50
+ lm_error("batch: " + opened.error);
46
51
  lm_exit(1);
47
52
  }
48
53
 
54
+ handle = opened.batch;
55
+
49
56
  # 3. Sequential prompt execution
50
57
  for (i = 0; i < len(prompts); i = i + 1) {
51
58
  promptText = prompts[i];
@@ -138,7 +138,7 @@ digest_content = "# Daily Engineering & AI Intelligence Digest\n\n" +
138
138
  "*Automated aggregation via LMScript (n8n Shape 3 pattern)*\n\n" +
139
139
  "### Top Stories & Analysis\n\n" +
140
140
  join(digest_entries, "\n\n") + "\n\n" +
141
- "---\n*Generated at unix epoch: " + str(clock()) + "*\n";
141
+ "---\n*Generated at unix epoch: " + str(now()) + "*\n";
142
142
 
143
143
  digest_file = "examples/n8n/daily-digest.md";
144
144
  write_file(digest_file, digest_content);
@@ -74,7 +74,7 @@ if (len(trim(env_key[0])) > 0) {
74
74
  sample_doc = "# Antigravity Architecture Guide\n\n" +
75
75
  "Antigravity uses a three-layer agent architecture: immutable role prompt, disposable session, and canonical durable-memory.\n\n" +
76
76
  "The command-line interface lmctl is strictly stateless. Persistent daemon processes, time-series metrics, and background mail relays belong to the sibling tool lmbee.\n\n" +
77
- "LMScript is an embedded scripting language executed via lmctl script. It provides core standard functions and lmctl orchestration host functions like lm_prompt and lm_members.\n\n" +
77
+ "LMScript is a standalone scripting language; lmctl script launches its sibling executable. It provides core standard functions and lmctl orchestration host functions like lm_prompt and lm_members.\n\n" +
78
78
  "Worktrees are used for isolated experimental branches. Bare git push is deliberately prevented on experimental branches until changes are reviewed and verified.";
79
79
 
80
80
  question = "Where do persistent background daemons and time-series metrics belong?";
@@ -143,7 +143,7 @@ if (category == "bug" || category == "ops" || category == "security") {
143
143
  # Branch 3: Billing / Feature -> Append to business queue ledger
144
144
  print("[Route: Business Queue] Appending to internal triage ledger...");
145
145
  log_entry = json_stringify({
146
- "timestamp": clock(),
146
+ "timestamp": now(),
147
147
  "category": category,
148
148
  "urgency": urgency,
149
149
  "summary": summary
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lmctl-ai/lmctl",
3
- "version": "0.2.47",
3
+ "version": "0.2.49",
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",
@@ -12,17 +12,13 @@
12
12
  },
13
13
  "repository": "git+https://github.com/lmctlhq/lmctl-mono.git",
14
14
  "exports": {
15
- "./script": {
16
- "default": "./dist/cli/script.js"
17
- },
18
15
  "./package.json": "./package.json"
19
16
  },
20
17
  "files": [
21
18
  "dist/cli/index.js",
22
- "dist/cli/script.js",
23
19
  "dist/cli/schema.sql",
24
20
  "dist/schema.sql",
25
- "dist/lmscript.js",
21
+ "dist/cli/lmscript.js",
26
22
  "dist/lmbee.js",
27
23
  "dist/config/",
28
24
  "dist/public/",