@counterfact-labs/cli 0.2.7 → 0.2.10

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 (142) hide show
  1. package/bundle/client-release.json +1 -1
  2. package/dist/adapters/analysis-results.js +87 -0
  3. package/dist/adapters/combined-results.js +27 -0
  4. package/dist/adapters/entry-files.js +5 -2
  5. package/dist/adapters/entry-packing.js +161 -0
  6. package/dist/adapters/entry-selection.js +13 -1
  7. package/dist/adapters/files.js +110 -5
  8. package/dist/adapters/git.js +10 -1
  9. package/dist/adapters/release.js +4 -1
  10. package/dist/adapters/results.js +9 -2
  11. package/dist/adapters/selection.js +19 -9
  12. package/dist/adapters/settings.js +37 -6
  13. package/dist/analysis-core/analysis/code-agents/skipped-files.js +1 -1
  14. package/dist/analysis-core/analysis/file-analysis-plan.js +1 -0
  15. package/dist/analysis-core/analysis/file-dependencies.js +1 -0
  16. package/dist/analysis-core/analysis/finding-activity-switch.js +1 -0
  17. package/dist/analysis-core/analysis/finding-identity.js +1 -0
  18. package/dist/analysis-core/analysis/finding-quality/policy.js +1 -0
  19. package/dist/analysis-core/analysis/folder-analysis-plan.js +1 -0
  20. package/dist/analysis-core/analysis/passes/full-file-passes.js +1 -0
  21. package/dist/analysis-core/analysis/project-architecture.js +1 -0
  22. package/dist/analysis-core/analysis/schemas.js +1 -1
  23. package/dist/analysis-core/chunks/2CWULBP2.js +1 -0
  24. package/dist/analysis-core/chunks/2M55DR2J.js +35 -0
  25. package/dist/analysis-core/chunks/34NVU26H.js +1 -0
  26. package/dist/analysis-core/chunks/{RNTMH4MU.js → 5BTATES5.js} +1 -1
  27. package/dist/analysis-core/chunks/5UWDUYHF.js +1 -0
  28. package/dist/analysis-core/chunks/{6RWEWBHK.js → 5VQ4VYIP.js} +1 -1
  29. package/dist/analysis-core/chunks/66JRJPJC.js +1 -0
  30. package/dist/analysis-core/chunks/6JTHFDH3.js +1 -0
  31. package/dist/analysis-core/chunks/6VHZNBII.js +19 -0
  32. package/dist/analysis-core/chunks/6YCABS5T.js +1 -0
  33. package/dist/analysis-core/chunks/C77LGCKF.js +1 -0
  34. package/dist/analysis-core/chunks/CNQZLSAP.js +1 -0
  35. package/dist/analysis-core/chunks/DHVEGIU6.js +1 -0
  36. package/dist/analysis-core/chunks/HCPO6QPU.js +1 -0
  37. package/dist/analysis-core/chunks/I4F56VMG.js +1 -0
  38. package/dist/analysis-core/chunks/INVTU73D.js +1 -0
  39. package/dist/analysis-core/chunks/{6B2HILSY.js → JAPVLU4Z.js} +1 -1
  40. package/dist/analysis-core/chunks/KBTM5MBC.js +14 -0
  41. package/dist/analysis-core/chunks/LFJQ4YLP.js +3 -0
  42. package/dist/analysis-core/chunks/M5YWJY7P.js +1 -0
  43. package/dist/analysis-core/chunks/MKYLK6DB.js +1 -0
  44. package/dist/analysis-core/chunks/N2L4JMJE.js +1 -0
  45. package/dist/analysis-core/chunks/NCW5LJ5O.js +6 -0
  46. package/dist/analysis-core/chunks/P253B344.js +7 -0
  47. package/dist/analysis-core/chunks/PBHHWFR6.js +1 -0
  48. package/dist/analysis-core/chunks/Q5WR6B2H.js +2 -0
  49. package/dist/analysis-core/chunks/QPIBHTON.js +67 -0
  50. package/dist/analysis-core/chunks/QVXARKP7.js +1 -0
  51. package/dist/analysis-core/chunks/SJESFYBA.js +10 -0
  52. package/dist/analysis-core/chunks/SP6XFHNH.js +1 -0
  53. package/dist/analysis-core/chunks/TA2NIQAY.js +1 -0
  54. package/dist/analysis-core/chunks/UKX3A6AX.js +1 -0
  55. package/dist/analysis-core/chunks/VC3PYT2B.js +1 -0
  56. package/dist/analysis-core/chunks/VDUFPTDD.js +1 -0
  57. package/dist/analysis-core/chunks/VWRFSVLI.js +1 -0
  58. package/dist/analysis-core/chunks/VX6LYNXV.js +1 -0
  59. package/dist/analysis-core/chunks/W73WF226.js +1 -0
  60. package/dist/analysis-core/chunks/{FBTOOUPL.js → XVGLY4EO.js} +1 -1
  61. package/dist/analysis-core/chunks/YWL52FVW.js +2 -0
  62. package/dist/analysis-core/contracts/analysis-selection.js +1 -1
  63. package/dist/analysis-core/contracts/analysis-size.js +1 -0
  64. package/dist/analysis-core/contracts/execution-request.js +1 -0
  65. package/dist/analysis-core/contracts/intake-finding.js +1 -0
  66. package/dist/analysis-core/contracts/model-call-identity.js +1 -0
  67. package/dist/analysis-core/contracts/relay-analysis.js +1 -1
  68. package/dist/analysis-core/contracts/requests.js +1 -1
  69. package/dist/analysis-core/conventions/analysis-fit.js +1 -1
  70. package/dist/analysis-core/conventions/file-admission.js +1 -1
  71. package/dist/analysis-core/conventions/read-limits.js +1 -0
  72. package/dist/analysis-core/import/calls/modules.js +1 -1
  73. package/dist/analysis-core/import/entry-selection.js +1 -0
  74. package/dist/analysis-core/import/outside-loader-sources.js +1 -1
  75. package/dist/analysis-core/index.js +3 -3
  76. package/dist/analysis-core/requests/answer-formats.js +1 -1
  77. package/dist/analysis-core/requests/local-plan.js +1 -1
  78. package/dist/analysis-core/requests/planned-limits.js +1 -0
  79. package/dist/analysis-core/requests/registry.js +1 -1
  80. package/dist/analysis-core/requests/template-version.js +1 -1
  81. package/dist/analysis-core/requests/tools.js +1 -1
  82. package/dist/analysis-core/settings.js +1 -1
  83. package/dist/analysis-core/stages/engine-entry.js +1 -1
  84. package/dist/bundle/seal/engine.js +1 -1
  85. package/dist/cli/analysis-selection.js +47 -5
  86. package/dist/cli/api.js +1 -1
  87. package/dist/cli/commands/analyze.js +55 -15
  88. package/dist/cli/commands/skipped.js +65 -0
  89. package/dist/cli/context.js +2 -1
  90. package/dist/cli/finding-display.js +102 -7
  91. package/dist/cli/finding-settings.js +3 -1
  92. package/dist/cli/intake-payload.js +25 -2
  93. package/dist/cli/main.js +21 -7
  94. package/dist/cli/mcp-failure.js +6 -5
  95. package/dist/cli/mcp.js +134 -28
  96. package/dist/cli/relay-check.js +103 -23
  97. package/dist/cli/relay-failure.js +4 -4
  98. package/dist/cli/relay-preparation.js +47 -9
  99. package/dist/cli/result-check.js +31 -12
  100. package/dist/cli/saved-results.js +10 -0
  101. package/dist/cli/scope-choices.js +3 -3
  102. package/dist/cli/sign-in.js +2 -2
  103. package/dist/cli/skipped-files.js +22 -0
  104. package/dist/cli/summary.js +15 -1
  105. package/dist/contract/agent-choice.js +25 -2
  106. package/dist/contract/cli-api-messages.js +3 -0
  107. package/dist/contract/cli-refusals.js +31 -5
  108. package/dist/contract/cli-version.js +1 -1
  109. package/dist/contract/console-url.js +3 -0
  110. package/dist/contract/console.js +17 -38
  111. package/dist/contract/engine.js +1 -1
  112. package/dist/contract/messages.js +4 -3
  113. package/dist/contract/project-architecture.js +1 -0
  114. package/dist/contract/scope-choice.js +31 -11
  115. package/dist/contract/selection-offer.js +5 -8
  116. package/dist/gatekeeper/cli-api.js +70 -4
  117. package/dist/gatekeeper/console.js +8 -3
  118. package/dist/gatekeeper/model.js +5 -23
  119. package/dist/gatekeeper/trust/policy.js +2 -1
  120. package/package.json +4 -2
  121. package/dist/analysis-core/analysis/source-notes/types.js +0 -1
  122. package/dist/analysis-core/chunks/2PSNOSJ2.js +0 -1
  123. package/dist/analysis-core/chunks/2PZF54KH.js +0 -10
  124. package/dist/analysis-core/chunks/6U2IR4XB.js +0 -1
  125. package/dist/analysis-core/chunks/7NEWZ74F.js +0 -1
  126. package/dist/analysis-core/chunks/CGCJN73D.js +0 -14
  127. package/dist/analysis-core/chunks/CWYVUGG4.js +0 -1
  128. package/dist/analysis-core/chunks/FSLWD2PE.js +0 -1
  129. package/dist/analysis-core/chunks/GOQVKWP3.js +0 -32
  130. package/dist/analysis-core/chunks/IHAVDX4L.js +0 -66
  131. package/dist/analysis-core/chunks/IVULXWQN.js +0 -21
  132. package/dist/analysis-core/chunks/K2GNDX3W.js +0 -1
  133. package/dist/analysis-core/chunks/K5FUABWW.js +0 -1
  134. package/dist/analysis-core/chunks/KGQYR62R.js +0 -7
  135. package/dist/analysis-core/chunks/KWECQJ6X.js +0 -1
  136. package/dist/analysis-core/chunks/PY5M22CL.js +0 -1
  137. package/dist/analysis-core/chunks/PYKAERZZ.js +0 -1
  138. package/dist/analysis-core/chunks/SLJ6CW7W.js +0 -1
  139. package/dist/analysis-core/chunks/UEORNVQB.js +0 -1
  140. package/dist/analysis-core/chunks/WYLIV2P2.js +0 -1
  141. package/dist/analysis-core/data/finding-types.js +0 -1
  142. /package/dist/analysis-core/chunks/{SKVEBOGA.js → 33EUZF25.js} +0 -0
@@ -1,11 +1,13 @@
1
+ import { fileScopeOf } from "../analysis-core/analysis/file-analysis-plan.js";
1
2
  import { createHash } from "node:crypto";
2
3
  import { realpath, stat } from "node:fs/promises";
3
4
  import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
4
- import { analysisScopeSchema, analysisSelectionRequestSchema } from "../analysis-core/contracts/analysis-selection.js";
5
- import { fileScopeEnabled, fileScopeRequested, selectionModeEnabled } from "../adapters/settings.js";
5
+ import { ANALYSIS_SELECTION_LIMITS, analysisScopeSchema, analysisSelectionRequestSchema } from "../analysis-core/contracts/analysis-selection.js";
6
+ import { fileScopeEnabled, fileScopeRequested, multiEntryEnabled, selectionModeEnabled } from "../adapters/settings.js";
6
7
  import { resolveFolderPath } from "../adapters/folder-path.js";
7
8
  import { INVALID_SCOPE_MESSAGE, INVALID_SELECTION_MESSAGE } from "../contract/scope-choice.js";
8
9
  import { cliRefusals } from "../contract/cli-refusals.js";
10
+ import { mapLimit } from "../analysis-core/map-limit.js";
9
11
  function within(root, path) {
10
12
  const rel = relative(root, path);
11
13
  return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
@@ -13,6 +15,10 @@ function within(root, path) {
13
15
  function localPath(root, path) {
14
16
  return relative(root, path).split(sep).join("/");
15
17
  }
18
+ function canonicalScope(scope) {
19
+ const imports = scope.imports;
20
+ return { ...scope, imports: Array.isArray(imports) ? imports.length ? [...imports].sort() : "none" : imports };
21
+ }
16
22
  function folderSelection(path, launchRoot) {
17
23
  const parsed = analysisSelectionRequestSchema.safeParse({ kind: "folder", launchRoot,
18
24
  folder: localPath(launchRoot, path) || "." });
@@ -21,6 +27,38 @@ function folderSelection(path, launchRoot) {
21
27
  }
22
28
  /** Selection coordinates are shared by reading, Git facts, outputs, and recovery. Membership stays in core. */
23
29
  export async function resolveAnalysisSelection(path, cwd, settings, scope) {
30
+ const paths = typeof path === "string" ? [path] : path;
31
+ if (!paths.length || paths.length > ANALYSIS_SELECTION_LIMITS.files
32
+ || paths.some(path => typeof path !== "string" || !path.trim()))
33
+ throw new Error(INVALID_SELECTION_MESSAGE);
34
+ if (paths.length > 1 || scope?.entries) {
35
+ if (!multiEntryEnabled(settings))
36
+ throw new Error(paths.length > 1 ? INVALID_SELECTION_MESSAGE : INVALID_SCOPE_MESSAGE);
37
+ const requested = await mapLimit(paths, 4, path => resolveAnalysisSelection(path, cwd, settings));
38
+ if (requested.some(selection => selection.kind !== "file"))
39
+ throw new Error(INVALID_SELECTION_MESSAGE);
40
+ const originals = scope?.entries ? await mapLimit(scope.entries, 4, path => resolveAnalysisSelection(path, cwd, settings)) : requested;
41
+ if (originals.some(selection => selection.kind !== "file")
42
+ || requested.some(selection => !originals.some(original => original.entryPath === selection.entryPath)))
43
+ throw new Error(INVALID_SCOPE_MESSAGE);
44
+ const unique = [...new Map(originals.map(selection => [selection.entryPath, selection])).values()].sort((a, b) => a.entryPath.localeCompare(b.entryPath));
45
+ if (unique.length === 1) {
46
+ if (scope?.entries)
47
+ throw new Error(INVALID_SCOPE_MESSAGE);
48
+ return resolveAnalysisSelection(unique[0].path, cwd, settings, scope);
49
+ }
50
+ const first = unique[0], entries = unique.map(selection => selection.entryPath);
51
+ const chosen = scope ? canonicalScope(scope) : undefined;
52
+ const request = analysisSelectionRequestSchema.safeParse({ kind: "file", launchRoot: first.launchRoot, entry: entries[0], entries,
53
+ ...(chosen ? { scope: chosen } : {}) });
54
+ if (!request.success)
55
+ throw new Error(INVALID_SCOPE_MESSAGE);
56
+ const identity = JSON.stringify([first.launchRoot, entries, chosen ? [chosen.snapshot, chosen.entry, chosen.imports] : null]);
57
+ const hash = createHash("sha256").update(JSON.stringify(entries)).digest("hex");
58
+ return { ...first, outputAnchor: first.launchRoot, outputDir: join(first.launchRoot, ".counterfact/files", hash),
59
+ selectionKey: `files:${createHash("sha256").update(identity).digest("hex")}`, request: request.data };
60
+ }
61
+ path = paths[0];
24
62
  if (fileScopeRequested(settings) && !fileScopeEnabled(settings))
25
63
  throw new Error(cliRefusals.release_unavailable);
26
64
  if (!fileScopeEnabled(settings)) {
@@ -62,8 +100,7 @@ export async function resolveAnalysisSelection(path, cwd, settings, scope) {
62
100
  const parsed = analysisScopeSchema.safeParse(scope);
63
101
  if (!parsed.success || parsed.data.entry !== entryPath)
64
102
  throw new Error(INVALID_SCOPE_MESSAGE);
65
- const imports = parsed.data.imports;
66
- chosen = { ...parsed.data, imports: Array.isArray(imports) ? imports.length ? [...imports].sort() : "none" : imports };
103
+ chosen = canonicalScope(parsed.data);
67
104
  }
68
105
  const request = analysisSelectionRequestSchema.safeParse({ kind: "file", launchRoot, entry: entryPath,
69
106
  ...(chosen === undefined ? {} : { scope: chosen }) });
@@ -78,12 +115,17 @@ export async function resolveAnalysisSelection(path, cwd, settings, scope) {
78
115
  }
79
116
  /** Entry points resolve coordinates once; downstream stages reuse the same captured description. */
80
117
  export async function resolveRunSelection(path, cwd, settings, options = {}) {
118
+ if (options.paths && !multiEntryEnabled(settings))
119
+ throw new Error(INVALID_SELECTION_MESSAGE);
81
120
  if (!selectionModeEnabled(settings)) {
82
121
  if (options.scope !== undefined)
83
122
  throw new Error(INVALID_SCOPE_MESSAGE);
84
123
  return undefined;
85
124
  }
86
- return options.selection ?? resolveAnalysisSelection(path, cwd, settings, options.scope);
125
+ const selection = options.selection ?? await resolveAnalysisSelection(options.paths ?? path, cwd, settings, options.scope);
126
+ if (selection.kind === "file" && fileScopeOf(settings) !== undefined)
127
+ throw new Error(INVALID_SCOPE_MESSAGE);
128
+ return selection;
87
129
  }
88
130
  export function analysisSaveOptions(selection, outputDir) {
89
131
  return { defaultFileOutput: selection?.kind === "file" && outputDir === undefined,
package/dist/cli/api.js CHANGED
@@ -1 +1 @@
1
- export { ApiClient, ApiError, DEFAULT_URL, errorCode, errorText } from "../gatekeeper/cli-api.js";
1
+ export { ApiClient, ApiError, SavedSignInError, DEFAULT_URL, errorCode, errorText } from "../gatekeeper/cli-api.js";
@@ -1,3 +1,5 @@
1
+ import { formatSavedResults } from "../saved-results.js";
2
+ import { skipped } from "./skipped.js";
1
3
  import { loadDevelopmentRelay } from "../development-relay.js";
2
4
  import { loadInstalledRelease } from "../../adapters/release.js";
3
5
  import { cliRefusals, START_FAILURE_MESSAGE } from "../../contract/cli-refusals.js";
@@ -10,8 +12,9 @@ import { GITIGNORE_OUTPUT_NOTICE } from "../../adapters/gitignore-output.js";
10
12
  import { readFile } from "node:fs/promises";
11
13
  import { resolve } from "node:path";
12
14
  import { loadAgentProposal } from "../../contract/agent-proposal.js";
13
- import { formatSavedFindings } from "../finding-display.js";
14
- import { scopeChoicesEnabled } from "../../adapters/settings.js";
15
+ import { fileScopeOf } from "../../analysis-core/analysis/file-analysis-plan.js";
16
+ import { folderPartsEnabled } from "../../analysis-core/settings.js";
17
+ import { captureArchitectureSettings, scopeChoicesEnabled } from "../../adapters/settings.js";
15
18
  import { resolveRunSelection } from "../analysis-selection.js";
16
19
  import { renderScopeChoices } from "../scope-choices.js";
17
20
  import { progressLine } from "../../contract/scope-choice.js";
@@ -34,36 +37,70 @@ function requireLocal(value) {
34
37
  // A local-capable client never falls back to sending source files to the hosted upload API.
35
38
  if (value.mode !== "local")
36
39
  throw new ApiError(503, "unavailable", unavailable);
40
+ return "skippedFiles" in value && value.skippedFiles === true;
37
41
  }
38
- export function parseAnalyzeOptions(args, choices = false) {
42
+ export function parseAnalyzeOptions(args, choices = false, multiple = false) {
39
43
  let path, proposalFile, scopeFile;
44
+ let skipped;
45
+ let scope;
46
+ const paths = [];
40
47
  for (let index = 0; index < args.length; index++) {
41
48
  const arg = args[index];
42
- if (arg === "--proposal" && proposalFile === undefined) {
49
+ if (["--skipped", "--skipped-plan", "--show-skipped"].includes(arg) && skipped === undefined) {
50
+ skipped = arg === "--skipped" ? "analyze" : arg === "--skipped-plan" ? "plan" : "files";
51
+ }
52
+ else if (arg === "--proposal" && proposalFile === undefined) {
43
53
  const value = args[++index];
44
54
  if (!value || value.startsWith("-"))
45
55
  throw new Error();
46
56
  proposalFile = value;
47
57
  }
48
- else if (choices && arg === "--scope" && scopeFile === undefined) {
58
+ else if ((choices || args[index + 1]?.startsWith("[")) && arg === "--scope" && scope === undefined && scopeFile === undefined) {
49
59
  const value = args[++index];
50
60
  if (!value || value.startsWith("-"))
51
61
  throw new Error();
52
- scopeFile = value;
62
+ if (value.startsWith("["))
63
+ scope = JSON.parse(value);
64
+ else
65
+ scopeFile = value;
66
+ }
67
+ else if (!arg.startsWith("-") && (path === undefined || multiple)) {
68
+ path ??= arg;
69
+ paths.push(arg);
53
70
  }
54
- else if (!arg.startsWith("-") && path === undefined)
55
- path = arg;
56
71
  else
57
72
  throw new Error();
58
73
  }
59
- return { path: path ?? ".", options: { ...(proposalFile === undefined ? {} : { proposalFile }), ...(scopeFile === undefined ? {} : { scopeFile }) } };
74
+ if (skipped && (paths.length > 1 || proposalFile || scope !== undefined || scopeFile !== undefined))
75
+ throw new Error();
76
+ return { path: path ?? ".", options: { ...(paths.length > 1 ? { paths } : {}), ...(scope === undefined ? {} : { scope }), ...(proposalFile === undefined ? {} : { proposalFile }), ...(scopeFile === undefined ? {} : { scopeFile }), ...(skipped ? { skipped } : {}) } };
60
77
  }
61
78
  export async function analyze(path, options, ctx) {
79
+ if (Array.isArray(options.scope) && (options.proposal !== undefined || options.proposalFile !== undefined)) {
80
+ throw new ApiError(400, "invalid_body", cliRefusals.invalid_body);
81
+ }
82
+ if (options.skipped)
83
+ return skipped(path, options.skipped, ctx, analyze, options.surface);
62
84
  const startup = ctx.startup;
63
- const scope = options.scope;
85
+ let settings = startup.settings;
86
+ if (Array.isArray(options.scope)) {
87
+ try {
88
+ if (!folderPartsEnabled(settings))
89
+ throw new Error();
90
+ const scope = fileScopeOf({ ...settings, CF_PART_FILES: JSON.stringify(options.scope) });
91
+ if (!scope?.length)
92
+ throw new Error();
93
+ settings = Object.freeze({ ...settings, CF_PART_FILES: JSON.stringify(scope) });
94
+ }
95
+ catch {
96
+ throw new ApiError(400, "invalid_body", cliRefusals.invalid_body);
97
+ }
98
+ }
99
+ const scope = Array.isArray(options.scope) ? undefined : options.scope;
64
100
  const proposal = await loadAgentProposal(async () => options.proposalFile === undefined ? options.proposal
65
101
  : JSON.parse(await readFile(resolve(ctx.cwd ?? process.cwd(), options.proposalFile), "utf8")), startup.settings);
66
- requireLocal(await ctx.api.request("checks/mode"));
102
+ const mode = await (ctx.consoleMode?.() ?? ctx.api.request("checks/mode"));
103
+ const skippedFilesSupported = requireLocal(mode);
67
104
  const development = startup.transport.CF_RELAY_DEV === "1" ? await loadDevelopmentRelay(startup.transport) : undefined;
68
105
  const release = development?.release ?? ctx.relayRuntime?.release ?? await loadInstalledRelease();
69
106
  if (release.projectContext !== "checks")
@@ -71,7 +108,7 @@ export async function analyze(path, options, ctx) {
71
108
  const token = await ctx.token?.();
72
109
  if (!token)
73
110
  throw new ApiError(401, "unauthorized", cliApiMessages.unauthorized);
74
- const selection = await resolveRunSelection(path, ctx.cwd ?? process.cwd(), startup.settings, options);
111
+ const selection = await resolveRunSelection(path, ctx.cwd ?? process.cwd(), settings, { ...options, scope });
75
112
  const folder = selection?.path ?? resolveAgentFolder(path, ctx.cwd);
76
113
  const config = Object.freeze({ ...startup.transport, CF_RELAY_URL: startup.transport.CF_RELAY_URL ?? release.relayUrl,
77
114
  CF_LOCAL_RELAY: "1", CF_CONSOLE_URL: ctx.api.baseUrl, CF_CONSOLE_TOKEN: token });
@@ -87,10 +124,13 @@ export async function analyze(path, options, ctx) {
87
124
  let choice;
88
125
  try {
89
126
  const code = await (ctx.relayRuntime?.run ?? runRelayCheck)(folder, {
90
- settings: startup.settings, config, release, cwd: ctx.cwd, connect: development?.connect ?? ctx.relayRuntime?.connect,
127
+ analyzedFolder: options.analyzedFolder ?? path,
128
+ settings: captureArchitectureSettings(settings, config), config, release, skippedFilesSupported, consoleSupportsActivity: !!mode && typeof mode === "object" && "findingActivity" in mode && mode.findingActivity === true, cwd: ctx.cwd, connect: development?.connect ?? ctx.relayRuntime?.connect,
91
129
  ...(ctx.relayRuntime?.selectionProvider ? { selectionProvider: ctx.relayRuntime.selectionProvider } : {}),
92
130
  ...(proposal ? { proposal } : {}),
93
131
  ...(selection ? { selection } : {}), ...(scope ? { scope } : {}),
132
+ ...(options.addToAnalysisId ? { addToAnalysisId: options.addToAnalysisId, expectedProjectId: options.expectedProjectId } : {}),
133
+ ...(options.retryUpload ? { retryUpload: true } : {}),
94
134
  signal: ctx.signal ? AbortSignal.any([controller.signal, ctx.signal]) : controller.signal,
95
135
  stdout() { }, stderr() { },
96
136
  onStart: () => { ctx.onCheckStart?.(); ctx.write(`\r\u001b[2K${progressLine(selection, "terminal")}`); progressShown = true; },
@@ -117,8 +157,8 @@ export async function analyze(path, options, ctx) {
117
157
  onSaved: result => {
118
158
  clearProgress();
119
159
  ctx.onCheckComplete?.(result);
120
- ctx.write(formatSavedFindings(result, { numbersEnabled: ctx.findingNumbers, audienceEnabled: result.audienceEnabled }));
121
- ctx.write(`Full results: ${result.url}\n`);
160
+ if (!options.deferResults)
161
+ ctx.write(formatSavedResults(result, settings, ctx.findingNumbers, !options.addToAnalysisId, { agentReadableResults: ctx.findingSettings.agentReadableResults, surface: options.surface, analyzedFolder: options.analyzedFolder ?? path }));
122
162
  },
123
163
  });
124
164
  if (choice && !ctx.onScopeChoice && selection)
@@ -0,0 +1,65 @@
1
+ import { followupSettings, skippedFilesEnabled } from "../../analysis-core/settings.js";
2
+ import { loadAnalysisResults } from "../../adapters/analysis-results.js";
3
+ import { readLocalFolder } from "../../adapters/files.js";
4
+ import { loadCompletion } from "../../adapters/completion.js";
5
+ import { sizeSkippedFiles } from "../../analysis-core/analysis/code-agents/skipped-files.js";
6
+ import { followupPlanText } from "../skipped-files.js";
7
+ import { resolveAgentFolder } from "../agent-folder.js";
8
+ import { formatSavedResults } from "../saved-results.js";
9
+ export async function skipped(path, action, ctx, run, surface) {
10
+ if (!skippedFilesEnabled(ctx.startup.settings))
11
+ throw new Error("Skipped-file analyses are not enabled.");
12
+ const folder = resolveAgentFolder(path, ctx.cwd);
13
+ let latest;
14
+ let mode;
15
+ const deferred = { ...ctx, consoleMode: ctx.consoleMode ?? (() => mode ??= ctx.api.request("checks/mode")),
16
+ onCheckComplete: (result) => { latest = result; } };
17
+ const publish = () => {
18
+ if (!latest)
19
+ return;
20
+ ctx.onCheckComplete?.(latest);
21
+ ctx.write(formatSavedResults(latest, ctx.startup.settings, ctx.findingNumbers, false, { agentReadableResults: ctx.findingSettings.agentReadableResults, surface }));
22
+ };
23
+ // Finish an already completed upload before planning; its reading must not be purchased again.
24
+ const pending = await loadCompletion(folder);
25
+ const unsettled = pending?.uploaded ? await loadAnalysisResults(folder).then(saved => saved.context.pending?.checkId === pending.payload.checkId).catch(() => false) : false;
26
+ if (pending && !pending.finished && (!pending.uploaded || unsettled)) {
27
+ const recovered = await run(folder, { retryUpload: true, deferResults: true, surface }, deferred);
28
+ if (recovered !== 0) {
29
+ if (action === "analyze")
30
+ publish();
31
+ return recovered;
32
+ }
33
+ }
34
+ let saved;
35
+ try {
36
+ saved = await loadAnalysisResults(folder);
37
+ }
38
+ catch {
39
+ throw new Error("The saved analysis is missing or unreadable. Run an analysis of this folder first.");
40
+ }
41
+ const paths = sizeSkippedFiles(saved.report.refusedFiles).map(file => file.path);
42
+ if (action === "files") {
43
+ ctx.write(paths.length ? `${paths.join("\n")}\n` : "No skipped files remain to analyze.\n");
44
+ return 0;
45
+ }
46
+ const settings = followupSettings(ctx.startup.settings, saved.context.settings);
47
+ const source = await readLocalFolder(settings, folder, undefined, { planSkipped: paths });
48
+ const plan = source.filePlan;
49
+ ctx.write(followupPlanText(plan));
50
+ if (action === "plan")
51
+ return 0;
52
+ // Groups share the same local result files; save one completely before starting the next.
53
+ for (const group of plan.groups) {
54
+ ctx.signal?.throwIfAborted();
55
+ const scoped = { ...deferred,
56
+ startup: { ...ctx.startup, settings: Object.freeze({ ...settings, CF_PART_FILES: JSON.stringify(group.files) }) } };
57
+ const code = await run(folder, { deferResults: true, surface, addToAnalysisId: saved.context.analysisId, expectedProjectId: saved.context.projectId }, scoped);
58
+ if (code !== 0) {
59
+ publish();
60
+ return code;
61
+ }
62
+ }
63
+ publish();
64
+ return 0;
65
+ }
@@ -11,11 +11,12 @@ export function commandContext(options = {}) {
11
11
  const startup = options.startup ?? readCliSettings(options.env, options.emailSignIn);
12
12
  const homeDir = options.homeDir ?? homedir();
13
13
  const baseUrl = (options.baseUrl ?? startup.baseUrl ?? DEFAULT_URL).replace(/\/+$/, "");
14
- const api = new ApiClient(baseUrl, () => readCredentials(homeDir, api.baseUrl), undefined, options.signal);
14
+ const api = new ApiClient(baseUrl, () => readCredentials(homeDir, api.baseUrl), undefined, options.signal, startup.transport.CF_CLI_TRANSPORT_ERRORS !== "0");
15
15
  const write = options.write ?? (text => { process.stdout.write(text); });
16
16
  const findingSettings = options.findingSettings ?? readFindingSettings(options.env);
17
17
  return {
18
18
  findingNumbers: findingSettings.numbersEnabled,
19
+ findingSettings,
19
20
  signInRetry: (options.env ?? process.env).CF_SIGN_IN_RETRY !== "0",
20
21
  startup, signal: options.signal, relayRuntime: options.relayRuntime, cwd: options.cwd ?? process.cwd(),
21
22
  token: () => readCredentials(homeDir, api.baseUrl), onCheckComplete: options.onCheckComplete, onCheckStart: options.onCheckStart, onAgentChoice: options.onAgentChoice,
@@ -1,5 +1,9 @@
1
- import { groupByAudience } from "../analysis-core/analysis/schemas.js";
1
+ import { stripVTControlCharacters } from "node:util";
2
+ import { groupByActivity, groupByAudience } from "../analysis-core/analysis/schemas.js";
2
3
  export const TEAM_WORKFLOW_HEADING = "Team workflow";
4
+ export const notActiveCountLine = (count) => `${count} more ${count === 1 ? "finding is" : "findings are"} about code that doesn't run today (not active)`;
5
+ export const activityAdvice = (activity) => activity.state === "dead" ? "Remove it (cleanup)."
6
+ : `Fix this before turning on ${activity.gate}, or remove it if you don't plan to use it.`;
3
7
  export const formatFindingNumber = (seq) => `F-${seq}`;
4
8
  /** Intake returns one id per payload finding in order; the optional number mapping is joined by id. */
5
9
  export function savedFindings(result, report) {
@@ -7,28 +11,119 @@ export function savedFindings(result, report) {
7
11
  return result.findingIds.map((id, index) => {
8
12
  const numbered = numbers.get(id), local = report[index];
9
13
  return { id, ...(numbered ? { seq: numbered.seq, number: formatFindingNumber(numbered.seq) } : {}),
10
- title: local?.title ?? "", ...(local?.audience === undefined ? {} : { audience: local.audience }) };
14
+ title: local?.title ?? "", ...(local?.audience === undefined ? {} : { audience: local.audience }),
15
+ ...(local?.activity === undefined ? {} : { activity: local.activity }),
16
+ ...(local?.activity === undefined || local.fixes === undefined ? {} : { fixes: local.fixes.map(({ title, approach }) => ({ title: title ?? "", approach: approach ?? "" })) }),
17
+ ...(local?.severity === undefined ? {} : { severity: local.severity }),
18
+ ...(local?.site === undefined ? {} : { site: local.site }),
19
+ ...(local?.fixes === undefined ? {} : { shortFix: shortFix(local.fixes[0]) }) };
11
20
  });
12
21
  }
22
+ function findingLabel(value, enabled, index, labels) {
23
+ return labels === "position" ? `${index + 1}.` : enabled && value.seq !== undefined ? formatFindingNumber(value.seq) : value.id ?? `${index + 1}.`;
24
+ }
13
25
  function findingLine(value, enabled, index, labels, audienceEnabled) {
14
26
  // The off switch preserves the original Markdown summary, including whitespace.
15
27
  if (labels === "position" && !audienceEnabled)
16
28
  return `${index + 1}. ${value.title}`;
17
29
  const title = value.title.replace(/[\r\n\u2028\u2029]+/g, " ");
18
- const label = labels === "position" ? `${index + 1}.` : enabled && value.seq !== undefined ? formatFindingNumber(value.seq) : value.id ?? `${index + 1}.`;
30
+ const label = findingLabel(value, enabled, index, labels);
19
31
  return `${label}${title ? ` ${title}` : ""}`;
20
32
  }
21
- export function findingDisplayRows(findings, { numbersEnabled = true, audienceEnabled, labels }) {
22
- const groups = audienceEnabled ? groupByAudience(findings) : { users: findings, team: [] };
33
+ export function findingDisplayRows(findings, { numbersEnabled = true, audienceEnabled, notActiveEnabled = false, labels }) {
34
+ const { active } = groupByActivity(findings, notActiveEnabled);
35
+ const groups = audienceEnabled ? groupByAudience(active) : { users: active, team: [] };
23
36
  return [...groups.users, ...groups.team].map((finding, index) => ({ finding,
24
37
  label: findingLine(finding, numbersEnabled, index, labels, audienceEnabled), startsTeamGroup: groups.team.length > 0 && index === groups.users.length }));
25
38
  }
26
- export function formatSavedFindings(result, options) {
39
+ function legacyFindingList(result, options) {
40
+ const groups = groupByActivity(result.findings ?? [], options.notActiveEnabled === true);
41
+ const collapsed = groups.notActive.length ? notActiveCountLine(groups.notActive.length) + "\n" : "";
27
42
  if (options.numbersEnabled !== false && result.findings?.some(value => value.seq !== undefined)) {
28
- return "Findings:\n" + findingDisplayRows(result.findings, { ...options, labels: "reference" }).map(row => `${row.startsTeamGroup ? `${TEAM_WORKFLOW_HEADING}:\n` : ""}${row.label}\n`).join("");
43
+ return "Findings:\n" + findingDisplayRows(result.findings, { ...options, labels: "reference" }).map(row => `${row.startsTeamGroup ? `${TEAM_WORKFLOW_HEADING}:\n` : ""}${row.label}\n`).join("") + collapsed;
29
44
  }
45
+ if (collapsed)
46
+ return (groups.active.length ? `Finding ids: ${groups.active.map(f => f.id).join(", ")}\n` : "") + collapsed;
30
47
  return result.findingIds?.length ? `Finding ids: ${result.findingIds.join(", ")}\n` : "";
31
48
  }
49
+ export function formatFindingActivity(activity) {
50
+ return `${activity.state === "dead" ? "Dead" : "Switched off"}: ${activity.reason}\n${activityAdvice(activity)}\n`;
51
+ }
52
+ // This is a presentation budget, separate from the size of the saved report.
53
+ export const AGENT_FINDING_LIST_BUDGET = 12_000;
54
+ function shortFix(fix) {
55
+ return [fix?.summary, fix?.title, fix?.approach].find(text => text?.trim()) ?? "";
56
+ }
57
+ function compactText(value, limit) {
58
+ const text = stripVTControlCharacters(value).replace(/[\p{Cc}\s]+/gu, " ").replace(/\s*\u2014\s*/g, ", ").trim();
59
+ if (text.length <= limit)
60
+ return text;
61
+ // Avoid ending on half of a supplementary Unicode character.
62
+ return Array.from(text.slice(0, limit - 3)).filter(char => !/^[\ud800-\udbff]$/.test(char)).join("") + "...";
63
+ }
64
+ function sentence(text) { return /[.!?]$/.test(text) ? text : `${text}.`; }
65
+ function compactFinding(value) {
66
+ const compact = legacyFinding(value);
67
+ // Full fixes remain in saved detail; compact lists use only the existing short fix.
68
+ delete compact.fixes;
69
+ return { ...compact, title: compactText(value.title, 160) || "Untitled finding",
70
+ severity: compactText(value.severity ?? "", 24) || "severity unavailable",
71
+ ...(value.site ? { site: { path: compactText(value.site.path, 240), line: value.site.line } } : {}),
72
+ shortFix: compactText(value.shortFix ?? "", 240) || "No suggested fix available." };
73
+ }
74
+ function legacyFinding(value) {
75
+ const original = { ...value };
76
+ delete original.severity;
77
+ delete original.site;
78
+ delete original.shortFix;
79
+ return original;
80
+ }
81
+ export function findingDetailReference(value, projectId) {
82
+ return { id: value.seq !== undefined ? formatFindingNumber(value.seq) : value.id, ...(projectId ? { projectId } : {}) };
83
+ }
84
+ export function formatFindingContinuation(next) {
85
+ return `Next finding: get_finding(${JSON.stringify(next)})\n`;
86
+ }
87
+ export function presentSavedFindings(result, options) {
88
+ const rows = findingDisplayRows(result.findings ?? [], { ...options, labels: "reference" });
89
+ const orderedReferences = rows.flatMap(({ finding }) => finding.id ? [{ id: finding.id, seq: finding.seq }] : []);
90
+ if (options.agentReadableResults === false || options.numbersEnabled === false || !rows.length) {
91
+ return { text: legacyFindingList(result, options), findings: rows.map(row => legacyFinding(row.finding)),
92
+ findingIds: result.findingIds ?? [], nextFinding: undefined, orderedReferences };
93
+ }
94
+ const { notActive } = groupByActivity(result.findings ?? [], options.notActiveEnabled === true);
95
+ const collapsed = notActive.length ? notActiveCountLine(notActive.length) + "\n" : "";
96
+ let text = "Findings:\n";
97
+ const findings = [];
98
+ let nextFinding;
99
+ for (const [index, row] of rows.entries()) {
100
+ const value = compactFinding(row.finding);
101
+ const label = findingLabel(value, true, index, "reference");
102
+ const site = value.site ? `${value.site.path}:${value.site.line ?? "?"}` : "File unavailable";
103
+ const line = `${row.startsTeamGroup ? `${TEAM_WORKFLOW_HEADING}:\n` : ""}${label} (${value.severity}) ${sentence(value.title)} ${site}. Fix: ${value.shortFix}\n`;
104
+ const next = rows[index + 1]?.finding;
105
+ const footer = next ? overflowLine(rows.length - index - 1, next, options) : "";
106
+ const candidate = { text: text + line + footer + collapsed, findings: [...findings, value],
107
+ findingIds: [...findings, value].flatMap(finding => finding.id ? [finding.id] : []),
108
+ ...(next ? { nextFinding: findingDetailReference(next, options.projectId) } : {}) };
109
+ if (JSON.stringify(candidate).length > AGENT_FINDING_LIST_BUDGET) {
110
+ nextFinding = findingDetailReference(row.finding, options.projectId);
111
+ text += overflowLine(rows.length - index, row.finding, options);
112
+ break;
113
+ }
114
+ text += line;
115
+ findings.push(value);
116
+ }
117
+ return { text: text + collapsed, findings, findingIds: findings.flatMap(value => value.id ? [value.id] : []), nextFinding, orderedReferences };
118
+ }
119
+ function overflowLine(count, next, options) {
120
+ const guidance = options.surface === "mcp" ? `Next: get_finding(${JSON.stringify(findingDetailReference(next, options.projectId))}); follow nextFinding for the rest.`
121
+ : `Run counterfact-labs finding ${next.id} for the next one, or open Full results below.`;
122
+ return `${count} more ${count === 1 ? "finding" : "findings"}. ${guidance}\n`;
123
+ }
124
+ export function formatSavedFindings(result, options) {
125
+ return presentSavedFindings(result, options).text;
126
+ }
32
127
  export function formatWaitingFixes(items, enabled, needsProject) {
33
128
  if (!items.length)
34
129
  return "No waiting fixes.";
@@ -1,5 +1,7 @@
1
1
  import { findingAudienceEnabled } from "../analysis-core/analysis/finding-audience-switch.js";
2
+ import { notActiveEnabled } from "../analysis-core/analysis/finding-activity-switch.js";
2
3
  export function readFindingSettings(env = process.env, bundle) {
3
4
  return { numbersEnabled: env.CF_FINDING_NUMBERS !== "0", fixesEnabled: env.CF_WAITING_FIXES !== "0",
4
- audienceEnabled: findingAudienceEnabled(env, bundle) };
5
+ audienceEnabled: findingAudienceEnabled(env, bundle), notActiveEnabled: notActiveEnabled(env),
6
+ agentReadableResults: env.CF_AGENT_READABLE_RESULTS !== "0" };
5
7
  }
@@ -1,8 +1,11 @@
1
+ import { skippedFilesEnabled } from "../analysis-core/settings.js";
2
+ import { fingerprintOf } from "../analysis-core/analysis/finding-identity.js";
1
3
  import { resultSafetyCheckEnabled } from "../analysis-core/settings.js";
2
4
  import { localFindingsPayloadSchema } from "../contract/console.js";
3
5
  import { projectPublicFinding } from "../contract/public-report.js";
4
6
  import { checkIntakeFindings } from "./result-check.js";
5
7
  import { RESULTS_TOO_LARGE_MESSAGE } from "../contract/cli-refusals.js";
8
+ import { projectArchitecture } from "../contract/project-architecture.js";
6
9
  export function buildIntakePayload(identity, agent, report, options = {}) {
7
10
  const repo = (key) => key === null || key === report.repoKey ? agent.repository : key;
8
11
  let findings = report.findings.map(finding => projectPublicFinding(finding, repo));
@@ -19,8 +22,28 @@ export function buildIntakePayload(identity, agent, report, options = {}) {
19
22
  resultCheck = counts;
20
23
  findingIndexes = acceptedIndexes;
21
24
  }
22
- const result = localFindingsPayloadSchema.safeParse({ ...identity, agent, findings });
25
+ const metadata = !skippedFilesEnabled(options.settings ?? {}) ? {} : {
26
+ ...(options.coverage ? { coverage: options.coverage } : {}),
27
+ ...(options.addToAnalysisId ? { addToAnalysisId: options.addToAnalysisId } : {}),
28
+ ...(options.findingAssessments ? { findingAssessments: findingIndexes.flatMap((index, at) => {
29
+ const assessment = options.findingAssessments.find(row => row.fingerprint === fingerprintOf(report.findings[index]));
30
+ return assessment ? [{ ...assessment, fingerprint: fingerprintOf(findings[at]) }] : [];
31
+ }) } : {}),
32
+ };
33
+ const findingActivities = findings.map(finding => finding.activity);
34
+ // Older consoles use a strict schema. Local grouping survives without sending an unsupported field.
35
+ if (options.consoleSupportsActivity !== true)
36
+ findings = findings.map(finding => {
37
+ const legacy = { ...finding };
38
+ delete legacy.activity;
39
+ return legacy;
40
+ });
41
+ const architecture = options.settings?.CF_LOCAL_ARCHITECTURE === "1" ? projectArchitecture(report.graph) : undefined;
42
+ let result = localFindingsPayloadSchema.safeParse({ ...identity, agent, findings, ...metadata, ...(architecture ? { architecture } : {}) });
43
+ // Optional map metadata must not crowd otherwise valid findings out of the total upload allowance.
44
+ if (!result.success && architecture)
45
+ result = localFindingsPayloadSchema.safeParse({ ...identity, agent, findings, ...metadata });
23
46
  if (!result.success)
24
47
  throw new Error(RESULTS_TOO_LARGE_MESSAGE);
25
- return { payload: result.data, resultCheck, findingIndexes };
48
+ return { payload: result.data, resultCheck, findingIndexes, findingActivities };
26
49
  }
package/dist/cli/main.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { pathToFileURL } from "node:url";
3
3
  import { realpath } from "node:fs/promises";
4
4
  import { errorCode, errorText } from "./api.js";
5
- import { readEmailSignIn, readSelectionSettings, scopeChoicesEnabled } from "../adapters/settings.js";
5
+ import { fileScopeEnabled, readEmailSignIn, readSelectionSettings, scopeChoicesEnabled, multiEntryEnabled } from "../adapters/settings.js";
6
6
  import { commandContext } from "./context.js";
7
7
  import { analyze, parseAnalyzeOptions } from "./commands/analyze.js";
8
8
  import { finding } from "./commands/finding.js";
@@ -10,23 +10,35 @@ import { balance } from "./commands/balance.js";
10
10
  import { login, parseLoginOptions } from "./commands/login.js";
11
11
  import { logout } from "./commands/logout.js";
12
12
  import { serveMcp } from "./mcp.js";
13
+ import { folderPartsEnabled, skippedFilesEnabled } from "../analysis-core/settings.js";
13
14
  import { loadAnalysisScope } from "./scope-choices.js";
14
15
  import { FILE_SCOPE_PUBLIC_USAGE } from "../contract/scope-choice.js";
15
- const usage = "Usage: counterfact-labs analyze [path] [--proposal <json-file>]\n counterfact-labs finding <id>\n counterfact-labs balance | login | logout | mcp\n";
16
16
  export async function run(args, options = {}) {
17
+ const skippedUsage = skippedFilesEnabled(options.startup?.settings ?? options.env ?? process.env)
18
+ ? " counterfact-labs analyze [path] [--show-skipped | --skipped-plan | --skipped]\n" : "";
19
+ const usage = `Usage: counterfact-labs analyze [path] [--proposal <json-file>]\n${skippedUsage} counterfact-labs finding <id>\n counterfact-labs balance | login | logout | mcp\n`;
17
20
  const write = options.write ?? (text => { process.stdout.write(text); });
18
21
  const emailSignIn = options.emailSignIn ?? readEmailSignIn(options.env);
22
+ let fileMode = false;
19
23
  try {
20
24
  const [command, ...rest] = args;
21
25
  const env = options.startup?.settings ?? options.env ?? process.env;
22
26
  // Help follows the CLI defaults without validating unrelated commands' analysis switches.
23
27
  const selectionSettings = readSelectionSettings(env, { validate: command === "analyze" || command === "mcp" });
28
+ fileMode = fileScopeEnabled(selectionSettings);
24
29
  const commandOptions = { ...options, emailSignIn, env: { ...(options.env ?? process.env), ...selectionSettings,
25
30
  ...(command === "analyze" || command === "mcp" ? {} : {
26
- CF_FILE_SCOPE: undefined, CF_SCOPE_CHOICES: undefined, CF_LOCAL_IMPORT_RESOLUTION: undefined,
31
+ CF_FILE_SCOPE: undefined, CF_SCOPE_CHOICES: undefined, CF_LOCAL_IMPORT_RESOLUTION: undefined, CF_MULTI_ENTRY: undefined,
27
32
  }) } };
28
33
  const choices = scopeChoicesEnabled(selectionSettings);
29
- const commandUsage = choices ? FILE_SCOPE_PUBLIC_USAGE : usage;
34
+ const scopeUsage = choices ? FILE_SCOPE_PUBLIC_USAGE : usage;
35
+ // Insert the skipped-files usage line after the first usage line.
36
+ const firstLineEnd = scopeUsage.indexOf("\n") + 1;
37
+ let commandUsage = choices && firstLineEnd > 0
38
+ ? `${scopeUsage.slice(0, firstLineEnd)}${skippedUsage}${scopeUsage.slice(firstLineEnd)}`
39
+ : scopeUsage;
40
+ if (folderPartsEnabled(env))
41
+ commandUsage = commandUsage.replace(choices ? "[--scope <json-file>]" : "[--proposal <json-file>]", choices ? "[--scope <json-file | json-list>]" : "[--proposal <json-file>] [--scope <json-list>]");
30
42
  if (["--help", "-h", "help"].includes(command) || !command) {
31
43
  write(commandUsage);
32
44
  return 0;
@@ -34,13 +46,15 @@ export async function run(args, options = {}) {
34
46
  if (command === "analyze") {
35
47
  let parsed;
36
48
  try {
37
- parsed = parseAnalyzeOptions(rest, choices);
49
+ parsed = parseAnalyzeOptions(rest, choices, multiEntryEnabled(selectionSettings));
38
50
  }
39
51
  catch {
40
52
  throw new Error(commandUsage.trimEnd());
41
53
  }
42
54
  const ctx = commandContext(commandOptions);
43
- const scope = await loadAnalysisScope(parsed.options, ctx.cwd ?? process.cwd(), ctx.startup.settings);
55
+ if (Array.isArray(parsed.options.scope) && !folderPartsEnabled(ctx.startup.settings))
56
+ throw new Error(commandUsage.trimEnd());
57
+ const scope = Array.isArray(parsed.options.scope) ? parsed.options.scope : await loadAnalysisScope(parsed.options, ctx.cwd ?? process.cwd(), ctx.startup.settings);
44
58
  return await analyze(parsed.path, { ...parsed.options, scope }, ctx);
45
59
  }
46
60
  if (command === "finding" && rest.length === 1 && !rest[0].startsWith("-"))
@@ -70,7 +84,7 @@ export async function run(args, options = {}) {
70
84
  throw new Error(commandUsage.trimEnd());
71
85
  }
72
86
  catch (error) {
73
- write(errorText(error) + "\n");
87
+ write(errorText(error, fileMode) + "\n");
74
88
  return errorCode(error);
75
89
  }
76
90
  }
@@ -1,8 +1,9 @@
1
1
  import { CliRefusalError, failureReason, START_FAILURE_MESSAGE } from "../contract/cli-refusals.js";
2
- import { errorText } from "./api.js";
3
- export function mcpFailure(error, enabled) {
4
- // Only outside_project changed its sentence; the other local refusals already use the old sentences.
5
- const message = !enabled && error instanceof CliRefusalError && error.code === "outside_project" ? START_FAILURE_MESSAGE
6
- : errorText(error);
2
+ import { cliApiMessages } from "../contract/cli-api-messages.js";
3
+ import { errorText, SavedSignInError } from "./api.js";
4
+ export function mcpFailure(error, enabled, fileMode = true) {
5
+ const message = error instanceof SavedSignInError ? cliApiMessages.unreadable_sign_in
6
+ : !enabled && !fileMode && error instanceof CliRefusalError && error.code === "outside_project"
7
+ ? START_FAILURE_MESSAGE : errorText(error, fileMode);
7
8
  return { message, ...(enabled ? { reason: failureReason(error) } : {}) };
8
9
  }