@bivy/bivy 0.16.18-staging.6 → 0.16.18-staging.8

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.
@@ -423,7 +423,7 @@ export class PiRuntime {
423
423
  modelsPath: path.join(piDir, "models.json"),
424
424
  allowModelNetwork,
425
425
  })
426
- : await createPiModelRuntime({ credsDir, piDir, allowModelNetwork });
426
+ : await createPiModelRuntime({ credsDir, piDir, allowModelNetwork, workspace: sessionManager.getCwd() || options.workspace });
427
427
  const backgroundShells = new BackgroundShellTracker();
428
428
  const createRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
429
429
  const sessionId = sessionManager.getSessionId();
@@ -10,23 +10,12 @@
10
10
  //
11
11
  // This keeps Bivy's hot credential path decoupled from Pi: Pi is just another
12
12
  // agent that reads the same store.
13
- import path from "node:path";
14
13
  import { createCredentialVault } from "./store.js";
15
- import { resolveCredential } from "./records.js";
14
+ import { selectCredential } from "./selection.js";
15
+ export { projectIdsFromWorkspace } from "./selection.js";
16
16
  import { loadPresets, defaultPresetsPath } from "./presets.js";
17
17
  /** Refresh an OAuth token this many ms before it expires (clock-skew guard). */
18
18
  const OAUTH_REFRESH_SKEW_MS = 60_000;
19
- /** Stable project identifiers discoverable without importing repo/session code. */
20
- export function projectIdsFromWorkspace(workspace) {
21
- const resolved = path.resolve(workspace);
22
- const ids = new Set([resolved, path.basename(resolved)]);
23
- for (const part of resolved.split(path.sep)) {
24
- const split = part.indexOf("__");
25
- if (split > 0 && split < part.length - 2)
26
- ids.add(`${part.slice(0, split)}/${part.slice(split + 2)}`);
27
- }
28
- return [...ids];
29
- }
30
19
  /** Resolver over Bivy's credential store, with OAuth refresh-on-read via the bridge. */
31
20
  export class NodeCredentialResolver {
32
21
  credsDir;
@@ -59,14 +48,7 @@ export class NodeCredentialResolver {
59
48
  // than guessing.
60
49
  const records = await this.store.listRecords().catch(() => []);
61
50
  const presets = this.presets();
62
- // Project assignments are ordinary preset mappings named `project:<id>`.
63
- // Bivy-managed clones encode owner/repo as owner__repo in their workspace
64
- // path; direct local workspaces also match their absolute path/basename.
65
- const explicitProject = context?.project?.trim();
66
- const workspace = context?.workspace?.trim();
67
- const projectCandidates = [explicitProject, ...(workspace ? projectIdsFromWorkspace(workspace) : [])].filter((value) => Boolean(value));
68
- const projectPreset = projectCandidates.map((value) => `project:${value}`).find((name) => presets.presets?.[name]?.[id]);
69
- const selection = resolveCredential(id, records, presets, { ...(projectPreset ? { preset: projectPreset } : {}), ...(context?.preferLabel ? { preferLabel: context.preferLabel } : {}) });
51
+ const selection = selectCredential(id, records, presets, context);
70
52
  if (!selection)
71
53
  return undefined;
72
54
  const source = selection.record.source;
@@ -0,0 +1,35 @@
1
+ import { defaultPresetsPath, loadPresets } from "./presets.js";
2
+ import { selectCredential } from "./selection.js";
3
+ /** A provider-addressed view of the vault for consumers without labeled accounts.
4
+ * Resolve on every operation; never copy a work credential into the default slot.
5
+ * Refresh writes stay attached to the selected record's label and metadata.
6
+ */
7
+ export function selectedCredentialStore(store, credsDir, context) {
8
+ const select = async (provider) => selectCredential(provider, await store.listRecords(), loadPresets(defaultPresetsPath(credsDir)), context)?.record;
9
+ return {
10
+ async read(provider) {
11
+ const record = await select(provider);
12
+ if (record?.source.kind !== "stored")
13
+ return undefined;
14
+ const { updatedAt: _updatedAt, ...credential } = record.source.cred;
15
+ return credential;
16
+ },
17
+ async list() {
18
+ const records = await store.listRecords();
19
+ const presets = loadPresets(defaultPresetsPath(credsDir));
20
+ return [...new Set(records.map((r) => r.provider))].flatMap((providerId) => {
21
+ const record = selectCredential(providerId, records, presets, context)?.record;
22
+ if (record?.source.kind !== "stored")
23
+ return [];
24
+ const credential = record.source.cred;
25
+ return [{ providerId, type: credential.type, ...(credential.type === "oauth" ? { expiresAt: credential.expires } : {}) }];
26
+ });
27
+ },
28
+ async modify(provider, fn) {
29
+ const record = await select(provider);
30
+ if (!record)
31
+ throw new Error(`No account selected for ${provider}`);
32
+ return store.modifyRecord(provider, record.label, fn);
33
+ },
34
+ };
35
+ }
@@ -0,0 +1,24 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ import path from "node:path";
3
+ import { resolveCredential } from "./records.js";
4
+ /** Stable project identifiers discoverable without importing repo/session code. */
5
+ export function projectIdsFromWorkspace(workspace) {
6
+ const resolved = path.resolve(workspace);
7
+ const ids = new Set([resolved, path.basename(resolved)]);
8
+ for (const part of resolved.split(path.sep)) {
9
+ const split = part.indexOf("__");
10
+ if (split > 0 && split < part.length - 2)
11
+ ids.add(`${part.slice(0, split)}/${part.slice(split + 2)}`);
12
+ }
13
+ return [...ids];
14
+ }
15
+ export function selectCredential(provider, records, presets, context) {
16
+ const id = provider.trim().toLowerCase();
17
+ const workspace = context?.workspace?.trim();
18
+ const projects = [context?.project?.trim(), ...(workspace ? projectIdsFromWorkspace(workspace) : [])].filter(Boolean);
19
+ const projectPreset = projects.map((value) => `project:${value}`).find((name) => presets.presets?.[name]?.[id]);
20
+ return resolveCredential(id, records, presets, {
21
+ ...(projectPreset ? { preset: projectPreset } : {}),
22
+ ...(context?.preferLabel ? { preferLabel: context.preferLabel } : {}),
23
+ });
24
+ }
@@ -11,6 +11,7 @@
11
11
  import path from "node:path";
12
12
  import { createCredentialVault } from "./credential-store.js";
13
13
  import { isNativeOAuthProvider } from "./oauth/model-oauth-providers.js";
14
+ import { selectedCredentialStore } from "../credentials/selected-store.js";
14
15
  /** Adapt Bivy's store to pi-ai's structurally-identical CredentialStore for injection. */
15
16
  export function piCredentialStore(store) {
16
17
  return store;
@@ -26,7 +27,7 @@ export async function createPiModelRuntime(opts) {
26
27
  const store = opts.store ?? createCredentialVault(opts.credsDir);
27
28
  const { ModelRuntime } = await import("@earendil-works/pi-coding-agent");
28
29
  return ModelRuntime.create({
29
- credentials: piCredentialStore(store),
30
+ credentials: piCredentialStore(selectedCredentialStore(store, opts.credsDir, { workspace: opts.workspace })),
30
31
  modelsPath: path.join(opts.piDir, "models.json"),
31
32
  allowModelNetwork: opts.allowModelNetwork ?? false,
32
33
  });
@@ -345,12 +345,14 @@ export function createRunTerminals(deps) {
345
345
  const discover = SESSION_DISCOVERY_BY_AGENT[agent];
346
346
  if (!discover)
347
347
  return undefined;
348
- for (let attempt = 0; attempt < TAKEOVER_DISCOVERY_ATTEMPTS; attempt++) {
348
+ const attempts = Math.max(1, Math.floor(deps.takeoverDiscoveryAttempts ?? TAKEOVER_DISCOVERY_ATTEMPTS));
349
+ const delayMs = Math.max(0, Math.floor(deps.takeoverDiscoveryDelayMs ?? TAKEOVER_DISCOVERY_DELAY_MS));
350
+ for (let attempt = 0; attempt < attempts; attempt++) {
349
351
  const ref = await discover(workspace, createdAt);
350
352
  if (ref)
351
353
  return ref;
352
- if (attempt + 1 < TAKEOVER_DISCOVERY_ATTEMPTS) {
353
- await new Promise((resolve) => setTimeout(resolve, TAKEOVER_DISCOVERY_DELAY_MS));
354
+ if (delayMs > 0 && attempt + 1 < attempts) {
355
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
354
356
  }
355
357
  }
356
358
  return undefined;
package/dist/terminal.js CHANGED
@@ -196,6 +196,8 @@ export class TerminalManager {
196
196
  env,
197
197
  });
198
198
  const now = Date.now();
199
+ let resolveExit;
200
+ const exitPromise = new Promise((resolve) => { resolveExit = resolve; });
199
201
  const entry = {
200
202
  proc,
201
203
  workspace: options.workspace,
@@ -209,6 +211,8 @@ export class TerminalManager {
209
211
  flushTimer: null,
210
212
  closed: false,
211
213
  onData: options.onData,
214
+ exitPromise,
215
+ resolveExit,
212
216
  };
213
217
  // Register the opener as a sized client so a later, smaller client shrinks
214
218
  // the PTY to the min of the two rather than clobbering the opener's size.
@@ -269,7 +273,12 @@ export class TerminalManager {
269
273
  flush();
270
274
  entry.closed = true;
271
275
  this.terminals.delete(id);
272
- options.onExit(exitCode, signal, entry.buffer);
276
+ try {
277
+ options.onExit(exitCode, signal, entry.buffer);
278
+ }
279
+ finally {
280
+ entry.resolveExit();
281
+ }
273
282
  });
274
283
  return id;
275
284
  }
@@ -347,6 +356,19 @@ export class TerminalManager {
347
356
  const entry = this.terminals.get(id);
348
357
  if (!entry)
349
358
  return false;
359
+ this.closeEntry(id, entry);
360
+ return true;
361
+ }
362
+ /** Close a terminal and wait for the underlying PTY process to report exit. */
363
+ async closeAndWait(id, timeoutMs = 2000) {
364
+ const entry = this.terminals.get(id);
365
+ if (!entry)
366
+ return false;
367
+ this.closeEntry(id, entry);
368
+ await waitForExit(entry.exitPromise, timeoutMs);
369
+ return true;
370
+ }
371
+ closeEntry(id, entry) {
350
372
  this.terminals.delete(id);
351
373
  // Drop any queued output — the client asked to close, so don't emit a
352
374
  // trailing batch (which would fire onData for a terminal it has torn down).
@@ -363,7 +385,6 @@ export class TerminalManager {
363
385
  catch {
364
386
  // already gone
365
387
  }
366
- return true;
367
388
  }
368
389
  has(id) {
369
390
  return this.terminals.has(id);
@@ -421,6 +442,28 @@ export class TerminalManager {
421
442
  for (const id of [...this.terminals.keys()])
422
443
  this.close(id);
423
444
  }
445
+ /** Kill every terminal and wait for node-pty to release its child handles. */
446
+ async disposeAllAndWait(timeoutMs = 2000) {
447
+ const exits = [];
448
+ for (const [id, entry] of [...this.terminals]) {
449
+ this.closeEntry(id, entry);
450
+ exits.push(waitForExit(entry.exitPromise, timeoutMs));
451
+ }
452
+ await Promise.all(exits);
453
+ }
454
+ }
455
+ async function waitForExit(exitPromise, timeoutMs) {
456
+ let timer;
457
+ try {
458
+ await Promise.race([
459
+ exitPromise,
460
+ new Promise((resolve) => { timer = setTimeout(resolve, timeoutMs); }),
461
+ ]);
462
+ }
463
+ finally {
464
+ if (timer)
465
+ clearTimeout(timer);
466
+ }
424
467
  }
425
468
  function clampDim(value, fallback) {
426
469
  const n = Math.floor(Number(value));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.16.18-staging.6",
3
+ "version": "0.16.18-staging.8",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-only",
6
6
  "description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",