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

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,22 @@
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.438",
6
+ "changes": [
7
+ "Live provider failures now keep their real diagnosis all the way through startup, repair, and the connect bay: expired credentials still prompt re-auth, but busy providers, provider outages, rate limits, quota failures, and network trouble now point humans toward retrying later, checking usage, or switching lanes instead of being mislabeled as an auth problem.",
8
+ "The readiness board and connect bay now share one classification-aware repair model, so a lane only says `needs credentials` when the live ping actually came back as an auth failure; other live-check failures stay in the broader `needs attention` bucket with matching next actions.",
9
+ "New daemon, connect-bay, human-readiness, hermetic-runtime, and full-suite coverage lock in the truthful failure guidance, and packaged-install verification confirms the fix survives beyond the repo checkout into the shipped CLI."
10
+ ]
11
+ },
12
+ {
13
+ "version": "0.1.0-alpha.437",
14
+ "changes": [
15
+ "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.",
16
+ "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.",
17
+ "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."
18
+ ]
19
+ },
4
20
  {
5
21
  "version": "0.1.0-alpha.436",
6
22
  "changes": [
@@ -298,12 +298,18 @@ function failedPingResult(agentName, lane, provider, model, result) {
298
298
  return {
299
299
  ok: false,
300
300
  error: `${lane} provider ${provider} model ${model} failed live check: ${result.message}`,
301
- fix: `Run 'ouro auth --agent ${agentName} --provider ${provider}' to refresh credentials.`,
301
+ fix: (0, readiness_repair_1.providerLiveCheckFix)({
302
+ agentName,
303
+ lane,
304
+ provider,
305
+ classification: result.classification,
306
+ }),
302
307
  issue: (0, readiness_repair_1.providerLiveCheckFailedIssue)({
303
308
  agentName,
304
309
  lane,
305
310
  provider,
306
311
  model,
312
+ classification: result.classification,
307
313
  message: result.message,
308
314
  }),
309
315
  };
@@ -6,6 +6,7 @@ exports.connectEntryNeedsAttention = connectEntryNeedsAttention;
6
6
  exports.renderConnectBay = renderConnectBay;
7
7
  const runtime_1 = require("../../nerves/runtime");
8
8
  const terminal_ui_1 = require("./terminal-ui");
9
+ const readiness_repair_1 = require("./readiness-repair");
9
10
  const CONNECT_STATUS_PRIORITY = {
10
11
  "needs attention": 0,
11
12
  locked: 1,
@@ -79,6 +80,16 @@ function extractCommand(fixHint, commandPrefix) {
79
80
  function resolveProviderHealthStatus(providerHealth) {
80
81
  if (!providerHealth || providerHealth.ok)
81
82
  return undefined;
83
+ const issue = providerHealth.issue;
84
+ if (issue?.kind === "vault-locked")
85
+ return "locked";
86
+ if (issue?.kind === "vault-unconfigured")
87
+ return "needs setup";
88
+ if (issue?.kind === "provider-credentials-missing")
89
+ return "needs credentials";
90
+ if (issue?.kind === "provider-live-check-failed") {
91
+ return issue.actions[0]?.kind === "provider-auth" ? "needs credentials" : "needs attention";
92
+ }
82
93
  const error = String(providerHealth.error).toLowerCase();
83
94
  const fix = String(providerHealth.fix).toLowerCase();
84
95
  if (error.includes("failed live check"))
@@ -99,7 +110,11 @@ function resolveProviderHealthStatus(providerHealth) {
99
110
  return "locked";
100
111
  return "needs attention";
101
112
  }
102
- function resolveProviderHealthCommand(fixHint, status) {
113
+ function resolveProviderHealthCommand(providerHealth, status) {
114
+ const issueCommand = (0, readiness_repair_1.preferredConnectRepairAction)(providerHealth?.issue)?.command;
115
+ if (issueCommand)
116
+ return issueCommand;
117
+ const fixHint = providerHealth?.fix;
103
118
  if (!fixHint)
104
119
  return undefined;
105
120
  const prefixes = status === "locked"
@@ -329,7 +344,7 @@ function renderNonTtyBay(entries, options) {
329
344
  }
330
345
  function summarizeProviderLane(agent, lane, providerHealth) {
331
346
  const providerHealthStatus = resolveProviderHealthStatus(providerHealth);
332
- const providerHealthCommand = resolveProviderHealthCommand(providerHealth?.fix, providerHealthStatus);
347
+ const providerHealthCommand = resolveProviderHealthCommand(providerHealth, providerHealthStatus);
333
348
  if (lane.status === "unconfigured") {
334
349
  return {
335
350
  lane: lane.lane,
@@ -372,7 +387,7 @@ function summarizeProviderLane(agent, lane, providerHealth) {
372
387
  status: "needs attention",
373
388
  title: `${lane.provider} / ${lane.model}`,
374
389
  detail: `failed live check: ${lane.readiness.error ?? "unknown error"}`,
375
- action: providerHealth?.fix ?? `ouro auth --agent ${agent} --provider ${lane.provider}`,
390
+ action: providerHealthCommand ?? providerHealth?.fix ?? `ouro auth --agent ${agent} --provider ${lane.provider}`,
376
391
  };
377
392
  }
378
393
  if (lane.readiness.status === "stale") {
@@ -404,7 +419,7 @@ function summarizeProvidersForConnect(agent, visibility, providerHealth) {
404
419
  const laneSummaries = visibility.lanes.map((lane) => summarizeProviderLane(agent, lane, providerHealth));
405
420
  const worstLaneStatus = laneSummaries.reduce((worst, lane) => CONNECT_STATUS_PRIORITY[lane.status] < CONNECT_STATUS_PRIORITY[worst] ? lane.status : worst, "ready");
406
421
  const providerHealthStatus = resolveProviderHealthStatus(providerHealth);
407
- const providerHealthCommand = resolveProviderHealthCommand(providerHealth?.fix, providerHealthStatus);
422
+ const providerHealthCommand = resolveProviderHealthCommand(providerHealth, providerHealthStatus);
408
423
  const nextLane = laneSummaries.find((lane) => isProblemStatus(lane.status));
409
424
  return {
410
425
  status: providerHealthStatus ?? worstLaneStatus,
@@ -22,7 +22,7 @@ function statusFromIssue(issue) {
22
22
  case "provider-credentials-missing":
23
23
  return "needs credentials";
24
24
  case "provider-live-check-failed":
25
- return "needs attention";
25
+ return issue.actions[0]?.kind === "provider-auth" ? "needs credentials" : "needs attention";
26
26
  case "generic":
27
27
  return "needs attention";
28
28
  }
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.vaultLockedIssue = vaultLockedIssue;
4
4
  exports.vaultUnconfiguredIssue = vaultUnconfiguredIssue;
5
5
  exports.providerCredentialMissingIssue = providerCredentialMissingIssue;
6
+ exports.providerLiveCheckFix = providerLiveCheckFix;
7
+ exports.preferredConnectRepairAction = preferredConnectRepairAction;
6
8
  exports.providerLiveCheckFailedIssue = providerLiveCheckFailedIssue;
7
9
  exports.genericReadinessIssue = genericReadinessIssue;
8
10
  exports.isKnownReadinessIssue = isKnownReadinessIssue;
@@ -92,6 +94,110 @@ function providerCredentialMissingIssue(input) {
92
94
  ],
93
95
  };
94
96
  }
97
+ function normalizeProviderLiveCheckClassification(classification) {
98
+ switch (classification) {
99
+ case "auth-failure":
100
+ case "usage-limit":
101
+ case "rate-limit":
102
+ case "server-error":
103
+ case "network-error":
104
+ case "unknown":
105
+ return classification;
106
+ default:
107
+ return "unknown";
108
+ }
109
+ }
110
+ function providerUseAction(input) {
111
+ return {
112
+ kind: "provider-use",
113
+ label: "Choose a different working provider/model",
114
+ command: `ouro use --agent ${input.agentName} --lane ${input.lane} --provider <provider> --model <model>`,
115
+ actor: "human-choice",
116
+ executable: false,
117
+ lane: input.lane,
118
+ };
119
+ }
120
+ function providerAuthAction(input) {
121
+ return {
122
+ kind: "provider-auth",
123
+ label: `Refresh ${input.provider} credentials`,
124
+ command: `ouro auth --agent ${input.agentName} --provider ${input.provider}`,
125
+ actor: "human-required",
126
+ provider: input.provider,
127
+ };
128
+ }
129
+ function providerRetryAction(input) {
130
+ return {
131
+ kind: "provider-retry",
132
+ label: input.label,
133
+ command: `ouro repair --agent ${input.agentName}`,
134
+ actor: "human-choice",
135
+ executable: false,
136
+ };
137
+ }
138
+ function providerLiveCheckFix(input) {
139
+ const classification = normalizeProviderLiveCheckClassification(input.classification);
140
+ const authCommand = `ouro auth --agent ${input.agentName} --provider ${input.provider}`;
141
+ const useCommand = `ouro use --agent ${input.agentName} --lane ${input.lane} --provider <provider> --model <model>`;
142
+ switch (classification) {
143
+ case "auth-failure":
144
+ return `Run '${authCommand}' to refresh credentials, or run '${useCommand}' to choose another provider/model for this lane.`;
145
+ case "usage-limit":
146
+ return `This usually means ${input.provider} hit a usage limit. Restore quota, then run 'ouro up' again. Or run '${useCommand}' to choose another provider/model for this lane.`;
147
+ case "rate-limit":
148
+ return `Run 'ouro up' again after a short wait. Or run '${useCommand}' to choose another provider/model for this lane.`;
149
+ case "server-error":
150
+ return `Run 'ouro up' again in a moment. If ${input.provider} keeps failing, run '${useCommand}' to choose another provider/model for this lane.`;
151
+ case "network-error":
152
+ return `Check the network or provider availability, then run 'ouro up' again. Or run '${useCommand}' to choose another provider/model for this lane.`;
153
+ case "unknown":
154
+ return `Run 'ouro up' again. If it keeps failing, run '${authCommand}' to refresh credentials or '${useCommand}' to choose another provider/model for this lane.`;
155
+ }
156
+ }
157
+ function providerLiveCheckActions(input) {
158
+ const classification = normalizeProviderLiveCheckClassification(input.classification);
159
+ const useAction = providerUseAction(input);
160
+ const authAction = providerAuthAction(input);
161
+ switch (classification) {
162
+ case "auth-failure":
163
+ return [authAction, useAction];
164
+ case "usage-limit":
165
+ return [
166
+ providerRetryAction({ agentName: input.agentName, label: "After restoring quota, check again" }),
167
+ useAction,
168
+ ];
169
+ case "rate-limit":
170
+ return [
171
+ providerRetryAction({ agentName: input.agentName, label: "Give it a minute, then check again" }),
172
+ useAction,
173
+ ];
174
+ case "server-error":
175
+ return [
176
+ providerRetryAction({ agentName: input.agentName, label: "Check again in a moment" }),
177
+ useAction,
178
+ ];
179
+ case "network-error":
180
+ return [
181
+ providerRetryAction({ agentName: input.agentName, label: "Check again after the network settles" }),
182
+ useAction,
183
+ authAction,
184
+ ];
185
+ case "unknown":
186
+ return [
187
+ providerRetryAction({ agentName: input.agentName, label: "Check again" }),
188
+ authAction,
189
+ useAction,
190
+ ];
191
+ }
192
+ }
193
+ function preferredConnectRepairAction(issue) {
194
+ if (!issue)
195
+ return undefined;
196
+ if (issue.kind === "provider-live-check-failed" && issue.actions[0]?.kind === "provider-retry") {
197
+ return issue.actions.find((action) => action.kind !== "provider-retry") ?? issue.actions[0];
198
+ }
199
+ return issue.actions[0];
200
+ }
95
201
  function providerLiveCheckFailedIssue(input) {
96
202
  return {
97
203
  kind: "provider-live-check-failed",
@@ -99,23 +205,7 @@ function providerLiveCheckFailedIssue(input) {
99
205
  actor: "human-choice",
100
206
  summary: `${input.agentName}: ${input.lane} provider ${input.provider} / ${input.model} failed live check`,
101
207
  detail: input.message,
102
- actions: [
103
- {
104
- kind: "provider-auth",
105
- label: `Refresh ${input.provider} credentials`,
106
- command: `ouro auth --agent ${input.agentName} --provider ${input.provider}`,
107
- actor: "human-required",
108
- provider: input.provider,
109
- },
110
- {
111
- kind: "provider-use",
112
- label: "Choose a different working provider/model for this lane",
113
- command: `ouro use --agent ${input.agentName} --lane ${input.lane} --provider <provider> --model <model>`,
114
- actor: "human-choice",
115
- executable: false,
116
- lane: input.lane,
117
- },
118
- ],
208
+ actions: providerLiveCheckActions(input),
119
209
  };
120
210
  }
121
211
  function genericReadinessIssue(input) {
@@ -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.438",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",