@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.
Files changed (3) hide show
  1. package/README.md +9 -7
  2. package/dist/cli.js +111 -54
  3. 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
- - `spaceId`
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 simple Space ID command for the current space.
69
- This mode identifies the Space, not a specific user:
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 --space-id "org_..."
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 --space-id "org_..." --workdir "$HOME/.buda/agents"
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 identities keyed by normalized server origin)
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. Existing single-device config files are upgraded in place on first use.
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: 2,
258
+ version: 3,
221
259
  ...typeof candidate.systemDeviceId === "string" ? { systemDeviceId: candidate.systemDeviceId } : {},
222
- servers: candidate.servers
260
+ profiles: migrateLegacyProfiles(Object.values(candidate.servers))
223
261
  },
224
- needsWrite: candidate.version !== 2
262
+ needsWrite: true
225
263
  };
226
264
  }
227
265
  if (isDeviceConfig(parsed)) {
228
266
  return {
229
267
  store: {
230
- version: 2,
231
- servers: { [normalizeServerOrigin(parsed.serverUrl)]: parsed }
268
+ version: 3,
269
+ profiles: migrateLegacyProfiles([parsed])
232
270
  },
233
271
  needsWrite: true
234
272
  };
235
273
  }
236
- return { store: { version: 2, servers: {} }, needsWrite: false };
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 origin = normalizeServerOrigin(serverUrl);
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 storedConfig = store.servers[origin];
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.servers[origin] = config;
317
+ store.profiles[getConnectorProfileKey(config.serverUrl, config.token)] = config;
292
318
  }
293
- if (needsWrite || migratedScopedConfig || normalizedWorkdirRoot) {
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.servers[normalizeServerOrigin(config.serverUrl)] = config;
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.servers).find(isDeviceConfig)?.deviceId;
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 existing = await readDeviceConfig(launchConfig);
330
- if (existing) {
331
- const overrideToken = getConnectorToken(launchConfig);
332
- return {
333
- ...existing,
334
- serverUrl: getServerUrl(launchConfig),
335
- workdirRoot: launchConfig?.workdirRoot || process.env.BUDA_CONNECTOR_WORKDIR || existing.workdirRoot || getWorkdirRoot(launchConfig),
336
- ...overrideToken ? { token: overrideToken } : {}
337
- };
338
- }
339
- const config = await createDeviceConfig(launchConfig);
340
- await writeDeviceConfig(config);
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 = existing ? await ensureDeviceConfig(launchConfig) : await createDeviceConfig(launchConfig);
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 = existing ? await ensureDeviceConfig(launchConfig) : await createDeviceConfig(launchConfig);
2982
+ const config = await ensureDeviceConfig(launchConfig);
2936
2983
  const paths = getConnectorPaths(config.serverUrl);
2937
- const daemonLockTarget = path9.join(
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
- const daemonLock = await acquireExclusiveFileLock(daemonLockTarget, { timeoutMs: 0 }).catch(
2945
- () => {
2946
- throw new Error(
2947
- `Another Buda Connector daemon is already running for ${config.serverUrl}. Stop it before starting another instance.`
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buda-ai/connector",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "publishConfig": {