scoutline 0.10.2 → 0.12.0
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/README.md +99 -44
- package/dist/capabilities/diagnostics.d.ts +75 -0
- package/dist/capabilities/diagnostics.d.ts.map +1 -1
- package/dist/capabilities/diagnostics.js.map +1 -1
- package/dist/capabilities/quota.d.ts +64 -1
- package/dist/capabilities/quota.d.ts.map +1 -1
- package/dist/capabilities/quota.js.map +1 -1
- package/dist/capabilities/vision.d.ts +4 -3
- package/dist/capabilities/vision.d.ts.map +1 -1
- package/dist/capabilities/vision.js +7 -5
- package/dist/capabilities/vision.js.map +1 -1
- package/dist/commands/code.d.ts +9 -1
- package/dist/commands/code.d.ts.map +1 -1
- package/dist/commands/code.js +4 -4
- package/dist/commands/code.js.map +1 -1
- package/dist/commands/crawl.d.ts.map +1 -1
- package/dist/commands/crawl.js +14 -3
- package/dist/commands/crawl.js.map +1 -1
- package/dist/commands/doctor.d.ts +71 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +96 -15
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts +186 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +1221 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/map.d.ts.map +1 -1
- package/dist/commands/map.js +16 -3
- package/dist/commands/map.js.map +1 -1
- package/dist/commands/quota.d.ts +32 -0
- package/dist/commands/quota.d.ts.map +1 -1
- package/dist/commands/quota.js +165 -24
- package/dist/commands/quota.js.map +1 -1
- package/dist/commands/read.d.ts.map +1 -1
- package/dist/commands/read.js +9 -3
- package/dist/commands/read.js.map +1 -1
- package/dist/commands/repo.d.ts.map +1 -1
- package/dist/commands/repo.js +5 -3
- package/dist/commands/repo.js.map +1 -1
- package/dist/commands/research.d.ts +35 -7
- package/dist/commands/research.d.ts.map +1 -1
- package/dist/commands/research.js +93 -15
- package/dist/commands/research.js.map +1 -1
- package/dist/commands/tools.d.ts +8 -0
- package/dist/commands/tools.d.ts.map +1 -1
- package/dist/commands/tools.js +4 -4
- package/dist/commands/tools.js.map +1 -1
- package/dist/commands/vision.d.ts +10 -0
- package/dist/commands/vision.d.ts.map +1 -1
- package/dist/commands/vision.js +25 -2
- package/dist/commands/vision.js.map +1 -1
- package/dist/index.d.ts +136 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +894 -298
- package/dist/index.js.map +1 -1
- package/dist/lib/api-client.d.ts +1 -1
- package/dist/lib/api-client.d.ts.map +1 -1
- package/dist/lib/api-client.js +6 -2
- package/dist/lib/api-client.js.map +1 -1
- package/dist/lib/cache.d.ts +9 -2
- package/dist/lib/cache.d.ts.map +1 -1
- package/dist/lib/cache.js +9 -2
- package/dist/lib/cache.js.map +1 -1
- package/dist/lib/code-mode.d.ts +8 -0
- package/dist/lib/code-mode.d.ts.map +1 -1
- package/dist/lib/code-mode.js +3 -1
- package/dist/lib/code-mode.js.map +1 -1
- package/dist/lib/config-store.d.ts +165 -0
- package/dist/lib/config-store.d.ts.map +1 -0
- package/dist/lib/config-store.js +332 -0
- package/dist/lib/config-store.js.map +1 -0
- package/dist/lib/config.d.ts +2 -2
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +19 -11
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/consumption.d.ts +144 -0
- package/dist/lib/consumption.d.ts.map +1 -0
- package/dist/lib/consumption.js +161 -0
- package/dist/lib/consumption.js.map +1 -0
- package/dist/lib/errors.d.ts +11 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +14 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/execution.d.ts +32 -4
- package/dist/lib/execution.d.ts.map +1 -1
- package/dist/lib/execution.js +156 -13
- package/dist/lib/execution.js.map +1 -1
- package/dist/lib/mcp-client.d.ts +12 -0
- package/dist/lib/mcp-client.d.ts.map +1 -1
- package/dist/lib/mcp-client.js +16 -7
- package/dist/lib/mcp-client.js.map +1 -1
- package/dist/lib/mcp-config.d.ts +10 -0
- package/dist/lib/mcp-config.d.ts.map +1 -1
- package/dist/lib/mcp-config.js +13 -7
- package/dist/lib/mcp-config.js.map +1 -1
- package/dist/lib/monitor-client.d.ts +19 -3
- package/dist/lib/monitor-client.d.ts.map +1 -1
- package/dist/lib/monitor-client.js +21 -5
- package/dist/lib/monitor-client.js.map +1 -1
- package/dist/lib/provider-fallback.d.ts +150 -0
- package/dist/lib/provider-fallback.d.ts.map +1 -0
- package/dist/lib/provider-fallback.js +510 -0
- package/dist/lib/provider-fallback.js.map +1 -0
- package/dist/lib/quota-mapping.d.ts +402 -0
- package/dist/lib/quota-mapping.d.ts.map +1 -0
- package/dist/lib/quota-mapping.js +539 -0
- package/dist/lib/quota-mapping.js.map +1 -0
- package/dist/lib/quota-store.d.ts +271 -0
- package/dist/lib/quota-store.d.ts.map +1 -0
- package/dist/lib/quota-store.js +540 -0
- package/dist/lib/quota-store.js.map +1 -0
- package/dist/lib/trigger-detection.d.ts +149 -0
- package/dist/lib/trigger-detection.d.ts.map +1 -0
- package/dist/lib/trigger-detection.js +149 -0
- package/dist/lib/trigger-detection.js.map +1 -0
- package/dist/lib/tty.d.ts +8 -1
- package/dist/lib/tty.d.ts.map +1 -1
- package/dist/lib/tty.js +53 -1
- package/dist/lib/tty.js.map +1 -1
- package/dist/providers/brave/adapter.d.ts +26 -0
- package/dist/providers/brave/adapter.d.ts.map +1 -1
- package/dist/providers/brave/adapter.js +66 -4
- package/dist/providers/brave/adapter.js.map +1 -1
- package/dist/providers/brave/client.d.ts +46 -2
- package/dist/providers/brave/client.d.ts.map +1 -1
- package/dist/providers/brave/client.js +55 -3
- package/dist/providers/brave/client.js.map +1 -1
- package/dist/providers/exa/adapter.d.ts.map +1 -1
- package/dist/providers/exa/adapter.js +10 -1
- package/dist/providers/exa/adapter.js.map +1 -1
- package/dist/providers/firecrawl/adapter.d.ts.map +1 -1
- package/dist/providers/firecrawl/adapter.js +13 -3
- package/dist/providers/firecrawl/adapter.js.map +1 -1
- package/dist/providers/minimax/adapter.d.ts.map +1 -1
- package/dist/providers/minimax/adapter.js +4 -0
- package/dist/providers/minimax/adapter.js.map +1 -1
- package/dist/providers/selection.d.ts +114 -1
- package/dist/providers/selection.d.ts.map +1 -1
- package/dist/providers/selection.js +99 -3
- package/dist/providers/selection.js.map +1 -1
- package/dist/providers/tavily/adapter.d.ts.map +1 -1
- package/dist/providers/tavily/adapter.js +2 -0
- package/dist/providers/tavily/adapter.js.map +1 -1
- package/dist/providers/types.d.ts +33 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js +8 -0
- package/dist/providers/types.js.map +1 -1
- package/dist/providers/zai/adapter.d.ts.map +1 -1
- package/dist/providers/zai/adapter.js +18 -5
- package/dist/providers/zai/adapter.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -17,13 +17,20 @@ import { isExtractMode } from "./lib/extract.js";
|
|
|
17
17
|
import { runCodeFile, evalCode, printInterfaces, printPromptTemplate, CODE_HELP, } from "./commands/code.js";
|
|
18
18
|
import { isOutputMode, OUTPUT_MODES } from "./lib/output.js";
|
|
19
19
|
import { formatErrorOutput } from "./lib/output.js";
|
|
20
|
-
import {
|
|
20
|
+
import { ValidationError, UnsupportedCapabilityError, getErrorExitCode, } from "./lib/errors.js";
|
|
21
21
|
import { invokeCommand } from "./command-invocation.js";
|
|
22
22
|
import { defaultResponseCache } from "./lib/cache.js";
|
|
23
23
|
import { configuredSecrets } from "./lib/redact.js";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
24
|
+
import { resolveEnvFromConfig } from "./lib/config-store.js";
|
|
25
|
+
import { inspectConfig, createDefaultVerificationPromoter, createDefaultHintShownStore, } from "./lib/config-store.js";
|
|
26
|
+
import { createDefaultQuotaStore, refreshQuotaSnapshots, } from "./lib/quota-store.js";
|
|
27
|
+
import { createQuotaStoreConsumptionSink } from "./lib/consumption.js";
|
|
28
|
+
import { classifyCredentialState, formatEnvOnlyHint, isCommandHelpInvocation, OBSERVATIONAL_COMMANDS, } from "./lib/trigger-detection.js";
|
|
29
|
+
import { resolveProviderId, resolveEffectiveProvider } from "./providers/selection.js";
|
|
30
|
+
import { BUILT_IN_PROVIDER_DESCRIPTORS } from "./providers/registry.js";
|
|
31
|
+
import { executeWithFallback } from "./lib/provider-fallback.js";
|
|
26
32
|
import { visionOperationToCapability } from "./capabilities/vision.js";
|
|
33
|
+
import { handleInitWithHelp, createInquirerPrompts, createDefaultConfigStore, } from "./commands/init.js";
|
|
27
34
|
import { createRequire } from "node:module";
|
|
28
35
|
const require = createRequire(import.meta.url);
|
|
29
36
|
const { version: VERSION } = require("../package.json");
|
|
@@ -54,6 +61,7 @@ Commands:
|
|
|
54
61
|
doctor Provider-aware environment + connectivity checks
|
|
55
62
|
cache Inspect or clear the local cache (stats / clear)
|
|
56
63
|
code Execute TypeScript tool chains (Code Mode, Z.AI)
|
|
64
|
+
init Interactive onboarding wizard (writes ~/.scoutline/config.json)
|
|
57
65
|
|
|
58
66
|
Provider selection (precedence: --provider, then SCOUTLINE_PROVIDER, then zai):
|
|
59
67
|
--provider <zai|minimax|tavily|exa|brave|firecrawl> Select the active Provider for shared capabilities
|
|
@@ -64,11 +72,14 @@ and 'research' commands participate in Provider selection: Z.AI
|
|
|
64
72
|
advertises and supplies repository-exploration and reader; Tavily
|
|
65
73
|
advertises and supplies reader plus crawl, map, and research; Exa
|
|
66
74
|
advertises and supplies search, reader, and research; MiniMax
|
|
67
|
-
advertises and supplies none of those Provider-only Capabilities.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
advertises and supplies none of those Provider-only Capabilities.
|
|
76
|
+
Provider fallback is always-on by default (0.11.0+): selecting a
|
|
77
|
+
non-supplier emits a stderr notice and silently reroutes to the next
|
|
78
|
+
eligible configured Provider in registry order. Use --no-fallback (or
|
|
79
|
+
SCOUTLINE_NO_FALLBACK=1) to restore the previous strict
|
|
80
|
+
UNSUPPORTED_CAPABILITY behavior. Z.AI-only commands (tools, tool,
|
|
81
|
+
call, code) carry the flag but ignore it. Quota and doctor report
|
|
82
|
+
per-Provider; --provider picks the effective Provider for metadata.
|
|
72
83
|
|
|
73
84
|
Global Options:
|
|
74
85
|
--output-format <data|json|pretty|compact|markdown|refs|tty> Output mode (default: data)
|
|
@@ -87,6 +98,7 @@ Help:
|
|
|
87
98
|
scoutline call --help
|
|
88
99
|
scoutline code --help
|
|
89
100
|
scoutline cache --help
|
|
101
|
+
scoutline init --help
|
|
90
102
|
`.trim();
|
|
91
103
|
function parseArgs(args) {
|
|
92
104
|
const flags = {};
|
|
@@ -137,6 +149,7 @@ function extractGlobalOptions(args) {
|
|
|
137
149
|
let forcePretty = false;
|
|
138
150
|
let forceRaw = false;
|
|
139
151
|
let provider;
|
|
152
|
+
let noFallback = false;
|
|
140
153
|
for (let i = 0; i < args.length; i += 1) {
|
|
141
154
|
const arg = args[i];
|
|
142
155
|
if (arg === "--output-format" || arg === "-O") {
|
|
@@ -161,9 +174,19 @@ function extractGlobalOptions(args) {
|
|
|
161
174
|
i += 1;
|
|
162
175
|
continue;
|
|
163
176
|
}
|
|
177
|
+
if (arg === "--no-fallback") {
|
|
178
|
+
// Provider-fallback kill-switch. Accepted before OR after the
|
|
179
|
+
// command token and removed from the rest stream so command-local
|
|
180
|
+
// parsing never observes it. Resolution against
|
|
181
|
+
// `SCOUTLINE_NO_FALLBACK` lives in `main` so a test that drives
|
|
182
|
+
// `extractGlobalOptions` directly can assert the flag shape in
|
|
183
|
+
// isolation.
|
|
184
|
+
noFallback = true;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
164
187
|
rest.push(arg);
|
|
165
188
|
}
|
|
166
|
-
return { outputFormat, forcePretty, forceRaw, provider, rest };
|
|
189
|
+
return { outputFormat, forcePretty, forceRaw, provider, noFallback, rest };
|
|
167
190
|
}
|
|
168
191
|
function resolveOutputMode(explicit, forcePretty, forceRaw, adapter) {
|
|
169
192
|
if (explicit !== undefined) {
|
|
@@ -197,66 +220,107 @@ async function handleVision(args, outputMode, deps) {
|
|
|
197
220
|
// a parse-time VALIDATION_ERROR and are rejected before any Provider
|
|
198
221
|
// resolution, support check, or media access.
|
|
199
222
|
const operation = visionOperationForCommand(command);
|
|
223
|
+
// Per-operation capability id (e.g. `vision.interpret-image`,
|
|
224
|
+
// `vision.chart`). The executor's preflight uses this to filter
|
|
225
|
+
// descriptors, and notice wording carries it verbatim. Vision
|
|
226
|
+
// sub-operations share the same `adapter.vision` slot — the
|
|
227
|
+
// per-operation capability is in the descriptor metadata. Computed
|
|
228
|
+
// before selection so PB-T4 can rank providers against the exact
|
|
229
|
+
// sub-operation being dispatched.
|
|
230
|
+
const capabilityId = visionOperationToCapability(operation);
|
|
200
231
|
// Resolve the effective Provider for Vision (DESIGN.md §6). Invalid
|
|
201
232
|
// explicit/env input fails here with VALIDATION_ERROR before any Vision
|
|
202
233
|
// support check or media access. Selection never consults credentials
|
|
203
|
-
// (FR-003); the
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
// dispatch layer surfaces a missing credential as ConfigurationError
|
|
217
|
-
// (exit 3), AFTER the capability support check (FR-023) but before any
|
|
218
|
-
// Adapter construction or media access (Fixup A — B5).
|
|
219
|
-
if (!descriptor.isConfigured(deps.env)) {
|
|
220
|
-
throw new ConfigurationError(`Provider "${providerId}" is not configured. Set the required API key.`);
|
|
221
|
-
}
|
|
222
|
-
const adapter = descriptor.create({ env: deps.env });
|
|
223
|
-
const visionCapability = adapter.vision;
|
|
224
|
-
if (!visionCapability || !visionCapability.supports(operation)) {
|
|
225
|
-
throw new UnsupportedCapabilityError(providerId, capabilityId);
|
|
226
|
-
}
|
|
234
|
+
// beyond the descriptor `isConfigured` metadata check (FR-003); the
|
|
235
|
+
// configured check below is the executor's responsibility (Provider
|
|
236
|
+
// Fallback Tech Plan §"Per-handler refactor pattern"). PB-T4: when
|
|
237
|
+
// no pin is present, the effective provider is the highest-scored
|
|
238
|
+
// configured+capable provider for this vision operation against the
|
|
239
|
+
// injected quota snapshot; pin input still bypasses ranking.
|
|
240
|
+
const providerId = resolveEffectiveProvider({
|
|
241
|
+
explicitProvider: deps.provider,
|
|
242
|
+
env: deps.env,
|
|
243
|
+
capabilityId,
|
|
244
|
+
descriptors: deps.providerDescriptors,
|
|
245
|
+
quotaSnapshot: deps.quotaState,
|
|
246
|
+
});
|
|
227
247
|
// Vision bypasses the response cache (FR-022). The shared execution
|
|
228
248
|
// primitives (sleep/random) are the same ones Search consumes; they
|
|
229
249
|
// drive retry backoff deterministically under test.
|
|
230
|
-
const
|
|
231
|
-
capability: visionCapability,
|
|
250
|
+
const visionDepsShape = {
|
|
232
251
|
sleep: deps.searchSleep,
|
|
233
252
|
random: deps.searchRandom,
|
|
234
253
|
};
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
254
|
+
// Provider-fallback Ticket 02: route every vision operation through
|
|
255
|
+
// the shared executor. The existing vision URL→temp-file fallback
|
|
256
|
+
// stays inside the Z.AI adapter's invoke, layered beneath provider
|
|
257
|
+
// fallback.
|
|
258
|
+
return invokeCommand(deps.invocation, async (context) => {
|
|
259
|
+
const outcome = await executeWithFallback({
|
|
260
|
+
capabilityId,
|
|
261
|
+
commandLabel: "vision",
|
|
262
|
+
effectiveProvider: providerId,
|
|
263
|
+
descriptors: deps.providerDescriptors,
|
|
264
|
+
env: deps.env,
|
|
265
|
+
fallbackEnabled: deps.fallbackEnabled,
|
|
266
|
+
writeStderr: (s) => deps.invocation.writeStderr(s),
|
|
267
|
+
}, async (descriptor) => {
|
|
268
|
+
const adapter = descriptor.create({ env: deps.env });
|
|
269
|
+
// The executor's preflight guarantees the descriptor advertises
|
|
270
|
+
// the capability and supplies a non-null adapter slot, so the
|
|
271
|
+
// cast is safe. Operation-level support is checked immediately
|
|
272
|
+
// against `visionCapability.supports(operation)` — descriptor
|
|
273
|
+
// metadata and adapter capability are independent sources of
|
|
274
|
+
// truth, and the live dispatch must fail closed on disagreement
|
|
275
|
+
// (Review Fix 4). A mismatch throws `UnsupportedCapabilityError`
|
|
276
|
+
// before any media work / transport use, and the Executor treats
|
|
277
|
+
// it as a `continue` so the next candidate (if any) gets a
|
|
278
|
+
// chance.
|
|
279
|
+
const visionCapability = adapter.vision;
|
|
280
|
+
if (!visionCapability || !visionCapability.supports(operation)) {
|
|
281
|
+
throw new UnsupportedCapabilityError(descriptor.id, capabilityId);
|
|
282
|
+
}
|
|
283
|
+
const visionDeps = {
|
|
284
|
+
capability: visionCapability,
|
|
285
|
+
...visionDepsShape,
|
|
286
|
+
// PB-T2: thread the actual attempted descriptor ID + sink
|
|
287
|
+
// through so vision emits one consumption event per
|
|
288
|
+
// billable invoke attempt at the execution seam. The
|
|
289
|
+
// descriptor ID is the *attempted* provider (not the
|
|
290
|
+
// registry-derived effective provider) so fallback attempts
|
|
291
|
+
// record the actual descriptor that invoked transport.
|
|
292
|
+
...(deps.consume !== undefined ? { consume: deps.consume } : {}),
|
|
293
|
+
...(deps.consume !== undefined ? { provider: descriptor.id } : {}),
|
|
294
|
+
...(deps.now !== undefined ? { now: deps.now } : {}),
|
|
295
|
+
};
|
|
296
|
+
switch (command) {
|
|
297
|
+
case "analyze":
|
|
298
|
+
return vision.analyze(source, prompt, visionDeps, context);
|
|
299
|
+
case "ui-to-code": {
|
|
300
|
+
const outputType = flags.output || "code";
|
|
301
|
+
return vision.uiToCode(source, prompt, outputType, visionDeps, context);
|
|
302
|
+
}
|
|
303
|
+
case "extract-text":
|
|
304
|
+
return vision.extractText(source, prompt, flags.language, visionDeps, context);
|
|
305
|
+
case "diagnose-error":
|
|
306
|
+
return vision.diagnoseError(source, prompt, flags.context, visionDeps, context);
|
|
307
|
+
case "diagram":
|
|
308
|
+
return vision.diagram(source, prompt, flags.type, visionDeps, context);
|
|
309
|
+
case "chart":
|
|
310
|
+
return vision.chart(source, prompt, flags.focus, visionDeps, context);
|
|
311
|
+
case "diff": {
|
|
312
|
+
const actual = positional[2];
|
|
313
|
+
const diffPrompt = positional[3];
|
|
314
|
+
return vision.diff(source, actual, diffPrompt, visionDeps, context);
|
|
315
|
+
}
|
|
316
|
+
case "video":
|
|
317
|
+
return vision.video(source, prompt, visionDeps, context);
|
|
318
|
+
default:
|
|
319
|
+
throw new ValidationError(`Unknown vision command: ${command}`, 'Run "scoutline vision --help" for available commands');
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
return outcome.result;
|
|
323
|
+
}, outputMode, deps.now, deps.secrets);
|
|
260
324
|
}
|
|
261
325
|
/**
|
|
262
326
|
* Map a Vision subcommand to its discriminated operation id. Used by
|
|
@@ -380,29 +444,23 @@ async function handleSearch(args, outputMode, deps) {
|
|
|
380
444
|
if (type !== undefined && topic !== undefined) {
|
|
381
445
|
throw new ValidationError("--type and --topic are mutually exclusive (--type has no editorial topic axis).", "Pass either --type or --topic, not both.");
|
|
382
446
|
}
|
|
383
|
-
// Resolve the Provider ONLY inside shared Search (DESIGN.md §6).
|
|
384
|
-
// command families carry the parsed flag but never resolve or
|
|
385
|
-
// it. An invalid explicit/env value throws VALIDATION_ERROR
|
|
386
|
-
// any Adapter construction or invocation. Selection never
|
|
387
|
-
// credentials
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
const adapter = descriptor.create({ env: deps.env });
|
|
402
|
-
const capability = adapter.search;
|
|
403
|
-
if (!capability) {
|
|
404
|
-
throw new UnsupportedCapabilityError(providerId, "search");
|
|
405
|
-
}
|
|
447
|
+
// Resolve the effective Provider ONLY inside shared Search (DESIGN.md §6).
|
|
448
|
+
// Other command families carry the parsed flag but never resolve or
|
|
449
|
+
// validate it. An invalid explicit/env value throws VALIDATION_ERROR
|
|
450
|
+
// here, before any Adapter construction or invocation. Selection never
|
|
451
|
+
// consults credentials beyond the descriptor `isConfigured` metadata
|
|
452
|
+
// check (FR-003); the configured check is the executor's
|
|
453
|
+
// responsibility (Provider Fallback Tech Plan §"Per-handler refactor
|
|
454
|
+
// pattern"). PB-T4: when no pin is present, the effective provider is
|
|
455
|
+
// the highest-scored configured+capable provider for `search` against
|
|
456
|
+
// the injected quota snapshot; pin input still bypasses ranking.
|
|
457
|
+
const providerId = resolveEffectiveProvider({
|
|
458
|
+
explicitProvider: deps.provider,
|
|
459
|
+
env: deps.env,
|
|
460
|
+
capabilityId: "search",
|
|
461
|
+
descriptors: deps.providerDescriptors,
|
|
462
|
+
quotaSnapshot: deps.quotaState,
|
|
463
|
+
});
|
|
406
464
|
const query = positional.join(" ");
|
|
407
465
|
const fieldsRaw = flags.fields;
|
|
408
466
|
const fields = fieldsRaw
|
|
@@ -411,7 +469,7 @@ async function handleSearch(args, outputMode, deps) {
|
|
|
411
469
|
.map((f) => f.trim())
|
|
412
470
|
.filter(Boolean)
|
|
413
471
|
: undefined;
|
|
414
|
-
|
|
472
|
+
const searchOptions = {
|
|
415
473
|
count,
|
|
416
474
|
domain: flags.domain,
|
|
417
475
|
recency: flags.recency,
|
|
@@ -419,18 +477,40 @@ async function handleSearch(args, outputMode, deps) {
|
|
|
419
477
|
location: flags.location,
|
|
420
478
|
topic,
|
|
421
479
|
type,
|
|
422
|
-
maxSummary: flags["max-summary"]
|
|
423
|
-
? parseInt(flags["max-summary"], 10)
|
|
424
|
-
: undefined,
|
|
480
|
+
maxSummary: flags["max-summary"] ? parseInt(flags["max-summary"], 10) : undefined,
|
|
425
481
|
fields: fields && fields.length > 0 ? fields : undefined,
|
|
426
482
|
noCache: flags["no-cache"] === true,
|
|
427
483
|
merge: flags.merge === true,
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
484
|
+
};
|
|
485
|
+
// Provider-fallback Ticket 02: route the call through the shared
|
|
486
|
+
// fallback executor. The executor owns capability+configured+adapter
|
|
487
|
+
// preflight (FR-023/024), candidate plan ordering, error
|
|
488
|
+
// classification, exhaustion semantics, and notices. The handler
|
|
489
|
+
// keeps command-specific work (request building, flag parsing, which
|
|
490
|
+
// executeX). `--merge` runs the whole parallel sub-query batch inside
|
|
491
|
+
// a single attempt so a fallback switch replaces the entire batch,
|
|
492
|
+
// never individual sub-queries (Tech Plan §"Handler non-uniformity").
|
|
493
|
+
return invokeCommand(deps.invocation, async (context) => {
|
|
494
|
+
const outcome = await executeWithFallback({
|
|
495
|
+
capabilityId: "search",
|
|
496
|
+
commandLabel: "search",
|
|
497
|
+
effectiveProvider: providerId,
|
|
498
|
+
descriptors: deps.providerDescriptors,
|
|
499
|
+
env: deps.env,
|
|
500
|
+
fallbackEnabled: deps.fallbackEnabled,
|
|
501
|
+
writeStderr: (s) => deps.invocation.writeStderr(s),
|
|
502
|
+
}, async (descriptor) => {
|
|
503
|
+
const adapter = descriptor.create({ env: deps.env });
|
|
504
|
+
const capability = adapter.search;
|
|
505
|
+
return search(query, searchOptions, {
|
|
506
|
+
capability,
|
|
507
|
+
cache: deps.searchCache,
|
|
508
|
+
sleep: deps.searchSleep,
|
|
509
|
+
random: deps.searchRandom,
|
|
510
|
+
}, context);
|
|
511
|
+
});
|
|
512
|
+
return outcome.result;
|
|
513
|
+
}, outputMode, deps.now, deps.secrets);
|
|
434
514
|
}
|
|
435
515
|
async function handleRead(args, outputMode, deps) {
|
|
436
516
|
const { flags, positional } = parseArgs(args);
|
|
@@ -456,44 +536,19 @@ async function handleRead(args, outputMode, deps) {
|
|
|
456
536
|
}
|
|
457
537
|
const extract = extractFlag !== undefined ? extractFlag : undefined;
|
|
458
538
|
// Resolve the effective Provider (DESIGN.md §6, FR-001–FR-005):
|
|
459
|
-
// explicit --provider > SCOUTLINE_PROVIDER >
|
|
460
|
-
// never consults credentials
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
// FR-003: selection returns the default zai even when unconfigured.
|
|
473
|
-
// Supported-but-unconfigured Z.AI surfaces a missing credential as
|
|
474
|
-
// ConfigurationError (exit 3) AFTER the support metadata check and
|
|
475
|
-
// BEFORE `descriptor.create`.
|
|
476
|
-
if (!descriptor.isConfigured(deps.env)) {
|
|
477
|
-
throw new ConfigurationError(`Provider "${providerId}" is not configured. Set the required API key.`);
|
|
478
|
-
}
|
|
479
|
-
const adapter = descriptor.create({ env: deps.env });
|
|
480
|
-
const capability = adapter.reader;
|
|
481
|
-
// Defensive fail-closed: the descriptor advertised support but the
|
|
482
|
-
// Adapter omitted the handle. Treat as unsupported so a registry
|
|
483
|
-
// mismatch can never reach transport.
|
|
484
|
-
if (!capability) {
|
|
485
|
-
throw new UnsupportedCapabilityError(providerId, "reader");
|
|
486
|
-
}
|
|
487
|
-
// Shared Reader execution dependencies. The cache/sleep/random
|
|
488
|
-
// default to the same production values as Search and Repository
|
|
489
|
-
// but are kept as separate optional MainDependencies so reader
|
|
490
|
-
// tests can inject isolated in-memory doubles.
|
|
491
|
-
const executionDeps = {
|
|
492
|
-
cache: deps.readerCache,
|
|
493
|
-
sleep: deps.readerSleep,
|
|
494
|
-
random: deps.readerRandom,
|
|
495
|
-
};
|
|
496
|
-
return invokeCommand(deps.invocation, (context) => read(url, {
|
|
539
|
+
// explicit --provider > SCOUTLINE_PROVIDER > quota-ranked pick.
|
|
540
|
+
// Selection never consults credentials beyond the descriptor
|
|
541
|
+
// `isConfigured` metadata check (FR-003) and never branches on
|
|
542
|
+
// Provider ID; an unknown explicit/env value throws
|
|
543
|
+
// VALIDATION_ERROR here. (PB-T4.)
|
|
544
|
+
const providerId = resolveEffectiveProvider({
|
|
545
|
+
explicitProvider: deps.provider,
|
|
546
|
+
env: deps.env,
|
|
547
|
+
capabilityId: "reader",
|
|
548
|
+
descriptors: deps.providerDescriptors,
|
|
549
|
+
quotaSnapshot: deps.quotaState,
|
|
550
|
+
});
|
|
551
|
+
const readOptions = {
|
|
497
552
|
format: flags.format,
|
|
498
553
|
noImages: flags["no-images"] === true,
|
|
499
554
|
noCache: flags["no-cache"] === true,
|
|
@@ -505,7 +560,39 @@ async function handleRead(args, outputMode, deps) {
|
|
|
505
560
|
maxChars: flags["max-chars"] ? parseInt(flags["max-chars"], 10) : undefined,
|
|
506
561
|
fullEnvelope: flags["full-envelope"] === true,
|
|
507
562
|
extract,
|
|
508
|
-
}
|
|
563
|
+
};
|
|
564
|
+
// Shared Reader execution dependencies. The cache/sleep/random
|
|
565
|
+
// default to the same production values as Search and Repository
|
|
566
|
+
// but are kept as separate optional MainDependencies so reader
|
|
567
|
+
// tests can inject isolated in-memory doubles.
|
|
568
|
+
const executionDeps = {
|
|
569
|
+
cache: deps.readerCache,
|
|
570
|
+
sleep: deps.readerSleep,
|
|
571
|
+
random: deps.readerRandom,
|
|
572
|
+
};
|
|
573
|
+
// Provider-fallback Ticket 02: route the call through the shared
|
|
574
|
+
// fallback executor. Capability + configured + adapter-handle
|
|
575
|
+
// agreement preflight now lives in the executor (FR-023/024
|
|
576
|
+
// preserved). Under --no-fallback the same preflight runs on the
|
|
577
|
+
// effective Provider only.
|
|
578
|
+
return invokeCommand(deps.invocation, async (context) => {
|
|
579
|
+
const outcome = await executeWithFallback({
|
|
580
|
+
capabilityId: "reader",
|
|
581
|
+
commandLabel: "read",
|
|
582
|
+
effectiveProvider: providerId,
|
|
583
|
+
descriptors: deps.providerDescriptors,
|
|
584
|
+
env: deps.env,
|
|
585
|
+
fallbackEnabled: deps.fallbackEnabled,
|
|
586
|
+
writeStderr: (s) => deps.invocation.writeStderr(s),
|
|
587
|
+
}, async (descriptor) => {
|
|
588
|
+
const adapter = descriptor.create({ env: deps.env });
|
|
589
|
+
return read(url, readOptions, {
|
|
590
|
+
capability: adapter.reader,
|
|
591
|
+
execution: executionDeps,
|
|
592
|
+
}, context);
|
|
593
|
+
});
|
|
594
|
+
return outcome.result;
|
|
595
|
+
}, outputMode, deps.now, deps.secrets);
|
|
509
596
|
}
|
|
510
597
|
async function handleCrawl(args, outputMode, deps) {
|
|
511
598
|
const { flags, positional } = parseArgs(args);
|
|
@@ -522,23 +609,15 @@ async function handleCrawl(args, outputMode, deps) {
|
|
|
522
609
|
throw new ValidationError("URL must start with http:// or https://");
|
|
523
610
|
}
|
|
524
611
|
// Resolve the effective Provider (DESIGN.md §6, FR-001–FR-005):
|
|
525
|
-
// explicit --provider > SCOUTLINE_PROVIDER >
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
}
|
|
534
|
-
if (!descriptor.isConfigured(deps.env)) {
|
|
535
|
-
throw new ConfigurationError(`Provider "${providerId}" is not configured. Set the required API key.`);
|
|
536
|
-
}
|
|
537
|
-
const adapter = descriptor.create({ env: deps.env });
|
|
538
|
-
const capability = adapter.crawl;
|
|
539
|
-
if (!capability) {
|
|
540
|
-
throw new UnsupportedCapabilityError(providerId, "crawl");
|
|
541
|
-
}
|
|
612
|
+
// explicit --provider > SCOUTLINE_PROVIDER > quota-ranked pick.
|
|
613
|
+
// (PB-T4.)
|
|
614
|
+
const providerId = resolveEffectiveProvider({
|
|
615
|
+
explicitProvider: deps.provider,
|
|
616
|
+
env: deps.env,
|
|
617
|
+
capabilityId: "crawl",
|
|
618
|
+
descriptors: deps.providerDescriptors,
|
|
619
|
+
quotaSnapshot: deps.quotaState,
|
|
620
|
+
});
|
|
542
621
|
// Shared Crawl execution dependencies. The cache/sleep/random default
|
|
543
622
|
// to the same production values as Search/Repository/Reader but are
|
|
544
623
|
// kept as separate optional MainDependencies so crawl tests can inject
|
|
@@ -548,19 +627,43 @@ async function handleCrawl(args, outputMode, deps) {
|
|
|
548
627
|
sleep: deps.crawlSleep,
|
|
549
628
|
random: deps.crawlRandom,
|
|
550
629
|
};
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
630
|
+
// Provider-fallback Ticket 03: route the call through the shared
|
|
631
|
+
// fallback executor. Capability + configured + adapter-handle
|
|
632
|
+
// preflight lives in the executor (FR-023/024 preserved). The
|
|
633
|
+
// async cost-bearing accepted-risk path (Tech Plan §"Accepted
|
|
634
|
+
// risk"): a runtime failure on crawl may fall back to another
|
|
635
|
+
// Provider even if the failed Provider had already accepted
|
|
636
|
+
// the job. `--no-fallback` eliminates this risk.
|
|
637
|
+
return invokeCommand(deps.invocation, async (context) => {
|
|
638
|
+
const outcome = await executeWithFallback({
|
|
639
|
+
capabilityId: "crawl",
|
|
640
|
+
commandLabel: "crawl",
|
|
641
|
+
effectiveProvider: providerId,
|
|
642
|
+
descriptors: deps.providerDescriptors,
|
|
643
|
+
env: deps.env,
|
|
644
|
+
fallbackEnabled: deps.fallbackEnabled,
|
|
645
|
+
writeStderr: (s) => deps.invocation.writeStderr(s),
|
|
646
|
+
}, async (descriptor) => {
|
|
647
|
+
const adapter = descriptor.create({ env: deps.env });
|
|
648
|
+
return crawl(url, {
|
|
649
|
+
depth: flags.depth ? parseInt(flags.depth, 10) : undefined,
|
|
650
|
+
breadth: flags.breadth ? parseInt(flags.breadth, 10) : undefined,
|
|
651
|
+
limit: flags.limit ? parseInt(flags.limit, 10) : undefined,
|
|
652
|
+
selectPaths: flags["select-paths"],
|
|
653
|
+
excludePaths: flags["exclude-paths"],
|
|
654
|
+
instructions: flags.instructions,
|
|
655
|
+
format: flags.format,
|
|
656
|
+
contentSize: flags["content-size"],
|
|
657
|
+
timeout: flags.timeout ? parseInt(flags.timeout, 10) : undefined,
|
|
658
|
+
noCache: flags["no-cache"] === true,
|
|
659
|
+
maxChars: flags["max-chars"] ? parseInt(flags["max-chars"], 10) : undefined,
|
|
660
|
+
}, {
|
|
661
|
+
capability: adapter.crawl,
|
|
662
|
+
execution: executionDeps,
|
|
663
|
+
}, context);
|
|
664
|
+
});
|
|
665
|
+
return outcome.result;
|
|
666
|
+
}, outputMode, deps.now, deps.secrets);
|
|
564
667
|
}
|
|
565
668
|
async function handleMap(args, outputMode, deps) {
|
|
566
669
|
const { flags, positional } = parseArgs(args);
|
|
@@ -577,23 +680,15 @@ async function handleMap(args, outputMode, deps) {
|
|
|
577
680
|
throw new ValidationError("URL must start with http:// or https://");
|
|
578
681
|
}
|
|
579
682
|
// Resolve the effective Provider (DESIGN.md §6, FR-001–FR-005):
|
|
580
|
-
// explicit --provider > SCOUTLINE_PROVIDER >
|
|
581
|
-
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
}
|
|
589
|
-
if (!descriptor.isConfigured(deps.env)) {
|
|
590
|
-
throw new ConfigurationError(`Provider "${providerId}" is not configured. Set the required API key.`);
|
|
591
|
-
}
|
|
592
|
-
const adapter = descriptor.create({ env: deps.env });
|
|
593
|
-
const capability = adapter.map;
|
|
594
|
-
if (!capability) {
|
|
595
|
-
throw new UnsupportedCapabilityError(providerId, "map");
|
|
596
|
-
}
|
|
683
|
+
// explicit --provider > SCOUTLINE_PROVIDER > quota-ranked pick.
|
|
684
|
+
// (PB-T4.)
|
|
685
|
+
const providerId = resolveEffectiveProvider({
|
|
686
|
+
explicitProvider: deps.provider,
|
|
687
|
+
env: deps.env,
|
|
688
|
+
capabilityId: "map",
|
|
689
|
+
descriptors: deps.providerDescriptors,
|
|
690
|
+
quotaSnapshot: deps.quotaState,
|
|
691
|
+
});
|
|
597
692
|
// Shared Map execution dependencies. The cache/sleep/random default
|
|
598
693
|
// to the same production values as Search/Repository/Reader/Crawl but
|
|
599
694
|
// are kept as separate optional MainDependencies so map tests can
|
|
@@ -603,15 +698,37 @@ async function handleMap(args, outputMode, deps) {
|
|
|
603
698
|
sleep: deps.mapSleep,
|
|
604
699
|
random: deps.mapRandom,
|
|
605
700
|
};
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
701
|
+
// Provider-fallback Ticket 03: route the call through the shared
|
|
702
|
+
// fallback executor. Map retries once per Provider today, so the
|
|
703
|
+
// documented worst-case charged request count under retry+fallback
|
|
704
|
+
// is `2 × N candidates` — see the troubleshooting entry added in
|
|
705
|
+
// Ticket 04. `--no-fallback` is the strict-mode opt-out.
|
|
706
|
+
return invokeCommand(deps.invocation, async (context) => {
|
|
707
|
+
const outcome = await executeWithFallback({
|
|
708
|
+
capabilityId: "map",
|
|
709
|
+
commandLabel: "map",
|
|
710
|
+
effectiveProvider: providerId,
|
|
711
|
+
descriptors: deps.providerDescriptors,
|
|
712
|
+
env: deps.env,
|
|
713
|
+
fallbackEnabled: deps.fallbackEnabled,
|
|
714
|
+
writeStderr: (s) => deps.invocation.writeStderr(s),
|
|
715
|
+
}, async (descriptor) => {
|
|
716
|
+
const adapter = descriptor.create({ env: deps.env });
|
|
717
|
+
return map(url, {
|
|
718
|
+
depth: flags.depth ? parseInt(flags.depth, 10) : undefined,
|
|
719
|
+
breadth: flags.breadth ? parseInt(flags.breadth, 10) : undefined,
|
|
720
|
+
limit: flags.limit ? parseInt(flags.limit, 10) : undefined,
|
|
721
|
+
selectPaths: flags["select-paths"],
|
|
722
|
+
excludePaths: flags["exclude-paths"],
|
|
723
|
+
instructions: flags.instructions,
|
|
724
|
+
noCache: flags["no-cache"] === true,
|
|
725
|
+
}, {
|
|
726
|
+
capability: adapter.map,
|
|
727
|
+
execution: executionDeps,
|
|
728
|
+
}, context);
|
|
729
|
+
});
|
|
730
|
+
return outcome.result;
|
|
731
|
+
}, outputMode, deps.now, deps.secrets);
|
|
615
732
|
}
|
|
616
733
|
async function handleResearch(args, outputMode, deps) {
|
|
617
734
|
const { flags, positional } = parseArgs(args);
|
|
@@ -627,42 +744,82 @@ async function handleResearch(args, outputMode, deps) {
|
|
|
627
744
|
const outputLength = validateResearchEnum(flags["output-length"], ["short", "standard", "long"], "--output-length");
|
|
628
745
|
const citationFormat = validateResearchEnum(flags["citation-format"], ["numbered", "mla", "apa", "chicago"], "--citation-format");
|
|
629
746
|
// Resolve the effective Provider (DESIGN.md §6, FR-001–FR-005):
|
|
630
|
-
// explicit --provider > SCOUTLINE_PROVIDER >
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
}
|
|
639
|
-
if (!descriptor.isConfigured(deps.env)) {
|
|
640
|
-
throw new ConfigurationError(`Provider "${providerId}" is not configured. Set the required API key.`);
|
|
641
|
-
}
|
|
642
|
-
const adapter = descriptor.create({ env: deps.env });
|
|
643
|
-
const capability = adapter.research;
|
|
644
|
-
if (!capability) {
|
|
645
|
-
throw new UnsupportedCapabilityError(providerId, "research");
|
|
646
|
-
}
|
|
747
|
+
// explicit --provider > SCOUTLINE_PROVIDER > quota-ranked pick.
|
|
748
|
+
// (PB-T4.)
|
|
749
|
+
const providerId = resolveEffectiveProvider({
|
|
750
|
+
explicitProvider: deps.provider,
|
|
751
|
+
env: deps.env,
|
|
752
|
+
capabilityId: "research",
|
|
753
|
+
descriptors: deps.providerDescriptors,
|
|
754
|
+
quotaSnapshot: deps.quotaState,
|
|
755
|
+
});
|
|
647
756
|
// Shared Research execution dependencies.
|
|
648
757
|
const executionDeps = {
|
|
649
758
|
cache: deps.researchCache,
|
|
650
759
|
sleep: deps.researchSleep,
|
|
651
760
|
random: deps.researchRandom,
|
|
652
761
|
};
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
762
|
+
// Provider-fallback Ticket 03: route the call through the shared
|
|
763
|
+
// fallback executor. The research command is the riskiest piece
|
|
764
|
+
// of the async set: the SIGINT handler, the polling-timeout
|
|
765
|
+
// controller, and the on-disk state-file identity/timeout must
|
|
766
|
+
// RE-BIND to the Provider that actually wins the candidate loop
|
|
767
|
+
// (Tech Plan §"Handler non-uniformity" — research bullet). The
|
|
768
|
+
// `research` command accepts a `registerInterrupt` injection on
|
|
769
|
+
// its handler dependencies; the executor's per-attempt callback
|
|
770
|
+
// builds the descriptor's `capability.run` and re-registers the
|
|
771
|
+
// SIGINT handler under THAT provider's identity before invoke,
|
|
772
|
+
// tearing it down on throw so the next candidate installs its
|
|
773
|
+
// own. The one-time credit warning is emitted ONCE, before the
|
|
774
|
+
// first attempt, so it is not duplicated across candidate
|
|
775
|
+
// switches.
|
|
776
|
+
return invokeCommand(deps.invocation, async (context) => {
|
|
777
|
+
const options = {
|
|
778
|
+
model,
|
|
779
|
+
outputLength,
|
|
780
|
+
citationFormat,
|
|
781
|
+
domain: flags.domain,
|
|
782
|
+
maxChars: flags["max-chars"] ? parseInt(flags["max-chars"], 10) : undefined,
|
|
783
|
+
timeout: flags.timeout ? parseInt(flags.timeout, 10) : undefined,
|
|
784
|
+
noCache: flags["no-cache"] === true,
|
|
785
|
+
};
|
|
786
|
+
// Closure-guarded one-time credit warning (Review Fix 7). The
|
|
787
|
+
// flag is captured INSIDE the attempt closure, so the line is
|
|
788
|
+
// written exactly once — the first time the executor actually
|
|
789
|
+
// visits a candidate. When the executor's preflight rejects
|
|
790
|
+
// every plan entry before invoking `attempt` (capability-
|
|
791
|
+
// mismatch, no candidates configured), the closure is never
|
|
792
|
+
// entered and the warning stays silent. Strict mode
|
|
793
|
+
// (`--no-fallback`) already runs the same preflight on the
|
|
794
|
+
// single remaining candidate so an ineligible effective also
|
|
795
|
+
// stays silent.
|
|
796
|
+
let warned = false;
|
|
797
|
+
const creditWarning = `Research in progress — this is a credit-intensive operation that may take several minutes.\n`;
|
|
798
|
+
const emitCreditWarning = () => {
|
|
799
|
+
if (warned)
|
|
800
|
+
return;
|
|
801
|
+
warned = true;
|
|
802
|
+
deps.invocation.writeStderr(creditWarning);
|
|
803
|
+
};
|
|
804
|
+
const outcome = await executeWithFallback({
|
|
805
|
+
capabilityId: "research",
|
|
806
|
+
commandLabel: "research",
|
|
807
|
+
effectiveProvider: providerId,
|
|
808
|
+
descriptors: deps.providerDescriptors,
|
|
809
|
+
env: deps.env,
|
|
810
|
+
fallbackEnabled: deps.fallbackEnabled,
|
|
811
|
+
writeStderr: (s) => deps.invocation.writeStderr(s),
|
|
812
|
+
}, async (descriptor) => {
|
|
813
|
+
emitCreditWarning();
|
|
814
|
+
const adapter = descriptor.create({ env: deps.env });
|
|
815
|
+
return research(query, options, {
|
|
816
|
+
capability: adapter.research,
|
|
817
|
+
execution: executionDeps,
|
|
818
|
+
registerInterrupt: deps.researchRegisterInterrupt,
|
|
819
|
+
}, context);
|
|
820
|
+
});
|
|
821
|
+
return outcome.result;
|
|
822
|
+
}, outputMode, deps.now, deps.secrets);
|
|
666
823
|
}
|
|
667
824
|
/**
|
|
668
825
|
* Validate a research enum flag (--model, --output-length,
|
|
@@ -721,34 +878,18 @@ async function handleRepo(args, outputMode, deps) {
|
|
|
721
878
|
throw new ValidationError(`Unknown repo command: ${command}`, 'Run "scoutline repo --help" for available commands');
|
|
722
879
|
}
|
|
723
880
|
// Resolve the effective Provider (DESIGN.md §6, FR-001–FR-005):
|
|
724
|
-
// explicit --provider > SCOUTLINE_PROVIDER >
|
|
725
|
-
// never consults credentials
|
|
726
|
-
//
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
737
|
-
// FR-003: selection returns the default zai even when unconfigured.
|
|
738
|
-
// Supported-but-unconfigured Z.AI surfaces a missing credential as
|
|
739
|
-
// ConfigurationError (exit 3) AFTER the support metadata check and
|
|
740
|
-
// BEFORE `descriptor.create`.
|
|
741
|
-
if (!descriptor.isConfigured(deps.env)) {
|
|
742
|
-
throw new ConfigurationError(`Provider "${providerId}" is not configured. Set the required API key.`);
|
|
743
|
-
}
|
|
744
|
-
const adapter = descriptor.create({ env: deps.env });
|
|
745
|
-
const capability = adapter.repository;
|
|
746
|
-
// Defensive fail-closed: the descriptor advertised support but the
|
|
747
|
-
// Adapter omitted the handle. Treat as unsupported so a registry
|
|
748
|
-
// mismatch can never reach transport.
|
|
749
|
-
if (!capability) {
|
|
750
|
-
throw new UnsupportedCapabilityError(providerId, "repository-exploration");
|
|
751
|
-
}
|
|
881
|
+
// explicit --provider > SCOUTLINE_PROVIDER > quota-ranked pick.
|
|
882
|
+
// Selection never consults credentials beyond the descriptor
|
|
883
|
+
// `isConfigured` metadata check (FR-003) and never branches on
|
|
884
|
+
// Provider ID; an unknown explicit/env value throws
|
|
885
|
+
// VALIDATION_ERROR here. (PB-T4.)
|
|
886
|
+
const providerId = resolveEffectiveProvider({
|
|
887
|
+
explicitProvider: deps.provider,
|
|
888
|
+
env: deps.env,
|
|
889
|
+
capabilityId: "repository-exploration",
|
|
890
|
+
descriptors: deps.providerDescriptors,
|
|
891
|
+
quotaSnapshot: deps.quotaState,
|
|
892
|
+
});
|
|
752
893
|
// Shared Repository execution dependencies. The cache/sleep/random
|
|
753
894
|
// default to the same production values as Search but are kept as
|
|
754
895
|
// separate optional MainDependencies so repository tests can inject
|
|
@@ -758,30 +899,43 @@ async function handleRepo(args, outputMode, deps) {
|
|
|
758
899
|
sleep: deps.repositorySleep,
|
|
759
900
|
random: deps.repositoryRandom,
|
|
760
901
|
};
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
902
|
+
const language = flags.language;
|
|
903
|
+
const maxChars = flags["max-chars"] ? parseInt(flags["max-chars"], 10) : undefined;
|
|
904
|
+
const noCache = flags["no-cache"] === true;
|
|
905
|
+
const treePath = flags.path;
|
|
906
|
+
const depth = flags.depth ? parseInt(flags.depth, 10) : undefined;
|
|
907
|
+
// Provider-fallback Ticket 02: route every subcommand through the
|
|
908
|
+
// shared executor. All three projections (search/tree/read) share
|
|
909
|
+
// the `repository-exploration` Capability, so a single executor
|
|
910
|
+
// invocation wraps the chosen subcommand.
|
|
911
|
+
return invokeCommand(deps.invocation, async (context) => {
|
|
912
|
+
const outcome = await executeWithFallback({
|
|
913
|
+
capabilityId: "repository-exploration",
|
|
914
|
+
commandLabel: "repo",
|
|
915
|
+
effectiveProvider: providerId,
|
|
916
|
+
descriptors: deps.providerDescriptors,
|
|
917
|
+
env: deps.env,
|
|
918
|
+
fallbackEnabled: deps.fallbackEnabled,
|
|
919
|
+
writeStderr: (s) => deps.invocation.writeStderr(s),
|
|
920
|
+
}, async (descriptor) => {
|
|
921
|
+
const adapter = descriptor.create({ env: deps.env });
|
|
922
|
+
const capability = adapter.repository;
|
|
923
|
+
switch (command) {
|
|
924
|
+
case "search":
|
|
925
|
+
return repoSearch(repo, searchQuery, { language, maxChars, noCache }, { capability, execution: executionDeps }, context);
|
|
926
|
+
case "tree":
|
|
927
|
+
return repoTree(repo, { path: treePath, depth, noCache }, { capability, execution: executionDeps }, context);
|
|
928
|
+
case "read":
|
|
929
|
+
return repoRead(repo, readPath, { maxChars, noCache }, { capability, execution: executionDeps }, context);
|
|
930
|
+
default:
|
|
931
|
+
// Unreachable: the parse-level validation above already
|
|
932
|
+
// rejected unknown subcommands. Keep a defensive throw so
|
|
933
|
+
// the dispatch table stays total.
|
|
934
|
+
throw new ValidationError(`Unknown repo command: ${command}`, 'Run "scoutline repo --help" for available commands');
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
return outcome.result;
|
|
938
|
+
}, outputMode, deps.now, deps.secrets);
|
|
785
939
|
}
|
|
786
940
|
async function handleTools(args, outputMode, deps) {
|
|
787
941
|
const { flags } = parseArgs(args);
|
|
@@ -794,6 +948,7 @@ async function handleTools(args, outputMode, deps) {
|
|
|
794
948
|
full: flags.full === true,
|
|
795
949
|
typescript: flags.typescript === true || flags.ts === true,
|
|
796
950
|
enableVision: flags.vision !== false,
|
|
951
|
+
env: deps.env,
|
|
797
952
|
}, context), outputMode, deps.now, deps.secrets);
|
|
798
953
|
}
|
|
799
954
|
async function handleTool(args, outputMode, deps) {
|
|
@@ -802,7 +957,7 @@ async function handleTool(args, outputMode, deps) {
|
|
|
802
957
|
deps.invocation.writeStdout(TOOLS_HELP);
|
|
803
958
|
return 0;
|
|
804
959
|
}
|
|
805
|
-
return invokeCommand(deps.invocation, (context) => showTool(positional[0], { enableVision: flags.vision !== false }, context), outputMode, deps.now, deps.secrets);
|
|
960
|
+
return invokeCommand(deps.invocation, (context) => showTool(positional[0], { enableVision: flags.vision !== false, env: deps.env }, context), outputMode, deps.now, deps.secrets);
|
|
806
961
|
}
|
|
807
962
|
async function handleCall(args, outputMode, deps) {
|
|
808
963
|
const { flags, positional } = parseArgs(args);
|
|
@@ -816,6 +971,7 @@ async function handleCall(args, outputMode, deps) {
|
|
|
816
971
|
stdin: flags.stdin === true,
|
|
817
972
|
dryRun: flags["dry-run"] === true,
|
|
818
973
|
enableVision: flags.vision !== false,
|
|
974
|
+
env: deps.env,
|
|
819
975
|
}, context), outputMode, deps.now, deps.secrets);
|
|
820
976
|
}
|
|
821
977
|
async function handleDoctor(args, outputMode, deps) {
|
|
@@ -845,6 +1001,29 @@ async function handleDoctor(args, outputMode, deps) {
|
|
|
845
1001
|
sleep: deps.searchSleep,
|
|
846
1002
|
random: deps.searchRandom,
|
|
847
1003
|
cacheSummary,
|
|
1004
|
+
// T3b: thread the injected promoter + clock through to the
|
|
1005
|
+
// report builder so a successful probe can flip the matching
|
|
1006
|
+
// record from `unverified` to `verified`. When the promoter
|
|
1007
|
+
// is absent (hermetic test path), Doctor runs without
|
|
1008
|
+
// promotion. The write-failure sink surfaces promotion
|
|
1009
|
+
// errors as a stderr notice without affecting the exit code.
|
|
1010
|
+
verificationPromoter: deps.verificationPromoter,
|
|
1011
|
+
now: deps.now ?? Date.now,
|
|
1012
|
+
onPromotionError: (providerId, error) => {
|
|
1013
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1014
|
+
deps.invocation.writeStderr(`scoutline: verification promotion failed for "${providerId}": ${message}\n`);
|
|
1015
|
+
},
|
|
1016
|
+
// PB-T5: thread the snapshot + verification records through
|
|
1017
|
+
// to the report builder so each Provider entry carries a
|
|
1018
|
+
// `quota` summary (source/freshness) and a `verification`
|
|
1019
|
+
// summary. Both are pure state reads; Doctor never
|
|
1020
|
+
// live-probes quota. The snapshot appears even under
|
|
1021
|
+
// --no-tools (a snapshot read is local state, not
|
|
1022
|
+
// transport).
|
|
1023
|
+
...(deps.quotaState !== undefined ? { quotaSnapshot: deps.quotaState } : {}),
|
|
1024
|
+
...(deps.verificationRecords !== undefined
|
|
1025
|
+
? { verificationRecords: deps.verificationRecords }
|
|
1026
|
+
: {}),
|
|
848
1027
|
}),
|
|
849
1028
|
}), outputMode, deps.now, deps.secrets);
|
|
850
1029
|
}
|
|
@@ -904,6 +1083,17 @@ async function handleQuota(args, outputMode, deps) {
|
|
|
904
1083
|
env: deps.env,
|
|
905
1084
|
sleep: deps.searchSleep,
|
|
906
1085
|
random: deps.searchRandom,
|
|
1086
|
+
// PB-T5: thread the snapshot + store + clock through so the
|
|
1087
|
+
// dashboard reads each Provider's snapshot first, labels
|
|
1088
|
+
// source/freshness, and persists a live-probe fallback via
|
|
1089
|
+
// the awaited write-through. When `quotaState` is absent
|
|
1090
|
+
// (test path that didn't opt in), the snapshot path is
|
|
1091
|
+
// disabled and every configured Provider is live-probed
|
|
1092
|
+
// (byte-for-byte pre-PB-T5 behavior — no `quotaSource`
|
|
1093
|
+
// field attached).
|
|
1094
|
+
...(deps.quotaState !== undefined ? { quotaSnapshot: deps.quotaState } : {}),
|
|
1095
|
+
...(deps.quotaStore !== undefined ? { quotaStore: deps.quotaStore } : {}),
|
|
1096
|
+
now: deps.now ?? Date.now,
|
|
907
1097
|
}),
|
|
908
1098
|
writeStderr: (value) => deps.invocation.writeStderr(value),
|
|
909
1099
|
secrets: deps.secrets,
|
|
@@ -924,17 +1114,17 @@ async function handleCode(args, outputMode, deps) {
|
|
|
924
1114
|
if (!filePath) {
|
|
925
1115
|
throw new ValidationError("Missing code file", "Usage: scoutline code run <file>");
|
|
926
1116
|
}
|
|
927
|
-
return invokeCommand(deps.invocation, (context) => runCodeFile(filePath, { timeout, includeLogs }, context), outputMode, deps.now, deps.secrets);
|
|
1117
|
+
return invokeCommand(deps.invocation, (context) => runCodeFile(filePath, { timeout, includeLogs, env: deps.env }, context), outputMode, deps.now, deps.secrets);
|
|
928
1118
|
}
|
|
929
1119
|
case "eval": {
|
|
930
1120
|
const code = positional.slice(1).join(" ");
|
|
931
1121
|
if (!code) {
|
|
932
1122
|
throw new ValidationError("Missing code string", "Usage: scoutline code eval <code>");
|
|
933
1123
|
}
|
|
934
|
-
return invokeCommand(deps.invocation, (context) => evalCode(code, { timeout, includeLogs }, context), outputMode, deps.now, deps.secrets);
|
|
1124
|
+
return invokeCommand(deps.invocation, (context) => evalCode(code, { timeout, includeLogs, env: deps.env }, context), outputMode, deps.now, deps.secrets);
|
|
935
1125
|
}
|
|
936
1126
|
case "interfaces":
|
|
937
|
-
return invokeCommand(deps.invocation, (context) => printInterfaces(context), outputMode, deps.now, deps.secrets);
|
|
1127
|
+
return invokeCommand(deps.invocation, (context) => printInterfaces({ env: deps.env }, context), outputMode, deps.now, deps.secrets);
|
|
938
1128
|
case "prompt":
|
|
939
1129
|
return invokeCommand(deps.invocation, async (context) => printPromptTemplate(context), outputMode, deps.now, deps.secrets);
|
|
940
1130
|
default:
|
|
@@ -942,6 +1132,37 @@ async function handleCode(args, outputMode, deps) {
|
|
|
942
1132
|
}
|
|
943
1133
|
}
|
|
944
1134
|
const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1135
|
+
/**
|
|
1136
|
+
* Map Plan A's `ProviderVerification` records (config-store shape) to
|
|
1137
|
+
* the capability contract's `ProviderVerificationSummary` (PB-T5). The
|
|
1138
|
+
* shapes are structurally identical; the indirection exists only so
|
|
1139
|
+
* `capabilities/diagnostics.ts` does not import `lib/config-store.ts`
|
|
1140
|
+
* (the capability contract keeps a strict import boundary). Returns
|
|
1141
|
+
* `undefined` when no Provider has a verification record, so the
|
|
1142
|
+
* dispatcher can omit the dependency entirely (Doctor's report builder
|
|
1143
|
+
* leaves the `verification` field off in that case).
|
|
1144
|
+
*
|
|
1145
|
+
* When the caller injected its own `verificationRecords` (test path),
|
|
1146
|
+
* that injection wins — the test owns the verification view.
|
|
1147
|
+
*/
|
|
1148
|
+
function loadVerificationRecords(config, dependencies) {
|
|
1149
|
+
if (dependencies.verificationRecords !== undefined)
|
|
1150
|
+
return dependencies.verificationRecords;
|
|
1151
|
+
const out = {};
|
|
1152
|
+
let hasAny = false;
|
|
1153
|
+
for (const id of Object.keys(config.providers)) {
|
|
1154
|
+
const record = config.providers[id]?.verification;
|
|
1155
|
+
if (!record)
|
|
1156
|
+
continue;
|
|
1157
|
+
out[id] = {
|
|
1158
|
+
status: record.status,
|
|
1159
|
+
checkedAt: record.checkedAt,
|
|
1160
|
+
...(record.reason !== undefined ? { reason: record.reason } : {}),
|
|
1161
|
+
};
|
|
1162
|
+
hasAny = true;
|
|
1163
|
+
}
|
|
1164
|
+
return hasAny ? out : undefined;
|
|
1165
|
+
}
|
|
945
1166
|
export async function main(args, dependencies) {
|
|
946
1167
|
const { invocation, env, now } = dependencies;
|
|
947
1168
|
const providerDescriptors = dependencies.providerDescriptors ?? BUILT_IN_PROVIDER_DESCRIPTORS;
|
|
@@ -985,8 +1206,13 @@ export async function main(args, dependencies) {
|
|
|
985
1206
|
// Resolve configured Provider credentials from the INJECTED env (B3) so
|
|
986
1207
|
// redaction follows the same environment the handlers see — a secret
|
|
987
1208
|
// that exists only in MainDependencies.env is still redacted from output.
|
|
988
|
-
|
|
989
|
-
|
|
1209
|
+
// T2a: this env-only view is used by credential-free commands that
|
|
1210
|
+
// short-circuit before config load; the full resolvedEnv (env + file keys)
|
|
1211
|
+
// is computed below for credentialed paths.
|
|
1212
|
+
const envSecrets = configuredSecrets(env);
|
|
1213
|
+
const { outputFormat, forcePretty, forceRaw, provider, noFallback, rest } = extractGlobalOptions([
|
|
1214
|
+
...args,
|
|
1215
|
+
]);
|
|
990
1216
|
// Fixup C — B10: resolve the output mode BEFORE the dispatch try/catch.
|
|
991
1217
|
// An invalid explicit mode still surfaces as a typed ValidationError,
|
|
992
1218
|
// but the surface formatter uses the user's REQUESTED mode (or the
|
|
@@ -1001,9 +1227,12 @@ export async function main(args, dependencies) {
|
|
|
1001
1227
|
catch (error) {
|
|
1002
1228
|
// The explicit mode is invalid — fall back to a deterministic
|
|
1003
1229
|
// compact form so we can still surface a structured error envelope.
|
|
1004
|
-
invocation.writeStderr(formatErrorOutput(error, "data",
|
|
1230
|
+
invocation.writeStderr(formatErrorOutput(error, "data", envSecrets));
|
|
1005
1231
|
return getErrorExitCode(error);
|
|
1006
1232
|
}
|
|
1233
|
+
// Credential-free commands: --help / --version short-circuit before any
|
|
1234
|
+
// config-file load so a corrupt or unreadable config.json never blocks
|
|
1235
|
+
// them (T2a — command classification).
|
|
1007
1236
|
if (rest.length === 0 || rest[0] === "--help" || rest[0] === "-h") {
|
|
1008
1237
|
invocation.writeStdout(MAIN_HELP);
|
|
1009
1238
|
return 0;
|
|
@@ -1014,13 +1243,53 @@ export async function main(args, dependencies) {
|
|
|
1014
1243
|
}
|
|
1015
1244
|
const command = rest[0];
|
|
1016
1245
|
const commandArgs = rest.slice(1);
|
|
1017
|
-
|
|
1246
|
+
// PB-T1/PB-T2 — Quota snapshot store + consumption sink.
|
|
1247
|
+
//
|
|
1248
|
+
// Constructed once here so `buildHandlerDeps` can close over
|
|
1249
|
+
// `consume` and thread it through every handler. The hermeticity
|
|
1250
|
+
// gate (`quotaRefreshEnabled`, mirroring trigger-detection): the
|
|
1251
|
+
// PRODUCTION sink is built ONLY in full production mode (no
|
|
1252
|
+
// injected `loadScoutlineConfig` AND no injected `providerDescriptors`).
|
|
1253
|
+
// Either injection signals a test that owns its own descriptor/config
|
|
1254
|
+
// construction; without this gate, the production sink would silently
|
|
1255
|
+
// reach the user's real `~/.scoutline/state.json` during a test run
|
|
1256
|
+
// that did not inject `quotaStore` either. Tests that need to assert
|
|
1257
|
+
// on consumption inject `MainDependencies.consume` directly.
|
|
1258
|
+
//
|
|
1259
|
+
// The sink is the *where* of consumption recording; shared execution
|
|
1260
|
+
// (`lib/execution.ts`) is the *when* (one event per billable invoke
|
|
1261
|
+
// attempt, after the cache-miss check, around each `invoke()` call).
|
|
1262
|
+
// Shared execution awaits `record()` per invoke attempt, so the write
|
|
1263
|
+
// is on the critical path before the result returns outward —
|
|
1264
|
+
// surviving the bin's immediate `process.exit(status)`.
|
|
1265
|
+
const quotaRefreshEnabled = !dependencies.loadScoutlineConfig && !dependencies.providerDescriptors;
|
|
1266
|
+
const quotaStore = dependencies.quotaStore ?? createDefaultQuotaStore();
|
|
1267
|
+
const consume = dependencies.consume ??
|
|
1268
|
+
(quotaRefreshEnabled
|
|
1269
|
+
? createQuotaStoreConsumptionSink({ store: quotaStore, now: now ?? Date.now })
|
|
1270
|
+
: undefined);
|
|
1271
|
+
// PB-T4: quota snapshot for selection. Declared here so
|
|
1272
|
+
// `buildHandlerDeps` closes over the binding; assigned AFTER the
|
|
1273
|
+
// PB-T1 pre-command refresh so observational commands' fresh data is
|
|
1274
|
+
// reflected. The seven shared handlers consume this via
|
|
1275
|
+
// `resolveEffectiveProvider`; Doctor/quota/cache/init/raw-tools
|
|
1276
|
+
// ignore it. Hermeticity gate mirrors `consume`: the PRODUCTION read
|
|
1277
|
+
// runs only in full production mode; tests inject `quotaState`
|
|
1278
|
+
// directly when they assert a specific selection outcome, and
|
|
1279
|
+
// otherwise leave it undefined so the resolver degrades to
|
|
1280
|
+
// first-eligible (pre-PB-T4 behaviour).
|
|
1281
|
+
let quotaState = dependencies.quotaState;
|
|
1282
|
+
// Build HandlerDependencies for a given credential view. The
|
|
1283
|
+
// cache/sleep/random fields are always available (resolved above); only
|
|
1284
|
+
// env/secrets/fallbackEnabled depend on whether config has been loaded.
|
|
1285
|
+
const buildHandlerDeps = (credEnv, credSecrets, credFallback) => ({
|
|
1018
1286
|
invocation,
|
|
1019
|
-
env,
|
|
1020
|
-
secrets,
|
|
1287
|
+
env: credEnv,
|
|
1288
|
+
secrets: credSecrets,
|
|
1021
1289
|
now,
|
|
1022
1290
|
provider,
|
|
1023
1291
|
providerDescriptors,
|
|
1292
|
+
fallbackEnabled: credFallback,
|
|
1024
1293
|
searchCache,
|
|
1025
1294
|
searchSleep,
|
|
1026
1295
|
searchRandom,
|
|
@@ -1039,40 +1308,342 @@ export async function main(args, dependencies) {
|
|
|
1039
1308
|
researchCache,
|
|
1040
1309
|
researchSleep,
|
|
1041
1310
|
researchRandom,
|
|
1311
|
+
researchRegisterInterrupt: dependencies.researchRegisterInterrupt,
|
|
1312
|
+
// Promoter wiring (T3b). When the caller explicitly injects a
|
|
1313
|
+
// promoter, use it (tests assert behavior this way). Otherwise, in
|
|
1314
|
+
// PRODUCTION (no injected config reader), construct the default
|
|
1315
|
+
// real-file promoter. When a test injects `loadScoutlineConfig`
|
|
1316
|
+
// (hermetic in-memory config), promotion is DISABLED so the
|
|
1317
|
+
// default promoter never reaches the user's real
|
|
1318
|
+
// ~/.scoutline/config.json during a test run.
|
|
1319
|
+
verificationPromoter: dependencies.verificationPromoter ??
|
|
1320
|
+
(dependencies.loadScoutlineConfig ? undefined : createDefaultVerificationPromoter()),
|
|
1321
|
+
// PB-T2: thread the production consumption sink through. `consume`
|
|
1322
|
+
// is constructed once in `main` (below) and shared by every
|
|
1323
|
+
// handler; tests inject their own through `MainDependencies.consume`.
|
|
1324
|
+
// When undefined (test path that didn't opt in), no events fire.
|
|
1325
|
+
consume,
|
|
1326
|
+
// PB-T4: thread the quota snapshot for selection. `quotaState` is
|
|
1327
|
+
// resolved once in `main` (below) after the PB-T1 pre-command
|
|
1328
|
+
// refresh and shared by every handler; tests inject their own
|
|
1329
|
+
// through `MainDependencies.quotaState`. When undefined (test path
|
|
1330
|
+
// or no-snapshot production run), `resolveEffectiveProvider`
|
|
1331
|
+
// degrades to first-eligible in registry order.
|
|
1332
|
+
quotaState,
|
|
1333
|
+
// PB-T5: thread the quota store for `quota`'s live-probe
|
|
1334
|
+
// write-through. Doctor ignores this field (it never live-probes
|
|
1335
|
+
// quota). Tests inject an in-memory double; production wires the
|
|
1336
|
+
// singleton constructed in `main`.
|
|
1337
|
+
quotaStore: dependencies.quotaStore ?? (quotaRefreshEnabled ? quotaStore : undefined),
|
|
1338
|
+
// PB-T5: verification records are NOT threaded here. They are
|
|
1339
|
+
// derived from `config` AFTER it is loaded (the credentialed
|
|
1340
|
+
// path); `buildHandlerDeps` runs once BEFORE config load (the
|
|
1341
|
+
// cache short-circuit), so referencing `config` here would be a
|
|
1342
|
+
// use-before-initialization error. The post-config
|
|
1343
|
+
// `handlerDepsWithVerification` spread below threads the resolved
|
|
1344
|
+
// records in for Doctor.
|
|
1345
|
+
});
|
|
1346
|
+
// Cache is credential-free (no Provider resolution, no descriptor lookup,
|
|
1347
|
+
// no transport). Short-circuit before config load so a corrupt
|
|
1348
|
+
// config.json never blocks cache inspection or clearing. Env-only
|
|
1349
|
+
// secrets suffice: the stats/clear output carries no credentials.
|
|
1350
|
+
if (command === "cache") {
|
|
1351
|
+
try {
|
|
1352
|
+
return await handleCache(commandArgs, outputMode, buildHandlerDeps(env, envSecrets, true));
|
|
1353
|
+
}
|
|
1354
|
+
catch (error) {
|
|
1355
|
+
invocation.writeStderr(formatErrorOutput(error, outputMode, envSecrets));
|
|
1356
|
+
return getErrorExitCode(error);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
// `init` manages config itself (it inspects + writes via the T1
|
|
1360
|
+
// primitives, never reads the resolvedEnv the credentialed path
|
|
1361
|
+
// produces). Short-circuit before the credentialed config load so a
|
|
1362
|
+
// corrupt or unreadable config.json never blocks the wizard that is
|
|
1363
|
+
// documented to repair it. The wizard is presented-only; it does not
|
|
1364
|
+
// dispatch through the credentialed handler boundary. Release gate
|
|
1365
|
+
// (T3a ticket): the command's code lands now, but its public docs
|
|
1366
|
+
// (MAIN_HELP Commands list, README setup, skills/) wait for T3b.
|
|
1367
|
+
if (command === "init") {
|
|
1368
|
+
const initDeps = {
|
|
1369
|
+
descriptors: providerDescriptors,
|
|
1370
|
+
prompts: dependencies.initPrompts ?? createInquirerPrompts(),
|
|
1371
|
+
configStore: dependencies.initConfigStore ?? createDefaultConfigStore(),
|
|
1372
|
+
env,
|
|
1373
|
+
now: now ?? Date.now,
|
|
1374
|
+
stdinIsTTY: invocation.stdinIsTTY,
|
|
1375
|
+
writeStderr: (value) => invocation.writeStderr(value),
|
|
1376
|
+
writeStdout: (value) => invocation.writeStdout(value),
|
|
1377
|
+
};
|
|
1378
|
+
try {
|
|
1379
|
+
return await handleInitWithHelp(commandArgs, initDeps);
|
|
1380
|
+
}
|
|
1381
|
+
catch (error) {
|
|
1382
|
+
invocation.writeStderr(formatErrorOutput(error, outputMode, envSecrets));
|
|
1383
|
+
return getErrorExitCode(error);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
// Credentialed path: load the config file (T2a — Plan A). T3b makes
|
|
1387
|
+
// the load TOLERANT so command help remains usable under a corrupt
|
|
1388
|
+
// config (review item 9: "do not force a credential check merely to
|
|
1389
|
+
// render help"). The injected reader (when provided) is still strict
|
|
1390
|
+
// for backward compatibility with existing tests; the production
|
|
1391
|
+
// path uses `inspectConfig`.
|
|
1392
|
+
//
|
|
1393
|
+
// Classification of the corrupt case:
|
|
1394
|
+
// - command help (`<cmd> --help`): proceed with an empty config so
|
|
1395
|
+
// the handler can render its help. The handler's own help
|
|
1396
|
+
// short-circuit never touches config.
|
|
1397
|
+
// - everything else (including observational `doctor`/`quota`):
|
|
1398
|
+
// refuse with the existing CONFIGURATION_ERROR (exit 3). The
|
|
1399
|
+
// error's help points at `init` as the recovery path.
|
|
1400
|
+
// Observational commands do NOT bypass the corrupt refuse — they
|
|
1401
|
+
// still need `resolvedEnv` (which needs config) to probe/report.
|
|
1402
|
+
const isHelpInvocation = isCommandHelpInvocation(commandArgs);
|
|
1403
|
+
const isObservational = OBSERVATIONAL_COMMANDS.has(command);
|
|
1404
|
+
let config;
|
|
1405
|
+
if (dependencies.loadScoutlineConfig) {
|
|
1406
|
+
try {
|
|
1407
|
+
config = await dependencies.loadScoutlineConfig();
|
|
1408
|
+
}
|
|
1409
|
+
catch (error) {
|
|
1410
|
+
if (isHelpInvocation) {
|
|
1411
|
+
config = { version: 1, providers: {} };
|
|
1412
|
+
}
|
|
1413
|
+
else {
|
|
1414
|
+
invocation.writeStderr(formatErrorOutput(error, outputMode, envSecrets));
|
|
1415
|
+
return getErrorExitCode(error);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
else {
|
|
1420
|
+
const inspection = await inspectConfig();
|
|
1421
|
+
if (inspection.status === "corrupt") {
|
|
1422
|
+
if (isHelpInvocation) {
|
|
1423
|
+
config = { version: 1, providers: {} };
|
|
1424
|
+
}
|
|
1425
|
+
else {
|
|
1426
|
+
invocation.writeStderr(formatErrorOutput(inspection.error, outputMode, envSecrets));
|
|
1427
|
+
return getErrorExitCode(inspection.error);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
else if (inspection.status === "absent") {
|
|
1431
|
+
config = { version: 1, providers: {} };
|
|
1432
|
+
}
|
|
1433
|
+
else {
|
|
1434
|
+
config = inspection.config;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
// Build resolvedEnv: the injected env with file-configured API keys
|
|
1438
|
+
// layered in for any Provider NOT already configured via env (env
|
|
1439
|
+
// overrides file; alias precedence preserved). process.env is never
|
|
1440
|
+
// mutated — the returned object is a fresh shallow copy owned by this
|
|
1441
|
+
// invocation.
|
|
1442
|
+
const resolvedEnv = resolveEnvFromConfig(env, config, providerDescriptors);
|
|
1443
|
+
const secrets = configuredSecrets(resolvedEnv);
|
|
1444
|
+
// Trigger detection (T3b — Option B). The ONLY interception here is
|
|
1445
|
+
// the env-only one-time hint: when the user is running on
|
|
1446
|
+
// environment-variable credentials and has never been through
|
|
1447
|
+
// `scoutline init`, we emit a single stderr hint pointing at the
|
|
1448
|
+
// wizard and persist `config.json.hintShown` so it never repeats.
|
|
1449
|
+
// The command then runs normally and preserves its natural output/exit.
|
|
1450
|
+
//
|
|
1451
|
+
// The "missing credential everywhere" case is NOT intercepted here:
|
|
1452
|
+
// the handler's own preflight already surfaces the existing
|
|
1453
|
+
// `CONFIGURATION_ERROR` exit 3 (see
|
|
1454
|
+
// {@link missingCredentialError} in trigger-detection.ts for the
|
|
1455
|
+
// shared error contract). Intercepting it pre-dispatch would break
|
|
1456
|
+
// the locked validation-before-configuration ordering (an invalid
|
|
1457
|
+
// `--count` must exit 1 with VALIDATION_ERROR even when no credential
|
|
1458
|
+
// is present — see search.test.js "count validation ordering").
|
|
1459
|
+
//
|
|
1460
|
+
// Hermeticity: trigger detection runs ONLY in full production mode
|
|
1461
|
+
// (no injected `loadScoutlineConfig` AND no injected
|
|
1462
|
+
// `providerDescriptors`). Either injection signals a test that owns
|
|
1463
|
+
// its own config/descriptor construction; fake descriptors routinely
|
|
1464
|
+
// lie about `isConfigured`, so trusting them here would mis-classify.
|
|
1465
|
+
// Dedicated trigger-detection tests run via subprocess (the real
|
|
1466
|
+
// binary) or via `main()` without injecting either, optionally
|
|
1467
|
+
// pointed at a temp `SCOUTLINE_CONFIG_DIR`.
|
|
1468
|
+
const triggerDetectionEnabled = !dependencies.loadScoutlineConfig && !dependencies.providerDescriptors;
|
|
1469
|
+
if (triggerDetectionEnabled && !isHelpInvocation && !isObservational) {
|
|
1470
|
+
const state = classifyCredentialState({
|
|
1471
|
+
descriptors: providerDescriptors,
|
|
1472
|
+
env,
|
|
1473
|
+
resolvedEnv,
|
|
1474
|
+
config,
|
|
1475
|
+
});
|
|
1476
|
+
if (state.kind === "env-only" && config.hintShown !== true) {
|
|
1477
|
+
invocation.writeStderr(formatEnvOnlyHint());
|
|
1478
|
+
// Persist hintShown best-effort. A write failure is isolated: the
|
|
1479
|
+
// hint simply does not repeat within this process; the next run
|
|
1480
|
+
// tries again. The injected store keeps tests hermetic.
|
|
1481
|
+
const hintStore = dependencies.hintShownStore ?? createDefaultHintShownStore();
|
|
1482
|
+
try {
|
|
1483
|
+
await hintStore.setHintShown();
|
|
1484
|
+
}
|
|
1485
|
+
catch {
|
|
1486
|
+
// Best-effort: do not turn a hint into a failure.
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
// Provider-fallback resolution (T2a — review blocker 5). Precedence:
|
|
1491
|
+
// 1. Invocation opt-out (`--no-fallback` flag or `SCOUTLINE_NO_FALLBACK`
|
|
1492
|
+
// env non-empty) — either disables the cross-Provider candidate
|
|
1493
|
+
// loop, matching the 0.11.0 kill-switch contract.
|
|
1494
|
+
// 2. `config.fallbackEnabled` — the wizard's Step-5 preference, so the
|
|
1495
|
+
// onboarding answer is no longer write-only.
|
|
1496
|
+
// 3. Default `true` (the 0.11.0 always-on contract).
|
|
1497
|
+
const envDisablesFallback = typeof env.SCOUTLINE_NO_FALLBACK === "string" && env.SCOUTLINE_NO_FALLBACK.length > 0;
|
|
1498
|
+
const fallbackEnabled = noFallback || envDisablesFallback ? false : (config.fallbackEnabled ?? true);
|
|
1499
|
+
const handlerDeps = buildHandlerDeps(resolvedEnv, secrets, fallbackEnabled);
|
|
1500
|
+
// PB-T5 — derive Plan A verification records from the loaded config
|
|
1501
|
+
// AFTER `config` is in scope. `buildHandlerDeps` runs once BEFORE
|
|
1502
|
+
// config load (the cache short-circuit), so this derivation cannot
|
|
1503
|
+
// live inside it (TDZ on `config`). Returned as a fresh spread so
|
|
1504
|
+
// the cache/init paths that already returned are unaffected; only
|
|
1505
|
+
// the credentialed handler chain sees the verification records.
|
|
1506
|
+
// Returns `undefined` when no Provider has a verification record,
|
|
1507
|
+
// leaving the field absent (Doctor's report builder omits the
|
|
1508
|
+
// `verification` field in that case — backward-compatible).
|
|
1509
|
+
const verificationRecords = loadVerificationRecords(config, dependencies);
|
|
1510
|
+
const handlerDepsWithVerification = verificationRecords === undefined ? handlerDeps : { ...handlerDeps, verificationRecords };
|
|
1511
|
+
// PB-T1 — Quota snapshot refresh lifecycle.
|
|
1512
|
+
//
|
|
1513
|
+
// The refresh is awaited and bounded (review blocker 3): every
|
|
1514
|
+
// refresh and store write is awaited before `main` returns so it
|
|
1515
|
+
// survives the bin's immediate `process.exit(status)`. There is no
|
|
1516
|
+
// fire-and-forget tail.
|
|
1517
|
+
//
|
|
1518
|
+
// Two triggers:
|
|
1519
|
+
// 1. Explicit `quota`/`doctor` refresh (force) — runs BEFORE the
|
|
1520
|
+
// handler so the dashboard/report reflects fresh data. The
|
|
1521
|
+
// cadence gate is bypassed (the user asked for fresh data); the
|
|
1522
|
+
// per-provider transport timeout + single-attempt contract
|
|
1523
|
+
// still applies.
|
|
1524
|
+
// 2. After-command due-refresh (cadence-gated) — runs AFTER every
|
|
1525
|
+
// other recognized credentialed command. A provider whose
|
|
1526
|
+
// `observedAt` is within the staleness threshold is skipped
|
|
1527
|
+
// (Tavily's 10/10min is the floor). This keeps the snapshot
|
|
1528
|
+
// live between explicit refreshes without excessive polling.
|
|
1529
|
+
//
|
|
1530
|
+
// Failure policy: best-effort. A per-provider refresh failure is
|
|
1531
|
+
// routed to a stderr notice and never rejects the outer promise. The
|
|
1532
|
+
// snapshot stays stale; the next due-refresh retries.
|
|
1533
|
+
//
|
|
1534
|
+
// Lock scope: each provider's `capability.invoke()` runs in parallel
|
|
1535
|
+
// (Promise.all). Store writes are serialized within the process by
|
|
1536
|
+
// the per-file async mutex in `createDefaultQuotaStore`. Cross-process
|
|
1537
|
+
// concurrency is last-write-wins (acceptable for an observational
|
|
1538
|
+
// heuristic; the atomic rename keeps the final state crash-safe).
|
|
1539
|
+
//
|
|
1540
|
+
// Hermeticity gate: the refresh runs ONLY in full production mode
|
|
1541
|
+
// (no injected `loadScoutlineConfig` AND no injected
|
|
1542
|
+
// `providerDescriptors`). Either injection signals a test that owns
|
|
1543
|
+
// its own descriptor/config construction; fake descriptors routinely
|
|
1544
|
+
// count `invoke()` calls and would see the refresh as a stray
|
|
1545
|
+
// invocation. Dedicated refresh lifecycle tests run via subprocess
|
|
1546
|
+
// (the real binary) or through `refreshQuotaSnapshots` directly.
|
|
1547
|
+
// This mirrors the trigger-detection gate (T3b).
|
|
1548
|
+
//
|
|
1549
|
+
// (`quotaRefreshEnabled`, `quotaStore`, and `consume` are declared
|
|
1550
|
+
// earlier alongside `buildHandlerDeps` so the sink closes over a
|
|
1551
|
+
// defined binding — see PB-T2 above.)
|
|
1552
|
+
const isQuotaObservationalCommand = command === "quota" || command === "doctor";
|
|
1553
|
+
const refreshOnError = (providerId, error) => {
|
|
1554
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1555
|
+
invocation.writeStderr(`scoutline: quota refresh failed for "${providerId}": ${message}\n`);
|
|
1042
1556
|
};
|
|
1557
|
+
if (quotaRefreshEnabled && isQuotaObservationalCommand) {
|
|
1558
|
+
await refreshQuotaSnapshots({
|
|
1559
|
+
descriptors: providerDescriptors,
|
|
1560
|
+
env: resolvedEnv,
|
|
1561
|
+
store: quotaStore,
|
|
1562
|
+
now: now ?? Date.now,
|
|
1563
|
+
force: true,
|
|
1564
|
+
onError: refreshOnError,
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
// PB-T4: read the quota snapshot once for selection, AFTER any
|
|
1568
|
+
// pre-command refresh so observational commands' fresh data is
|
|
1569
|
+
// reflected. `quotaStore.read()` is fail-open (a corrupt or absent
|
|
1570
|
+
// `state.json` yields an empty state + warning, never a throw), so
|
|
1571
|
+
// this can never block dispatch. Tests that need a specific
|
|
1572
|
+
// selection outcome inject `MainDependencies.quotaState` directly,
|
|
1573
|
+
// which short-circuits this read. The snapshot is observational; the
|
|
1574
|
+
// resolver never writes or decrements it.
|
|
1575
|
+
if (dependencies.quotaState === undefined && quotaRefreshEnabled) {
|
|
1576
|
+
quotaState = await quotaStore.read();
|
|
1577
|
+
}
|
|
1578
|
+
// Thread the resolved snapshot into the handler deps. When undefined
|
|
1579
|
+
// (test path that didn't opt in, or non-production mode without an
|
|
1580
|
+
// injected snapshot), `resolveEffectiveProvider` degrades to
|
|
1581
|
+
// first-eligible — the pre-PB-T4 behaviour. Built on top of
|
|
1582
|
+
// `handlerDepsWithVerification` so PB-T5's verification records also
|
|
1583
|
+
// reach every handler.
|
|
1584
|
+
const handlerDepsWithSelection = quotaState === undefined
|
|
1585
|
+
? handlerDepsWithVerification
|
|
1586
|
+
: { ...handlerDepsWithVerification, quotaState };
|
|
1587
|
+
let exitCode;
|
|
1588
|
+
let commandRecognized = false;
|
|
1043
1589
|
try {
|
|
1044
1590
|
switch (command) {
|
|
1045
1591
|
case "vision":
|
|
1046
|
-
|
|
1592
|
+
commandRecognized = true;
|
|
1593
|
+
exitCode = await handleVision(commandArgs, outputMode, handlerDepsWithSelection);
|
|
1594
|
+
break;
|
|
1047
1595
|
case "search":
|
|
1048
|
-
|
|
1596
|
+
commandRecognized = true;
|
|
1597
|
+
exitCode = await handleSearch(commandArgs, outputMode, handlerDepsWithSelection);
|
|
1598
|
+
break;
|
|
1049
1599
|
case "read":
|
|
1050
|
-
|
|
1600
|
+
commandRecognized = true;
|
|
1601
|
+
exitCode = await handleRead(commandArgs, outputMode, handlerDepsWithSelection);
|
|
1602
|
+
break;
|
|
1051
1603
|
case "crawl":
|
|
1052
|
-
|
|
1604
|
+
commandRecognized = true;
|
|
1605
|
+
exitCode = await handleCrawl(commandArgs, outputMode, handlerDepsWithSelection);
|
|
1606
|
+
break;
|
|
1053
1607
|
case "map":
|
|
1054
|
-
|
|
1608
|
+
commandRecognized = true;
|
|
1609
|
+
exitCode = await handleMap(commandArgs, outputMode, handlerDepsWithSelection);
|
|
1610
|
+
break;
|
|
1055
1611
|
case "research":
|
|
1056
|
-
|
|
1612
|
+
commandRecognized = true;
|
|
1613
|
+
exitCode = await handleResearch(commandArgs, outputMode, handlerDepsWithSelection);
|
|
1614
|
+
break;
|
|
1057
1615
|
case "repo":
|
|
1058
|
-
|
|
1616
|
+
commandRecognized = true;
|
|
1617
|
+
exitCode = await handleRepo(commandArgs, outputMode, handlerDepsWithSelection);
|
|
1618
|
+
break;
|
|
1059
1619
|
case "tools":
|
|
1060
|
-
|
|
1620
|
+
commandRecognized = true;
|
|
1621
|
+
exitCode = await handleTools(commandArgs, outputMode, handlerDepsWithSelection);
|
|
1622
|
+
break;
|
|
1061
1623
|
case "tool":
|
|
1062
|
-
|
|
1624
|
+
commandRecognized = true;
|
|
1625
|
+
exitCode = await handleTool(commandArgs, outputMode, handlerDepsWithSelection);
|
|
1626
|
+
break;
|
|
1063
1627
|
case "call":
|
|
1064
|
-
|
|
1628
|
+
commandRecognized = true;
|
|
1629
|
+
exitCode = await handleCall(commandArgs, outputMode, handlerDepsWithSelection);
|
|
1630
|
+
break;
|
|
1065
1631
|
case "doctor":
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1632
|
+
commandRecognized = true;
|
|
1633
|
+
exitCode = await handleDoctor(commandArgs, outputMode, handlerDepsWithSelection);
|
|
1634
|
+
break;
|
|
1069
1635
|
case "quota":
|
|
1070
|
-
|
|
1636
|
+
commandRecognized = true;
|
|
1637
|
+
exitCode = await handleQuota(commandArgs, outputMode, handlerDepsWithSelection);
|
|
1638
|
+
break;
|
|
1071
1639
|
case "code":
|
|
1072
|
-
|
|
1640
|
+
commandRecognized = true;
|
|
1641
|
+
exitCode = await handleCode(commandArgs, outputMode, handlerDepsWithSelection);
|
|
1642
|
+
break;
|
|
1073
1643
|
default:
|
|
1074
1644
|
invocation.writeStderr(formatErrorOutput(new ValidationError(`Unknown command: ${command}`, 'Run "scoutline --help" for available commands'), outputMode, secrets));
|
|
1075
|
-
|
|
1645
|
+
exitCode = 1;
|
|
1646
|
+
break;
|
|
1076
1647
|
}
|
|
1077
1648
|
}
|
|
1078
1649
|
catch (error) {
|
|
@@ -1081,7 +1652,32 @@ export async function main(args, dependencies) {
|
|
|
1081
1652
|
// formatted in the resolved output mode — they used to be hardcoded
|
|
1082
1653
|
// to "data" regardless of what the user asked for.
|
|
1083
1654
|
invocation.writeStderr(formatErrorOutput(error, outputMode, secrets));
|
|
1084
|
-
|
|
1085
|
-
|
|
1655
|
+
exitCode = getErrorExitCode(error);
|
|
1656
|
+
commandRecognized = true;
|
|
1657
|
+
}
|
|
1658
|
+
// After-command due-refresh (cadence-gated). Skip for:
|
|
1659
|
+
// - `quota`/`doctor` (already force-refreshed before the handler)
|
|
1660
|
+
// - help invocations (`<cmd> --help` exits 0 but did no real work)
|
|
1661
|
+
// - unrecognized commands (the default case above)
|
|
1662
|
+
// Run even when the handler threw (commandRecognized + caught):
|
|
1663
|
+
// the refresh is independent of the command's outcome and the
|
|
1664
|
+
// snapshot stays live regardless. The staleness check inside
|
|
1665
|
+
// `refreshQuotaSnapshots` skips providers whose `observedAt` is
|
|
1666
|
+
// within the threshold, so the common case (fresh snapshot) is a
|
|
1667
|
+
// single state-file read + no transport calls.
|
|
1668
|
+
if (quotaRefreshEnabled &&
|
|
1669
|
+
commandRecognized &&
|
|
1670
|
+
!isQuotaObservationalCommand &&
|
|
1671
|
+
!isHelpInvocation) {
|
|
1672
|
+
await refreshQuotaSnapshots({
|
|
1673
|
+
descriptors: providerDescriptors,
|
|
1674
|
+
env: resolvedEnv,
|
|
1675
|
+
store: quotaStore,
|
|
1676
|
+
now: now ?? Date.now,
|
|
1677
|
+
force: false,
|
|
1678
|
+
onError: refreshOnError,
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
return exitCode;
|
|
1086
1682
|
}
|
|
1087
1683
|
//# sourceMappingURL=index.js.map
|