@ouro.bot/cli 0.1.0-alpha.436 → 0.1.0-alpha.437

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/changelog.json CHANGED
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.437",
6
+ "changes": [
7
+ "Provider-credential refresh now reads the known `providers/<provider>` vault items directly instead of starting with a full `bw list items` scan, so startup, repair, and connect-bay readiness stop paying whole-vault latency just to inspect the handful of provider records Ouro already names exactly.",
8
+ "Structured Ouro vault items now treat an exact-item `not found` from Bitwarden as a real miss instead of falling back into a fuzzy filtered search, which removes the slow retrying scan path for absent `providers/*` and `runtime/*` records without changing the safety fallback for malformed direct responses.",
9
+ "This closes the remaining real-world `connect` slowdown found after the live-ping work: local dogfood dropped the branch connect-bay readiness run from 127 seconds in the shipped path to 18 seconds after the direct provider-read fix, with new coverage locking the direct-read contract in place."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.436",
6
14
  "changes": [
@@ -96,6 +96,11 @@ function isPresentCredentialValue(value) {
96
96
  return value.trim().length > 0;
97
97
  return Number.isFinite(value) && value !== 0;
98
98
  }
99
+ function isMissingProviderCredentialError(message, itemName) {
100
+ const normalized = message.toLowerCase();
101
+ return normalized.includes(itemName.toLowerCase())
102
+ && (normalized.includes("no credential found") || normalized.includes("missing") || normalized.includes("not found"));
103
+ }
99
104
  function copyKnownFields(source, fields) {
100
105
  const result = {};
101
106
  for (const field of fields) {
@@ -225,17 +230,21 @@ async function refreshProviderCredentialPool(agentName, options = {}) {
225
230
  try {
226
231
  const store = (0, credential_access_1.getCredentialStore)(agentName);
227
232
  options.onProgress?.(`reading vault items for ${agentName}...`);
228
- const items = await store.list();
229
233
  const providers = {};
230
234
  let updatedAt = new Date(0).toISOString();
231
- for (const item of items) {
232
- if (!item.domain.startsWith(VAULT_ITEM_PREFIX))
233
- continue;
234
- const provider = item.domain.slice(VAULT_ITEM_PREFIX.length);
235
- if (!isAgentProvider(provider))
236
- continue;
235
+ for (const provider of VALID_PROVIDERS) {
236
+ const itemName = providerCredentialItemName(provider);
237
237
  options.onProgress?.(`reading ${provider} credentials...`);
238
- const raw = await store.getRawSecret(item.domain, "password");
238
+ let raw;
239
+ try {
240
+ raw = await store.getRawSecret(itemName, "password");
241
+ }
242
+ catch (error) {
243
+ const message = error instanceof Error ? error.message : String(error);
244
+ if (isMissingProviderCredentialError(message, itemName))
245
+ continue;
246
+ throw error;
247
+ }
239
248
  const payload = validateProviderCredentialPayload(JSON.parse(raw), provider);
240
249
  const record = recordFromPayload(payload);
241
250
  providers[provider] = record;
@@ -134,11 +134,13 @@ function isBwConfigLogoutRequired(err) {
134
134
  function shouldPreferExactItemLookup(domain) {
135
135
  return domain.includes("/");
136
136
  }
137
+ function isBwDirectLookupMissingError(err) {
138
+ const message = err.message.toLowerCase();
139
+ return message.includes("bw cli error: not found") || message.includes("bw cli error: item not found");
140
+ }
137
141
  function isBwDirectLookupFallbackError(err) {
138
142
  const message = err.message.toLowerCase();
139
- return (message.includes("bw cli error: not found") ||
140
- message.includes("bw cli error: item not found") ||
141
- message.includes("invalid json from bw get item") ||
143
+ return (message.includes("invalid json from bw get item") ||
142
144
  message.includes("invalid item from bw get item"));
143
145
  }
144
146
  // ---------------------------------------------------------------------------
@@ -246,14 +248,14 @@ async function withBwLock(appDataDir, fn) {
246
248
  }
247
249
  }
248
250
  }
249
- function execBw(args, sessionToken, appDataDir, stdin) {
251
+ function execBw(args, sessionToken, appDataDir, stdin, bwBinaryPath = "bw") {
250
252
  const env = {
251
253
  ...process.env,
252
254
  ...(sessionToken ? { BW_SESSION: sessionToken } : {}),
253
255
  ...(appDataDir ? { BITWARDENCLI_APPDATA_DIR: appDataDir } : {}),
254
256
  };
255
257
  const runCommand = () => new Promise((resolve, reject) => {
256
- const child = (0, node_child_process_1.execFile)("bw", args, { timeout: 30_000, env }, (err, stdout, stderr) => {
258
+ const child = (0, node_child_process_1.execFile)(bwBinaryPath, args, { timeout: 30_000, env }, (err, stdout, stderr) => {
257
259
  if (err) {
258
260
  if (isBwNotInstalled(err)) {
259
261
  reject(new Error("bw CLI not found. Install from https://bitwarden.com/help/cli/"));
@@ -274,7 +276,7 @@ function execBw(args, sessionToken, appDataDir, stdin) {
274
276
  function isBwNotInstalled(err) {
275
277
  const msg = err.message.toLowerCase();
276
278
  const code = err.code;
277
- return code === "ENOENT" || msg.includes("enoent") || msg.includes("not found") || msg.includes("command not found");
279
+ return code === "ENOENT" || /\bspawn\b.*\benoent\b/.test(msg) || msg.includes("command not found");
278
280
  }
279
281
  /** Check if the error is transient (network/timeout) and worth retrying. */
280
282
  function isTransientError(err) {
@@ -385,6 +387,7 @@ class BitwardenCredentialStore {
385
387
  masterPassword;
386
388
  appDataDir;
387
389
  sessionToken = null;
390
+ bwBinaryPath = "bw";
388
391
  constructor(serverUrl, email, masterPassword, options = {}) {
389
392
  this.serverUrl = serverUrl;
390
393
  this.email = email;
@@ -394,6 +397,9 @@ class BitwardenCredentialStore {
394
397
  isReady() {
395
398
  return true;
396
399
  }
400
+ execBw(args, sessionToken, stdin) {
401
+ return execBw(args, sessionToken, this.appDataDir, stdin, this.bwBinaryPath);
402
+ }
397
403
  /**
398
404
  * Ensure the bw CLI is authenticated and unlocked.
399
405
  * Handles three states: logged out → login, locked → unlock, already unlocked → no-op.
@@ -401,7 +407,7 @@ class BitwardenCredentialStore {
401
407
  */
402
408
  async login() {
403
409
  // Ensure bw CLI is installed before any bw commands
404
- await (0, bw_installer_1.ensureBwCli)();
410
+ this.bwBinaryPath = await (0, bw_installer_1.ensureBwCli)();
405
411
  if (this.appDataDir) {
406
412
  fs.mkdirSync(this.appDataDir, { recursive: true, mode: 0o700 });
407
413
  }
@@ -438,7 +444,7 @@ class BitwardenCredentialStore {
438
444
  // Check current status
439
445
  let status = {};
440
446
  try {
441
- const raw = await execBw(["status"], undefined, this.appDataDir);
447
+ const raw = await this.execBw(["status"]);
442
448
  status = JSON.parse(raw);
443
449
  }
444
450
  catch (err) {
@@ -451,7 +457,7 @@ class BitwardenCredentialStore {
451
457
  // Configure server URL if needed (only works when logged out)
452
458
  if (status.status === "unauthenticated" || !status.serverUrl) {
453
459
  try {
454
- await execBw(["config", "server", this.serverUrl], undefined, this.appDataDir);
460
+ await this.execBw(["config", "server", this.serverUrl]);
455
461
  }
456
462
  catch (error) {
457
463
  const err = error;
@@ -463,12 +469,12 @@ class BitwardenCredentialStore {
463
469
  }
464
470
  if (status.status === "locked") {
465
471
  // Already logged in, just needs unlock
466
- const unlockOutput = await execBw(["unlock", this.masterPassword, "--raw"], undefined, this.appDataDir);
472
+ const unlockOutput = await this.execBw(["unlock", this.masterPassword, "--raw"]);
467
473
  this.sessionToken = unlockOutput.trim();
468
474
  }
469
475
  else if (status.status === "unauthenticated" || !status.status) {
470
476
  // Not logged in — full login
471
- const loginOutput = await execBw(["login", this.email, this.masterPassword, "--raw"], undefined, this.appDataDir);
477
+ const loginOutput = await this.execBw(["login", this.email, this.masterPassword, "--raw"]);
472
478
  try {
473
479
  const parsed = JSON.parse(loginOutput);
474
480
  this.sessionToken = parsed.access_token ?? loginOutput.trim();
@@ -479,12 +485,12 @@ class BitwardenCredentialStore {
479
485
  }
480
486
  else {
481
487
  // Status is "unlocked" — already good, just need the session token
482
- const unlockOutput = await execBw(["unlock", this.masterPassword, "--raw"], undefined, this.appDataDir);
488
+ const unlockOutput = await this.execBw(["unlock", this.masterPassword, "--raw"]);
483
489
  this.sessionToken = unlockOutput.trim();
484
490
  }
485
491
  // Sync vault data after obtaining a fresh session token
486
492
  /* v8 ignore next -- defensive: loginAttempt always sets sessionToken before sync @preserve */
487
- await execBw(["sync"], this.sessionToken ?? undefined, this.appDataDir);
493
+ await this.execBw(["sync"], this.sessionToken ?? undefined);
488
494
  }
489
495
  async ensureSession() {
490
496
  if (!this.sessionToken) {
@@ -610,12 +616,12 @@ class BitwardenCredentialStore {
610
616
  const encoded = Buffer.from(JSON.stringify(item)).toString("base64");
611
617
  let savedItem;
612
618
  if (existing) {
613
- const stdout = await execBw(["edit", "item", existing.id], session, this.appDataDir, encoded);
619
+ const stdout = await this.execBw(["edit", "item", existing.id], session, encoded);
614
620
  const savedItemId = parseBwItemId(stdout) ?? existing.id;
615
621
  savedItem = await this.findItemById(savedItemId, session);
616
622
  }
617
623
  else {
618
- const stdout = await execBw(["create", "item"], session, this.appDataDir, encoded);
624
+ const stdout = await this.execBw(["create", "item"], session, encoded);
619
625
  const savedItemId = parseBwItemId(stdout);
620
626
  savedItem = savedItemId
621
627
  ? await this.findItemById(savedItemId, session)
@@ -637,7 +643,7 @@ class BitwardenCredentialStore {
637
643
  message: "listing bw credentials",
638
644
  meta: { backend: "bitwarden" },
639
645
  });
640
- const stdout = await this.withTransientRetry(() => this.withSessionRetry((session) => execBw(["list", "items"], session, this.appDataDir)));
646
+ const stdout = await this.withTransientRetry(() => this.withSessionRetry((session) => this.execBw(["list", "items"], session)));
641
647
  const items = parseBwItems(stdout, "bw list items");
642
648
  const results = items.map((item) => ({
643
649
  domain: item.name,
@@ -670,7 +676,7 @@ class BitwardenCredentialStore {
670
676
  });
671
677
  return false;
672
678
  }
673
- await this.withSessionRetry((session) => execBw(["delete", "item", item.id], session, this.appDataDir));
679
+ await this.withSessionRetry((session) => this.execBw(["delete", "item", item.id], session));
674
680
  (0, runtime_1.emitNervesEvent)({
675
681
  event: "repertoire.bw_credential_delete_end",
676
682
  component: "repertoire",
@@ -683,24 +689,26 @@ class BitwardenCredentialStore {
683
689
  async findItemByDomain(domain, session) {
684
690
  if (shouldPreferExactItemLookup(domain)) {
685
691
  try {
686
- const stdout = await execBw(["get", "item", domain], session, this.appDataDir);
692
+ const stdout = await this.execBw(["get", "item", domain], session);
687
693
  const item = parseBwItem(stdout, "bw get item");
688
694
  if (item.name === domain)
689
695
  return item;
690
696
  }
691
697
  catch (error) {
692
698
  const err = error;
699
+ if (isBwDirectLookupMissingError(err))
700
+ return null;
693
701
  if (!isBwDirectLookupFallbackError(err))
694
702
  throw err;
695
703
  }
696
704
  }
697
- const stdout = await execBw(["list", "items", "--search", domain], session, this.appDataDir);
705
+ const stdout = await this.execBw(["list", "items", "--search", domain], session);
698
706
  const items = parseBwItems(stdout, "bw list items --search");
699
707
  // Find exact match by name
700
708
  return items.find((item) => item.name === domain) ?? null;
701
709
  }
702
710
  async findItemById(id, session) {
703
- const stdout = await execBw(["get", "item", id], session, this.appDataDir);
711
+ const stdout = await this.execBw(["get", "item", id], session);
704
712
  return parseBwItem(stdout, "bw get item");
705
713
  }
706
714
  assertStoredCredentialMatches(domain, data, item) {
@@ -5,12 +5,50 @@
5
5
  * Mirrors the whisper-cpp pattern in senses/bluebubbles/media.ts:
6
6
  * check PATH first, install via npm if missing, emit nerves event.
7
7
  */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
8
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.findExecutableOnPath = findExecutableOnPath;
43
+ exports.findExecutableViaNpmPrefix = findExecutableViaNpmPrefix;
9
44
  exports.ensureBwCli = ensureBwCli;
10
45
  const node_child_process_1 = require("node:child_process");
46
+ const fs = __importStar(require("node:fs"));
47
+ const path = __importStar(require("node:path"));
11
48
  const runtime_1 = require("../nerves/runtime");
12
49
  const INSTALL_TIMEOUT_MS = 120_000;
13
50
  const WHICH_TIMEOUT_MS = 5_000;
51
+ const DEFAULT_WINDOWS_PATHEXT = ".EXE;.CMD;.BAT;.COM";
14
52
  function execFileAsync(cmd, args, timeout) {
15
53
  return new Promise((resolve, reject) => {
16
54
  (0, node_child_process_1.execFile)(cmd, args, { timeout }, (err, stdout) => {
@@ -22,20 +60,88 @@ function execFileAsync(cmd, args, timeout) {
22
60
  });
23
61
  });
24
62
  }
63
+ function stripWrappingQuotes(value) {
64
+ const trimmed = value.trim();
65
+ if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
66
+ return trimmed.slice(1, -1);
67
+ }
68
+ return trimmed;
69
+ }
70
+ function isExecutableFile(targetPath, platform) {
71
+ try {
72
+ fs.accessSync(targetPath, platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK);
73
+ return true;
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ function executableNames(command, platform, pathExt) {
80
+ if (platform !== "win32")
81
+ return [command];
82
+ if (path.extname(command))
83
+ return [command];
84
+ const extensions = pathExt
85
+ .split(";")
86
+ .map((entry) => entry.trim())
87
+ .filter((entry) => entry.length > 0);
88
+ return extensions.length === 0
89
+ ? [command]
90
+ : extensions.map((extension) => (extension.startsWith(".") ? `${command}${extension}` : `${command}.${extension}`));
91
+ }
92
+ function findExecutableInDirectory(command, directory, platform, pathExt) {
93
+ const cleanDirectory = stripWrappingQuotes(directory);
94
+ if (!cleanDirectory)
95
+ return null;
96
+ for (const candidateName of executableNames(command, platform, pathExt)) {
97
+ const candidatePath = path.isAbsolute(candidateName)
98
+ ? candidateName
99
+ : path.join(cleanDirectory, candidateName);
100
+ if (isExecutableFile(candidatePath, platform)) {
101
+ return candidatePath;
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+ function findExecutableOnPath(command, envPath = process.env.PATH ?? "", platform = process.platform, pathExt = process.env.PATHEXT ?? DEFAULT_WINDOWS_PATHEXT) {
107
+ if (path.isAbsolute(command) || command.includes(path.sep)) {
108
+ return isExecutableFile(command, platform) ? command : null;
109
+ }
110
+ for (const directory of envPath.split(path.delimiter)) {
111
+ const found = findExecutableInDirectory(command, directory, platform, pathExt);
112
+ if (found)
113
+ return found;
114
+ }
115
+ return null;
116
+ }
117
+ async function findExecutableViaNpmPrefix(command, platform = process.platform, pathExt = process.env.PATHEXT ?? DEFAULT_WINDOWS_PATHEXT) {
118
+ try {
119
+ const prefix = stripWrappingQuotes((await execFileAsync("npm", ["prefix", "-g"], WHICH_TIMEOUT_MS)).trim());
120
+ if (!prefix)
121
+ return null;
122
+ const searchDirs = platform === "win32"
123
+ ? [prefix, path.join(prefix, "bin")]
124
+ : [path.join(prefix, "bin"), prefix];
125
+ for (const directory of searchDirs) {
126
+ const found = findExecutableInDirectory(command, directory, platform, pathExt);
127
+ if (found)
128
+ return found;
129
+ }
130
+ }
131
+ catch {
132
+ // Prefix lookup is only a post-install fallback.
133
+ }
134
+ return null;
135
+ }
25
136
  /**
26
137
  * Ensure the `bw` CLI is available, installing it via npm if needed.
27
138
  * Returns the path to the `bw` binary.
28
139
  */
29
140
  async function ensureBwCli() {
30
141
  // 1. Check if bw is already in PATH
31
- try {
32
- const existing = (await execFileAsync("which", ["bw"], WHICH_TIMEOUT_MS)).trim();
33
- if (existing) {
34
- return existing;
35
- }
36
- }
37
- catch {
38
- // Not found — fall through to install
142
+ const existing = findExecutableOnPath("bw");
143
+ if (existing) {
144
+ return existing;
39
145
  }
40
146
  // 2. Install via npm
41
147
  (0, runtime_1.emitNervesEvent)({
@@ -60,20 +166,15 @@ async function ensureBwCli() {
60
166
  throw new Error(`failed to install bw CLI via npm: ${reason}`);
61
167
  }
62
168
  // 3. Verify installation and return path
63
- try {
64
- const installed = (await execFileAsync("which", ["bw"], WHICH_TIMEOUT_MS)).trim();
65
- if (installed) {
66
- (0, runtime_1.emitNervesEvent)({
67
- event: "repertoire.bw_cli_install_end",
68
- component: "repertoire",
69
- message: "bw CLI installed successfully",
70
- meta: { path: installed },
71
- });
72
- return installed;
73
- }
74
- }
75
- catch {
76
- // Fall through to error
169
+ const installed = findExecutableOnPath("bw") ?? await findExecutableViaNpmPrefix("bw");
170
+ if (installed) {
171
+ (0, runtime_1.emitNervesEvent)({
172
+ event: "repertoire.bw_cli_install_end",
173
+ component: "repertoire",
174
+ message: "bw CLI installed successfully",
175
+ meta: { path: installed },
176
+ });
177
+ return installed;
77
178
  }
78
- throw new Error("bw CLI installed via npm but binary not found in PATH");
179
+ throw new Error("bw CLI installed via npm but binary not found in PATH or npm global bin");
79
180
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.436",
3
+ "version": "0.1.0-alpha.437",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",