borgmcp 0.9.56 → 0.9.58

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 (60) hide show
  1. package/README.md +104 -176
  2. package/dist/assimilate-cmd.d.ts +2 -5
  3. package/dist/assimilate-cmd.d.ts.map +1 -1
  4. package/dist/assimilate-cmd.js +12 -0
  5. package/dist/assimilate-cmd.js.map +1 -1
  6. package/dist/assimilate-deps.d.ts.map +1 -1
  7. package/dist/assimilate-deps.js +2 -9
  8. package/dist/assimilate-deps.js.map +1 -1
  9. package/dist/auth-env.d.ts +52 -0
  10. package/dist/auth-env.d.ts.map +1 -0
  11. package/dist/auth-env.js +107 -0
  12. package/dist/auth-env.js.map +1 -0
  13. package/dist/auth.d.ts +33 -13
  14. package/dist/auth.d.ts.map +1 -1
  15. package/dist/auth.js +100 -4
  16. package/dist/auth.js.map +1 -1
  17. package/dist/claude.js +28 -6
  18. package/dist/claude.js.map +1 -1
  19. package/dist/cli-help.d.ts +16 -0
  20. package/dist/cli-help.d.ts.map +1 -0
  21. package/dist/cli-help.js +27 -0
  22. package/dist/cli-help.js.map +1 -0
  23. package/dist/cli-platform.js +1 -1
  24. package/dist/cli-platform.js.map +1 -1
  25. package/dist/codex-remote.d.ts +60 -12
  26. package/dist/codex-remote.d.ts.map +1 -1
  27. package/dist/codex-remote.js +173 -80
  28. package/dist/codex-remote.js.map +1 -1
  29. package/dist/config.d.ts +13 -10
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +105 -60
  32. package/dist/config.js.map +1 -1
  33. package/dist/device-auth.d.ts +75 -0
  34. package/dist/device-auth.d.ts.map +1 -0
  35. package/dist/device-auth.js +167 -0
  36. package/dist/device-auth.js.map +1 -0
  37. package/dist/inbox-monitor.d.ts.map +1 -1
  38. package/dist/inbox-monitor.js +15 -0
  39. package/dist/inbox-monitor.js.map +1 -1
  40. package/dist/index.js +5 -3
  41. package/dist/index.js.map +1 -1
  42. package/dist/setup.js +25 -7
  43. package/dist/setup.js.map +1 -1
  44. package/dist/subscription-retry.d.ts +40 -0
  45. package/dist/subscription-retry.d.ts.map +1 -0
  46. package/dist/subscription-retry.js +23 -0
  47. package/dist/subscription-retry.js.map +1 -0
  48. package/dist/templates.d.ts +1 -0
  49. package/dist/templates.d.ts.map +1 -1
  50. package/dist/templates.js +59 -11
  51. package/dist/templates.js.map +1 -1
  52. package/dist/token-crypto.d.ts +50 -0
  53. package/dist/token-crypto.d.ts.map +1 -0
  54. package/dist/token-crypto.js +91 -0
  55. package/dist/token-crypto.js.map +1 -0
  56. package/dist/token-store.d.ts +98 -0
  57. package/dist/token-store.d.ts.map +1 -0
  58. package/dist/token-store.js +136 -0
  59. package/dist/token-store.js.map +1 -0
  60. package/package.json +1 -9
@@ -0,0 +1,91 @@
1
+ /**
2
+ * gh#557 — AES-256-GCM crypto for the keychain-less token store.
3
+ *
4
+ * ⚠ OBFUSCATION-GRADE, BY DESIGN. The encryption key is DERIVED from stable
5
+ * machine+user identifiers (hostname, username, platform) — there is no
6
+ * passphrase. This means:
7
+ *
8
+ * - It DEFENDS against casual/accidental exposure: a dotfile backup, an
9
+ * `scp -r ~`, a synced home directory, a shoulder-surfed `cat`. The
10
+ * on-disk bytes are ciphertext, not a readable token.
11
+ * - It does NOT defend against a same-uid process or root on the SAME
12
+ * machine: anything that can read ~/.borg/credentials can also read the
13
+ * same hostname/username/platform and re-derive the key. That is an
14
+ * accepted limitation (SR-endorsed, gh#557 ESCALATION 2) and matches
15
+ * gcloud's own at-rest posture for its credential files.
16
+ *
17
+ * The OS keychain (config.ts default) remains the real at-rest encryption
18
+ * path; this fallback only engages when no keychain is available (headless
19
+ * Linux without Secret Service, etc.).
20
+ *
21
+ * Machine identifiers are injected (MachineKeyInputs) rather than read here,
22
+ * both for testability and so the production caller can choose OS primitives
23
+ * that work in headless/container environments (os.hostname()/os.userInfo()
24
+ * never spawn a subprocess, unlike a hardware machine-id probe).
25
+ */
26
+ import crypto from 'crypto';
27
+ /**
28
+ * Static application salt — domain-separates this key derivation from any
29
+ * other use of the same machine identifiers. NOT a secret (it ships in the
30
+ * published client); its only job is to make the derived key specific to
31
+ * borg-mcp token storage.
32
+ */
33
+ const KEY_SALT = 'borg-mcp/token-store/v1';
34
+ const ENVELOPE_VERSION = 'v1';
35
+ const IV_BYTES = 12; // 96-bit nonce, the GCM standard
36
+ const KEY_BYTES = 32; // AES-256
37
+ /**
38
+ * Derive a stable 32-byte AES-256 key from machine+user identifiers.
39
+ * Deterministic for a given machine+user (so a token written today decrypts
40
+ * tomorrow) and distinct across machines/users.
41
+ */
42
+ export function deriveMachineKey(inputs) {
43
+ const material = [inputs.hostname, inputs.username, inputs.platform, KEY_SALT].join('\0');
44
+ return crypto.createHash('sha256').update(material).digest(); // 32 bytes
45
+ }
46
+ /**
47
+ * Encrypt a plaintext string under the given key. Returns a versioned,
48
+ * dot-delimited envelope: `v1.<base64(iv)>.<base64(tag)>.<base64(ct)>`.
49
+ * A fresh random IV per call means the same plaintext encrypts differently
50
+ * every time (no deterministic-ciphertext leak).
51
+ */
52
+ export function encryptString(plaintext, key) {
53
+ if (key.length !== KEY_BYTES) {
54
+ throw new Error(`token-crypto: key must be ${KEY_BYTES} bytes`);
55
+ }
56
+ const iv = crypto.randomBytes(IV_BYTES);
57
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
58
+ const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
59
+ const tag = cipher.getAuthTag();
60
+ return [
61
+ ENVELOPE_VERSION,
62
+ iv.toString('base64'),
63
+ tag.toString('base64'),
64
+ ciphertext.toString('base64'),
65
+ ].join('.');
66
+ }
67
+ /**
68
+ * Decrypt an envelope produced by encryptString. Throws on a malformed
69
+ * envelope, a wrong key, or a tampered ciphertext (the GCM auth tag fails
70
+ * verification) — fail-closed is correct for credential material.
71
+ */
72
+ export function decryptString(envelope, key) {
73
+ if (key.length !== KEY_BYTES) {
74
+ throw new Error(`token-crypto: key must be ${KEY_BYTES} bytes`);
75
+ }
76
+ const parts = envelope.split('.');
77
+ if (parts.length !== 4 || parts[0] !== ENVELOPE_VERSION) {
78
+ throw new Error('token-crypto: malformed or unsupported envelope');
79
+ }
80
+ const iv = Buffer.from(parts[1], 'base64');
81
+ const tag = Buffer.from(parts[2], 'base64');
82
+ const ciphertext = Buffer.from(parts[3], 'base64');
83
+ if (iv.length !== IV_BYTES) {
84
+ throw new Error('token-crypto: malformed IV');
85
+ }
86
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
87
+ decipher.setAuthTag(tag);
88
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
89
+ return plaintext.toString('utf8');
90
+ }
91
+ //# sourceMappingURL=token-crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-crypto.js","sourceRoot":"","sources":["../src/token-crypto.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,98 @@
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
+ }
55
+ export interface EncryptedFileBackendDeps {
56
+ filePath: string;
57
+ key: Buffer;
58
+ fs: FileStoreFs;
59
+ }
60
+ /**
61
+ * Build the encrypted-file backend. All accounts live in one JSON object
62
+ * encrypted as a single AES-256-GCM envelope at `filePath`.
63
+ *
64
+ * A missing file reads as an empty map. A file that won't decrypt (wrong
65
+ * machine key after a hostname change, truncation, tampering) is ALSO
66
+ * treated as empty: the only consequence is the user re-runs `borg setup`,
67
+ * which is the right fail-safe for credential material — a hard crash on a
68
+ * corrupt dotfile would be worse UX than transparent re-auth.
69
+ */
70
+ export declare function makeEncryptedFileBackend(deps: EncryptedFileBackendDeps): TokenBackend;
71
+ /** User-facing BORG_TOKEN_STORE values (friendlier than the backend names). */
72
+ export type ForcedStore = 'keychain' | 'file';
73
+ export interface SelectTokenBackendDeps {
74
+ keyringAvailable: () => Promise<boolean>;
75
+ makeKeychain: () => TokenBackend;
76
+ makeFile: () => TokenBackend;
77
+ /** BORG_TOKEN_STORE override: skip probing and force a backend. */
78
+ forced?: ForcedStore;
79
+ }
80
+ /**
81
+ * Select the persistent backend: a forced choice (BORG_TOKEN_STORE=keychain|file)
82
+ * wins and skips the probe; otherwise probe the keychain and fall back to the
83
+ * encrypted file when it's unavailable.
84
+ */
85
+ export declare function selectTokenBackend(deps: SelectTokenBackendDeps): Promise<TokenBackend>;
86
+ export interface CallerManagedDeps {
87
+ env: NodeJS.ProcessEnv;
88
+ readFile: (filePath: string) => Promise<string>;
89
+ }
90
+ /**
91
+ * Resolve an externally-supplied id_token (no storage). BORG_TOKEN takes
92
+ * precedence; otherwise BORG_TOKEN_FILE is read from disk. Returns null when
93
+ * neither is configured. The value is trimmed (env vars and files commonly
94
+ * carry trailing newlines). The caller owns this token's freshness, so it
95
+ * bypasses the keychain AND the expiry check in config.ts.
96
+ */
97
+ export declare function readCallerManagedIdToken(deps: CallerManagedDeps): Promise<string | null>;
98
+ //# 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":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAQH,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,gBAAgB,CAAC;AAE7D;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAID;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,WAAW,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAClD,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,YAAY,CAAC;AAKpE;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,GAAE,mBAAyC,GACtD,YAAY,CAmBd;AAID,0EAA0E;AAC1E,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjD;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,WAAW,CAAC;CACjB;AAID;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,wBAAwB,GAAG,YAAY,CA2CrF;AAID,+EAA+E;AAC/E,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,MAAM,CAAC;AAE9C,MAAM,WAAW,sBAAsB;IACrC,gBAAgB,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,YAAY,EAAE,MAAM,YAAY,CAAC;IACjC,QAAQ,EAAE,MAAM,YAAY,CAAC;IAC7B,mEAAmE;IACnE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAIvB;AAID,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACjD;AAED;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAWxB"}
@@ -0,0 +1,136 @@
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 { AsyncEntry } from '@napi-rs/keyring';
23
+ import { decryptString, encryptString } from './token-crypto.js';
24
+ const SERVICE_NAME = 'borg-mcp';
25
+ const defaultEntryFactory = (account) => new AsyncEntry(SERVICE_NAME, account);
26
+ /**
27
+ * Build the OS-keychain backend. Preserves config.ts's prior semantics:
28
+ * a missing entry reads as null, and delete is silent on a NoEntry error
29
+ * (idempotent clear) while other errors propagate (fail-loud).
30
+ */
31
+ export function makeKeychainBackend(entryFactory = defaultEntryFactory) {
32
+ return {
33
+ name: 'keychain',
34
+ async get(account) {
35
+ return (await entryFactory(account).getPassword()) ?? null;
36
+ },
37
+ async set(account, value) {
38
+ await entryFactory(account).setPassword(value);
39
+ },
40
+ async delete(account) {
41
+ try {
42
+ await entryFactory(account).deletePassword();
43
+ }
44
+ catch (err) {
45
+ const msg = String(err?.message ?? '');
46
+ if (/no entry|not found|no matching/i.test(msg))
47
+ return;
48
+ throw err;
49
+ }
50
+ },
51
+ };
52
+ }
53
+ /**
54
+ * Build the encrypted-file backend. All accounts live in one JSON object
55
+ * encrypted as a single AES-256-GCM envelope at `filePath`.
56
+ *
57
+ * A missing file reads as an empty map. A file that won't decrypt (wrong
58
+ * machine key after a hostname change, truncation, tampering) is ALSO
59
+ * treated as empty: the only consequence is the user re-runs `borg setup`,
60
+ * which is the right fail-safe for credential material — a hard crash on a
61
+ * corrupt dotfile would be worse UX than transparent re-auth.
62
+ */
63
+ export function makeEncryptedFileBackend(deps) {
64
+ const { filePath, key, fs } = deps;
65
+ async function readMap() {
66
+ let raw;
67
+ try {
68
+ raw = await fs.readFile(filePath);
69
+ }
70
+ catch {
71
+ return {}; // missing file → no stored tokens yet
72
+ }
73
+ try {
74
+ const json = decryptString(raw.trim(), key);
75
+ const parsed = JSON.parse(json);
76
+ return parsed && typeof parsed === 'object' ? parsed : {};
77
+ }
78
+ catch {
79
+ return {}; // undecryptable / corrupt → fail safe to re-auth
80
+ }
81
+ }
82
+ async function writeMap(map) {
83
+ await fs.mkdir(path.dirname(filePath), 0o700);
84
+ await fs.writeFile(filePath, encryptString(JSON.stringify(map), key), 0o600);
85
+ }
86
+ return {
87
+ name: 'encrypted-file',
88
+ async get(account) {
89
+ const map = await readMap();
90
+ return Object.prototype.hasOwnProperty.call(map, account) ? map[account] : null;
91
+ },
92
+ async set(account, value) {
93
+ const map = await readMap();
94
+ map[account] = value;
95
+ await writeMap(map);
96
+ },
97
+ async delete(account) {
98
+ const map = await readMap();
99
+ if (Object.prototype.hasOwnProperty.call(map, account)) {
100
+ delete map[account];
101
+ await writeMap(map);
102
+ }
103
+ },
104
+ };
105
+ }
106
+ /**
107
+ * Select the persistent backend: a forced choice (BORG_TOKEN_STORE=keychain|file)
108
+ * wins and skips the probe; otherwise probe the keychain and fall back to the
109
+ * encrypted file when it's unavailable.
110
+ */
111
+ export async function selectTokenBackend(deps) {
112
+ if (deps.forced === 'keychain')
113
+ return deps.makeKeychain();
114
+ if (deps.forced === 'file')
115
+ return deps.makeFile();
116
+ return (await deps.keyringAvailable()) ? deps.makeKeychain() : deps.makeFile();
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 async function readCallerManagedIdToken(deps) {
126
+ const inline = deps.env.BORG_TOKEN?.trim();
127
+ if (inline)
128
+ return inline;
129
+ const file = deps.env.BORG_TOKEN_FILE?.trim();
130
+ if (file) {
131
+ const contents = await deps.readFile(file);
132
+ return contents.trim();
133
+ }
134
+ return null;
135
+ }
136
+ //# 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.56",
3
+ "version": "0.9.58",
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",