@counterfact-labs/cli 0.2.5 → 0.2.7
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/completion-lock.js +95 -0
- package/dist/adapters/completion.js +132 -17
- package/dist/adapters/entry-files.js +126 -0
- package/dist/adapters/entry-selection.js +45 -0
- package/dist/adapters/files.js +2 -1
- package/dist/adapters/results.js +10 -2
- package/dist/adapters/selection.js +104 -0
- package/dist/adapters/settings.js +29 -2
- package/dist/analysis-core/analysis/schemas.js +1 -1
- package/dist/analysis-core/chunks/2PZF54KH.js +10 -0
- package/dist/analysis-core/chunks/{OPMA32A6.js → 6RWEWBHK.js} +1 -1
- package/dist/analysis-core/chunks/{PECX3OXB.js → 7NEWZ74F.js} +1 -1
- package/dist/analysis-core/chunks/{6QY5BV5B.js → CGCJN73D.js} +1 -1
- package/dist/analysis-core/chunks/CWYVUGG4.js +1 -0
- package/dist/analysis-core/chunks/{3JWEFCTJ.js → FBTOOUPL.js} +1 -1
- package/dist/analysis-core/chunks/GOQVKWP3.js +32 -0
- package/dist/analysis-core/chunks/IHAVDX4L.js +66 -0
- package/dist/analysis-core/chunks/IVULXWQN.js +21 -0
- package/dist/analysis-core/chunks/K5FUABWW.js +1 -0
- package/dist/analysis-core/chunks/KGQYR62R.js +7 -0
- package/dist/analysis-core/chunks/PY5M22CL.js +1 -0
- package/dist/analysis-core/chunks/{YDBO7P4X.js → RNTMH4MU.js} +1 -1
- package/dist/analysis-core/chunks/RUVMFAST.js +1 -0
- package/dist/analysis-core/chunks/S27VIQMZ.js +1 -0
- package/dist/analysis-core/chunks/{5DGPLXYB.js → SLJ6CW7W.js} +1 -1
- package/dist/analysis-core/chunks/{STI4JE7L.js → UEORNVQB.js} +1 -1
- package/dist/analysis-core/chunks/{X7WZAA7S.js → WYLIV2P2.js} +1 -1
- package/dist/analysis-core/contracts/analysis-selection.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/import/calls/modules.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/local-plan.js +1 -1
- 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/cli/agent-folder.js +3 -3
- package/dist/cli/analysis-selection.js +91 -0
- package/dist/cli/commands/analyze.js +34 -8
- package/dist/cli/commands/balance.js +6 -3
- package/dist/cli/completion-delivery.js +63 -0
- package/dist/cli/completion-recovery.js +68 -14
- package/dist/cli/context.js +4 -1
- package/dist/cli/main.js +21 -9
- package/dist/cli/mcp-failure.js +8 -0
- package/dist/cli/mcp.js +130 -30
- package/dist/cli/prior-completion.js +35 -0
- package/dist/cli/relay-check.js +163 -97
- package/dist/cli/relay-failure.js +32 -0
- package/dist/cli/relay-preparation.js +24 -11
- package/dist/cli/results-upload.js +131 -0
- package/dist/cli/scope-choices.js +57 -0
- package/dist/cli/sign-in.js +61 -2
- package/dist/contract/cli-api-messages.js +2 -0
- package/dist/contract/cli-refusals.js +74 -1
- package/dist/contract/cli-version.js +1 -1
- package/dist/contract/console.js +7 -2
- package/dist/contract/email-sign-in-messages.js +4 -0
- package/dist/contract/scope-choice.js +78 -0
- package/dist/contract/selection-metadata.js +9 -0
- package/dist/contract/selection-offer.js +43 -0
- package/dist/gatekeeper/cli-api.js +16 -7
- package/dist/gatekeeper/connection-exchange.js +1 -0
- package/dist/gatekeeper/console.js +61 -36
- package/package.json +2 -2
- package/dist/analysis-core/chunks/377MLK4M.js +0 -21
- package/dist/analysis-core/chunks/DLPVYWHZ.js +0 -1
- package/dist/analysis-core/chunks/FOKDE64Y.js +0 -1
- package/dist/analysis-core/chunks/KTNA2KTN.js +0 -7
- package/dist/analysis-core/chunks/NIOS7U6C.js +0 -41
- package/dist/analysis-core/chunks/QFY6A3MS.js +0 -66
- package/dist/analysis-core/chunks/SVMEWGD3.js +0 -1
- package/dist/analysis-core/chunks/ZTOVBAG7.js +0 -1
package/dist/cli/context.js
CHANGED
|
@@ -8,7 +8,7 @@ import { userEmail } from "./git.js";
|
|
|
8
8
|
import { terminalSignInInput } from "./sign-in-input.js";
|
|
9
9
|
import { readFindingSettings } from "./finding-settings.js";
|
|
10
10
|
export function commandContext(options = {}) {
|
|
11
|
-
const startup = readCliSettings(options.env, options.emailSignIn);
|
|
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
14
|
const api = new ApiClient(baseUrl, () => readCredentials(homeDir, api.baseUrl), undefined, options.signal);
|
|
@@ -16,8 +16,11 @@ export function commandContext(options = {}) {
|
|
|
16
16
|
const findingSettings = options.findingSettings ?? readFindingSettings(options.env);
|
|
17
17
|
return {
|
|
18
18
|
findingNumbers: findingSettings.numbersEnabled,
|
|
19
|
+
signInRetry: (options.env ?? process.env).CF_SIGN_IN_RETRY !== "0",
|
|
19
20
|
startup, signal: options.signal, relayRuntime: options.relayRuntime, cwd: options.cwd ?? process.cwd(),
|
|
20
21
|
token: () => readCredentials(homeDir, api.baseUrl), onCheckComplete: options.onCheckComplete, onCheckStart: options.onCheckStart, onAgentChoice: options.onAgentChoice,
|
|
22
|
+
onCheckFailure: options.onCheckFailure,
|
|
23
|
+
onScopeChoice: options.onScopeChoice,
|
|
21
24
|
api, homeDir, write, openBrowser: options.openBrowser ?? openBrowser,
|
|
22
25
|
signInInput: options.signInInput ?? terminalSignInInput(), readEmail: options.readEmail ?? userEmail,
|
|
23
26
|
sleep: options.sleep ?? (async (ms) => { await delay(ms, undefined, { signal: options.signal }); }), now: options.now ?? Date.now,
|
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 { readEmailSignIn, readSelectionSettings, scopeChoicesEnabled } 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,38 @@ 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 { loadAnalysisScope } from "./scope-choices.js";
|
|
14
|
+
import { FILE_SCOPE_PUBLIC_USAGE } from "../contract/scope-choice.js";
|
|
13
15
|
const usage = "Usage: counterfact-labs analyze [path] [--proposal <json-file>]\n counterfact-labs finding <id>\n counterfact-labs balance | login | logout | mcp\n";
|
|
14
16
|
export async function run(args, options = {}) {
|
|
15
17
|
const write = options.write ?? (text => { process.stdout.write(text); });
|
|
16
18
|
const emailSignIn = options.emailSignIn ?? readEmailSignIn(options.env);
|
|
17
|
-
const commandOptions = { ...options, emailSignIn };
|
|
18
19
|
try {
|
|
19
20
|
const [command, ...rest] = args;
|
|
21
|
+
const env = options.startup?.settings ?? options.env ?? process.env;
|
|
22
|
+
// Help follows the CLI defaults without validating unrelated commands' analysis switches.
|
|
23
|
+
const selectionSettings = readSelectionSettings(env, { validate: command === "analyze" || command === "mcp" });
|
|
24
|
+
const commandOptions = { ...options, emailSignIn, env: { ...(options.env ?? process.env), ...selectionSettings,
|
|
25
|
+
...(command === "analyze" || command === "mcp" ? {} : {
|
|
26
|
+
CF_FILE_SCOPE: undefined, CF_SCOPE_CHOICES: undefined, CF_LOCAL_IMPORT_RESOLUTION: undefined,
|
|
27
|
+
}) } };
|
|
28
|
+
const choices = scopeChoicesEnabled(selectionSettings);
|
|
29
|
+
const commandUsage = choices ? FILE_SCOPE_PUBLIC_USAGE : usage;
|
|
20
30
|
if (["--help", "-h", "help"].includes(command) || !command) {
|
|
21
|
-
write(
|
|
31
|
+
write(commandUsage);
|
|
22
32
|
return 0;
|
|
23
33
|
}
|
|
24
34
|
if (command === "analyze") {
|
|
25
35
|
let parsed;
|
|
26
36
|
try {
|
|
27
|
-
parsed = parseAnalyzeOptions(rest);
|
|
37
|
+
parsed = parseAnalyzeOptions(rest, choices);
|
|
28
38
|
}
|
|
29
39
|
catch {
|
|
30
|
-
throw new Error(
|
|
40
|
+
throw new Error(commandUsage.trimEnd());
|
|
31
41
|
}
|
|
32
|
-
|
|
42
|
+
const ctx = commandContext(commandOptions);
|
|
43
|
+
const scope = await loadAnalysisScope(parsed.options, ctx.cwd ?? process.cwd(), ctx.startup.settings);
|
|
44
|
+
return await analyze(parsed.path, { ...parsed.options, scope }, ctx);
|
|
33
45
|
}
|
|
34
46
|
if (command === "finding" && rest.length === 1 && !rest[0].startsWith("-"))
|
|
35
47
|
return await finding(rest[0], commandContext(commandOptions));
|
|
@@ -39,12 +51,12 @@ export async function run(args, options = {}) {
|
|
|
39
51
|
parsed = parseLoginOptions(rest);
|
|
40
52
|
}
|
|
41
53
|
catch {
|
|
42
|
-
throw new Error(
|
|
54
|
+
throw new Error(commandUsage.trimEnd());
|
|
43
55
|
}
|
|
44
56
|
return await login(commandContext(commandOptions), parsed);
|
|
45
57
|
}
|
|
46
58
|
if (rest.length)
|
|
47
|
-
throw new Error(
|
|
59
|
+
throw new Error(commandUsage.trimEnd());
|
|
48
60
|
if (command === "balance")
|
|
49
61
|
return await balance(commandContext(commandOptions));
|
|
50
62
|
if (command === "login")
|
|
@@ -55,7 +67,7 @@ export async function run(args, options = {}) {
|
|
|
55
67
|
await serveMcp(process.stdin, process.stdout, options);
|
|
56
68
|
return 0;
|
|
57
69
|
}
|
|
58
|
-
throw new Error(
|
|
70
|
+
throw new Error(commandUsage.trimEnd());
|
|
59
71
|
}
|
|
60
72
|
catch (error) {
|
|
61
73
|
write(errorText(error) + "\n");
|
|
@@ -0,0 +1,8 @@
|
|
|
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);
|
|
7
|
+
return { message, ...(enabled ? { reason: failureReason(error) } : {}) };
|
|
8
|
+
}
|
package/dist/cli/mcp.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
2
|
import { once } from "node:events";
|
|
3
3
|
import { resolveFolderPath } from "../adapters/folder-path.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { CliRefusalError, failureReason } from "../contract/cli-refusals.js";
|
|
5
|
+
import { mcpFailure } from "./mcp-failure.js";
|
|
6
|
+
import { errorCode } from "./api.js";
|
|
6
7
|
import { commandContext } from "./context.js";
|
|
7
8
|
import { analyze } from "./commands/analyze.js";
|
|
8
9
|
import { CLI_VERSION } from "../contract/cli-version.js";
|
|
9
10
|
import { finding } from "./commands/finding.js";
|
|
10
11
|
import { balance } from "./commands/balance.js";
|
|
11
12
|
import { ANALYZE_TOOL_NAME, analyzeToolAllowed } from "./claude-code-permission.js";
|
|
12
|
-
import { signInStatus, startSignIn } from "./sign-in.js";
|
|
13
|
+
import { signInStatus, startSignIn, waitForSignIn, SignInExpiredError, SignInDeniedError } from "./sign-in.js";
|
|
13
14
|
import { readEmailSignIn } from "../adapters/settings.js";
|
|
14
15
|
import { emailSignInMessages } from "../contract/email-sign-in-messages.js";
|
|
15
16
|
import { EMAIL_SIGN_IN_TERMS, normalizeSignInEmail, validSignInCode, startEmailSignIn, signInWithCode } from "./email-sign-in.js";
|
|
@@ -20,14 +21,42 @@ import { fixRequestTools, waitingFixes, waitingFixCount, claimFix, FIX_DISCOVERY
|
|
|
20
21
|
import { FINDING_PROJECT_MESSAGE, FindingReferences, numberedFindingInputSchema } from "./finding-lookup.js";
|
|
21
22
|
import { findingDisplayRows, formatFindingNumber, formatWaitingFixes } from "./finding-display.js";
|
|
22
23
|
import { readFindingSettings } from "./finding-settings.js";
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
import { readCliSettings, readSelectionSettings, selectionModeEnabled, scopeChoicesEnabled } from "../adapters/settings.js";
|
|
26
|
+
import { resolveAnalysisSelection } from "./analysis-selection.js";
|
|
27
|
+
import { analysisScopeSchema, chooseScopeMcpResultSchema } from "../contract/scope-choice.js";
|
|
28
|
+
import { scopeChoiceRefusal } from "../contract/selection-offer.js";
|
|
29
|
+
import { loadAnalysisScope, scopeChoiceText } from "./scope-choices.js";
|
|
30
|
+
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.";
|
|
31
|
+
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.";
|
|
32
|
+
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.";
|
|
33
|
+
const filePathDescription = "The agent folder or agent file on this computer, inside the launch folder.";
|
|
34
|
+
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.";
|
|
35
|
+
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.";
|
|
36
|
+
const fileProposalPathDescription = "Paths are relative to the selected folder, or to the launch folder when selecting a file.";
|
|
37
|
+
function fileAnalysisTool(choices) {
|
|
38
|
+
const proposal = structuredClone(agentProposalInputSchema);
|
|
39
|
+
if (typeof proposal.properties?.files === "object" && proposal.properties.files && "items" in proposal.properties.files) {
|
|
40
|
+
const files = proposal.properties.files.items;
|
|
41
|
+
if (files && !Array.isArray(files) && typeof files === "object" && typeof files.properties?.path === "object")
|
|
42
|
+
files.properties.path.description = fileProposalPathDescription;
|
|
43
|
+
}
|
|
44
|
+
const scope = z.toJSONSchema(analysisScopeSchema);
|
|
45
|
+
delete scope.$schema;
|
|
46
|
+
return { name: ANALYZE_TOOL_NAME,
|
|
47
|
+
description: [fileAnalysisDescription, ...(choices ? [scopeChoiceDescription] : []), fileAnalysisEnding].join(" "),
|
|
48
|
+
inputSchema: { type: "object", properties: { path: { type: "string", description: filePathDescription },
|
|
49
|
+
proposal: { ...proposal, description: fileProposalDescription }, ...(choices ? { scope: { ...scope, description: fileScopeDescription } } : {}) },
|
|
50
|
+
required: ["path"], additionalProperties: false } };
|
|
51
|
+
}
|
|
23
52
|
const legacyTools = [
|
|
24
53
|
{
|
|
25
54
|
name: ANALYZE_TOOL_NAME,
|
|
26
|
-
description: "Analyze the agent in a folder on this computer. Each call runs one analysis and charges the user's balance, so call it only when the user asks for an analysis. After edits, suggest an analysis and wait for the user to ask. 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.",
|
|
55
|
+
description: "Analyze the agent in a folder on this computer. Each call runs one analysis and 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. 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.",
|
|
27
56
|
inputSchema: { type: "object", properties: { path: { type: "string", description: "The agent folder on this computer." }, proposal: { ...agentProposalInputSchema, description: "Optional: the files you believe make up the agent, with each file's role. Paths are relative to the agent folder." } }, required: ["path"], additionalProperties: false },
|
|
28
57
|
},
|
|
29
58
|
{ name: "get_finding", description: "Get a finding's explanation, quoted lines, and suggested fixes.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"], additionalProperties: false } },
|
|
30
|
-
{ name: "get_balance", description: "Get the balance and top-up link.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
59
|
+
{ name: "get_balance", description: "Get the balance and top-up link, and whether a free first analysis is available. A free first analysis needs no top-up.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
31
60
|
{ 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 } },
|
|
32
61
|
{ 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 } },
|
|
33
62
|
];
|
|
@@ -43,12 +72,19 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
43
72
|
const audienceChoice = (options.env ?? process.env).CF_FINDING_AUDIENCE;
|
|
44
73
|
const cwd = options.cwd ?? process.cwd();
|
|
45
74
|
const guarded = (options.env ?? process.env).CF_MCP_PATH_GUARD !== "0";
|
|
75
|
+
const failureReasons = (options.env ?? process.env).CF_MCP_FAILURE_REASONS !== "0";
|
|
46
76
|
const launch = guarded ? resolveFolderPath(cwd).catch(() => null) : Promise.resolve(cwd);
|
|
47
77
|
const lastAnalysis = new Map();
|
|
48
78
|
const findingSettings = readFindingSettings(options.env);
|
|
79
|
+
const analysisSwitches = readSelectionSettings(options.startup?.settings ?? options.env ?? process.env);
|
|
80
|
+
const choicesEnabled = scopeChoicesEnabled(analysisSwitches);
|
|
81
|
+
const selectionEnabled = selectionModeEnabled(analysisSwitches);
|
|
82
|
+
const analysisEnv = selectionEnabled ? { ...(options.env ?? process.env), ...analysisSwitches } : undefined;
|
|
83
|
+
let analysisStartup = options.startup;
|
|
49
84
|
const { numbersEnabled, fixesEnabled } = findingSettings;
|
|
50
85
|
const references = new FindingReferences();
|
|
51
|
-
const
|
|
86
|
+
const emailEnv = analysisEnv && Object.hasOwn(analysisEnv, "CF_EMAIL_SIGN_IN") ? analysisEnv : options.env;
|
|
87
|
+
const emailSignIn = options.emailSignIn ?? readEmailSignIn(emailEnv);
|
|
52
88
|
const signInTools = emailSignIn ? [...legacyTools.slice(0, 3),
|
|
53
89
|
{
|
|
54
90
|
name: "sign_in_status",
|
|
@@ -74,10 +110,11 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
74
110
|
}, required: ["email", "code"], additionalProperties: false },
|
|
75
111
|
},
|
|
76
112
|
] : legacyTools;
|
|
77
|
-
const tools = signInTools.map(tool => tool.name === "get_finding" && numbersEnabled ? {
|
|
113
|
+
const tools = signInTools.map(tool => tool.name === ANALYZE_TOOL_NAME && selectionEnabled ? fileAnalysisTool(choicesEnabled) : tool).map(tool => tool.name === "get_finding" && numbersEnabled ? {
|
|
78
114
|
...tool, inputSchema: numberedFindingInputSchema,
|
|
79
115
|
} : tool).concat(fixesEnabled ? fixRequestTools : []);
|
|
80
116
|
let pendingSignIn;
|
|
117
|
+
let signInFailure;
|
|
81
118
|
const requests = new Map();
|
|
82
119
|
let queue = Promise.resolve();
|
|
83
120
|
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
@@ -88,7 +125,7 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
88
125
|
if (!object(args))
|
|
89
126
|
throw new ProtocolError(-32602, "Tool arguments must be an object.");
|
|
90
127
|
const name = params.name;
|
|
91
|
-
const allowed = name === ANALYZE_TOOL_NAME ? ["path", "proposal"] : name === "get_finding" ? (numbersEnabled ? Object.keys(numberedFindingInputSchema.properties) : ["id"])
|
|
128
|
+
const allowed = name === ANALYZE_TOOL_NAME ? ["path", "proposal", ...(choicesEnabled ? ["scope"] : [])] : name === "get_finding" ? (numbersEnabled ? Object.keys(numberedFindingInputSchema.properties) : ["id"])
|
|
92
129
|
: name === "claim_fix" ? ["id"]
|
|
93
130
|
: name === "sign_in_status" ? (emailSignIn ? ["wait", "path"] : ["wait"])
|
|
94
131
|
: emailSignIn && name === "sign_in" ? ["email"] : emailSignIn && name === "sign_in_with_code" ? ["email", "code"] : [];
|
|
@@ -111,7 +148,10 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
111
148
|
if (name === "sign_in_with_code" && !validSignInCode(args.code))
|
|
112
149
|
throw new ProtocolError(-32602, emailSignInMessages.invalid_code);
|
|
113
150
|
let text = "", exitCode = 0;
|
|
151
|
+
let reason;
|
|
152
|
+
let freeAnalysisAvailable = false;
|
|
114
153
|
let agentChoice;
|
|
154
|
+
let scopeChoice;
|
|
115
155
|
let findingIds;
|
|
116
156
|
let findings, projectId;
|
|
117
157
|
let loadedFinding;
|
|
@@ -121,12 +161,22 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
121
161
|
let started = false;
|
|
122
162
|
let signIn;
|
|
123
163
|
try {
|
|
124
|
-
|
|
164
|
+
// Listing tools must not validate settings used only when a tool runs.
|
|
165
|
+
if (selectionEnabled)
|
|
166
|
+
analysisStartup ??= readCliSettings(analysisEnv, emailSignIn);
|
|
167
|
+
const analysisSettings = analysisStartup?.settings ?? {};
|
|
168
|
+
const ctx = commandContext({ ...options, ...(analysisStartup ? { startup: analysisStartup } : {}), env: { ...(analysisEnv ?? options.env ?? process.env), ...analysisSwitches, CF_FINDING_AUDIENCE: audienceChoice },
|
|
169
|
+
onCheckFailure: failure => { reason = failure.reason; options.onCheckFailure?.(failure); },
|
|
125
170
|
emailSignIn, findingSettings, signal, onCheckStart: () => {
|
|
126
171
|
started = true;
|
|
127
172
|
if (guarded && analysisFolder)
|
|
128
173
|
lastAnalysis.set(analysisFolder, ctx.now());
|
|
129
174
|
options.onCheckStart?.();
|
|
175
|
+
}, onScopeChoice: choice => {
|
|
176
|
+
scopeChoice = choice;
|
|
177
|
+
if (guarded && analysisFolder)
|
|
178
|
+
lastAnalysis.delete(analysisFolder);
|
|
179
|
+
options.onScopeChoice?.(choice);
|
|
130
180
|
}, onAgentChoice: choice => { agentChoice = choice; options.onAgentChoice?.(choice); }, onCheckComplete: result => {
|
|
131
181
|
findingIds = result.findingIds ?? [];
|
|
132
182
|
if (numbersEnabled) {
|
|
@@ -160,20 +210,32 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
160
210
|
} });
|
|
161
211
|
}
|
|
162
212
|
else if (name === "get_balance")
|
|
163
|
-
await balance(ctx);
|
|
213
|
+
await balance(ctx, { onLoaded: value => { freeAnalysisAvailable = value.freeAnalysisAvailable; } });
|
|
164
214
|
else if (name === "sign_in_status") {
|
|
165
215
|
const pending = pendingSignIn;
|
|
166
216
|
const allowed = await analyzeToolAllowed(ctx.homeDir, cwd);
|
|
167
217
|
if (args.wait === true && pending) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
218
|
+
if (pending.flow.retryEnabled) {
|
|
219
|
+
try {
|
|
220
|
+
await waitForSignIn(pending.flow, signal);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (signal.aborted)
|
|
224
|
+
throw error;
|
|
225
|
+
// The flow stores its failure; collect the usual status fields before reporting it.
|
|
226
|
+
}
|
|
174
227
|
}
|
|
175
|
-
|
|
176
|
-
|
|
228
|
+
else {
|
|
229
|
+
const cancel = () => pending.controller.abort();
|
|
230
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
231
|
+
try {
|
|
232
|
+
if (signal.aborted)
|
|
233
|
+
cancel();
|
|
234
|
+
await pending.flow.done;
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
signal.removeEventListener("abort", cancel);
|
|
238
|
+
}
|
|
177
239
|
}
|
|
178
240
|
}
|
|
179
241
|
signIn = { ...await signInStatus(ctx), analyzeToolAllowed: allowed };
|
|
@@ -185,17 +247,31 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
185
247
|
if (suggestion)
|
|
186
248
|
signIn.suggestedEmail = suggestion;
|
|
187
249
|
}
|
|
250
|
+
if (signInFailure)
|
|
251
|
+
signal.throwIfAborted();
|
|
252
|
+
const failure = signInFailure;
|
|
253
|
+
signInFailure = undefined;
|
|
254
|
+
if (failure && !signIn.signedIn) {
|
|
255
|
+
const shown = mcpFailure(failure.error, failureReasons);
|
|
256
|
+
text = (failure.error instanceof SignInExpiredError
|
|
257
|
+
? emailSignInMessages.mcp_expired : failure.error instanceof SignInDeniedError
|
|
258
|
+
? emailSignInMessages.mcp_cancelled : shown.message) + "\n";
|
|
259
|
+
exitCode = errorCode(failure.error);
|
|
260
|
+
reason = shown.reason;
|
|
261
|
+
}
|
|
188
262
|
}
|
|
189
263
|
else if (emailFlow) {
|
|
190
264
|
if (pendingSignIn)
|
|
191
265
|
throw new Error(emailSignInMessages.browser_sign_in_pending);
|
|
192
266
|
if (name === "sign_in") {
|
|
193
267
|
const start = await startEmailSignIn(ctx, email);
|
|
268
|
+
signInFailure = undefined;
|
|
194
269
|
signIn = { email: email, expiresIn: start.expiresIn };
|
|
195
270
|
text = `We sent a sign-in code to ${email}. Ask the user for the code from their email, then call sign_in_with_code with the same email and their code. Do not repeat the code.`;
|
|
196
271
|
}
|
|
197
272
|
else {
|
|
198
273
|
await signInWithCode(ctx, email, args.code);
|
|
274
|
+
signInFailure = undefined;
|
|
199
275
|
signIn = { signedIn: true };
|
|
200
276
|
text = "Signed in.";
|
|
201
277
|
}
|
|
@@ -206,20 +282,25 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
206
282
|
else {
|
|
207
283
|
let pending = pendingSignIn;
|
|
208
284
|
if (!pending) {
|
|
209
|
-
//
|
|
285
|
+
// With retry on, the browser flow belongs to the session, so a cancelled status wait leaves it running.
|
|
210
286
|
const controller = new AbortController();
|
|
211
287
|
const cancel = () => controller.abort();
|
|
212
288
|
signal.addEventListener("abort", cancel, { once: true });
|
|
213
289
|
try {
|
|
214
290
|
signal.throwIfAborted();
|
|
215
|
-
const flow = await startSignIn(commandContext({ ...options, emailSignIn, findingSettings, homeDir: ctx.homeDir, baseUrl: ctx.api.baseUrl, signal: controller.signal }));
|
|
291
|
+
const flow = await startSignIn(commandContext({ ...options, startup: ctx.startup, emailSignIn, findingSettings, homeDir: ctx.homeDir, baseUrl: ctx.api.baseUrl, signal: controller.signal }));
|
|
216
292
|
pending = { flow, controller, opened: false };
|
|
217
293
|
pendingSignIn = pending;
|
|
294
|
+
signInFailure = undefined;
|
|
218
295
|
const current = pending;
|
|
219
296
|
const clear = () => { if (pendingSignIn === current)
|
|
220
297
|
pendingSignIn = undefined; };
|
|
221
298
|
// Handle background failures even when the client never calls wait.
|
|
222
|
-
void flow.done.then(clear,
|
|
299
|
+
void flow.done.then(clear, error => {
|
|
300
|
+
if (flow.retryEnabled && pendingSignIn === current && !current.controller.signal.aborted)
|
|
301
|
+
signInFailure = { error };
|
|
302
|
+
clear();
|
|
303
|
+
});
|
|
223
304
|
pending.opened = await ctx.openBrowser(flow.pageUrl);
|
|
224
305
|
}
|
|
225
306
|
finally {
|
|
@@ -234,30 +315,49 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
234
315
|
else {
|
|
235
316
|
const launchFolder = await launch;
|
|
236
317
|
if (guarded && !launchFolder)
|
|
237
|
-
throw new
|
|
238
|
-
const
|
|
239
|
-
const
|
|
318
|
+
throw new CliRefusalError("launch_folder_unavailable");
|
|
319
|
+
const scope = await loadAnalysisScope({ scope: args.scope }, cwd, analysisSettings);
|
|
320
|
+
const selection = selectionEnabled ? await resolveAnalysisSelection(args.path, launchFolder ?? cwd, analysisSettings, scope) : undefined;
|
|
321
|
+
if (guarded && selection?.kind === "folder")
|
|
322
|
+
await resolveMcpAgentFolder(selection.path, launchFolder, ctx.homeDir);
|
|
323
|
+
const folder = selection?.path ?? (guarded ? await resolveMcpAgentFolder(args.path, launchFolder, ctx.homeDir) : resolveAgentFolder(args.path, cwd));
|
|
324
|
+
const key = selection ? JSON.stringify([selection.launchRoot, selection.path]) : folder;
|
|
325
|
+
const last = lastAnalysis.get(key);
|
|
240
326
|
if (guarded && last !== undefined && ctx.now() - last < 10 * 60 * 1000) {
|
|
241
|
-
throw new
|
|
327
|
+
throw new CliRefusalError("repeat_too_soon");
|
|
242
328
|
}
|
|
243
|
-
analysisFolder =
|
|
329
|
+
analysisFolder = key;
|
|
244
330
|
try {
|
|
245
|
-
exitCode = await analyze(folder, { proposal: args.proposal }, ctx);
|
|
331
|
+
exitCode = await analyze(folder, { proposal: args.proposal, scope, selection }, ctx);
|
|
246
332
|
}
|
|
247
333
|
finally {
|
|
248
|
-
if (
|
|
249
|
-
lastAnalysis.
|
|
334
|
+
if (reason === "results_pending")
|
|
335
|
+
lastAnalysis.delete(key);
|
|
336
|
+
else if (guarded && started && !scopeChoice)
|
|
337
|
+
lastAnalysis.set(key, ctx.now());
|
|
250
338
|
}
|
|
251
339
|
}
|
|
252
340
|
}
|
|
253
341
|
catch (error) {
|
|
254
|
-
|
|
342
|
+
const failure = mcpFailure(error, failureReasons);
|
|
343
|
+
text += failure.message + "\n";
|
|
344
|
+
reason = failure.reason;
|
|
255
345
|
exitCode = errorCode(error);
|
|
256
346
|
}
|
|
347
|
+
if (scopeChoice) {
|
|
348
|
+
const refusal = scopeChoiceRefusal(scopeChoice);
|
|
349
|
+
if (!refusal)
|
|
350
|
+
return chooseScopeMcpResultSchema.parse({ content: [{ type: "text", text: scopeChoiceText(scopeChoice) }], isError: false,
|
|
351
|
+
structuredContent: { ...scopeChoice, exitCode: 0 } });
|
|
352
|
+
text = refusal + "\n";
|
|
353
|
+
exitCode = 1;
|
|
354
|
+
}
|
|
257
355
|
// Some clients (Claude Code among them) give the model only the structured result when one is present, so it
|
|
258
356
|
// repeats the text: without it the model cannot read the result.
|
|
259
357
|
return { content: [{ type: "text", text }], isError: exitCode !== 0,
|
|
260
358
|
structuredContent: { text, exitCode, ...agentChoice, ...(findingIds ? { findingIds } : {}), ...(url ? { url } : {}), ...signIn,
|
|
359
|
+
...(exitCode !== 0 && failureReasons ? { reason: reason ?? failureReason(undefined) } : {}),
|
|
360
|
+
...(freeAnalysisAvailable ? { freeAnalysisAvailable } : {}),
|
|
261
361
|
...(findings ? { findings, projectId } : {}), ...(loadedFinding ? { finding: loadedFinding } : {}),
|
|
262
362
|
...(fixRequests ? { fixRequests } : {}), ...claimed, ...(waitingCount !== undefined ? { waitingFixes: waitingCount } : {}) } };
|
|
263
363
|
}
|
|
@@ -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
|
+
}
|