scoutline 0.1.0 → 0.2.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.
Files changed (206) hide show
  1. package/README.md +170 -5
  2. package/bin/scoutline.js +36 -15
  3. package/dist/capabilities/diagnostics.d.ts +97 -0
  4. package/dist/capabilities/diagnostics.d.ts.map +1 -0
  5. package/dist/capabilities/diagnostics.js +88 -0
  6. package/dist/capabilities/diagnostics.js.map +1 -0
  7. package/dist/capabilities/quota.d.ts +99 -0
  8. package/dist/capabilities/quota.d.ts.map +1 -0
  9. package/dist/capabilities/quota.js +129 -0
  10. package/dist/capabilities/quota.js.map +1 -0
  11. package/dist/capabilities/search.d.ts +110 -0
  12. package/dist/capabilities/search.d.ts.map +1 -0
  13. package/dist/capabilities/search.js +21 -0
  14. package/dist/capabilities/search.js.map +1 -0
  15. package/dist/capabilities/vision.d.ts +156 -0
  16. package/dist/capabilities/vision.d.ts.map +1 -0
  17. package/dist/capabilities/vision.js +111 -0
  18. package/dist/capabilities/vision.js.map +1 -0
  19. package/dist/command-invocation.d.ts +58 -0
  20. package/dist/command-invocation.d.ts.map +1 -0
  21. package/dist/command-invocation.js +95 -0
  22. package/dist/command-invocation.js.map +1 -0
  23. package/dist/commands/code.d.ts +11 -4
  24. package/dist/commands/code.d.ts.map +1 -1
  25. package/dist/commands/code.js +16 -42
  26. package/dist/commands/code.js.map +1 -1
  27. package/dist/commands/doctor.d.ts +58 -3
  28. package/dist/commands/doctor.d.ts.map +1 -1
  29. package/dist/commands/doctor.js +154 -52
  30. package/dist/commands/doctor.js.map +1 -1
  31. package/dist/commands/quota.d.ts +45 -3
  32. package/dist/commands/quota.d.ts.map +1 -1
  33. package/dist/commands/quota.js +116 -60
  34. package/dist/commands/quota.js.map +1 -1
  35. package/dist/commands/read.d.ts +15 -1
  36. package/dist/commands/read.d.ts.map +1 -1
  37. package/dist/commands/read.js +53 -54
  38. package/dist/commands/read.js.map +1 -1
  39. package/dist/commands/repo.d.ts +8 -3
  40. package/dist/commands/repo.d.ts.map +1 -1
  41. package/dist/commands/repo.js +40 -78
  42. package/dist/commands/repo.js.map +1 -1
  43. package/dist/commands/search.d.ts +27 -2
  44. package/dist/commands/search.d.ts.map +1 -1
  45. package/dist/commands/search.js +95 -103
  46. package/dist/commands/search.js.map +1 -1
  47. package/dist/commands/tools.d.ts +9 -3
  48. package/dist/commands/tools.d.ts.map +1 -1
  49. package/dist/commands/tools.js +29 -59
  50. package/dist/commands/tools.js.map +1 -1
  51. package/dist/commands/vision.d.ts +35 -9
  52. package/dist/commands/vision.d.ts.map +1 -1
  53. package/dist/commands/vision.js +184 -131
  54. package/dist/commands/vision.js.map +1 -1
  55. package/dist/index.d.ts +42 -1
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +389 -207
  58. package/dist/index.js.map +1 -1
  59. package/dist/lib/cache.d.ts +61 -0
  60. package/dist/lib/cache.d.ts.map +1 -1
  61. package/dist/lib/cache.js +74 -11
  62. package/dist/lib/cache.js.map +1 -1
  63. package/dist/lib/code-mode.d.ts +13 -0
  64. package/dist/lib/code-mode.d.ts.map +1 -1
  65. package/dist/lib/code-mode.js +26 -6
  66. package/dist/lib/code-mode.js.map +1 -1
  67. package/dist/lib/config.d.ts.map +1 -1
  68. package/dist/lib/config.js +15 -18
  69. package/dist/lib/config.js.map +1 -1
  70. package/dist/lib/errors.d.ts +65 -10
  71. package/dist/lib/errors.d.ts.map +1 -1
  72. package/dist/lib/errors.js +135 -19
  73. package/dist/lib/errors.js.map +1 -1
  74. package/dist/lib/execution.d.ts +76 -0
  75. package/dist/lib/execution.d.ts.map +1 -0
  76. package/dist/lib/execution.js +177 -0
  77. package/dist/lib/execution.js.map +1 -0
  78. package/dist/lib/index.d.ts +10 -2
  79. package/dist/lib/index.d.ts.map +1 -1
  80. package/dist/lib/index.js +10 -2
  81. package/dist/lib/index.js.map +1 -1
  82. package/dist/lib/mcp-client.d.ts +46 -11
  83. package/dist/lib/mcp-client.d.ts.map +1 -1
  84. package/dist/lib/mcp-client.js +92 -27
  85. package/dist/lib/mcp-client.js.map +1 -1
  86. package/dist/lib/mcp-config.d.ts +17 -0
  87. package/dist/lib/mcp-config.d.ts.map +1 -1
  88. package/dist/lib/mcp-config.js +29 -0
  89. package/dist/lib/mcp-config.js.map +1 -1
  90. package/dist/lib/monitor-client.d.ts +17 -3
  91. package/dist/lib/monitor-client.d.ts.map +1 -1
  92. package/dist/lib/monitor-client.js +23 -69
  93. package/dist/lib/monitor-client.js.map +1 -1
  94. package/dist/lib/output.d.ts +53 -7
  95. package/dist/lib/output.d.ts.map +1 -1
  96. package/dist/lib/output.js +126 -39
  97. package/dist/lib/output.js.map +1 -1
  98. package/dist/lib/redact.d.ts +62 -3
  99. package/dist/lib/redact.d.ts.map +1 -1
  100. package/dist/lib/redact.js +165 -31
  101. package/dist/lib/redact.js.map +1 -1
  102. package/dist/lib/tty.d.ts +7 -20
  103. package/dist/lib/tty.d.ts.map +1 -1
  104. package/dist/lib/tty.js +51 -30
  105. package/dist/lib/tty.js.map +1 -1
  106. package/dist/node-command-invocation-adapter.d.ts +28 -0
  107. package/dist/node-command-invocation-adapter.d.ts.map +1 -0
  108. package/dist/node-command-invocation-adapter.js +88 -0
  109. package/dist/node-command-invocation-adapter.js.map +1 -0
  110. package/dist/providers/minimax/adapter.d.ts +47 -0
  111. package/dist/providers/minimax/adapter.d.ts.map +1 -0
  112. package/dist/providers/minimax/adapter.js +443 -0
  113. package/dist/providers/minimax/adapter.js.map +1 -0
  114. package/dist/providers/minimax/config.d.ts +32 -0
  115. package/dist/providers/minimax/config.d.ts.map +1 -0
  116. package/dist/providers/minimax/config.js +84 -0
  117. package/dist/providers/minimax/config.js.map +1 -0
  118. package/dist/providers/minimax/media.d.ts +34 -0
  119. package/dist/providers/minimax/media.d.ts.map +1 -0
  120. package/dist/providers/minimax/media.js +92 -0
  121. package/dist/providers/minimax/media.js.map +1 -0
  122. package/dist/providers/minimax/quota-client.d.ts +37 -0
  123. package/dist/providers/minimax/quota-client.d.ts.map +1 -0
  124. package/dist/providers/minimax/quota-client.js +78 -0
  125. package/dist/providers/minimax/quota-client.js.map +1 -0
  126. package/dist/providers/minimax/quota.d.ts +48 -0
  127. package/dist/providers/minimax/quota.d.ts.map +1 -0
  128. package/dist/providers/minimax/quota.js +120 -0
  129. package/dist/providers/minimax/quota.js.map +1 -0
  130. package/dist/providers/minimax/sdk-client.d.ts +29 -0
  131. package/dist/providers/minimax/sdk-client.d.ts.map +1 -0
  132. package/dist/providers/minimax/sdk-client.js +50 -0
  133. package/dist/providers/minimax/sdk-client.js.map +1 -0
  134. package/dist/providers/minimax/vision-attestations.d.ts +29 -0
  135. package/dist/providers/minimax/vision-attestations.d.ts.map +1 -0
  136. package/dist/providers/minimax/vision-attestations.js +54 -0
  137. package/dist/providers/minimax/vision-attestations.js.map +1 -0
  138. package/dist/providers/minimax/vision-conformance.d.ts +184 -0
  139. package/dist/providers/minimax/vision-conformance.d.ts.map +1 -0
  140. package/dist/providers/minimax/vision-conformance.js +369 -0
  141. package/dist/providers/minimax/vision-conformance.js.map +1 -0
  142. package/dist/providers/minimax/vision-mappings/chart.d.ts +35 -0
  143. package/dist/providers/minimax/vision-mappings/chart.d.ts.map +1 -0
  144. package/dist/providers/minimax/vision-mappings/chart.js +55 -0
  145. package/dist/providers/minimax/vision-mappings/chart.js.map +1 -0
  146. package/dist/providers/minimax/vision-mappings/common.d.ts +60 -0
  147. package/dist/providers/minimax/vision-mappings/common.d.ts.map +1 -0
  148. package/dist/providers/minimax/vision-mappings/common.js +75 -0
  149. package/dist/providers/minimax/vision-mappings/common.js.map +1 -0
  150. package/dist/providers/minimax/vision-mappings/diagnose-error.d.ts +35 -0
  151. package/dist/providers/minimax/vision-mappings/diagnose-error.d.ts.map +1 -0
  152. package/dist/providers/minimax/vision-mappings/diagnose-error.js +55 -0
  153. package/dist/providers/minimax/vision-mappings/diagnose-error.js.map +1 -0
  154. package/dist/providers/minimax/vision-mappings/diagram.d.ts +35 -0
  155. package/dist/providers/minimax/vision-mappings/diagram.d.ts.map +1 -0
  156. package/dist/providers/minimax/vision-mappings/diagram.js +55 -0
  157. package/dist/providers/minimax/vision-mappings/diagram.js.map +1 -0
  158. package/dist/providers/minimax/vision-mappings/extract-text.d.ts +35 -0
  159. package/dist/providers/minimax/vision-mappings/extract-text.d.ts.map +1 -0
  160. package/dist/providers/minimax/vision-mappings/extract-text.js +55 -0
  161. package/dist/providers/minimax/vision-mappings/extract-text.js.map +1 -0
  162. package/dist/providers/minimax/vision-mappings/ui-artifact.d.ts +35 -0
  163. package/dist/providers/minimax/vision-mappings/ui-artifact.d.ts.map +1 -0
  164. package/dist/providers/minimax/vision-mappings/ui-artifact.js +69 -0
  165. package/dist/providers/minimax/vision-mappings/ui-artifact.js.map +1 -0
  166. package/dist/providers/minimax/vision-mappings.generated.d.ts +29 -0
  167. package/dist/providers/minimax/vision-mappings.generated.d.ts.map +1 -0
  168. package/dist/providers/minimax/vision-mappings.generated.js +29 -0
  169. package/dist/providers/minimax/vision-mappings.generated.js.map +1 -0
  170. package/dist/providers/minimax/vision-revisions.d.ts +22 -0
  171. package/dist/providers/minimax/vision-revisions.d.ts.map +1 -0
  172. package/dist/providers/minimax/vision-revisions.js +27 -0
  173. package/dist/providers/minimax/vision-revisions.js.map +1 -0
  174. package/dist/providers/registry.d.ts +36 -0
  175. package/dist/providers/registry.d.ts.map +1 -0
  176. package/dist/providers/registry.js +45 -0
  177. package/dist/providers/registry.js.map +1 -0
  178. package/dist/providers/selection.d.ts +44 -0
  179. package/dist/providers/selection.d.ts.map +1 -0
  180. package/dist/providers/selection.js +85 -0
  181. package/dist/providers/selection.js.map +1 -0
  182. package/dist/providers/types.d.ts +247 -0
  183. package/dist/providers/types.d.ts.map +1 -0
  184. package/dist/providers/types.js +127 -0
  185. package/dist/providers/types.js.map +1 -0
  186. package/dist/providers/zai/adapter.d.ts +35 -0
  187. package/dist/providers/zai/adapter.d.ts.map +1 -0
  188. package/dist/providers/zai/adapter.js +619 -0
  189. package/dist/providers/zai/adapter.js.map +1 -0
  190. package/dist/providers/zai/credentials.d.ts +38 -0
  191. package/dist/providers/zai/credentials.d.ts.map +1 -0
  192. package/dist/providers/zai/credentials.js +60 -0
  193. package/dist/providers/zai/credentials.js.map +1 -0
  194. package/dist/providers/zai/media.d.ts +38 -0
  195. package/dist/providers/zai/media.d.ts.map +1 -0
  196. package/dist/providers/zai/media.js +107 -0
  197. package/dist/providers/zai/media.js.map +1 -0
  198. package/dist/providers/zai/monitor-client.d.ts +53 -0
  199. package/dist/providers/zai/monitor-client.d.ts.map +1 -0
  200. package/dist/providers/zai/monitor-client.js +117 -0
  201. package/dist/providers/zai/monitor-client.js.map +1 -0
  202. package/dist/providers/zai/quota.d.ts +47 -0
  203. package/dist/providers/zai/quota.d.ts.map +1 -0
  204. package/dist/providers/zai/quota.js +127 -0
  205. package/dist/providers/zai/quota.js.map +1 -0
  206. package/package.json +10 -5
package/dist/index.js CHANGED
@@ -6,34 +6,50 @@ import { search, SEARCH_HELP } from "./commands/search.js";
6
6
  import { read, READ_HELP } from "./commands/read.js";
7
7
  import { repoSearch, repoTree, repoRead, REPO_HELP } from "./commands/repo.js";
8
8
  import { listTools, showTool, callTool, TOOLS_HELP, CALL_HELP } from "./commands/tools.js";
9
- import { doctor, DOCTOR_HELP } from "./commands/doctor.js";
10
- import { quota, QUOTA_HELP } from "./commands/quota.js";
9
+ import { doctor, buildDiagnosticsReport, DOCTOR_HELP } from "./commands/doctor.js";
10
+ import { quota, buildQuotaDashboard, QUOTA_HELP } from "./commands/quota.js";
11
11
  import { isExtractMode } from "./lib/extract.js";
12
12
  import { runCodeFile, evalCode, printInterfaces, printPromptTemplate, CODE_HELP, } from "./commands/code.js";
13
- import { outputError, setOutputMode } from "./lib/output.js";
13
+ import { isOutputMode, OUTPUT_MODES } from "./lib/output.js";
14
+ import { formatErrorOutput } from "./lib/output.js";
15
+ import { ConfigurationError, ValidationError, UnsupportedCapabilityError, getErrorExitCode, } from "./lib/errors.js";
16
+ import { invokeCommand } from "./command-invocation.js";
17
+ import { defaultResponseCache } from "./lib/cache.js";
18
+ import { configuredSecrets } from "./lib/redact.js";
19
+ import { resolveProviderId } from "./providers/selection.js";
20
+ import { BUILT_IN_PROVIDER_DESCRIPTORS, getProviderDescriptor } from "./providers/registry.js";
21
+ import { visionOperationToCapability } from "./capabilities/vision.js";
14
22
  import { createRequire } from "node:module";
15
23
  const require = createRequire(import.meta.url);
16
24
  const { version: VERSION } = require("../package.json");
17
25
  const MAIN_HELP = `
18
- scoutline v${VERSION} - CLI for Z.AI MCP services
26
+ scoutline v${VERSION} - Multimodal source investigation CLI
19
27
 
20
28
  Usage: scoutline <command> [args] [options]
21
29
 
22
30
  Commands:
23
- vision Image and video analysis (MCP)
24
- search Real-time web search
25
- read Fetch and parse web pages
26
- repo GitHub repository exploration
27
- quota Authoritative Z.AI plan usage (calls remaining, reset time)
28
- tools List available MCP tools
29
- tool Show a tool schema
30
- call Call a tool directly
31
- doctor Environment + connectivity checks
32
- code Execute TypeScript tool chains (Code Mode)
31
+ vision Image and video analysis (Z.AI; MiniMax for interpret-image)
32
+ search Real-time web search (shared: Z.AI + MiniMax)
33
+ read Fetch and parse web pages (Z.AI only)
34
+ repo GitHub repository exploration (Z.AI only)
35
+ quota Provider-aware plan usage (calls remaining, reset time)
36
+ tools List available MCP tools (Z.AI)
37
+ tool Show a tool schema (Z.AI)
38
+ call Call a tool directly (Z.AI)
39
+ doctor Provider-aware environment + connectivity checks
40
+ code Execute TypeScript tool chains (Code Mode, Z.AI)
41
+
42
+ Provider selection (precedence: --provider, then SCOUTLINE_PROVIDER, then zai):
43
+ --provider <zai|minimax> Select the active Provider for shared capabilities
44
+ SCOUTLINE_PROVIDER=<id> Fallback when --provider is not passed
45
+
46
+ Shared capabilities accept --provider. Z.AI-only commands (read, repo,
47
+ tools, tool, call, code) carry the flag but ignore it. Quota and doctor
48
+ report per-Provider; --provider picks the effective Provider for metadata.
33
49
 
34
50
  Global Options:
35
- --output-format <data|json|pretty|compact|markdown|refs> Output mode (default: data)
36
- -O <mode> Alias for --output-format
51
+ --output-format <data|json|pretty|compact|markdown|refs|tty> Output mode (default: data)
52
+ -O <mode> Alias for --output-format
37
53
 
38
54
  Help:
39
55
  scoutline --help
@@ -93,6 +109,7 @@ function extractGlobalOptions(args) {
93
109
  let outputFormat;
94
110
  let forcePretty = false;
95
111
  let forceRaw = false;
112
+ let provider;
96
113
  for (let i = 0; i < args.length; i += 1) {
97
114
  const arg = args[i];
98
115
  if (arg === "--output-format" || arg === "-O") {
@@ -108,90 +125,208 @@ function extractGlobalOptions(args) {
108
125
  forceRaw = true;
109
126
  continue;
110
127
  }
128
+ if (arg === "--provider") {
129
+ // Global option: accepted before OR after the command token. It is
130
+ // removed from the rest stream so command-local positional parsing
131
+ // never observes it. Only shared Search resolves/validates it; the
132
+ // Z.AI-only command families carry it but never consult it.
133
+ provider = args[i + 1];
134
+ i += 1;
135
+ continue;
136
+ }
111
137
  rest.push(arg);
112
138
  }
113
- return { outputFormat, forcePretty, forceRaw, rest };
139
+ return { outputFormat, forcePretty, forceRaw, provider, rest };
114
140
  }
115
- function applyOutputMode(mode) {
116
- const valid = ["data", "json", "pretty", "compact", "markdown", "refs", "tty"];
117
- if (!mode)
118
- return;
119
- if (valid.includes(mode)) {
120
- setOutputMode(mode);
121
- return;
122
- }
123
- outputError(`Invalid output format: ${mode}`, "INVALID_ARGS", `Use one of: ${valid.join(", ")}`);
141
+ function resolveOutputMode(explicit, forcePretty, forceRaw, adapter) {
142
+ if (explicit !== undefined) {
143
+ if (!isOutputMode(explicit)) {
144
+ throw new ValidationError(`Invalid output format: ${explicit}`, `Use one of: ${OUTPUT_MODES.join(", ")}`);
145
+ }
146
+ return explicit;
147
+ }
148
+ if (forcePretty)
149
+ return "tty";
150
+ if (forceRaw)
151
+ return "data";
152
+ const envMode = adapter.environmentOutputMode;
153
+ if (typeof envMode === "string" && isOutputMode(envMode)) {
154
+ return envMode;
155
+ }
156
+ if (adapter.stdoutIsTTY)
157
+ return "tty";
158
+ return "data";
124
159
  }
125
- async function handleVision(args) {
160
+ async function handleVision(args, outputMode, deps) {
126
161
  const { flags, positional } = parseArgs(args);
127
162
  if (flags.help || flags.h || positional.length === 0) {
128
- console.log(vision.VISION_HELP);
129
- return;
163
+ deps.invocation.writeStdout(vision.VISION_HELP);
164
+ return 0;
130
165
  }
131
166
  const command = positional[0];
132
167
  const source = positional[1];
133
168
  const prompt = positional[2];
169
+ // Map the subcommand to its Vision operation. Unknown subcommands are
170
+ // a parse-time VALIDATION_ERROR and are rejected before any Provider
171
+ // resolution, support check, or media access.
172
+ const operation = visionOperationForCommand(command);
173
+ // Resolve the effective Provider for Vision (DESIGN.md §6). Invalid
174
+ // explicit/env input fails here with VALIDATION_ERROR before any Vision
175
+ // support check or media access. Selection never consults credentials
176
+ // (FR-003); the configured check below is the caller's responsibility.
177
+ const providerId = resolveProviderId(deps.provider, deps.env);
178
+ const descriptor = getProviderDescriptor(providerId, deps.providerDescriptors);
179
+ // Gate the operation on descriptor metadata BEFORE Adapter construction.
180
+ // An unsupported operation (e.g. MiniMax for any specialized op, diff, or
181
+ // video) fails with UNSUPPORTED_CAPABILITY before credentials, media,
182
+ // transport, cache, or any Z.AI fallback (FR-023, FR-024). No command
183
+ // branches on a Provider ID: the support check alone decides availability.
184
+ const capabilityId = visionOperationToCapability(operation);
185
+ if (!descriptor.capabilities().has(capabilityId)) {
186
+ throw new UnsupportedCapabilityError(providerId, capabilityId);
187
+ }
188
+ // FR-003: selection returns the default zai even when unconfigured. The
189
+ // dispatch layer surfaces a missing credential as ConfigurationError
190
+ // (exit 3), AFTER the capability support check (FR-023) but before any
191
+ // Adapter construction or media access (Fixup A — B5).
192
+ if (!descriptor.isConfigured(deps.env)) {
193
+ throw new ConfigurationError(`Provider "${providerId}" is not configured. Set the required API key.`);
194
+ }
195
+ const adapter = descriptor.create({ env: deps.env });
196
+ const visionCapability = adapter.vision;
197
+ if (!visionCapability || !visionCapability.supports(operation)) {
198
+ throw new UnsupportedCapabilityError(providerId, capabilityId);
199
+ }
200
+ // Vision bypasses the response cache (FR-022). The shared execution
201
+ // primitives (sleep/random) are the same ones Search consumes; they
202
+ // drive retry backoff deterministically under test.
203
+ const visionDeps = {
204
+ capability: visionCapability,
205
+ sleep: deps.searchSleep,
206
+ random: deps.searchRandom,
207
+ };
134
208
  switch (command) {
135
209
  case "analyze":
136
- if (!source) {
137
- outputError("Missing image source", "INVALID_ARGS", "Usage: scoutline vision analyze <image> [prompt]");
138
- }
139
- await vision.analyze(source, prompt);
140
- break;
141
- case "ui-to-code":
142
- if (!source) {
143
- outputError("Missing image source", "INVALID_ARGS", "Usage: scoutline vision ui-to-code <image> [prompt]");
144
- }
210
+ return invokeCommand(deps.invocation, (context) => vision.analyze(source, prompt, visionDeps, context), outputMode, deps.now, deps.secrets);
211
+ case "ui-to-code": {
145
212
  const outputType = flags.output || "code";
146
- await vision.uiToCode(source, prompt, outputType);
147
- break;
213
+ return invokeCommand(deps.invocation, (context) => vision.uiToCode(source, prompt, outputType, visionDeps, context), outputMode, deps.now, deps.secrets);
214
+ }
148
215
  case "extract-text":
149
- if (!source) {
150
- outputError("Missing image source", "INVALID_ARGS", "Usage: scoutline vision extract-text <image> [prompt] [--language <lang>]");
151
- }
152
- await vision.extractText(source, prompt, flags.language);
153
- break;
216
+ return invokeCommand(deps.invocation, (context) => vision.extractText(source, prompt, flags.language, visionDeps, context), outputMode, deps.now, deps.secrets);
154
217
  case "diagnose-error":
155
- if (!source) {
156
- outputError("Missing image source", "INVALID_ARGS", "Usage: scoutline vision diagnose-error <image> [prompt] [--context <ctx>]");
157
- }
158
- await vision.diagnoseError(source, prompt, flags.context);
159
- break;
218
+ return invokeCommand(deps.invocation, (context) => vision.diagnoseError(source, prompt, flags.context, visionDeps, context), outputMode, deps.now, deps.secrets);
160
219
  case "diagram":
161
- if (!source) {
162
- outputError("Missing image source", "INVALID_ARGS", "Usage: scoutline vision diagram <image> [prompt] [--type <type>]");
163
- }
164
- await vision.diagram(source, prompt, flags.type);
165
- break;
220
+ return invokeCommand(deps.invocation, (context) => vision.diagram(source, prompt, flags.type, visionDeps, context), outputMode, deps.now, deps.secrets);
166
221
  case "chart":
167
- if (!source) {
168
- outputError("Missing image source", "INVALID_ARGS", "Usage: scoutline vision chart <image> [prompt] [--focus <focus>]");
169
- }
170
- await vision.chart(source, prompt, flags.focus);
171
- break;
172
- case "diff":
222
+ return invokeCommand(deps.invocation, (context) => vision.chart(source, prompt, flags.focus, visionDeps, context), outputMode, deps.now, deps.secrets);
223
+ case "diff": {
173
224
  const actual = positional[2];
174
225
  const diffPrompt = positional[3];
175
- if (!source || !actual) {
176
- outputError("Missing image sources", "INVALID_ARGS", "Usage: scoutline vision diff <expected> <actual> [prompt]");
177
- }
178
- await vision.diff(source, actual, diffPrompt);
179
- break;
226
+ return invokeCommand(deps.invocation, (context) => vision.diff(source, actual, diffPrompt, visionDeps, context), outputMode, deps.now, deps.secrets);
227
+ }
180
228
  case "video":
181
- if (!source) {
182
- outputError("Missing video source", "INVALID_ARGS", "Usage: scoutline vision video <video> [prompt]");
183
- }
184
- await vision.video(source, prompt);
185
- break;
229
+ return invokeCommand(deps.invocation, (context) => vision.video(source, prompt, visionDeps, context), outputMode, deps.now, deps.secrets);
230
+ default:
231
+ throw new ValidationError(`Unknown vision command: ${command}`, 'Run "scoutline vision --help" for available commands');
232
+ }
233
+ }
234
+ /**
235
+ * Map a Vision subcommand to its discriminated operation id. Used by
236
+ * `handleVision` to gate the operation against descriptor metadata before
237
+ * Adapter construction. Unknown subcommands throw `ValidationError`.
238
+ */
239
+ function visionOperationForCommand(command) {
240
+ switch (command) {
241
+ case "analyze":
242
+ return "interpret-image";
243
+ case "ui-to-code":
244
+ return "ui-artifact";
245
+ case "extract-text":
246
+ return "extract-text";
247
+ case "diagnose-error":
248
+ return "diagnose-error";
249
+ case "diagram":
250
+ return "diagram";
251
+ case "chart":
252
+ return "chart";
253
+ case "diff":
254
+ return "diff";
255
+ case "video":
256
+ return "video";
186
257
  default:
187
- outputError(`Unknown vision command: ${command}`, "INVALID_ARGS", 'Run "scoutline vision --help" for available commands');
258
+ throw new ValidationError(`Unknown vision command: ${command}`, 'Run "scoutline vision --help" for available commands');
259
+ }
260
+ }
261
+ /**
262
+ * Parse and validate the `--count` flag value (Fixup C — B11, Fixup D). Per
263
+ * DESIGN.md §7, count must be a safe integer >= 0. Invalid values (NaN,
264
+ * negative, non-integer, Infinity, values above Number.MAX_SAFE_INTEGER)
265
+ * throw `ValidationError` BEFORE any Provider resolution or invocation.
266
+ *
267
+ * Fixup D hardens two gaps:
268
+ * - `--count` without a value parses to `true`; that is a user error,
269
+ * not an absent flag, and now throws VALIDATION_ERROR instead of being
270
+ * silently treated as absent.
271
+ * - Uses `Number.isSafeInteger` instead of `Number.isFinite` +
272
+ * `Number.isInteger` so values above 2^53-1 are rejected rather than
273
+ * silently rounded.
274
+ *
275
+ * Exported for testing so the validation can be exercised without going
276
+ * through the CLI parser (which does not deliver negative numbers as flag
277
+ * values today).
278
+ */
279
+ export function parseAndValidateCount(raw) {
280
+ if (raw === undefined || raw === "")
281
+ return undefined;
282
+ if (raw === true) {
283
+ throw new ValidationError("Count requires a numeric value.", "Use a non-negative integer (e.g. --count 5).");
284
+ }
285
+ const str = typeof raw === "string" ? raw : String(raw);
286
+ if (!/^\d+$/.test(str)) {
287
+ throw new ValidationError(`Invalid --count value "${str}": must be a non-negative integer`, "Use a non-negative integer (e.g. --count 5).");
188
288
  }
289
+ const parsed = Number(str);
290
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
291
+ throw new ValidationError(`Invalid --count value "${str}": must be a non-negative safe integer`, "Use a non-negative integer (e.g. --count 5).");
292
+ }
293
+ return parsed;
189
294
  }
190
- async function handleSearch(args) {
295
+ async function handleSearch(args, outputMode, deps) {
191
296
  const { flags, positional } = parseArgs(args);
192
297
  if (flags.help || flags.h || positional.length === 0) {
193
- console.log(SEARCH_HELP);
194
- return;
298
+ deps.invocation.writeStdout(SEARCH_HELP);
299
+ return 0;
300
+ }
301
+ // Fixup D — B11: validate --count BEFORE Provider resolution and the
302
+ // configured/credential check. A syntax error in a CLI argument must
303
+ // not depend on whether credentials are present: `search q --count nope`
304
+ // with NO credentials must surface VALIDATION_ERROR (exit 1), not
305
+ // CONFIGURATION_ERROR (exit 3). Order: parse global options -> validate
306
+ // count -> resolve provider -> check configured -> dispatch.
307
+ const count = parseAndValidateCount(flags.count);
308
+ // Resolve the Provider ONLY inside shared Search (DESIGN.md §6). Other
309
+ // command families carry the parsed flag but never resolve or validate
310
+ // it. An invalid explicit/env value throws VALIDATION_ERROR here, before
311
+ // any Adapter construction or invocation. Selection never consults
312
+ // credentials (FR-003); the configured check below is the caller's
313
+ // responsibility (Fixup A — B5).
314
+ const providerId = resolveProviderId(deps.provider, deps.env);
315
+ const descriptor = getProviderDescriptor(providerId, deps.providerDescriptors);
316
+ if (!descriptor.capabilities().has("search")) {
317
+ throw new UnsupportedCapabilityError(providerId, "search");
318
+ }
319
+ // FR-003: selection returns the default zai even when unconfigured. The
320
+ // dispatch layer surfaces a missing credential as ConfigurationError
321
+ // (exit 3), AFTER the capability support check (FR-023) but before any
322
+ // Adapter construction (Fixup A — B5).
323
+ if (!descriptor.isConfigured(deps.env)) {
324
+ throw new ConfigurationError(`Provider "${providerId}" is not configured. Set the required API key.`);
325
+ }
326
+ const adapter = descriptor.create({ env: deps.env });
327
+ const capability = adapter.search;
328
+ if (!capability) {
329
+ throw new UnsupportedCapabilityError(providerId, "search");
195
330
  }
196
331
  const query = positional.join(" ");
197
332
  const fieldsRaw = flags.fields;
@@ -201,26 +336,33 @@ async function handleSearch(args) {
201
336
  .map((f) => f.trim())
202
337
  .filter(Boolean)
203
338
  : undefined;
204
- await search(query, {
205
- count: flags.count ? parseInt(flags.count, 10) : undefined,
339
+ return invokeCommand(deps.invocation, (context) => search(query, {
340
+ count,
206
341
  domain: flags.domain,
207
342
  recency: flags.recency,
208
343
  contentSize: flags["content-size"],
209
344
  location: flags.location,
210
- maxSummary: flags["max-summary"] ? parseInt(flags["max-summary"], 10) : undefined,
345
+ maxSummary: flags["max-summary"]
346
+ ? parseInt(flags["max-summary"], 10)
347
+ : undefined,
211
348
  fields: fields && fields.length > 0 ? fields : undefined,
212
349
  noCache: flags["no-cache"] === true,
213
350
  merge: flags.merge === true,
214
- });
351
+ }, {
352
+ capability,
353
+ cache: deps.searchCache,
354
+ sleep: deps.searchSleep,
355
+ random: deps.searchRandom,
356
+ }, context), outputMode, deps.now, deps.secrets);
215
357
  }
216
- async function handleRead(args) {
358
+ async function handleRead(args, outputMode, deps) {
217
359
  const { flags, positional } = parseArgs(args);
218
360
  if (flags.help || flags.h || positional.length === 0) {
219
- console.log(READ_HELP);
220
- return;
361
+ deps.invocation.writeStdout(READ_HELP);
362
+ return 0;
221
363
  }
222
364
  const url = positional[0];
223
- await read(url, {
365
+ return invokeCommand(deps.invocation, (context) => read(url, {
224
366
  format: flags.format,
225
367
  noImages: flags["no-images"] === true,
226
368
  noCache: flags["no-cache"] === true,
@@ -231,112 +373,142 @@ async function handleRead(args) {
231
373
  withImagesSummary: flags["with-images-summary"] === true,
232
374
  maxChars: flags["max-chars"] ? parseInt(flags["max-chars"], 10) : undefined,
233
375
  fullEnvelope: flags["full-envelope"] === true,
234
- extract: isExtractMode(flags.extract) ? flags.extract : undefined,
235
- });
376
+ extract: isExtractMode(flags.extract)
377
+ ? flags.extract
378
+ : undefined,
379
+ }, outputMode, context), outputMode, deps.now, deps.secrets);
236
380
  }
237
- async function handleRepo(args) {
381
+ async function handleRepo(args, outputMode, deps) {
238
382
  const { flags, positional } = parseArgs(args);
239
383
  if (flags.help || flags.h || positional.length === 0) {
240
- console.log(REPO_HELP);
241
- return;
384
+ deps.invocation.writeStdout(REPO_HELP);
385
+ return 0;
242
386
  }
243
387
  const command = positional[0];
244
388
  const repo = positional[1];
245
389
  switch (command) {
246
- case "search":
390
+ case "search": {
247
391
  const query = positional.slice(2).join(" ");
248
392
  if (!repo || !query) {
249
- outputError("Missing repo or query", "INVALID_ARGS", "Usage: scoutline repo search <owner/repo> <query>");
393
+ throw new ValidationError("Missing repo or query", "Usage: scoutline repo search <owner/repo> <query>");
250
394
  }
251
- await repoSearch(repo, query, {
395
+ return invokeCommand(deps.invocation, (context) => repoSearch(repo, query, {
252
396
  language: flags.language,
253
397
  maxChars: flags["max-chars"] ? parseInt(flags["max-chars"], 10) : undefined,
254
398
  noCache: flags["no-cache"] === true,
255
- });
256
- break;
257
- case "tree":
399
+ }, context), outputMode, deps.now, deps.secrets);
400
+ }
401
+ case "tree": {
258
402
  if (!repo) {
259
- outputError("Missing repo", "INVALID_ARGS", "Usage: scoutline repo tree <owner/repo>");
403
+ throw new ValidationError("Missing repo", "Usage: scoutline repo tree <owner/repo>");
260
404
  }
261
- await repoTree(repo, {
405
+ return invokeCommand(deps.invocation, (context) => repoTree(repo, {
262
406
  path: flags.path,
263
407
  depth: flags.depth ? parseInt(flags.depth, 10) : undefined,
264
408
  noCache: flags["no-cache"] === true,
265
- });
266
- break;
267
- case "read":
409
+ }, context), outputMode, deps.now, deps.secrets);
410
+ }
411
+ case "read": {
268
412
  const path = positional[2];
269
413
  if (!repo || !path) {
270
- outputError("Missing repo or path", "INVALID_ARGS", "Usage: scoutline repo read <owner/repo> <path>");
414
+ throw new ValidationError("Missing repo or path", "Usage: scoutline repo read <owner/repo> <path>");
271
415
  }
272
- await repoRead(repo, path, {
416
+ return invokeCommand(deps.invocation, (context) => repoRead(repo, path, {
273
417
  maxChars: flags["max-chars"] ? parseInt(flags["max-chars"], 10) : undefined,
274
418
  noCache: flags["no-cache"] === true,
275
- });
276
- break;
419
+ }, context), outputMode, deps.now, deps.secrets);
420
+ }
277
421
  default:
278
- outputError(`Unknown repo command: ${command}`, "INVALID_ARGS", 'Run "scoutline repo --help" for available commands');
422
+ throw new ValidationError(`Unknown repo command: ${command}`, 'Run "scoutline repo --help" for available commands');
279
423
  }
280
424
  }
281
- async function handleTools(args) {
425
+ async function handleTools(args, outputMode, deps) {
282
426
  const { flags } = parseArgs(args);
283
427
  if (flags.help || flags.h) {
284
- console.log(TOOLS_HELP);
285
- return;
428
+ deps.invocation.writeStdout(TOOLS_HELP);
429
+ return 0;
286
430
  }
287
- await listTools({
431
+ return invokeCommand(deps.invocation, (context) => listTools({
288
432
  filter: flags.filter,
289
433
  full: flags.full === true,
290
434
  typescript: flags.typescript === true || flags.ts === true,
291
435
  enableVision: flags.vision !== false,
292
- });
436
+ }, context), outputMode, deps.now, deps.secrets);
293
437
  }
294
- async function handleTool(args) {
438
+ async function handleTool(args, outputMode, deps) {
295
439
  const { flags, positional } = parseArgs(args);
296
440
  if (flags.help || flags.h || positional.length === 0) {
297
- console.log(TOOLS_HELP);
298
- return;
441
+ deps.invocation.writeStdout(TOOLS_HELP);
442
+ return 0;
299
443
  }
300
- await showTool(positional[0], { enableVision: flags.vision !== false });
444
+ return invokeCommand(deps.invocation, (context) => showTool(positional[0], { enableVision: flags.vision !== false }, context), outputMode, deps.now, deps.secrets);
301
445
  }
302
- async function handleCall(args) {
446
+ async function handleCall(args, outputMode, deps) {
303
447
  const { flags, positional } = parseArgs(args);
304
448
  if (flags.help || flags.h || positional.length === 0) {
305
- console.log(CALL_HELP);
306
- return;
449
+ deps.invocation.writeStdout(CALL_HELP);
450
+ return 0;
307
451
  }
308
- await callTool(positional[0], {
452
+ return invokeCommand(deps.invocation, (context) => callTool(positional[0], {
309
453
  json: flags.json,
310
454
  file: flags.file,
311
455
  stdin: flags.stdin === true,
312
456
  dryRun: flags["dry-run"] === true,
313
457
  enableVision: flags.vision !== false,
314
- });
458
+ }, context), outputMode, deps.now, deps.secrets);
315
459
  }
316
- async function handleDoctor(args) {
460
+ async function handleDoctor(args, outputMode, deps) {
317
461
  const { flags } = parseArgs(args);
318
462
  if (flags.help || flags.h) {
319
- console.log(DOCTOR_HELP);
320
- return;
463
+ deps.invocation.writeStdout(DOCTOR_HELP);
464
+ return 0;
321
465
  }
322
- await doctor({
323
- noTools: flags["no-tools"] === true,
324
- enableVision: flags.vision !== false,
325
- });
466
+ const noTools = flags["no-tools"] === true;
467
+ // Resolve the effective Provider ID for report metadata, mirroring
468
+ // Search/Vision/quota. Descriptors are intentionally NOT passed here so
469
+ // the report always lists every built-in Provider even when the
470
+ // effective Provider is unconfigured.
471
+ const effectiveProvider = resolveProviderId(deps.provider, deps.env);
472
+ return invokeCommand(deps.invocation, () => doctor({
473
+ buildReport: () => buildDiagnosticsReport({
474
+ noTools,
475
+ effectiveProvider,
476
+ descriptors: deps.providerDescriptors,
477
+ env: deps.env,
478
+ sleep: deps.searchSleep,
479
+ random: deps.searchRandom,
480
+ }),
481
+ }), outputMode, deps.now, deps.secrets);
326
482
  }
327
- async function handleQuota(args) {
483
+ async function handleQuota(args, outputMode, deps) {
328
484
  const { flags } = parseArgs(args);
329
485
  if (flags.help || flags.h) {
330
- console.log(QUOTA_HELP);
331
- return;
486
+ deps.invocation.writeStdout(QUOTA_HELP);
487
+ return 0;
332
488
  }
333
- await quota({});
489
+ const allProviders = flags["all-providers"] === true;
490
+ // Resolve the effective Provider ID for dashboard metadata. Config
491
+ // validation is owned by the dashboard builder (ConfigurationError,
492
+ // exit 3) so an unconfigured default is reported as configuration, not
493
+ // a registry error. Descriptors are intentionally NOT passed here so
494
+ // all-provider mode is not blocked by an unconfigured effective.
495
+ const effectiveProvider = resolveProviderId(deps.provider, deps.env);
496
+ return invokeCommand(deps.invocation, () => quota({
497
+ buildDashboard: () => buildQuotaDashboard({
498
+ allProviders,
499
+ effectiveProvider,
500
+ descriptors: deps.providerDescriptors,
501
+ env: deps.env,
502
+ sleep: deps.searchSleep,
503
+ random: deps.searchRandom,
504
+ }),
505
+ }), outputMode, deps.now, deps.secrets);
334
506
  }
335
- async function handleCode(args) {
507
+ async function handleCode(args, outputMode, deps) {
336
508
  const { flags, positional } = parseArgs(args);
337
509
  if (flags.help || flags.h || positional.length === 0) {
338
- console.log(CODE_HELP);
339
- return;
510
+ deps.invocation.writeStdout(CODE_HELP);
511
+ return 0;
340
512
  }
341
513
  const command = positional[0];
342
514
  const timeout = flags.timeout ? parseInt(flags.timeout, 10) : undefined;
@@ -345,99 +517,109 @@ async function handleCode(args) {
345
517
  case "run": {
346
518
  const filePath = positional[1];
347
519
  if (!filePath) {
348
- outputError("Missing code file", "INVALID_ARGS", "Usage: scoutline code run <file>");
520
+ throw new ValidationError("Missing code file", "Usage: scoutline code run <file>");
349
521
  }
350
- await runCodeFile(filePath, { timeout, includeLogs });
351
- break;
522
+ return invokeCommand(deps.invocation, (context) => runCodeFile(filePath, { timeout, includeLogs }, context), outputMode, deps.now, deps.secrets);
352
523
  }
353
524
  case "eval": {
354
525
  const code = positional.slice(1).join(" ");
355
526
  if (!code) {
356
- outputError("Missing code string", "INVALID_ARGS", "Usage: scoutline code eval <code>");
527
+ throw new ValidationError("Missing code string", "Usage: scoutline code eval <code>");
357
528
  }
358
- await evalCode(code, { timeout, includeLogs });
359
- break;
529
+ return invokeCommand(deps.invocation, (context) => evalCode(code, { timeout, includeLogs }, context), outputMode, deps.now, deps.secrets);
360
530
  }
361
531
  case "interfaces":
362
- await printInterfaces();
363
- break;
532
+ return invokeCommand(deps.invocation, (context) => printInterfaces(context), outputMode, deps.now, deps.secrets);
364
533
  case "prompt":
365
- printPromptTemplate();
366
- break;
534
+ return invokeCommand(deps.invocation, async (context) => printPromptTemplate(context), outputMode, deps.now, deps.secrets);
367
535
  default:
368
- outputError(`Unknown code command: ${command}`, "INVALID_ARGS", 'Run "scoutline code --help" for available commands');
536
+ throw new ValidationError(`Unknown code command: ${command}`, 'Run "scoutline code --help" for available commands');
369
537
  }
370
538
  }
371
- export async function main(args) {
372
- const cleanArgs = args[0]?.includes("node") || args[0]?.includes("scoutline") ? args.slice(2) : args;
373
- const { outputFormat, forcePretty, forceRaw, rest } = extractGlobalOptions(cleanArgs);
374
- // Resolve effective mode: explicit -O wins; else --pretty-output / --raw;
375
- // else auto: "tty" when stdout is a TTY, "data" when piped.
376
- if (outputFormat) {
377
- applyOutputMode(outputFormat);
378
- }
379
- else if (forcePretty) {
380
- setOutputMode("tty");
381
- }
382
- else if (forceRaw) {
383
- setOutputMode("data");
384
- }
385
- else if (process.stdout.isTTY) {
386
- setOutputMode("tty");
387
- } // else: default "data" mode (set at module load in output.ts)
539
+ const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
540
+ export async function main(args, dependencies) {
541
+ const { invocation, env, now } = dependencies;
542
+ const providerDescriptors = dependencies.providerDescriptors ?? BUILT_IN_PROVIDER_DESCRIPTORS;
543
+ const searchCache = dependencies.searchCache ?? defaultResponseCache;
544
+ const searchSleep = dependencies.searchSleep ?? realSleep;
545
+ const searchRandom = dependencies.searchRandom ?? Math.random;
546
+ // Resolve configured Provider credentials from the INJECTED env (B3) so
547
+ // redaction follows the same environment the handlers see — a secret
548
+ // that exists only in MainDependencies.env is still redacted from output.
549
+ const secrets = configuredSecrets(env);
550
+ const { outputFormat, forcePretty, forceRaw, provider, rest } = extractGlobalOptions([...args]);
551
+ // Fixup C — B10: resolve the output mode BEFORE the dispatch try/catch.
552
+ // An invalid explicit mode still surfaces as a typed ValidationError,
553
+ // but the surface formatter uses the user's REQUESTED mode (or the
554
+ // best deterministic fallback) so the envelope matches what the user
555
+ // asked for. Pre-invocation validation errors (provider resolution,
556
+ // missing credentials, count parsing, etc.) MUST honour the requested
557
+ // output mode the same way handler errors do.
558
+ let outputMode;
559
+ try {
560
+ outputMode = resolveOutputMode(outputFormat, forcePretty ?? false, forceRaw ?? false, invocation);
561
+ }
562
+ catch (error) {
563
+ // The explicit mode is invalid — fall back to a deterministic
564
+ // compact form so we can still surface a structured error envelope.
565
+ invocation.writeStderr(formatErrorOutput(error, "data", secrets));
566
+ return getErrorExitCode(error);
567
+ }
388
568
  if (rest.length === 0 || rest[0] === "--help" || rest[0] === "-h") {
389
- console.log(MAIN_HELP);
390
- return;
569
+ invocation.writeStdout(MAIN_HELP);
570
+ return 0;
391
571
  }
392
572
  if (rest[0] === "--version" || rest[0] === "-v") {
393
- console.log(VERSION);
394
- return;
573
+ invocation.writeStdout(VERSION);
574
+ return 0;
395
575
  }
396
576
  const command = rest[0];
397
577
  const commandArgs = rest.slice(1);
398
- switch (command) {
399
- case "vision":
400
- await handleVision(commandArgs);
401
- break;
402
- case "search":
403
- await handleSearch(commandArgs);
404
- break;
405
- case "read":
406
- await handleRead(commandArgs);
407
- break;
408
- case "repo":
409
- await handleRepo(commandArgs);
410
- break;
411
- case "tools":
412
- await handleTools(commandArgs);
413
- break;
414
- case "tool":
415
- await handleTool(commandArgs);
416
- break;
417
- case "call":
418
- await handleCall(commandArgs);
419
- break;
420
- case "doctor":
421
- await handleDoctor(commandArgs);
422
- break;
423
- case "quota":
424
- await handleQuota(commandArgs);
425
- break;
426
- case "code":
427
- await handleCode(commandArgs);
428
- break;
429
- default:
430
- outputError(`Unknown command: ${command}`, "INVALID_ARGS", 'Run "scoutline --help" for available commands');
578
+ const handlerDeps = {
579
+ invocation,
580
+ env,
581
+ secrets,
582
+ now,
583
+ provider,
584
+ providerDescriptors,
585
+ searchCache,
586
+ searchSleep,
587
+ searchRandom,
588
+ };
589
+ try {
590
+ switch (command) {
591
+ case "vision":
592
+ return await handleVision(commandArgs, outputMode, handlerDeps);
593
+ case "search":
594
+ return await handleSearch(commandArgs, outputMode, handlerDeps);
595
+ case "read":
596
+ return await handleRead(commandArgs, outputMode, handlerDeps);
597
+ case "repo":
598
+ return await handleRepo(commandArgs, outputMode, handlerDeps);
599
+ case "tools":
600
+ return await handleTools(commandArgs, outputMode, handlerDeps);
601
+ case "tool":
602
+ return await handleTool(commandArgs, outputMode, handlerDeps);
603
+ case "call":
604
+ return await handleCall(commandArgs, outputMode, handlerDeps);
605
+ case "doctor":
606
+ return await handleDoctor(commandArgs, outputMode, handlerDeps);
607
+ case "quota":
608
+ return await handleQuota(commandArgs, outputMode, handlerDeps);
609
+ case "code":
610
+ return await handleCode(commandArgs, outputMode, handlerDeps);
611
+ default:
612
+ invocation.writeStderr(formatErrorOutput(new ValidationError(`Unknown command: ${command}`, 'Run "scoutline --help" for available commands'), outputMode, secrets));
613
+ return 1;
614
+ }
615
+ }
616
+ catch (error) {
617
+ // Fixup C — B10: pre-invocation validation errors (provider
618
+ // resolution, missing credential, count parsing, etc.) MUST be
619
+ // formatted in the resolved output mode — they used to be hardcoded
620
+ // to "data" regardless of what the user asked for.
621
+ invocation.writeStderr(formatErrorOutput(error, outputMode, secrets));
622
+ return getErrorExitCode(error);
431
623
  }
432
624
  }
433
- main(process.argv)
434
- .then(() => process.exit(0))
435
- .catch((error) => {
436
- console.error(JSON.stringify({
437
- success: false,
438
- error: error.message,
439
- code: "FATAL_ERROR",
440
- }, null, 2));
441
- process.exit(1);
442
- });
443
625
  //# sourceMappingURL=index.js.map