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

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.448",
6
+ "changes": [
7
+ "`ouro status` and agent prompt provider visibility now distinguish \"the daemon has not loaded provider credentials in this process\" from \"the agent vault is missing credentials.\" A saved live check that passed stays ready instead of being downgraded to stale/missing just because status rendering is running in a fresh daemon process.",
8
+ "Provider visibility now carries a safe `not-loaded` credential state and renders it as `checked previously`, preserving the last live-check result without reading or printing secrets during ordinary status/prompt rendering.",
9
+ "Regression coverage locks the installed-product failure shape: an empty in-process provider cache plus ready provider state no longer produces misleading auth repair guidance."
10
+ ]
11
+ },
12
+ {
13
+ "version": "0.1.0-alpha.447",
14
+ "changes": [
15
+ "`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.",
16
+ "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.",
17
+ "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."
18
+ ]
19
+ },
4
20
  {
5
21
  "version": "0.1.0-alpha.446",
6
22
  "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")
@@ -2943,7 +2966,7 @@ async function executeProviderCheck(command, deps) {
2943
2966
  throw error;
2944
2967
  }
2945
2968
  }
2946
- function renderProviderCredentialLine(credential) {
2969
+ function renderProviderCredentialLine(agentName, credential) {
2947
2970
  if (credential.status === "present") {
2948
2971
  const credentialFields = credential.credentialFields.length > 0 ? ` credentials: ${credential.credentialFields.join(", ")}` : " credentials: none";
2949
2972
  const configFields = credential.configFields.length > 0 ? ` config: ${credential.configFields.join(", ")}` : " config: none";
@@ -2952,6 +2975,9 @@ function renderProviderCredentialLine(credential) {
2952
2975
  if (credential.status === "invalid-pool") {
2953
2976
  return `credentials: vault unavailable (${credential.error}); repair: ${credential.repair.command}`;
2954
2977
  }
2978
+ if (credential.status === "not-loaded") {
2979
+ return `credentials: not loaded in this process; run \`ouro provider refresh --agent ${agentName}\` to read the vault now`;
2980
+ }
2955
2981
  return `credentials: missing; repair: ${credential.repair.command}`;
2956
2982
  }
2957
2983
  async function executeProviderStatus(command, deps) {
@@ -2987,7 +3013,7 @@ async function executeProviderStatus(command, deps) {
2987
3013
  const binding = resolved.binding;
2988
3014
  lines.push(` ${lane}: ${binding.provider} / ${binding.model} (${binding.source})`);
2989
3015
  lines.push(` readiness: ${binding.readiness.status}${binding.readiness.error ? ` (${binding.readiness.error})` : ""}`);
2990
- lines.push(` ${renderProviderCredentialLine(binding.credential)}`);
3016
+ lines.push(` ${renderProviderCredentialLine(command.agent, binding.credential)}`);
2991
3017
  for (const warning of binding.warnings) {
2992
3018
  lines.push(` warning: ${warning.message}`);
2993
3019
  }
@@ -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,
@@ -89,6 +89,16 @@ function resolveCredential(poolResult, provider, agentName) {
89
89
  warnings: [missingCredentialWarning(provider)],
90
90
  };
91
91
  }
92
+ if ((0, provider_credentials_1.isProviderCredentialPoolNotLoaded)(poolResult)) {
93
+ return {
94
+ credential: {
95
+ status: "not-loaded",
96
+ provider,
97
+ poolPath: poolResult.poolPath,
98
+ },
99
+ warnings: [],
100
+ };
101
+ }
92
102
  if (poolResult.reason === "invalid" || poolResult.reason === "unavailable") {
93
103
  return {
94
104
  credential: {
@@ -130,6 +140,9 @@ function staleReadiness(readiness, reason) {
130
140
  }
131
141
  function resolveReadiness(input) {
132
142
  if (!input.readiness) {
143
+ if (input.credential.status === "not-loaded") {
144
+ return { readiness: { status: "unknown" }, warnings: [] };
145
+ }
133
146
  if (input.credential.status === "missing") {
134
147
  return { readiness: { status: "unknown", reason: "credential-missing" }, warnings: [] };
135
148
  }
@@ -170,6 +183,9 @@ function resolveReadiness(input) {
170
183
  warnings: [],
171
184
  };
172
185
  }
186
+ if (input.credential.status === "not-loaded") {
187
+ return { readiness: readinessFromState(input.readiness), warnings: [] };
188
+ }
173
189
  return { readiness: readinessFromState(input.readiness), warnings: [] };
174
190
  }
175
191
  function resolveEffectiveProviderBinding(input) {
@@ -33,10 +33,12 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PROVIDER_CREDENTIAL_POOL_NOT_LOADED_ERROR = void 0;
36
37
  exports.splitProviderCredentialFields = splitProviderCredentialFields;
37
38
  exports.providerCredentialsVaultPath = providerCredentialsVaultPath;
38
39
  exports.providerCredentialItemName = providerCredentialItemName;
39
40
  exports.providerCredentialMachineHomeDir = providerCredentialMachineHomeDir;
41
+ exports.isProviderCredentialPoolNotLoaded = isProviderCredentialPoolNotLoaded;
40
42
  exports.readProviderCredentialPool = readProviderCredentialPool;
41
43
  exports.refreshProviderCredentialPool = refreshProviderCredentialPool;
42
44
  exports.createProviderCredentialRecord = createProviderCredentialRecord;
@@ -54,6 +56,7 @@ const credential_access_1 = require("../repertoire/credential-access");
54
56
  const VALID_PROVIDERS = ["azure", "minimax", "anthropic", "openai-codex", "github-copilot"];
55
57
  const VALID_PROVENANCE_SOURCES = ["auth-flow", "manual"];
56
58
  const VAULT_ITEM_PREFIX = "providers/";
59
+ exports.PROVIDER_CREDENTIAL_POOL_NOT_LOADED_ERROR = "provider credentials have not been loaded from vault";
57
60
  const PROVIDER_FIELD_SPLITS = {
58
61
  anthropic: {
59
62
  credentials: ["setupToken", "refreshToken", "expiresAt"],
@@ -197,7 +200,7 @@ function recordFromPayload(payload) {
197
200
  provenance: { ...payload.provenance },
198
201
  };
199
202
  }
200
- function missingPool(agentName, error = "provider credentials have not been loaded from vault") {
203
+ function missingPool(agentName, error = exports.PROVIDER_CREDENTIAL_POOL_NOT_LOADED_ERROR) {
201
204
  return {
202
205
  ok: false,
203
206
  reason: "missing",
@@ -205,10 +208,20 @@ function missingPool(agentName, error = "provider credentials have not been load
205
208
  error,
206
209
  };
207
210
  }
211
+ function isProviderCredentialPoolNotLoaded(result) {
212
+ return !result.ok
213
+ && result.reason === "missing"
214
+ && result.error === exports.PROVIDER_CREDENTIAL_POOL_NOT_LOADED_ERROR;
215
+ }
208
216
  function cacheResult(agentName, result) {
209
217
  cachedPools.set(agentName, result);
210
218
  return result;
211
219
  }
220
+ function selectedProviders(providers) {
221
+ if (!providers || providers.length === 0)
222
+ return [...VALID_PROVIDERS];
223
+ return [...new Set(providers)];
224
+ }
212
225
  function resultForProvider(poolResult, provider) {
213
226
  if (!poolResult.ok)
214
227
  return poolResult;
@@ -227,12 +240,13 @@ function readProviderCredentialPool(agentName) {
227
240
  return cachedPools.get(agentName) ?? missingPool(agentName);
228
241
  }
229
242
  async function refreshProviderCredentialPool(agentName, options = {}) {
243
+ const providersToRead = selectedProviders(options.providers);
230
244
  try {
231
245
  const store = (0, credential_access_1.getCredentialStore)(agentName);
232
246
  options.onProgress?.(`reading vault items for ${agentName}...`);
233
247
  const providers = {};
234
248
  let updatedAt = new Date(0).toISOString();
235
- for (const provider of VALID_PROVIDERS) {
249
+ for (const provider of providersToRead) {
236
250
  const itemName = providerCredentialItemName(provider);
237
251
  options.onProgress?.(`reading ${provider} credentials...`);
238
252
  let raw;
@@ -261,9 +275,19 @@ async function refreshProviderCredentialPool(agentName, options = {}) {
261
275
  component: "config/identity",
262
276
  event: "config.provider_credentials_loaded",
263
277
  message: "loaded provider credentials from vault",
264
- meta: { agentName, providerCount: Object.keys(providers).length },
278
+ meta: {
279
+ agentName,
280
+ providerCount: Object.keys(providers).length,
281
+ requestedProviderCount: providersToRead.length,
282
+ cacheSkipped: options.skipCache === true,
283
+ },
265
284
  });
266
- return cacheResult(agentName, { ok: true, poolPath: providerCredentialsVaultPath(agentName), pool });
285
+ const result = {
286
+ ok: true,
287
+ poolPath: providerCredentialsVaultPath(agentName),
288
+ pool,
289
+ };
290
+ return options.skipCache ? result : cacheResult(agentName, result);
267
291
  }
268
292
  catch (error) {
269
293
  const cached = cachedPools.get(agentName);
@@ -282,7 +306,7 @@ async function refreshProviderCredentialPool(agentName, options = {}) {
282
306
  });
283
307
  if (options.preserveCachedOnFailure && cached?.ok)
284
308
  return cached;
285
- return cacheResult(agentName, result);
309
+ return options.skipCache ? result : cacheResult(agentName, result);
286
310
  }
287
311
  }
288
312
  function createProviderCredentialRecord(input) {
@@ -21,7 +21,7 @@ function credentialVisibility(binding) {
21
21
  }
22
22
  return {
23
23
  status: credential.status,
24
- repairCommand: credential.repair.command,
24
+ ...("repair" in credential ? { repairCommand: credential.repair.command } : {}),
25
25
  };
26
26
  }
27
27
  function readinessVisibility(binding) {
@@ -87,6 +87,8 @@ function credentialLabel(credential) {
87
87
  return credential.source ?? "vault";
88
88
  if (credential.status === "invalid-pool")
89
89
  return "vault unavailable";
90
+ if (credential.status === "not-loaded")
91
+ return "checked previously";
90
92
  return "missing";
91
93
  }
92
94
  function readinessLabel(readiness) {
@@ -102,7 +104,7 @@ function readinessLabel(readiness) {
102
104
  return readiness.status;
103
105
  }
104
106
  function providerStatusDetail(lane) {
105
- if (lane.credential.status !== "present")
107
+ if (lane.credential.status === "missing" || lane.credential.status === "invalid-pool")
106
108
  return undefined;
107
109
  return lane.readiness.error;
108
110
  }
@@ -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.448",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",