scoutline 0.22.0 → 0.24.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 +108 -26
- package/dist/capabilities/investigation.d.ts +134 -0
- package/dist/capabilities/investigation.d.ts.map +1 -0
- package/dist/capabilities/investigation.js +278 -0
- package/dist/capabilities/investigation.js.map +1 -0
- package/dist/capabilities/search.d.ts +18 -4
- package/dist/capabilities/search.d.ts.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +14 -5
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/crawl.d.ts.map +1 -1
- package/dist/commands/crawl.js +2 -1
- package/dist/commands/crawl.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts +42 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +42 -5
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/investigate.d.ts +232 -0
- package/dist/commands/investigate.d.ts.map +1 -0
- package/dist/commands/investigate.js +838 -0
- package/dist/commands/investigate.js.map +1 -0
- package/dist/commands/map.d.ts.map +1 -1
- package/dist/commands/map.js +2 -1
- package/dist/commands/map.js.map +1 -1
- package/dist/commands/quota.d.ts.map +1 -1
- package/dist/commands/quota.js +2 -2
- package/dist/commands/quota.js.map +1 -1
- package/dist/commands/read.d.ts.map +1 -1
- package/dist/commands/read.js +2 -1
- package/dist/commands/read.js.map +1 -1
- package/dist/commands/repo.js +1 -1
- package/dist/commands/research.d.ts.map +1 -1
- package/dist/commands/research.js +2 -1
- package/dist/commands/research.js.map +1 -1
- package/dist/commands/search.d.ts +54 -6
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +273 -25
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/vision.d.ts.map +1 -1
- package/dist/commands/vision.js +15 -2
- package/dist/commands/vision.js.map +1 -1
- package/dist/index.d.ts +93 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +737 -14
- package/dist/index.js.map +1 -1
- package/dist/lib/code-mode.d.ts +12 -0
- package/dist/lib/code-mode.d.ts.map +1 -1
- package/dist/lib/code-mode.js +18 -10
- package/dist/lib/code-mode.js.map +1 -1
- package/dist/lib/config-store.d.ts +45 -1
- package/dist/lib/config-store.d.ts.map +1 -1
- package/dist/lib/config-store.js +70 -1
- package/dist/lib/config-store.js.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +5 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/errors.d.ts +7 -1
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +8 -2
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/execution.d.ts +14 -5
- package/dist/lib/execution.d.ts.map +1 -1
- package/dist/lib/execution.js +25 -6
- package/dist/lib/execution.js.map +1 -1
- package/dist/lib/investigate-claims.d.ts +90 -0
- package/dist/lib/investigate-claims.d.ts.map +1 -0
- package/dist/lib/investigate-claims.js +188 -0
- package/dist/lib/investigate-claims.js.map +1 -0
- package/dist/lib/investigate-extract.d.ts +35 -0
- package/dist/lib/investigate-extract.d.ts.map +1 -0
- package/dist/lib/investigate-extract.js +101 -0
- package/dist/lib/investigate-extract.js.map +1 -0
- package/dist/lib/investigate-planner.d.ts +58 -0
- package/dist/lib/investigate-planner.d.ts.map +1 -0
- package/dist/lib/investigate-planner.js +141 -0
- package/dist/lib/investigate-planner.js.map +1 -0
- package/dist/lib/mcp-client.d.ts +7 -0
- package/dist/lib/mcp-client.d.ts.map +1 -1
- package/dist/lib/mcp-client.js +11 -1
- package/dist/lib/mcp-client.js.map +1 -1
- package/dist/lib/parse-zoned-instant.d.ts +13 -0
- package/dist/lib/parse-zoned-instant.d.ts.map +1 -0
- package/dist/lib/parse-zoned-instant.js +17 -0
- package/dist/lib/parse-zoned-instant.js.map +1 -0
- package/dist/lib/quota-mapping.d.ts +4 -2
- package/dist/lib/quota-mapping.d.ts.map +1 -1
- package/dist/lib/quota-mapping.js +25 -2
- package/dist/lib/quota-mapping.js.map +1 -1
- package/dist/lib/redact.d.ts +3 -2
- package/dist/lib/redact.d.ts.map +1 -1
- package/dist/lib/redact.js +90 -24
- package/dist/lib/redact.js.map +1 -1
- package/dist/lib/timeout.d.ts +28 -0
- package/dist/lib/timeout.d.ts.map +1 -0
- package/dist/lib/timeout.js +30 -0
- package/dist/lib/timeout.js.map +1 -0
- package/dist/lib/url.d.ts +10 -4
- package/dist/lib/url.d.ts.map +1 -1
- package/dist/lib/url.js +21 -6
- package/dist/lib/url.js.map +1 -1
- package/dist/providers/arxiv/client.d.ts +4 -1
- package/dist/providers/arxiv/client.d.ts.map +1 -1
- package/dist/providers/arxiv/client.js +11 -3
- package/dist/providers/arxiv/client.js.map +1 -1
- package/dist/providers/bocha/adapter.d.ts +43 -0
- package/dist/providers/bocha/adapter.d.ts.map +1 -0
- package/dist/providers/bocha/adapter.js +197 -0
- package/dist/providers/bocha/adapter.js.map +1 -0
- package/dist/providers/bocha/client.d.ts +51 -0
- package/dist/providers/bocha/client.d.ts.map +1 -0
- package/dist/providers/bocha/client.js +104 -0
- package/dist/providers/bocha/client.js.map +1 -0
- package/dist/providers/bocha/credentials.d.ts +12 -0
- package/dist/providers/bocha/credentials.d.ts.map +1 -0
- package/dist/providers/bocha/credentials.js +29 -0
- package/dist/providers/bocha/credentials.js.map +1 -0
- package/dist/providers/bocha/diagnostics.d.ts +17 -0
- package/dist/providers/bocha/diagnostics.d.ts.map +1 -0
- package/dist/providers/bocha/diagnostics.js +33 -0
- package/dist/providers/bocha/diagnostics.js.map +1 -0
- package/dist/providers/brave/adapter.d.ts.map +1 -1
- package/dist/providers/brave/adapter.js +4 -3
- package/dist/providers/brave/adapter.js.map +1 -1
- package/dist/providers/brave/client.d.ts +1 -0
- package/dist/providers/brave/client.d.ts.map +1 -1
- package/dist/providers/brave/client.js +4 -6
- package/dist/providers/brave/client.js.map +1 -1
- package/dist/providers/brave/credentials.d.ts +2 -0
- package/dist/providers/brave/credentials.d.ts.map +1 -1
- package/dist/providers/brave/credentials.js +2 -0
- package/dist/providers/brave/credentials.js.map +1 -1
- package/dist/providers/catalog.d.ts +34 -0
- package/dist/providers/catalog.d.ts.map +1 -0
- package/dist/providers/catalog.js +57 -0
- package/dist/providers/catalog.js.map +1 -0
- package/dist/providers/crossref/adapter.js +2 -2
- package/dist/providers/crossref/adapter.js.map +1 -1
- package/dist/providers/crossref/client.d.ts +4 -1
- package/dist/providers/crossref/client.d.ts.map +1 -1
- package/dist/providers/crossref/client.js +11 -3
- package/dist/providers/crossref/client.js.map +1 -1
- package/dist/providers/crossref/diagnostics.d.ts +1 -1
- package/dist/providers/crossref/diagnostics.js +1 -1
- package/dist/providers/crossref/diagnostics.js.map +1 -1
- package/dist/providers/europepmc/adapter.js +2 -2
- package/dist/providers/europepmc/adapter.js.map +1 -1
- package/dist/providers/europepmc/client.d.ts +4 -1
- package/dist/providers/europepmc/client.d.ts.map +1 -1
- package/dist/providers/europepmc/client.js +11 -3
- package/dist/providers/europepmc/client.js.map +1 -1
- package/dist/providers/exa/client.d.ts +1 -0
- package/dist/providers/exa/client.d.ts.map +1 -1
- package/dist/providers/exa/client.js +3 -2
- package/dist/providers/exa/client.js.map +1 -1
- package/dist/providers/exa/credentials.d.ts +2 -0
- package/dist/providers/exa/credentials.d.ts.map +1 -1
- package/dist/providers/exa/credentials.js +2 -0
- package/dist/providers/exa/credentials.js.map +1 -1
- package/dist/providers/firecrawl/adapter.d.ts.map +1 -1
- package/dist/providers/firecrawl/adapter.js +2 -1
- package/dist/providers/firecrawl/adapter.js.map +1 -1
- package/dist/providers/firecrawl/client.d.ts +1 -0
- package/dist/providers/firecrawl/client.d.ts.map +1 -1
- package/dist/providers/firecrawl/client.js +3 -2
- package/dist/providers/firecrawl/client.js.map +1 -1
- package/dist/providers/firecrawl/credentials.d.ts +2 -0
- package/dist/providers/firecrawl/credentials.d.ts.map +1 -1
- package/dist/providers/firecrawl/credentials.js +2 -0
- package/dist/providers/firecrawl/credentials.js.map +1 -1
- package/dist/providers/firecrawl/quota.d.ts.map +1 -1
- package/dist/providers/firecrawl/quota.js +5 -2
- package/dist/providers/firecrawl/quota.js.map +1 -1
- package/dist/providers/jina/adapter.d.ts.map +1 -1
- package/dist/providers/jina/adapter.js +6 -4
- package/dist/providers/jina/adapter.js.map +1 -1
- package/dist/providers/jina/client.d.ts +2 -0
- package/dist/providers/jina/client.d.ts.map +1 -1
- package/dist/providers/jina/client.js +6 -5
- package/dist/providers/jina/client.js.map +1 -1
- package/dist/providers/jina/credentials.d.ts +2 -0
- package/dist/providers/jina/credentials.d.ts.map +1 -1
- package/dist/providers/jina/credentials.js +2 -0
- package/dist/providers/jina/credentials.js.map +1 -1
- package/dist/providers/kagi/adapter.d.ts +44 -0
- package/dist/providers/kagi/adapter.d.ts.map +1 -0
- package/dist/providers/kagi/adapter.js +188 -0
- package/dist/providers/kagi/adapter.js.map +1 -0
- package/dist/providers/kagi/client.d.ts +41 -0
- package/dist/providers/kagi/client.d.ts.map +1 -0
- package/dist/providers/kagi/client.js +100 -0
- package/dist/providers/kagi/client.js.map +1 -0
- package/dist/providers/kagi/credentials.d.ts +15 -0
- package/dist/providers/kagi/credentials.d.ts.map +1 -0
- package/dist/providers/kagi/credentials.js +35 -0
- package/dist/providers/kagi/credentials.js.map +1 -0
- package/dist/providers/kagi/diagnostics.d.ts +17 -0
- package/dist/providers/kagi/diagnostics.d.ts.map +1 -0
- package/dist/providers/kagi/diagnostics.js +33 -0
- package/dist/providers/kagi/diagnostics.js.map +1 -0
- package/dist/providers/linkup/client.d.ts +1 -0
- package/dist/providers/linkup/client.d.ts.map +1 -1
- package/dist/providers/linkup/client.js +3 -2
- package/dist/providers/linkup/client.js.map +1 -1
- package/dist/providers/linkup/credentials.d.ts +2 -0
- package/dist/providers/linkup/credentials.d.ts.map +1 -1
- package/dist/providers/linkup/credentials.js +2 -0
- package/dist/providers/linkup/credentials.js.map +1 -1
- package/dist/providers/minimax/adapter.d.ts.map +1 -1
- package/dist/providers/minimax/adapter.js +2 -2
- package/dist/providers/minimax/adapter.js.map +1 -1
- package/dist/providers/minimax/coding-plan-client.d.ts +1 -0
- package/dist/providers/minimax/coding-plan-client.d.ts.map +1 -1
- package/dist/providers/minimax/coding-plan-client.js +3 -2
- package/dist/providers/minimax/coding-plan-client.js.map +1 -1
- package/dist/providers/minimax/quota-client.d.ts +1 -0
- package/dist/providers/minimax/quota-client.d.ts.map +1 -1
- package/dist/providers/minimax/quota-client.js +3 -2
- package/dist/providers/minimax/quota-client.js.map +1 -1
- package/dist/providers/openalex/client.d.ts +2 -0
- package/dist/providers/openalex/client.d.ts.map +1 -1
- package/dist/providers/openalex/client.js +10 -3
- package/dist/providers/openalex/client.js.map +1 -1
- package/dist/providers/openalex/diagnostics.d.ts +1 -1
- package/dist/providers/openalex/diagnostics.js +1 -1
- package/dist/providers/openalex/diagnostics.js.map +1 -1
- package/dist/providers/parallel/client.d.ts +1 -0
- package/dist/providers/parallel/client.d.ts.map +1 -1
- package/dist/providers/parallel/client.js +3 -2
- package/dist/providers/parallel/client.js.map +1 -1
- package/dist/providers/parallel/credentials.d.ts +2 -0
- package/dist/providers/parallel/credentials.d.ts.map +1 -1
- package/dist/providers/parallel/credentials.js +2 -0
- package/dist/providers/parallel/credentials.js.map +1 -1
- package/dist/providers/perplexity/client.d.ts +2 -0
- package/dist/providers/perplexity/client.d.ts.map +1 -1
- package/dist/providers/perplexity/client.js +5 -4
- package/dist/providers/perplexity/client.js.map +1 -1
- package/dist/providers/perplexity/credentials.d.ts +2 -0
- package/dist/providers/perplexity/credentials.d.ts.map +1 -1
- package/dist/providers/perplexity/credentials.js +2 -0
- package/dist/providers/perplexity/credentials.js.map +1 -1
- package/dist/providers/pubmed/client.d.ts +2 -0
- package/dist/providers/pubmed/client.d.ts.map +1 -1
- package/dist/providers/pubmed/client.js +10 -3
- package/dist/providers/pubmed/client.js.map +1 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +10 -1
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/searchapi/adapter.d.ts +54 -0
- package/dist/providers/searchapi/adapter.d.ts.map +1 -0
- package/dist/providers/searchapi/adapter.js +310 -0
- package/dist/providers/searchapi/adapter.js.map +1 -0
- package/dist/providers/searchapi/client.d.ts +73 -0
- package/dist/providers/searchapi/client.d.ts.map +1 -0
- package/dist/providers/searchapi/client.js +196 -0
- package/dist/providers/searchapi/client.js.map +1 -0
- package/dist/providers/searchapi/credentials.d.ts +46 -0
- package/dist/providers/searchapi/credentials.d.ts.map +1 -0
- package/dist/providers/searchapi/credentials.js +71 -0
- package/dist/providers/searchapi/credentials.js.map +1 -0
- package/dist/providers/searchapi/diagnostics.d.ts +48 -0
- package/dist/providers/searchapi/diagnostics.d.ts.map +1 -0
- package/dist/providers/searchapi/diagnostics.js +70 -0
- package/dist/providers/searchapi/diagnostics.js.map +1 -0
- package/dist/providers/searchapi/quota.d.ts +60 -0
- package/dist/providers/searchapi/quota.d.ts.map +1 -0
- package/dist/providers/searchapi/quota.js +125 -0
- package/dist/providers/searchapi/quota.js.map +1 -0
- package/dist/providers/spider/client.d.ts +2 -0
- package/dist/providers/spider/client.d.ts.map +1 -1
- package/dist/providers/spider/client.js +16 -6
- package/dist/providers/spider/client.js.map +1 -1
- package/dist/providers/spider/credentials.d.ts +2 -0
- package/dist/providers/spider/credentials.d.ts.map +1 -1
- package/dist/providers/spider/credentials.js +2 -0
- package/dist/providers/spider/credentials.js.map +1 -1
- package/dist/providers/tavily/client.d.ts +1 -0
- package/dist/providers/tavily/client.d.ts.map +1 -1
- package/dist/providers/tavily/client.js +3 -2
- package/dist/providers/tavily/client.js.map +1 -1
- package/dist/providers/tavily/credentials.d.ts +2 -0
- package/dist/providers/tavily/credentials.d.ts.map +1 -1
- package/dist/providers/tavily/credentials.js +2 -0
- package/dist/providers/tavily/credentials.js.map +1 -1
- package/dist/providers/types.d.ts +31 -2
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js +3 -0
- package/dist/providers/types.js.map +1 -1
- package/dist/providers/you/client.d.ts +4 -0
- package/dist/providers/you/client.d.ts.map +1 -1
- package/dist/providers/you/client.js +4 -5
- package/dist/providers/you/client.js.map +1 -1
- package/dist/providers/you/credentials.d.ts +2 -0
- package/dist/providers/you/credentials.d.ts.map +1 -1
- package/dist/providers/you/credentials.js +3 -3
- package/dist/providers/you/credentials.js.map +1 -1
- package/dist/providers/zai/adapter.d.ts.map +1 -1
- package/dist/providers/zai/adapter.js +219 -4
- package/dist/providers/zai/adapter.js.map +1 -1
- package/dist/providers/zai/credentials.d.ts +2 -0
- package/dist/providers/zai/credentials.d.ts.map +1 -1
- package/dist/providers/zai/credentials.js +2 -0
- package/dist/providers/zai/credentials.js.map +1 -1
- package/dist/providers/zai/layout-parsing.d.ts +76 -0
- package/dist/providers/zai/layout-parsing.d.ts.map +1 -0
- package/dist/providers/zai/layout-parsing.js +151 -0
- package/dist/providers/zai/layout-parsing.js.map +1 -0
- package/dist/providers/zai/media.d.ts +19 -0
- package/dist/providers/zai/media.d.ts.map +1 -1
- package/dist/providers/zai/media.js +61 -0
- package/dist/providers/zai/media.js.map +1 -1
- package/dist/providers/zai/monitor-client.d.ts +1 -0
- package/dist/providers/zai/monitor-client.d.ts.map +1 -1
- package/dist/providers/zai/monitor-client.js +3 -2
- package/dist/providers/zai/monitor-client.js.map +1 -1
- package/dist/providers/zai/quota.d.ts +4 -0
- package/dist/providers/zai/quota.d.ts.map +1 -1
- package/dist/providers/zai/quota.js +16 -1
- package/dist/providers/zai/quota.js.map +1 -1
- package/package.json +1 -1
- package/skills/scoutline/SKILL.md +163 -45
- package/skills/scoutline/references/advanced.md +12 -10
package/dist/index.js
CHANGED
|
@@ -20,6 +20,8 @@ import { handleFetch } from "./commands/fetch.js";
|
|
|
20
20
|
import { handleArchive, parseArchiveArgs } from "./commands/archive.js";
|
|
21
21
|
import { handleWatch } from "./commands/watch.js";
|
|
22
22
|
import { handleScience } from "./commands/science.js";
|
|
23
|
+
import { investigate, INVESTIGATE_HELP, } from "./commands/investigate.js";
|
|
24
|
+
import { ZaiApiClient } from "./lib/api-client.js";
|
|
23
25
|
import { buildProviderCacheKey, cacheStats, clearAllCaches, parsePruneDuration, pruneCaches, } from "./lib/cache.js";
|
|
24
26
|
import { parseBatchManifest } from "./lib/batch-manifest.js";
|
|
25
27
|
import { assignBatchProviders } from "./lib/batch-assign.js";
|
|
@@ -33,7 +35,7 @@ import { buildJournalEntry, buildNoteEntry, buildSearchSkeleton, buildReadSkelet
|
|
|
33
35
|
import { buildJournalRepeatMarker } from "./lib/journal.js";
|
|
34
36
|
import { applyBudget } from "./lib/output-budget.js";
|
|
35
37
|
import { persistCompaction } from "./lib/output-budget-persistence.js";
|
|
36
|
-
import { FileError, ValidationError, UnsupportedCapabilityError, CommandOptionUnsupportedError, getErrorExitCode, } from "./lib/errors.js";
|
|
38
|
+
import { ApiError, FileError, ValidationError, UnsupportedCapabilityError, CommandOptionUnsupportedError, getErrorExitCode, } from "./lib/errors.js";
|
|
37
39
|
import * as os from "node:os";
|
|
38
40
|
import * as path from "node:path";
|
|
39
41
|
import * as fs from "node:fs/promises";
|
|
@@ -42,15 +44,17 @@ import { invokeCommand, } from "./command-invocation.js";
|
|
|
42
44
|
import { createFileResponseCache, defaultResponseCache, isIsolatedEnv, responseCacheDir, } from "./lib/cache.js";
|
|
43
45
|
import { MAX_SUBQUERIES, parseContextText, readContextSource } from "./lib/context-file.js";
|
|
44
46
|
import { configuredSecrets, redactSecrets } from "./lib/redact.js";
|
|
45
|
-
import { configFilePath, readConfig, resolveConfigRoot, resolveConfigRootPure, resolveEnvFromConfig, setConfigValue, unsetConfigValue, } from "./lib/config-store.js";
|
|
47
|
+
import { configFilePath, readConfig, resolveConfigRoot, resolveConfigRootPure, resolveEnvFromConfig, setConfigValue, unsetConfigValue, resolveFusionMode, } from "./lib/config-store.js";
|
|
46
48
|
import { inspectConfig, createDefaultVerificationPromoter, createDefaultHintShownStore, } from "./lib/config-store.js";
|
|
47
49
|
import { createDefaultQuotaStore, refreshQuotaSnapshots, } from "./lib/quota-store.js";
|
|
48
50
|
import { createCompositeConsumptionSink, createQuotaStoreConsumptionSink, } from "./lib/consumption.js";
|
|
49
51
|
import { createUsageLedgerSink, readUsageLedger, resolveUsageLedgerPath, } from "./lib/usage-ledger.js";
|
|
50
52
|
import { classifyCredentialState, formatEnvOnlyHint, isCommandHelpInvocation, isDryRunBatchInvocation, OBSERVATIONAL_COMMANDS, } from "./lib/trigger-detection.js";
|
|
51
|
-
import { resolveProviderId, resolveEffectiveProvider } from "./providers/selection.js";
|
|
53
|
+
import { resolveProviderId, resolveEffectiveProvider, parseProviderId, } from "./providers/selection.js";
|
|
52
54
|
import { BUILT_IN_PROVIDER_DESCRIPTORS } from "./providers/registry.js";
|
|
55
|
+
import { createZaiDescriptor } from "./providers/zai/adapter.js";
|
|
53
56
|
import { PROVIDER_IDS } from "./providers/types.js";
|
|
57
|
+
import { SHARED_PROVIDER_FLAG_IDS, SHARED_PROVIDER_IDS } from "./providers/catalog.js";
|
|
54
58
|
import { executeWithFallback } from "./lib/provider-fallback.js";
|
|
55
59
|
import { visionOperationToCapability } from "./capabilities/vision.js";
|
|
56
60
|
import { handleInitWithHelp, createInquirerPrompts, createDefaultConfigStore, } from "./commands/init.js";
|
|
@@ -64,7 +68,7 @@ Usage: scoutline <command> [args] [options]
|
|
|
64
68
|
|
|
65
69
|
Commands:
|
|
66
70
|
vision Image and video analysis (Z.AI; MiniMax for interpret-image)
|
|
67
|
-
search Real-time web search (shared: all
|
|
71
|
+
search Real-time web search (shared: all ${SHARED_PROVIDER_IDS.length} Providers; --topic
|
|
68
72
|
honored by every Provider)
|
|
69
73
|
read Fetch and parse web pages (Provider Capability; Z.AI, Tavily,
|
|
70
74
|
Exa, Firecrawl, Parallel, Jina, You.com, Linkup, and Spider.cloud supply it) crawl Crawl a website from a starting URL (Provider Capability;
|
|
@@ -100,12 +104,16 @@ Commands:
|
|
|
100
104
|
DOI, PMID, arXiv ids; keyless scholarly suppliers:
|
|
101
105
|
openalex, arxiv, crossref, pubmed, europepmc)
|
|
102
106
|
code Execute TypeScript tool chains (Code Mode, Z.AI)
|
|
107
|
+
investigate Local investigation pipeline: plan sub-queries, fan out
|
|
108
|
+
search, read top sources, extract passages into an
|
|
109
|
+
EvidencePack (search+reader supplier union; the pack is data,
|
|
110
|
+
text modes fall back to JSON)
|
|
103
111
|
init Interactive onboarding wizard (writes ~/.scoutline/config.json)
|
|
104
112
|
config Manage ~/.scoutline/config.json keys (get / set / unset,
|
|
105
113
|
credential-free)
|
|
106
114
|
|
|
107
115
|
Provider selection (precedence: --provider, then SCOUTLINE_PROVIDER, then zai):
|
|
108
|
-
--provider
|
|
116
|
+
--provider <${SHARED_PROVIDER_FLAG_IDS}> Select the active Provider for shared capabilities (science suppliers pin only within "scoutline science ...") SCOUTLINE_PROVIDER=<id> Fallback when --provider is not passed
|
|
109
117
|
|
|
110
118
|
Shared capabilities accept --provider. The 'repo', 'read', 'crawl', 'map',
|
|
111
119
|
and 'research' commands participate in Provider selection: Z.AI
|
|
@@ -115,8 +123,10 @@ advertises and supplies search, reader, and research; Parallel AI
|
|
|
115
123
|
advertises search, research, and reader; Perplexity advertises search
|
|
116
124
|
and research; Jina AI advertises search, reader, and research (keyless
|
|
117
125
|
supported); You.com advertises search, reader, and research; Linkup advertises search, reader, and research; Spider.cloud
|
|
118
|
-
advertises search, reader, crawl, and map;
|
|
119
|
-
|
|
126
|
+
advertises search, reader, crawl, and map; Bocha AI advertises and
|
|
127
|
+
supplies search; SearchApi advertises and supplies search; Kagi
|
|
128
|
+
advertises and supplies search; MiniMax advertises and supplies none of
|
|
129
|
+
those Provider-only Capabilities.
|
|
120
130
|
Provider fallback is always-on by default (0.11.0+): selecting a
|
|
121
131
|
non-supplier emits a stderr notice and silently reroutes to the next
|
|
122
132
|
eligible configured Provider in registry order. Use --no-fallback (or
|
|
@@ -125,6 +135,16 @@ UNSUPPORTED_CAPABILITY behavior. Z.AI-only commands (tools, tool,
|
|
|
125
135
|
call, code) carry the flag but ignore it. Quota and doctor report
|
|
126
136
|
per-Provider; --provider picks the effective Provider for metadata.
|
|
127
137
|
|
|
138
|
+
Merged-search ranking: fan-out and --merge results rank by reciprocal
|
|
139
|
+
rank fusion (SCOUTLINE_FUSION=<rrf|occurrence>; default rrf; also the
|
|
140
|
+
config \`fusion\` key, env wins). \`occurrence\` restores the legacy
|
|
141
|
+
ordering byte-for-byte. See \`scoutline search --help\`.
|
|
142
|
+
|
|
143
|
+
Flag strictness: unknown command flags are silently ignored by default
|
|
144
|
+
(a typo like --fusio runs as if the flag were absent). Set
|
|
145
|
+
SCOUTLINE_STRICT_FLAGS=1 to reject unknown flags on every command with
|
|
146
|
+
a VALIDATION_ERROR naming the offender.
|
|
147
|
+
|
|
128
148
|
Global Options:
|
|
129
149
|
--output-format <data|json|pretty|compact|markdown|refs|tty> Output mode (default: data)
|
|
130
150
|
-O <mode> Alias for --output-format
|
|
@@ -153,6 +173,7 @@ Help:
|
|
|
153
173
|
scoutline archive --help
|
|
154
174
|
scoutline watch --help
|
|
155
175
|
scoutline science --help
|
|
176
|
+
scoutline investigate --help
|
|
156
177
|
scoutline init --help
|
|
157
178
|
`.trim();
|
|
158
179
|
function parseArgs(args) {
|
|
@@ -165,6 +186,16 @@ function parseArgs(args) {
|
|
|
165
186
|
break;
|
|
166
187
|
if (arg.startsWith("--")) {
|
|
167
188
|
const key = arg.slice(2);
|
|
189
|
+
// TWO-SPELLING CONTRACT (#242): this branch maps `--no-X` to BOTH
|
|
190
|
+
// `flags.X = false` AND `flags["no-X"] = true`. A feature that
|
|
191
|
+
// must reject a flag therefore has to reject BOTH spellings — a
|
|
192
|
+
// handler checking only `flags.X` silently accepts `--no-X`, and
|
|
193
|
+
// one checking only `flags["no-X"]` misses the plain form. Use
|
|
194
|
+
// `rejectFlagPair(flags, name, makeError)` (below) for every
|
|
195
|
+
// flag-forbidden surface; the fusion rejection is the reference
|
|
196
|
+
// retrofit. (A structural fix — making `--no-X` set a single
|
|
197
|
+
// `flags.noX` key — would break every existing `--no-*`
|
|
198
|
+
// consumer.)
|
|
168
199
|
if (key.startsWith("no-")) {
|
|
169
200
|
flags[key.slice(3)] = false;
|
|
170
201
|
flags[key] = true;
|
|
@@ -200,6 +231,20 @@ function parseArgs(args) {
|
|
|
200
231
|
}
|
|
201
232
|
return { flags, positional };
|
|
202
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Guard for flag-forbidden features (#242): parseArgs maps `--no-X` to
|
|
236
|
+
* BOTH `flags.X = false` AND `flags["no-X"] = true`, so a feature that
|
|
237
|
+
* must reject a flag has to reject both spellings — checking one
|
|
238
|
+
* silently accepts the other (the footgun the fusion lane hit when its
|
|
239
|
+
* `--fusion` rejection had to know to test both). Throws the CALLER's
|
|
240
|
+
* error (built lazily, so the no-reject path allocates nothing) when
|
|
241
|
+
* either spelling of `--<name>` / `--no-<name>` is present.
|
|
242
|
+
*/
|
|
243
|
+
export function rejectFlagPair(flags, name, makeError) {
|
|
244
|
+
if (flags[name] !== undefined || flags[`no-${name}`] !== undefined) {
|
|
245
|
+
throw makeError();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
203
248
|
/**
|
|
204
249
|
* Collect every occurrence of a long `--<name>` flag in argv order,
|
|
205
250
|
* mirroring parseArgs' value-consumption rule (the next argument is the
|
|
@@ -226,6 +271,290 @@ function collectLongFlagValues(args, name) {
|
|
|
226
271
|
return values;
|
|
227
272
|
}
|
|
228
273
|
// ---------------------------------------------------------------------------
|
|
274
|
+
// Strict flag mode (#241). Outside `batch` (and `vision batch` /
|
|
275
|
+
// `history clear`, which carry their own gates) unknown CLI flags are
|
|
276
|
+
// silently ignored — `search "q" --fusio rrf` runs as if the flag were
|
|
277
|
+
// absent and parseArgs swallows the value into flag state. Global
|
|
278
|
+
// strictness would break users passing redundant flags, so rejection is
|
|
279
|
+
// OPT-IN: SCOUTLINE_STRICT_FLAGS set to any non-empty value (the
|
|
280
|
+
// SCOUTLINE_NO_FALLBACK idiom) makes the dispatcher reject every flag
|
|
281
|
+
// token the command's allowlist does not name, with the batch-style
|
|
282
|
+
// error naming the offender.
|
|
283
|
+
//
|
|
284
|
+
// Semantics of the scan (deliberate rulings):
|
|
285
|
+
// - Exact spellings: `--no-journal` is accepted only as the literal
|
|
286
|
+
// `no-journal` row; it does NOT license a bare `--journal`. The
|
|
287
|
+
// parseArgs `--no-X` double-map never participates — the scan reads
|
|
288
|
+
// raw argv tokens, not parsed flag state.
|
|
289
|
+
// - Short flags are checked too (`-h` everywhere; fetch's `-A/-X/-H`).
|
|
290
|
+
// A multi-dash-letter token like `-ab` (which parseArgs would treat
|
|
291
|
+
// as positional) rejects under strict mode — strict means strict.
|
|
292
|
+
// - Global flags (`--provider`, `--output-format`, `--save*`, ...) are
|
|
293
|
+
// stripped by extractGlobalOptions BEFORE this gate, so they are not
|
|
294
|
+
// listed; a few commands (cache/usage) keep `provider` rows anyway
|
|
295
|
+
// for direct in-process callers whose argv was never extracted.
|
|
296
|
+
// - Flags a command's handler REJECTS with a specific error (e.g.
|
|
297
|
+
// search's `--fusion`, map's `--max-chars`) are absent from the set
|
|
298
|
+
// on purpose: under strict mode the generic rejection fires first;
|
|
299
|
+
// under the lenient default the handler's specific error still does.
|
|
300
|
+
// - When you add a flag to a command, add its spelling here — a miss
|
|
301
|
+
// only bites strict-mode users, but it bites them loudly.
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
/**
|
|
304
|
+
* Per-command accepted flag spellings for SCOUTLINE_STRICT_FLAGS mode
|
|
305
|
+
* (#241). Keys are post-dash spellings (`no-journal`, not
|
|
306
|
+
* `--no-journal`); sets are the UNION across a command's subcommands.
|
|
307
|
+
* Exported for the strict-mode tests and the DISPATCHED_COMMANDS
|
|
308
|
+
* coverage pin.
|
|
309
|
+
*/
|
|
310
|
+
export const STRICT_FLAG_ALLOWLIST = {
|
|
311
|
+
vision: new Set([
|
|
312
|
+
"help",
|
|
313
|
+
"h",
|
|
314
|
+
"context",
|
|
315
|
+
"focus",
|
|
316
|
+
"language",
|
|
317
|
+
"output",
|
|
318
|
+
"type",
|
|
319
|
+
// `vision batch` wrapper (the VISION_BATCH_FLAGS surface)
|
|
320
|
+
"out",
|
|
321
|
+
"prompt",
|
|
322
|
+
"concurrency",
|
|
323
|
+
"dry-run",
|
|
324
|
+
]),
|
|
325
|
+
search: new Set([
|
|
326
|
+
"help",
|
|
327
|
+
"h",
|
|
328
|
+
"count",
|
|
329
|
+
"domain",
|
|
330
|
+
"fields",
|
|
331
|
+
"recency",
|
|
332
|
+
"topic",
|
|
333
|
+
"type",
|
|
334
|
+
"location",
|
|
335
|
+
"content-size",
|
|
336
|
+
"merge",
|
|
337
|
+
"max-summary",
|
|
338
|
+
"no-cache",
|
|
339
|
+
"max-chars",
|
|
340
|
+
"context",
|
|
341
|
+
"context-stdin",
|
|
342
|
+
"no-journal",
|
|
343
|
+
]),
|
|
344
|
+
read: new Set([
|
|
345
|
+
"help",
|
|
346
|
+
"h",
|
|
347
|
+
"extract",
|
|
348
|
+
"format",
|
|
349
|
+
"full-envelope",
|
|
350
|
+
"keep-img-data-url",
|
|
351
|
+
"no-cache",
|
|
352
|
+
"no-gfm",
|
|
353
|
+
"no-images",
|
|
354
|
+
"timeout",
|
|
355
|
+
"with-images-summary",
|
|
356
|
+
"with-links",
|
|
357
|
+
"max-chars",
|
|
358
|
+
"no-journal",
|
|
359
|
+
]),
|
|
360
|
+
crawl: new Set([
|
|
361
|
+
"help",
|
|
362
|
+
"h",
|
|
363
|
+
"breadth",
|
|
364
|
+
"content-size",
|
|
365
|
+
"depth",
|
|
366
|
+
"exclude-paths",
|
|
367
|
+
"format",
|
|
368
|
+
"instructions",
|
|
369
|
+
"limit",
|
|
370
|
+
"no-cache",
|
|
371
|
+
"select-paths",
|
|
372
|
+
"timeout",
|
|
373
|
+
"max-chars",
|
|
374
|
+
]),
|
|
375
|
+
map: new Set([
|
|
376
|
+
"help",
|
|
377
|
+
"h",
|
|
378
|
+
"breadth",
|
|
379
|
+
"depth",
|
|
380
|
+
"exclude-paths",
|
|
381
|
+
"instructions",
|
|
382
|
+
"limit",
|
|
383
|
+
"no-cache",
|
|
384
|
+
"select-paths",
|
|
385
|
+
]),
|
|
386
|
+
research: new Set([
|
|
387
|
+
"help",
|
|
388
|
+
"h",
|
|
389
|
+
"citation-format",
|
|
390
|
+
"context",
|
|
391
|
+
"context-mode",
|
|
392
|
+
"context-stdin",
|
|
393
|
+
"domain",
|
|
394
|
+
"model",
|
|
395
|
+
"no-cache",
|
|
396
|
+
"output-length",
|
|
397
|
+
"timeout",
|
|
398
|
+
"max-chars",
|
|
399
|
+
"no-journal",
|
|
400
|
+
]),
|
|
401
|
+
repo: new Set([
|
|
402
|
+
"help",
|
|
403
|
+
"h",
|
|
404
|
+
"depth",
|
|
405
|
+
"focus",
|
|
406
|
+
"lang",
|
|
407
|
+
"language",
|
|
408
|
+
"max-chars",
|
|
409
|
+
"no-cache",
|
|
410
|
+
"no-focus",
|
|
411
|
+
"path",
|
|
412
|
+
]),
|
|
413
|
+
batch: new Set(["help", "h", "concurrency", "fail-fast", "dry-run"]),
|
|
414
|
+
tools: new Set(["help", "h", "filter", "full", "ts", "typescript", "vision", "no-vision"]),
|
|
415
|
+
tool: new Set(["help", "h", "vision", "no-vision"]),
|
|
416
|
+
call: new Set(["help", "h", "dry-run", "file", "json", "stdin", "vision", "no-vision"]),
|
|
417
|
+
doctor: new Set(["help", "h", "available", "health", "no-tools"]),
|
|
418
|
+
quota: new Set(["help", "h", "all-providers"]),
|
|
419
|
+
code: new Set(["help", "h", "logs", "timeout"]),
|
|
420
|
+
cache: new Set(["help", "h", "capability", "older-than", "provider"]),
|
|
421
|
+
usage: new Set(["help", "h", "days", "provider"]),
|
|
422
|
+
history: new Set([
|
|
423
|
+
"help",
|
|
424
|
+
"h",
|
|
425
|
+
"all",
|
|
426
|
+
"as-of",
|
|
427
|
+
"capability",
|
|
428
|
+
"command",
|
|
429
|
+
"kind",
|
|
430
|
+
"limit",
|
|
431
|
+
"repeats",
|
|
432
|
+
"since",
|
|
433
|
+
"tags",
|
|
434
|
+
// `history note` consumes --url/--title pairs through its own
|
|
435
|
+
// raw-argv scan (repeatable rows, not parseArgs flag state) — the
|
|
436
|
+
// final-review false-reject probe.
|
|
437
|
+
"url",
|
|
438
|
+
"title",
|
|
439
|
+
]),
|
|
440
|
+
init: new Set(["help", "h", "unregister"]),
|
|
441
|
+
config: new Set(["help", "h"]),
|
|
442
|
+
fetch: new Set([
|
|
443
|
+
"help",
|
|
444
|
+
"h",
|
|
445
|
+
"md5",
|
|
446
|
+
"sha256",
|
|
447
|
+
"raw",
|
|
448
|
+
"out",
|
|
449
|
+
"ua",
|
|
450
|
+
"user-agent",
|
|
451
|
+
"A",
|
|
452
|
+
"method",
|
|
453
|
+
"X",
|
|
454
|
+
"data",
|
|
455
|
+
"header",
|
|
456
|
+
"H",
|
|
457
|
+
"pdf",
|
|
458
|
+
"pdf-repair",
|
|
459
|
+
"timeout",
|
|
460
|
+
]),
|
|
461
|
+
archive: new Set(["help", "h", "at", "from", "limit", "raw", "since", "status", "timeout", "to"]),
|
|
462
|
+
watch: new Set(["help", "h", "all", "format", "keep", "name", "purge", "timeout"]),
|
|
463
|
+
science: new Set([
|
|
464
|
+
"help",
|
|
465
|
+
"h",
|
|
466
|
+
"author",
|
|
467
|
+
"year",
|
|
468
|
+
"venue",
|
|
469
|
+
"type",
|
|
470
|
+
"provider",
|
|
471
|
+
"no-cache",
|
|
472
|
+
"max-chars",
|
|
473
|
+
"no-journal",
|
|
474
|
+
]),
|
|
475
|
+
// investigate-pipeline T6: the documented control surface only
|
|
476
|
+
// (PRD AC-1). The rejected trio (depth/arms/budget-tokens) and
|
|
477
|
+
// --context-stdin are deliberately ABSENT — under strict flags they
|
|
478
|
+
// reject here, and the lenient default rejects them inside
|
|
479
|
+
// handleInvestigate (the rejection IS the feature).
|
|
480
|
+
investigate: new Set([
|
|
481
|
+
"help",
|
|
482
|
+
"h",
|
|
483
|
+
"provider",
|
|
484
|
+
"context",
|
|
485
|
+
"sources",
|
|
486
|
+
"max-chars",
|
|
487
|
+
"no-cache",
|
|
488
|
+
"no-journal",
|
|
489
|
+
"synthesize",
|
|
490
|
+
// investigate-verify lane (DESIGN D5): claim-corroboration mode
|
|
491
|
+
// flag. Pair-rejected with --context below (verify owns planning).
|
|
492
|
+
"verify",
|
|
493
|
+
]),
|
|
494
|
+
};
|
|
495
|
+
/**
|
|
496
|
+
* The strict-flag gate (#241): scan a command's argv (the tokens AFTER
|
|
497
|
+
* the command name — global options were already extracted) and return
|
|
498
|
+
* the first flag token the command's allowlist does not name, or
|
|
499
|
+
* `undefined` when every token is accepted. Pure; throws never; the
|
|
500
|
+
* caller owns the error envelope. Commands without an allowlist row
|
|
501
|
+
* (unknown commands — the dispatcher's own `Unknown command` path) scan
|
|
502
|
+
* nothing.
|
|
503
|
+
*/
|
|
504
|
+
export function findUnknownStrictFlag(command, args) {
|
|
505
|
+
const allowed = STRICT_FLAG_ALLOWLIST[command];
|
|
506
|
+
if (allowed === undefined)
|
|
507
|
+
return undefined;
|
|
508
|
+
for (const arg of args) {
|
|
509
|
+
if (typeof arg !== "string" || !arg.startsWith("-") || arg.length < 2)
|
|
510
|
+
continue;
|
|
511
|
+
const key = arg.startsWith("--") ? arg.slice(2) : arg.slice(1);
|
|
512
|
+
if (key.length === 0)
|
|
513
|
+
continue; // a bare `--` token: never a flag name
|
|
514
|
+
if (!allowed.has(key))
|
|
515
|
+
return arg;
|
|
516
|
+
}
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* One early resolution pass over the known env doors (#244), so a bad
|
|
521
|
+
* value fails identically on EVERY command — previously
|
|
522
|
+
* SCOUTLINE_FUSION=bogus failed `quota` (the credentialed path
|
|
523
|
+
* resolves the door unconditionally) but silently succeeded on
|
|
524
|
+
* early-return commands (`config get` never read it).
|
|
525
|
+
*
|
|
526
|
+
* - SCOUTLINE_FUSION — strict enum via {@link resolveFusionMode}
|
|
527
|
+
* (config passed as undefined: this pass validates the ENV door
|
|
528
|
+
* only; the file value arrives at the later resolution already
|
|
529
|
+
* leniently parsed). Empty string is unset.
|
|
530
|
+
* - SCOUTLINE_PROVIDER — a single shared Provider id via
|
|
531
|
+
* {@link parseProviderId}, the exact validation the shared-capability
|
|
532
|
+
* paths already run — except on `science`, whose env-door grammar
|
|
533
|
+
* is the science supplier ids + "all" and is validated inside
|
|
534
|
+
* handleScience against the D5 arm order. The `--provider` FLAG is
|
|
535
|
+
* out of scope: it is extracted globally and still surfaces its
|
|
536
|
+
* per-command errors where it is consumed.
|
|
537
|
+
* - SCOUTLINE_NO_FALLBACK — boolean kill-switch: any non-empty value
|
|
538
|
+
* disables fallback, so there is nothing to validate (listed here
|
|
539
|
+
* because the door set is the contract, not just the checks).
|
|
540
|
+
*
|
|
541
|
+
* Pure; throws ValidationError on a bad door. `main` runs this
|
|
542
|
+
* pre-dispatch for every command (help/version bare short-circuits have
|
|
543
|
+
* already returned) and owns the error envelope.
|
|
544
|
+
*/
|
|
545
|
+
export function validateEnvDoors(env, command, explicitProvider) {
|
|
546
|
+
resolveFusionMode(env, undefined);
|
|
547
|
+
// PR #253 round 1: a present --provider flag (any value — id, comma
|
|
548
|
+
// list, or "all") wins the precedence chain, so SCOUTLINE_PROVIDER is
|
|
549
|
+
// a dead value on a pinned run and must not fail it. The flag's own
|
|
550
|
+
// value is validated downstream, where the consuming command reads it.
|
|
551
|
+
if (command !== "science" &&
|
|
552
|
+
explicitProvider === undefined &&
|
|
553
|
+
env.SCOUTLINE_PROVIDER !== undefined) {
|
|
554
|
+
parseProviderId(env.SCOUTLINE_PROVIDER);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
229
558
|
// Save-artifacts flag surface (batch ticket T3). `--save [<path>]`,
|
|
230
559
|
// `--save-format <json|markdown>`, and `--save-force` are global options:
|
|
231
560
|
// extracted for every command, removed from the rest stream, and consumed
|
|
@@ -252,6 +581,7 @@ const SAVE_CAPABLE_COMMANDS = new Set([
|
|
|
252
581
|
"research",
|
|
253
582
|
"repo",
|
|
254
583
|
"vision",
|
|
584
|
+
"investigate",
|
|
255
585
|
]);
|
|
256
586
|
/**
|
|
257
587
|
* ADR-0007 D5 — every command the dispatcher routes (below): the
|
|
@@ -286,6 +616,7 @@ export const DISPATCHED_COMMANDS = new Set([
|
|
|
286
616
|
"archive",
|
|
287
617
|
"watch",
|
|
288
618
|
"science",
|
|
619
|
+
"investigate",
|
|
289
620
|
]);
|
|
290
621
|
/**
|
|
291
622
|
* ADR-0007 D5 — commands WITHOUT an Output Budget ladder. They reject
|
|
@@ -330,6 +661,11 @@ export const ACCEPT_NO_JOURNAL_COMMANDS = new Set([
|
|
|
330
661
|
// Science verticals (T7): the science noun journals (skeleton entries,
|
|
331
662
|
// PRD AC-5c), so its --no-journal per-call escape must exist too.
|
|
332
663
|
"science",
|
|
664
|
+
// investigate-pipeline T6 (PRD AC-10): the run journals its
|
|
665
|
+
// UNDERLYING search/read ops exactly as running them standalone
|
|
666
|
+
// would (umbrella capability "search" — see the journalWiring map),
|
|
667
|
+
// so its --no-journal per-call escape exists on the same seam.
|
|
668
|
+
"investigate",
|
|
333
669
|
]);
|
|
334
670
|
/**
|
|
335
671
|
* Review N10 (fix-round F-6) — the dispatch surface, extracted from
|
|
@@ -716,7 +1052,19 @@ async function handleVision(args, outputMode, deps) {
|
|
|
716
1052
|
// descriptor ID is the *attempted* provider (not the
|
|
717
1053
|
// registry-derived effective provider) so fallback attempts
|
|
718
1054
|
// record the actual descriptor that invoked transport.
|
|
719
|
-
|
|
1055
|
+
// glm-ocr lane (ADR-0014 D7): extract-text on zai counts
|
|
1056
|
+
// usage-ledger attempts at the ADAPTER (the OCR cache is
|
|
1057
|
+
// adapter-internal; an 1113+fallback run is two attempts
|
|
1058
|
+
// inside one executor invoke; a cache hit is zero). The
|
|
1059
|
+
// adapter's seam receives the sink through the zai
|
|
1060
|
+
// descriptor's layoutParsingConsume dep (threaded at
|
|
1061
|
+
// registry construction below); the executor emission is
|
|
1062
|
+
// suppressed here so attempts are counted exactly once.
|
|
1063
|
+
...((deps.consume !== undefined &&
|
|
1064
|
+
!(descriptor.zaiOcrLedgerSeam === true &&
|
|
1065
|
+
operation === "extract-text"))
|
|
1066
|
+
? { consume: deps.consume }
|
|
1067
|
+
: {}),
|
|
720
1068
|
...(deps.consume !== undefined ? { provider: descriptor.id } : {}),
|
|
721
1069
|
...(deps.now !== undefined ? { now: deps.now } : {}),
|
|
722
1070
|
};
|
|
@@ -871,6 +1219,14 @@ async function handleSearch(args, outputMode, deps) {
|
|
|
871
1219
|
if (typeof flags["context-stdin"] === "string") {
|
|
872
1220
|
throw new ValidationError("--context-stdin does not take a value.", 'Pipe the context on standard input: cat notes.md | scoutline search "<query>" --context-stdin.');
|
|
873
1221
|
}
|
|
1222
|
+
// Fusion seed-24 AC-1 pin: search takes NO --fusion flag — the
|
|
1223
|
+
// algorithm is a standing setting, not a per-query option — so a
|
|
1224
|
+
// query flag could never exist. rejectFlagPair (#242) guards BOTH
|
|
1225
|
+
// spellings (parseArgs maps `--no-fusion` to flags.fusion=false AND
|
|
1226
|
+
// flags["no-fusion"]=true); rejection runs BEFORE the help-gate for
|
|
1227
|
+
// the same reason as `--context` above (parseArgs swallows the value
|
|
1228
|
+
// into flag state, and help would exit 0).
|
|
1229
|
+
rejectFlagPair(flags, "fusion", () => new ValidationError("search has no --fusion flag; the ranking algorithm is a standing setting, not a per-query option.", "Use `scoutline config set fusion <rrf|occurrence>` or the SCOUTLINE_FUSION environment variable."));
|
|
874
1230
|
if (flags.help || flags.h || positional.length === 0) {
|
|
875
1231
|
deps.invocation.writeStdout(SEARCH_HELP);
|
|
876
1232
|
return 0;
|
|
@@ -1227,6 +1583,9 @@ async function handleSearch(args, outputMode, deps) {
|
|
|
1227
1583
|
env: deps.env,
|
|
1228
1584
|
query: dispatchQuery,
|
|
1229
1585
|
searchOptions,
|
|
1586
|
+
// Seed-24 T3: the resolved ranking mode (env > config >
|
|
1587
|
+
// "rrf") — the executor's merge names it explicitly.
|
|
1588
|
+
fusionMode: deps.fusionMode ?? "rrf",
|
|
1230
1589
|
dependencies: {
|
|
1231
1590
|
cache: deps.searchCache,
|
|
1232
1591
|
sleep: deps.searchSleep,
|
|
@@ -1268,6 +1627,10 @@ async function handleSearch(args, outputMode, deps) {
|
|
|
1268
1627
|
cache: deps.searchCache,
|
|
1269
1628
|
sleep: deps.searchSleep,
|
|
1270
1629
|
random: deps.searchRandom,
|
|
1630
|
+
// Seed-24 T3: the resolved ranking mode (env > config >
|
|
1631
|
+
// "rrf") reaches the single-provider --merge seam too, so
|
|
1632
|
+
// one merge implementation serves both paths.
|
|
1633
|
+
fusionMode: deps.fusionMode ?? "rrf",
|
|
1271
1634
|
// PB-T2 parity with the fan-out path above (usage-ledger
|
|
1272
1635
|
// DESIGN D7): thread the configured consumption sink +
|
|
1273
1636
|
// clock so every billable sub-query on the single-pin path
|
|
@@ -3224,6 +3587,273 @@ async function handleHistoryExport(args, outputMode, deps) {
|
|
|
3224
3587
|
...(since !== undefined ? { since } : {}),
|
|
3225
3588
|
}), outputMode, now, deps.secrets);
|
|
3226
3589
|
}
|
|
3590
|
+
// ---------------------------------------------------------------------------
|
|
3591
|
+
// investigate-pipeline T6 (ADR-0013): the CLI wiring for the local
|
|
3592
|
+
// investigation pipeline. Mirrors handleSearch's ORDER — valueless-flag
|
|
3593
|
+
// guards BEFORE the help-gate, count-class validation BEFORE provider
|
|
3594
|
+
// resolution — and hands the parsed controls to commands/investigate.ts
|
|
3595
|
+
// (T4/T5), which owns the plan → fan-out → merge → read → pack
|
|
3596
|
+
// orchestration. The command is NOT routed through executeWithFallback:
|
|
3597
|
+
// it owns its provider grid through resolveFanoutPlan tiers (D3).
|
|
3598
|
+
// ---------------------------------------------------------------------------
|
|
3599
|
+
/**
|
|
3600
|
+
* Strict positive-integer parse for `--sources` (the parseAndValidateCount
|
|
3601
|
+
* class — not the lax parseInt idiom). Parse-level, before the help-gate
|
|
3602
|
+
* is bypassed and before provider resolution, so a bad value is
|
|
3603
|
+
* VALIDATION_ERROR regardless of credentials or provider state.
|
|
3604
|
+
*/
|
|
3605
|
+
export function parseAndValidateSources(raw) {
|
|
3606
|
+
if (raw === undefined || raw === "")
|
|
3607
|
+
return undefined;
|
|
3608
|
+
if (raw === true) {
|
|
3609
|
+
throw new ValidationError("--sources requires a value.", "Use a positive integer (e.g. --sources 5).");
|
|
3610
|
+
}
|
|
3611
|
+
const str = typeof raw === "string" ? raw : String(raw);
|
|
3612
|
+
if (!/^\d+$/.test(str)) {
|
|
3613
|
+
throw new ValidationError(`Invalid --sources value "${str}": must be a positive integer`, "Use a positive integer (e.g. --sources 5).");
|
|
3614
|
+
}
|
|
3615
|
+
const parsed = Number(str);
|
|
3616
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
|
3617
|
+
throw new ValidationError(`Invalid --sources value "${str}": must be a positive safe integer`, "Use a positive integer (e.g. --sources 5).");
|
|
3618
|
+
}
|
|
3619
|
+
return parsed;
|
|
3620
|
+
}
|
|
3621
|
+
async function handleInvestigate(args, outputMode, deps) {
|
|
3622
|
+
const { flags, positional } = parseArgs(args);
|
|
3623
|
+
// Valueless --context guard BEFORE the help-gate (the handleSearch
|
|
3624
|
+
// D1 placement pin): parseArgs records `true` for a valueless flag,
|
|
3625
|
+
// so without this check `investigate --context` would short-circuit
|
|
3626
|
+
// to HELP + exit 0 with the malformed flag silently swallowed.
|
|
3627
|
+
if (flags.context === true) {
|
|
3628
|
+
throw new ValidationError("--context requires a value.", "Pass a file path: --context <path>.");
|
|
3629
|
+
}
|
|
3630
|
+
// PRD AC-1 pins the accepted controls exactly; --context-stdin is NOT
|
|
3631
|
+
// investigate's (search-only spelling — the question's pipes and
|
|
3632
|
+
// --context cover the sub-query sources). Rejected explicitly, never
|
|
3633
|
+
// accepted-and-dropped; before the help-gate like every other
|
|
3634
|
+
// valueless/malformed-flag guard here.
|
|
3635
|
+
if (flags["context-stdin"] !== undefined) {
|
|
3636
|
+
throw new ValidationError("investigate has no --context-stdin flag.", "Pipe sub-queries with | in the question, or pass --context <path> (see `scoutline investigate --help`).");
|
|
3637
|
+
}
|
|
3638
|
+
// investigate-verify lane (DESIGN D5, PRD AC-1): --verify owns
|
|
3639
|
+
// planning — the claims ARE the grid — so a --context file is a
|
|
3640
|
+
// mode conflict. Both spellings of --context carry; the valueless
|
|
3641
|
+
// --context guard above already fired for the true case.
|
|
3642
|
+
if (flags.verify !== undefined && flags.context !== undefined) {
|
|
3643
|
+
throw new ValidationError("--verify and --context cannot be combined: --verify splits the statement into claims (verify owns planning).", "Drop --context, or run without --verify to plan from the notes file.");
|
|
3644
|
+
}
|
|
3645
|
+
// Valueless/malformed --verify guards (the --synthesize pattern):
|
|
3646
|
+
// parseArgs would swallow a following token as the value.
|
|
3647
|
+
for (const token of args) {
|
|
3648
|
+
if (typeof token === "string" && token.startsWith("--verify=")) {
|
|
3649
|
+
throw new ValidationError(`Invalid flag "${token}": the --flag=value form is not supported; --verify takes no value.`, "Pass the bare --verify to enable claim-corroboration mode, or omit it.");
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
if (flags.verify !== undefined && flags.verify !== true) {
|
|
3653
|
+
throw new ValidationError("--verify is a boolean flag and takes no value", "Pass the bare --verify to enable claim-corroboration mode, or omit it.");
|
|
3654
|
+
}
|
|
3655
|
+
const verify = flags.verify === true;
|
|
3656
|
+
// PRD AC-1 / ADR-0013 rejected extensions: --depth, --arms, and
|
|
3657
|
+
// --budget-tokens DO NOT EXIST (the parser rejecting them IS the
|
|
3658
|
+
// feature — no accept-and-drop). rejectFlagPair guards BOTH
|
|
3659
|
+
// spellings (#242); before the help-gate for the same reason as
|
|
3660
|
+
// --context above.
|
|
3661
|
+
rejectFlagPair(flags, "depth", () => new ValidationError("investigate has no --depth flag; planning is deterministic (pipes > --context > template).", "Structure the question with | sub-queries or --context <path> instead."));
|
|
3662
|
+
rejectFlagPair(flags, "arms", () => new ValidationError("investigate has no --arms flag; the arm set IS the provider pin.", "Use --provider <tavily,exa|all> to fan out, or a single --provider id for one arm."));
|
|
3663
|
+
rejectFlagPair(flags, "budget-tokens", () => new ValidationError("investigate has no --budget-tokens flag; --max-chars is the budget.", "Use --max-chars <n> (characters, not tokens)."));
|
|
3664
|
+
// T7 (PRD AC-7): --synthesize is VALUELESS. parseArgs assigns the
|
|
3665
|
+
// next non-dash token as a flag value, so `--synthesize foo` would
|
|
3666
|
+
// swallow the value; the =-form parses as a garbage key and is
|
|
3667
|
+
// silently dropped (#172 review F6). Both reject here, before the
|
|
3668
|
+
// help-gate, like every other malformed-flag guard in this handler.
|
|
3669
|
+
for (const token of args) {
|
|
3670
|
+
if (typeof token === "string" && token.startsWith("--synthesize=")) {
|
|
3671
|
+
throw new ValidationError(`Invalid flag "${token}": the --flag=value form is not supported; --synthesize takes no value.`, "Pass the bare --synthesize to enable it, or omit it.");
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
if (flags.synthesize !== undefined && flags.synthesize !== true) {
|
|
3675
|
+
throw new ValidationError("--synthesize is a boolean flag and takes no value", "Pass the bare --synthesize to enable it, or omit it.");
|
|
3676
|
+
}
|
|
3677
|
+
const synthesize = flags.synthesize === true;
|
|
3678
|
+
if (flags.help || flags.h || positional.length === 0) {
|
|
3679
|
+
deps.invocation.writeStdout(INVESTIGATE_HELP);
|
|
3680
|
+
return 0;
|
|
3681
|
+
}
|
|
3682
|
+
// Z.AI-only (PRD AC-7, the Code Mode precedent): the escape hatch
|
|
3683
|
+
// is Z.AI chat regardless of the provider pin. The handler is the
|
|
3684
|
+
// seam where the RAW pin is visible — the command cannot see it —
|
|
3685
|
+
// so the notice fires here, before any billable work.
|
|
3686
|
+
if (synthesize && deps.provider !== undefined && deps.provider.trim().length > 0) {
|
|
3687
|
+
const pinned = deps.provider.trim().toLowerCase();
|
|
3688
|
+
if (pinned !== "zai") {
|
|
3689
|
+
deps.invocation.writeStderr(`investigate: --synthesize is Z.AI-only; ignoring the --provider ${pinned} pin for the brief (the pack still uses it).`);
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
// The synthesis dep (T7): a Z.AI chat completion through the
|
|
3693
|
+
// in-repo /chat/completions surface. Built HERE, at the handler
|
|
3694
|
+
// seam, exactly like every other capability — the command holds no
|
|
3695
|
+
// transport. `visionComplete` is the only chat-completions method
|
|
3696
|
+
// on ZaiApiClient; the transport is fetch-per-call (no client to
|
|
3697
|
+
// close), and nothing is constructed when the flag is absent.
|
|
3698
|
+
const synthesizeDep = !synthesize
|
|
3699
|
+
? undefined
|
|
3700
|
+
: deps.synthesize ??
|
|
3701
|
+
(async (prompt) => {
|
|
3702
|
+
const client = new ZaiApiClient(undefined, deps.env);
|
|
3703
|
+
const response = await client.visionComplete([
|
|
3704
|
+
{
|
|
3705
|
+
role: "system",
|
|
3706
|
+
content: "You are given an evidence pack assembled from web sources: a question, " +
|
|
3707
|
+
"its planned sub-queries, and verbatim passage quotes extracted from the " +
|
|
3708
|
+
"read sources. Write a short, direct brief answering the question using " +
|
|
3709
|
+
"ONLY the quoted evidence. Do not invent facts, do not add citations that " +
|
|
3710
|
+
"are not in the quotes, and say plainly when the evidence is insufficient.",
|
|
3711
|
+
},
|
|
3712
|
+
{
|
|
3713
|
+
role: "user",
|
|
3714
|
+
// Deterministic serialization: fixed key order, compact JSON.
|
|
3715
|
+
content: JSON.stringify({
|
|
3716
|
+
question: prompt.question,
|
|
3717
|
+
subQueries: [...prompt.subQueries],
|
|
3718
|
+
quotes: [...prompt.quotes],
|
|
3719
|
+
}),
|
|
3720
|
+
},
|
|
3721
|
+
]);
|
|
3722
|
+
const content = response.choices?.[0]?.message?.content;
|
|
3723
|
+
if (typeof content !== "string") {
|
|
3724
|
+
throw new ApiError("synthesis returned no brief text", 502);
|
|
3725
|
+
}
|
|
3726
|
+
return content;
|
|
3727
|
+
});
|
|
3728
|
+
// Count-class validation BEFORE provider resolution (Fixup D order):
|
|
3729
|
+
// a bad --sources is VALIDATION_ERROR even with no credentials, and
|
|
3730
|
+
// --max-chars rides the strict parseMaxCharsFlag (parseBriefMaxChars
|
|
3731
|
+
// class). Both are re-validated inside the command's trust boundary.
|
|
3732
|
+
const sources = parseAndValidateSources(flags.sources);
|
|
3733
|
+
const maxChars = parseMaxCharsFlag(flags);
|
|
3734
|
+
const question = positional.join(" ");
|
|
3735
|
+
// Resolve the fan-out activation plan once HERE (pure — the same
|
|
3736
|
+
// inputs handleSearch passes) so the journal hook can stamp per-arm
|
|
3737
|
+
// serving cells BEFORE any arm runs; the command resolves its own
|
|
3738
|
+
// plan identically from the same inputs (the tiers are deterministic,
|
|
3739
|
+
// so the two resolutions agree by construction).
|
|
3740
|
+
const fanoutPlan = resolveFanoutPlan({
|
|
3741
|
+
explicitProviderRaw: deps.provider,
|
|
3742
|
+
env: deps.env,
|
|
3743
|
+
configFanout: deps.configFanout === true,
|
|
3744
|
+
...(deps.routing !== undefined ? { routing: deps.routing } : {}),
|
|
3745
|
+
descriptors: deps.providerDescriptors,
|
|
3746
|
+
});
|
|
3747
|
+
// Save wiring (D7): the pack is a first-class saveable result. The
|
|
3748
|
+
// provider routing mirrors the in-code vocabulary — fan-out records
|
|
3749
|
+
// the arm list with no single effective; single records the resolved
|
|
3750
|
+
// arm (the resolver's single-mode arm, not a quota-ranked pick — the
|
|
3751
|
+
// command runs exactly that arm). Args carry the
|
|
3752
|
+
// provider-influencing allow-list only.
|
|
3753
|
+
const investigateSaveArgs = {
|
|
3754
|
+
...(deps.provider !== undefined ? { provider: deps.provider } : {}),
|
|
3755
|
+
...(sources !== undefined ? { sources } : {}),
|
|
3756
|
+
...(flags["no-cache"] === true ? { "no-cache": true } : {}),
|
|
3757
|
+
...(synthesize ? { synthesize: true } : {}),
|
|
3758
|
+
...(verify ? { verify: true } : {}),
|
|
3759
|
+
};
|
|
3760
|
+
const investigateProviderRouting = fanoutPlan.mode === "fanout"
|
|
3761
|
+
? {
|
|
3762
|
+
mode: "fanout",
|
|
3763
|
+
...(deps.provider !== undefined ? { requested: deps.provider } : {}),
|
|
3764
|
+
arms: fanoutPlan.arms.map((arm) => String(arm)),
|
|
3765
|
+
}
|
|
3766
|
+
: {
|
|
3767
|
+
mode: "single",
|
|
3768
|
+
...(deps.provider !== undefined ? { requested: deps.provider } : {}),
|
|
3769
|
+
effective: String(fanoutPlan.arms[0]),
|
|
3770
|
+
};
|
|
3771
|
+
const save = createSaveArtifactHook(deps, {
|
|
3772
|
+
command: "investigate",
|
|
3773
|
+
outputMode,
|
|
3774
|
+
args: investigateSaveArgs,
|
|
3775
|
+
provider: investigateProviderRouting,
|
|
3776
|
+
});
|
|
3777
|
+
// Journal wiring (PRD AC-10): investigate journals its UNDERLYING
|
|
3778
|
+
// search/read ops exactly as running them standalone would. The
|
|
3779
|
+
// umbrella capability is "search" — JournalableCapability is closed
|
|
3780
|
+
// over {search, read, research, science} and lib/journal.ts is a
|
|
3781
|
+
// read-only seam for this lane; a literal "investigate" entry fails
|
|
3782
|
+
// asJournalEntry validation and would poison the whole log
|
|
3783
|
+
// (fail-open drop + corruption notice). Entry shape: query = the
|
|
3784
|
+
// question, skeleton rows = the pack sources (url/title) — fed via
|
|
3785
|
+
// the resultRows thunk read after dispatch resolves. Fan-out runs
|
|
3786
|
+
// stamp per-arm cells on the shared capture first (the arm-race fix),
|
|
3787
|
+
// exactly like handleSearch.
|
|
3788
|
+
let journalRows;
|
|
3789
|
+
if (fanoutPlan.mode === "fanout" && deps.journal !== undefined) {
|
|
3790
|
+
installFanoutArmCells(deps.journal.capture, fanoutPlan.arms);
|
|
3791
|
+
}
|
|
3792
|
+
const journal = deps.journal === undefined
|
|
3793
|
+
? undefined
|
|
3794
|
+
: createJournalHook(deps, {
|
|
3795
|
+
journal: deps.journal,
|
|
3796
|
+
query: question,
|
|
3797
|
+
resultRows: () => journalRows,
|
|
3798
|
+
fanoutRouting: fanoutPlan.mode === "fanout"
|
|
3799
|
+
? {
|
|
3800
|
+
mode: "fanout",
|
|
3801
|
+
...(deps.provider !== undefined ? { requested: deps.provider } : {}),
|
|
3802
|
+
arms: fanoutPlan.arms,
|
|
3803
|
+
}
|
|
3804
|
+
: undefined,
|
|
3805
|
+
});
|
|
3806
|
+
return invokeCommand(deps.invocation, async (context) => {
|
|
3807
|
+
const result = await investigate(question, {
|
|
3808
|
+
...(deps.provider !== undefined ? { provider: deps.provider } : {}),
|
|
3809
|
+
...(typeof flags.context === "string" ? { contextFile: flags.context } : {}),
|
|
3810
|
+
...(sources !== undefined ? { sources } : {}),
|
|
3811
|
+
...(maxChars !== undefined ? { maxChars } : {}),
|
|
3812
|
+
noCache: flags["no-cache"] === true,
|
|
3813
|
+
noJournal: collectLongFlagValues(args, "no-journal").length > 0,
|
|
3814
|
+
...(synthesize ? { synthesize: true } : {}),
|
|
3815
|
+
...(verify ? { verify: true } : {}),
|
|
3816
|
+
}, {
|
|
3817
|
+
descriptors: deps.providerDescriptors,
|
|
3818
|
+
env: deps.env,
|
|
3819
|
+
configFanout: deps.configFanout === true,
|
|
3820
|
+
...(deps.routing !== undefined ? { routing: deps.routing } : {}),
|
|
3821
|
+
// One shared cache for the search arms AND the reads — the
|
|
3822
|
+
// production defaultCache aliases every capability triple to
|
|
3823
|
+
// the same root, and the command's warm-cacheHit accounting
|
|
3824
|
+
// (search + reader partitions) assumes the one store.
|
|
3825
|
+
cache: deps.searchCache,
|
|
3826
|
+
sleep: deps.searchSleep,
|
|
3827
|
+
random: deps.searchRandom,
|
|
3828
|
+
...(deps.consume !== undefined ? { consume: deps.consume } : {}),
|
|
3829
|
+
...(deps.now !== undefined ? { now: deps.now } : {}),
|
|
3830
|
+
fusionMode: deps.fusionMode ?? "rrf",
|
|
3831
|
+
...(deps.now !== undefined ? { nowWall: () => new Date(deps.now()) } : {}),
|
|
3832
|
+
loadContextText: async (filePath) => (await readContextSource({ file: filePath }, {
|
|
3833
|
+
readFile: (f) => fs.readFile(f),
|
|
3834
|
+
readStdin: () => {
|
|
3835
|
+
throw new Error("stdin context is not investigate's flag");
|
|
3836
|
+
},
|
|
3837
|
+
})).text,
|
|
3838
|
+
readerCapabilityFor: (descriptor) => descriptor.create({ env: deps.env }).reader,
|
|
3839
|
+
...(deps.secrets !== undefined ? { secrets: deps.secrets } : {}),
|
|
3840
|
+
...(synthesizeDep !== undefined ? { synthesize: synthesizeDep } : {}),
|
|
3841
|
+
}, context);
|
|
3842
|
+
// Feed the journal skeleton from the pack BEFORE the result
|
|
3843
|
+
// leaves the behavior — url/title identities of the read
|
|
3844
|
+
// sources, the same row shape search journals.
|
|
3845
|
+
if (result.kind === "data") {
|
|
3846
|
+
const pack = result.data;
|
|
3847
|
+
if (Array.isArray(pack.sources)) {
|
|
3848
|
+
journalRows = pack.sources.map((source) => ({
|
|
3849
|
+
url: typeof source.url === "string" ? source.url : undefined,
|
|
3850
|
+
title: typeof source.title === "string" ? source.title : undefined,
|
|
3851
|
+
}));
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
return result;
|
|
3855
|
+
}, outputMode, deps.now, deps.secrets, save, journal);
|
|
3856
|
+
}
|
|
3227
3857
|
export { handleFetch, fetchCommand, executeFetch, FETCH_HELP } from "./commands/fetch.js";
|
|
3228
3858
|
export { handleArchive, archiveCdxCommand, archiveGetCommand, ARCHIVE_HELP, } from "./commands/archive.js";
|
|
3229
3859
|
async function handleQuota(args, outputMode, deps) {
|
|
@@ -3786,7 +4416,34 @@ export async function main(args, dependencies) {
|
|
|
3786
4416
|
const depsConfig = dependencies.config;
|
|
3787
4417
|
const loadScoutlineConfig = dependencies.loadScoutlineConfig ??
|
|
3788
4418
|
(depsConfig !== undefined ? async () => depsConfig : undefined);
|
|
4419
|
+
// glm-ocr lane (ADR-0014 D7): in full production mode (no injected
|
|
4420
|
+
// descriptor list), rebuild the zai entry with the shared consumption
|
|
4421
|
+
// sink threaded into its layoutParsingConsume seam so the extract-text
|
|
4422
|
+
// OCR arm counts ledger attempts at the adapter (cache hits = zero
|
|
4423
|
+
// rows; 1113+fallback = two rows). The sink binding exists BELOW this
|
|
4424
|
+
// point (main body order), so this is a lazy rebuild: the array holds
|
|
4425
|
+
// a descriptor whose create() closes over a sink REFERENCE resolved
|
|
4426
|
+
// at first vision dispatch. Tests injecting providerDescriptors keep
|
|
4427
|
+
// full control (their descriptor carries their own seams).
|
|
3789
4428
|
const providerDescriptors = dependencies.providerDescriptors ?? BUILT_IN_PROVIDER_DESCRIPTORS;
|
|
4429
|
+
const productionZaiLedgerDescriptors = dependencies.providerDescriptors
|
|
4430
|
+
? providerDescriptors
|
|
4431
|
+
: providerDescriptors.map((descriptor) => descriptor.id === "zai"
|
|
4432
|
+
? Object.assign(createZaiDescriptor({
|
|
4433
|
+
notice: (line) => process.stderr.write(`${line}\n`),
|
|
4434
|
+
layoutParsingConsume: {
|
|
4435
|
+
record: (event) => {
|
|
4436
|
+
if (zaiLedgerSink === undefined)
|
|
4437
|
+
return Promise.resolve();
|
|
4438
|
+
return zaiLedgerSink.record(event);
|
|
4439
|
+
},
|
|
4440
|
+
},
|
|
4441
|
+
}),
|
|
4442
|
+
// Marker: this descriptor's zai adapter owns extract-text
|
|
4443
|
+
// ledger rows through its seam (checked in handleVision
|
|
4444
|
+
// to suppress the executor emission exactly once).
|
|
4445
|
+
{ zaiOcrLedgerSeam: true })
|
|
4446
|
+
: descriptor);
|
|
3790
4447
|
// Resolve configured Provider credentials from the INJECTED env (B3) so
|
|
3791
4448
|
// redaction follows the same environment the handlers see — a secret
|
|
3792
4449
|
// that exists only in MainDependencies.env is still redacted from output.
|
|
@@ -3891,6 +4548,39 @@ export async function main(args, dependencies) {
|
|
|
3891
4548
|
}
|
|
3892
4549
|
const command = rest[0] ?? "";
|
|
3893
4550
|
const commandArgs = rest.slice(1);
|
|
4551
|
+
// #244: validate the env doors ONCE, pre-dispatch, for ALL command
|
|
4552
|
+
// RUNS — a typo'd env value used to fail only on the paths that
|
|
4553
|
+
// happened to resolve it first (quota yes, config get no). Help
|
|
4554
|
+
// invocations stay exempt (documentation, not a run — the same
|
|
4555
|
+
// doctrine as the --max-chars/--no-journal gates and the existing
|
|
4556
|
+
// "<cmd> --help succeeds with an invalid SCOUTLINE_PROVIDER" pins:
|
|
4557
|
+
// rendering help must never require a valid environment). Runs before
|
|
4558
|
+
// the strict-flags gate and every command-specific gate, and before
|
|
4559
|
+
// any disk or network work (the agent-registration check below), so a
|
|
4560
|
+
// doomed run costs nothing. Bare `--help`/`--version` already
|
|
4561
|
+
// returned above.
|
|
4562
|
+
if (!isCommandHelpInvocation(commandArgs)) {
|
|
4563
|
+
try {
|
|
4564
|
+
validateEnvDoors(env, command, provider);
|
|
4565
|
+
}
|
|
4566
|
+
catch (error) {
|
|
4567
|
+
invocation.writeStderr(formatErrorOutput(error, outputMode, envSecrets));
|
|
4568
|
+
return getErrorExitCode(error);
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
// #241: opt-in strict flag mode. Fires before every other pre-dispatch
|
|
4572
|
+
// gate (and before the agent-registration disk check) so a doomed run
|
|
4573
|
+
// costs nothing: with SCOUTLINE_STRICT_FLAGS set to any non-empty
|
|
4574
|
+
// value, a flag token the command's allowlist does not name rejects
|
|
4575
|
+
// with the batch-style error. The lenient default (unset/empty env)
|
|
4576
|
+
// stays byte-identical — unknown flags are accepted and dropped.
|
|
4577
|
+
if (typeof env.SCOUTLINE_STRICT_FLAGS === "string" && env.SCOUTLINE_STRICT_FLAGS.length > 0) {
|
|
4578
|
+
const unknownFlag = findUnknownStrictFlag(command, commandArgs);
|
|
4579
|
+
if (unknownFlag !== undefined) {
|
|
4580
|
+
invocation.writeStderr(formatErrorOutput(new ValidationError(`unknown flag "${unknownFlag}" for command "${command}" (SCOUTLINE_STRICT_FLAGS is enabled)`, `Run "scoutline ${command} --help" for the accepted flags, or unset SCOUTLINE_STRICT_FLAGS to restore lenient flag handling.`), outputMode, envSecrets));
|
|
4581
|
+
return 1;
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
3894
4584
|
// Lazy agent-registration stamp check (agent registration D5/D6):
|
|
3895
4585
|
// fires exactly once per CLI run, before command dispatch. Stamp-absent
|
|
3896
4586
|
// runs are zero-cost no-ops; drift refreshes the registered tools. A
|
|
@@ -3979,6 +4669,9 @@ export async function main(args, dependencies) {
|
|
|
3979
4669
|
// like the quota sink). The composite isolates each side — one
|
|
3980
4670
|
// sink's failure becomes one redacted warning and never blocks or
|
|
3981
4671
|
// fails the other.
|
|
4672
|
+
// glm-ocr lane: late-bound sink reference for the rebuilt zai
|
|
4673
|
+
// descriptor (assigned immediately after `consume` below).
|
|
4674
|
+
let zaiLedgerSink;
|
|
3982
4675
|
const consume = dependencies.consume ??
|
|
3983
4676
|
// ADR-0006 §5: --isolated runs skip local state persistence
|
|
3984
4677
|
// entirely — no usage-ledger writes, no quota-snapshot writes — so
|
|
@@ -3996,6 +4689,7 @@ export async function main(args, dependencies) {
|
|
|
3996
4689
|
filePath: resolveUsageLedgerPath(resolveConfigRootPure(env, { homedir: os.homedir() })),
|
|
3997
4690
|
}))
|
|
3998
4691
|
: undefined);
|
|
4692
|
+
zaiLedgerSink = consume;
|
|
3999
4693
|
// PB-T4: quota snapshot for selection. Declared here so
|
|
4000
4694
|
// `buildHandlerDeps` closes over the binding; assigned AFTER the
|
|
4001
4695
|
// PB-T1 pre-command refresh so observational commands' fresh data is
|
|
@@ -4010,16 +4704,18 @@ export async function main(args, dependencies) {
|
|
|
4010
4704
|
// Build HandlerDependencies for a given credential view. The
|
|
4011
4705
|
// cache/sleep/random fields are always available (resolved above); only
|
|
4012
4706
|
// env/secrets/fallbackEnabled depend on whether config has been loaded.
|
|
4013
|
-
const buildHandlerDeps = (credEnv, credSecrets, credFallback, credRouting = undefined, credFanout = undefined) => ({
|
|
4707
|
+
const buildHandlerDeps = (credEnv, credSecrets, credFallback, credRouting = undefined, credFanout = undefined, credFusion = undefined) => ({
|
|
4014
4708
|
invocation,
|
|
4709
|
+
synthesize: dependencies.synthesize,
|
|
4015
4710
|
env: credEnv,
|
|
4016
4711
|
secrets: credSecrets,
|
|
4017
4712
|
now,
|
|
4018
4713
|
provider,
|
|
4019
|
-
providerDescriptors,
|
|
4714
|
+
providerDescriptors: productionZaiLedgerDescriptors,
|
|
4020
4715
|
fallbackEnabled: credFallback,
|
|
4021
4716
|
routing: credRouting,
|
|
4022
4717
|
configFanout: credFanout,
|
|
4718
|
+
fusionMode: credFusion,
|
|
4023
4719
|
searchCache,
|
|
4024
4720
|
searchSleep,
|
|
4025
4721
|
searchRandom,
|
|
@@ -4541,9 +5237,14 @@ export async function main(args, dependencies) {
|
|
|
4541
5237
|
// Ticket 4. Read leniently: an absent or non-boolean field simply means
|
|
4542
5238
|
// fan-out stays off.
|
|
4543
5239
|
const configFanout = dependencies.configFanout ?? config.fanout === true;
|
|
5240
|
+
// Fusion seed-24: resolve the effective ranking mode ONCE per
|
|
5241
|
+
// invocation, injectable-wins exactly like configFanout above.
|
|
5242
|
+
// Strict: a non-enum SCOUTLINE_FUSION throws here (exit 1) — typos
|
|
5243
|
+
// never silently fall back to rrf.
|
|
5244
|
+
const fusionMode = dependencies.fusionMode ?? resolveFusionMode(env, config);
|
|
4544
5245
|
const handlerDeps = buildHandlerDeps(resolvedEnv, secrets, fallbackEnabled,
|
|
4545
5246
|
// #72: injectable-wins, mirroring configFanout above.
|
|
4546
|
-
dependencies.routing ?? config.routing, configFanout);
|
|
5247
|
+
dependencies.routing ?? config.routing, configFanout, fusionMode);
|
|
4547
5248
|
// PB-T5 — derive Plan A verification records from the loaded config
|
|
4548
5249
|
// AFTER `config` is in scope. `buildHandlerDeps` runs once BEFORE
|
|
4549
5250
|
// config load (the cache short-circuit), so this derivation cannot
|
|
@@ -4660,12 +5361,27 @@ export async function main(args, dependencies) {
|
|
|
4660
5361
|
config.journal !== false
|
|
4661
5362
|
? {}
|
|
4662
5363
|
: undefined;
|
|
5364
|
+
// M1 (glm-ocr review): journal/save chains build from the ledger-
|
|
5365
|
+
// rebuilt list so --save vision runs keep the adapter-owned
|
|
5366
|
+
// extract-text seam (marker survives the capture spread).
|
|
4663
5367
|
const journalingDescriptors = journalCapture === undefined
|
|
4664
|
-
?
|
|
4665
|
-
: captureServingDescriptors(
|
|
5368
|
+
? productionZaiLedgerDescriptors
|
|
5369
|
+
: captureServingDescriptors(productionZaiLedgerDescriptors, journalCapture);
|
|
4666
5370
|
const journalWiring = journalCapture === undefined
|
|
4667
5371
|
? undefined
|
|
4668
|
-
: {
|
|
5372
|
+
: {
|
|
5373
|
+
// investigate-pipeline T6: the run journals its UNDERLYING
|
|
5374
|
+
// search/read ops under the umbrella capability "search" —
|
|
5375
|
+
// JournalableCapability is closed over
|
|
5376
|
+
// {search,read,research,science} (lib/journal.ts is a
|
|
5377
|
+
// read-only seam for this lane) and a literal "investigate"
|
|
5378
|
+
// entry would fail asJournalEntry validation and poison the
|
|
5379
|
+
// whole log (fail-open drop + corruption notice). The
|
|
5380
|
+
// underlying ops ARE search/read; running them standalone
|
|
5381
|
+
// journals exactly so (orchestrator ruling 2026-09-20).
|
|
5382
|
+
capability: (command === "investigate" ? "search" : command),
|
|
5383
|
+
capture: journalCapture,
|
|
5384
|
+
};
|
|
4669
5385
|
const saveWiring = saveRequest === undefined || isHelpInvocation
|
|
4670
5386
|
? undefined
|
|
4671
5387
|
: buildSaveWiring(saveRequest, journalingDescriptors, journalCapture);
|
|
@@ -4770,6 +5486,13 @@ export async function main(args, dependencies) {
|
|
|
4770
5486
|
commandRecognized = true;
|
|
4771
5487
|
exitCode = await handleQuota(commandArgs, outputMode, handlerDepsWithSelection);
|
|
4772
5488
|
break;
|
|
5489
|
+
case "investigate":
|
|
5490
|
+
commandRecognized = true;
|
|
5491
|
+
// PRD AC-9: --isolated is ACCEPTED (never rejected — investigate
|
|
5492
|
+
// has no stateful dir; resumability is pure cache replay, and the
|
|
5493
|
+
// isolated/<pid> cache segment is exactly the right namespace).
|
|
5494
|
+
exitCode = await handleInvestigate(commandArgs, outputMode, handlerDepsWithSave);
|
|
5495
|
+
break;
|
|
4773
5496
|
case "code":
|
|
4774
5497
|
commandRecognized = true;
|
|
4775
5498
|
exitCode = await handleCode(commandArgs, outputMode, handlerDepsWithSelection);
|