borgmcp 0.9.57 → 0.9.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -176
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +23 -5
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/auth-env.d.ts +52 -0
- package/dist/auth-env.d.ts.map +1 -0
- package/dist/auth-env.js +107 -0
- package/dist/auth-env.js.map +1 -0
- package/dist/auth.d.ts +33 -13
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +100 -4
- package/dist/auth.js.map +1 -1
- package/dist/claude.js +28 -4
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +6 -3
- package/dist/cli-help.js.map +1 -1
- package/dist/cli-platform.js +1 -1
- package/dist/cli-platform.js.map +1 -1
- package/dist/codex-launch.d.ts +1 -0
- package/dist/codex-launch.d.ts.map +1 -1
- package/dist/codex-launch.js +4 -2
- package/dist/codex-launch.js.map +1 -1
- package/dist/config.d.ts +13 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +140 -60
- package/dist/config.js.map +1 -1
- package/dist/device-auth.d.ts +75 -0
- package/dist/device-auth.d.ts.map +1 -0
- package/dist/device-auth.js +167 -0
- package/dist/device-auth.js.map +1 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/regen-format.d.ts.map +1 -1
- package/dist/regen-format.js +3 -1
- package/dist/regen-format.js.map +1 -1
- package/dist/roster-render.d.ts +1 -0
- package/dist/roster-render.d.ts.map +1 -1
- package/dist/roster-render.js +4 -1
- package/dist/roster-render.js.map +1 -1
- package/dist/setup.js +10 -1
- package/dist/setup.js.map +1 -1
- package/dist/token-crypto.d.ts +50 -0
- package/dist/token-crypto.d.ts.map +1 -0
- package/dist/token-crypto.js +91 -0
- package/dist/token-crypto.js.map +1 -0
- package/dist/token-store.d.ts +126 -0
- package/dist/token-store.d.ts.map +1 -0
- package/dist/token-store.js +222 -0
- package/dist/token-store.js.map +1 -0
- package/package.json +1 -9
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gh#557 — token storage backends + selection.
|
|
3
|
+
*
|
|
4
|
+
* config.ts exposes the public token API (storeIdToken/getIdToken/...). This
|
|
5
|
+
* module supplies the interchangeable storage engines it sits on top of:
|
|
6
|
+
*
|
|
7
|
+
* - KeychainBackend — OS keychain via @napi-rs/keyring (the default;
|
|
8
|
+
* real platform at-rest encryption).
|
|
9
|
+
* - EncryptedFileBackend — ~/.borg/credentials, all accounts in one
|
|
10
|
+
* AES-256-GCM blob, file 0600 / dir 0700. Engages
|
|
11
|
+
* only when no keychain is available. Obfuscation-
|
|
12
|
+
* grade (see token-crypto.ts).
|
|
13
|
+
* - caller-managed — BORG_TOKEN / BORG_TOKEN_FILE: an externally
|
|
14
|
+
* supplied id_token, used read-only with no store
|
|
15
|
+
* (the caller owns its lifecycle/freshness).
|
|
16
|
+
*
|
|
17
|
+
* Every engine takes its side-effecting dependencies (keyring entry factory,
|
|
18
|
+
* fs, machine key) by injection so the logic is unit-tested without a real
|
|
19
|
+
* keychain or disk.
|
|
20
|
+
*/
|
|
21
|
+
export type TokenBackendName = 'keychain' | 'encrypted-file';
|
|
22
|
+
/**
|
|
23
|
+
* Account-agnostic key/value store over a backing engine. `account` is one
|
|
24
|
+
* of config.ts's three slots (id-token, refresh-token, expiry).
|
|
25
|
+
*/
|
|
26
|
+
export interface TokenBackend {
|
|
27
|
+
readonly name: TokenBackendName;
|
|
28
|
+
get(account: string): Promise<string | null>;
|
|
29
|
+
set(account: string, value: string): Promise<void>;
|
|
30
|
+
delete(account: string): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The slice of @napi-rs/keyring's AsyncEntry this backend depends on. The
|
|
34
|
+
* return types mirror AsyncEntry exactly (deletePassword resolves to an
|
|
35
|
+
* implementation-defined value we ignore) so the real class is assignable.
|
|
36
|
+
*/
|
|
37
|
+
export interface KeyringEntry {
|
|
38
|
+
setPassword(value: string): Promise<void>;
|
|
39
|
+
getPassword(): Promise<string | null | undefined>;
|
|
40
|
+
deletePassword(): Promise<unknown>;
|
|
41
|
+
}
|
|
42
|
+
export type KeyringEntryFactory = (account: string) => KeyringEntry;
|
|
43
|
+
/**
|
|
44
|
+
* Build the OS-keychain backend. Preserves config.ts's prior semantics:
|
|
45
|
+
* a missing entry reads as null, and delete is silent on a NoEntry error
|
|
46
|
+
* (idempotent clear) while other errors propagate (fail-loud).
|
|
47
|
+
*/
|
|
48
|
+
export declare function makeKeychainBackend(entryFactory?: KeyringEntryFactory): TokenBackend;
|
|
49
|
+
/** The minimal fs surface the file backend needs (injected for tests). */
|
|
50
|
+
export interface FileStoreFs {
|
|
51
|
+
readFile(filePath: string): Promise<string>;
|
|
52
|
+
writeFile(filePath: string, data: string, mode: number): Promise<void>;
|
|
53
|
+
mkdir(dir: string, mode: number): Promise<void>;
|
|
54
|
+
/** Atomically move `from`→`to` (overwrites). Used for crash-safe writes. */
|
|
55
|
+
rename(from: string, to: string): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Atomically create `lockPath` ONLY if it does not exist (O_EXCL). Resolves
|
|
58
|
+
* `true` if it was created (lock acquired) or `false` if it already existed
|
|
59
|
+
* (lock held by another process). This is the inter-process serialization
|
|
60
|
+
* primitive — no native dependency.
|
|
61
|
+
*/
|
|
62
|
+
createExclusive(lockPath: string, content: string): Promise<boolean>;
|
|
63
|
+
/** Remove a file; silent if already gone (lock release + stale-lock steal). */
|
|
64
|
+
removeFile(filePath: string): Promise<void>;
|
|
65
|
+
/** Age in ms (now − mtime) of a file, or null if it does not exist. */
|
|
66
|
+
fileAgeMs(filePath: string): Promise<number | null>;
|
|
67
|
+
}
|
|
68
|
+
export interface EncryptedFileBackendDeps {
|
|
69
|
+
filePath: string;
|
|
70
|
+
key: Buffer;
|
|
71
|
+
fs: FileStoreFs;
|
|
72
|
+
/** Backoff between lock-acquire retries; defaults to a real setTimeout sleep. */
|
|
73
|
+
sleep?: (ms: number) => Promise<void>;
|
|
74
|
+
/** Monotonic clock for the acquire deadline; defaults to Date.now. */
|
|
75
|
+
now?: () => number;
|
|
76
|
+
/** Unique suffix for the write temp file; defaults to a random hex token. */
|
|
77
|
+
uniqueSuffix?: () => string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build the encrypted-file backend. All accounts live in one JSON object
|
|
81
|
+
* encrypted as a single AES-256-GCM envelope at `filePath`.
|
|
82
|
+
*
|
|
83
|
+
* A missing file reads as an empty map. A file that won't decrypt (wrong
|
|
84
|
+
* machine key after a hostname change, truncation, tampering) is ALSO
|
|
85
|
+
* treated as empty: the only consequence is the user re-runs `borg setup`,
|
|
86
|
+
* which is the right fail-safe for credential material — a hard crash on a
|
|
87
|
+
* corrupt dotfile would be worse UX than transparent re-auth.
|
|
88
|
+
*
|
|
89
|
+
* gh#570 — concurrency + atomicity. Multiple `borg` processes (e.g. sibling
|
|
90
|
+
* drone sessions on one host) can share `~/.borg/credentials`. Two fixes:
|
|
91
|
+
* - Anti-lost-update (load-bearing): `set`/`delete` serialize their whole
|
|
92
|
+
* read-modify-write cycle behind an O_EXCL lock file, so concurrent
|
|
93
|
+
* writers no longer each read a stale map and clobber each other.
|
|
94
|
+
* - Anti-corruption: every write goes to a unique temp file then `rename`s
|
|
95
|
+
* into place, so a reader (which is intentionally lock-FREE) always sees a
|
|
96
|
+
* complete old-or-new file, never a torn one.
|
|
97
|
+
*/
|
|
98
|
+
export declare function makeEncryptedFileBackend(deps: EncryptedFileBackendDeps): TokenBackend;
|
|
99
|
+
/** User-facing BORG_TOKEN_STORE values (friendlier than the backend names). */
|
|
100
|
+
export type ForcedStore = 'keychain' | 'file';
|
|
101
|
+
export interface SelectTokenBackendDeps {
|
|
102
|
+
keyringAvailable: () => Promise<boolean>;
|
|
103
|
+
makeKeychain: () => TokenBackend;
|
|
104
|
+
makeFile: () => TokenBackend;
|
|
105
|
+
/** BORG_TOKEN_STORE override: skip probing and force a backend. */
|
|
106
|
+
forced?: ForcedStore;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Select the persistent backend: a forced choice (BORG_TOKEN_STORE=keychain|file)
|
|
110
|
+
* wins and skips the probe; otherwise probe the keychain and fall back to the
|
|
111
|
+
* encrypted file when it's unavailable.
|
|
112
|
+
*/
|
|
113
|
+
export declare function selectTokenBackend(deps: SelectTokenBackendDeps): Promise<TokenBackend>;
|
|
114
|
+
export interface CallerManagedDeps {
|
|
115
|
+
env: NodeJS.ProcessEnv;
|
|
116
|
+
readFile: (filePath: string) => Promise<string>;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Resolve an externally-supplied id_token (no storage). BORG_TOKEN takes
|
|
120
|
+
* precedence; otherwise BORG_TOKEN_FILE is read from disk. Returns null when
|
|
121
|
+
* neither is configured. The value is trimmed (env vars and files commonly
|
|
122
|
+
* carry trailing newlines). The caller owns this token's freshness, so it
|
|
123
|
+
* bypasses the keychain AND the expiry check in config.ts.
|
|
124
|
+
*/
|
|
125
|
+
export declare function readCallerManagedIdToken(deps: CallerManagedDeps): Promise<string | null>;
|
|
126
|
+
//# sourceMappingURL=token-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../src/token-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gh#557 — token storage backends + selection.
|
|
3
|
+
*
|
|
4
|
+
* config.ts exposes the public token API (storeIdToken/getIdToken/...). This
|
|
5
|
+
* module supplies the interchangeable storage engines it sits on top of:
|
|
6
|
+
*
|
|
7
|
+
* - KeychainBackend — OS keychain via @napi-rs/keyring (the default;
|
|
8
|
+
* real platform at-rest encryption).
|
|
9
|
+
* - EncryptedFileBackend — ~/.borg/credentials, all accounts in one
|
|
10
|
+
* AES-256-GCM blob, file 0600 / dir 0700. Engages
|
|
11
|
+
* only when no keychain is available. Obfuscation-
|
|
12
|
+
* grade (see token-crypto.ts).
|
|
13
|
+
* - caller-managed — BORG_TOKEN / BORG_TOKEN_FILE: an externally
|
|
14
|
+
* supplied id_token, used read-only with no store
|
|
15
|
+
* (the caller owns its lifecycle/freshness).
|
|
16
|
+
*
|
|
17
|
+
* Every engine takes its side-effecting dependencies (keyring entry factory,
|
|
18
|
+
* fs, machine key) by injection so the logic is unit-tested without a real
|
|
19
|
+
* keychain or disk.
|
|
20
|
+
*/
|
|
21
|
+
import path from 'path';
|
|
22
|
+
import crypto from 'crypto';
|
|
23
|
+
import { AsyncEntry } from '@napi-rs/keyring';
|
|
24
|
+
import { decryptString, encryptString } from './token-crypto.js';
|
|
25
|
+
const SERVICE_NAME = 'borg-mcp';
|
|
26
|
+
const defaultEntryFactory = (account) => new AsyncEntry(SERVICE_NAME, account);
|
|
27
|
+
/**
|
|
28
|
+
* Build the OS-keychain backend. Preserves config.ts's prior semantics:
|
|
29
|
+
* a missing entry reads as null, and delete is silent on a NoEntry error
|
|
30
|
+
* (idempotent clear) while other errors propagate (fail-loud).
|
|
31
|
+
*/
|
|
32
|
+
export function makeKeychainBackend(entryFactory = defaultEntryFactory) {
|
|
33
|
+
return {
|
|
34
|
+
name: 'keychain',
|
|
35
|
+
async get(account) {
|
|
36
|
+
return (await entryFactory(account).getPassword()) ?? null;
|
|
37
|
+
},
|
|
38
|
+
async set(account, value) {
|
|
39
|
+
await entryFactory(account).setPassword(value);
|
|
40
|
+
},
|
|
41
|
+
async delete(account) {
|
|
42
|
+
try {
|
|
43
|
+
await entryFactory(account).deletePassword();
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const msg = String(err?.message ?? '');
|
|
47
|
+
if (/no entry|not found|no matching/i.test(msg))
|
|
48
|
+
return;
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// gh#570 lock tuning. Token writes are tiny + infrequent, so a short retry
|
|
55
|
+
// cadence + a generous staleness window are right: the staleness window only
|
|
56
|
+
// has to exceed a real write (milliseconds), and a crashed lock-holder is
|
|
57
|
+
// reclaimed after it.
|
|
58
|
+
const LOCK_RETRY_DELAY_MS = 25;
|
|
59
|
+
const LOCK_MAX_WAIT_MS = 2000;
|
|
60
|
+
const LOCK_STALE_MS = 10_000;
|
|
61
|
+
function defaultSleep(ms) {
|
|
62
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
63
|
+
}
|
|
64
|
+
function defaultUniqueSuffix() {
|
|
65
|
+
// PID + random keeps two concurrent writers' temp files distinct so the
|
|
66
|
+
// temp write itself never races. crypto is already a client dependency.
|
|
67
|
+
return `${process.pid}.${crypto.randomBytes(6).toString('hex')}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build the encrypted-file backend. All accounts live in one JSON object
|
|
71
|
+
* encrypted as a single AES-256-GCM envelope at `filePath`.
|
|
72
|
+
*
|
|
73
|
+
* A missing file reads as an empty map. A file that won't decrypt (wrong
|
|
74
|
+
* machine key after a hostname change, truncation, tampering) is ALSO
|
|
75
|
+
* treated as empty: the only consequence is the user re-runs `borg setup`,
|
|
76
|
+
* which is the right fail-safe for credential material — a hard crash on a
|
|
77
|
+
* corrupt dotfile would be worse UX than transparent re-auth.
|
|
78
|
+
*
|
|
79
|
+
* gh#570 — concurrency + atomicity. Multiple `borg` processes (e.g. sibling
|
|
80
|
+
* drone sessions on one host) can share `~/.borg/credentials`. Two fixes:
|
|
81
|
+
* - Anti-lost-update (load-bearing): `set`/`delete` serialize their whole
|
|
82
|
+
* read-modify-write cycle behind an O_EXCL lock file, so concurrent
|
|
83
|
+
* writers no longer each read a stale map and clobber each other.
|
|
84
|
+
* - Anti-corruption: every write goes to a unique temp file then `rename`s
|
|
85
|
+
* into place, so a reader (which is intentionally lock-FREE) always sees a
|
|
86
|
+
* complete old-or-new file, never a torn one.
|
|
87
|
+
*/
|
|
88
|
+
export function makeEncryptedFileBackend(deps) {
|
|
89
|
+
const { filePath, key, fs } = deps;
|
|
90
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
91
|
+
const now = deps.now ?? Date.now;
|
|
92
|
+
const uniqueSuffix = deps.uniqueSuffix ?? defaultUniqueSuffix;
|
|
93
|
+
const lockPath = `${filePath}.lock`;
|
|
94
|
+
async function readMap() {
|
|
95
|
+
let raw;
|
|
96
|
+
try {
|
|
97
|
+
raw = await fs.readFile(filePath);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return {}; // missing file → no stored tokens yet
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const json = decryptString(raw.trim(), key);
|
|
104
|
+
const parsed = JSON.parse(json);
|
|
105
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return {}; // undecryptable / corrupt → fail safe to re-auth
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function writeMap(map) {
|
|
112
|
+
await fs.mkdir(path.dirname(filePath), 0o700);
|
|
113
|
+
// Atomic write: encrypt → temp → rename. rename preserves the temp's 0600.
|
|
114
|
+
const tmpPath = `${filePath}.${uniqueSuffix()}.tmp`;
|
|
115
|
+
await fs.writeFile(tmpPath, encryptString(JSON.stringify(map), key), 0o600);
|
|
116
|
+
try {
|
|
117
|
+
await fs.rename(tmpPath, filePath);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
// rename failed (disk full / permission). Remove the orphaned temp so
|
|
121
|
+
// repeated failures don't accumulate .tmp files, then rethrow so the
|
|
122
|
+
// caller still fails loud. Cleanup is best-effort — a failed unlink must
|
|
123
|
+
// not mask the original error.
|
|
124
|
+
try {
|
|
125
|
+
await fs.removeFile(tmpPath);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
/* ignore cleanup failure — the original rename error takes precedence */
|
|
129
|
+
}
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Run `fn` while holding the O_EXCL lock, serializing read-modify-write
|
|
135
|
+
* across processes. A lock left by a crashed holder is reclaimed once it is
|
|
136
|
+
* older than LOCK_STALE_MS. If the lock can't be acquired within
|
|
137
|
+
* LOCK_MAX_WAIT_MS, proceed best-effort (steal it): the worst case is the
|
|
138
|
+
* original benign lost-update, never a stuck auth.
|
|
139
|
+
*/
|
|
140
|
+
async function withFileLock(fn) {
|
|
141
|
+
const deadline = now() + LOCK_MAX_WAIT_MS;
|
|
142
|
+
let held = false;
|
|
143
|
+
while (!held) {
|
|
144
|
+
held = await fs.createExclusive(lockPath, `${process.pid}@${now()}`);
|
|
145
|
+
if (held)
|
|
146
|
+
break;
|
|
147
|
+
const age = await fs.fileAgeMs(lockPath);
|
|
148
|
+
if (age !== null && age > LOCK_STALE_MS) {
|
|
149
|
+
await fs.removeFile(lockPath); // reclaim a stale (crashed-holder) lock
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (now() >= deadline) {
|
|
153
|
+
await fs.removeFile(lockPath); // last resort: steal + proceed best-effort
|
|
154
|
+
held = await fs.createExclusive(lockPath, `${process.pid}@${now()}`);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
await sleep(LOCK_RETRY_DELAY_MS);
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
return await fn();
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
if (held)
|
|
164
|
+
await fs.removeFile(lockPath);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
name: 'encrypted-file',
|
|
169
|
+
async get(account) {
|
|
170
|
+
// Lock-free: temp+rename guarantees readMap sees a complete file.
|
|
171
|
+
const map = await readMap();
|
|
172
|
+
return Object.prototype.hasOwnProperty.call(map, account) ? map[account] : null;
|
|
173
|
+
},
|
|
174
|
+
set(account, value) {
|
|
175
|
+
return withFileLock(async () => {
|
|
176
|
+
const map = await readMap();
|
|
177
|
+
map[account] = value;
|
|
178
|
+
await writeMap(map);
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
delete(account) {
|
|
182
|
+
return withFileLock(async () => {
|
|
183
|
+
const map = await readMap();
|
|
184
|
+
if (Object.prototype.hasOwnProperty.call(map, account)) {
|
|
185
|
+
delete map[account];
|
|
186
|
+
await writeMap(map);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Select the persistent backend: a forced choice (BORG_TOKEN_STORE=keychain|file)
|
|
194
|
+
* wins and skips the probe; otherwise probe the keychain and fall back to the
|
|
195
|
+
* encrypted file when it's unavailable.
|
|
196
|
+
*/
|
|
197
|
+
export async function selectTokenBackend(deps) {
|
|
198
|
+
if (deps.forced === 'keychain')
|
|
199
|
+
return deps.makeKeychain();
|
|
200
|
+
if (deps.forced === 'file')
|
|
201
|
+
return deps.makeFile();
|
|
202
|
+
return (await deps.keyringAvailable()) ? deps.makeKeychain() : deps.makeFile();
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Resolve an externally-supplied id_token (no storage). BORG_TOKEN takes
|
|
206
|
+
* precedence; otherwise BORG_TOKEN_FILE is read from disk. Returns null when
|
|
207
|
+
* neither is configured. The value is trimmed (env vars and files commonly
|
|
208
|
+
* carry trailing newlines). The caller owns this token's freshness, so it
|
|
209
|
+
* bypasses the keychain AND the expiry check in config.ts.
|
|
210
|
+
*/
|
|
211
|
+
export async function readCallerManagedIdToken(deps) {
|
|
212
|
+
const inline = deps.env.BORG_TOKEN?.trim();
|
|
213
|
+
if (inline)
|
|
214
|
+
return inline;
|
|
215
|
+
const file = deps.env.BORG_TOKEN_FILE?.trim();
|
|
216
|
+
if (file) {
|
|
217
|
+
const contents = await deps.readFile(file);
|
|
218
|
+
return contents.trim();
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=token-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.js","sourceRoot":"","sources":["../src/token-store.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "borgmcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.59",
|
|
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",
|
|
@@ -40,15 +40,7 @@
|
|
|
40
40
|
],
|
|
41
41
|
"author": "Theodor Storm <theodor@byteventures.se>",
|
|
42
42
|
"license": "Apache-2.0",
|
|
43
|
-
"repository": {
|
|
44
|
-
"type": "git",
|
|
45
|
-
"url": "https://github.com/theodorstorm/borg-mcp",
|
|
46
|
-
"directory": "client"
|
|
47
|
-
},
|
|
48
43
|
"homepage": "https://borgmcp.ai",
|
|
49
|
-
"bugs": {
|
|
50
|
-
"url": "https://github.com/theodorstorm/borg-mcp/issues"
|
|
51
|
-
},
|
|
52
44
|
"dependencies": {
|
|
53
45
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
54
46
|
"@napi-rs/keyring": "^1.3.0",
|