@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 +8 -0
- package/dist/heart/daemon/agent-config-check.js +14 -1
- package/dist/heart/daemon/cli-exec.js +36 -13
- package/dist/heart/daemon/connect-bay.js +51 -8
- package/dist/heart/provider-credentials.js +20 -4
- package/dist/repertoire/bitwarden-store.js +66 -25
- package/package.json +1 -1
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
|
|
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
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
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
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
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
|
|
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: {
|
|
270
|
+
meta: {
|
|
271
|
+
agentName,
|
|
272
|
+
providerCount: Object.keys(providers).length,
|
|
273
|
+
requestedProviderCount: providersToRead.length,
|
|
274
|
+
cacheSkipped: options.skipCache === true,
|
|
275
|
+
},
|
|
265
276
|
});
|
|
266
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
692
|
-
|
|
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");
|