@counterfact-labs/cli 0.2.3 → 0.2.5
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 +7 -1
- package/bundle/client-release.json +1 -1
- package/dist/adapters/anthropic.js +11 -2
- package/dist/adapters/completion.js +3 -0
- package/dist/adapters/durable-write.js +9 -2
- package/dist/adapters/files.js +29 -13
- package/dist/adapters/git-ignore.js +28 -14
- package/dist/adapters/git.js +1 -1
- package/dist/adapters/gitignore-output.js +41 -0
- package/dist/adapters/models.js +1 -6
- package/dist/adapters/output-paths.js +5 -2
- package/dist/adapters/public-instructions.js +1 -1
- package/dist/adapters/relay-plan.js +4 -7
- package/dist/adapters/release.js +10 -4
- package/dist/adapters/repository-files.js +164 -4
- package/dist/adapters/repository-root.js +18 -2
- package/dist/adapters/results.js +8 -4
- package/dist/adapters/settings.js +27 -1
- package/dist/adapters/snapshot.js +86 -15
- package/dist/analysis-core/analysis/finding-audience-switch.js +1 -0
- package/dist/analysis-core/analysis/schemas.js +1 -1
- package/dist/analysis-core/analysis/source-notes/types.js +1 -1
- package/dist/analysis-core/chunks/377MLK4M.js +21 -0
- package/dist/analysis-core/chunks/3GUKUEEJ.js +1 -0
- package/dist/analysis-core/chunks/{Y5AUKKYG.js → 3JWEFCTJ.js} +1 -1
- package/dist/analysis-core/chunks/{3227KXHF.js → 4H23CSUM.js} +1 -1
- package/dist/analysis-core/chunks/5DGPLXYB.js +1 -0
- package/dist/analysis-core/chunks/5TATKWLG.js +1 -0
- package/dist/analysis-core/chunks/{EH3QQ3T4.js → 6B2HILSY.js} +1 -1
- package/dist/analysis-core/chunks/6QY5BV5B.js +14 -0
- package/dist/analysis-core/chunks/6U2IR4XB.js +1 -0
- package/dist/analysis-core/chunks/A7WJJMRY.js +1 -0
- package/dist/analysis-core/chunks/C6X5GDG4.js +1 -0
- package/dist/analysis-core/chunks/DLPVYWHZ.js +1 -0
- package/dist/analysis-core/chunks/DZZBQDGO.js +1 -0
- package/dist/analysis-core/chunks/F3R4J3ND.js +1 -0
- package/dist/analysis-core/chunks/FOKDE64Y.js +1 -0
- package/dist/analysis-core/chunks/JKWTHRUR.js +1 -0
- package/dist/analysis-core/chunks/K2GNDX3W.js +1 -0
- package/dist/analysis-core/chunks/KTNA2KTN.js +7 -0
- package/dist/analysis-core/chunks/KWECQJ6X.js +1 -0
- package/dist/analysis-core/chunks/{SL27GMEN.js → M6HYSTQD.js} +1 -1
- package/dist/analysis-core/chunks/{VFENG4MR.js → MJGB25S5.js} +1 -1
- package/dist/analysis-core/chunks/{ZAA44GPI.js → N7U5N42V.js} +1 -1
- package/dist/analysis-core/chunks/NIOS7U6C.js +41 -0
- package/dist/analysis-core/chunks/{QPXNXMLC.js → OPMA32A6.js} +1 -1
- package/dist/analysis-core/chunks/PECX3OXB.js +1 -0
- package/dist/analysis-core/chunks/QFY6A3MS.js +66 -0
- package/dist/analysis-core/chunks/SKVEBOGA.js +1 -0
- package/dist/analysis-core/chunks/{VER355VN.js → STI4JE7L.js} +1 -1
- package/dist/analysis-core/chunks/SVMEWGD3.js +1 -0
- package/dist/analysis-core/chunks/X7WZAA7S.js +1 -0
- package/dist/analysis-core/chunks/YDBO7P4X.js +1 -0
- package/dist/analysis-core/chunks/YUY436SM.js +1 -0
- package/dist/analysis-core/chunks/ZTOVBAG7.js +1 -0
- package/dist/analysis-core/contracts/file-safety.js +1 -0
- package/dist/analysis-core/contracts/relay-analysis.js +1 -0
- package/dist/analysis-core/contracts/requests.js +1 -1
- package/dist/analysis-core/conventions/analysis-fit.js +1 -0
- package/dist/analysis-core/conventions/file-admission.js +1 -1
- package/dist/analysis-core/data/finding-types.js +1 -1
- package/dist/analysis-core/errors.js +1 -1
- package/dist/analysis-core/findings/sites.js +1 -0
- package/dist/analysis-core/github/repo-key.js +1 -1
- package/dist/analysis-core/import/outside-loader-sources.js +1 -1
- package/dist/analysis-core/index.js +3 -3
- package/dist/analysis-core/llm/cache-policy.js +1 -1
- package/dist/analysis-core/llm/cost.js +1 -1
- package/dist/analysis-core/llm/output-limit.js +1 -1
- package/dist/analysis-core/llm/types.js +1 -0
- package/dist/analysis-core/requests/answer-formats.js +1 -1
- package/dist/analysis-core/requests/headline-request.js +1 -1
- 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 -0
- package/dist/analysis-core/stages/execution-ports.js +1 -0
- package/dist/bundle/seal/deserialise.js +9 -1
- package/dist/bundle/seal/engine.js +76 -0
- package/dist/bundle/seal/freeze.js +9 -0
- package/dist/bundle/seal/metadata.js +3 -0
- package/dist/bundle/seal/open-engine.js +26 -0
- package/dist/bundle/seal/open.js +2 -8
- package/dist/bundle/seal/signature.js +2 -2
- package/dist/cli/commands/analyze.js +39 -5
- package/dist/cli/commands/finding.js +9 -3
- package/dist/cli/context.js +4 -1
- package/dist/cli/development-relay.js +34 -0
- package/dist/cli/finding-display.js +39 -0
- package/dist/cli/finding-lookup.js +92 -0
- package/dist/cli/finding-settings.js +5 -0
- package/dist/cli/fix-requests.js +42 -0
- package/dist/cli/intake-payload.js +19 -11
- package/dist/cli/main.js +9 -4
- package/dist/cli/mcp.js +65 -18
- package/dist/cli/relay-check.js +47 -14
- package/dist/cli/relay-preparation.js +31 -6
- package/dist/cli/result-check.js +68 -0
- package/dist/cli/summary.js +10 -3
- package/dist/contract/agent-choice.js +15 -0
- package/dist/contract/agent-proposal.js +38 -0
- package/dist/contract/attestation.js +106 -0
- package/dist/contract/cli-refusals.js +5 -0
- package/dist/contract/cli-version.js +1 -1
- package/dist/contract/console.js +41 -16
- package/dist/contract/engine.js +44 -0
- package/dist/contract/findings.js +28 -0
- package/dist/contract/fix-requests.js +30 -0
- package/dist/contract/messages.js +6 -94
- package/dist/contract/public-report.js +1 -0
- package/dist/contract/runtime-environment.js +12 -1
- package/dist/core/analysis/run-to-end.js +7 -52
- package/dist/gatekeeper/attestation.js +1 -1
- package/dist/gatekeeper/channel-binding.js +15 -0
- package/dist/gatekeeper/cli-api.js +12 -2
- package/dist/gatekeeper/connection-exchange.js +18 -13
- package/dist/gatekeeper/console.js +11 -4
- package/dist/gatekeeper/model.js +2 -1
- package/dist/gatekeeper/production-attestation.js +23 -17
- package/dist/gatekeeper/trust/policy.js +4 -1
- package/dist/gatekeeper/trust/schema.js +4 -0
- package/dist/transport/kms.js +25 -0
- package/package.json +4 -3
- package/dist/adapters/quality.js +0 -6
- package/dist/adapters/report.js +0 -29
- package/dist/analysis-core/analysis/analysis-flags.js +0 -1
- package/dist/analysis-core/analysis/finding-quality/config.js +0 -1
- package/dist/analysis-core/analysis/finding-quality/policy.js +0 -1
- package/dist/analysis-core/analysis/headlines/attachment.js +0 -1
- package/dist/analysis-core/analysis/headlines/input.js +0 -1
- package/dist/analysis-core/chunks/6YCABS5T.js +0 -1
- package/dist/analysis-core/chunks/7QDOHWSY.js +0 -27
- package/dist/analysis-core/chunks/BZNHHLNP.js +0 -1
- package/dist/analysis-core/chunks/DTMHXKP6.js +0 -7
- package/dist/analysis-core/chunks/EQTIRIMU.js +0 -37
- package/dist/analysis-core/chunks/G322LST6.js +0 -1
- package/dist/analysis-core/chunks/IL2KRP3T.js +0 -1
- package/dist/analysis-core/chunks/JF4GYMOZ.js +0 -1
- package/dist/analysis-core/chunks/KEKTZDIK.js +0 -1
- package/dist/analysis-core/chunks/M7MMAUAK.js +0 -1
- package/dist/analysis-core/chunks/OFBJNHC6.js +0 -1
- package/dist/analysis-core/chunks/ORCYIIHB.js +0 -60
- package/dist/analysis-core/chunks/PVPEY7NF.js +0 -1
- package/dist/analysis-core/chunks/STZOC7LN.js +0 -1
- package/dist/analysis-core/chunks/T5TREZCQ.js +0 -1
- package/dist/analysis-core/chunks/TDNTU4TF.js +0 -1
- package/dist/analysis-core/chunks/U5HOEVPE.js +0 -1
- package/dist/analysis-core/chunks/UEWL3GMQ.js +0 -1
- package/dist/analysis-core/chunks/YVDCJN7D.js +0 -14
- package/dist/analysis-core/chunks/ZMUBMYWN.js +0 -1
- package/dist/core/analysis/finding-quality/config.js +0 -4
- package/dist/core/analysis/structural-run.js +0 -16
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadDevelopmentRelay } from "../development-relay.js";
|
|
1
2
|
import { loadInstalledRelease } from "../../adapters/release.js";
|
|
2
3
|
import { cliRefusals, START_FAILURE_MESSAGE } from "../../contract/cli-refusals.js";
|
|
3
4
|
import { CLI_VERSION } from "../../contract/cli-version.js";
|
|
@@ -5,6 +6,11 @@ import { cliApiMessages } from "../../contract/cli-api-messages.js";
|
|
|
5
6
|
import { ApiError } from "../api.js";
|
|
6
7
|
import { resolveAgentFolder } from "../agent-folder.js";
|
|
7
8
|
import { runRelayCheck } from "../relay-check.js";
|
|
9
|
+
import { GITIGNORE_OUTPUT_NOTICE } from "../../adapters/gitignore-output.js";
|
|
10
|
+
import { readFile } from "node:fs/promises";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
import { loadAgentProposal } from "../../contract/agent-proposal.js";
|
|
13
|
+
import { formatSavedFindings } from "../finding-display.js";
|
|
8
14
|
// Only a real pause says "paused"; an unreviewed release or an upload-only server cannot start.
|
|
9
15
|
const unavailable = START_FAILURE_MESSAGE;
|
|
10
16
|
function requireLocal(value) {
|
|
@@ -25,10 +31,30 @@ function requireLocal(value) {
|
|
|
25
31
|
if (value.mode !== "local")
|
|
26
32
|
throw new ApiError(503, "unavailable", unavailable);
|
|
27
33
|
}
|
|
28
|
-
export
|
|
34
|
+
export function parseAnalyzeOptions(args) {
|
|
35
|
+
let path, proposalFile;
|
|
36
|
+
for (let index = 0; index < args.length; index++) {
|
|
37
|
+
const arg = args[index];
|
|
38
|
+
if (arg === "--proposal" && proposalFile === undefined) {
|
|
39
|
+
const value = args[++index];
|
|
40
|
+
if (!value || value.startsWith("-"))
|
|
41
|
+
throw new Error();
|
|
42
|
+
proposalFile = value;
|
|
43
|
+
}
|
|
44
|
+
else if (!arg.startsWith("-") && path === undefined)
|
|
45
|
+
path = arg;
|
|
46
|
+
else
|
|
47
|
+
throw new Error();
|
|
48
|
+
}
|
|
49
|
+
return { path: path ?? ".", options: proposalFile === undefined ? {} : { proposalFile } };
|
|
50
|
+
}
|
|
51
|
+
export async function analyze(path, options, ctx) {
|
|
29
52
|
const startup = ctx.startup;
|
|
53
|
+
const proposal = await loadAgentProposal(async () => options.proposalFile === undefined ? options.proposal
|
|
54
|
+
: JSON.parse(await readFile(resolve(ctx.cwd ?? process.cwd(), options.proposalFile), "utf8")), startup.settings);
|
|
30
55
|
requireLocal(await ctx.api.request("checks/mode"));
|
|
31
|
-
const
|
|
56
|
+
const development = startup.transport.CF_RELAY_DEV === "1" ? await loadDevelopmentRelay(startup.transport) : undefined;
|
|
57
|
+
const release = development?.release ?? ctx.relayRuntime?.release ?? await loadInstalledRelease();
|
|
32
58
|
if (release.projectContext !== "checks")
|
|
33
59
|
throw new Error(unavailable);
|
|
34
60
|
const token = await ctx.token?.();
|
|
@@ -48,19 +74,27 @@ export async function analyze(path, _options, ctx) {
|
|
|
48
74
|
} };
|
|
49
75
|
try {
|
|
50
76
|
return await (ctx.relayRuntime?.run ?? runRelayCheck)(folder, {
|
|
51
|
-
settings: startup.settings, config, release, connect: ctx.relayRuntime?.connect,
|
|
77
|
+
settings: startup.settings, config, release, cwd: ctx.cwd, connect: development?.connect ?? ctx.relayRuntime?.connect,
|
|
78
|
+
...(proposal ? { proposal } : {}),
|
|
52
79
|
signal: ctx.signal ? AbortSignal.any([controller.signal, ctx.signal]) : controller.signal,
|
|
53
80
|
stdout() { }, stderr() { },
|
|
54
81
|
onStart: () => { ctx.onCheckStart?.(); ctx.write("\r\u001b[2KAnalyzing the agent…"); progressShown = true; },
|
|
82
|
+
onLocalSaved: result => {
|
|
83
|
+
if (result.ignoreAdded) {
|
|
84
|
+
clearProgress();
|
|
85
|
+
ctx.write(GITIGNORE_OUTPUT_NOTICE);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
55
88
|
onError: failure => {
|
|
89
|
+
if (failure.agentChoice)
|
|
90
|
+
ctx.onAgentChoice?.(failure.agentChoice);
|
|
56
91
|
clearProgress();
|
|
57
92
|
ctx.write(`${failure.message}\n`);
|
|
58
93
|
},
|
|
59
94
|
onSaved: result => {
|
|
60
95
|
clearProgress();
|
|
61
96
|
ctx.onCheckComplete?.(result);
|
|
62
|
-
|
|
63
|
-
ctx.write(`Finding ids: ${result.findingIds.join(", ")}\n`);
|
|
97
|
+
ctx.write(formatSavedFindings(result, { numbersEnabled: ctx.findingNumbers, audienceEnabled: result.audienceEnabled }));
|
|
64
98
|
ctx.write(`Full results: ${result.url}\n`);
|
|
65
99
|
},
|
|
66
100
|
});
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { loadFinding } from "../finding-lookup.js";
|
|
2
|
+
import { formatFindingNumber } from "../finding-display.js";
|
|
3
|
+
import { findingSequenceSchema } from "../../contract/findings.js";
|
|
4
|
+
export async function finding(id, ctx, options = {}) {
|
|
5
|
+
const value = await loadFinding(id, ctx.api, options.projectId, options.numbers ?? false);
|
|
6
|
+
const showNumber = ctx.findingNumbers !== false && findingSequenceSchema.safeParse(value.seq).success;
|
|
7
|
+
options.onLoaded?.(value);
|
|
8
|
+
const number = showNumber ? `${formatFindingNumber(value.seq)} ` : "";
|
|
9
|
+
ctx.write(`${number}${value.title}\n${value.explanation}\n`);
|
|
4
10
|
for (const quote of value.quotes) {
|
|
5
11
|
ctx.write(`\n${quote.path}${quote.line === null ? "" : `:${quote.line}`}\n`);
|
|
6
12
|
if (quote.text !== null)
|
package/dist/cli/context.js
CHANGED
|
@@ -6,15 +6,18 @@ import { openBrowser } from "./browser.js";
|
|
|
6
6
|
import { readCredentials } from "./credentials.js";
|
|
7
7
|
import { userEmail } from "./git.js";
|
|
8
8
|
import { terminalSignInInput } from "./sign-in-input.js";
|
|
9
|
+
import { readFindingSettings } from "./finding-settings.js";
|
|
9
10
|
export function commandContext(options = {}) {
|
|
10
11
|
const startup = readCliSettings(options.env, options.emailSignIn);
|
|
11
12
|
const homeDir = options.homeDir ?? homedir();
|
|
12
13
|
const baseUrl = (options.baseUrl ?? startup.baseUrl ?? DEFAULT_URL).replace(/\/+$/, "");
|
|
13
14
|
const api = new ApiClient(baseUrl, () => readCredentials(homeDir, api.baseUrl), undefined, options.signal);
|
|
14
15
|
const write = options.write ?? (text => { process.stdout.write(text); });
|
|
16
|
+
const findingSettings = options.findingSettings ?? readFindingSettings(options.env);
|
|
15
17
|
return {
|
|
18
|
+
findingNumbers: findingSettings.numbersEnabled,
|
|
16
19
|
startup, signal: options.signal, relayRuntime: options.relayRuntime, cwd: options.cwd ?? process.cwd(),
|
|
17
|
-
token: () => readCredentials(homeDir, api.baseUrl), onCheckComplete: options.onCheckComplete, onCheckStart: options.onCheckStart,
|
|
20
|
+
token: () => readCredentials(homeDir, api.baseUrl), onCheckComplete: options.onCheckComplete, onCheckStart: options.onCheckStart, onAgentChoice: options.onAgentChoice,
|
|
18
21
|
api, homeDir, write, openBrowser: options.openBrowser ?? openBrowser,
|
|
19
22
|
signInInput: options.signInInput ?? terminalSignInInput(), readEmail: options.readEmail ?? userEmail,
|
|
20
23
|
sleep: options.sleep ?? (async (ms) => { await delay(ms, undefined, { signal: options.signal }); }), now: options.now ?? Date.now,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createPublicKey } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { cliRefusals } from "../contract/cli-refusals.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { loadDevelopmentRelease } from "../adapters/release.js";
|
|
6
|
+
import { attestationPolicy } from "../gatekeeper/attestation.js";
|
|
7
|
+
import { connectRelay } from "../gatekeeper/connection.js";
|
|
8
|
+
const developmentTrust = z.object({
|
|
9
|
+
imageDigests: z.array(z.string().regex(/^sha256:[a-f0-9]{64}$/)).min(1),
|
|
10
|
+
developmentIssuer: z.object({ issuer: z.string().url(), keys: z.array(z.object({
|
|
11
|
+
kid: z.string().min(1), publicKey: z.string().min(1),
|
|
12
|
+
}).strict()).min(1) }).strict(),
|
|
13
|
+
}).strict();
|
|
14
|
+
/** Development paths are explicit and never replace the installed production pins. */
|
|
15
|
+
export async function loadDevelopmentRelay(config) {
|
|
16
|
+
try {
|
|
17
|
+
if (config.CF_RELAY_DEV !== "1" || !config.CF_RELAY_DEV_TRUST || !config.CF_RELAY_DEV_BUNDLE || !config.CF_RELAY_DEV_RELEASE)
|
|
18
|
+
throw new Error();
|
|
19
|
+
const trust = developmentTrust.parse(JSON.parse(await readFile(config.CF_RELAY_DEV_TRUST, "utf8")));
|
|
20
|
+
const policy = attestationPolicy(config, { ...trust, developmentIssuer: { ...trust.developmentIssuer,
|
|
21
|
+
keys: trust.developmentIssuer.keys.map(({ kid, publicKey }) => {
|
|
22
|
+
const key = createPublicKey(publicKey);
|
|
23
|
+
if (key.asymmetricKeyType !== "ed25519")
|
|
24
|
+
throw new Error();
|
|
25
|
+
return { kid, publicKey: key };
|
|
26
|
+
}),
|
|
27
|
+
} });
|
|
28
|
+
const release = await loadDevelopmentRelease({ bundle: config.CF_RELAY_DEV_BUNDLE, metadata: config.CF_RELAY_DEV_RELEASE });
|
|
29
|
+
return { release, connect: (url) => connectRelay(url, { development: policy }) };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new Error(cliRefusals.release_unavailable);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { groupByAudience } from "../analysis-core/analysis/schemas.js";
|
|
2
|
+
export const TEAM_WORKFLOW_HEADING = "Team workflow";
|
|
3
|
+
export const formatFindingNumber = (seq) => `F-${seq}`;
|
|
4
|
+
/** Intake returns one id per payload finding in order; the optional number mapping is joined by id. */
|
|
5
|
+
export function savedFindings(result, report) {
|
|
6
|
+
const numbers = new Map(result.findings?.map(value => [value.id, value]));
|
|
7
|
+
return result.findingIds.map((id, index) => {
|
|
8
|
+
const numbered = numbers.get(id), local = report[index];
|
|
9
|
+
return { id, ...(numbered ? { seq: numbered.seq, number: formatFindingNumber(numbered.seq) } : {}),
|
|
10
|
+
title: local?.title ?? "", ...(local?.audience === undefined ? {} : { audience: local.audience }) };
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
function findingLine(value, enabled, index, labels, audienceEnabled) {
|
|
14
|
+
// The off switch preserves the original Markdown summary, including whitespace.
|
|
15
|
+
if (labels === "position" && !audienceEnabled)
|
|
16
|
+
return `${index + 1}. ${value.title}`;
|
|
17
|
+
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}.`;
|
|
19
|
+
return `${label}${title ? ` ${title}` : ""}`;
|
|
20
|
+
}
|
|
21
|
+
export function findingDisplayRows(findings, { numbersEnabled = true, audienceEnabled, labels }) {
|
|
22
|
+
const groups = audienceEnabled ? groupByAudience(findings) : { users: findings, team: [] };
|
|
23
|
+
return [...groups.users, ...groups.team].map((finding, index) => ({ finding,
|
|
24
|
+
label: findingLine(finding, numbersEnabled, index, labels, audienceEnabled), startsTeamGroup: groups.team.length > 0 && index === groups.users.length }));
|
|
25
|
+
}
|
|
26
|
+
export function formatSavedFindings(result, options) {
|
|
27
|
+
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("");
|
|
29
|
+
}
|
|
30
|
+
return result.findingIds?.length ? `Finding ids: ${result.findingIds.join(", ")}\n` : "";
|
|
31
|
+
}
|
|
32
|
+
export function formatWaitingFixes(items, enabled, needsProject) {
|
|
33
|
+
if (!items.length)
|
|
34
|
+
return "No waiting fixes.";
|
|
35
|
+
return "Waiting fixes:\n" + findingDisplayRows(items.map(item => ({ ...item, id: item.findingId })), { numbersEnabled: enabled, audienceEnabled: false, labels: "reference" }).map(({ finding: item, label }) => {
|
|
36
|
+
const scope = [item.repoKey, ...(enabled && needsProject(item) ? [`project ${item.projectId}`] : [])].filter(Boolean);
|
|
37
|
+
return `${label}${scope.length ? ` (${scope.join("; ")})` : ""}\n`;
|
|
38
|
+
}).join("");
|
|
39
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { canonicalUuidSchema, findingNumberParamsSchema, findingNumberResponseSchema, findingSequenceSchema } from "../contract/findings.js";
|
|
2
|
+
import { cliApiMessages } from "../contract/cli-api-messages.js";
|
|
3
|
+
export const FINDING_PROJECT_MESSAGE = "Give the project for this finding number, which analysis results and the waiting_fixes tool list.";
|
|
4
|
+
export const numberedFindingInputSchema = { type: "object", properties: {
|
|
5
|
+
id: { type: "string", description: "A finding id, or a number such as F-10." },
|
|
6
|
+
projectId: { type: "string", description: "The project the finding number belongs to, needed unless this session knows only one project." },
|
|
7
|
+
}, required: ["id"], additionalProperties: false };
|
|
8
|
+
export function findingNumber(value) {
|
|
9
|
+
const match = /^F-?([1-9][0-9]*)$/i.exec(value.trim());
|
|
10
|
+
const seq = match ? Number(match[1]) : undefined;
|
|
11
|
+
return seq !== undefined && Number.isSafeInteger(seq) ? seq : undefined;
|
|
12
|
+
}
|
|
13
|
+
export class FindingReferences {
|
|
14
|
+
projects = new Set();
|
|
15
|
+
analysedProjects = new Set();
|
|
16
|
+
remember(projectId, source = "analysis") {
|
|
17
|
+
const canonical = projectId.toLowerCase();
|
|
18
|
+
this.projects.add(canonical);
|
|
19
|
+
if (source === "analysis")
|
|
20
|
+
this.analysedProjects.add(canonical);
|
|
21
|
+
}
|
|
22
|
+
// Observed findings cannot prove another project lacks the same historical number.
|
|
23
|
+
project() { return this.projects.size === 1 ? [...this.projects][0] : undefined; }
|
|
24
|
+
async resolve(reference, projectId, readWaiting) {
|
|
25
|
+
const seq = findingNumber(reference);
|
|
26
|
+
if (projectId || seq === undefined)
|
|
27
|
+
return projectId;
|
|
28
|
+
let waiting = [];
|
|
29
|
+
try {
|
|
30
|
+
waiting = await readWaiting?.() ?? [];
|
|
31
|
+
}
|
|
32
|
+
catch { /* A known project can still resolve a finding when the queue is unavailable. */ }
|
|
33
|
+
return this.resolveNumber(this.waitingByNumber(waiting).get(seq) ?? []);
|
|
34
|
+
}
|
|
35
|
+
projectsNeeded(waiting) {
|
|
36
|
+
const needed = new Set();
|
|
37
|
+
for (const matches of this.waitingByNumber(waiting).values()) {
|
|
38
|
+
const project = this.resolveNumber(matches);
|
|
39
|
+
for (const item of matches)
|
|
40
|
+
if (project !== item.projectId.toLowerCase())
|
|
41
|
+
needed.add(item.findingId);
|
|
42
|
+
}
|
|
43
|
+
return needed;
|
|
44
|
+
}
|
|
45
|
+
waitingByNumber(waiting) {
|
|
46
|
+
const numbers = new Map();
|
|
47
|
+
for (const item of waiting) {
|
|
48
|
+
this.remember(item.projectId, "waiting");
|
|
49
|
+
const matches = numbers.get(item.seq);
|
|
50
|
+
if (matches)
|
|
51
|
+
matches.push(item);
|
|
52
|
+
else
|
|
53
|
+
numbers.set(item.seq, [item]);
|
|
54
|
+
}
|
|
55
|
+
return numbers;
|
|
56
|
+
}
|
|
57
|
+
resolveNumber(matches) {
|
|
58
|
+
if (!matches.length && this.analysedProjects.size === 1)
|
|
59
|
+
return [...this.analysedProjects][0];
|
|
60
|
+
if (matches.length !== 1)
|
|
61
|
+
return this.project();
|
|
62
|
+
const candidate = matches[0].projectId.toLowerCase();
|
|
63
|
+
// A queued fix may be older than the project the user just analysed.
|
|
64
|
+
if (this.analysedProjects.size > 0 && (this.analysedProjects.size !== 1 || !this.analysedProjects.has(candidate)))
|
|
65
|
+
return undefined;
|
|
66
|
+
return candidate;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function loadFinding(reference, api, projectId, numbered = true) {
|
|
70
|
+
const seq = numbered ? findingNumber(reference) : undefined;
|
|
71
|
+
let id = reference;
|
|
72
|
+
if (seq !== undefined) {
|
|
73
|
+
if (!projectId)
|
|
74
|
+
throw new Error(FINDING_PROJECT_MESSAGE);
|
|
75
|
+
const params = findingNumberParamsSchema.safeParse({ projectId, seq: String(seq) });
|
|
76
|
+
if (!params.success)
|
|
77
|
+
throw new Error(FINDING_PROJECT_MESSAGE);
|
|
78
|
+
const result = findingNumberResponseSchema.safeParse(await api.request(`projects/${params.data.projectId}/findings/${params.data.seq}`));
|
|
79
|
+
if (!result.success || result.data.seq !== seq)
|
|
80
|
+
throw new Error(cliApiMessages.invalid_response);
|
|
81
|
+
id = result.data.id;
|
|
82
|
+
}
|
|
83
|
+
const value = await api.request(`findings/${encodeURIComponent(id)}`);
|
|
84
|
+
const canonicalId = canonicalUuidSchema.safeParse(value.id);
|
|
85
|
+
if (seq !== undefined && (!canonicalId.success || canonicalId.data !== id || value.seq !== undefined && value.seq !== seq)) {
|
|
86
|
+
throw new Error(cliApiMessages.invalid_response);
|
|
87
|
+
}
|
|
88
|
+
if (seq !== undefined)
|
|
89
|
+
return { ...value, id, seq };
|
|
90
|
+
const { seq: detailSeq, ...detail } = value;
|
|
91
|
+
return numbered && detailSeq !== undefined && !findingSequenceSchema.safeParse(detailSeq).success ? detail : value;
|
|
92
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { findingAudienceEnabled } from "../analysis-core/analysis/finding-audience-switch.js";
|
|
2
|
+
export function readFindingSettings(env = process.env, bundle) {
|
|
3
|
+
return { numbersEnabled: env.CF_FINDING_NUMBERS !== "0", fixesEnabled: env.CF_WAITING_FIXES !== "0",
|
|
4
|
+
audienceEnabled: findingAudienceEnabled(env, bundle) };
|
|
5
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { waitingFixesResponseSchema, claimFixRequestParamsSchema, claimFixRequestResponseSchema } from "../contract/fix-requests.js";
|
|
2
|
+
import { cliApiMessages } from "../contract/cli-api-messages.js";
|
|
3
|
+
import { ApiError } from "./api.js";
|
|
4
|
+
export const FIX_DISCOVERY_TIMEOUT_MS = 2_000;
|
|
5
|
+
export const fixRequestTools = [
|
|
6
|
+
{ name: "waiting_fixes", description: "Lists the fixes you asked for in the console that are still waiting. It is read-only and free.",
|
|
7
|
+
annotations: { readOnlyHint: true },
|
|
8
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
9
|
+
{ name: "claim_fix", description: "Marks a waiting fix as taken when you start it, without editing files or running an analysis.",
|
|
10
|
+
inputSchema: { type: "object", properties: { id: { type: "string", description: "The findingId returned by waiting_fixes." } }, required: ["id"], additionalProperties: false } },
|
|
11
|
+
];
|
|
12
|
+
export async function waitingFixes(api, options = {}) {
|
|
13
|
+
try {
|
|
14
|
+
const result = waitingFixesResponseSchema.safeParse(await api.request("fix-requests", undefined, true, options));
|
|
15
|
+
if (!result.success)
|
|
16
|
+
throw new Error(cliApiMessages.invalid_response);
|
|
17
|
+
return result.data;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
// An older console has no queue; read-only discovery still works during rollout.
|
|
21
|
+
if (error instanceof ApiError && error.status === 404)
|
|
22
|
+
return [];
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function waitingFixCount(api) {
|
|
27
|
+
try {
|
|
28
|
+
return (await waitingFixes(api, { timeoutMs: FIX_DISCOVERY_TIMEOUT_MS })).length;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function claimFix(api, id) {
|
|
35
|
+
const params = claimFixRequestParamsSchema.safeParse({ id });
|
|
36
|
+
if (!params.success)
|
|
37
|
+
throw new Error("Provide a finding id.");
|
|
38
|
+
const result = claimFixRequestResponseSchema.safeParse(await api.request(`fix-requests/${params.data.id}/claim`, {}));
|
|
39
|
+
if (!result.success || result.data.findingId !== params.data.id)
|
|
40
|
+
throw new Error(cliApiMessages.invalid_response);
|
|
41
|
+
return result.data;
|
|
42
|
+
}
|
|
@@ -1,18 +1,26 @@
|
|
|
1
|
+
import { resultSafetyCheckEnabled } from "../analysis-core/settings.js";
|
|
1
2
|
import { localFindingsPayloadSchema } from "../contract/console.js";
|
|
3
|
+
import { projectPublicFinding } from "../contract/public-report.js";
|
|
4
|
+
import { checkIntakeFindings } from "./result-check.js";
|
|
2
5
|
import { RESULTS_TOO_LARGE_MESSAGE } from "../contract/cli-refusals.js";
|
|
3
|
-
export function buildIntakePayload(identity, agent, report) {
|
|
6
|
+
export function buildIntakePayload(identity, agent, report, options = {}) {
|
|
4
7
|
const repo = (key) => key === null || key === report.repoKey ? agent.repository : key;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
let findings = report.findings.map(finding => projectPublicFinding(finding, repo));
|
|
9
|
+
let resultCheck = null;
|
|
10
|
+
let findingIndexes = report.findings.map((_, index) => index);
|
|
11
|
+
if (resultSafetyCheckEnabled(options.settings ?? {})) {
|
|
12
|
+
const captured = options.files;
|
|
13
|
+
if (!captured)
|
|
14
|
+
throw new Error("Result check requires a captured file snapshot");
|
|
15
|
+
const admitted = Object.fromEntries(agent.files.filter(file => Object.hasOwn(captured, file.path))
|
|
16
|
+
.map(file => [file.path, captured[file.path]]));
|
|
17
|
+
const { findings: accepted, acceptedIndexes, ...counts } = checkIntakeFindings(findings, { repoKey: agent.repository, files: admitted });
|
|
18
|
+
findings = accepted;
|
|
19
|
+
resultCheck = counts;
|
|
20
|
+
findingIndexes = acceptedIndexes;
|
|
21
|
+
}
|
|
14
22
|
const result = localFindingsPayloadSchema.safeParse({ ...identity, agent, findings });
|
|
15
23
|
if (!result.success)
|
|
16
24
|
throw new Error(RESULTS_TOO_LARGE_MESSAGE);
|
|
17
|
-
return result.data;
|
|
25
|
+
return { payload: result.data, resultCheck, findingIndexes };
|
|
18
26
|
}
|
package/dist/cli/main.js
CHANGED
|
@@ -4,13 +4,13 @@ import { realpath } from "node:fs/promises";
|
|
|
4
4
|
import { errorCode, errorText } from "./api.js";
|
|
5
5
|
import { readEmailSignIn } from "../adapters/settings.js";
|
|
6
6
|
import { commandContext } from "./context.js";
|
|
7
|
-
import { analyze } from "./commands/analyze.js";
|
|
7
|
+
import { analyze, parseAnalyzeOptions } from "./commands/analyze.js";
|
|
8
8
|
import { finding } from "./commands/finding.js";
|
|
9
9
|
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
|
-
const usage = "Usage: counterfact-labs analyze [path]\n counterfact-labs finding <id>\n counterfact-labs balance | login | logout | mcp\n";
|
|
13
|
+
const usage = "Usage: counterfact-labs analyze [path] [--proposal <json-file>]\n counterfact-labs finding <id>\n counterfact-labs balance | login | logout | mcp\n";
|
|
14
14
|
export async function run(args, options = {}) {
|
|
15
15
|
const write = options.write ?? (text => { process.stdout.write(text); });
|
|
16
16
|
const emailSignIn = options.emailSignIn ?? readEmailSignIn(options.env);
|
|
@@ -22,9 +22,14 @@ export async function run(args, options = {}) {
|
|
|
22
22
|
return 0;
|
|
23
23
|
}
|
|
24
24
|
if (command === "analyze") {
|
|
25
|
-
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = parseAnalyzeOptions(rest);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
26
30
|
throw new Error(usage.trimEnd());
|
|
27
|
-
|
|
31
|
+
}
|
|
32
|
+
return await analyze(parsed.path, parsed.options, commandContext(commandOptions));
|
|
28
33
|
}
|
|
29
34
|
if (command === "finding" && rest.length === 1 && !rest[0].startsWith("-"))
|
|
30
35
|
return await finding(rest[0], commandContext(commandOptions));
|
package/dist/cli/mcp.js
CHANGED
|
@@ -15,11 +15,16 @@ import { emailSignInMessages } from "../contract/email-sign-in-messages.js";
|
|
|
15
15
|
import { EMAIL_SIGN_IN_TERMS, normalizeSignInEmail, validSignInCode, startEmailSignIn, signInWithCode } from "./email-sign-in.js";
|
|
16
16
|
import { resolveAgentFolder, resolveMcpAgentFolder } from "./agent-folder.js";
|
|
17
17
|
import { suggestedEmail } from "./suggested-email.js";
|
|
18
|
+
import { agentProposalInputSchema } from "../contract/agent-proposal.js";
|
|
19
|
+
import { fixRequestTools, waitingFixes, waitingFixCount, claimFix, FIX_DISCOVERY_TIMEOUT_MS } from "./fix-requests.js";
|
|
20
|
+
import { FINDING_PROJECT_MESSAGE, FindingReferences, numberedFindingInputSchema } from "./finding-lookup.js";
|
|
21
|
+
import { findingDisplayRows, formatFindingNumber, formatWaitingFixes } from "./finding-display.js";
|
|
22
|
+
import { readFindingSettings } from "./finding-settings.js";
|
|
18
23
|
const legacyTools = [
|
|
19
24
|
{
|
|
20
25
|
name: ANALYZE_TOOL_NAME,
|
|
21
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.",
|
|
22
|
-
inputSchema: { type: "object", properties: { path: { type: "string", description: "The agent folder on this computer." } }, required: ["path"], additionalProperties: false },
|
|
27
|
+
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 },
|
|
23
28
|
},
|
|
24
29
|
{ 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 } },
|
|
25
30
|
{ name: "get_balance", description: "Get the balance and top-up link.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
@@ -35,12 +40,16 @@ class ProtocolError extends Error {
|
|
|
35
40
|
}
|
|
36
41
|
function object(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
37
42
|
export async function serveMcp(input, output, options = {}) {
|
|
43
|
+
const audienceChoice = (options.env ?? process.env).CF_FINDING_AUDIENCE;
|
|
38
44
|
const cwd = options.cwd ?? process.cwd();
|
|
39
45
|
const guarded = (options.env ?? process.env).CF_MCP_PATH_GUARD !== "0";
|
|
40
46
|
const launch = guarded ? resolveFolderPath(cwd).catch(() => null) : Promise.resolve(cwd);
|
|
41
47
|
const lastAnalysis = new Map();
|
|
48
|
+
const findingSettings = readFindingSettings(options.env);
|
|
49
|
+
const { numbersEnabled, fixesEnabled } = findingSettings;
|
|
50
|
+
const references = new FindingReferences();
|
|
42
51
|
const emailSignIn = options.emailSignIn ?? readEmailSignIn(options.env);
|
|
43
|
-
const
|
|
52
|
+
const signInTools = emailSignIn ? [...legacyTools.slice(0, 3),
|
|
44
53
|
{
|
|
45
54
|
name: "sign_in_status",
|
|
46
55
|
description: "Check whether this computer is signed in to Counterfact and whether the user has allowed the analysis tool in Claude Code. Use path only to choose the agent folder for the local git email suggestion; otherwise use the launch folder. Analysis permission is always checked in the launch folder. A valid local git email is returned as suggestedEmail. Prefer the signed-in user's email already in your context; otherwise offer suggestedEmail. Ask the user to confirm an address before sending a code. With wait: true, wait only for browser sign-in to finish.",
|
|
@@ -65,6 +74,9 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
65
74
|
}, required: ["email", "code"], additionalProperties: false },
|
|
66
75
|
},
|
|
67
76
|
] : legacyTools;
|
|
77
|
+
const tools = signInTools.map(tool => tool.name === "get_finding" && numbersEnabled ? {
|
|
78
|
+
...tool, inputSchema: numberedFindingInputSchema,
|
|
79
|
+
} : tool).concat(fixesEnabled ? fixRequestTools : []);
|
|
68
80
|
let pendingSignIn;
|
|
69
81
|
const requests = new Map();
|
|
70
82
|
let queue = Promise.resolve();
|
|
@@ -76,15 +88,18 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
76
88
|
if (!object(args))
|
|
77
89
|
throw new ProtocolError(-32602, "Tool arguments must be an object.");
|
|
78
90
|
const name = params.name;
|
|
79
|
-
const allowed = name === ANALYZE_TOOL_NAME ? ["path"] : name === "get_finding" ? ["id"]
|
|
80
|
-
: name === "
|
|
81
|
-
:
|
|
91
|
+
const allowed = name === ANALYZE_TOOL_NAME ? ["path", "proposal"] : name === "get_finding" ? (numbersEnabled ? Object.keys(numberedFindingInputSchema.properties) : ["id"])
|
|
92
|
+
: name === "claim_fix" ? ["id"]
|
|
93
|
+
: name === "sign_in_status" ? (emailSignIn ? ["wait", "path"] : ["wait"])
|
|
94
|
+
: emailSignIn && name === "sign_in" ? ["email"] : emailSignIn && name === "sign_in_with_code" ? ["email", "code"] : [];
|
|
82
95
|
if (!tools.some(tool => tool.name === name) || Object.keys(args).some(key => !allowed.includes(key)))
|
|
83
96
|
throw new ProtocolError(-32602, "Unknown tool or argument.");
|
|
84
97
|
if (name === ANALYZE_TOOL_NAME && (typeof args.path !== "string" || !args.path.trim()))
|
|
85
98
|
throw new ProtocolError(-32602, "Provide a path.");
|
|
86
99
|
if (name === "get_finding" && (typeof args.id !== "string" || !args.id.trim()))
|
|
87
100
|
throw new ProtocolError(-32602, "Provide a finding id.");
|
|
101
|
+
if (name === "get_finding" && args.projectId !== undefined && (typeof args.projectId !== "string" || !args.projectId.trim()))
|
|
102
|
+
throw new ProtocolError(-32602, FINDING_PROJECT_MESSAGE);
|
|
88
103
|
if (name === "sign_in_status" && args.wait !== undefined && typeof args.wait !== "boolean")
|
|
89
104
|
throw new ProtocolError(-32602, "Provide an optional boolean wait.");
|
|
90
105
|
if (name === "sign_in_status" && "path" in args && (typeof args.path !== "string" || !args.path.trim()))
|
|
@@ -96,12 +111,30 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
96
111
|
if (name === "sign_in_with_code" && !validSignInCode(args.code))
|
|
97
112
|
throw new ProtocolError(-32602, emailSignInMessages.invalid_code);
|
|
98
113
|
let text = "", exitCode = 0;
|
|
114
|
+
let agentChoice;
|
|
99
115
|
let findingIds;
|
|
100
|
-
let
|
|
116
|
+
let findings, projectId;
|
|
117
|
+
let loadedFinding;
|
|
118
|
+
let fixRequests, claimed;
|
|
119
|
+
let waitingCount;
|
|
120
|
+
let url, analysisFolder;
|
|
121
|
+
let started = false;
|
|
101
122
|
let signIn;
|
|
102
123
|
try {
|
|
103
|
-
const ctx = commandContext({ ...options,
|
|
124
|
+
const ctx = commandContext({ ...options, env: { ...(options.env ?? process.env), CF_FINDING_AUDIENCE: audienceChoice },
|
|
125
|
+
emailSignIn, findingSettings, signal, onCheckStart: () => {
|
|
126
|
+
started = true;
|
|
127
|
+
if (guarded && analysisFolder)
|
|
128
|
+
lastAnalysis.set(analysisFolder, ctx.now());
|
|
129
|
+
options.onCheckStart?.();
|
|
130
|
+
}, onAgentChoice: choice => { agentChoice = choice; options.onAgentChoice?.(choice); }, onCheckComplete: result => {
|
|
104
131
|
findingIds = result.findingIds ?? [];
|
|
132
|
+
if (numbersEnabled) {
|
|
133
|
+
findings = findingDisplayRows(result.findings ?? [], { numbersEnabled,
|
|
134
|
+
audienceEnabled: result.audienceEnabled, labels: "reference" }).map(row => row.finding);
|
|
135
|
+
projectId = result.projectId;
|
|
136
|
+
references.remember(result.projectId);
|
|
137
|
+
}
|
|
105
138
|
url = result.url;
|
|
106
139
|
options.onCheckComplete?.(result);
|
|
107
140
|
}, write: chunk => {
|
|
@@ -109,8 +142,23 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
109
142
|
if (!chunk.startsWith("\r"))
|
|
110
143
|
text += chunk;
|
|
111
144
|
} });
|
|
112
|
-
if (name === "
|
|
113
|
-
await
|
|
145
|
+
if (name === "waiting_fixes") {
|
|
146
|
+
const waiting = await waitingFixes(ctx.api);
|
|
147
|
+
fixRequests = waiting;
|
|
148
|
+
const projectsNeeded = references.projectsNeeded(waiting);
|
|
149
|
+
text = formatWaitingFixes(waiting, numbersEnabled, item => projectsNeeded.has(item.findingId));
|
|
150
|
+
}
|
|
151
|
+
else if (name === "claim_fix") {
|
|
152
|
+
claimed = await claimFix(ctx.api, args.id);
|
|
153
|
+
text = "The fix request is marked as taken.";
|
|
154
|
+
}
|
|
155
|
+
else if (name === "get_finding") {
|
|
156
|
+
const project = numbersEnabled ? await references.resolve(args.id, args.projectId, fixesEnabled ? () => waitingFixes(ctx.api, { timeoutMs: FIX_DISCOVERY_TIMEOUT_MS }) : undefined) : undefined;
|
|
157
|
+
await finding(args.id, ctx, { projectId: project, numbers: numbersEnabled, onLoaded: value => {
|
|
158
|
+
if (numbersEnabled)
|
|
159
|
+
loadedFinding = { ...value, ...(value.seq !== undefined ? { number: formatFindingNumber(value.seq) } : {}) };
|
|
160
|
+
} });
|
|
161
|
+
}
|
|
114
162
|
else if (name === "get_balance")
|
|
115
163
|
await balance(ctx);
|
|
116
164
|
else if (name === "sign_in_status") {
|
|
@@ -129,6 +177,8 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
129
177
|
}
|
|
130
178
|
}
|
|
131
179
|
signIn = { ...await signInStatus(ctx), analyzeToolAllowed: allowed };
|
|
180
|
+
if (fixesEnabled)
|
|
181
|
+
waitingCount = signIn.signedIn ? await waitingFixCount(ctx.api) : 0;
|
|
132
182
|
text = signIn.signedIn ? "Signed in." : "Not signed in.";
|
|
133
183
|
if (emailSignIn) {
|
|
134
184
|
const suggestion = await suggestedEmail(resolveAgentFolder(args.path ?? ".", cwd), ctx.readEmail);
|
|
@@ -162,7 +212,7 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
162
212
|
signal.addEventListener("abort", cancel, { once: true });
|
|
163
213
|
try {
|
|
164
214
|
signal.throwIfAborted();
|
|
165
|
-
const flow = await startSignIn(commandContext({ ...options, emailSignIn, homeDir: ctx.homeDir, baseUrl: ctx.api.baseUrl, signal: controller.signal }));
|
|
215
|
+
const flow = await startSignIn(commandContext({ ...options, emailSignIn, findingSettings, homeDir: ctx.homeDir, baseUrl: ctx.api.baseUrl, signal: controller.signal }));
|
|
166
216
|
pending = { flow, controller, opened: false };
|
|
167
217
|
pendingSignIn = pending;
|
|
168
218
|
const current = pending;
|
|
@@ -190,14 +240,9 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
190
240
|
if (guarded && last !== undefined && ctx.now() - last < 10 * 60 * 1000) {
|
|
191
241
|
throw new Error(FINISH_FAILURE_MESSAGE);
|
|
192
242
|
}
|
|
193
|
-
|
|
243
|
+
analysisFolder = folder;
|
|
194
244
|
try {
|
|
195
|
-
exitCode = await analyze(folder, {},
|
|
196
|
-
started = true;
|
|
197
|
-
if (guarded)
|
|
198
|
-
lastAnalysis.set(folder, ctx.now());
|
|
199
|
-
ctx.onCheckStart?.();
|
|
200
|
-
} });
|
|
245
|
+
exitCode = await analyze(folder, { proposal: args.proposal }, ctx);
|
|
201
246
|
}
|
|
202
247
|
finally {
|
|
203
248
|
if (guarded && started)
|
|
@@ -212,7 +257,9 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
212
257
|
// Some clients (Claude Code among them) give the model only the structured result when one is present, so it
|
|
213
258
|
// repeats the text: without it the model cannot read the result.
|
|
214
259
|
return { content: [{ type: "text", text }], isError: exitCode !== 0,
|
|
215
|
-
structuredContent: { text, exitCode, ...(findingIds ? { findingIds } : {}), ...(url ? { url } : {}), ...signIn
|
|
260
|
+
structuredContent: { text, exitCode, ...agentChoice, ...(findingIds ? { findingIds } : {}), ...(url ? { url } : {}), ...signIn,
|
|
261
|
+
...(findings ? { findings, projectId } : {}), ...(loadedFinding ? { finding: loadedFinding } : {}),
|
|
262
|
+
...(fixRequests ? { fixRequests } : {}), ...claimed, ...(waitingCount !== undefined ? { waitingFixes: waitingCount } : {}) } };
|
|
216
263
|
}
|
|
217
264
|
async function respond(message, id, signal) {
|
|
218
265
|
if (signal.aborted)
|