borgmcp 1.0.6 → 1.0.7

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.
Files changed (157) hide show
  1. package/dist/assimilate-cmd.js +39 -511
  2. package/dist/assimilate-deps.js +3 -177
  3. package/dist/assimilate-welcome.js +2 -24
  4. package/dist/auth-env.js +1 -107
  5. package/dist/auth.js +23 -612
  6. package/dist/claude.js +11 -281
  7. package/dist/cli-help.js +29 -50
  8. package/dist/cli-platform.js +4 -94
  9. package/dist/codex-app-server.js +4 -228
  10. package/dist/codex-app-wake.js +2 -122
  11. package/dist/codex-launch.js +1 -81
  12. package/dist/codex-remote.js +1 -250
  13. package/dist/config-utils.js +3 -385
  14. package/dist/config.js +1 -190
  15. package/dist/console-prefix.js +1 -86
  16. package/dist/cube-name.js +1 -65
  17. package/dist/cubes.js +4 -269
  18. package/dist/debug.js +1 -71
  19. package/dist/device-auth.js +1 -167
  20. package/dist/direct-log.js +1 -11
  21. package/dist/health-beat.js +1 -168
  22. package/dist/inbox-monitor.js +1 -129
  23. package/dist/index.js +26 -1378
  24. package/dist/lifecycle-log-guard.js +2 -93
  25. package/dist/list-roles-render.js +6 -39
  26. package/dist/log-audit.js +3 -186
  27. package/dist/log-stream.js +9 -848
  28. package/dist/name-validator.js +1 -22
  29. package/dist/parse-assimilate-args.js +1 -82
  30. package/dist/postinstall.js +8 -22
  31. package/dist/regen-format.js +11 -337
  32. package/dist/regen.js +5 -83
  33. package/dist/remote-client.js +1 -695
  34. package/dist/role-resolver.js +1 -36
  35. package/dist/role-section.js +8 -208
  36. package/dist/roster-render.js +3 -96
  37. package/dist/setup.js +36 -251
  38. package/dist/shell-escape.js +1 -22
  39. package/dist/spawn.js +10 -29
  40. package/dist/stale-version-check.js +1 -102
  41. package/dist/stream-owner.js +2 -202
  42. package/dist/stream-status.js +3 -211
  43. package/dist/subscription-retry.js +1 -23
  44. package/dist/sync-roles-render.js +3 -118
  45. package/dist/sync.js +22 -286
  46. package/dist/templates.js +120 -626
  47. package/dist/terminal-title.js +1 -68
  48. package/dist/token-crypto.js +1 -91
  49. package/dist/token-store.js +1 -222
  50. package/dist/types.js +0 -5
  51. package/dist/version.js +2 -78
  52. package/dist/worktree-lifecycle.js +2 -173
  53. package/package.json +11 -2
  54. package/dist/assimilate-cmd.d.ts.map +0 -1
  55. package/dist/assimilate-cmd.js.map +0 -1
  56. package/dist/assimilate-deps.d.ts.map +0 -1
  57. package/dist/assimilate-deps.js.map +0 -1
  58. package/dist/assimilate-welcome.d.ts.map +0 -1
  59. package/dist/assimilate-welcome.js.map +0 -1
  60. package/dist/auth-env.d.ts.map +0 -1
  61. package/dist/auth-env.js.map +0 -1
  62. package/dist/auth.d.ts.map +0 -1
  63. package/dist/auth.js.map +0 -1
  64. package/dist/claude.d.ts.map +0 -1
  65. package/dist/claude.js.map +0 -1
  66. package/dist/cli-help.d.ts.map +0 -1
  67. package/dist/cli-help.js.map +0 -1
  68. package/dist/cli-platform.d.ts.map +0 -1
  69. package/dist/cli-platform.js.map +0 -1
  70. package/dist/codex-app-server.d.ts.map +0 -1
  71. package/dist/codex-app-server.js.map +0 -1
  72. package/dist/codex-app-wake.d.ts.map +0 -1
  73. package/dist/codex-app-wake.js.map +0 -1
  74. package/dist/codex-launch.d.ts.map +0 -1
  75. package/dist/codex-launch.js.map +0 -1
  76. package/dist/codex-remote.d.ts.map +0 -1
  77. package/dist/codex-remote.js.map +0 -1
  78. package/dist/config-utils.d.ts.map +0 -1
  79. package/dist/config-utils.js.map +0 -1
  80. package/dist/config.d.ts.map +0 -1
  81. package/dist/config.js.map +0 -1
  82. package/dist/console-prefix.d.ts.map +0 -1
  83. package/dist/console-prefix.js.map +0 -1
  84. package/dist/cube-name.d.ts.map +0 -1
  85. package/dist/cube-name.js.map +0 -1
  86. package/dist/cubes.d.ts.map +0 -1
  87. package/dist/cubes.js.map +0 -1
  88. package/dist/debug.d.ts.map +0 -1
  89. package/dist/debug.js.map +0 -1
  90. package/dist/device-auth.d.ts.map +0 -1
  91. package/dist/device-auth.js.map +0 -1
  92. package/dist/direct-log.d.ts.map +0 -1
  93. package/dist/direct-log.js.map +0 -1
  94. package/dist/health-beat.d.ts.map +0 -1
  95. package/dist/health-beat.js.map +0 -1
  96. package/dist/inbox-monitor.d.ts.map +0 -1
  97. package/dist/inbox-monitor.js.map +0 -1
  98. package/dist/index.d.ts.map +0 -1
  99. package/dist/index.js.map +0 -1
  100. package/dist/lifecycle-log-guard.d.ts.map +0 -1
  101. package/dist/lifecycle-log-guard.js.map +0 -1
  102. package/dist/list-roles-render.d.ts.map +0 -1
  103. package/dist/list-roles-render.js.map +0 -1
  104. package/dist/log-audit.d.ts.map +0 -1
  105. package/dist/log-audit.js.map +0 -1
  106. package/dist/log-stream.d.ts.map +0 -1
  107. package/dist/log-stream.js.map +0 -1
  108. package/dist/name-validator.d.ts.map +0 -1
  109. package/dist/name-validator.js.map +0 -1
  110. package/dist/parse-assimilate-args.d.ts.map +0 -1
  111. package/dist/parse-assimilate-args.js.map +0 -1
  112. package/dist/postinstall.d.ts.map +0 -1
  113. package/dist/postinstall.js.map +0 -1
  114. package/dist/regen-format.d.ts.map +0 -1
  115. package/dist/regen-format.js.map +0 -1
  116. package/dist/regen.d.ts.map +0 -1
  117. package/dist/regen.js.map +0 -1
  118. package/dist/remote-client.d.ts.map +0 -1
  119. package/dist/remote-client.js.map +0 -1
  120. package/dist/role-resolver.d.ts.map +0 -1
  121. package/dist/role-resolver.js.map +0 -1
  122. package/dist/role-section.d.ts.map +0 -1
  123. package/dist/role-section.js.map +0 -1
  124. package/dist/roster-render.d.ts.map +0 -1
  125. package/dist/roster-render.js.map +0 -1
  126. package/dist/setup.d.ts.map +0 -1
  127. package/dist/setup.js.map +0 -1
  128. package/dist/shell-escape.d.ts.map +0 -1
  129. package/dist/shell-escape.js.map +0 -1
  130. package/dist/spawn.d.ts.map +0 -1
  131. package/dist/spawn.js.map +0 -1
  132. package/dist/stale-version-check.d.ts.map +0 -1
  133. package/dist/stale-version-check.js.map +0 -1
  134. package/dist/stream-owner.d.ts.map +0 -1
  135. package/dist/stream-owner.js.map +0 -1
  136. package/dist/stream-status.d.ts.map +0 -1
  137. package/dist/stream-status.js.map +0 -1
  138. package/dist/subscription-retry.d.ts.map +0 -1
  139. package/dist/subscription-retry.js.map +0 -1
  140. package/dist/sync-roles-render.d.ts.map +0 -1
  141. package/dist/sync-roles-render.js.map +0 -1
  142. package/dist/sync.d.ts.map +0 -1
  143. package/dist/sync.js.map +0 -1
  144. package/dist/templates.d.ts.map +0 -1
  145. package/dist/templates.js.map +0 -1
  146. package/dist/terminal-title.d.ts.map +0 -1
  147. package/dist/terminal-title.js.map +0 -1
  148. package/dist/token-crypto.d.ts.map +0 -1
  149. package/dist/token-crypto.js.map +0 -1
  150. package/dist/token-store.d.ts.map +0 -1
  151. package/dist/token-store.js.map +0 -1
  152. package/dist/types.d.ts.map +0 -1
  153. package/dist/types.js.map +0 -1
  154. package/dist/version.d.ts.map +0 -1
  155. package/dist/version.js.map +0 -1
  156. package/dist/worktree-lifecycle.d.ts.map +0 -1
  157. package/dist/worktree-lifecycle.js.map +0 -1
package/dist/config.js CHANGED
@@ -1,190 +1 @@
1
- /**
2
- * Secure token storage.
3
- *
4
- * The public API (storeIdToken / getIdToken / getRefreshToken / clearTokens /
5
- * isAuthenticated) is unchanged; what changed in gh#557 is what sits beneath
6
- * it. Three storage paths, in precedence order:
7
- *
8
- * 1. Caller-managed (read-only): if BORG_TOKEN / BORG_TOKEN_FILE supplies an
9
- * id_token, it's served verbatim — no keychain, no expiry check, no
10
- * refresh_token. The caller owns the token's lifecycle (CI, containers,
11
- * `borg --token-file`).
12
- * 2. OS keychain (default): @napi-rs/keyring — real platform at-rest
13
- * encryption (macOS Keychain / Windows Credential Vault / libsecret).
14
- * 3. Encrypted file (fallback): ~/.borg/credentials, AES-256-GCM under a
15
- * machine-derived key, 0600. Engages only when no keychain is available
16
- * (headless Linux without Secret Service). Obfuscation-grade — see
17
- * token-crypto.ts.
18
- *
19
- * The persistent backend (2 or 3) is selected once per process and memoized.
20
- * BORG_TOKEN_STORE=keychain|file forces the choice and skips the probe.
21
- */
22
- import os from 'os';
23
- import path from 'path';
24
- import { promises as fsp } from 'fs';
25
- import { isKeyringAvailable } from './auth-env.js';
26
- import { deriveMachineKey } from './token-crypto.js';
27
- import { makeKeychainBackend, makeEncryptedFileBackend, selectTokenBackend, readCallerManagedIdToken, } from './token-store.js';
28
- const ID_TOKEN_ACCOUNT = 'google-id-token';
29
- const REFRESH_TOKEN_ACCOUNT = 'google-refresh-token';
30
- const TOKEN_EXPIRY_ACCOUNT = 'token-expiry';
31
- /** Where the encrypted-file fallback lives when no keychain is available. */
32
- function credentialsPath() {
33
- return path.join(os.homedir(), '.borg', 'credentials');
34
- }
35
- /** Production fs adapter for the encrypted-file backend. */
36
- const nodeFs = {
37
- readFile: (filePath) => fsp.readFile(filePath, 'utf8'),
38
- writeFile: async (filePath, data, mode) => {
39
- // `mode` on writeFile only applies when the file is CREATED; chmod after
40
- // guarantees 0600 even when rewriting an existing credentials file.
41
- await fsp.writeFile(filePath, data, { mode });
42
- await fsp.chmod(filePath, mode);
43
- },
44
- mkdir: async (dir, mode) => {
45
- await fsp.mkdir(dir, { recursive: true, mode });
46
- },
47
- // gh#570: atomic write (temp→rename) + O_EXCL lock primitives.
48
- rename: (from, to) => fsp.rename(from, to),
49
- createExclusive: async (lockPath, content) => {
50
- try {
51
- // 'wx' = O_CREAT | O_EXCL | O_WRONLY → fails with EEXIST if the lock
52
- // already exists, giving us the atomic acquire primitive.
53
- await fsp.writeFile(lockPath, content, { flag: 'wx', mode: 0o600 });
54
- return true;
55
- }
56
- catch (err) {
57
- if (err?.code === 'EEXIST')
58
- return false;
59
- throw err;
60
- }
61
- },
62
- removeFile: async (filePath) => {
63
- try {
64
- await fsp.unlink(filePath);
65
- }
66
- catch (err) {
67
- if (err?.code !== 'ENOENT')
68
- throw err; // silent if already gone
69
- }
70
- },
71
- fileAgeMs: async (filePath) => {
72
- try {
73
- const stat = await fsp.stat(filePath);
74
- return Date.now() - stat.mtimeMs;
75
- }
76
- catch (err) {
77
- if (err?.code === 'ENOENT')
78
- return null;
79
- throw err;
80
- }
81
- },
82
- };
83
- /** Map the user-facing BORG_TOKEN_STORE value to a forced backend, if valid. */
84
- function parseForcedStore(value) {
85
- const v = value?.trim().toLowerCase();
86
- if (v === 'keychain')
87
- return 'keychain';
88
- if (v === 'file' || v === 'encrypted-file')
89
- return 'file';
90
- return undefined;
91
- }
92
- // Memoized persistent-backend selection (one keychain probe per process).
93
- let backendPromise = null;
94
- function getBackend() {
95
- if (!backendPromise) {
96
- backendPromise = selectTokenBackend({
97
- keyringAvailable: () => isKeyringAvailable(),
98
- makeKeychain: () => makeKeychainBackend(),
99
- makeFile: () => makeEncryptedFileBackend({
100
- filePath: credentialsPath(),
101
- key: deriveMachineKey({
102
- hostname: os.hostname(),
103
- username: os.userInfo().username,
104
- platform: process.platform,
105
- }),
106
- fs: nodeFs,
107
- }),
108
- forced: parseForcedStore(process.env.BORG_TOKEN_STORE),
109
- });
110
- }
111
- return backendPromise;
112
- }
113
- /** Caller-managed id_token (BORG_TOKEN / BORG_TOKEN_FILE), or null. */
114
- function callerManagedIdToken() {
115
- return readCallerManagedIdToken({
116
- env: process.env,
117
- readFile: (filePath) => fsp.readFile(filePath, 'utf8'),
118
- });
119
- }
120
- /**
121
- * Store Google OAuth ID token securely in the selected backend.
122
- */
123
- export async function storeIdToken(idToken, expiresAt) {
124
- const backend = await getBackend();
125
- await backend.set(ID_TOKEN_ACCOUNT, idToken);
126
- await backend.set(TOKEN_EXPIRY_ACCOUNT, expiresAt.toString());
127
- }
128
- /**
129
- * Store Google OAuth refresh token securely in the selected backend.
130
- */
131
- export async function storeRefreshToken(refreshToken) {
132
- const backend = await getBackend();
133
- await backend.set(REFRESH_TOKEN_ACCOUNT, refreshToken);
134
- }
135
- /**
136
- * Retrieve the Google OAuth ID token.
137
- *
138
- * A caller-managed token (BORG_TOKEN / BORG_TOKEN_FILE) takes precedence and
139
- * is returned verbatim — the caller owns its freshness, so the expiry buffer
140
- * does not apply. Otherwise reads the persistent backend and returns null if
141
- * not stored or within the 5-minute expiry buffer.
142
- */
143
- export async function getIdToken() {
144
- const callerManaged = await callerManagedIdToken();
145
- if (callerManaged)
146
- return callerManaged;
147
- const backend = await getBackend();
148
- const token = await backend.get(ID_TOKEN_ACCOUNT);
149
- const expiryStr = await backend.get(TOKEN_EXPIRY_ACCOUNT);
150
- if (!token || !expiryStr) {
151
- return null;
152
- }
153
- const expiresAt = parseInt(expiryStr, 10);
154
- const now = Date.now();
155
- // Check if token is expired (with 5 minute buffer).
156
- if (expiresAt - now < 5 * 60 * 1000) {
157
- return null;
158
- }
159
- return token;
160
- }
161
- /**
162
- * Retrieve the Google OAuth refresh token. There is no refresh_token in
163
- * caller-managed mode (the externally-supplied id_token has no refresh
164
- * counterpart), so this returns null whenever a caller-managed token is set.
165
- */
166
- export async function getRefreshToken() {
167
- if (await callerManagedIdToken())
168
- return null;
169
- const backend = await getBackend();
170
- return backend.get(REFRESH_TOKEN_ACCOUNT);
171
- }
172
- /**
173
- * Clear all stored tokens from the selected backend. Idempotent — clearing
174
- * an already-empty store is a no-op. Does not touch caller-managed env vars
175
- * (those are the caller's to manage).
176
- */
177
- export async function clearTokens() {
178
- const backend = await getBackend();
179
- await backend.delete(ID_TOKEN_ACCOUNT);
180
- await backend.delete(REFRESH_TOKEN_ACCOUNT);
181
- await backend.delete(TOKEN_EXPIRY_ACCOUNT);
182
- }
183
- /**
184
- * Check if user has valid authentication.
185
- */
186
- export async function isAuthenticated() {
187
- const token = await getIdToken();
188
- return token !== null;
189
- }
190
- //# sourceMappingURL=config.js.map
1
+ import o from"os";import m from"path";import{promises as r}from"fs";import{isKeyringAvailable as w}from"./auth-env.js";import{deriveMachineKey as p}from"./token-crypto.js";import{makeKeychainBackend as y,makeEncryptedFileBackend as h,selectTokenBackend as g,readCallerManagedIdToken as T}from"./token-store.js";const i="google-id-token",c="google-refresh-token",s="token-expiry";function E(){return m.join(o.homedir(),".borg","credentials")}const F={readFile:e=>r.readFile(e,"utf8"),writeFile:async(e,t,n)=>{await r.writeFile(e,t,{mode:n}),await r.chmod(e,n)},mkdir:async(e,t)=>{await r.mkdir(e,{recursive:!0,mode:t})},rename:(e,t)=>r.rename(e,t),createExclusive:async(e,t)=>{try{return await r.writeFile(e,t,{flag:"wx",mode:384}),!0}catch(n){if(n?.code==="EEXIST")return!1;throw n}},removeFile:async e=>{try{await r.unlink(e)}catch(t){if(t?.code!=="ENOENT")throw t}},fileAgeMs:async e=>{try{const t=await r.stat(e);return Date.now()-t.mtimeMs}catch(t){if(t?.code==="ENOENT")return null;throw t}}};function b(e){const t=e?.trim().toLowerCase();if(t==="keychain")return"keychain";if(t==="file"||t==="encrypted-file")return"file"}let l=null;function a(){return l||(l=g({keyringAvailable:()=>w(),makeKeychain:()=>y(),makeFile:()=>h({filePath:E(),key:p({hostname:o.hostname(),username:o.userInfo().username,platform:process.platform}),fs:F}),forced:b(process.env.BORG_TOKEN_STORE)})),l}function u(){return T({env:process.env,readFile:e=>r.readFile(e,"utf8")})}async function K(e,t){const n=await a();await n.set(i,e),await n.set(s,t.toString())}async function _(e){await(await a()).set(c,e)}async function x(){const e=await u();if(e)return e;const t=await a(),n=await t.get(i),d=await t.get(s);if(!n||!d)return null;const f=parseInt(d,10),k=Date.now();return f-k<300*1e3?null:n}async function R(){return await u()?null:(await a()).get(c)}async function M(){const e=await a();await e.delete(i),await e.delete(c),await e.delete(s)}async function S(){return await x()!==null}export{M as clearTokens,x as getIdToken,R as getRefreshToken,S as isAuthenticated,K as storeIdToken,_ as storeRefreshToken};
@@ -1,86 +1 @@
1
- /**
2
- * Drone self-identification prefix for client-emitted console messages.
3
- *
4
- * Per gh#25: when a drone session emits a console error (e.g.
5
- * "Authentication expired. Run: borg assimilate"), the Queen has no way to
6
- * tell which drone window the message came from without scanning every
7
- * open terminal. Window title alone (set by terminal-title.ts) is
8
- * insufficient — the Queen reads the active terminal's output stream,
9
- * not its title bar.
10
- *
11
- * This module exports a one-shot initializer that resolves the prefix
12
- * from the local cube state cache, plus a synchronous getter that
13
- * call sites use to wrap each console.error.
14
- *
15
- * Format (matches the terminal-title.ts middle-dot convention so
16
- * surfaces stay internally consistent):
17
- * `[<drone-label> · <cube-name>]` (assimilated)
18
- * `[unassimilated · <repo-basename>]` (no cube cached)
19
- */
20
- import { basename } from 'node:path';
21
- import chalk from 'chalk';
22
- import { getActiveCube } from './cubes.js';
23
- let cachedPrefix = null;
24
- /**
25
- * Resolve the drone-self-identification prefix from cube state and
26
- * cache it for subsequent synchronous reads. Idempotent — calling
27
- * multiple times returns the same value. Falls back silently to the
28
- * unassimilated shape on any read error so console emission is never
29
- * blocked.
30
- */
31
- export async function initConsolePrefix() {
32
- if (cachedPrefix !== null)
33
- return cachedPrefix;
34
- try {
35
- const active = await getActiveCube();
36
- if (active?.droneLabel && active?.name) {
37
- cachedPrefix = `[${active.droneLabel} · ${active.name}]`;
38
- return cachedPrefix;
39
- }
40
- }
41
- catch {
42
- // Fall through to unassimilated fallback.
43
- }
44
- cachedPrefix = `[unassimilated · ${basename(process.cwd())}]`;
45
- return cachedPrefix;
46
- }
47
- /**
48
- * Synchronous prefix getter. Returns the cached value if initialized,
49
- * otherwise the unassimilated fallback — safe to call before
50
- * initConsolePrefix() resolves.
51
- */
52
- export function droneIdPrefix() {
53
- if (cachedPrefix !== null)
54
- return cachedPrefix;
55
- return `[unassimilated · ${basename(process.cwd())}]`;
56
- }
57
- /**
58
- * Prefix + trailing space, styled dim/gray so the prefix is metadata
59
- * and the message body retains visual emphasis. Use as
60
- * `${consolePrefix()}<message>`.
61
- */
62
- export function consolePrefix() {
63
- return chalk.gray(droneIdPrefix()) + ' ';
64
- }
65
- /**
66
- * Drop-in replacement for `console.error` that prepends the drone
67
- * self-id prefix. If the first arg is a string, the prefix is
68
- * concatenated to it; otherwise the prefix is emitted as its own arg
69
- * (handles the `console.error('label:', value)` shape).
70
- */
71
- export function cerr(...args) {
72
- if (args.length === 0) {
73
- console.error(consolePrefix());
74
- return;
75
- }
76
- if (typeof args[0] === 'string') {
77
- console.error(consolePrefix() + args[0], ...args.slice(1));
78
- }
79
- else {
80
- console.error(consolePrefix(), ...args);
81
- }
82
- }
83
- export function _resetCachedPrefixForTests() {
84
- cachedPrefix = null;
85
- }
86
- //# sourceMappingURL=console-prefix.js.map
1
+ import{basename as o}from"node:path";import t from"chalk";import{getActiveCube as i}from"./cubes.js";let r=null;async function s(){if(r!==null)return r;try{const e=await i();if(e?.droneLabel&&e?.name)return r=`[${e.droneLabel} \xB7 ${e.name}]`,r}catch{}return r=`[unassimilated \xB7 ${o(process.cwd())}]`,r}function c(){return r!==null?r:`[unassimilated \xB7 ${o(process.cwd())}]`}function n(){return t.gray(c())+" "}function a(...e){if(e.length===0){console.error(n());return}typeof e[0]=="string"?console.error(n()+e[0],...e.slice(1)):console.error(n(),...e)}function m(){r=null}export{m as _resetCachedPrefixForTests,a as cerr,n as consolePrefix,c as droneIdPrefix,s as initConsolePrefix};
package/dist/cube-name.js CHANGED
@@ -1,65 +1 @@
1
- const MAX_URL_LEN = 2048;
2
- const CONTROL_CHAR_RE = /[\x00-\x1F\x7F]/;
3
- /**
4
- * Trim + reject control chars + cap length. Returns null on rejection
5
- * so callers fall through to no-remote derivation.
6
- */
7
- export function sanitizeRemoteUrl(raw) {
8
- const trimmed = raw.trim();
9
- if (trimmed.length === 0 || trimmed.length > MAX_URL_LEN)
10
- return null;
11
- if (CONTROL_CHAR_RE.test(trimmed))
12
- return null;
13
- return trimmed;
14
- }
15
- /**
16
- * Extract the repo name from a git remote URL. Handles SSH/HTTPS/git/file
17
- * forms and embedded credentials. Returns null when nothing parseable
18
- * is present.
19
- *
20
- * Strategy: strip protocol + credentials, then take the last path segment
21
- * after the final `/` or `:`, stripping a trailing `.git`.
22
- */
23
- export function parseGitRemote(url) {
24
- if (!url)
25
- return null;
26
- let s = url.replace(/^[a-z]+:\/\//, '');
27
- s = s.replace(/^[^@\/]*@/, '');
28
- const lastSep = Math.max(s.lastIndexOf('/'), s.lastIndexOf(':'));
29
- if (lastSep === -1)
30
- return null;
31
- let name = s.slice(lastSep + 1);
32
- name = name.replace(/\.git$/, '');
33
- return name.length > 0 ? name : null;
34
- }
35
- /**
36
- * Normalize an arbitrary string into a valid cube name:
37
- * lowercase, underscores+spaces → hyphens, strip [^a-z0-9-], truncate 64.
38
- */
39
- export function normalizeCubeName(raw) {
40
- return raw
41
- .toLowerCase()
42
- .replace(/[_\s]+/g, '-')
43
- .replace(/[^a-z0-9-]/g, '')
44
- .slice(0, 64);
45
- }
46
- /**
47
- * Compose the full derivation: sanitize + parse + normalize, with
48
- * project-root basename as fallback. Returns null when no valid name
49
- * can be derived.
50
- */
51
- export function deriveCubeName(projectRoot, gitRemoteUrl) {
52
- if (gitRemoteUrl) {
53
- const sanitized = sanitizeRemoteUrl(gitRemoteUrl);
54
- if (sanitized) {
55
- const repo = parseGitRemote(sanitized);
56
- if (repo) {
57
- const normalized = normalizeCubeName(repo);
58
- if (normalized.length > 0)
59
- return normalized;
60
- }
61
- }
62
- }
63
- return null;
64
- }
65
- //# sourceMappingURL=cube-name.js.map
1
+ const c=2048,i=/[\x00-\x1F\x7F]/;function o(t){const e=t.trim();return e.length===0||e.length>2048||i.test(e)?null:e}function a(t){if(!t)return null;let e=t.replace(/^[a-z]+:\/\//,"");e=e.replace(/^[^@\/]*@/,"");const r=Math.max(e.lastIndexOf("/"),e.lastIndexOf(":"));if(r===-1)return null;let n=e.slice(r+1);return n=n.replace(/\.git$/,""),n.length>0?n:null}function u(t){return t.toLowerCase().replace(/[_\s]+/g,"-").replace(/[^a-z0-9-]/g,"").slice(0,64)}function s(t,e){if(e){const r=o(e);if(r){const n=a(r);if(n){const l=u(n);if(l.length>0)return l}}}return null}export{s as deriveCubeName,u as normalizeCubeName,a as parseGitRemote,o as sanitizeRemoteUrl};
package/dist/cubes.js CHANGED
@@ -1,269 +1,4 @@
1
- /**
2
- * Per-project active-cube persistence for Borg MCP client
3
- *
4
- * Stores the currently-assimilated cube (name + drone session token + drone
5
- * label + apiUrl) PER PROJECT in ~/.config/borgmcp/cubes.json. The "project"
6
- * is identified by walking up from cwd to find a .git directory; if none is
7
- * found, cwd itself is used as the project key.
8
- *
9
- * The session token is a bearer credential for drone-scoped REST endpoints,
10
- * but it is not as sensitive as the OAuth tokens (which still live in the
11
- * OS keychain via config.ts) — it only authorizes access within the cube
12
- * the user has already been admitted to.
13
- *
14
- * apiUrl is captured at assimilate time so subprocess invocations (e.g. the
15
- * SessionStart hook firing borg-regen) don't need BORG_API_URL in their env
16
- * to know which worker to talk to.
17
- */
18
- import { existsSync } from 'node:fs';
19
- import { mkdir, readFile, writeFile, unlink } from 'node:fs/promises';
20
- import { homedir } from 'node:os';
21
- import { dirname, join, resolve } from 'node:path';
22
- const CUBES_DIR = join(homedir(), '.config', 'borgmcp');
23
- const CUBES_FILE = join(CUBES_DIR, 'cubes.json');
24
- const LAUNCH_FILE = join(CUBES_DIR, 'launch.json');
25
- const CODEX_WAKE_TARGETS_FILE = join(CUBES_DIR, 'codex-wake-targets.json');
26
- const INBOX_DIR = join(CUBES_DIR, 'inboxes');
27
- /**
28
- * Walk up from cwd looking for a .git directory. If found, return that
29
- * directory. If not found by filesystem root, return the original cwd.
30
- * The returned absolute path is the "project key" used to scope cube state.
31
- */
32
- export function findProjectRoot(cwd = process.cwd()) {
33
- let dir = resolve(cwd);
34
- while (true) {
35
- if (existsSync(join(dir, '.git')))
36
- return dir;
37
- const parent = dirname(dir);
38
- if (parent === dir)
39
- return resolve(cwd); // hit root, fall back to cwd
40
- dir = parent;
41
- }
42
- }
43
- /**
44
- * Per-(cube, drone) inbox file path. Each drone gets its own file so that
45
- * multiple drones in the same cube don't trample each other's writes when
46
- * they each receive the same long-poll batch. The file lives under a
47
- * per-cube subdir keyed by cube ID, then by drone ID (a UUID, globally
48
- * unique).
49
- *
50
- * Validates cubeId/droneId as UUIDs before using them in a filesystem
51
- * path. The values come from cubes.json (populated from server response),
52
- * so the input is trusted in normal operation — but a regex guard is
53
- * cheap defense against a corrupted file or future bug that would
54
- * otherwise let `../` slip through into the inbox path.
55
- */
56
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
57
- export function inboxPathForDrone(cubeId, droneId) {
58
- if (!UUID_RE.test(cubeId))
59
- throw new Error(`Invalid cubeId: ${cubeId}`);
60
- if (!UUID_RE.test(droneId))
61
- throw new Error(`Invalid droneId: ${droneId}`);
62
- return join(INBOX_DIR, cubeId, `${droneId}.log`);
63
- }
64
- /**
65
- * Type guard: true iff the parsed JSON looks like the new schema. Anything
66
- * else (old single-cube schema, malformed, missing) is treated as "no state".
67
- */
68
- function isCubesFile(data) {
69
- return (data !== null &&
70
- typeof data === 'object' &&
71
- typeof data.projects === 'object' &&
72
- data.projects !== null &&
73
- !Array.isArray(data.projects));
74
- }
75
- /**
76
- * Read the cubes.json file. Returns null if the file does not exist, is
77
- * unparseable, or is in the old single-cube schema (per the project's no-
78
- * backward-compat stance, the old shape is treated as absent — it will be
79
- * overwritten the next time setActiveCube() runs).
80
- */
81
- async function readCubesFile() {
82
- let raw;
83
- try {
84
- raw = await readFile(CUBES_FILE, 'utf8');
85
- }
86
- catch (error) {
87
- if (error?.code === 'ENOENT')
88
- return null;
89
- throw error;
90
- }
91
- let parsed;
92
- try {
93
- parsed = JSON.parse(raw);
94
- }
95
- catch {
96
- return null;
97
- }
98
- if (!isCubesFile(parsed))
99
- return null;
100
- return parsed;
101
- }
102
- /**
103
- * Write the cubes.json file, ensuring the parent directory exists.
104
- */
105
- async function writeCubesFile(data) {
106
- await mkdir(dirname(CUBES_FILE), { recursive: true });
107
- await writeFile(CUBES_FILE, JSON.stringify(data, null, 2) + '\n', {
108
- mode: 0o600,
109
- });
110
- }
111
- function isLaunchFile(data) {
112
- return (data !== null &&
113
- typeof data === 'object' &&
114
- typeof data.projects === 'object' &&
115
- data.projects !== null &&
116
- !Array.isArray(data.projects));
117
- }
118
- async function readLaunchFile() {
119
- let raw;
120
- try {
121
- raw = await readFile(LAUNCH_FILE, 'utf8');
122
- }
123
- catch (error) {
124
- if (error?.code === 'ENOENT')
125
- return null;
126
- throw error;
127
- }
128
- try {
129
- const parsed = JSON.parse(raw);
130
- return isLaunchFile(parsed) ? parsed : null;
131
- }
132
- catch {
133
- return null;
134
- }
135
- }
136
- async function writeLaunchFile(data) {
137
- await mkdir(dirname(LAUNCH_FILE), { recursive: true });
138
- await writeFile(LAUNCH_FILE, JSON.stringify(data, null, 2) + '\n', {
139
- mode: 0o600,
140
- });
141
- }
142
- function codexWakeTargetKey(cubeId, droneId) {
143
- if (!UUID_RE.test(cubeId))
144
- throw new Error(`Invalid cubeId: ${cubeId}`);
145
- if (!UUID_RE.test(droneId))
146
- throw new Error(`Invalid droneId: ${droneId}`);
147
- return `${cubeId}:${droneId}`;
148
- }
149
- function isCodexWakeTargetsFile(data) {
150
- return (data !== null &&
151
- typeof data === 'object' &&
152
- typeof data.targets === 'object' &&
153
- data.targets !== null &&
154
- !Array.isArray(data.targets));
155
- }
156
- async function readCodexWakeTargetsFile() {
157
- let raw;
158
- try {
159
- raw = await readFile(CODEX_WAKE_TARGETS_FILE, 'utf8');
160
- }
161
- catch (error) {
162
- if (error?.code === 'ENOENT')
163
- return null;
164
- throw error;
165
- }
166
- try {
167
- const parsed = JSON.parse(raw);
168
- return isCodexWakeTargetsFile(parsed) ? parsed : null;
169
- }
170
- catch {
171
- return null;
172
- }
173
- }
174
- async function writeCodexWakeTargetsFile(data) {
175
- await mkdir(dirname(CODEX_WAKE_TARGETS_FILE), { recursive: true });
176
- await writeFile(CODEX_WAKE_TARGETS_FILE, JSON.stringify(data, null, 2) + '\n', {
177
- mode: 0o600,
178
- });
179
- }
180
- /**
181
- * Get the currently-active cube for the current project, or null if not
182
- * assimilated in this project. Entries written by older client versions
183
- * that lack the `cubeId` field are treated as absent — re-assimilate to
184
- * refresh.
185
- */
186
- export async function getActiveCube() {
187
- const data = await readCubesFile();
188
- if (!data)
189
- return null;
190
- const key = findProjectRoot();
191
- const entry = data.projects[key];
192
- if (!entry || typeof entry.cubeId !== 'string' || !entry.cubeId)
193
- return null;
194
- if (typeof entry.droneId !== 'string' || !entry.droneId)
195
- return null;
196
- return entry;
197
- }
198
- /**
199
- * Set the active cube for the current project. Preserves entries for all
200
- * other projects.
201
- */
202
- export async function setActiveCube(active) {
203
- const existing = (await readCubesFile()) ?? { projects: {} };
204
- existing.projects[findProjectRoot()] = active;
205
- await writeCubesFile(existing);
206
- }
207
- export function activeCubeWithFreshRegenIdentity(active, result) {
208
- const name = result.cube?.name ?? active.name;
209
- const droneLabel = result.drone?.label ?? active.droneLabel;
210
- if (name === active.name && droneLabel === active.droneLabel)
211
- return active;
212
- return { ...active, name, droneLabel };
213
- }
214
- /**
215
- * Clear the active cube for the current project. If the projects map
216
- * becomes empty as a result, remove the file entirely rather than leave
217
- * an empty {projects:{}} skeleton.
218
- */
219
- export async function clearActiveCube() {
220
- const existing = await readCubesFile();
221
- if (!existing)
222
- return;
223
- const key = findProjectRoot();
224
- if (!(key in existing.projects))
225
- return;
226
- delete existing.projects[key];
227
- if (Object.keys(existing.projects).length === 0) {
228
- try {
229
- await unlink(CUBES_FILE);
230
- }
231
- catch (error) {
232
- if (error?.code !== 'ENOENT')
233
- throw error;
234
- }
235
- return;
236
- }
237
- await writeCubesFile(existing);
238
- }
239
- export async function getProjectCliPreference() {
240
- const data = await readLaunchFile();
241
- if (!data)
242
- return null;
243
- const entry = data.projects[findProjectRoot()];
244
- return entry?.cli === 'claude' || entry?.cli === 'codex' ? entry.cli : null;
245
- }
246
- export async function setProjectCliPreference(cli) {
247
- const existing = (await readLaunchFile()) ?? { projects: {} };
248
- existing.projects[findProjectRoot()] = { cli };
249
- await writeLaunchFile(existing);
250
- }
251
- export async function setCodexWakeTarget(cubeId, droneId, target) {
252
- const existing = (await readCodexWakeTargetsFile()) ?? { targets: {} };
253
- existing.targets[codexWakeTargetKey(cubeId, droneId)] = {
254
- ...target,
255
- updatedAt: new Date().toISOString(),
256
- };
257
- await writeCodexWakeTargetsFile(existing);
258
- }
259
- export async function getCodexWakeTarget(cubeId, droneId) {
260
- const existing = await readCodexWakeTargetsFile();
261
- if (!existing)
262
- return null;
263
- const target = existing.targets[codexWakeTargetKey(cubeId, droneId)];
264
- if (!target || typeof target.threadId !== 'string' || typeof target.socketPath !== 'string') {
265
- return null;
266
- }
267
- return target;
268
- }
269
- //# sourceMappingURL=cubes.js.map
1
+ import{existsSync as E}from"node:fs";import{mkdir as l,readFile as f,writeFile as p,unlink as m}from"node:fs/promises";import{homedir as C}from"node:os";import{dirname as c,join as o,resolve as d}from"node:path";const s=o(C(),".config","borgmcp"),a=o(s,"cubes.json"),y=o(s,"launch.json"),w=o(s,"codex-wake-targets.json"),F=o(s,"inboxes");function i(t=process.cwd()){let e=d(t);for(;;){if(E(o(e,".git")))return e;const r=c(e);if(r===e)return d(t);e=r}}const u=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function _(t,e){if(!u.test(t))throw new Error(`Invalid cubeId: ${t}`);if(!u.test(e))throw new Error(`Invalid droneId: ${e}`);return o(F,t,`${e}.log`)}function N(t){return t!==null&&typeof t=="object"&&typeof t.projects=="object"&&t.projects!==null&&!Array.isArray(t.projects)}async function g(){let t;try{t=await f(a,"utf8")}catch(r){if(r?.code==="ENOENT")return null;throw r}let e;try{e=JSON.parse(t)}catch{return null}return N(e)?e:null}async function j(t){await l(c(a),{recursive:!0}),await p(a,JSON.stringify(t,null,2)+`
2
+ `,{mode:384})}function I(t){return t!==null&&typeof t=="object"&&typeof t.projects=="object"&&t.projects!==null&&!Array.isArray(t.projects)}async function h(){let t;try{t=await f(y,"utf8")}catch(e){if(e?.code==="ENOENT")return null;throw e}try{const e=JSON.parse(t);return I(e)?e:null}catch{return null}}async function O(t){await l(c(y),{recursive:!0}),await p(y,JSON.stringify(t,null,2)+`
3
+ `,{mode:384})}function x(t,e){if(!u.test(t))throw new Error(`Invalid cubeId: ${t}`);if(!u.test(e))throw new Error(`Invalid droneId: ${e}`);return`${t}:${e}`}function k(t){return t!==null&&typeof t=="object"&&typeof t.targets=="object"&&t.targets!==null&&!Array.isArray(t.targets)}async function b(){let t;try{t=await f(w,"utf8")}catch(e){if(e?.code==="ENOENT")return null;throw e}try{const e=JSON.parse(t);return k(e)?e:null}catch{return null}}async function A(t){await l(c(w),{recursive:!0}),await p(w,JSON.stringify(t,null,2)+`
4
+ `,{mode:384})}async function $(){const t=await g();if(!t)return null;const e=i(),r=t.projects[e];return!r||typeof r.cubeId!="string"||!r.cubeId||typeof r.droneId!="string"||!r.droneId?null:r}async function v(t){const e=await g()??{projects:{}};e.projects[i()]=t,await j(e)}function P(t,e){const r=e.cube?.name??t.name,n=e.drone?.label??t.droneLabel;return r===t.name&&n===t.droneLabel?t:{...t,name:r,droneLabel:n}}async function D(){const t=await g();if(!t)return;const e=i();if(e in t.projects){if(delete t.projects[e],Object.keys(t.projects).length===0){try{await m(a)}catch(r){if(r?.code!=="ENOENT")throw r}return}await j(t)}}async function J(){const t=await h();if(!t)return null;const e=t.projects[i()];return e?.cli==="claude"||e?.cli==="codex"?e.cli:null}async function R(t){const e=await h()??{projects:{}};e.projects[i()]={cli:t},await O(e)}async function U(t,e,r){const n=await b()??{targets:{}};n.targets[x(t,e)]={...r,updatedAt:new Date().toISOString()},await A(n)}async function B(t,e){const r=await b();if(!r)return null;const n=r.targets[x(t,e)];return!n||typeof n.threadId!="string"||typeof n.socketPath!="string"?null:n}export{P as activeCubeWithFreshRegenIdentity,D as clearActiveCube,i as findProjectRoot,$ as getActiveCube,B as getCodexWakeTarget,J as getProjectCliPreference,_ as inboxPathForDrone,v as setActiveCube,U as setCodexWakeTarget,R as setProjectCliPreference};