@counterfact-labs/cli 0.1.2 → 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.
Files changed (184) hide show
  1. package/README.md +178 -38
  2. package/bundle/client-release.json +7 -0
  3. package/bundle/sealed.json +908 -0
  4. package/dist/adapters/anthropic.js +151 -0
  5. package/dist/adapters/completion.js +112 -0
  6. package/dist/adapters/durable-write.js +34 -0
  7. package/dist/adapters/files.js +140 -0
  8. package/dist/adapters/folder-path.js +6 -0
  9. package/dist/adapters/git-ignore.js +49 -0
  10. package/dist/adapters/git.js +44 -0
  11. package/dist/adapters/ignored-files.js +36 -0
  12. package/dist/adapters/instructions.js +35 -0
  13. package/dist/adapters/models.js +51 -0
  14. package/dist/adapters/output-paths.js +92 -0
  15. package/dist/adapters/prompt-cache.js +28 -0
  16. package/dist/adapters/public-instructions.js +36 -0
  17. package/dist/adapters/quality.js +6 -0
  18. package/dist/adapters/relay-plan.js +15 -0
  19. package/dist/adapters/release.js +57 -0
  20. package/dist/adapters/report.js +29 -0
  21. package/dist/adapters/repository-files.js +199 -0
  22. package/dist/adapters/repository-root.js +120 -0
  23. package/dist/adapters/results.js +9 -0
  24. package/dist/adapters/settings.js +25 -0
  25. package/dist/adapters/snapshot.js +68 -0
  26. package/dist/analysis-core/analysis/analysis-flags.js +1 -0
  27. package/dist/analysis-core/analysis/code-agents/skipped-files.js +1 -0
  28. package/dist/analysis-core/analysis/finding-quality/config.js +1 -0
  29. package/dist/analysis-core/analysis/finding-quality/policy.js +1 -0
  30. package/dist/analysis-core/analysis/headlines/attachment.js +1 -0
  31. package/dist/analysis-core/analysis/headlines/input.js +1 -0
  32. package/dist/analysis-core/analysis/headlines/types.js +1 -0
  33. package/dist/analysis-core/analysis/schemas.js +1 -0
  34. package/dist/analysis-core/analysis/source-notes/types.js +1 -0
  35. package/dist/analysis-core/chunks/2PSNOSJ2.js +1 -0
  36. package/dist/analysis-core/chunks/3227KXHF.js +1 -0
  37. package/dist/analysis-core/chunks/326F4XNG.js +1 -0
  38. package/dist/analysis-core/chunks/3MTXB4MK.js +6 -0
  39. package/dist/analysis-core/chunks/4F6ZIHXU.js +1 -0
  40. package/dist/analysis-core/chunks/57M4OQJM.js +1 -0
  41. package/dist/analysis-core/chunks/6YCABS5T.js +1 -0
  42. package/dist/analysis-core/chunks/724WZMYW.js +2 -0
  43. package/dist/analysis-core/chunks/74FPPPIG.js +1 -0
  44. package/dist/analysis-core/chunks/7QDOHWSY.js +27 -0
  45. package/dist/analysis-core/chunks/BZNHHLNP.js +1 -0
  46. package/dist/analysis-core/chunks/DTMHXKP6.js +7 -0
  47. package/dist/analysis-core/chunks/E7TFIHC5.js +1 -0
  48. package/dist/analysis-core/chunks/EH3QQ3T4.js +1 -0
  49. package/dist/analysis-core/chunks/EQTIRIMU.js +37 -0
  50. package/dist/analysis-core/chunks/FSLWD2PE.js +1 -0
  51. package/dist/analysis-core/chunks/G322LST6.js +1 -0
  52. package/dist/analysis-core/chunks/HTLIRVP7.js +1 -0
  53. package/dist/analysis-core/chunks/IBCFGUHI.js +1 -0
  54. package/dist/analysis-core/chunks/IL2KRP3T.js +1 -0
  55. package/dist/analysis-core/chunks/J4AXGE5W.js +1 -0
  56. package/dist/analysis-core/chunks/JF4GYMOZ.js +1 -0
  57. package/dist/analysis-core/chunks/KEKTZDIK.js +1 -0
  58. package/dist/analysis-core/chunks/KU5DINLW.js +1 -0
  59. package/dist/analysis-core/chunks/M7MMAUAK.js +1 -0
  60. package/dist/analysis-core/chunks/OFBJNHC6.js +1 -0
  61. package/dist/analysis-core/chunks/ORCYIIHB.js +60 -0
  62. package/dist/analysis-core/chunks/PVPEY7NF.js +1 -0
  63. package/dist/analysis-core/chunks/PYKAERZZ.js +1 -0
  64. package/dist/analysis-core/chunks/QDYMR6KS.js +1 -0
  65. package/dist/analysis-core/chunks/QPXNXMLC.js +1 -0
  66. package/dist/analysis-core/chunks/SL27GMEN.js +1 -0
  67. package/dist/analysis-core/chunks/STZOC7LN.js +1 -0
  68. package/dist/analysis-core/chunks/T5TREZCQ.js +1 -0
  69. package/dist/analysis-core/chunks/TDNTU4TF.js +1 -0
  70. package/dist/analysis-core/chunks/U5HOEVPE.js +1 -0
  71. package/dist/analysis-core/chunks/UEWL3GMQ.js +1 -0
  72. package/dist/analysis-core/chunks/VER355VN.js +1 -0
  73. package/dist/analysis-core/chunks/VFENG4MR.js +1 -0
  74. package/dist/analysis-core/chunks/VGYWI4X2.js +1 -0
  75. package/dist/analysis-core/chunks/Y5AUKKYG.js +1 -0
  76. package/dist/analysis-core/chunks/Y7JGSUOA.js +1 -0
  77. package/dist/analysis-core/chunks/YVDCJN7D.js +14 -0
  78. package/dist/analysis-core/chunks/ZAA44GPI.js +1 -0
  79. package/dist/analysis-core/chunks/ZMUBMYWN.js +1 -0
  80. package/dist/analysis-core/contracts/requests.js +1 -0
  81. package/dist/analysis-core/conventions/file-admission.js +1 -0
  82. package/dist/analysis-core/data/finding-types.js +1 -0
  83. package/dist/analysis-core/errors.js +1 -0
  84. package/dist/analysis-core/findings/presentation.js +1 -0
  85. package/dist/analysis-core/github/repo-key.js +1 -0
  86. package/dist/analysis-core/import/outside-loader-sources.js +1 -0
  87. package/dist/analysis-core/index.js +6 -0
  88. package/dist/analysis-core/llm/cache-policy.js +1 -0
  89. package/dist/analysis-core/llm/cost.js +1 -0
  90. package/dist/analysis-core/llm/execution.js +1 -0
  91. package/dist/analysis-core/llm/output-limit.js +1 -0
  92. package/dist/analysis-core/llm/slots.js +1 -0
  93. package/dist/analysis-core/llm/stable-json.js +1 -0
  94. package/dist/analysis-core/llm/structured-output-error.js +1 -0
  95. package/dist/analysis-core/map-limit.js +1 -0
  96. package/dist/analysis-core/requests/answer-formats.js +1 -0
  97. package/dist/analysis-core/requests/check-diagnostics.js +1 -0
  98. package/dist/analysis-core/requests/headline-request.js +1 -0
  99. package/dist/analysis-core/requests/instruction-text.js +1 -0
  100. package/dist/analysis-core/requests/local-plan.js +1 -0
  101. package/dist/analysis-core/requests/registry.js +1 -0
  102. package/dist/analysis-core/requests/schema-descriptions.js +1 -0
  103. package/dist/analysis-core/requests/shape-finding-input.js +1 -0
  104. package/dist/analysis-core/requests/template-version.js +1 -0
  105. package/dist/analysis-core/requests/tools.js +1 -0
  106. package/dist/analysis-core/settings.js +1 -0
  107. package/dist/bundle/seal/deserialise.js +75 -0
  108. package/dist/bundle/seal/encryption.js +25 -0
  109. package/dist/bundle/seal/metadata.js +69 -0
  110. package/dist/bundle/seal/open.js +38 -0
  111. package/dist/bundle/seal/planned-calls.js +20 -0
  112. package/dist/bundle/seal/signature.js +51 -0
  113. package/dist/cli/agent-folder.js +17 -0
  114. package/dist/cli/api-types.js +2 -0
  115. package/dist/cli/api.js +1 -0
  116. package/dist/cli/browser.js +23 -0
  117. package/dist/cli/claude-code-permission.js +20 -0
  118. package/dist/cli/commands/analyze.js +73 -0
  119. package/dist/cli/commands/login.js +51 -0
  120. package/dist/cli/completion-recovery.js +18 -0
  121. package/dist/cli/context.js +22 -0
  122. package/dist/cli/email-sign-in.js +56 -0
  123. package/dist/cli/git.js +39 -0
  124. package/dist/cli/intake-payload.js +18 -0
  125. package/dist/{main.js → cli/main.js} +23 -12
  126. package/dist/cli/mcp.js +297 -0
  127. package/dist/cli/output.js +10 -0
  128. package/dist/cli/relay-check.js +239 -0
  129. package/dist/cli/relay-preparation.js +31 -0
  130. package/dist/cli/sign-in-input.js +97 -0
  131. package/dist/cli/sign-in.js +37 -0
  132. package/dist/cli/suggested-email.js +10 -0
  133. package/dist/cli/summary.js +30 -0
  134. package/dist/contract/cli-api-messages.js +5 -0
  135. package/dist/contract/cli-refusals.js +22 -0
  136. package/dist/contract/cli-version.js +1 -0
  137. package/dist/contract/console-url.js +8 -0
  138. package/dist/contract/console.js +111 -0
  139. package/dist/contract/email-sign-in-messages.js +18 -0
  140. package/dist/contract/messages.js +252 -0
  141. package/dist/contract/release-key.js +2 -0
  142. package/dist/contract/runtime-environment.js +37 -0
  143. package/dist/core/analysis/finding-quality/config.js +4 -0
  144. package/dist/core/analysis/run-to-end.js +55 -0
  145. package/dist/core/analysis/structural-run.js +16 -0
  146. package/dist/gatekeeper/attestation.js +67 -0
  147. package/dist/gatekeeper/certificate-algorithms.js +18 -0
  148. package/dist/gatekeeper/certificate-path.js +99 -0
  149. package/dist/gatekeeper/cli-api.js +174 -0
  150. package/dist/gatekeeper/client.js +76 -0
  151. package/dist/gatekeeper/connection-exchange.js +60 -0
  152. package/dist/gatekeeper/connection.js +140 -0
  153. package/dist/gatekeeper/console.js +168 -0
  154. package/dist/gatekeeper/loopback.js +5 -0
  155. package/dist/gatekeeper/model.js +70 -0
  156. package/dist/gatekeeper/production-attestation.js +149 -0
  157. package/dist/gatekeeper/trust/hardware.js +21 -0
  158. package/dist/gatekeeper/trust/policy.js +42 -0
  159. package/dist/gatekeeper/trust/production.json +25 -0
  160. package/dist/setup-pings/client.js +31 -0
  161. package/dist/setup-pings/contract.js +2 -0
  162. package/dist/setup-pings/policy.js +3 -0
  163. package/dist/setup-pings/state.js +261 -0
  164. package/dist/setup-pings/worker.js +95 -0
  165. package/dist/transport/base64url.js +9 -0
  166. package/dist/transport/http-response.js +89 -0
  167. package/dist/transport/tls-exporter.js +5 -0
  168. package/package.json +57 -11
  169. package/LICENSE +0 -1
  170. package/dist/api-types.js +0 -2
  171. package/dist/api.js +0 -97
  172. package/dist/commands/check.js +0 -104
  173. package/dist/commands/login.js +0 -21
  174. package/dist/context.js +0 -27
  175. package/dist/files.js +0 -99
  176. package/dist/git.js +0 -21
  177. package/dist/gitignore.js +0 -70
  178. package/dist/mcp.js +0 -165
  179. package/dist/output.js +0 -23
  180. package/dist/secret-files.js +0 -5
  181. /package/dist/{commands → cli/commands}/balance.js +0 -0
  182. /package/dist/{commands → cli/commands}/finding.js +0 -0
  183. /package/dist/{commands → cli/commands}/logout.js +0 -0
  184. /package/dist/{credentials.js → cli/credentials.js} +0 -0
@@ -0,0 +1,20 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ const servers = ["mcp__plugin_counterfact_counterfact", "mcp__counterfact"];
4
+ export const ANALYZE_TOOL_NAME = "analyze_agent";
5
+ export const ANALYZE_TOOL_RULE = `${servers[0]}__${ANALYZE_TOOL_NAME}`;
6
+ const allowedRules = new Set(servers.flatMap(server => [server, `${server}__*`, `${server}__${ANALYZE_TOOL_NAME}`]));
7
+ export async function analyzeToolAllowed(homeDir, cwd) {
8
+ const paths = [join(homeDir, ".claude/settings.json"), join(cwd, ".claude/settings.json"), join(cwd, ".claude/settings.local.json")];
9
+ const matches = await Promise.all(paths.map(async (path) => {
10
+ try {
11
+ const settings = JSON.parse(await readFile(path, "utf8"));
12
+ const allow = settings?.permissions?.allow;
13
+ return Array.isArray(allow) && allow.some(rule => allowedRules.has(rule));
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }));
19
+ return matches.some(Boolean);
20
+ }
@@ -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
+ }
@@ -0,0 +1,22 @@
1
+ import { readCliSettings } from "../adapters/settings.js";
2
+ import { homedir } from "node:os";
3
+ import { setTimeout as delay } from "node:timers/promises";
4
+ import { ApiClient, DEFAULT_URL } from "./api.js";
5
+ import { openBrowser } from "./browser.js";
6
+ import { readCredentials } from "./credentials.js";
7
+ import { userEmail } from "./git.js";
8
+ import { terminalSignInInput } from "./sign-in-input.js";
9
+ export function commandContext(options = {}) {
10
+ const startup = readCliSettings(options.env, options.emailSignIn);
11
+ const homeDir = options.homeDir ?? homedir();
12
+ const baseUrl = (options.baseUrl ?? startup.baseUrl ?? DEFAULT_URL).replace(/\/+$/, "");
13
+ const api = new ApiClient(baseUrl, () => readCredentials(homeDir, api.baseUrl), undefined, options.signal);
14
+ const write = options.write ?? (text => { process.stdout.write(text); });
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,
18
+ api, homeDir, write, openBrowser: options.openBrowser ?? openBrowser,
19
+ signInInput: options.signInInput ?? terminalSignInInput(), readEmail: options.readEmail ?? userEmail,
20
+ sleep: options.sleep ?? (async (ms) => { await delay(ms, undefined, { signal: options.signal }); }), now: options.now ?? Date.now,
21
+ };
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
+ }
@@ -0,0 +1,39 @@
1
+ import { basename, relative, resolve, sep } from "node:path";
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")) {
5
+ try {
6
+ return await git(["config", "--get", "user.email"]);
7
+ }
8
+ catch {
9
+ return undefined;
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 };
17
+ let remoteUrl;
18
+ try {
19
+ remoteUrl = (await git(["config", "--get", "remote.origin.url"])).trim().replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/]*@/i, "$1") || null;
20
+ }
21
+ catch {
22
+ remoteUrl = null;
23
+ }
24
+ const read = async (args) => {
25
+ try {
26
+ return (await git(args)).trim();
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ };
32
+ const [branch, commit, status] = await Promise.all([
33
+ read(["rev-parse", "--abbrev-ref", "HEAD"]), read(["rev-parse", "HEAD"]),
34
+ // Include new files even when Git hides them, and exclude sibling folders.
35
+ read(["status", "--porcelain=v2", "--no-renames", "--ignore-submodules=all", "--untracked-files=normal", "--", "."]),
36
+ ]);
37
+ return { repoRoot, remoteUrl, relPath: relative(repoRoot, root).split(sep).join("/"), folderName,
38
+ branch: branch === "HEAD" ? null : branch || null, commit: commit || null, dirty: status === null ? null : status.length > 0 };
39
+ }
@@ -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 { check } from "./commands/check.js";
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 check [path] [--yes] [--dry-run]\n counterfact-labs finding <id>\n counterfact-labs balance | login | logout | mcp\n";
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 === "check") {
22
- const flags = rest.filter(arg => arg.startsWith("--"));
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 check(paths[0] ?? ".", { yes: flags.includes("--yes"), dryRun: flags.includes("--dry-run") }, commandContext(options));
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(options));
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(options));
44
+ return await balance(commandContext(commandOptions));
34
45
  if (command === "login")
35
- return await login(commandContext(options));
46
+ return await login(commandContext(commandOptions));
36
47
  if (command === "logout")
37
- return await logout(commandContext(options));
48
+ return await logout(commandContext(commandOptions));
38
49
  if (command === "mcp") {
39
50
  await serveMcp(process.stdin, process.stdout, options);
40
51
  return 0;