@buda-ai/connector 0.1.2 → 0.1.4
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 +9 -7
- package/dist/cli.js +111 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ printf '{"serverUrl":"https://buda.im","oauthToken":"..."}\n' \
|
|
|
57
57
|
Supported launch config fields:
|
|
58
58
|
|
|
59
59
|
- `serverUrl`
|
|
60
|
-
- `
|
|
60
|
+
- `token`
|
|
61
61
|
- `workdirRoot`
|
|
62
62
|
- `oauthToken`
|
|
63
63
|
- `bunnyDaemonHost`
|
|
@@ -65,11 +65,11 @@ Supported launch config fields:
|
|
|
65
65
|
|
|
66
66
|
## One-line install
|
|
67
67
|
|
|
68
|
-
Buda Space Settings can show a
|
|
69
|
-
|
|
68
|
+
Buda Space Settings can show a connector token command for the current space.
|
|
69
|
+
The token selects that Space's independent local connector profile:
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
|
-
npx -y @buda-ai/connector@latest daemon --
|
|
72
|
+
npx -y @buda-ai/connector@latest daemon --token "cet_..."
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
Desktop and other authenticated clients should use OAuth token launch config
|
|
@@ -83,17 +83,19 @@ printf '%s\n' '{"serverUrl":"https://buda.im","oauthToken":"..."}' \
|
|
|
83
83
|
You can also pass launch options directly:
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
|
-
buda-connector daemon --
|
|
86
|
+
buda-connector daemon --token "cet_..." --workdir "$HOME/.buda/agents"
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
## Local Files
|
|
90
90
|
|
|
91
|
-
- `~/.buda/device.json` (versioned device
|
|
91
|
+
- `~/.buda/device.json` (versioned device profiles keyed by server origin and token fingerprint)
|
|
92
92
|
- `~/.buda/connector.log`
|
|
93
93
|
- `~/.buda/agents/`
|
|
94
94
|
|
|
95
95
|
Server URL paths and query strings do not create a new identity. Scheme and non-default port
|
|
96
|
-
changes do.
|
|
96
|
+
changes do. Connector tokens for different Spaces on the same server select independent profiles,
|
|
97
|
+
so their daemons can run concurrently. Raw tokens are never used in profile keys or lock paths.
|
|
98
|
+
The installation-level physical device ID remains shared across profiles.
|
|
97
99
|
|
|
98
100
|
New server profiles share a privacy-preserving device ID derived by hashing the operating
|
|
99
101
|
system's stable machine identifier. The raw machine identifier is never stored or sent. If the
|
package/dist/cli.js
CHANGED
|
@@ -147,6 +147,13 @@ function getServerOriginKey(serverUrl) {
|
|
|
147
147
|
const digest = createHash2("sha256").update(origin).digest("hex").slice(0, 12);
|
|
148
148
|
return `${hostnameKey}-${digest}`;
|
|
149
149
|
}
|
|
150
|
+
function getConnectorTokenFingerprint(token) {
|
|
151
|
+
return createHash2("sha256").update(`buda:connector-profile:v1:${token.trim()}`, "utf8").digest("hex");
|
|
152
|
+
}
|
|
153
|
+
function getConnectorProfileKey(serverUrl, token) {
|
|
154
|
+
const discriminator = token?.trim() ? getConnectorTokenFingerprint(token) : "legacy-no-token";
|
|
155
|
+
return `${getServerOriginKey(serverUrl)}--${discriminator}`;
|
|
156
|
+
}
|
|
150
157
|
function getConnectorPaths(serverUrl, configRoot = path.join(homedir(), ".buda")) {
|
|
151
158
|
const scopedLegacyDir = path.join(configRoot, "servers", getServerOriginKey(serverUrl));
|
|
152
159
|
return {
|
|
@@ -158,6 +165,16 @@ function getConnectorPaths(serverUrl, configRoot = path.join(homedir(), ".buda")
|
|
|
158
165
|
scopedLegacyWorkdirRoot: path.join(scopedLegacyDir, "agents")
|
|
159
166
|
};
|
|
160
167
|
}
|
|
168
|
+
function getConnectorDaemonLockTarget(serverUrl, connectorId, configRoot = path.join(homedir(), ".buda")) {
|
|
169
|
+
const connectorKey = connectorId.replace(/[^a-z0-9_-]+/gi, "-").toLowerCase();
|
|
170
|
+
return path.join(
|
|
171
|
+
configRoot,
|
|
172
|
+
"daemons",
|
|
173
|
+
getServerOriginKey(serverUrl),
|
|
174
|
+
connectorKey,
|
|
175
|
+
"connector-daemon"
|
|
176
|
+
);
|
|
177
|
+
}
|
|
161
178
|
function getServerUrl(launchConfig) {
|
|
162
179
|
return (launchConfig?.serverUrl || process.env.BUDA_CONNECTOR_SERVER_URL || process.env.BUDA_SERVER_URL || process.env.NEXT_PUBLIC_APP_URL || DEFAULT_SERVER_URL).replace(/\/$/, "");
|
|
163
180
|
}
|
|
@@ -211,29 +228,50 @@ async function readJsonFile(configPath) {
|
|
|
211
228
|
return null;
|
|
212
229
|
}
|
|
213
230
|
}
|
|
231
|
+
function migrateLegacyProfiles(values) {
|
|
232
|
+
const profiles = {};
|
|
233
|
+
for (const value of values) {
|
|
234
|
+
if (!isDeviceConfig(value)) continue;
|
|
235
|
+
try {
|
|
236
|
+
profiles[getConnectorProfileKey(value.serverUrl, value.token)] = value;
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return profiles;
|
|
241
|
+
}
|
|
214
242
|
async function loadDeviceConfigStore(configPath) {
|
|
215
243
|
const parsed = await readJsonFile(configPath);
|
|
216
244
|
const candidate = parsed;
|
|
245
|
+
if (candidate?.version === 3 && candidate.profiles && typeof candidate.profiles === "object") {
|
|
246
|
+
return {
|
|
247
|
+
store: {
|
|
248
|
+
version: 3,
|
|
249
|
+
...typeof candidate.systemDeviceId === "string" ? { systemDeviceId: candidate.systemDeviceId } : {},
|
|
250
|
+
profiles: candidate.profiles
|
|
251
|
+
},
|
|
252
|
+
needsWrite: false
|
|
253
|
+
};
|
|
254
|
+
}
|
|
217
255
|
if (candidate && (candidate.version === 1 || candidate.version === 2) && candidate.servers && typeof candidate.servers === "object") {
|
|
218
256
|
return {
|
|
219
257
|
store: {
|
|
220
|
-
version:
|
|
258
|
+
version: 3,
|
|
221
259
|
...typeof candidate.systemDeviceId === "string" ? { systemDeviceId: candidate.systemDeviceId } : {},
|
|
222
|
-
|
|
260
|
+
profiles: migrateLegacyProfiles(Object.values(candidate.servers))
|
|
223
261
|
},
|
|
224
|
-
needsWrite:
|
|
262
|
+
needsWrite: true
|
|
225
263
|
};
|
|
226
264
|
}
|
|
227
265
|
if (isDeviceConfig(parsed)) {
|
|
228
266
|
return {
|
|
229
267
|
store: {
|
|
230
|
-
version:
|
|
231
|
-
|
|
268
|
+
version: 3,
|
|
269
|
+
profiles: migrateLegacyProfiles([parsed])
|
|
232
270
|
},
|
|
233
271
|
needsWrite: true
|
|
234
272
|
};
|
|
235
273
|
}
|
|
236
|
-
return { store: { version:
|
|
274
|
+
return { store: { version: 3, profiles: {} }, needsWrite: false };
|
|
237
275
|
}
|
|
238
276
|
function normalizeStoredWorkdirRoot(config, paths) {
|
|
239
277
|
const configuredRoot = config.workdirRoot?.trim();
|
|
@@ -264,33 +302,21 @@ async function writeDeviceConfigStore(configPath, store) {
|
|
|
264
302
|
async function readDeviceConfig(launchConfig, configRoot) {
|
|
265
303
|
const serverUrl = getServerUrl(launchConfig);
|
|
266
304
|
const paths = getConnectorPaths(serverUrl, configRoot);
|
|
267
|
-
const
|
|
305
|
+
const launchToken = getConnectorToken(launchConfig);
|
|
268
306
|
await mkdir(paths.configDir, { recursive: true });
|
|
269
307
|
return withExclusiveFileLock(paths.configPath, async () => {
|
|
270
308
|
const { store, needsWrite } = await loadDeviceConfigStore(paths.configPath);
|
|
271
|
-
const
|
|
309
|
+
const profileKey = getConnectorProfileKey(serverUrl, launchToken);
|
|
310
|
+
const storedConfig = store.profiles[profileKey];
|
|
272
311
|
let config = isDeviceConfig(storedConfig) ? storedConfig : null;
|
|
273
|
-
let migratedScopedConfig = false;
|
|
274
312
|
let normalizedWorkdirRoot = false;
|
|
275
|
-
if (!config) {
|
|
276
|
-
const scopedLegacy = await readJsonFile(paths.scopedLegacyConfigPath);
|
|
277
|
-
if (isDeviceConfig(scopedLegacy)) {
|
|
278
|
-
try {
|
|
279
|
-
if (normalizeServerOrigin(scopedLegacy.serverUrl) === origin) {
|
|
280
|
-
config = scopedLegacy;
|
|
281
|
-
migratedScopedConfig = true;
|
|
282
|
-
}
|
|
283
|
-
} catch {
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
313
|
if (config) {
|
|
288
314
|
const normalized = normalizeStoredWorkdirRoot(config, paths);
|
|
289
315
|
config = normalized.config;
|
|
290
316
|
normalizedWorkdirRoot = normalized.changed;
|
|
291
|
-
store.
|
|
317
|
+
store.profiles[getConnectorProfileKey(config.serverUrl, config.token)] = config;
|
|
292
318
|
}
|
|
293
|
-
if (needsWrite ||
|
|
319
|
+
if (needsWrite || normalizedWorkdirRoot) {
|
|
294
320
|
await writeDeviceConfigStore(paths.configPath, store);
|
|
295
321
|
}
|
|
296
322
|
return config;
|
|
@@ -302,7 +328,7 @@ async function writeDeviceConfig(config, configRoot) {
|
|
|
302
328
|
await mkdir(paths.configDir, { recursive: true });
|
|
303
329
|
await withExclusiveFileLock(paths.configPath, async () => {
|
|
304
330
|
const { store } = await loadDeviceConfigStore(paths.configPath);
|
|
305
|
-
store.
|
|
331
|
+
store.profiles[getConnectorProfileKey(config.serverUrl, config.token)] = config;
|
|
306
332
|
await writeDeviceConfigStore(paths.configPath, store);
|
|
307
333
|
});
|
|
308
334
|
}
|
|
@@ -315,7 +341,7 @@ async function getOrCreateSystemDeviceId(configRoot, resolver = resolveSystemDev
|
|
|
315
341
|
if (needsWrite) await writeDeviceConfigStore(paths.configPath, store);
|
|
316
342
|
return store.systemDeviceId;
|
|
317
343
|
}
|
|
318
|
-
const existingDeviceId = Object.values(store.
|
|
344
|
+
const existingDeviceId = Object.values(store.profiles).find(isDeviceConfig)?.deviceId;
|
|
319
345
|
store.systemDeviceId = resolver() || existingDeviceId || `dvc_${randomUUID()}`;
|
|
320
346
|
await writeDeviceConfigStore(paths.configPath, store);
|
|
321
347
|
return store.systemDeviceId;
|
|
@@ -325,19 +351,40 @@ async function createDeviceConfig(launchConfig, configRoot, resolver) {
|
|
|
325
351
|
const deviceId = await getOrCreateSystemDeviceId(configRoot, resolver);
|
|
326
352
|
return buildDeviceConfig(launchConfig, deviceId);
|
|
327
353
|
}
|
|
328
|
-
async function ensureDeviceConfig(launchConfig) {
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
354
|
+
async function ensureDeviceConfig(launchConfig, configRoot, resolver = resolveSystemDeviceId) {
|
|
355
|
+
const serverUrl = getServerUrl(launchConfig);
|
|
356
|
+
const paths = getConnectorPaths(serverUrl, configRoot);
|
|
357
|
+
const token = getConnectorToken(launchConfig);
|
|
358
|
+
const profileKey = getConnectorProfileKey(serverUrl, token);
|
|
359
|
+
await mkdir(paths.configDir, { recursive: true });
|
|
360
|
+
const config = await withExclusiveFileLock(paths.configPath, async () => {
|
|
361
|
+
const { store, needsWrite } = await loadDeviceConfigStore(paths.configPath);
|
|
362
|
+
const storedConfig = store.profiles[profileKey];
|
|
363
|
+
if (isDeviceConfig(storedConfig)) {
|
|
364
|
+
const normalized = normalizeStoredWorkdirRoot(storedConfig, paths);
|
|
365
|
+
if (needsWrite || normalized.changed) {
|
|
366
|
+
store.profiles[profileKey] = normalized.config;
|
|
367
|
+
await writeDeviceConfigStore(paths.configPath, store);
|
|
368
|
+
}
|
|
369
|
+
const existing = normalized.config;
|
|
370
|
+
const overrideToken = getConnectorToken(launchConfig);
|
|
371
|
+
return {
|
|
372
|
+
...existing,
|
|
373
|
+
serverUrl,
|
|
374
|
+
workdirRoot: launchConfig?.workdirRoot || process.env.BUDA_CONNECTOR_WORKDIR || existing.workdirRoot || getWorkdirRoot(launchConfig),
|
|
375
|
+
...overrideToken ? { token: overrideToken } : {}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
if (!store.systemDeviceId) {
|
|
379
|
+
const existingDeviceId = Object.values(store.profiles).find(isDeviceConfig)?.deviceId;
|
|
380
|
+
store.systemDeviceId = resolver() || existingDeviceId || `dvc_${randomUUID()}`;
|
|
381
|
+
}
|
|
382
|
+
const created = buildDeviceConfig(launchConfig, store.systemDeviceId);
|
|
383
|
+
store.profiles[profileKey] = created;
|
|
384
|
+
await writeDeviceConfigStore(paths.configPath, store);
|
|
385
|
+
return created;
|
|
386
|
+
});
|
|
387
|
+
await mkdir(config.workdirRoot, { recursive: true });
|
|
341
388
|
return config;
|
|
342
389
|
}
|
|
343
390
|
function parseLaunchConfig(raw) {
|
|
@@ -2916,7 +2963,7 @@ import { mkdir as mkdir9 } from "fs/promises";
|
|
|
2916
2963
|
import path9 from "path";
|
|
2917
2964
|
async function runNewConnector(launchConfig) {
|
|
2918
2965
|
const existing = await readDeviceConfig(launchConfig);
|
|
2919
|
-
const config =
|
|
2966
|
+
const config = await ensureDeviceConfig(launchConfig);
|
|
2920
2967
|
const paths = getConnectorPaths(config.serverUrl);
|
|
2921
2968
|
await migrateLegacyWorkspaceRoot(config.workdirRoot);
|
|
2922
2969
|
await provisionWorkdirLayout(config.workdirRoot);
|
|
@@ -2932,22 +2979,15 @@ async function runNewConnector(launchConfig) {
|
|
|
2932
2979
|
}
|
|
2933
2980
|
async function runConnectorDaemon(launchConfig) {
|
|
2934
2981
|
const existing = await readDeviceConfig(launchConfig);
|
|
2935
|
-
const config =
|
|
2982
|
+
const config = await ensureDeviceConfig(launchConfig);
|
|
2936
2983
|
const paths = getConnectorPaths(config.serverUrl);
|
|
2937
|
-
const daemonLockTarget =
|
|
2938
|
-
paths.configDir,
|
|
2939
|
-
"servers",
|
|
2940
|
-
getServerOriginKey(config.serverUrl),
|
|
2941
|
-
"connector-daemon"
|
|
2942
|
-
);
|
|
2984
|
+
const daemonLockTarget = getConnectorDaemonLockTarget(config.serverUrl, config.connectorId);
|
|
2943
2985
|
await mkdir9(path9.dirname(daemonLockTarget), { recursive: true, mode: 448 });
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
}
|
|
2950
|
-
);
|
|
2986
|
+
let daemonLock = await acquireExclusiveFileLock(daemonLockTarget, { timeoutMs: 0 }).catch(() => {
|
|
2987
|
+
throw new Error(
|
|
2988
|
+
`Another Buda Connector daemon is already running for this profile on ${config.serverUrl}. Stop it before starting another instance.`
|
|
2989
|
+
);
|
|
2990
|
+
});
|
|
2951
2991
|
let bunnyDaemon;
|
|
2952
2992
|
let relayProtocol;
|
|
2953
2993
|
try {
|
|
@@ -2959,9 +2999,26 @@ async function runConnectorDaemon(launchConfig) {
|
|
|
2959
2999
|
const registration = await registerConnector(config, launchConfig, activeBunnyDaemon, {
|
|
2960
3000
|
recoverMissingProfile: !existing
|
|
2961
3001
|
});
|
|
3002
|
+
const connectorIdChanged = config.connectorId !== registration.connectorId;
|
|
3003
|
+
if (connectorIdChanged) {
|
|
3004
|
+
const registeredLockTarget = getConnectorDaemonLockTarget(
|
|
3005
|
+
config.serverUrl,
|
|
3006
|
+
registration.connectorId
|
|
3007
|
+
);
|
|
3008
|
+
await mkdir9(path9.dirname(registeredLockTarget), { recursive: true, mode: 448 });
|
|
3009
|
+
const registeredLock = await acquireExclusiveFileLock(registeredLockTarget, {
|
|
3010
|
+
timeoutMs: 0
|
|
3011
|
+
}).catch(() => {
|
|
3012
|
+
throw new Error(
|
|
3013
|
+
`Another Buda Connector daemon is already running for this profile on ${config.serverUrl}. Stop it before starting another instance.`
|
|
3014
|
+
);
|
|
3015
|
+
});
|
|
3016
|
+
await daemonLock.release();
|
|
3017
|
+
daemonLock = registeredLock;
|
|
3018
|
+
}
|
|
2962
3019
|
config.connectorId = registration.connectorId;
|
|
2963
3020
|
relayProtocol = registration.relayProtocol;
|
|
2964
|
-
if (!existing) await writeDeviceConfig(config);
|
|
3021
|
+
if (!existing || connectorIdChanged) await writeDeviceConfig(config);
|
|
2965
3022
|
const relay = startConnectorRelay(
|
|
2966
3023
|
config,
|
|
2967
3024
|
activeBunnyDaemon,
|