borgmcp 1.0.14 → 1.0.15

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.
@@ -3,6 +3,7 @@ import { CodexAppServerClient } from './codex-app-server.js';
3
3
  import { checkCodexBridgeHealthy } from './codex-remote.js';
4
4
  export declare const CODEX_WAKE_PROMPT = "New Borg cube-log activity arrived.";
5
5
  export declare function formatCodexWakePrompt(inboxLine: string): string;
6
+ export declare const CODEX_CATCHUP_PROMPT = "Borg cube activity arrived while you were busy. Run `borg:read-log unread_only=true` and DRAIN \u2014 repeat until the returned page is under the limit and behind_by is 0 \u2014 so no entries are skipped.";
6
7
  export declare function isCodexRemoteWakeEnabled(env?: NodeJS.ProcessEnv): boolean;
7
8
  export declare function resolveSessionAgentKind(env?: NodeJS.ProcessEnv): 'claude' | 'codex';
8
9
  export interface CodexWakeTarget {
@@ -35,6 +36,8 @@ export interface CodexWakeDeps {
35
36
  getActiveCube?: typeof getActiveCube;
36
37
  getCodexWakeTarget?: typeof getCodexWakeTarget;
37
38
  createClient?: (socketPath: string) => Pick<CodexAppServerClient, 'connect' | 'readThread' | 'startTurn' | 'close'>;
39
+ sleep?: (ms: number) => Promise<void>;
40
+ now?: () => number;
38
41
  }
39
42
  export declare function wakeCodexViaAppServer(reason?: string, env?: NodeJS.ProcessEnv, deps?: CodexWakeDeps): void;
40
43
  export declare function resetCodexWakeForTests(): void;
@@ -1,2 +1,2 @@
1
- import{getActiveCube as f,getCodexWakeTarget as u}from"./cubes.js";import{CodexAppServerClient as h}from"./codex-app-server.js";import{checkCodexBridgeHealthy as g}from"./codex-remote.js";import{recordEventReceipt as p}from"./health-beat.js";const v="New Borg cube-log activity arrived.";function T(e){return`New Borg cube-log activity arrived:
2
- ${e}`}function l(e=process.env){return e.BORG_CODEX_REMOTE_WAKE==="1"}function I(e=process.env){return l(e)?"codex":"claude"}function x(e=process.env){return l(e)?{enabled:!0}:{enabled:!1}}async function K(e,t={}){try{const r=await(t.getCodexWakeTarget??u)(e.cubeId,e.droneId);return r?(t.checkBridge??g)(r.socketPath):!1}catch{return null}}let c=!1;const i=[],o=new Set,s=[],k=100;function _(e=v,t=process.env,n={}){x(t).enabled&&(i.push({reason:e,deps:n}),!c&&(c=!0,C().finally(()=>{c=!1})))}async function C(){for(;i.length>0;){const e=i.shift();await w(e.reason,e.deps)}}async function w(e,t){try{const n=await(t.getActiveCube??f)();if(!n)return;const r=await(t.getCodexWakeTarget??u)(n.cubeId,n.droneId);if(!r)return;const d=`${r.threadId}\0${e}`;if(o.has(d))return;const a=t.createClient?t.createClient(r.socketPath):new h(r.socketPath);await a.connect();try{if((await a.readThread(r.threadId))?.status?.type==="active")return;await a.startTurn(r.threadId,e),p(),W(d)}finally{a.close()}}catch{}}function P(){c=!1,i.length=0,o.clear(),s.length=0}function W(e){if(!o.has(e))for(o.add(e),s.push(e);s.length>k;){const t=s.shift();t&&o.delete(t)}}export{v as CODEX_WAKE_PROMPT,T as formatCodexWakePrompt,l as isCodexRemoteWakeEnabled,K as probeCodexBridgeArmed,P as resetCodexWakeForTests,x as resolveCodexWakeTarget,I as resolveSessionAgentKind,_ as wakeCodexViaAppServer};
1
+ import{getActiveCube as h,getCodexWakeTarget as f}from"./cubes.js";import{CodexAppServerClient as g}from"./codex-app-server.js";import{checkCodexBridgeHealthy as p}from"./codex-remote.js";import{recordEventReceipt as C}from"./health-beat.js";const v="New Borg cube-log activity arrived.";function B(e){return`New Borg cube-log activity arrived:
2
+ ${e}`}const k="Borg cube activity arrived while you were busy. Run `borg:read-log unread_only=true` and DRAIN \u2014 repeat until the returned page is under the limit and behind_by is 0 \u2014 so no entries are skipped.",y=5e3,x=15*6e4;function w(e=process.env){return e.BORG_CODEX_REMOTE_WAKE==="1"}function S(e=process.env){return w(e)?"codex":"claude"}function _(e=process.env){return w(e)?{enabled:!0}:{enabled:!1}}async function U(e,t={}){try{const r=await(t.getCodexWakeTarget??f)(e.cubeId,e.droneId);return r?(t.checkBridge??p)(r.socketPath):!1}catch{return null}}let s=!1;const d=[],c=new Set,u=[],b=100;let l=!1;function T(e){return new Promise(t=>setTimeout(t,e))}function M(e=v,t=process.env,a={}){_(t).enabled&&(d.push({reason:e,deps:a}),!s&&(s=!0,P().finally(()=>{s=!1})))}async function P(){for(;d.length>0;){const e=d.shift();await m(e.reason,e.deps)}}async function m(e,t){try{const a=await(t.getActiveCube??h)();if(!a)return;const r=await(t.getCodexWakeTarget??f)(a.cubeId,a.droneId);if(!r)return;const o=`${r.threadId}\0${e}`;if(c.has(o))return;const n=t.createClient?t.createClient(r.socketPath):new g(r.socketPath);await n.connect();try{if((await n.readThread(r.threadId))?.status?.type==="active"){I(t);return}await n.startTurn(r.threadId,e),C(),E(o)}finally{n.close()}}catch{}}function I(e){l||(l=!0,A(e).finally(()=>{l=!1}))}async function A(e){const t=e.sleep??T,a=e.now??Date.now,r=a()+x;for(;a()<r;){await t(y);try{const o=await(e.getActiveCube??h)();if(!o)continue;const n=await(e.getCodexWakeTarget??f)(o.cubeId,o.droneId);if(!n)continue;const i=e.createClient?e.createClient(n.socketPath):new g(n.socketPath);await i.connect();try{if((await i.readThread(n.threadId))?.status?.type==="active")continue;await i.startTurn(n.threadId,k),C();return}finally{i.close()}}catch{}}}function H(){s=!1,d.length=0,c.clear(),u.length=0,l=!1}function E(e){if(!c.has(e))for(c.add(e),u.push(e);u.length>b;){const t=u.shift();t&&c.delete(t)}}export{k as CODEX_CATCHUP_PROMPT,v as CODEX_WAKE_PROMPT,B as formatCodexWakePrompt,w as isCodexRemoteWakeEnabled,U as probeCodexBridgeArmed,H as resetCodexWakeForTests,_ as resolveCodexWakeTarget,S as resolveSessionAgentKind,M as wakeCodexViaAppServer};
@@ -49,6 +49,36 @@ export declare class RecentLineDeduper {
49
49
  export declare function formatEventLine(inboxLine: string): string | null;
50
50
  export declare function formatFreshEventLine(inboxLine: string, deduper: RecentLineDeduper): string | null;
51
51
  export declare function seedDeduperFromInboxTail(inboxPath: string, deduper: RecentLineDeduper, maxLines?: number): void;
52
+ export interface InboxLockDeps {
53
+ /**
54
+ * ATOMICALLY create the pidfile WITH `content` iff it does not exist. True =
55
+ * claimed, false = already exists. Atomic-with-content (no create-then-write
56
+ * gap) so a concurrent reader never sees an empty pidfile (gh#795 TOCTOU
57
+ * window 2).
58
+ */
59
+ claim(path: string, content: string): boolean;
60
+ /** File contents, or null if absent/unreadable. */
61
+ read(path: string): string | null;
62
+ /**
63
+ * COMPARE-AND-DELETE: remove the file ONLY if its current content still
64
+ * equals `expected`. A no-op if the content changed (a successor reclaimed)
65
+ * or the file is gone — so we never delete another live holder's pidfile
66
+ * (gh#795 TOCTOU windows 1 + 3).
67
+ */
68
+ removeIfContent(path: string, expected: string): void;
69
+ /** kill(pid,0) liveness: true if the process exists (alive), false if gone (ESRCH). */
70
+ isAlive(pid: number): boolean;
71
+ }
72
+ export declare function pidfilePathFor(inboxPath: string): string;
73
+ /**
74
+ * Try to become the SOLE monitor for this inbox. Returns true if we claimed the
75
+ * pidfile (caller proceeds to tail + must release it on exit); false if a LIVE
76
+ * holder already owns it (caller yields/exits without tailing). Never signals a
77
+ * live PID, and never deletes a pidfile that changed under us (compare-and-
78
+ * delete) — only reaps a still-present provably-dead (ESRCH) / unparseable
79
+ * pidfile, then re-claims.
80
+ */
81
+ export declare function acquireInboxLock(pidfilePath: string, ownPid: number, deps: InboxLockDeps, maxAttempts?: number): boolean;
52
82
  /**
53
83
  * Is this module being invoked as the bin entry point?
54
84
  *
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{spawn as u}from"node:child_process";import{readFileSync as p,realpathSync as a}from"node:fs";import{createInterface as f}from"node:readline";import{fileURLToPath as m}from"node:url";const d=/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\S*)\s+(\S+)\s+\(([^)]+)\):\s*(.*)$/,h=1024;class b{cap;seen=new Set;order=[];constructor(e=h){if(this.cap=e,!Number.isInteger(e)||e<1)throw new Error("cap must be a positive integer")}remember(e){if(this.seen.has(e))return!1;for(this.seen.add(e),this.order.push(e);this.order.length>this.cap;){const r=this.order.shift();r&&this.seen.delete(r)}return!0}}function l(t){const e=d.exec(t);if(!e)return null;const[,,r,s,n]=e,o=n.trim();return`${r} (${s}): ${o}`}function x(t,e){const r=l(t);return r===null?null:e.remember(t)?r:null}function E(t,e,r=512){if(!Number.isInteger(r)||r<1)throw new Error("maxLines must be a positive integer");let s;try{s=p(t,"utf-8")}catch(o){if(o?.code==="ENOENT")return;throw o}const n=s.split(/\r?\n/);n.at(-1)===""&&n.pop();for(const o of n.slice(-r))l(o)!==null&&e.remember(o)}function g(){const t=process.argv[2];t||(console.error("borg-inbox-monitor: usage: borg-inbox-monitor <inbox-path>"),process.exit(2));const e=new b;E(t,e);const r=u("tail",["-F","-n","0",t],{stdio:["ignore","pipe","inherit"]});r.stdout||(console.error("borg-inbox-monitor: tail subprocess has no stdout"),process.exit(1));const s=f({input:r.stdout,crlfDelay:1/0});let n=!1;s.on("line",i=>{const c=x(i,e);c!==null&&console.log(c)}),r.on("error",i=>{console.error(`borg-inbox-monitor: tail failed: ${i.message}`),process.exit(1)}),r.on("exit",(i,c)=>{n&&process.exit(0),c&&process.exit(0),process.exit(i??0)});const o=i=>{n||(n=!0,s.close(),!r.killed&&!r.kill(i)&&process.exit(0),setTimeout(()=>process.exit(0),1e3).unref())};process.once("SIGTERM",()=>o("SIGTERM")),process.once("SIGINT",()=>o("SIGINT"))}function I(t,e){try{return a(t)===m(e)}catch{return!1}}I(process.argv[1],import.meta.url)&&g();export{h as RECENT_EMITTED_LINE_CAP,b as RecentLineDeduper,l as formatEventLine,x as formatFreshEventLine,I as isEntryInvocation,E as seedDeduperFromInboxTail};
2
+ import{spawn as d}from"node:child_process";import{randomBytes as h}from"node:crypto";import{linkSync as x,readFileSync as a,realpathSync as b,unlinkSync as p,writeFileSync as y}from"node:fs";import{createInterface as I}from"node:readline";import{fileURLToPath as E}from"node:url";const g=/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\S*)\s+(\S+)\s+\(([^)]+)\):\s*(.*)$/,S=1024;class w{cap;seen=new Set;order=[];constructor(r=S){if(this.cap=r,!Number.isInteger(r)||r<1)throw new Error("cap must be a positive integer")}remember(r){if(this.seen.has(r))return!1;for(this.seen.add(r),this.order.push(r);this.order.length>this.cap;){const t=this.order.shift();t&&this.seen.delete(t)}return!0}}function m(e){const r=g.exec(e);if(!r)return null;const[,,t,n,i]=r,o=i.trim();return`${t} (${n}): ${o}`}function v(e,r){const t=m(e);return t===null?null:r.remember(e)?t:null}function N(e,r,t=512){if(!Number.isInteger(t)||t<1)throw new Error("maxLines must be a positive integer");let n;try{n=a(e,"utf-8")}catch(o){if(o?.code==="ENOENT")return;throw o}const i=n.split(/\r?\n/);i.at(-1)===""&&i.pop();for(const o of i.slice(-t))m(o)!==null&&r.remember(o)}function T(e){return`${e}.monitor.pid`}function $(e,r,t,n=3){const i=String(r);for(let o=0;o<n;o++){if(t.claim(e,i))return!0;const s=t.read(e);if(s===null)continue;const u=s.trim();if(u===""){t.removeIfContent(e,s);continue}const l=Number.parseInt(u,10);if(!Number.isNaN(l)&&t.isAlive(l))return!1;t.removeIfContent(e,s)}return!1}function k(){return{claim:(e,r)=>{const t=`${e}.tmp.${process.pid}.${h(6).toString("hex")}`;try{y(t,r,{mode:384});try{return x(t,e),!0}catch(n){if(n?.code==="EEXIST")return!1;throw n}}finally{try{p(t)}catch{}}},read:e=>{try{return a(e,"utf8")}catch{return null}},removeIfContent:(e,r)=>{try{a(e,"utf8")===r&&p(e)}catch{}},isAlive:e=>{try{return process.kill(e,0),!0}catch(r){return r?.code==="EPERM"}}}}function R(){const e=process.argv[2];e||(console.error("borg-inbox-monitor: usage: borg-inbox-monitor <inbox-path>"),process.exit(2));const r=T(e),t=k();$(r,process.pid,t)||process.exit(0);const n=()=>t.removeIfContent(r,String(process.pid)),i=new w;N(e,i);const o=d("tail",["-F","-n","0",e],{stdio:["ignore","pipe","inherit"]});o.stdout||(console.error("borg-inbox-monitor: tail subprocess has no stdout"),n(),process.exit(1));const s=I({input:o.stdout,crlfDelay:1/0});let u=!1;s.on("line",c=>{const f=v(c,i);f!==null&&console.log(f)}),o.on("error",c=>{console.error(`borg-inbox-monitor: tail failed: ${c.message}`),n(),process.exit(1)}),o.on("exit",(c,f)=>{n(),u&&process.exit(0),f&&process.exit(0),process.exit(c??0)});const l=c=>{u||(u=!0,n(),s.close(),!o.killed&&!o.kill(c)&&process.exit(0),setTimeout(()=>process.exit(0),1e3).unref())};process.once("SIGTERM",()=>l("SIGTERM")),process.once("SIGINT",()=>l("SIGINT"))}function D(e,r){try{return b(e)===E(r)}catch{return!1}}D(process.argv[1],import.meta.url)&&R();export{S as RECENT_EMITTED_LINE_CAP,w as RecentLineDeduper,$ as acquireInboxLock,m as formatEventLine,v as formatFreshEventLine,D as isEntryInvocation,T as pidfilePathFor,N as seedDeduperFromInboxTail};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "Coordinate AI coding agents in shared cubes. Works with Claude Code and Codex. Create projects, assign roles, and share a live activity log.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",