@ouro.bot/cli 0.1.0-alpha.446 → 0.1.0-alpha.447

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.447",
6
+ "changes": [
7
+ "`ouro connect` now checks only the providers selected for the current agent lanes during preflight, and those live reads avoid mutating the older provider snapshot cache. That keeps the command focused on what this machine actually needs instead of paying whole-vault latency for unrelated provider records.",
8
+ "Structured vault reads for `providers/*` and `runtime/config` now reuse one short-lived Bitwarden item listing per store instance and skip redundant `bw sync` calls while the local Bitwarden cache is fresh, cutting repeated vault startup work without adding any disk credential cache.",
9
+ "The connections screen now lets the fresh live provider check outrank stale local credential visibility, so a provider that just passed appears ready and a provider that just failed shows the real live-check failure instead of falling back to misleading `credentials missing` guidance. `@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the connect preflight latency release."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.446",
6
14
  "changes": [
@@ -330,12 +330,20 @@ function selectedProviderPlan(agentName, state) {
330
330
  ...["outward", "inner"].map((lane) => `- ${laneAudienceLabel(lane)}: ${bindingLabel(state.lanes[lane])}`),
331
331
  ].join("\n");
332
332
  }
333
+ function selectedProvidersForState(state) {
334
+ return [...new Set(["outward", "inner"].map((lane) => state.lanes[lane].provider))];
335
+ }
333
336
  function mapVaultRefreshProgress(agentName, onProgress) {
334
337
  return (message) => {
335
338
  if (message.startsWith("reading vault items for ")) {
336
339
  onProgress(`${agentName}: opening saved provider credentials in the vault`);
337
340
  return;
338
341
  }
342
+ const providerRead = message.match(/^reading ([a-z0-9-]+) credentials\.\.\.$/i);
343
+ if (providerRead) {
344
+ onProgress(`${agentName}: reading saved ${providerRead[1]} credentials`);
345
+ return;
346
+ }
339
347
  if (message === "parsing provider credentials...") {
340
348
  onProgress(`${agentName}: organizing saved provider credentials`);
341
349
  }
@@ -381,7 +389,12 @@ async function checkAgentConfigWithProviderHealth(agentName, bundlesRoot, deps =
381
389
  return { ok: true };
382
390
  deps.onProgress?.(selectedProviderPlan(agentName, stateResult.state));
383
391
  const ping = deps.pingProvider ?? (await Promise.resolve().then(() => __importStar(require("../provider-ping")))).pingProvider;
384
- const poolResult = await (0, provider_credentials_1.refreshProviderCredentialPool)(agentName, deps.onProgress ? { onProgress: mapVaultRefreshProgress(agentName, deps.onProgress) } : undefined);
392
+ const providers = selectedProvidersForState(stateResult.state);
393
+ const poolResult = await (0, provider_credentials_1.refreshProviderCredentialPool)(agentName, {
394
+ ...(deps.onProgress ? { onProgress: mapVaultRefreshProgress(agentName, deps.onProgress) } : {}),
395
+ providers,
396
+ skipCache: true,
397
+ });
385
398
  const pingGroups = new Map();
386
399
  const lanes = ["outward", "inner"];
387
400
  for (const lane of lanes) {
@@ -2060,11 +2060,33 @@ async function buildConnectMenu(agent, deps, onProgress) {
2060
2060
  onProgress?.("loading this machine's settings");
2061
2061
  const machineRuntime = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, currentMachineId(deps), { preserveCachedOnFailure: true });
2062
2062
  const { teamsEnabled, blueBubblesEnabled } = readConnectBaySenseFlags(agent, deps);
2063
- let perplexityStatus;
2064
- let perplexityDetailLines;
2065
2063
  const perplexityApiKey = runtimeConfig.ok
2066
2064
  ? readRuntimeConfigString(runtimeConfig.config, "integrations.perplexityApiKey")
2067
2065
  : null;
2066
+ const embeddingsApiKey = runtimeConfig.ok
2067
+ ? readRuntimeConfigString(runtimeConfig.config, "integrations.openaiEmbeddingsApiKey")
2068
+ : null;
2069
+ const shouldVerifyPerplexity = runtimeConfig.ok && !!perplexityApiKey;
2070
+ const shouldVerifyEmbeddings = runtimeConfig.ok && !!embeddingsApiKey;
2071
+ let perplexityVerification;
2072
+ let embeddingsVerification;
2073
+ if (shouldVerifyPerplexity && shouldVerifyEmbeddings) {
2074
+ onProgress?.("verifying Perplexity search and memory embeddings");
2075
+ [perplexityVerification, embeddingsVerification] = await Promise.all([
2076
+ (0, runtime_capability_check_1.verifyPerplexityCapability)(perplexityApiKey),
2077
+ (0, runtime_capability_check_1.verifyEmbeddingsCapability)(embeddingsApiKey),
2078
+ ]);
2079
+ }
2080
+ else if (shouldVerifyPerplexity) {
2081
+ onProgress?.("verifying Perplexity search");
2082
+ perplexityVerification = await (0, runtime_capability_check_1.verifyPerplexityCapability)(perplexityApiKey);
2083
+ }
2084
+ else if (shouldVerifyEmbeddings) {
2085
+ onProgress?.("verifying memory embeddings");
2086
+ embeddingsVerification = await (0, runtime_capability_check_1.verifyEmbeddingsCapability)(embeddingsApiKey);
2087
+ }
2088
+ let perplexityStatus;
2089
+ let perplexityDetailLines;
2068
2090
  if (!runtimeConfig.ok) {
2069
2091
  perplexityStatus = runtimeConfigReadStatus(runtimeConfig);
2070
2092
  perplexityDetailLines = [];
@@ -2074,16 +2096,15 @@ async function buildConnectMenu(agent, deps, onProgress) {
2074
2096
  perplexityDetailLines = ["no API key saved yet"];
2075
2097
  }
2076
2098
  else {
2077
- onProgress?.("verifying Perplexity search");
2078
- const verification = await (0, runtime_capability_check_1.verifyPerplexityCapability)(perplexityApiKey);
2079
- perplexityStatus = verification.ok ? "ready" : "needs attention";
2080
- perplexityDetailLines = [verification.ok ? "verified live just now" : `live check failed: ${verification.summary}`];
2099
+ perplexityStatus = perplexityVerification?.ok ? "ready" : "needs attention";
2100
+ perplexityDetailLines = [
2101
+ perplexityVerification?.ok
2102
+ ? "verified live just now"
2103
+ : `live check failed: ${perplexityVerification.summary}`,
2104
+ ];
2081
2105
  }
2082
2106
  let embeddingsStatus;
2083
2107
  let embeddingsDetailLines;
2084
- const embeddingsApiKey = runtimeConfig.ok
2085
- ? readRuntimeConfigString(runtimeConfig.config, "integrations.openaiEmbeddingsApiKey")
2086
- : null;
2087
2108
  if (!runtimeConfig.ok) {
2088
2109
  embeddingsStatus = runtimeConfigReadStatus(runtimeConfig);
2089
2110
  embeddingsDetailLines = [];
@@ -2093,10 +2114,12 @@ async function buildConnectMenu(agent, deps, onProgress) {
2093
2114
  embeddingsDetailLines = ["no API key saved yet"];
2094
2115
  }
2095
2116
  else {
2096
- onProgress?.("verifying memory embeddings");
2097
- const verification = await (0, runtime_capability_check_1.verifyEmbeddingsCapability)(embeddingsApiKey);
2098
- embeddingsStatus = verification.ok ? "ready" : "needs attention";
2099
- embeddingsDetailLines = [verification.ok ? "verified live just now" : `live check failed: ${verification.summary}`];
2117
+ embeddingsStatus = embeddingsVerification?.ok ? "ready" : "needs attention";
2118
+ embeddingsDetailLines = [
2119
+ embeddingsVerification?.ok
2120
+ ? "verified live just now"
2121
+ : `live check failed: ${embeddingsVerification.summary}`,
2122
+ ];
2100
2123
  }
2101
2124
  const teamsStatus = runtimeConfig.ok
2102
2125
  ? hasRuntimeConfigValue(runtimeConfig.config, "teams.clientId")
@@ -84,6 +84,40 @@ function resolveProviderHealthCommand(providerHealth, status) {
84
84
  }
85
85
  return undefined;
86
86
  }
87
+ function providerHealthTargetLane(providerHealth) {
88
+ const issue = providerHealth?.issue;
89
+ const actionLane = issue?.actions
90
+ .map((action) => "lane" in action && action.lane ? action.lane : undefined)
91
+ .find((lane) => lane === "outward" || lane === "inner");
92
+ if (actionLane)
93
+ return actionLane;
94
+ const text = [issue?.summary, issue?.detail, providerHealth?.error]
95
+ .filter(Boolean)
96
+ .join(" ")
97
+ .toLowerCase();
98
+ if (/\boutward provider\b/.test(text) || /\boutward lane\b/.test(text))
99
+ return "outward";
100
+ if (/\binner provider\b/.test(text) || /\binner lane\b/.test(text))
101
+ return "inner";
102
+ return undefined;
103
+ }
104
+ function providerHealthAppliesToLane(providerHealth, lane) {
105
+ const targetLane = providerHealthTargetLane(providerHealth);
106
+ return !targetLane || targetLane === lane.lane;
107
+ }
108
+ function providerHealthDetail(providerHealth, status) {
109
+ if (status === "locked")
110
+ return "vault locked on this machine";
111
+ if (status === "needs credentials")
112
+ return "credentials missing";
113
+ if (status === "needs setup") {
114
+ return providerHealth?.issue?.detail ?? providerHealth?.error ?? "needs setup";
115
+ }
116
+ const detail = providerHealth?.issue?.detail ?? providerHealth?.error;
117
+ if (!detail)
118
+ return "live check needs attention";
119
+ return /failed live check/i.test(detail) ? detail : `failed live check: ${detail}`;
120
+ }
87
121
  function isProblemStatus(status) {
88
122
  return status !== "ready" && status !== "attached";
89
123
  }
@@ -160,6 +194,23 @@ function summarizeProviderLane(agent, lane, providerHealth) {
160
194
  };
161
195
  }
162
196
  const fallbackAction = providerHealthCommand ?? lane.credential.repairCommand;
197
+ if (providerHealth?.ok) {
198
+ return {
199
+ lane: lane.lane,
200
+ status: "ready",
201
+ title: `${lane.provider} / ${lane.model}`,
202
+ detail: "ready",
203
+ };
204
+ }
205
+ if (providerHealthStatus && providerHealthAppliesToLane(providerHealth, lane)) {
206
+ return {
207
+ lane: lane.lane,
208
+ status: providerHealthStatus,
209
+ title: `${lane.provider} / ${lane.model}`,
210
+ detail: providerHealthDetail(providerHealth, providerHealthStatus),
211
+ action: fallbackAction,
212
+ };
213
+ }
163
214
  if (lane.credential.status === "missing") {
164
215
  return {
165
216
  lane: lane.lane,
@@ -178,14 +229,6 @@ function summarizeProviderLane(agent, lane, providerHealth) {
178
229
  action: fallbackAction,
179
230
  };
180
231
  }
181
- if (providerHealth?.ok) {
182
- return {
183
- lane: lane.lane,
184
- status: "ready",
185
- title: `${lane.provider} / ${lane.model}`,
186
- detail: "ready",
187
- };
188
- }
189
232
  if (lane.readiness.status === "failed") {
190
233
  return {
191
234
  lane: lane.lane,
@@ -209,6 +209,11 @@ function cacheResult(agentName, result) {
209
209
  cachedPools.set(agentName, result);
210
210
  return result;
211
211
  }
212
+ function selectedProviders(providers) {
213
+ if (!providers || providers.length === 0)
214
+ return [...VALID_PROVIDERS];
215
+ return [...new Set(providers)];
216
+ }
212
217
  function resultForProvider(poolResult, provider) {
213
218
  if (!poolResult.ok)
214
219
  return poolResult;
@@ -227,12 +232,13 @@ function readProviderCredentialPool(agentName) {
227
232
  return cachedPools.get(agentName) ?? missingPool(agentName);
228
233
  }
229
234
  async function refreshProviderCredentialPool(agentName, options = {}) {
235
+ const providersToRead = selectedProviders(options.providers);
230
236
  try {
231
237
  const store = (0, credential_access_1.getCredentialStore)(agentName);
232
238
  options.onProgress?.(`reading vault items for ${agentName}...`);
233
239
  const providers = {};
234
240
  let updatedAt = new Date(0).toISOString();
235
- for (const provider of VALID_PROVIDERS) {
241
+ for (const provider of providersToRead) {
236
242
  const itemName = providerCredentialItemName(provider);
237
243
  options.onProgress?.(`reading ${provider} credentials...`);
238
244
  let raw;
@@ -261,9 +267,19 @@ async function refreshProviderCredentialPool(agentName, options = {}) {
261
267
  component: "config/identity",
262
268
  event: "config.provider_credentials_loaded",
263
269
  message: "loaded provider credentials from vault",
264
- meta: { agentName, providerCount: Object.keys(providers).length },
270
+ meta: {
271
+ agentName,
272
+ providerCount: Object.keys(providers).length,
273
+ requestedProviderCount: providersToRead.length,
274
+ cacheSkipped: options.skipCache === true,
275
+ },
265
276
  });
266
- return cacheResult(agentName, { ok: true, poolPath: providerCredentialsVaultPath(agentName), pool });
277
+ const result = {
278
+ ok: true,
279
+ poolPath: providerCredentialsVaultPath(agentName),
280
+ pool,
281
+ };
282
+ return options.skipCache ? result : cacheResult(agentName, result);
267
283
  }
268
284
  catch (error) {
269
285
  const cached = cachedPools.get(agentName);
@@ -282,7 +298,7 @@ async function refreshProviderCredentialPool(agentName, options = {}) {
282
298
  });
283
299
  if (options.preserveCachedOnFailure && cached?.ok)
284
300
  return cached;
285
- return cacheResult(agentName, result);
301
+ return options.skipCache ? result : cacheResult(agentName, result);
286
302
  }
287
303
  }
288
304
  function createProviderCredentialRecord(input) {
@@ -134,15 +134,6 @@ 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
- }
141
- function isBwDirectLookupFallbackError(err) {
142
- const message = err.message.toLowerCase();
143
- return (message.includes("invalid json from bw get item") ||
144
- message.includes("invalid item from bw get item"));
145
- }
146
137
  // ---------------------------------------------------------------------------
147
138
  // Cross-process bw CLI lock
148
139
  // ---------------------------------------------------------------------------
@@ -159,6 +150,9 @@ function isBwDirectLookupFallbackError(err) {
159
150
  const BW_LOCK_FILENAME = ".ouro-bw.lock";
160
151
  const BW_LOCK_TIMEOUT_MS = 30_000;
161
152
  const BW_LOCK_POLL_MS = 100;
153
+ const BW_DATA_FILENAME = "data.json";
154
+ const BW_SYNC_MARKER_FILENAME = ".ouro-last-sync";
155
+ const BW_SYNC_FRESH_MS = 60_000;
162
156
  /** In-process async mutex keyed by appDataDir. */
163
157
  const inProcessLocks = new Map();
164
158
  function isPidAlive(pid) {
@@ -388,6 +382,7 @@ class BitwardenCredentialStore {
388
382
  appDataDir;
389
383
  sessionToken = null;
390
384
  bwBinaryPath = "bw";
385
+ structuredItemCache = null;
391
386
  constructor(serverUrl, email, masterPassword, options = {}) {
392
387
  this.serverUrl = serverUrl;
393
388
  this.email = email;
@@ -488,9 +483,19 @@ class BitwardenCredentialStore {
488
483
  const unlockOutput = await this.execBw(["unlock", this.masterPassword, "--raw"]);
489
484
  this.sessionToken = unlockOutput.trim();
490
485
  }
491
- // Sync vault data after obtaining a fresh session token
492
- /* v8 ignore next -- defensive: loginAttempt always sets sessionToken before sync @preserve */
493
- await this.execBw(["sync"], this.sessionToken ?? undefined);
486
+ if (this.shouldSyncVaultAfterSession(status)) {
487
+ /* v8 ignore next -- defensive: loginAttempt always sets sessionToken before sync @preserve */
488
+ await this.execBw(["sync"], this.sessionToken ?? undefined);
489
+ this.writeSyncMarker();
490
+ }
491
+ else {
492
+ (0, runtime_1.emitNervesEvent)({
493
+ event: "repertoire.bw_sync_skipped",
494
+ component: "repertoire",
495
+ message: "skipping bw sync because local vault cache is still fresh",
496
+ meta: { email: this.email, serverUrl: this.serverUrl, freshnessWindowMs: BW_SYNC_FRESH_MS },
497
+ });
498
+ }
494
499
  }
495
500
  async ensureSession() {
496
501
  if (!this.sessionToken) {
@@ -512,6 +517,7 @@ class BitwardenCredentialStore {
512
517
  throw err;
513
518
  }
514
519
  this.sessionToken = null;
520
+ this.structuredItemCache = null;
515
521
  attemptedFreshSession = true;
516
522
  }
517
523
  }
@@ -614,6 +620,7 @@ class BitwardenCredentialStore {
614
620
  notes: data.notes ?? null,
615
621
  };
616
622
  const encoded = Buffer.from(JSON.stringify(item)).toString("base64");
623
+ this.structuredItemCache = null;
617
624
  let savedItem;
618
625
  if (existing) {
619
626
  const stdout = await this.execBw(["edit", "item", existing.id], session, encoded);
@@ -629,6 +636,7 @@ class BitwardenCredentialStore {
629
636
  }
630
637
  this.assertStoredCredentialMatches(domain, data, savedItem);
631
638
  });
639
+ this.structuredItemCache = null;
632
640
  (0, runtime_1.emitNervesEvent)({
633
641
  event: "repertoire.bw_credential_store_end",
634
642
  component: "repertoire",
@@ -677,6 +685,7 @@ class BitwardenCredentialStore {
677
685
  return false;
678
686
  }
679
687
  await this.withSessionRetry((session) => this.execBw(["delete", "item", item.id], session));
688
+ this.structuredItemCache = null;
680
689
  (0, runtime_1.emitNervesEvent)({
681
690
  event: "repertoire.bw_credential_delete_end",
682
691
  component: "repertoire",
@@ -688,25 +697,57 @@ class BitwardenCredentialStore {
688
697
  // --- Private ---
689
698
  async findItemByDomain(domain, session) {
690
699
  if (shouldPreferExactItemLookup(domain)) {
691
- try {
692
- const stdout = await this.execBw(["get", "item", domain], session);
693
- const item = parseBwItem(stdout, "bw get item");
694
- if (item.name === domain)
695
- return item;
696
- }
697
- catch (error) {
698
- const err = error;
699
- if (isBwDirectLookupMissingError(err))
700
- return null;
701
- if (!isBwDirectLookupFallbackError(err))
702
- throw err;
703
- }
700
+ const items = await this.readStructuredItemCache(session);
701
+ return items.get(domain) ?? null;
704
702
  }
705
703
  const stdout = await this.execBw(["list", "items", "--search", domain], session);
706
704
  const items = parseBwItems(stdout, "bw list items --search");
707
705
  // Find exact match by name
708
706
  return items.find((item) => item.name === domain) ?? null;
709
707
  }
708
+ shouldSyncVaultAfterSession(status) {
709
+ if (status.status === "unauthenticated" || !status.status)
710
+ return true;
711
+ if (!this.appDataDir)
712
+ return true;
713
+ const freshnessTimestamp = this.latestLocalSyncTimestamp();
714
+ if (freshnessTimestamp !== null) {
715
+ return Date.now() - freshnessTimestamp > BW_SYNC_FRESH_MS;
716
+ }
717
+ return true;
718
+ }
719
+ latestLocalSyncTimestamp() {
720
+ const files = [BW_SYNC_MARKER_FILENAME, BW_DATA_FILENAME];
721
+ let latest = null;
722
+ for (const name of files) {
723
+ try {
724
+ const mtimeMs = fs.statSync(path.join(this.appDataDir, name)).mtimeMs;
725
+ latest = latest === null ? mtimeMs : Math.max(latest, mtimeMs);
726
+ }
727
+ catch {
728
+ // Missing freshness file is fine; use any other available timestamp.
729
+ }
730
+ }
731
+ return latest;
732
+ }
733
+ writeSyncMarker() {
734
+ if (!this.appDataDir)
735
+ return;
736
+ try {
737
+ fs.writeFileSync(path.join(this.appDataDir, BW_SYNC_MARKER_FILENAME), `${Date.now()}\n`, { mode: 0o600 });
738
+ }
739
+ catch {
740
+ // If the marker cannot be written, fall back to syncing next time.
741
+ }
742
+ }
743
+ async readStructuredItemCache(session) {
744
+ if (this.structuredItemCache)
745
+ return this.structuredItemCache;
746
+ const stdout = await this.execBw(["list", "items"], session);
747
+ const items = parseBwItems(stdout, "bw list items");
748
+ this.structuredItemCache = new Map(items.map((item) => [item.name, item]));
749
+ return this.structuredItemCache;
750
+ }
710
751
  async findItemById(id, session) {
711
752
  const stdout = await this.execBw(["get", "item", id], session);
712
753
  return parseBwItem(stdout, "bw get item");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.446",
3
+ "version": "0.1.0-alpha.447",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",