@counterfact-labs/cli 0.2.6 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/bundle/client-release.json +1 -1
- package/dist/adapters/analysis-results.js +87 -0
- package/dist/adapters/combined-results.js +27 -0
- package/dist/adapters/completion-lock.js +95 -0
- package/dist/adapters/completion.js +132 -17
- package/dist/adapters/entry-files.js +129 -0
- package/dist/adapters/entry-packing.js +161 -0
- package/dist/adapters/entry-selection.js +57 -0
- package/dist/adapters/files.js +111 -5
- package/dist/adapters/git.js +10 -1
- package/dist/adapters/relay-plan.js +5 -7
- package/dist/adapters/release.js +4 -1
- package/dist/adapters/results.js +18 -3
- package/dist/adapters/selection.js +114 -0
- package/dist/adapters/settings.js +62 -4
- package/dist/adapters/source-relay-plan.js +7 -0
- package/dist/analysis-core/analysis/code-agents/skipped-files.js +1 -1
- package/dist/analysis-core/analysis/file-analysis-plan.js +1 -0
- package/dist/analysis-core/analysis/file-dependencies.js +1 -0
- package/dist/analysis-core/analysis/finding-activity-switch.js +1 -0
- package/dist/analysis-core/analysis/finding-identity.js +1 -0
- package/dist/analysis-core/analysis/finding-quality/policy.js +1 -0
- package/dist/analysis-core/analysis/folder-analysis-plan.js +1 -0
- package/dist/analysis-core/analysis/passes/full-file-passes.js +1 -0
- package/dist/analysis-core/analysis/project-architecture.js +1 -0
- package/dist/analysis-core/analysis/schemas.js +1 -1
- package/dist/analysis-core/chunks/2AASSZRO.js +35 -0
- package/dist/analysis-core/chunks/2YREYGOG.js +1 -0
- package/dist/analysis-core/chunks/33HRXJ5Q.js +1 -0
- package/dist/analysis-core/chunks/33IGHOYT.js +1 -0
- package/dist/analysis-core/chunks/4PT5C4IT.js +7 -0
- package/dist/analysis-core/chunks/4RXRAB5S.js +1 -0
- package/dist/analysis-core/chunks/4UDQBDDE.js +1 -0
- package/dist/analysis-core/chunks/{OPMA32A6.js → 5B5UCWD3.js} +1 -1
- package/dist/analysis-core/chunks/6YCABS5T.js +1 -0
- package/dist/analysis-core/chunks/CQQ4TTXE.js +1 -0
- package/dist/analysis-core/chunks/CTLHKFPH.js +19 -0
- package/dist/analysis-core/chunks/F54DOWKN.js +6 -0
- package/dist/analysis-core/chunks/FRXOQ2M6.js +3 -0
- package/dist/analysis-core/chunks/HCPO6QPU.js +1 -0
- package/dist/analysis-core/chunks/HHFDNY6V.js +14 -0
- package/dist/analysis-core/chunks/{3JWEFCTJ.js → HVQJKPQ4.js} +1 -1
- package/dist/analysis-core/chunks/INVTU73D.js +1 -0
- package/dist/analysis-core/chunks/JEWBPIQI.js +1 -0
- package/dist/analysis-core/chunks/JGK37AWN.js +1 -0
- package/dist/analysis-core/chunks/LFYTD7JF.js +1 -0
- package/dist/analysis-core/chunks/LMUXOKIM.js +1 -0
- package/dist/analysis-core/chunks/LOBIDUCA.js +1 -0
- package/dist/analysis-core/chunks/M5YWJY7P.js +1 -0
- package/dist/analysis-core/chunks/MGVIKA74.js +1 -0
- package/dist/analysis-core/chunks/MKYLK6DB.js +1 -0
- package/dist/analysis-core/chunks/MZOKZ3N6.js +1 -0
- package/dist/analysis-core/chunks/NXHVBJL4.js +1 -0
- package/dist/analysis-core/chunks/OKBK3JL3.js +1 -0
- package/dist/analysis-core/chunks/PBHHWFR6.js +1 -0
- package/dist/analysis-core/chunks/Q5WR6B2H.js +2 -0
- package/dist/analysis-core/chunks/QVXARKP7.js +1 -0
- package/dist/analysis-core/chunks/RQTNTIR4.js +1 -0
- package/dist/analysis-core/chunks/RUVMFAST.js +1 -0
- package/dist/analysis-core/chunks/S27VIQMZ.js +1 -0
- package/dist/analysis-core/chunks/{YDBO7P4X.js → S27Y2TP5.js} +1 -1
- package/dist/analysis-core/chunks/SJESFYBA.js +10 -0
- package/dist/analysis-core/chunks/SP6XFHNH.js +1 -0
- package/dist/analysis-core/chunks/VDUFPTDD.js +1 -0
- package/dist/analysis-core/chunks/W24SFYZM.js +67 -0
- package/dist/analysis-core/chunks/YTHF2E2S.js +1 -0
- package/dist/analysis-core/chunks/YWL52FVW.js +2 -0
- package/dist/analysis-core/contracts/analysis-selection.js +1 -0
- package/dist/analysis-core/contracts/analysis-size.js +1 -0
- package/dist/analysis-core/contracts/execution-request.js +1 -0
- package/dist/analysis-core/contracts/intake-finding.js +1 -0
- package/dist/analysis-core/contracts/model-call-identity.js +1 -0
- package/dist/analysis-core/contracts/module-resolution.js +1 -0
- package/dist/analysis-core/contracts/relay-analysis.js +1 -1
- package/dist/analysis-core/contracts/requests.js +1 -1
- package/dist/analysis-core/contracts/selection-paths.js +1 -0
- package/dist/analysis-core/conventions/analysis-fit.js +1 -1
- package/dist/analysis-core/conventions/file-admission.js +1 -1
- package/dist/analysis-core/conventions/read-limits.js +1 -0
- package/dist/analysis-core/import/calls/modules.js +1 -0
- package/dist/analysis-core/import/entry-selection.js +1 -0
- package/dist/analysis-core/import/outside-loader-sources.js +1 -1
- package/dist/analysis-core/index.js +3 -3
- package/dist/analysis-core/requests/answer-formats.js +1 -1
- package/dist/analysis-core/requests/local-plan.js +1 -1
- package/dist/analysis-core/requests/planned-limits.js +1 -0
- package/dist/analysis-core/requests/registry.js +1 -1
- package/dist/analysis-core/requests/template-version.js +1 -1
- package/dist/analysis-core/requests/tools.js +1 -1
- package/dist/analysis-core/settings.js +1 -1
- package/dist/analysis-core/stages/engine-entry.js +1 -1
- package/dist/bundle/seal/engine.js +1 -1
- package/dist/cli/analysis-selection.js +133 -0
- package/dist/cli/api.js +1 -1
- package/dist/cli/commands/analyze.js +82 -14
- package/dist/cli/commands/skipped.js +65 -0
- package/dist/cli/completion-delivery.js +63 -0
- package/dist/cli/completion-recovery.js +68 -14
- package/dist/cli/context.js +4 -2
- package/dist/cli/finding-display.js +102 -7
- package/dist/cli/finding-settings.js +3 -1
- package/dist/cli/intake-payload.js +26 -2
- package/dist/cli/main.js +37 -11
- package/dist/cli/mcp-failure.js +6 -5
- package/dist/cli/mcp.js +191 -26
- package/dist/cli/prior-completion.js +35 -0
- package/dist/cli/relay-check.js +260 -110
- package/dist/cli/relay-failure.js +32 -0
- package/dist/cli/relay-preparation.js +72 -21
- package/dist/cli/result-check.js +31 -12
- package/dist/cli/results-upload.js +131 -0
- package/dist/cli/saved-results.js +10 -0
- package/dist/cli/scope-choices.js +57 -0
- package/dist/cli/sign-in.js +2 -2
- package/dist/cli/skipped-files.js +22 -0
- package/dist/cli/summary.js +15 -1
- package/dist/contract/agent-choice.js +25 -2
- package/dist/contract/cli-api-messages.js +3 -0
- package/dist/contract/cli-refusals.js +39 -5
- package/dist/contract/cli-version.js +1 -1
- package/dist/contract/console-url.js +3 -0
- package/dist/contract/console.js +24 -40
- package/dist/contract/engine.js +1 -1
- package/dist/contract/messages.js +4 -3
- package/dist/contract/project-architecture.js +1 -0
- package/dist/contract/scope-choice.js +98 -0
- package/dist/contract/selection-metadata.js +9 -0
- package/dist/contract/selection-offer.js +40 -0
- package/dist/gatekeeper/cli-api.js +70 -4
- package/dist/gatekeeper/console.js +55 -28
- package/dist/gatekeeper/model.js +5 -23
- package/dist/gatekeeper/trust/policy.js +2 -1
- package/package.json +5 -3
- package/dist/analysis-core/analysis/source-notes/types.js +0 -1
- package/dist/analysis-core/chunks/2PSNOSJ2.js +0 -1
- package/dist/analysis-core/chunks/377MLK4M.js +0 -21
- package/dist/analysis-core/chunks/5DGPLXYB.js +0 -1
- package/dist/analysis-core/chunks/6B2HILSY.js +0 -1
- package/dist/analysis-core/chunks/6QY5BV5B.js +0 -14
- package/dist/analysis-core/chunks/6U2IR4XB.js +0 -1
- package/dist/analysis-core/chunks/DLPVYWHZ.js +0 -1
- package/dist/analysis-core/chunks/FOKDE64Y.js +0 -1
- package/dist/analysis-core/chunks/FSLWD2PE.js +0 -1
- package/dist/analysis-core/chunks/K2GNDX3W.js +0 -1
- package/dist/analysis-core/chunks/KTNA2KTN.js +0 -7
- package/dist/analysis-core/chunks/KWECQJ6X.js +0 -1
- package/dist/analysis-core/chunks/NIOS7U6C.js +0 -41
- package/dist/analysis-core/chunks/PECX3OXB.js +0 -1
- package/dist/analysis-core/chunks/PYKAERZZ.js +0 -1
- package/dist/analysis-core/chunks/QFY6A3MS.js +0 -66
- package/dist/analysis-core/chunks/SKVEBOGA.js +0 -1
- package/dist/analysis-core/chunks/STI4JE7L.js +0 -1
- package/dist/analysis-core/chunks/SVMEWGD3.js +0 -1
- package/dist/analysis-core/chunks/X7WZAA7S.js +0 -1
- package/dist/analysis-core/chunks/ZTOVBAG7.js +0 -1
- package/dist/analysis-core/data/finding-types.js +0 -1
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 } 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,26 +10,52 @@ 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
|
-
|
|
13
|
+
import { folderPartsEnabled, skippedFilesEnabled } from "../analysis-core/settings.js";
|
|
14
|
+
import { loadAnalysisScope } from "./scope-choices.js";
|
|
15
|
+
import { FILE_SCOPE_PUBLIC_USAGE } from "../contract/scope-choice.js";
|
|
14
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`;
|
|
15
20
|
const write = options.write ?? (text => { process.stdout.write(text); });
|
|
16
21
|
const emailSignIn = options.emailSignIn ?? readEmailSignIn(options.env);
|
|
17
|
-
|
|
22
|
+
let fileMode = false;
|
|
18
23
|
try {
|
|
19
24
|
const [command, ...rest] = args;
|
|
25
|
+
const env = options.startup?.settings ?? options.env ?? process.env;
|
|
26
|
+
// Help follows the CLI defaults without validating unrelated commands' analysis switches.
|
|
27
|
+
const selectionSettings = readSelectionSettings(env, { validate: command === "analyze" || command === "mcp" });
|
|
28
|
+
fileMode = fileScopeEnabled(selectionSettings);
|
|
29
|
+
const commandOptions = { ...options, emailSignIn, env: { ...(options.env ?? process.env), ...selectionSettings,
|
|
30
|
+
...(command === "analyze" || command === "mcp" ? {} : {
|
|
31
|
+
CF_FILE_SCOPE: undefined, CF_SCOPE_CHOICES: undefined, CF_LOCAL_IMPORT_RESOLUTION: undefined, CF_MULTI_ENTRY: undefined,
|
|
32
|
+
}) } };
|
|
33
|
+
const choices = scopeChoicesEnabled(selectionSettings);
|
|
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>]");
|
|
20
42
|
if (["--help", "-h", "help"].includes(command) || !command) {
|
|
21
|
-
write(
|
|
43
|
+
write(commandUsage);
|
|
22
44
|
return 0;
|
|
23
45
|
}
|
|
24
46
|
if (command === "analyze") {
|
|
25
47
|
let parsed;
|
|
26
48
|
try {
|
|
27
|
-
parsed = parseAnalyzeOptions(rest);
|
|
49
|
+
parsed = parseAnalyzeOptions(rest, choices, multiEntryEnabled(selectionSettings));
|
|
28
50
|
}
|
|
29
51
|
catch {
|
|
30
|
-
throw new Error(
|
|
52
|
+
throw new Error(commandUsage.trimEnd());
|
|
31
53
|
}
|
|
32
|
-
|
|
54
|
+
const ctx = commandContext(commandOptions);
|
|
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);
|
|
58
|
+
return await analyze(parsed.path, { ...parsed.options, scope }, ctx);
|
|
33
59
|
}
|
|
34
60
|
if (command === "finding" && rest.length === 1 && !rest[0].startsWith("-"))
|
|
35
61
|
return await finding(rest[0], commandContext(commandOptions));
|
|
@@ -39,12 +65,12 @@ export async function run(args, options = {}) {
|
|
|
39
65
|
parsed = parseLoginOptions(rest);
|
|
40
66
|
}
|
|
41
67
|
catch {
|
|
42
|
-
throw new Error(
|
|
68
|
+
throw new Error(commandUsage.trimEnd());
|
|
43
69
|
}
|
|
44
70
|
return await login(commandContext(commandOptions), parsed);
|
|
45
71
|
}
|
|
46
72
|
if (rest.length)
|
|
47
|
-
throw new Error(
|
|
73
|
+
throw new Error(commandUsage.trimEnd());
|
|
48
74
|
if (command === "balance")
|
|
49
75
|
return await balance(commandContext(commandOptions));
|
|
50
76
|
if (command === "login")
|
|
@@ -55,10 +81,10 @@ export async function run(args, options = {}) {
|
|
|
55
81
|
await serveMcp(process.stdin, process.stdout, options);
|
|
56
82
|
return 0;
|
|
57
83
|
}
|
|
58
|
-
throw new Error(
|
|
84
|
+
throw new Error(commandUsage.trimEnd());
|
|
59
85
|
}
|
|
60
86
|
catch (error) {
|
|
61
|
-
write(errorText(error) + "\n");
|
|
87
|
+
write(errorText(error, fileMode) + "\n");
|
|
62
88
|
return errorCode(error);
|
|
63
89
|
}
|
|
64
90
|
}
|
package/dist/cli/mcp-failure.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { CliRefusalError, failureReason, START_FAILURE_MESSAGE } from "../contract/cli-refusals.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const message =
|
|
6
|
-
:
|
|
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
|
}
|
package/dist/cli/mcp.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { folderPartsEnabled, skippedFilesEnabled } from "../analysis-core/settings.js";
|
|
2
|
+
import { readSkippedFilesSetting, readFolderPartsSetting } from "../adapters/settings.js";
|
|
1
3
|
import { createInterface } from "node:readline";
|
|
2
4
|
import { once } from "node:events";
|
|
3
5
|
import { resolveFolderPath } from "../adapters/folder-path.js";
|
|
4
|
-
import { CliRefusalError, failureReason } from "../contract/cli-refusals.js";
|
|
6
|
+
import { cliRefusals, CliRefusalError, failureReason } from "../contract/cli-refusals.js";
|
|
5
7
|
import { mcpFailure } from "./mcp-failure.js";
|
|
8
|
+
import { agentChoiceMessage } from "../contract/agent-choice.js";
|
|
6
9
|
import { errorCode } from "./api.js";
|
|
7
10
|
import { commandContext } from "./context.js";
|
|
8
11
|
import { analyze } from "./commands/analyze.js";
|
|
12
|
+
import { skipped } from "./commands/skipped.js";
|
|
9
13
|
import { CLI_VERSION } from "../contract/cli-version.js";
|
|
10
14
|
import { finding } from "./commands/finding.js";
|
|
11
15
|
import { balance } from "./commands/balance.js";
|
|
@@ -19,8 +23,48 @@ import { suggestedEmail } from "./suggested-email.js";
|
|
|
19
23
|
import { agentProposalInputSchema } from "../contract/agent-proposal.js";
|
|
20
24
|
import { fixRequestTools, waitingFixes, waitingFixCount, claimFix, FIX_DISCOVERY_TIMEOUT_MS } from "./fix-requests.js";
|
|
21
25
|
import { FINDING_PROJECT_MESSAGE, FindingReferences, numberedFindingInputSchema } from "./finding-lookup.js";
|
|
22
|
-
import {
|
|
26
|
+
import { formatFindingNumber, formatWaitingFixes, formatFindingActivity, presentSavedFindings, findingDetailReference, formatFindingContinuation } from "./finding-display.js";
|
|
27
|
+
import { groupByActivity } from "../analysis-core/analysis/schemas.js";
|
|
23
28
|
import { readFindingSettings } from "./finding-settings.js";
|
|
29
|
+
import { sizeSkippedFiles } from "../analysis-core/analysis/code-agents/skipped-files.js";
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
import { fileScopeEnabled, readCliSettings, readSelectionSettings, selectionModeEnabled, scopeChoicesEnabled, multiEntryEnabled } from "../adapters/settings.js";
|
|
32
|
+
import { resolveAnalysisSelection } from "./analysis-selection.js";
|
|
33
|
+
import { analysisScopeSchema, chooseScopeMcpResultSchema } from "../contract/scope-choice.js";
|
|
34
|
+
import { scopeChoiceRefusal } from "../contract/selection-offer.js";
|
|
35
|
+
import { loadAnalysisScope, scopeChoiceText } from "./scope-choices.js";
|
|
36
|
+
const fileAnalysisDescription = "Analyze an agent folder or one agent file on this computer. For a file, Counterfact reads that file and the project files it imports inside the launch folder. Each call that runs an analysis charges the user's balance unless a free analysis is available, so call it only when the user asks for an analysis. A free first analysis needs no top-up. After edits, suggest an analysis and wait for the user to ask.";
|
|
37
|
+
const scopeChoiceDescription = "If Counterfact returns scope choices, show what each includes and ask the user to choose before calling again with that scope, unless the user already chose it. Do not run all choices automatically.";
|
|
38
|
+
const fileAnalysisEnding = "Prices are listed on the top-up page and website. If the balance doesn't cover the analysis, show the returned message and top-up link.";
|
|
39
|
+
const filePathDescription = "The agent folder or agent file on this computer, inside the launch folder.";
|
|
40
|
+
const fileScopeDescription = "Optional: a scope returned by Counterfact, using the same snapshot and selected file. Include all imports, no imports, or the listed imports chosen by the user.";
|
|
41
|
+
const fileProposalDescription = "Optional: the files you believe make up the agent, with each file's role. For a folder, paths are relative to that folder. For a file, paths are relative to the launch folder and must belong to the selected file's import scope.";
|
|
42
|
+
const fileProposalPathDescription = "Paths are relative to the selected folder, or to the launch folder when selecting a file.";
|
|
43
|
+
const multiAnalysisDescription = "Analyze an agent folder, one agent file, or several agent files on this computer. Pass path for one folder or file, or paths for several files inside the launch folder. Counterfact includes each file's project imports and reads shared files once. Up to 5 agent files run as one analysis with one result list and one charge unless a free analysis is available. If the selection is too large or has more than 5 agent files, Counterfact returns scope choices. Call only when the user asks for an analysis. A free first analysis needs no top-up. After edits, suggest an analysis and wait for the user to ask.";
|
|
44
|
+
const multiPathsDescription = "Optional: several agent file paths inside the launch folder, analyzed together. Use either path or paths. Folders cannot be mixed into paths.";
|
|
45
|
+
const partChoiceDescription = " If parts are returned, ask the user to choose one, then call analyze_agent again with that part's files as scope.";
|
|
46
|
+
const partScopeSchema = { type: "array", items: { type: "string" }, minItems: 1,
|
|
47
|
+
description: "The files in the part the user chose. Ask the user to pick a returned part before supplying scope." };
|
|
48
|
+
function fileAnalysisTool(choices, parts, multiple) {
|
|
49
|
+
const proposal = structuredClone(agentProposalInputSchema);
|
|
50
|
+
if (typeof proposal.properties?.files === "object" && proposal.properties.files && "items" in proposal.properties.files) {
|
|
51
|
+
const files = proposal.properties.files.items;
|
|
52
|
+
if (files && !Array.isArray(files) && typeof files === "object" && typeof files.properties?.path === "object")
|
|
53
|
+
files.properties.path.description = fileProposalPathDescription;
|
|
54
|
+
}
|
|
55
|
+
const scope = z.toJSONSchema(analysisScopeSchema);
|
|
56
|
+
delete scope.$schema;
|
|
57
|
+
if (!multiple && scope.properties)
|
|
58
|
+
delete scope.properties.entries;
|
|
59
|
+
const scopeSchema = parts && choices ? { anyOf: [scope, partScopeSchema] } : parts ? partScopeSchema : scope;
|
|
60
|
+
return { name: ANALYZE_TOOL_NAME,
|
|
61
|
+
description: [multiple ? multiAnalysisDescription : fileAnalysisDescription, ...(choices ? [scopeChoiceDescription] : []), fileAnalysisEnding].join(" ") + (parts ? partChoiceDescription : ""),
|
|
62
|
+
inputSchema: { type: "object", properties: { path: { type: "string", description: filePathDescription },
|
|
63
|
+
...(multiple ? { paths: { type: "array", items: { type: "string" }, minItems: 1, description: multiPathsDescription } } : {}),
|
|
64
|
+
proposal: { ...proposal, description: fileProposalDescription }, ...(choices || parts ? { scope: { ...scopeSchema, description: choices && parts
|
|
65
|
+
? `${fileScopeDescription} ${partScopeSchema.description}` : parts ? partScopeSchema.description : fileScopeDescription } } : {}) },
|
|
66
|
+
...(multiple ? {} : { required: ["path"] }), additionalProperties: false } };
|
|
67
|
+
}
|
|
24
68
|
const legacyTools = [
|
|
25
69
|
{
|
|
26
70
|
name: ANALYZE_TOOL_NAME,
|
|
@@ -32,6 +76,12 @@ const legacyTools = [
|
|
|
32
76
|
{ name: "sign_in_status", description: "Check whether this computer is signed in to Counterfact, and whether the user has allowed the analysis tool in Claude Code. With wait: true, first wait for a sign-in started by sign_in to finish.", inputSchema: { type: "object", properties: { wait: { type: "boolean" } }, additionalProperties: false } },
|
|
33
77
|
{ name: "sign_in", description: "Open the Counterfact sign-in page in the user's browser. Then call sign_in_status with wait: true.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
34
78
|
];
|
|
79
|
+
const skippedTool = {
|
|
80
|
+
name: "analyze_skipped_files",
|
|
81
|
+
description: "Handle files skipped by the saved analysis of a local folder. Use action files to list their names, or plan to find the fewest analyses that fit, including imported files. Tell the user that count before starting. Use action analyze only after the user accepts the offer to analyze the skipped files. Findings are added to the same results.",
|
|
82
|
+
inputSchema: { type: "object", properties: { path: { type: "string", description: "The original agent folder on this computer." },
|
|
83
|
+
action: { type: "string", enum: ["files", "plan", "analyze"] } }, required: ["path", "action"], additionalProperties: false },
|
|
84
|
+
};
|
|
35
85
|
class ProtocolError extends Error {
|
|
36
86
|
code;
|
|
37
87
|
constructor(code, message) {
|
|
@@ -39,6 +89,7 @@ class ProtocolError extends Error {
|
|
|
39
89
|
this.code = code;
|
|
40
90
|
}
|
|
41
91
|
}
|
|
92
|
+
const MAX_ANALYSIS_FINDING_HISTORY = 5;
|
|
42
93
|
function object(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
43
94
|
export async function serveMcp(input, output, options = {}) {
|
|
44
95
|
const audienceChoice = (options.env ?? process.env).CF_FINDING_AUDIENCE;
|
|
@@ -47,10 +98,24 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
47
98
|
const failureReasons = (options.env ?? process.env).CF_MCP_FAILURE_REASONS !== "0";
|
|
48
99
|
const launch = guarded ? resolveFolderPath(cwd).catch(() => null) : Promise.resolve(cwd);
|
|
49
100
|
const lastAnalysis = new Map();
|
|
50
|
-
const
|
|
51
|
-
const {
|
|
101
|
+
const startupSettings = options.startup?.settings ?? options.env ?? process.env;
|
|
102
|
+
const skippedSettings = Object.freeze({ CF_SKIPPED_FILES: readSkippedFilesSetting(startupSettings.CF_SKIPPED_FILES) });
|
|
103
|
+
const skippedEnabled = skippedFilesEnabled(skippedSettings);
|
|
104
|
+
const partSettings = Object.freeze({ CF_FOLDER_PARTS: readFolderPartsSetting(startupSettings.CF_FOLDER_PARTS) });
|
|
105
|
+
const scopeEnabled = folderPartsEnabled({ ...skippedSettings, ...partSettings });
|
|
106
|
+
const findingSettings = options.findingSettings ?? readFindingSettings(options.env);
|
|
107
|
+
const notActiveFindings = new Map();
|
|
108
|
+
const analysisFindings = new Map();
|
|
109
|
+
const { numbersEnabled, fixesEnabled, agentReadableResults } = findingSettings;
|
|
110
|
+
const analysisSwitches = readSelectionSettings(startupSettings);
|
|
111
|
+
const choicesEnabled = scopeChoicesEnabled(analysisSwitches);
|
|
112
|
+
const selectionEnabled = selectionModeEnabled(analysisSwitches);
|
|
113
|
+
const multipleEnabled = multiEntryEnabled(analysisSwitches);
|
|
114
|
+
const analysisEnv = selectionEnabled ? { ...(options.env ?? process.env), ...analysisSwitches } : undefined;
|
|
115
|
+
let analysisStartup = options.startup;
|
|
52
116
|
const references = new FindingReferences();
|
|
53
|
-
const
|
|
117
|
+
const emailEnv = analysisEnv && Object.hasOwn(analysisEnv, "CF_EMAIL_SIGN_IN") ? analysisEnv : options.env;
|
|
118
|
+
const emailSignIn = options.emailSignIn ?? readEmailSignIn(emailEnv);
|
|
54
119
|
const signInTools = emailSignIn ? [...legacyTools.slice(0, 3),
|
|
55
120
|
{
|
|
56
121
|
name: "sign_in_status",
|
|
@@ -76,9 +141,19 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
76
141
|
}, required: ["email", "code"], additionalProperties: false },
|
|
77
142
|
},
|
|
78
143
|
] : legacyTools;
|
|
79
|
-
const tools = signInTools.map(tool =>
|
|
80
|
-
|
|
81
|
-
|
|
144
|
+
const tools = [...signInTools.map(tool => {
|
|
145
|
+
if (tool.name === ANALYZE_TOOL_NAME) {
|
|
146
|
+
if (selectionEnabled)
|
|
147
|
+
return fileAnalysisTool(choicesEnabled, scopeEnabled, multipleEnabled);
|
|
148
|
+
if (scopeEnabled)
|
|
149
|
+
return { ...tool,
|
|
150
|
+
description: tool.description + partChoiceDescription,
|
|
151
|
+
inputSchema: { ...tool.inputSchema, properties: { ...tool.inputSchema.properties, scope: partScopeSchema } } };
|
|
152
|
+
}
|
|
153
|
+
return tool;
|
|
154
|
+
}).map(tool => tool.name === "get_finding" && numbersEnabled ? {
|
|
155
|
+
...tool, inputSchema: numberedFindingInputSchema,
|
|
156
|
+
} : tool), ...(fixesEnabled ? fixRequestTools : []), ...(skippedEnabled ? [skippedTool] : [])];
|
|
82
157
|
let pendingSignIn;
|
|
83
158
|
let signInFailure;
|
|
84
159
|
const requests = new Map();
|
|
@@ -91,14 +166,21 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
91
166
|
if (!object(args))
|
|
92
167
|
throw new ProtocolError(-32602, "Tool arguments must be an object.");
|
|
93
168
|
const name = params.name;
|
|
94
|
-
const allowed = name === ANALYZE_TOOL_NAME ? ["path", "proposal"] : name === "get_finding" ? (numbersEnabled ? Object.keys(numberedFindingInputSchema.properties) : ["id"])
|
|
169
|
+
const allowed = name === "analyze_skipped_files" ? ["path", "action"] : name === ANALYZE_TOOL_NAME ? ["path", "proposal", ...(multipleEnabled ? ["paths"] : []), ...((scopeEnabled || choicesEnabled) ? ["scope"] : [])] : name === "get_finding" ? (numbersEnabled ? Object.keys(numberedFindingInputSchema.properties) : ["id"])
|
|
95
170
|
: name === "claim_fix" ? ["id"]
|
|
96
171
|
: name === "sign_in_status" ? (emailSignIn ? ["wait", "path"] : ["wait"])
|
|
97
172
|
: emailSignIn && name === "sign_in" ? ["email"] : emailSignIn && name === "sign_in_with_code" ? ["email", "code"] : [];
|
|
98
173
|
if (!tools.some(tool => tool.name === name) || Object.keys(args).some(key => !allowed.includes(key)))
|
|
99
174
|
throw new ProtocolError(-32602, "Unknown tool or argument.");
|
|
100
|
-
if (name === ANALYZE_TOOL_NAME && (
|
|
175
|
+
if (name === ANALYZE_TOOL_NAME && (args.paths === undefined
|
|
176
|
+
? typeof args.path !== "string" || !args.path.trim()
|
|
177
|
+
: args.path !== undefined || !Array.isArray(args.paths) || !args.paths.length || args.paths.some(path => typeof path !== "string" || !path.trim()))) {
|
|
178
|
+
throw new ProtocolError(-32602, "Provide a path.");
|
|
179
|
+
}
|
|
180
|
+
if (name === "analyze_skipped_files" && (typeof args.path !== "string" || !args.path.trim()))
|
|
101
181
|
throw new ProtocolError(-32602, "Provide a path.");
|
|
182
|
+
if (name === "analyze_skipped_files" && !["files", "plan", "analyze"].includes(args.action))
|
|
183
|
+
throw new ProtocolError(-32602, "Choose files, plan, or analyze.");
|
|
102
184
|
if (name === "get_finding" && (typeof args.id !== "string" || !args.id.trim()))
|
|
103
185
|
throw new ProtocolError(-32602, "Provide a finding id.");
|
|
104
186
|
if (name === "get_finding" && args.projectId !== undefined && (typeof args.projectId !== "string" || !args.projectId.trim()))
|
|
@@ -117,27 +199,63 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
117
199
|
let reason;
|
|
118
200
|
let freeAnalysisAvailable = false;
|
|
119
201
|
let agentChoice;
|
|
202
|
+
let scopeChoice;
|
|
120
203
|
let findingIds;
|
|
121
204
|
let findings, projectId;
|
|
205
|
+
let skippedFilesCount;
|
|
206
|
+
let topFindingIds;
|
|
207
|
+
let notActiveCount, analysisId;
|
|
208
|
+
let nextFinding;
|
|
122
209
|
let loadedFinding;
|
|
210
|
+
let notActiveHint = "";
|
|
123
211
|
let fixRequests, claimed;
|
|
124
212
|
let waitingCount;
|
|
125
213
|
let url, analysisFolder;
|
|
126
214
|
let started = false;
|
|
127
215
|
let signIn;
|
|
128
216
|
try {
|
|
129
|
-
|
|
217
|
+
// Listing tools must not validate settings used only when a tool runs.
|
|
218
|
+
if (selectionEnabled)
|
|
219
|
+
analysisStartup ??= readCliSettings(analysisEnv, emailSignIn);
|
|
220
|
+
const analysisSettings = analysisStartup?.settings ?? {};
|
|
221
|
+
const ctx = commandContext({ ...options, ...(analysisStartup ? { startup: analysisStartup } : {}), env: { ...(analysisEnv ?? options.env ?? process.env), ...analysisSwitches, ...skippedSettings, ...partSettings, CF_FINDING_AUDIENCE: audienceChoice },
|
|
130
222
|
onCheckFailure: failure => { reason = failure.reason; options.onCheckFailure?.(failure); },
|
|
131
223
|
emailSignIn, findingSettings, signal, onCheckStart: () => {
|
|
132
224
|
started = true;
|
|
133
225
|
if (guarded && analysisFolder)
|
|
134
226
|
lastAnalysis.set(analysisFolder, ctx.now());
|
|
135
227
|
options.onCheckStart?.();
|
|
228
|
+
}, onScopeChoice: choice => {
|
|
229
|
+
scopeChoice = choice;
|
|
230
|
+
if (guarded && analysisFolder)
|
|
231
|
+
lastAnalysis.delete(analysisFolder);
|
|
232
|
+
options.onScopeChoice?.(choice);
|
|
136
233
|
}, onAgentChoice: choice => { agentChoice = choice; options.onAgentChoice?.(choice); }, onCheckComplete: result => {
|
|
137
234
|
findingIds = result.findingIds ?? [];
|
|
235
|
+
topFindingIds = result.topFindingIds;
|
|
236
|
+
if (skippedEnabled && result.skippedFiles)
|
|
237
|
+
skippedFilesCount = sizeSkippedFiles(result.skippedFiles).length;
|
|
238
|
+
const groups = groupByActivity(result.findings ?? [], result.notActiveEnabled);
|
|
239
|
+
notActiveFindings.set(result.projectId, groups.notActive);
|
|
240
|
+
if (groups.notActive.length) {
|
|
241
|
+
notActiveCount = groups.notActive.length;
|
|
242
|
+
analysisId = result.analysisId;
|
|
243
|
+
const ids = groups.notActive.map(finding => numbersEnabled && finding.seq !== undefined ? formatFindingNumber(finding.seq) : finding.id);
|
|
244
|
+
notActiveHint = `Not active details: use get_finding for ${ids.join(", ")}.\n`;
|
|
245
|
+
}
|
|
138
246
|
if (numbersEnabled) {
|
|
139
|
-
|
|
140
|
-
audienceEnabled: result.audienceEnabled,
|
|
247
|
+
const displayed = presentSavedFindings(result, { numbersEnabled, agentReadableResults,
|
|
248
|
+
audienceEnabled: result.audienceEnabled, notActiveEnabled: result.notActiveEnabled, projectId: result.projectId, surface: "mcp" });
|
|
249
|
+
findings = displayed.findings;
|
|
250
|
+
findingIds = displayed.findingIds;
|
|
251
|
+
nextFinding = displayed.nextFinding;
|
|
252
|
+
if (agentReadableResults && result.analysisId !== undefined) {
|
|
253
|
+
analysisFindings.delete(result.analysisId);
|
|
254
|
+
analysisFindings.set(result.analysisId, { projectId: result.projectId, findings: displayed.orderedReferences });
|
|
255
|
+
// Older details remain available from the console without keeping every chain in memory.
|
|
256
|
+
if (analysisFindings.size > MAX_ANALYSIS_FINDING_HISTORY)
|
|
257
|
+
analysisFindings.delete(analysisFindings.keys().next().value);
|
|
258
|
+
}
|
|
141
259
|
projectId = result.projectId;
|
|
142
260
|
references.remember(result.projectId);
|
|
143
261
|
}
|
|
@@ -161,9 +279,23 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
161
279
|
else if (name === "get_finding") {
|
|
162
280
|
const project = numbersEnabled ? await references.resolve(args.id, args.projectId, fixesEnabled ? () => waitingFixes(ctx.api, { timeoutMs: FIX_DISCOVERY_TIMEOUT_MS }) : undefined) : undefined;
|
|
163
281
|
await finding(args.id, ctx, { projectId: project, numbers: numbersEnabled, onLoaded: value => {
|
|
164
|
-
|
|
165
|
-
|
|
282
|
+
// Older consoles retain the finding and fixes but may not store its activity yet.
|
|
283
|
+
const activity = [...notActiveFindings.values()].flat().find(saved => saved.id === value.id)?.activity;
|
|
284
|
+
if (activity)
|
|
285
|
+
ctx.write(formatFindingActivity(activity));
|
|
286
|
+
if (numbersEnabled || activity)
|
|
287
|
+
loadedFinding = { ...value, ...(activity ? { activity } : {}),
|
|
288
|
+
...(numbersEnabled && value.seq !== undefined ? { number: formatFindingNumber(value.seq) } : {}) };
|
|
289
|
+
if (agentReadableResults && numbersEnabled) {
|
|
290
|
+
for (const { projectId, findings } of analysisFindings.values()) {
|
|
291
|
+
const index = findings.findIndex(item => item.id === value.id);
|
|
292
|
+
if (index >= 0 && findings[index + 1])
|
|
293
|
+
nextFinding = findingDetailReference(findings[index + 1], projectId);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
166
296
|
} });
|
|
297
|
+
if (nextFinding)
|
|
298
|
+
text += formatFindingContinuation(nextFinding);
|
|
167
299
|
}
|
|
168
300
|
else if (name === "get_balance")
|
|
169
301
|
await balance(ctx, { onLoaded: value => { freeAnalysisAvailable = value.freeAnalysisAvailable; } });
|
|
@@ -208,7 +340,7 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
208
340
|
const failure = signInFailure;
|
|
209
341
|
signInFailure = undefined;
|
|
210
342
|
if (failure && !signIn.signedIn) {
|
|
211
|
-
const shown = mcpFailure(failure.error, failureReasons);
|
|
343
|
+
const shown = mcpFailure(failure.error, failureReasons, fileScopeEnabled(analysisSwitches));
|
|
212
344
|
text = (failure.error instanceof SignInExpiredError
|
|
213
345
|
? emailSignInMessages.mcp_expired : failure.error instanceof SignInDeniedError
|
|
214
346
|
? emailSignInMessages.mcp_cancelled : shown.message) + "\n";
|
|
@@ -244,7 +376,7 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
244
376
|
signal.addEventListener("abort", cancel, { once: true });
|
|
245
377
|
try {
|
|
246
378
|
signal.throwIfAborted();
|
|
247
|
-
const flow = await startSignIn(commandContext({ ...options, emailSignIn, findingSettings, homeDir: ctx.homeDir, baseUrl: ctx.api.baseUrl, signal: controller.signal }));
|
|
379
|
+
const flow = await startSignIn(commandContext({ ...options, startup: ctx.startup, emailSignIn, findingSettings, homeDir: ctx.homeDir, baseUrl: ctx.api.baseUrl, signal: controller.signal }));
|
|
248
380
|
pending = { flow, controller, opened: false };
|
|
249
381
|
pendingSignIn = pending;
|
|
250
382
|
signInFailure = undefined;
|
|
@@ -272,34 +404,67 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
272
404
|
const launchFolder = await launch;
|
|
273
405
|
if (guarded && !launchFolder)
|
|
274
406
|
throw new CliRefusalError("launch_folder_unavailable");
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
407
|
+
const partScope = Array.isArray(args.scope) ? args.scope : undefined;
|
|
408
|
+
if (partScope && (!scopeEnabled || args.proposal !== undefined))
|
|
409
|
+
throw new Error(cliRefusals.invalid_body);
|
|
410
|
+
const scope = partScope ? undefined : await loadAnalysisScope({ scope: args.scope }, cwd, analysisSettings);
|
|
411
|
+
const selection = selectionEnabled ? await resolveAnalysisSelection((args.paths ?? args.path), launchFolder ?? cwd, analysisSettings, scope) : undefined;
|
|
412
|
+
if (guarded && selection?.kind === "folder")
|
|
413
|
+
await resolveMcpAgentFolder(selection.path, launchFolder, ctx.homeDir);
|
|
414
|
+
const folder = selection?.path ?? (guarded ? await resolveMcpAgentFolder(args.path, launchFolder, ctx.homeDir) : resolveAgentFolder(args.path, cwd));
|
|
415
|
+
const request = selection?.request;
|
|
416
|
+
const originalEntries = request?.kind === "file" ? request.entries ?? [request.entry] : undefined;
|
|
417
|
+
const chosen = request?.kind === "file" ? request.scope : undefined;
|
|
418
|
+
const includedEntries = originalEntries && (request?.kind === "file" && request.entries && chosen && chosen.imports !== "all"
|
|
419
|
+
? originalEntries.filter(entry => entry === chosen.entry || Array.isArray(chosen.imports) && chosen.imports.includes(entry))
|
|
420
|
+
: originalEntries);
|
|
421
|
+
const key = selection ? includedEntries
|
|
422
|
+
? JSON.stringify([selection.launchRoot, [...includedEntries].sort()]) : JSON.stringify([selection.launchRoot, selection.path]) : folder;
|
|
423
|
+
const last = lastAnalysis.get(key);
|
|
424
|
+
if (name === ANALYZE_TOOL_NAME && guarded && last !== undefined && ctx.now() - last < 10 * 60 * 1000) {
|
|
278
425
|
throw new CliRefusalError("repeat_too_soon");
|
|
279
426
|
}
|
|
280
|
-
analysisFolder =
|
|
427
|
+
analysisFolder = key;
|
|
281
428
|
try {
|
|
282
|
-
exitCode = await
|
|
429
|
+
exitCode = name === "analyze_skipped_files" ? await skipped(folder, args.action, ctx, analyze, "mcp")
|
|
430
|
+
: await analyze(folder, { proposal: args.proposal, scope: partScope ?? scope, selection, surface: "mcp", analyzedFolder: args.path }, ctx);
|
|
283
431
|
}
|
|
284
432
|
finally {
|
|
285
|
-
if (
|
|
286
|
-
lastAnalysis.
|
|
433
|
+
if (reason === "results_pending")
|
|
434
|
+
lastAnalysis.delete(key);
|
|
435
|
+
else if (guarded && started && !scopeChoice)
|
|
436
|
+
lastAnalysis.set(key, ctx.now());
|
|
287
437
|
}
|
|
288
438
|
}
|
|
289
439
|
}
|
|
290
440
|
catch (error) {
|
|
291
|
-
const failure = mcpFailure(error, failureReasons);
|
|
441
|
+
const failure = mcpFailure(error, failureReasons, fileScopeEnabled(analysisSwitches));
|
|
292
442
|
text += failure.message + "\n";
|
|
293
443
|
reason = failure.reason;
|
|
294
444
|
exitCode = errorCode(error);
|
|
295
445
|
}
|
|
446
|
+
// Choices return before other output today; replace the terminal command list with the names-only MCP list.
|
|
447
|
+
if (agentChoice?.kind === "choose_scope")
|
|
448
|
+
text = agentChoiceMessage(agentChoice, "mcp") + "\n";
|
|
449
|
+
text += notActiveHint;
|
|
450
|
+
if (scopeChoice) {
|
|
451
|
+
const refusal = scopeChoiceRefusal(scopeChoice);
|
|
452
|
+
if (!refusal)
|
|
453
|
+
return chooseScopeMcpResultSchema.parse({ content: [{ type: "text", text: scopeChoiceText(scopeChoice) }], isError: false,
|
|
454
|
+
structuredContent: { ...scopeChoice, exitCode: 0 } });
|
|
455
|
+
text = refusal + "\n";
|
|
456
|
+
exitCode = 1;
|
|
457
|
+
}
|
|
296
458
|
// Some clients (Claude Code among them) give the model only the structured result when one is present, so it
|
|
297
459
|
// repeats the text: without it the model cannot read the result.
|
|
298
460
|
return { content: [{ type: "text", text }], isError: exitCode !== 0,
|
|
299
461
|
structuredContent: { text, exitCode, ...agentChoice, ...(findingIds ? { findingIds } : {}), ...(url ? { url } : {}), ...signIn,
|
|
300
462
|
...(exitCode !== 0 && failureReasons ? { reason: reason ?? failureReason(undefined) } : {}),
|
|
301
463
|
...(freeAnalysisAvailable ? { freeAnalysisAvailable } : {}),
|
|
302
|
-
...(
|
|
464
|
+
...(skippedFilesCount ? { skippedFilesCount } : {}),
|
|
465
|
+
...(topFindingIds ? { topFindingIds } : {}),
|
|
466
|
+
...(notActiveCount === undefined ? {} : { notActiveCount }), ...(analysisId === undefined ? {} : { analysisId }),
|
|
467
|
+
...(findings ? { findings, projectId } : {}), ...(nextFinding ? { nextFinding } : {}), ...(loadedFinding ? { finding: loadedFinding } : {}),
|
|
303
468
|
...(fixRequests ? { fixRequests } : {}), ...claimed, ...(waitingCount !== undefined ? { waitingFixes: waitingCount } : {}) } };
|
|
304
469
|
}
|
|
305
470
|
async function respond(message, id, signal) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { discardCompletion, loadCompletions, matchesCompletionSetup } from "../adapters/completion.js";
|
|
2
|
+
import { completionRecoveryDecision } from "./completion-recovery.js";
|
|
3
|
+
export class CompletionSettingsMismatchError extends Error {
|
|
4
|
+
reason = "completion_settings_mismatch";
|
|
5
|
+
}
|
|
6
|
+
/** Leave other setups alone before the runner assigns any reservation or recovery state. */
|
|
7
|
+
export async function priorCompletion(folder, options) {
|
|
8
|
+
const records = await loadCompletions(folder, { retainUnsaved: options.retryEnabled,
|
|
9
|
+
consoleOrigin: options.consoleOrigin, projectId: options.projectId, identity: options.identity });
|
|
10
|
+
const notices = new Set();
|
|
11
|
+
let prior;
|
|
12
|
+
for (const item of records) {
|
|
13
|
+
const { record } = item;
|
|
14
|
+
if ((record.uploaded || record.finished) && !options.retryUpload)
|
|
15
|
+
continue;
|
|
16
|
+
const localMismatch = !matchesCompletionSetup(record, options);
|
|
17
|
+
const decision = completionRecoveryDecision({ retryEnabled: options.retryEnabled, payload: record.payload,
|
|
18
|
+
localMismatch, timing: { completedAt: record.completedAt, absoluteExpiresAt: record.absoluteExpiresAt },
|
|
19
|
+
now: (options.now ?? Date.now)(), statusLookup: { kind: "skipped" }, uploadOutcome: { kind: record.uploaded ? "saved" : "pending" } });
|
|
20
|
+
if (decision.action === "leave") {
|
|
21
|
+
notices.add(decision.message);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (localMismatch && decision.action === "give-up") {
|
|
25
|
+
await discardCompletion(folder, item.path, options.identity);
|
|
26
|
+
if (decision.message)
|
|
27
|
+
notices.add(decision.message);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (localMismatch)
|
|
31
|
+
throw new CompletionSettingsMismatchError("message" in decision ? decision.message : undefined);
|
|
32
|
+
prior ??= item;
|
|
33
|
+
}
|
|
34
|
+
return { prior, notices: [...notices] };
|
|
35
|
+
}
|