/** * scoutline - CLI for Z.AI capabilities */ import type { PruneSelectors, PruneCachesResult } from "./lib/cache.js"; import { type OutputMode } from "./lib/output.js"; import { readLog } from "./lib/artifacts.js"; import { type JournalableCapability } from "./lib/journal.js"; import { type CommandInvocationAdapter } from "./command-invocation.js"; import { type ResponseCache } from "./lib/cache.js"; import { type FusionMode, type ScoutlineConfig } from "./lib/config-store.js"; import { type VerificationPromotionStore, type HintShownStore } from "./lib/config-store.js"; import { type QuotaStore, type QuotaState } from "./lib/quota-store.js"; import type { ProviderVerificationSummary } from "./capabilities/diagnostics.js"; import { type ConsumptionSink } from "./lib/consumption.js"; import type { ProviderDescriptor, ProviderId } from "./providers/types.js"; import { type InitDependencies, type InitPrompts } from "./commands/init.js"; /** * Guard for flag-forbidden features (#242): parseArgs maps `--no-X` to * BOTH `flags.X = false` AND `flags["no-X"] = true`, so a feature that * must reject a flag has to reject both spellings — checking one * silently accepts the other (the footgun the fusion lane hit when its * `--fusion` rejection had to know to test both). Throws the CALLER's * error (built lazily, so the no-reject path allocates nothing) when * either spelling of `--` / `--no-` is present. */ export declare function rejectFlagPair(flags: Record, name: string, makeError: () => Error): void; /** * Per-command accepted flag spellings for SCOUTLINE_STRICT_FLAGS mode * (#241). Keys are post-dash spellings (`no-journal`, not * `--no-journal`); sets are the UNION across a command's subcommands. * Exported for the strict-mode tests and the DISPATCHED_COMMANDS * coverage pin. */ export declare const STRICT_FLAG_ALLOWLIST: Readonly>>; /** * The strict-flag gate (#241): scan a command's argv (the tokens AFTER * the command name — global options were already extracted) and return * the first flag token the command's allowlist does not name, or * `undefined` when every token is accepted. Pure; throws never; the * caller owns the error envelope. Commands without an allowlist row * (unknown commands — the dispatcher's own `Unknown command` path) scan * nothing. */ export declare function findUnknownStrictFlag(command: string, args: readonly string[]): string | undefined; /** * One early resolution pass over the known env doors (#244), so a bad * value fails identically on EVERY command — previously * SCOUTLINE_FUSION=bogus failed `quota` (the credentialed path * resolves the door unconditionally) but silently succeeded on * early-return commands (`config get` never read it). * * - SCOUTLINE_FUSION — strict enum via {@link resolveFusionMode} * (config passed as undefined: this pass validates the ENV door * only; the file value arrives at the later resolution already * leniently parsed). Empty string is unset. * - SCOUTLINE_PROVIDER — a single shared Provider id via * {@link parseProviderId}, the exact validation the shared-capability * paths already run — except on `science`, whose env-door grammar * is the science supplier ids + "all" and is validated inside * handleScience against the D5 arm order. The `--provider` FLAG is * out of scope: it is extracted globally and still surfaces its * per-command errors where it is consumed. * - SCOUTLINE_NO_FALLBACK — boolean kill-switch: any non-empty value * disables fallback, so there is nothing to validate (listed here * because the door set is the contract, not just the checks). * * Pure; throws ValidationError on a bad door. `main` runs this * pre-dispatch for every command (help/version bare short-circuits have * already returned) and owns the error envelope. */ export declare function validateEnvDoors(env: NodeJS.ProcessEnv, command: string, explicitProvider?: string): void; /** Artifact serialization formats (spec ruling: json | markdown, default json). */ declare const SAVE_FORMATS: readonly ["json", "markdown"]; type SaveFormat = (typeof SAVE_FORMATS)[number]; /** * A parsed `--save` request. `exportPath === undefined` is the * valueless-trailing-`--save` form: a master-only save with no export copy * (DESIGN D1). The flags only configure a save when `--save` itself is * present; `--save-format` / `--save-force` alone are validated and dropped. */ export interface SaveRequest { exportPath?: string; format: SaveFormat; force: boolean; } /** * ADR-0007 D5 — every command the dispatcher routes (below): the * exhaustive 22-command surface. Exported so the rejection-matrix test * derives its enumeration from the dispatcher's own partition instead * of a hand-maintained list that drifts when a command is added — a * future command without a ladder or a rejection row fails the * enumeration pin by omission. (tests/help-surface.test.js pins the * reverse direction: every entry carries a MAIN_HELP Commands row.) */ export declare const DISPATCHED_COMMANDS: ReadonlySet; /** * ADR-0007 D5 — commands WITHOUT an Output Budget ladder. They reject * `--max-chars` at parse time with UNSUPPORTED_OPTION: nothing outside * the ladder surfaces (search/read/crawl/research + repo * search/read/brief; repo tree rejects on its own) may accept-and-drop * the flag. Exported for the structural enumeration pin. */ export declare const REJECT_MAX_CHARS_COMMANDS: ReadonlySet; /** * History-journal merge T2a — the `--no-journal` per-call escape ships * on exactly the journalable capabilities (search/read/research; ADR * -0008's `--no-fallback` idiom). Every OTHER command rejects it at * parse time with UNSUPPORTED_OPTION — the `--max-chars` * command-local pattern, NOT `--save`'s global accept-and-drop: a * privacy off-switch accepted-and-dropped somewhere would be a silent * no-op. `read`/`research` ACCEPT the flag in T2a; their journaling * itself arrives in T3 — the SURFACE ships with the switch. */ export declare const ACCEPT_NO_JOURNAL_COMMANDS: ReadonlySet; export declare const SWITCH_CASES: Set; export declare const IF_ARMS: Set; /** * Dependencies threaded from `main` into every command handler. * * `now` flows onward to `invokeCommand` (DESIGN §2) as its optional fourth * argument so success envelopes carry deterministic timestamps under test. * `env` is the injectable environment seam: it is plumbed to the handler * boundary here so Phase 2 can route it into `CommandContext` / * `ProviderContext` without reshaping the dispatch layer again. Commands * still read `process.env` directly today; that migration is Phase 2 and * intentionally out of scope for this plumbing fix. * * `provider` is the parsed global `--provider` flag. Shared Search, the * P6-07 Repository commands, and the Reader Migration 04 `read` command * resolve/validate it; `cache prune` reads it as a PRUNE SELECTOR * (`--provider` is stripped from the rest stream before `handleCache` * sees it, so the dispatcher falls back to this field); the remaining * Z.AI-only command families (tools, tool, call, code) carry it but * never consult it. `providerDescriptors` * is the injectable registry (tests pass doubles; production uses the * static built-in list). * * `fallbackEnabled` is the resolved provider-fallback kill-switch * (Provider Fallback Tech Plan §"Kill-switch plumbing"). It is the * boolean AND of "flag absent" and "`SCOUTLINE_NO_FALLBACK` absent" — * either disables the cross-Provider candidate loop. The dispatch * layer plumbs the value through but no handler in this ticket * consumes it; the per-handler wiring lands in later tickets. * * Search execution dependencies (`searchCache`, `searchSleep`, * `searchRandom`) default to the on-disk cache and real sleep/random in * production; tests inject in-memory doubles. * * `repositoryCache`, `repositorySleep`, and `repositoryRandom` are the * analogous seams for the P6-07 Repository commands. They default to * the same production values as Search (single on-disk cache, real * sleep, Math.random) but stay as separate optional MainDependencies * so repository tests can inject isolated in-memory doubles without * touching Search state. They are not a rename of the Search seams. * * `readerCache`, `readerSleep`, and `readerRandom` are the analogous * seams for the Reader Migration 04 `read` command. Same defaults as * Search and Repository; separate optional MainDependencies so reader * tests can inject isolated in-memory doubles. Not a rename of either * prior seam. * * `pruneCaches` is the cache-prune dispatcher seam (Cache Prune * Ticket 5). Production wires the on-disk `pruneCaches` from * `src/lib/cache.js`; tests inject a double so the dispatcher can be * exercised in-process without real I/O. Defaults to the production * function when omitted. */ export interface HandlerDependencies { readonly invocation: CommandInvocationAdapter; readonly env: NodeJS.ProcessEnv; readonly secrets: string[]; readonly now?: () => number; readonly provider?: string; readonly providerDescriptors: readonly ProviderDescriptor[]; readonly fallbackEnabled: boolean; /** * Validated per-capability routing preference (routing-table plan). * Absent → handlers pass `routing: undefined` and selection is * byte-identical to pre-routing behavior. */ readonly routing?: Readonly>; /** * Whether multi-provider search fan-out is enabled (search-fanout * plan, Ticket 3). Production derives this from the loaded config * (`config.fanout === true`; the typed registry row + `config set` * surface arrive in Ticket 4); tests inject it directly so * activation-tier assertions stay hermetic. Absent/false → the * search handler's fan-out path never engages (byte-identical * pre-fan-out behavior). */ readonly configFanout?: boolean; /** * Effective merged-search ranking algorithm (fusion seed-24). * Production derives this once per invocation via `resolveFusionMode` * (SCOUTLINE_FUSION env > config `fusion` > default "rrf"); tests * inject it directly so precedence/consumption assertions stay * hermetic. Absent → the "rrf" default applies (byte-identical * pre-fusion ranking). */ readonly fusionMode?: FusionMode; /** * save-artifacts T4: present only when this run will actually save * (save-capable command + --save + not a help invocation). Handlers * turn it into an invokeCommand save hook via createSaveArtifactHook; * every other run is byte-identical to pre-T4. */ readonly save?: SaveHookInput; /** * History-journal merge T2a: present when this run will journal * (journalable command, not a help run, journaling not switched off * via config `"journal": false` or `--no-journal`). The journalable * handlers turn it into an invokeCommand journal hook beside the * save hook via createJournalHook; read/research (T3) consume the * SAME field — the seam is capability-driven, not command-hardcoded. * Shares the ServingCapture cell with {@link save} when both wired, * which is how the saveRef cross-link works. */ readonly journal?: JournalHookInput; /** * History-journal merge T2a must-fix 1: batch-driven ops journal per * their OWN capability (PRD AC3 — no exclusion branch). The top-level * wiring above sets `journal` directly for single commands; for the * batch noun (not in ACCEPT_NO_JOURNAL_COMMANDS) main sets this * switch instead and the batch runner builds a per-op journal input * keyed on the op's command (config switch only — no per-op flag in * v1; `--no-journal` on the batch COMMAND itself stays rejected). */ readonly journalBatchEnabled?: boolean; readonly searchCache: ResponseCache; readonly searchSleep: (ms: number) => Promise; readonly searchRandom: () => number; readonly repositoryCache: ResponseCache; readonly repositorySleep: (ms: number) => Promise; readonly repositoryRandom: () => number; readonly readerCache: ResponseCache; readonly readerSleep: (ms: number) => Promise; readonly readerRandom: () => number; readonly crawlCache: ResponseCache; readonly crawlSleep: (ms: number) => Promise; readonly crawlRandom: () => number; readonly mapCache: ResponseCache; readonly mapSleep: (ms: number) => Promise; readonly mapRandom: () => number; readonly researchCache: ResponseCache; readonly researchSleep: (ms: number) => Promise; readonly researchRandom: () => number; readonly scienceCache: ResponseCache; readonly scienceSleep: (ms: number) => Promise; readonly scienceRandom: () => number; /** * Optional SIGINT registrar for the research command (Review Fix 3). * Production wires `process.on('SIGINT', ...)`. When provided, the * research handler uses this to register / tear down the listener on * every per-attempt entry / exit; when absent, the production * registrar is used. */ readonly researchRegisterInterrupt?: (stateFilePath: string, resumeCommand: string) => (print: () => void) => () => void; /** * Optional verification promoter for Doctor (T3b). Production wires * the configured `verificationPromoter` from `MainDependencies`; * tests inject a double. When absent, Doctor runs without * promotion. */ readonly verificationPromoter?: VerificationPromotionStore; /** * Optional consumption sink (PB-T2 — Plan B). Production wires the * configured `consume` from `MainDependencies` (a quota-store-backed * sink); tests inject an in-memory double so event-sequence * assertions stay hermetic. When absent, no consumption events are * emitted and shared execution is byte-for-byte identical to * pre-PB-T2. */ readonly consume?: ConsumptionSink; /** * Optional quota snapshot (PB-T4 — Plan B). Production is read once * by `main` via `quotaStore.read()` after the PB-T1 pre-command * refresh and threaded through every handler; tests inject a crafted * snapshot so selection assertions stay hermetic — no real * `state.json` I/O. The seven shared handlers pass this to * `resolveEffectiveProvider` for quota-aware first-pick selection. * When absent, the resolver degrades to the first eligible provider * in registry order (the pre-PB-T4 behaviour). Doctor, quota, * cache, init, and raw Z.AI commands never read it. */ readonly quotaState?: QuotaState; /** * Optional quota store for live-probe write-through (PB-T5 — Plan B). * Production wires the singleton constructed in `main`; tests inject * an in-memory double so write-through assertions stay hermetic. * Doctor never consults it (Doctor reads the snapshot, never * live-probes quota). The `quota` command consults it only when the * snapshot path is enabled AND a configured Provider's snapshot is * stale/missing (a successful live-probe fallback is persisted * before the dashboard returns). When absent, the `quota` command * emits a `quotaSource` label but does not persist the live refresh. */ readonly quotaStore?: QuotaStore; /** * Optional verification records for Doctor's per-Provider * `verification` summary (PB-T5 — Plan B). Production maps * `config.providers[id].verification` (Plan A) to * `ProviderVerificationSummary` (capability contract); tests inject * a crafted record so Doctor assertions stay hermetic. When absent, * the `verification` field is omitted (pre-PB-T5 callers). */ readonly verificationRecords?: Partial>; /** * Optional injectable `pruneCaches` for the `cache prune` subcommand * (Cache Prune Ticket 5). Production wires the on-disk * `pruneCaches` from `src/lib/cache.js` (see `MainDependencies`); * tests inject a double so the dispatcher's selector-parsing / * error-propagation contract can be exercised without touching disk. * When absent, the dispatcher uses the production function. */ readonly pruneCaches?: (selectors: PruneSelectors) => Promise; /** * Review r3 (P1): injectable artifacts-log read for the history * handlers. Production wires the real `readLog`; tests inject a * counting spy. handleHistoryExport memoizes ONE read per invocation * through this seam — the save-entry index and the renderer consume * the same result, so export never rescans the log per row. */ readonly readArtifactsLog?: typeof readLog; } /** * Parse and validate the `--count` flag value (Fixup C — B11, Fixup D). Per * DESIGN.md §7, count must be a safe integer >= 0. Invalid values (NaN, * negative, non-integer, Infinity, values above Number.MAX_SAFE_INTEGER) * throw `ValidationError` BEFORE any Provider resolution or invocation. * * Fixup D hardens two gaps: * - `--count` without a value parses to `true`; that is a user error, * not an absent flag, and now throws VALIDATION_ERROR instead of being * silently treated as absent. * - Uses `Number.isSafeInteger` instead of `Number.isFinite` + * `Number.isInteger` so values above 2^53-1 are rejected rather than * silently rounded. * * Exported for testing so the validation can be exercised without going * through the CLI parser (which does not deliver negative numbers as flag * values today). */ export declare function parseAndValidateCount(raw: unknown): number | undefined; /** * Validate the `--topic` flag value BEFORE Provider resolution, mirroring * the `--count` parse-level gate (Fixup D — B11). An invalid value * surfaces VALIDATION_ERROR regardless of which Provider would have been * selected, because parse-level validation fires before the support / * configuration gates. Exported for testing. */ export declare function parseAndValidateTopic(raw: unknown): "general" | "news" | "finance" | undefined; /** * Validate the `--type` flag value BEFORE Provider resolution, mirroring * `parseAndValidateTopic`. `type` is a content axis (not an editorial * topic axis) and is mutually exclusive with `--topic` (checked in * `handleSearch`). Exported for testing. */ export declare function parseAndValidateType(raw: unknown): "video" | undefined; /** * `scoutline cache ` — local cache utility. Like * Doctor, it bypasses Provider resolution entirely (no descriptor * lookup, no Adapter, no transport). The command surfaces the inventory * and clear helpers owned by `src/lib/cache.ts` (Ticket 01) through the * presentation-only handlers in `src/commands/cache.ts`. The `prune` * case was added in Cache Prune Ticket 5 and parses `--older-than` / * `--provider` / `--capability` flags into the `PruneSelectors` shape * the lib expects (DESIGN D2/D3); unknown provider/capability values * are intentionally NOT pre-validated against the registry — they * filename-match nothing in the response cache while the selector-free * tool scan still runs (DESIGN D2/D4). `--provider` may appear before * or after the command token: `extractGlobalOptions` strips it either * way and this handler recovers it from `deps.provider`. A valueless * `--older-than`/`--provider`/`--capability` is a VALIDATION_ERROR. A * lock-acquire timeout in the production `pruneCaches` THROWS (DESIGN * D5) so the dispatcher's error boundary emits a sanitized stderr * envelope with exit 1. */ export declare function handleCache(args: string[], outputMode: OutputMode, deps: HandlerDependencies): Promise; /** * `scoutline usage [--days N] [--provider ]` — report the local * usage ledger (usage-ledger plan, Ticket 5). Credential-free and * read-only: like `cache` it bypasses Provider resolution entirely, and * like `handleCache` it keeps the injection-free posture — the ledger * path resolves at handler time via * `resolveConfigRootPure(deps.env, ...)` (no new `MainDependencies` * field, no injected reader object). Production reads * `readUsageLedger(resolveUsageLedgerPath(...))` with DEFAULT deps * (real reader, no `onWarning`) so DESIGN D8's silent-on-corrupt * contract holds: a missing, corrupt, or wrong-version ledger yields an * empty window with exit 0 and no stderr noise. * * Flag contract (DESIGN D8): `--days` must be an integer ≥ 1 (unlike * `--count`, 0 is invalid) and defaults to 7; `--provider` must be a * known registry id (unknown ids are a VALIDATION_ERROR listing the * accepted ids; a known-but-unrecorded id is an empty result, exit 0). * `--provider` may appear before or after the command token — * `extractGlobalOptions` strips it either way, so the handler recovers * it from `deps.provider` (same recovery as `cache prune`). */ export declare function handleUsage(args: string[], outputMode: OutputMode, deps: HandlerDependencies): Promise; /** * `history` dispatcher (save-artifacts T5): flag/subcommand validation * up front, then the pure `historyCommand` through the invocation seam * with the artifacts store as the only I/O. The store path resolves * against `SCOUTLINE_ARTIFACTS_DIR` / the config root; reads are * fail-open (`readLog` never throws) so a missing or corrupt store is * an empty inventory, exit 0. FILE_ERROR paths (unknown id, missing * master) ride the seam's existing error boundary. */ export declare function handleHistory(args: string[], outputMode: OutputMode, deps: HandlerDependencies, historyLock?: { timeoutMs?: number; setTimeout?: typeof setTimeout; }): Promise; export { handleFetch, fetchCommand, executeFetch, FETCH_HELP } from "./commands/fetch.js"; export { handleArchive, archiveCdxCommand, archiveGetCommand, ARCHIVE_HELP, } from "./commands/archive.js"; /** The report file's own schema version (DESIGN D4 namespace, log-agnostic). */ /** Observation cell: the provider whose invoke() actually resolved. */ export interface ServingCapture { servedProvider?: ProviderId; /** * Issue #108: where the serving bytes came from. `"live"` = the * recorded invoke() resolved (set by the invoke wrapper); `"cache"` = * the serving attempt returned without ever invoking (set by the * cacheIdentity wrapper when the subsequent cache consult hits). * Unset = no save-capable attempt observed (non-capable commands, * pre-run failures) — the save hook then records `"live"`, matching * pre-#108 entries' implicit assumption. */ servedFrom?: "live" | "cache"; /** * History-journal merge T2a: the response-cache key of the serving * attempt's request (stamped beside servedFrom by the cacheIdentity * wrapper — it is the per-attempt hook that knows the identity). The * journal entry records cacheKey per PRD AC2; unset = no capable * attempt observed. */ cacheKey?: string; /** * T2a saveRef cross-link cell: the save hook (which runs FIRST in * invokeCommand) stamps its requestId here; the journal hook reads * it — same-run `--save` + journaling links both entries (PRD AC10). */ savedRequestId?: string; /** * Fan-out arm-race fix: one serving cell per PLANNED arm, keyed by * provider id. handleSearch attaches the cells (from the resolved * fan-out plan) BEFORE the arms run; the descriptor wrappers look the * cell up at create() time and stamp it alongside the shared cell. A * delayed cache-hit arm can therefore never overwrite a live arm's * servedFrom: each arm owns its cell. Absent on the single-provider * path (and on batch ops, which already use per-op capture cells). * * Review round (cubic, wave-2): a `--merge` arm runs MULTIPLE * sub-queries through the SAME cell (the grid is arm × sub-query), * so the cell is a per-ARM aggregate, not a per-attempt record — * `servedFrom === "cache"` may only stand when every sub-query on * the arm was cache-served. `failed` is the sticky latch: once any * sub-query's invoke threw, later speculative "cache" re-stamps are * suppressed so the journal hook never reads a partially-failed arm * as all-cache (a repeat marker for an INCOMPLETE combined result). */ armServing?: ReadonlyMap; } /** What main hands the save-capable handlers when a save will happen. */ interface SaveHookInput { readonly request: SaveRequest; readonly capture: ServingCapture; } /** * History-journal merge T2a — what main hands the journalable handlers * when this run will journal (journalable command + not a help run + * journaling not switched off). Mirrors SaveHookInput's shape: the * SAME capture cell the save path uses, so provider honesty and the * cache resolution inherit the #108 fix for free. */ interface JournalHookInput { readonly capability: JournalableCapability; readonly capture: ServingCapture; } /** * T2a must-fix 1: the batch runner's per-op wrapper — same behavior as * {@link captureServingDescriptors}, exported because each batch op owns * its own ServingCapture cell (concurrent ops must not cross-stamp). */ export declare function captureServingDescriptorsForOp(descriptors: readonly ProviderDescriptor[], capture: ServingCapture): readonly ProviderDescriptor[]; import { createSaveArtifactHook } from "./lib/save-artifacts.js"; export { createSaveArtifactHook }; export interface MainDependencies { readonly invocation: CommandInvocationAdapter; readonly env: NodeJS.ProcessEnv; readonly now?: () => number; /** * Optional injectable agent-registration stamp check (agent * registration D5/D6). Production wires `checkAgentRegistration` from * `src/lib/agent-registration/deploy.js` against `os.homedir()`, the * ambient config root (resolveConfigRoot reads process.env directly), * and the package version; tests inject doubles so dispatch runs stay * hermetic. Invoked exactly once per CLI run, before command dispatch; * a rejection is caught and degraded to a stderr notice so a broken * refresh never breaks the invoked command. */ readonly agentRegistrationCheck?: () => Promise<{ refreshed: boolean; }>; /** * Injectable Provider registry. Production defaults to the static * built-in descriptors; tests pass doubles to route Search through a * fake Adapter without touching real transports. */ readonly providerDescriptors?: readonly ProviderDescriptor[]; /** * Injectable config-file reader (T2a — Plan A). Production defaults to * `readConfig` from `lib/config-store.js`, which reads the versioned * `~/.scoutline/config.json`. Tests inject an in-memory double so * `main()` stays hermetic — no real config-root I/O — and can drive * file-only credential flows without touching disk. * * The reader returns the parsed {@link ScoutlineConfig}; `main` uses it * to build `resolvedEnv` (file keys layered under the injected env) and * to resolve `fallbackEnabled`. A returned config with no `providers` * and no `fallbackEnabled` is a no-op: the env path is byte-for-byte * unchanged from the pre-T2a behavior. */ readonly loadScoutlineConfig?: () => Promise; /** * Injectable config short-circuit (#73). When provided, main() uses * this config verbatim and never reads the operator's config file — * the deps-level twin of loadScoutlineConfig for tests that want to * pin exact config values without building a loader. An explicit * loadScoutlineConfig still wins over this. */ readonly config?: ScoutlineConfig; /** * Injectable fan-out activation override (search-fanout plan, * Ticket 3). When provided, this wins over the file-configured * `fanout` value so activation-tier tests stay hermetic (no real * config.json needed). Production leaves it undefined and derives * the switch from the loaded config. */ readonly configFanout?: boolean; /** * Injectable fusion-mode override (fusion seed-24). When provided, * this wins over the resolveFusionMode derivation (SCOUTLINE_FUSION * env > config `fusion` > default "rrf") so precedence/consumption * tests stay hermetic. Production leaves it undefined. */ readonly fusionMode?: FusionMode; /** * Injectable routing preference override (#72). When provided, this * wins over the config file's routing table — the injectable-wins * twin of configFanout, closing the last ambient-config leak into * main()-driven tests. */ readonly routing?: HandlerDependencies["routing"]; /** * Injectable shared-Search execution dependencies. Production defaults * to the on-disk cache and real sleep/random; tests inject in-memory * doubles for deterministic, offline behaviour. */ readonly searchCache?: ResponseCache; readonly searchSleep?: (ms: number) => Promise; readonly searchRandom?: () => number; /** * Injectable shared-Repository execution dependencies (P6-07). * Production defaults to the same on-disk cache and real sleep/random * as Search; tests inject in-memory doubles so Repository dispatch * tests stay isolated from Search state. These are NOT a rename of * the Search seams. */ readonly repositoryCache?: ResponseCache; readonly repositorySleep?: (ms: number) => Promise; readonly repositoryRandom?: () => number; /** * Injectable shared-Reader execution dependencies (Reader Migration * Ticket 04). Production defaults to the same on-disk cache and real * sleep/random as Search/Repository; tests inject in-memory doubles * so Reader dispatch tests stay isolated from Search/Repository * state. NOT a rename of either prior seam. */ readonly readerCache?: ResponseCache; readonly readerSleep?: (ms: number) => Promise; readonly readerRandom?: () => number; /** * Injectable shared-Crawl execution dependencies (Tavily integration * Ticket 05). Production defaults to the same on-disk cache and real * sleep/random as Search/Repository/Reader; tests inject in-memory * doubles so Crawl dispatch tests stay isolated. NOT a rename of any * prior seam. */ readonly crawlCache?: ResponseCache; readonly crawlSleep?: (ms: number) => Promise; readonly crawlRandom?: () => number; /** * Injectable shared-Map execution dependencies (Tavily integration * Ticket 06). Production defaults to the same on-disk cache and real * sleep/random as Search/Repository/Reader/Crawl; tests inject * in-memory doubles so Map dispatch tests stay isolated. NOT a rename * of any prior seam. */ readonly mapCache?: ResponseCache; readonly mapSleep?: (ms: number) => Promise; readonly mapRandom?: () => number; /** * Injectable shared-Research execution dependencies (Tavily integration * Ticket 07). Production defaults to the same on-disk cache and real * sleep/random as Search/Repository/Reader/Crawl/Map; tests inject * in-memory doubles so Research dispatch tests stay isolated. NOT a * rename of any prior seam. */ readonly researchCache?: ResponseCache; readonly researchSleep?: (ms: number) => Promise; readonly researchRandom?: () => number; /** * Injectable shared-Science execution dependencies (issue #140). * Production defaults to the same on-disk cache and real sleep/random * as every prior triple; tests inject in-memory doubles so science * dispatch tests stay isolated. NOT a rename of any prior seam. */ readonly scienceCache?: ResponseCache; readonly scienceSleep?: (ms: number) => Promise; readonly scienceRandom?: () => number; /** * Optional injectable SIGINT registrar factory for the research * command. Production wraps `process.on('SIGINT', ...)` inside the * command module; tests inject a recorder so they can capture the * registered callback, trigger it manually, and assert the printed * resume command / state-file path / loser listener cleanup. The * factory receives the per-attempt state-file path + canonical * resume command (binding is computed inside the research handler * from the per-attempt Provider capability). Review Fix 3. */ readonly researchRegisterInterrupt?: (stateFilePath: string, resumeCommand: string) => (print: () => void) => () => void; /** * Optional injectable prompt IO seam for the `init` wizard (T3a — * Plan A). Production wires `createInquirerPrompts` (which lazily * resolves `@inquirer/prompts`); tests inject a scripted double so * the wizard runs fully hermetically without a real TTY. */ readonly initPrompts?: InitPrompts; /** * Optional injectable config-store seam for the `init` wizard (T3a). * Production wires `createDefaultConfigStore()` (real `inspectConfig` * + `writeConfig` against `~/.scoutline/config.json`); tests inject * a temp-dir-backed double so onboarding assertions never touch the * user's real config root. */ readonly initConfigStore?: InitDependencies["configStore"]; /** * Optional injectable agent-registration home/config roots (agent * registration D4/D6). Production defaults to `os.homedir()` + * `resolveConfigRoot()`; the init wizard's agent step and the * `init --unregister` disk-scan consume it. Tests inject temp roots so * neither surface ever probes the real HOME. */ readonly agentRegistrationRoots?: { home: string; configRoot: string; }; /** * Optional injectable verification-promotion store (T3b). Production * wires `createDefaultVerificationPromoter()` (real read-modify-write * against `~/.scoutline/config.json`); tests inject an in-memory * double so Doctor's promotion assertions stay hermetic. When * omitted, the production promoter is constructed at dispatch time. * * Doctor calls `promote(providerId, checkedAt)` after a successful * probe to flip the matching record from `unverified` to `verified`. * Best-effort: a write failure is isolated and never turns a * successful probe into a Doctor failure. */ readonly verificationPromoter?: VerificationPromotionStore; /** * Optional injectable hint-shown store (T3b). Production wires * `createDefaultHintShownStore()`; tests inject an in-memory double * so the trigger-detection hint persistence assertions are hermetic. * When omitted, the production store is constructed at dispatch time. * * Trigger detection calls `setHintShown()` once after emitting the * env-only hint so the hint never repeats. */ readonly hintShownStore?: HintShownStore; /** * Optional injectable quota snapshot store (PB-T1 — Plan B). Production * defaults to `createDefaultQuotaStore()` (real atomic read-merge-write * against `~/.scoutline/state.json`); tests inject an in-memory double * so refresh assertions stay hermetic. When omitted, the production * store is constructed at dispatch time. * * `main` refreshes the store BEFORE the `quota`/`doctor` handlers * (force) and AFTER every other credentialed command (cadence-gated * by the per-provider staleness threshold). All refreshes and store * writes are awaited before `main` returns so they survive the bin's * immediate `process.exit`. */ readonly quotaStore?: QuotaStore; /** * Optional injectable verification records for Doctor's per-Provider * `verification` summary (PB-T5 — Plan B). Production maps * `config.providers[id].verification` (Plan A) to * `ProviderVerificationSummary` (capability contract — structural * twin kept separate so the capability contract stays free of * `lib/config-store.ts` imports); tests inject a crafted record so * Doctor assertions stay hermetic — no real `~/.scoutline/config.json` * read. When omitted, the production path derives the records from * the loaded `config`. */ readonly verificationRecords?: Partial>; /** * Optional injectable consumption sink (PB-T2 — Plan B). Production * defaults to `createCompositeConsumptionSink(quotaStoreSink, * usageLedgerSink)` — the PB-T1 snapshot store (advancing * `locallyUpdatedAt` and * adjusting the matching category's count set); tests inject an * in-memory double so event-sequence assertions stay hermetic. * * The sink records ONE event per billable `invoke()` attempt at the * execution seam (`lib/execution.ts`), so cache hits emit nothing, * retries emit one event per attempt, and observational handlers * (`quota`/`doctor`) emit nothing. Variable/unknown-cost capabilities * (Research, Vision, Crawl) persist an explicit `unknown` amount * rather than a fake-precise number. * * Hermeticity gate: the PRODUCTION sink is constructed only in full * production mode (no injected `loadScoutlineConfig` AND no injected * `providerDescriptors`). Either injection signals a test that owns * its own descriptor/config construction; the production sink would * otherwise reach the user's real `~/.scoutline/state.json` during * such a test. Dedicated consumption tests inject this field directly * with `createInMemoryConsumptionSink()`. */ readonly consume?: ConsumptionSink; /** * Optional injectable quota snapshot for selection (PB-T4 — Plan B). * Production reads it once via `quotaStore.read()` after the PB-T1 * pre-command refresh (the seven shared handlers consume it through * `resolveEffectiveProvider`); tests inject a crafted snapshot so * selection assertions are hermetic — no real `state.json` read. * * Hermeticity gate mirrors `quotaStore`/`consume`: the PRODUCTION * read happens only in full production mode (no injected * `loadScoutlineConfig` AND no injected `providerDescriptors`). * Either injection signals a test that owns its own * descriptor/config construction; such a test injects `quotaState` * directly when it needs to assert a specific selection outcome, or * leaves it absent so the resolver degrades to first-eligible (the * pre-PB-T4 behaviour). */ readonly quotaState?: QuotaState; /** * Optional injectable `pruneCaches` for the `cache prune` subcommand * (Cache Prune Ticket 5). Production defaults to the on-disk * `pruneCaches` from `src/lib/cache.js`; tests inject a double so the * dispatcher's selector-parsing / error-propagation contract can be * exercised without touching disk. When omitted, the dispatcher * falls back to the production function so the seam stays opt-in. */ readonly pruneCaches?: (selectors: PruneSelectors) => Promise; /** * Review r3 (P1): injectable artifacts-log read for the history * handlers. Production wires the real `readLog`; tests inject a * counting spy. handleHistoryExport memoizes ONE read per invocation * through this seam — the save-entry index and the renderer consume * the same result, so export never rescans the log per row. */ readonly readArtifactsLog?: typeof readLog; } export declare function main(args: readonly string[], dependencies: MainDependencies): Promise; //# sourceMappingURL=index.d.ts.map