@counterfact-labs/cli 0.1.3 → 0.2.3
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 +176 -42
- package/bundle/client-release.json +7 -0
- package/bundle/sealed.json +908 -0
- package/dist/adapters/anthropic.js +151 -0
- package/dist/adapters/completion.js +112 -0
- package/dist/adapters/durable-write.js +34 -0
- package/dist/adapters/files.js +140 -0
- package/dist/adapters/folder-path.js +6 -0
- package/dist/adapters/git-ignore.js +49 -0
- package/dist/adapters/git.js +44 -0
- package/dist/adapters/ignored-files.js +36 -0
- package/dist/adapters/instructions.js +35 -0
- package/dist/adapters/models.js +51 -0
- package/dist/adapters/output-paths.js +92 -0
- package/dist/adapters/prompt-cache.js +28 -0
- package/dist/adapters/public-instructions.js +36 -0
- package/dist/adapters/quality.js +6 -0
- package/dist/adapters/relay-plan.js +15 -0
- package/dist/adapters/release.js +57 -0
- package/dist/adapters/report.js +29 -0
- package/dist/adapters/repository-files.js +199 -0
- package/dist/adapters/repository-root.js +120 -0
- package/dist/adapters/results.js +9 -0
- package/dist/adapters/settings.js +25 -0
- package/dist/adapters/snapshot.js +68 -0
- package/dist/analysis-core/analysis/analysis-flags.js +1 -0
- package/dist/analysis-core/analysis/code-agents/skipped-files.js +1 -0
- package/dist/analysis-core/analysis/finding-quality/config.js +1 -0
- package/dist/analysis-core/analysis/finding-quality/policy.js +1 -0
- package/dist/analysis-core/analysis/headlines/attachment.js +1 -0
- package/dist/analysis-core/analysis/headlines/input.js +1 -0
- package/dist/analysis-core/analysis/headlines/types.js +1 -0
- package/dist/analysis-core/analysis/schemas.js +1 -0
- package/dist/analysis-core/analysis/source-notes/types.js +1 -0
- package/dist/analysis-core/chunks/2PSNOSJ2.js +1 -0
- package/dist/analysis-core/chunks/3227KXHF.js +1 -0
- package/dist/analysis-core/chunks/326F4XNG.js +1 -0
- package/dist/analysis-core/chunks/3MTXB4MK.js +6 -0
- package/dist/analysis-core/chunks/4F6ZIHXU.js +1 -0
- package/dist/analysis-core/chunks/57M4OQJM.js +1 -0
- package/dist/analysis-core/chunks/6YCABS5T.js +1 -0
- package/dist/analysis-core/chunks/724WZMYW.js +2 -0
- package/dist/analysis-core/chunks/74FPPPIG.js +1 -0
- package/dist/analysis-core/chunks/7QDOHWSY.js +27 -0
- package/dist/analysis-core/chunks/BZNHHLNP.js +1 -0
- package/dist/analysis-core/chunks/DTMHXKP6.js +7 -0
- package/dist/analysis-core/chunks/E7TFIHC5.js +1 -0
- package/dist/analysis-core/chunks/EH3QQ3T4.js +1 -0
- package/dist/analysis-core/chunks/EQTIRIMU.js +37 -0
- package/dist/analysis-core/chunks/FSLWD2PE.js +1 -0
- package/dist/analysis-core/chunks/G322LST6.js +1 -0
- package/dist/analysis-core/chunks/HTLIRVP7.js +1 -0
- package/dist/analysis-core/chunks/IBCFGUHI.js +1 -0
- package/dist/analysis-core/chunks/IL2KRP3T.js +1 -0
- package/dist/analysis-core/chunks/J4AXGE5W.js +1 -0
- package/dist/analysis-core/chunks/JF4GYMOZ.js +1 -0
- package/dist/analysis-core/chunks/KEKTZDIK.js +1 -0
- package/dist/analysis-core/chunks/KU5DINLW.js +1 -0
- package/dist/analysis-core/chunks/M7MMAUAK.js +1 -0
- package/dist/analysis-core/chunks/OFBJNHC6.js +1 -0
- package/dist/analysis-core/chunks/ORCYIIHB.js +60 -0
- package/dist/analysis-core/chunks/PVPEY7NF.js +1 -0
- package/dist/analysis-core/chunks/PYKAERZZ.js +1 -0
- package/dist/analysis-core/chunks/QDYMR6KS.js +1 -0
- package/dist/analysis-core/chunks/QPXNXMLC.js +1 -0
- package/dist/analysis-core/chunks/SL27GMEN.js +1 -0
- package/dist/analysis-core/chunks/STZOC7LN.js +1 -0
- package/dist/analysis-core/chunks/T5TREZCQ.js +1 -0
- package/dist/analysis-core/chunks/TDNTU4TF.js +1 -0
- package/dist/analysis-core/chunks/U5HOEVPE.js +1 -0
- package/dist/analysis-core/chunks/UEWL3GMQ.js +1 -0
- package/dist/analysis-core/chunks/VER355VN.js +1 -0
- package/dist/analysis-core/chunks/VFENG4MR.js +1 -0
- package/dist/analysis-core/chunks/VGYWI4X2.js +1 -0
- package/dist/analysis-core/chunks/Y5AUKKYG.js +1 -0
- package/dist/analysis-core/chunks/Y7JGSUOA.js +1 -0
- package/dist/analysis-core/chunks/YVDCJN7D.js +14 -0
- package/dist/analysis-core/chunks/ZAA44GPI.js +1 -0
- package/dist/analysis-core/chunks/ZMUBMYWN.js +1 -0
- package/dist/analysis-core/contracts/requests.js +1 -0
- package/dist/analysis-core/conventions/file-admission.js +1 -0
- package/dist/analysis-core/data/finding-types.js +1 -0
- package/dist/analysis-core/errors.js +1 -0
- package/dist/analysis-core/findings/presentation.js +1 -0
- package/dist/analysis-core/github/repo-key.js +1 -0
- package/dist/analysis-core/import/outside-loader-sources.js +1 -0
- package/dist/analysis-core/index.js +6 -0
- package/dist/analysis-core/llm/cache-policy.js +1 -0
- package/dist/analysis-core/llm/cost.js +1 -0
- package/dist/analysis-core/llm/execution.js +1 -0
- package/dist/analysis-core/llm/output-limit.js +1 -0
- package/dist/analysis-core/llm/slots.js +1 -0
- package/dist/analysis-core/llm/stable-json.js +1 -0
- package/dist/analysis-core/llm/structured-output-error.js +1 -0
- package/dist/analysis-core/map-limit.js +1 -0
- package/dist/analysis-core/requests/answer-formats.js +1 -0
- package/dist/analysis-core/requests/check-diagnostics.js +1 -0
- package/dist/analysis-core/requests/headline-request.js +1 -0
- package/dist/analysis-core/requests/instruction-text.js +1 -0
- package/dist/analysis-core/requests/local-plan.js +1 -0
- package/dist/analysis-core/requests/registry.js +1 -0
- package/dist/analysis-core/requests/schema-descriptions.js +1 -0
- package/dist/analysis-core/requests/shape-finding-input.js +1 -0
- package/dist/analysis-core/requests/template-version.js +1 -0
- package/dist/analysis-core/requests/tools.js +1 -0
- package/dist/analysis-core/settings.js +1 -0
- package/dist/bundle/seal/deserialise.js +75 -0
- package/dist/bundle/seal/encryption.js +25 -0
- package/dist/bundle/seal/metadata.js +69 -0
- package/dist/bundle/seal/open.js +38 -0
- package/dist/bundle/seal/planned-calls.js +20 -0
- package/dist/bundle/seal/signature.js +51 -0
- package/dist/cli/agent-folder.js +17 -0
- package/dist/cli/api-types.js +2 -0
- package/dist/cli/api.js +1 -0
- package/dist/{claude-code-permission.js → cli/claude-code-permission.js} +4 -3
- package/dist/cli/commands/analyze.js +73 -0
- package/dist/cli/commands/login.js +51 -0
- package/dist/cli/completion-recovery.js +18 -0
- package/dist/{context.js → cli/context.js} +8 -14
- package/dist/cli/email-sign-in.js +56 -0
- package/dist/{git.js → cli/git.js} +12 -8
- package/dist/cli/intake-payload.js +18 -0
- package/dist/{main.js → cli/main.js} +23 -12
- package/dist/{mcp.js → cli/mcp.js} +109 -40
- package/dist/cli/output.js +10 -0
- package/dist/cli/relay-check.js +239 -0
- package/dist/cli/relay-preparation.js +31 -0
- package/dist/cli/sign-in-input.js +97 -0
- package/dist/{sign-in.js → cli/sign-in.js} +2 -1
- package/dist/cli/suggested-email.js +10 -0
- package/dist/cli/summary.js +30 -0
- package/dist/contract/cli-api-messages.js +5 -0
- package/dist/contract/cli-refusals.js +22 -0
- package/dist/contract/cli-version.js +1 -0
- package/dist/contract/console-url.js +8 -0
- package/dist/contract/console.js +111 -0
- package/dist/contract/email-sign-in-messages.js +18 -0
- package/dist/contract/messages.js +252 -0
- package/dist/contract/release-key.js +2 -0
- package/dist/contract/runtime-environment.js +37 -0
- package/dist/core/analysis/finding-quality/config.js +4 -0
- package/dist/core/analysis/run-to-end.js +55 -0
- package/dist/core/analysis/structural-run.js +16 -0
- package/dist/gatekeeper/attestation.js +67 -0
- package/dist/gatekeeper/certificate-algorithms.js +18 -0
- package/dist/gatekeeper/certificate-path.js +99 -0
- package/dist/gatekeeper/cli-api.js +174 -0
- package/dist/gatekeeper/client.js +76 -0
- package/dist/gatekeeper/connection-exchange.js +60 -0
- package/dist/gatekeeper/connection.js +140 -0
- package/dist/gatekeeper/console.js +168 -0
- package/dist/gatekeeper/loopback.js +5 -0
- package/dist/gatekeeper/model.js +70 -0
- package/dist/gatekeeper/production-attestation.js +149 -0
- package/dist/gatekeeper/trust/hardware.js +21 -0
- package/dist/gatekeeper/trust/policy.js +42 -0
- package/dist/gatekeeper/trust/production.json +25 -0
- package/dist/setup-pings/client.js +31 -0
- package/dist/setup-pings/contract.js +2 -0
- package/dist/setup-pings/policy.js +3 -0
- package/dist/setup-pings/state.js +261 -0
- package/dist/setup-pings/worker.js +95 -0
- package/dist/transport/base64url.js +9 -0
- package/dist/transport/http-response.js +89 -0
- package/dist/transport/tls-exporter.js +5 -0
- package/package.json +57 -11
- package/LICENSE +0 -1
- package/dist/api-types.js +0 -2
- package/dist/api.js +0 -97
- package/dist/commands/check.js +0 -104
- package/dist/commands/login.js +0 -9
- package/dist/files.js +0 -99
- package/dist/gitignore.js +0 -70
- package/dist/output.js +0 -23
- package/dist/secret-files.js +0 -5
- /package/dist/{browser.js → cli/browser.js} +0 -0
- /package/dist/{commands → cli/commands}/balance.js +0 -0
- /package/dist/{commands → cli/commands}/finding.js +0 -0
- /package/dist/{commands → cli/commands}/logout.js +0 -0
- /package/dist/{credentials.js → cli/credentials.js} +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { loadInstalledRelease } from "../../adapters/release.js";
|
|
2
|
+
import { cliRefusals, START_FAILURE_MESSAGE } from "../../contract/cli-refusals.js";
|
|
3
|
+
import { CLI_VERSION } from "../../contract/cli-version.js";
|
|
4
|
+
import { cliApiMessages } from "../../contract/cli-api-messages.js";
|
|
5
|
+
import { ApiError } from "../api.js";
|
|
6
|
+
import { resolveAgentFolder } from "../agent-folder.js";
|
|
7
|
+
import { runRelayCheck } from "../relay-check.js";
|
|
8
|
+
// Only a real pause says "paused"; an unreviewed release or an upload-only server cannot start.
|
|
9
|
+
const unavailable = START_FAILURE_MESSAGE;
|
|
10
|
+
function requireLocal(value) {
|
|
11
|
+
if (!value || typeof value !== "object" || !("mode" in value) || !("minimumLocalCliVersion" in value)
|
|
12
|
+
|| typeof value.minimumLocalCliVersion !== "string" || !/^\d+\.\d+\.\d+$/.test(value.minimumLocalCliVersion)) {
|
|
13
|
+
throw new Error(cliApiMessages.invalid_response);
|
|
14
|
+
}
|
|
15
|
+
const current = CLI_VERSION.split(".").map(BigInt), minimum = value.minimumLocalCliVersion.split(".").map(BigInt);
|
|
16
|
+
for (let i = 0; i < current.length; i++) {
|
|
17
|
+
if (minimum[i] > current[i])
|
|
18
|
+
throw new ApiError(426, "cli_update_required", cliRefusals.cli_update_required);
|
|
19
|
+
if (minimum[i] < current[i])
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
if (value.mode === "paused")
|
|
23
|
+
throw new ApiError(503, "local_checks_paused", cliRefusals.local_checks_paused);
|
|
24
|
+
// A local-capable client never falls back to sending source files to the hosted upload API.
|
|
25
|
+
if (value.mode !== "local")
|
|
26
|
+
throw new ApiError(503, "unavailable", unavailable);
|
|
27
|
+
}
|
|
28
|
+
export async function analyze(path, _options, ctx) {
|
|
29
|
+
const startup = ctx.startup;
|
|
30
|
+
requireLocal(await ctx.api.request("checks/mode"));
|
|
31
|
+
const release = ctx.relayRuntime?.release ?? await loadInstalledRelease();
|
|
32
|
+
if (release.projectContext !== "checks")
|
|
33
|
+
throw new Error(unavailable);
|
|
34
|
+
const token = await ctx.token?.();
|
|
35
|
+
if (!token)
|
|
36
|
+
throw new Error("Not signed in. Run counterfact-labs login.");
|
|
37
|
+
const folder = resolveAgentFolder(path, ctx.cwd);
|
|
38
|
+
const config = Object.freeze({ ...startup.transport, CF_RELAY_URL: startup.transport.CF_RELAY_URL ?? release.relayUrl,
|
|
39
|
+
CF_LOCAL_RELAY: "1", CF_CONSOLE_URL: ctx.api.baseUrl, CF_CONSOLE_TOKEN: token });
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const cancel = () => controller.abort();
|
|
42
|
+
process.once("SIGINT", cancel);
|
|
43
|
+
process.once("SIGTERM", cancel);
|
|
44
|
+
let progressShown = false;
|
|
45
|
+
const clearProgress = () => { if (progressShown) {
|
|
46
|
+
ctx.write("\r\u001b[2K");
|
|
47
|
+
progressShown = false;
|
|
48
|
+
} };
|
|
49
|
+
try {
|
|
50
|
+
return await (ctx.relayRuntime?.run ?? runRelayCheck)(folder, {
|
|
51
|
+
settings: startup.settings, config, release, connect: ctx.relayRuntime?.connect,
|
|
52
|
+
signal: ctx.signal ? AbortSignal.any([controller.signal, ctx.signal]) : controller.signal,
|
|
53
|
+
stdout() { }, stderr() { },
|
|
54
|
+
onStart: () => { ctx.onCheckStart?.(); ctx.write("\r\u001b[2KAnalyzing the agent…"); progressShown = true; },
|
|
55
|
+
onError: failure => {
|
|
56
|
+
clearProgress();
|
|
57
|
+
ctx.write(`${failure.message}\n`);
|
|
58
|
+
},
|
|
59
|
+
onSaved: result => {
|
|
60
|
+
clearProgress();
|
|
61
|
+
ctx.onCheckComplete?.(result);
|
|
62
|
+
if (result.findingIds?.length)
|
|
63
|
+
ctx.write(`Finding ids: ${result.findingIds.join(", ")}\n`);
|
|
64
|
+
ctx.write(`Full results: ${result.url}\n`);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
clearProgress();
|
|
70
|
+
process.removeListener("SIGINT", cancel);
|
|
71
|
+
process.removeListener("SIGTERM", cancel);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { emailSignInMessages } from "../../contract/email-sign-in-messages.js";
|
|
2
|
+
import { EMAIL_SIGN_IN_TERMS, normalizeSignInEmail, signInWithCode, startEmailSignIn } from "../email-sign-in.js";
|
|
3
|
+
import { startSignIn } from "../sign-in.js";
|
|
4
|
+
export function parseLoginOptions(args) {
|
|
5
|
+
// The router supplies usage for syntax errors without repeating private arguments.
|
|
6
|
+
const options = {};
|
|
7
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
8
|
+
const flag = args[index];
|
|
9
|
+
const value = args[index + 1];
|
|
10
|
+
if ((flag !== "--email" && flag !== "--code") || !value?.trim() || value.startsWith("--"))
|
|
11
|
+
throw new Error();
|
|
12
|
+
const key = flag === "--email" ? "email" : "code";
|
|
13
|
+
if (options[key] !== undefined)
|
|
14
|
+
throw new Error();
|
|
15
|
+
options[key] = value;
|
|
16
|
+
}
|
|
17
|
+
if (options.code !== undefined && options.email === undefined)
|
|
18
|
+
throw new Error();
|
|
19
|
+
return options;
|
|
20
|
+
}
|
|
21
|
+
export async function login(ctx, options = {}) {
|
|
22
|
+
if (options.email !== undefined) {
|
|
23
|
+
if (!ctx.startup.emailSignIn)
|
|
24
|
+
throw new Error(emailSignInMessages.email_unavailable);
|
|
25
|
+
const email = normalizeSignInEmail(options.email);
|
|
26
|
+
if (!email)
|
|
27
|
+
throw new Error(emailSignInMessages.invalid_email);
|
|
28
|
+
ctx.write(EMAIL_SIGN_IN_TERMS + "\n");
|
|
29
|
+
let code = options.code;
|
|
30
|
+
if (code === undefined) {
|
|
31
|
+
await startEmailSignIn(ctx, email);
|
|
32
|
+
ctx.write(`We sent a sign-in code to ${email}.\n`);
|
|
33
|
+
if (!ctx.signInInput.isTerminal) {
|
|
34
|
+
ctx.write(`To finish signing in, run counterfact-labs login --email ${email} --code <code>.\n`);
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
code = await ctx.signInInput.readCode("Enter the code from your email: ", ctx.signal);
|
|
38
|
+
if (code === undefined)
|
|
39
|
+
throw new Error(emailSignInMessages.cancelled);
|
|
40
|
+
}
|
|
41
|
+
await signInWithCode(ctx, email, code);
|
|
42
|
+
ctx.write("Signed in.\n");
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
const flow = await startSignIn(ctx);
|
|
46
|
+
ctx.write(`1. Open ${flow.verificationUri}. If asked, sign in with the code we email you.\n`
|
|
47
|
+
+ `2. Then enter this code on that page: ${flow.userCode}\n`);
|
|
48
|
+
await flow.done;
|
|
49
|
+
ctx.write("Signed in.\n");
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const COMPLETION_SETTINGS_MISMATCH_MESSAGE = "This folder has a saved result for a different Counterfact project or address. Run the analysis again with the original settings to save it, or delete .counterfact/completion.json to start a new analysis.";
|
|
2
|
+
export const matchesCheck = (status, identity) => status.checkId === identity.checkId && status.projectId === identity.projectId;
|
|
3
|
+
export function completionRecoveryDecision(input) {
|
|
4
|
+
if (input.localMismatch)
|
|
5
|
+
return { finished: false, message: COMPLETION_SETTINGS_MISMATCH_MESSAGE };
|
|
6
|
+
if (!input.statusSucceeded)
|
|
7
|
+
return { finished: false };
|
|
8
|
+
const { status, completedPayload } = input;
|
|
9
|
+
// A different analysis cannot settle this saved payload, even when submission reports a conflict.
|
|
10
|
+
if (status && completedPayload && !matchesCheck(status, completedPayload))
|
|
11
|
+
return { finished: false };
|
|
12
|
+
// Only trusted terminal status retires unsaved work; outages and closing work remain retryable.
|
|
13
|
+
if (status?.uploadState === "missing" && (status.state === "closed" || status.state === "expired")
|
|
14
|
+
&& completedPayload && matchesCheck(status, completedPayload))
|
|
15
|
+
return { finished: true };
|
|
16
|
+
// A refusal never retires paid work by itself; only the console's own terminal status above can.
|
|
17
|
+
return { finished: false };
|
|
18
|
+
}
|
|
@@ -1,28 +1,22 @@
|
|
|
1
|
+
import { readCliSettings } from "../adapters/settings.js";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
|
-
import { createInterface } from "node:readline/promises";
|
|
3
3
|
import { setTimeout as delay } from "node:timers/promises";
|
|
4
4
|
import { ApiClient, DEFAULT_URL } from "./api.js";
|
|
5
5
|
import { openBrowser } from "./browser.js";
|
|
6
6
|
import { readCredentials } from "./credentials.js";
|
|
7
|
+
import { userEmail } from "./git.js";
|
|
8
|
+
import { terminalSignInInput } from "./sign-in-input.js";
|
|
7
9
|
export function commandContext(options = {}) {
|
|
10
|
+
const startup = readCliSettings(options.env, options.emailSignIn);
|
|
8
11
|
const homeDir = options.homeDir ?? homedir();
|
|
9
|
-
const baseUrl = (options.baseUrl ??
|
|
12
|
+
const baseUrl = (options.baseUrl ?? startup.baseUrl ?? DEFAULT_URL).replace(/\/+$/, "");
|
|
10
13
|
const api = new ApiClient(baseUrl, () => readCredentials(homeDir, api.baseUrl), undefined, options.signal);
|
|
11
14
|
const write = options.write ?? (text => { process.stdout.write(text); });
|
|
12
15
|
return {
|
|
16
|
+
startup, signal: options.signal, relayRuntime: options.relayRuntime, cwd: options.cwd ?? process.cwd(),
|
|
17
|
+
token: () => readCredentials(homeDir, api.baseUrl), onCheckComplete: options.onCheckComplete, onCheckStart: options.onCheckStart,
|
|
13
18
|
api, homeDir, write, openBrowser: options.openBrowser ?? openBrowser,
|
|
19
|
+
signInInput: options.signInInput ?? terminalSignInInput(), readEmail: options.readEmail ?? userEmail,
|
|
14
20
|
sleep: options.sleep ?? (async (ms) => { await delay(ms, undefined, { signal: options.signal }); }), now: options.now ?? Date.now,
|
|
15
|
-
ask: options.ask ?? (async (question) => {
|
|
16
|
-
if (!process.stdin.isTTY)
|
|
17
|
-
throw new Error("Use --yes to confirm a check, or --dry-run to see the files and price.");
|
|
18
|
-
write(question);
|
|
19
|
-
const input = createInterface({ input: process.stdin });
|
|
20
|
-
try {
|
|
21
|
-
return /^(?:y|yes)?$/i.test((await input.question("")).trim());
|
|
22
|
-
}
|
|
23
|
-
finally {
|
|
24
|
-
input.close();
|
|
25
|
-
}
|
|
26
|
-
}),
|
|
27
21
|
};
|
|
28
22
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { emailSignInMessages } from "../contract/email-sign-in-messages.js";
|
|
2
|
+
import { saveCredentials } from "./credentials.js";
|
|
3
|
+
export const EMAIL_SIGN_IN_TERMS = "By signing in, you agree to our Terms of Service (https://counterfactlabs.com/terms) and Privacy Policy (https://counterfactlabs.com/privacy).";
|
|
4
|
+
function hasControlCharacters(value) {
|
|
5
|
+
return [...value].some(character => {
|
|
6
|
+
const code = character.charCodeAt(0);
|
|
7
|
+
return code < 32 || code === 127;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
export function normalizeSignInEmail(value) {
|
|
11
|
+
if (typeof value !== "string")
|
|
12
|
+
return undefined;
|
|
13
|
+
const email = value.trim().toLowerCase();
|
|
14
|
+
if (email.length > 320 || !/^[^@\s]+@[^@\s.]+\.[^@\s]+$/.test(email)
|
|
15
|
+
|| /[<>()[\],:;"\\]/.test(email) || hasControlCharacters(email))
|
|
16
|
+
return undefined;
|
|
17
|
+
// ASCII domain labels keep the recipient spelling consistent with the console's mailer.
|
|
18
|
+
const domain = email.split("@")[1];
|
|
19
|
+
return /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/.test(domain) ? email : undefined;
|
|
20
|
+
}
|
|
21
|
+
export function validSignInCode(value) {
|
|
22
|
+
return typeof value === "string" && value.trim().length > 0 && !hasControlCharacters(value.trim()) && !/[\u2028\u2029]/.test(value.trim());
|
|
23
|
+
}
|
|
24
|
+
function requireEmail(value) {
|
|
25
|
+
const email = normalizeSignInEmail(value);
|
|
26
|
+
if (!email)
|
|
27
|
+
throw new Error(emailSignInMessages.invalid_email);
|
|
28
|
+
return email;
|
|
29
|
+
}
|
|
30
|
+
export async function startEmailSignIn(ctx, email) {
|
|
31
|
+
const value = await ctx.api.request("email/start", { email: requireEmail(email) }, false);
|
|
32
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || !("expiresIn" in value)
|
|
33
|
+
|| typeof value.expiresIn !== "number" || !Number.isInteger(value.expiresIn) || value.expiresIn <= 0) {
|
|
34
|
+
throw new Error(emailSignInMessages.invalid_response);
|
|
35
|
+
}
|
|
36
|
+
return { expiresIn: value.expiresIn };
|
|
37
|
+
}
|
|
38
|
+
export async function signInWithCode(ctx, email, code) {
|
|
39
|
+
const normalizedEmail = requireEmail(email);
|
|
40
|
+
if (!validSignInCode(code))
|
|
41
|
+
throw new Error(emailSignInMessages.invalid_code);
|
|
42
|
+
const value = await ctx.api.request("email/verify", { email: normalizedEmail, code: code.trim(), clientName: "Command line" }, false);
|
|
43
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || !("token" in value)
|
|
44
|
+
|| typeof value.token !== "string" || !value.token.trim())
|
|
45
|
+
throw new Error(emailSignInMessages.invalid_response);
|
|
46
|
+
// A request can complete just as the caller cancels; do not replace a saved sign-in then.
|
|
47
|
+
if (ctx.signal?.aborted)
|
|
48
|
+
throw new DOMException(emailSignInMessages.cancelled, "AbortError");
|
|
49
|
+
try {
|
|
50
|
+
await saveCredentials(ctx.homeDir, ctx.api.baseUrl, value.token);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new Error(emailSignInMessages.request_failed);
|
|
54
|
+
}
|
|
55
|
+
return "signed_in";
|
|
56
|
+
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
1
|
import { basename, relative, resolve, sep } from "node:path";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { gitRepositoryRoot } from "../adapters/repository-root.js";
|
|
3
|
+
import { runGit } from "../adapters/git.js";
|
|
4
|
+
export async function userEmail(folder, git = async (args) => (await runGit(folder, args, { userConfig: true })).toString("utf8")) {
|
|
7
5
|
try {
|
|
8
|
-
|
|
6
|
+
return await git(["config", "--get", "user.email"]);
|
|
9
7
|
}
|
|
10
8
|
catch {
|
|
11
|
-
return
|
|
9
|
+
return undefined;
|
|
12
10
|
}
|
|
11
|
+
}
|
|
12
|
+
export async function repositoryFacts(root, git = async (args) => (await runGit(root, args)).toString("utf8")) {
|
|
13
|
+
const folderName = basename(resolve(root));
|
|
14
|
+
const repoRoot = await gitRepositoryRoot(git);
|
|
15
|
+
if (repoRoot === null)
|
|
16
|
+
return { remoteUrl: null, relPath: "", folderName, branch: null, commit: null, dirty: null };
|
|
13
17
|
let remoteUrl;
|
|
14
18
|
try {
|
|
15
19
|
remoteUrl = (await git(["config", "--get", "remote.origin.url"])).trim().replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/]*@/i, "$1") || null;
|
|
@@ -28,7 +32,7 @@ export async function repositoryFacts(root, git = async (args) => (await promisi
|
|
|
28
32
|
const [branch, commit, status] = await Promise.all([
|
|
29
33
|
read(["rev-parse", "--abbrev-ref", "HEAD"]), read(["rev-parse", "HEAD"]),
|
|
30
34
|
// Include new files even when Git hides them, and exclude sibling folders.
|
|
31
|
-
read(["status", "--porcelain", "--untracked-files=normal", "--", "."]),
|
|
35
|
+
read(["status", "--porcelain=v2", "--no-renames", "--ignore-submodules=all", "--untracked-files=normal", "--", "."]),
|
|
32
36
|
]);
|
|
33
37
|
return { repoRoot, remoteUrl, relPath: relative(repoRoot, root).split(sep).join("/"), folderName,
|
|
34
38
|
branch: branch === "HEAD" ? null : branch || null, commit: commit || null, dirty: status === null ? null : status.length > 0 };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { localFindingsPayloadSchema } from "../contract/console.js";
|
|
2
|
+
import { RESULTS_TOO_LARGE_MESSAGE } from "../contract/cli-refusals.js";
|
|
3
|
+
export function buildIntakePayload(identity, agent, report) {
|
|
4
|
+
const repo = (key) => key === null || key === report.repoKey ? agent.repository : key;
|
|
5
|
+
const findings = report.findings.map(finding => {
|
|
6
|
+
// The local report may add presentation fields; the intake accepts cleaned evidence only.
|
|
7
|
+
const { kind, severity, title, cause, summary, category, question, site, excerpt, otherSites, fixes, sourceNotes } = finding;
|
|
8
|
+
return { kind, severity, title, cause, summary, category, question, site, excerpt,
|
|
9
|
+
fixes: fixes.map(({ title, approach, kind, targetFiles, expectedOutcome, summary, impact, confidence, objective, status, blockedBy, alsoFixes, delta }) => ({ title, approach, kind, targetFiles, expectedOutcome, summary, impact, confidence, objective, status, blockedBy, alsoFixes, delta })),
|
|
10
|
+
siteRepo: repo(finding.siteRepo), otherSites: otherSites.map(site => ({ ...site, repo: repo(site.repo) })),
|
|
11
|
+
...(sourceNotes === undefined ? {} : { sourceNotes: sourceNotes && { ...sourceNotes,
|
|
12
|
+
counterEvidence: sourceNotes.counterEvidence.map(site => ({ ...site, repo: repo(site.repo) })) } }) };
|
|
13
|
+
});
|
|
14
|
+
const result = localFindingsPayloadSchema.safeParse({ ...identity, agent, findings });
|
|
15
|
+
if (!result.success)
|
|
16
|
+
throw new Error(RESULTS_TOO_LARGE_MESSAGE);
|
|
17
|
+
return result.data;
|
|
18
|
+
}
|
|
@@ -2,39 +2,50 @@
|
|
|
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
6
|
import { commandContext } from "./context.js";
|
|
6
|
-
import {
|
|
7
|
+
import { analyze } from "./commands/analyze.js";
|
|
7
8
|
import { finding } from "./commands/finding.js";
|
|
8
9
|
import { balance } from "./commands/balance.js";
|
|
9
|
-
import { login } from "./commands/login.js";
|
|
10
|
+
import { login, parseLoginOptions } from "./commands/login.js";
|
|
10
11
|
import { logout } from "./commands/logout.js";
|
|
11
12
|
import { serveMcp } from "./mcp.js";
|
|
12
|
-
const usage = "Usage: counterfact-labs
|
|
13
|
+
const usage = "Usage: counterfact-labs analyze [path]\n counterfact-labs finding <id>\n counterfact-labs balance | login | logout | mcp\n";
|
|
13
14
|
export async function run(args, options = {}) {
|
|
14
15
|
const write = options.write ?? (text => { process.stdout.write(text); });
|
|
16
|
+
const emailSignIn = options.emailSignIn ?? readEmailSignIn(options.env);
|
|
17
|
+
const commandOptions = { ...options, emailSignIn };
|
|
15
18
|
try {
|
|
16
19
|
const [command, ...rest] = args;
|
|
17
20
|
if (["--help", "-h", "help"].includes(command) || !command) {
|
|
18
21
|
write(usage);
|
|
19
22
|
return 0;
|
|
20
23
|
}
|
|
21
|
-
if (command === "
|
|
22
|
-
|
|
23
|
-
const paths = rest.filter(arg => !arg.startsWith("--"));
|
|
24
|
-
if (paths.length > 1 || flags.some(flag => !["--yes", "--dry-run"].includes(flag)))
|
|
24
|
+
if (command === "analyze") {
|
|
25
|
+
if (rest.length > 1 || rest.some(arg => arg.startsWith("-")))
|
|
25
26
|
throw new Error(usage.trimEnd());
|
|
26
|
-
return await
|
|
27
|
+
return await analyze(rest[0] ?? ".", {}, commandContext(commandOptions));
|
|
27
28
|
}
|
|
28
29
|
if (command === "finding" && rest.length === 1 && !rest[0].startsWith("-"))
|
|
29
|
-
return await finding(rest[0], commandContext(
|
|
30
|
+
return await finding(rest[0], commandContext(commandOptions));
|
|
31
|
+
if (command === "login" && emailSignIn) {
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = parseLoginOptions(rest);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new Error(usage.trimEnd());
|
|
38
|
+
}
|
|
39
|
+
return await login(commandContext(commandOptions), parsed);
|
|
40
|
+
}
|
|
30
41
|
if (rest.length)
|
|
31
42
|
throw new Error(usage.trimEnd());
|
|
32
43
|
if (command === "balance")
|
|
33
|
-
return await balance(commandContext(
|
|
44
|
+
return await balance(commandContext(commandOptions));
|
|
34
45
|
if (command === "login")
|
|
35
|
-
return await login(commandContext(
|
|
46
|
+
return await login(commandContext(commandOptions));
|
|
36
47
|
if (command === "logout")
|
|
37
|
-
return await logout(commandContext(
|
|
48
|
+
return await logout(commandContext(commandOptions));
|
|
38
49
|
if (command === "mcp") {
|
|
39
50
|
await serveMcp(process.stdin, process.stdout, options);
|
|
40
51
|
return 0;
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
2
|
import { once } from "node:events";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { resolveFolderPath } from "../adapters/folder-path.js";
|
|
4
|
+
import { FINISH_FAILURE_MESSAGE, START_FAILURE_MESSAGE } from "../contract/cli-refusals.js";
|
|
5
5
|
import { errorCode, errorText } from "./api.js";
|
|
6
6
|
import { commandContext } from "./context.js";
|
|
7
|
-
import {
|
|
7
|
+
import { analyze } from "./commands/analyze.js";
|
|
8
|
+
import { CLI_VERSION } from "../contract/cli-version.js";
|
|
8
9
|
import { finding } from "./commands/finding.js";
|
|
9
10
|
import { balance } from "./commands/balance.js";
|
|
10
|
-
import {
|
|
11
|
+
import { ANALYZE_TOOL_NAME, analyzeToolAllowed } from "./claude-code-permission.js";
|
|
11
12
|
import { signInStatus, startSignIn } from "./sign-in.js";
|
|
12
|
-
|
|
13
|
+
import { readEmailSignIn } from "../adapters/settings.js";
|
|
14
|
+
import { emailSignInMessages } from "../contract/email-sign-in-messages.js";
|
|
15
|
+
import { EMAIL_SIGN_IN_TERMS, normalizeSignInEmail, validSignInCode, startEmailSignIn, signInWithCode } from "./email-sign-in.js";
|
|
16
|
+
import { resolveAgentFolder, resolveMcpAgentFolder } from "./agent-folder.js";
|
|
17
|
+
import { suggestedEmail } from "./suggested-email.js";
|
|
18
|
+
const legacyTools = [
|
|
13
19
|
{
|
|
14
|
-
name:
|
|
15
|
-
description: "
|
|
16
|
-
|
|
17
|
-
+ "Suggest a check after edits; wait for the user to ask for it. If the tool call is cancelled or times out, "
|
|
18
|
-
+ "a confirmed check keeps running on the server and its result will be in the console's Checks page.",
|
|
19
|
-
inputSchema: { type: "object", properties: { path: { type: "string", description: "The agent folder on this computer." }, confirm: { type: "boolean", default: false } }, required: ["path"], additionalProperties: false },
|
|
20
|
+
name: ANALYZE_TOOL_NAME,
|
|
21
|
+
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 },
|
|
20
23
|
},
|
|
21
|
-
{ 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 } },
|
|
24
|
+
{ 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 } },
|
|
22
25
|
{ name: "get_balance", description: "Get the balance and top-up link.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
23
|
-
{ name: "sign_in_status", description: "Check whether this computer is signed in to Counterfact, and whether the user has allowed the
|
|
26
|
+
{ 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 } },
|
|
24
27
|
{ 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 } },
|
|
25
28
|
];
|
|
26
29
|
class ProtocolError extends Error {
|
|
@@ -32,9 +35,36 @@ class ProtocolError extends Error {
|
|
|
32
35
|
}
|
|
33
36
|
function object(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
34
37
|
export async function serveMcp(input, output, options = {}) {
|
|
35
|
-
const { version } = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
36
|
-
const previews = new Map();
|
|
37
38
|
const cwd = options.cwd ?? process.cwd();
|
|
39
|
+
const guarded = (options.env ?? process.env).CF_MCP_PATH_GUARD !== "0";
|
|
40
|
+
const launch = guarded ? resolveFolderPath(cwd).catch(() => null) : Promise.resolve(cwd);
|
|
41
|
+
const lastAnalysis = new Map();
|
|
42
|
+
const emailSignIn = options.emailSignIn ?? readEmailSignIn(options.env);
|
|
43
|
+
const tools = emailSignIn ? [...legacyTools.slice(0, 3),
|
|
44
|
+
{
|
|
45
|
+
name: "sign_in_status",
|
|
46
|
+
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.",
|
|
47
|
+
inputSchema: { type: "object", properties: {
|
|
48
|
+
wait: { type: "boolean" },
|
|
49
|
+
path: { type: "string", description: "The chosen agent folder for the local git email suggestion. Defaults to the launch folder; does not change the analysis permission check." },
|
|
50
|
+
}, additionalProperties: false },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "sign_in",
|
|
54
|
+
description: `With email, send a Counterfact sign-in code only after the user confirms that address. Before calling, show: ${EMAIL_SIGN_IN_TERMS} Then ask the user for the code from their email and call sign_in_with_code. Without email, open the Counterfact sign-in page in the user's browser, show the returned link and code, then call sign_in_status with wait: true.`,
|
|
55
|
+
inputSchema: { type: "object", properties: {
|
|
56
|
+
email: { type: "string", description: "The email address confirmed by the user." },
|
|
57
|
+
}, additionalProperties: false },
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "sign_in_with_code",
|
|
61
|
+
description: "Complete Counterfact email sign-in with the confirmed email address and the code supplied by the user. Save the sign-in on this computer. Never repeat the code in messages, logs, or reports. Do not request another code automatically.",
|
|
62
|
+
inputSchema: { type: "object", properties: {
|
|
63
|
+
email: { type: "string", description: "The email address confirmed by the user." },
|
|
64
|
+
code: { type: "string", description: "The sign-in code supplied by the user from their email." },
|
|
65
|
+
}, required: ["email", "code"], additionalProperties: false },
|
|
66
|
+
},
|
|
67
|
+
] : legacyTools;
|
|
38
68
|
let pendingSignIn;
|
|
39
69
|
const requests = new Map();
|
|
40
70
|
let queue = Promise.resolve();
|
|
@@ -46,21 +76,35 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
46
76
|
if (!object(args))
|
|
47
77
|
throw new ProtocolError(-32602, "Tool arguments must be an object.");
|
|
48
78
|
const name = params.name;
|
|
49
|
-
const allowed = name ===
|
|
79
|
+
const allowed = name === ANALYZE_TOOL_NAME ? ["path"] : name === "get_finding" ? ["id"]
|
|
80
|
+
: name === "sign_in_status" ? (emailSignIn ? ["wait", "path"] : ["wait"])
|
|
81
|
+
: emailSignIn && name === "sign_in" ? ["email"] : emailSignIn && name === "sign_in_with_code" ? ["email", "code"] : [];
|
|
50
82
|
if (!tools.some(tool => tool.name === name) || Object.keys(args).some(key => !allowed.includes(key)))
|
|
51
83
|
throw new ProtocolError(-32602, "Unknown tool or argument.");
|
|
52
|
-
if (name ===
|
|
53
|
-
throw new ProtocolError(-32602, "Provide a path
|
|
84
|
+
if (name === ANALYZE_TOOL_NAME && (typeof args.path !== "string" || !args.path.trim()))
|
|
85
|
+
throw new ProtocolError(-32602, "Provide a path.");
|
|
54
86
|
if (name === "get_finding" && (typeof args.id !== "string" || !args.id.trim()))
|
|
55
87
|
throw new ProtocolError(-32602, "Provide a finding id.");
|
|
56
88
|
if (name === "sign_in_status" && args.wait !== undefined && typeof args.wait !== "boolean")
|
|
57
89
|
throw new ProtocolError(-32602, "Provide an optional boolean wait.");
|
|
90
|
+
if (name === "sign_in_status" && "path" in args && (typeof args.path !== "string" || !args.path.trim()))
|
|
91
|
+
throw new ProtocolError(-32602, "Provide a path.");
|
|
92
|
+
const emailFlow = name === "sign_in_with_code" || (name === "sign_in" && "email" in args);
|
|
93
|
+
const email = emailFlow ? normalizeSignInEmail(args.email) : undefined;
|
|
94
|
+
if (emailFlow && !email)
|
|
95
|
+
throw new ProtocolError(-32602, emailSignInMessages.invalid_email);
|
|
96
|
+
if (name === "sign_in_with_code" && !validSignInCode(args.code))
|
|
97
|
+
throw new ProtocolError(-32602, emailSignInMessages.invalid_code);
|
|
58
98
|
let text = "", exitCode = 0;
|
|
59
99
|
let findingIds;
|
|
60
100
|
let url;
|
|
61
101
|
let signIn;
|
|
62
102
|
try {
|
|
63
|
-
const ctx = commandContext({ ...options, signal,
|
|
103
|
+
const ctx = commandContext({ ...options, emailSignIn, signal, onCheckComplete: result => {
|
|
104
|
+
findingIds = result.findingIds ?? [];
|
|
105
|
+
url = result.url;
|
|
106
|
+
options.onCheckComplete?.(result);
|
|
107
|
+
}, write: chunk => {
|
|
64
108
|
// Progress is transient terminal text; only the final lines belong in a tool response.
|
|
65
109
|
if (!chunk.startsWith("\r"))
|
|
66
110
|
text += chunk;
|
|
@@ -71,7 +115,7 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
71
115
|
await balance(ctx);
|
|
72
116
|
else if (name === "sign_in_status") {
|
|
73
117
|
const pending = pendingSignIn;
|
|
74
|
-
const allowed = await
|
|
118
|
+
const allowed = await analyzeToolAllowed(ctx.homeDir, cwd);
|
|
75
119
|
if (args.wait === true && pending) {
|
|
76
120
|
const cancel = () => pending.controller.abort();
|
|
77
121
|
signal.addEventListener("abort", cancel, { once: true });
|
|
@@ -84,8 +128,27 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
84
128
|
signal.removeEventListener("abort", cancel);
|
|
85
129
|
}
|
|
86
130
|
}
|
|
87
|
-
signIn = { ...await signInStatus(ctx),
|
|
131
|
+
signIn = { ...await signInStatus(ctx), analyzeToolAllowed: allowed };
|
|
88
132
|
text = signIn.signedIn ? "Signed in." : "Not signed in.";
|
|
133
|
+
if (emailSignIn) {
|
|
134
|
+
const suggestion = await suggestedEmail(resolveAgentFolder(args.path ?? ".", cwd), ctx.readEmail);
|
|
135
|
+
if (suggestion)
|
|
136
|
+
signIn.suggestedEmail = suggestion;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (emailFlow) {
|
|
140
|
+
if (pendingSignIn)
|
|
141
|
+
throw new Error(emailSignInMessages.browser_sign_in_pending);
|
|
142
|
+
if (name === "sign_in") {
|
|
143
|
+
const start = await startEmailSignIn(ctx, email);
|
|
144
|
+
signIn = { email: email, expiresIn: start.expiresIn };
|
|
145
|
+
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.`;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
await signInWithCode(ctx, email, args.code);
|
|
149
|
+
signIn = { signedIn: true };
|
|
150
|
+
text = "Signed in.";
|
|
151
|
+
}
|
|
89
152
|
}
|
|
90
153
|
else if (name === "sign_in") {
|
|
91
154
|
if ((await signInStatus(ctx)).signedIn)
|
|
@@ -99,7 +162,7 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
99
162
|
signal.addEventListener("abort", cancel, { once: true });
|
|
100
163
|
try {
|
|
101
164
|
signal.throwIfAborted();
|
|
102
|
-
const flow = await startSignIn(commandContext({ ...options, homeDir: ctx.homeDir, baseUrl: ctx.api.baseUrl, signal: controller.signal }));
|
|
165
|
+
const flow = await startSignIn(commandContext({ ...options, emailSignIn, homeDir: ctx.homeDir, baseUrl: ctx.api.baseUrl, signal: controller.signal }));
|
|
103
166
|
pending = { flow, controller, opened: false };
|
|
104
167
|
pendingSignIn = pending;
|
|
105
168
|
const current = pending;
|
|
@@ -119,22 +182,26 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
119
182
|
}
|
|
120
183
|
}
|
|
121
184
|
else {
|
|
122
|
-
const
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const result = await confirmPrepared(prepared, ctx);
|
|
130
|
-
findingIds = result.findingIds;
|
|
131
|
-
url = result.taste.url;
|
|
185
|
+
const launchFolder = await launch;
|
|
186
|
+
if (guarded && !launchFolder)
|
|
187
|
+
throw new Error(START_FAILURE_MESSAGE);
|
|
188
|
+
const folder = guarded ? await resolveMcpAgentFolder(args.path, launchFolder, ctx.homeDir) : resolveAgentFolder(args.path, cwd);
|
|
189
|
+
const last = lastAnalysis.get(folder);
|
|
190
|
+
if (guarded && last !== undefined && ctx.now() - last < 10 * 60 * 1000) {
|
|
191
|
+
throw new Error(FINISH_FAILURE_MESSAGE);
|
|
132
192
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
193
|
+
let started = false;
|
|
194
|
+
try {
|
|
195
|
+
exitCode = await analyze(folder, {}, { ...ctx, onCheckStart: () => {
|
|
196
|
+
started = true;
|
|
197
|
+
if (guarded)
|
|
198
|
+
lastAnalysis.set(folder, ctx.now());
|
|
199
|
+
ctx.onCheckStart?.();
|
|
200
|
+
} });
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
if (guarded && started)
|
|
204
|
+
lastAnalysis.set(folder, ctx.now());
|
|
138
205
|
}
|
|
139
206
|
}
|
|
140
207
|
}
|
|
@@ -143,7 +210,7 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
143
210
|
exitCode = errorCode(error);
|
|
144
211
|
}
|
|
145
212
|
// Some clients (Claude Code among them) give the model only the structured result when one is present, so it
|
|
146
|
-
// repeats the text: without it the model
|
|
213
|
+
// repeats the text: without it the model cannot read the result.
|
|
147
214
|
return { content: [{ type: "text", text }], isError: exitCode !== 0,
|
|
148
215
|
structuredContent: { text, exitCode, ...(findingIds ? { findingIds } : {}), ...(url ? { url } : {}), ...signIn } };
|
|
149
216
|
}
|
|
@@ -156,7 +223,7 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
156
223
|
if (message.method === "initialize") {
|
|
157
224
|
const requested = object(message.params) ? message.params.protocolVersion : undefined;
|
|
158
225
|
result = { protocolVersion: ["2024-11-05", "2025-03-26", "2025-06-18"].includes(requested) ? requested : "2025-06-18",
|
|
159
|
-
capabilities: { tools: {} }, serverInfo: { name: "counterfact-labs", version } };
|
|
226
|
+
capabilities: { tools: {} }, serverInfo: { name: "counterfact-labs", version: CLI_VERSION } };
|
|
160
227
|
}
|
|
161
228
|
else if (message.method === "ping")
|
|
162
229
|
result = {};
|
|
@@ -172,6 +239,8 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
172
239
|
reply = errorReply(id, error);
|
|
173
240
|
}
|
|
174
241
|
// The caller stopped waiting; a late result must not look like a completed call.
|
|
242
|
+
// The output is an MCP stdio Writable carrying JSON-RPC, not an HTML HTTP response.
|
|
243
|
+
// nosemgrep: javascript.express.security.audit.xss.direct-response-write.direct-response-write
|
|
175
244
|
if (!signal.aborted && !output.write(JSON.stringify(reply) + "\n"))
|
|
176
245
|
await once(output, "drain");
|
|
177
246
|
}
|
|
@@ -202,7 +271,7 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
202
271
|
id = message.id;
|
|
203
272
|
const controller = new AbortController();
|
|
204
273
|
requests.set(id, controller);
|
|
205
|
-
// Keep
|
|
274
|
+
// Keep tool requests in order while still reading cancellation notifications.
|
|
206
275
|
const request = message;
|
|
207
276
|
queue = queue.then(() => respond(request, id, controller.signal)).finally(() => { requests.delete(id); });
|
|
208
277
|
continue;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function money(micro) {
|
|
2
|
+
const cents = Math.round(micro / 10_000);
|
|
3
|
+
const whole = Math.trunc(cents / 100);
|
|
4
|
+
const fraction = Math.abs(cents % 100);
|
|
5
|
+
return `${cents < 0 ? "-" : ""}$${Math.abs(whole)}${fraction ? `.${String(fraction).padStart(2, "0")}` : ""}`;
|
|
6
|
+
}
|
|
7
|
+
export function tasteText(taste, findingIds) {
|
|
8
|
+
return [taste.headline, ...taste.lines.map(line => ` ${line}`),
|
|
9
|
+
...(findingIds.length ? [`Finding ids: ${findingIds.join(", ")}`] : []), `Full results: ${taste.url}`, ""].join("\n");
|
|
10
|
+
}
|