@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
package/dist/api.js DELETED
@@ -1,97 +0,0 @@
1
- import { CLI_BODY_LIMIT } from "./api-types.js";
2
- export const DEFAULT_URL = "https://app.counterfactlabs.com";
3
- export class ApiError extends Error {
4
- status;
5
- code;
6
- retryAfterMs;
7
- constructor(status, code, message, retryAfterMs = 0) {
8
- super(message);
9
- this.status = status;
10
- this.code = code;
11
- this.retryAfterMs = retryAfterMs;
12
- }
13
- }
14
- function retryAfterMs(value) {
15
- if (!value)
16
- return 0;
17
- const seconds = Number(value);
18
- if (Number.isFinite(seconds) && seconds >= 0)
19
- return seconds * 1000;
20
- const date = Date.parse(value);
21
- return Number.isFinite(date) ? Math.max(0, date - Date.now()) : 0;
22
- }
23
- export class ApiClient {
24
- token;
25
- fetcher;
26
- signal;
27
- baseUrl;
28
- constructor(baseUrl, token, fetcher = fetch, signal) {
29
- this.token = token;
30
- this.fetcher = fetcher;
31
- this.signal = signal;
32
- const url = new URL(baseUrl);
33
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash)
34
- throw new Error("COUNTERFACT_URL must be an HTTP or HTTPS address without credentials or query fields.");
35
- this.baseUrl = url.href.replace(/\/+$/, "");
36
- }
37
- async request(path, body, authenticated = true) {
38
- this.signal?.throwIfAborted();
39
- const headers = { accept: "application/json" };
40
- if (authenticated) {
41
- const token = await this.token();
42
- if (!token)
43
- throw new Error("Not signed in. Run counterfact-labs login.");
44
- headers.authorization = `Bearer ${token}`;
45
- }
46
- if (body !== undefined)
47
- headers["content-type"] = "application/json";
48
- const serialized = body === undefined ? undefined : JSON.stringify(body);
49
- if (serialized !== undefined && Buffer.byteLength(serialized, "utf8") > CLI_BODY_LIMIT) {
50
- throw new ApiError(413, "body_too_large", "The upload is too large. Send at most 2 MB.");
51
- }
52
- const controller = new AbortController();
53
- const cancel = () => controller.abort(this.signal?.reason);
54
- const timeout = setTimeout(() => controller.abort(), 120_000);
55
- timeout.unref();
56
- this.signal?.addEventListener("abort", cancel, { once: true });
57
- try {
58
- this.signal?.throwIfAborted();
59
- let response;
60
- try {
61
- response = await this.fetcher(`${this.baseUrl}/api/cli/${path}`, {
62
- method: body === undefined ? "GET" : "POST", headers,
63
- body: serialized,
64
- redirect: "error", signal: controller.signal,
65
- });
66
- }
67
- catch {
68
- this.signal?.throwIfAborted();
69
- throw new ApiError(0, "unreachable", "Can't reach Counterfact. Try again shortly.");
70
- }
71
- let value;
72
- try {
73
- value = await response.json();
74
- }
75
- catch (error) {
76
- this.signal?.throwIfAborted();
77
- if (response.ok) {
78
- if (error instanceof SyntaxError)
79
- throw new Error("Counterfact returned an invalid response. Try again shortly.");
80
- throw new ApiError(0, "unreachable", "Can't reach Counterfact. Try again shortly.");
81
- }
82
- }
83
- this.signal?.throwIfAborted();
84
- if (!response.ok) {
85
- const error = value;
86
- throw new ApiError(response.status, error?.code ?? "request_failed", typeof error?.error === "string" ? error.error : "The request could not be completed. Try again shortly.", retryAfterMs(response.headers.get("retry-after")));
87
- }
88
- return value;
89
- }
90
- finally {
91
- clearTimeout(timeout);
92
- this.signal?.removeEventListener("abort", cancel);
93
- }
94
- }
95
- }
96
- export function errorCode(error) { return error instanceof ApiError && error.code === "low_balance" ? 2 : 1; }
97
- export function errorText(error) { return error instanceof Error ? error.message : "The request could not be completed. Try again shortly."; }
@@ -1,104 +0,0 @@
1
- import { resolve } from "node:path";
2
- import { realpath } from "node:fs/promises";
3
- import { listFiles, readUpload } from "../files.js";
4
- import { repositoryFacts } from "../git.js";
5
- import { fileText, quoteText, tasteText } from "../output.js";
6
- import { ApiError } from "../api.js";
7
- async function awaitQuote(response, ctx) {
8
- let progressShown = false;
9
- try {
10
- while (response.status === "running") {
11
- ctx.write(`\r\u001b[2K${response.progress}`);
12
- progressShown = true;
13
- await ctx.sleep(2000);
14
- response = await ctx.api.request(`checks/${encodeURIComponent(response.id)}`);
15
- }
16
- if (response.status === "failed" || (response.status === "awaiting_confirm" && "error" in response))
17
- throw new Error(response.error);
18
- if (response.status !== "awaiting_confirm")
19
- throw new Error("The check has already finished. Run the check again for a new quote.");
20
- return response;
21
- }
22
- finally {
23
- if (progressShown)
24
- ctx.write("\r\u001b[2K");
25
- }
26
- }
27
- export async function prepareCheck(path, ctx) {
28
- const root = await realpath(resolve(path));
29
- const { repoRoot, ...facts } = await repositoryFacts(root);
30
- const files = await listFiles(root, repoRoot);
31
- const plan = await ctx.api.request("checks/plan", { files });
32
- const selected = new Set(plan.wanted);
33
- const wanted = files.filter(file => selected.has(file.path));
34
- if (!wanted.length)
35
- throw new Error("No agent files were found in this folder.");
36
- ctx.write(fileText(path, wanted));
37
- const upload = await readUpload(root, files, plan.wanted);
38
- const created = await ctx.api.request("checks", { ...facts, files: upload });
39
- ctx.write(`Follow it in the app: ${created.url}\n`);
40
- const response = await awaitQuote(created, ctx);
41
- ctx.write(quoteText(response.quote));
42
- return { root, response, files: upload, url: created.url };
43
- }
44
- /** A confirmed check can keep running and charge while the connection is down. */
45
- async function pollConfirmed(id, ctx) {
46
- const started = ctx.now();
47
- let waited = 0, backoff = 2000;
48
- for (;;) {
49
- try {
50
- return await ctx.api.request(`checks/${id}`);
51
- }
52
- catch (error) {
53
- if (!(error instanceof ApiError) || !(error.status === 0 || error.status === 429 || error.status >= 500 && error.status < 600))
54
- throw error;
55
- const wait = Math.max(backoff, error.retryAfterMs);
56
- // Count both elapsed requests and scheduled waits, including injected clocks.
57
- if (Math.max(ctx.now() - started, waited) + wait >= 300_000) {
58
- throw new Error(`The check is still running. Its result will appear on the console's Checks page: ${ctx.api.baseUrl}/checks`);
59
- }
60
- await ctx.sleep(wait);
61
- waited += wait;
62
- backoff = Math.min(backoff * 2, 30_000);
63
- }
64
- }
65
- }
66
- export async function confirmPrepared(prepared, ctx) {
67
- // A preview's price belongs to those uploaded files. Edits need a fresh preview and agreement.
68
- const files = Object.entries(prepared.files).map(([path, text]) => ({ path, size: Buffer.byteLength(text, "utf8") }));
69
- const current = await readUpload(prepared.root, files, files.map(file => file.path));
70
- if (files.some(file => current[file.path] !== prepared.files[file.path]))
71
- throw new Error("The agent files changed. Get a new preview and agree to its price before confirming.");
72
- const id = encodeURIComponent(prepared.response.id);
73
- await ctx.api.request(`checks/${id}/confirm`, { quoteId: prepared.response.quote.quoteId });
74
- ctx.write("\r\u001b[2KChecking the agent…");
75
- try {
76
- for (;;) {
77
- await ctx.sleep(2000);
78
- const response = await pollConfirmed(id, ctx);
79
- if (response.status === "done") {
80
- ctx.write("\r\u001b[2K");
81
- ctx.write(tasteText(response.taste, response.findingIds));
82
- return response;
83
- }
84
- if (response.status === "failed" || (response.status === "awaiting_confirm" && "error" in response))
85
- throw new Error(response.error);
86
- if (response.status === "awaiting_confirm")
87
- throw new Error("The check needs approval again. Run the check again to see its price.");
88
- ctx.write(`\r\u001b[2K${response.progress}`);
89
- }
90
- }
91
- catch (error) {
92
- ctx.write("\r\u001b[2K");
93
- throw error;
94
- }
95
- }
96
- export async function check(path, options, ctx) {
97
- const prepared = await prepareCheck(path, ctx);
98
- if (options.dryRun)
99
- return 0;
100
- if (!options.yes && !await ctx.ask("Continue? [Y/n] "))
101
- return 0;
102
- await confirmPrepared(prepared, ctx);
103
- return 0;
104
- }
@@ -1,21 +0,0 @@
1
- import { saveCredentials } from "../credentials.js";
2
- export async function login(ctx) {
3
- const device = await ctx.api.request("device", {}, false);
4
- ctx.write(`1. Open ${device.verificationUri}. If asked, sign in with the code we email you.\n`
5
- + `2. Then enter this code on that page: ${device.userCode}\n`);
6
- const until = ctx.now() + device.expiresIn * 1000;
7
- while (ctx.now() < until) {
8
- await ctx.sleep(device.interval * 1000);
9
- const poll = await ctx.api.request("device/token", { deviceCode: device.deviceCode }, false);
10
- if (poll.status === "ok") {
11
- await saveCredentials(ctx.homeDir, ctx.api.baseUrl, poll.token);
12
- ctx.write("Signed in.\n");
13
- return 0;
14
- }
15
- if (poll.status === "expired")
16
- break;
17
- if (poll.status === "denied")
18
- throw new Error("Sign-in was cancelled. Run counterfact-labs login to try again.");
19
- }
20
- throw new Error("The sign-in code expired. Run counterfact-labs login to try again.");
21
- }
package/dist/context.js DELETED
@@ -1,27 +0,0 @@
1
- import { homedir } from "node:os";
2
- import { createInterface } from "node:readline/promises";
3
- import { setTimeout as delay } from "node:timers/promises";
4
- import { ApiClient, DEFAULT_URL } from "./api.js";
5
- import { readCredentials } from "./credentials.js";
6
- export function commandContext(options = {}) {
7
- const homeDir = options.homeDir ?? homedir();
8
- const baseUrl = (options.baseUrl ?? process.env.COUNTERFACT_URL ?? DEFAULT_URL).replace(/\/+$/, "");
9
- const api = new ApiClient(baseUrl, () => readCredentials(homeDir, api.baseUrl), undefined, options.signal);
10
- const write = options.write ?? (text => { process.stdout.write(text); });
11
- return {
12
- api, homeDir, write,
13
- sleep: options.sleep ?? (async (ms) => { await delay(ms, undefined, { signal: options.signal }); }), now: options.now ?? Date.now,
14
- ask: options.ask ?? (async (question) => {
15
- if (!process.stdin.isTTY)
16
- throw new Error("Use --yes to confirm a check, or --dry-run to see the files and price.");
17
- write(question);
18
- const input = createInterface({ input: process.stdin });
19
- try {
20
- return /^(?:y|yes)?$/i.test((await input.question("")).trim());
21
- }
22
- finally {
23
- input.close();
24
- }
25
- }),
26
- };
27
- }
package/dist/files.js DELETED
@@ -1,99 +0,0 @@
1
- import { constants } from "node:fs";
2
- import { lstat, open, readdir, realpath } from "node:fs/promises";
3
- import { dirname, join, relative, resolve, sep } from "node:path";
4
- import { ignored, parseIgnore } from "./gitignore.js";
5
- import { isReadableRepoPath } from "./secret-files.js";
6
- const slash = (path) => path.split(sep).join("/");
7
- function safe(path) {
8
- return !!path && !path.includes("\\") && !path.includes("\0") && !path.startsWith("/") && isReadableRepoPath(path)
9
- && path.split("/").every(part => !["", ".", "..", ".git", "node_modules"].includes(part));
10
- }
11
- async function ignoreFile(directory, base) {
12
- try {
13
- const file = await open(join(directory, ".gitignore"), constants.O_RDONLY | constants.O_NOFOLLOW);
14
- try {
15
- return parseIgnore(await file.readFile("utf8"), base);
16
- }
17
- finally {
18
- await file.close();
19
- }
20
- }
21
- catch (error) {
22
- if (["ENOENT", "ELOOP"].includes(error.code ?? ""))
23
- return [];
24
- throw error;
25
- }
26
- }
27
- export async function listFiles(root, repoRoot) {
28
- root = await realpath(root);
29
- const base = repoRoot ? await realpath(repoRoot) : root;
30
- const prefix = slash(relative(base, root));
31
- if (prefix.startsWith("../") || prefix === "..")
32
- throw new Error("The agent folder must be inside its repository.");
33
- let parentRules = [];
34
- if (prefix) {
35
- let directory = base;
36
- parentRules = await ignoreFile(directory, "");
37
- for (const part of prefix.split("/")) {
38
- directory = join(directory, part);
39
- if (ignored(slash(relative(base, directory)), true, parentRules))
40
- return [];
41
- if (directory !== root)
42
- parentRules.push(...await ignoreFile(directory, slash(relative(base, directory))));
43
- }
44
- }
45
- const files = [];
46
- async function walk(directory, inherited) {
47
- const scope = slash(relative(base, directory));
48
- const rules = [...inherited, ...await ignoreFile(directory, scope)];
49
- const entries = await readdir(directory, { withFileTypes: true });
50
- for (const entry of entries) {
51
- const absolute = join(directory, entry.name);
52
- const path = slash(relative(root, absolute));
53
- if (!safe(path) || entry.isSymbolicLink() || ignored(slash(relative(base, absolute)), entry.isDirectory(), rules))
54
- continue;
55
- if (entry.isDirectory())
56
- await walk(absolute, rules);
57
- else if (entry.isFile()) {
58
- const stat = await lstat(absolute);
59
- if (stat.isFile())
60
- files.push({ path, size: stat.size });
61
- }
62
- }
63
- }
64
- await walk(root, parentRules);
65
- return files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
66
- }
67
- export async function readUpload(root, files, wanted) {
68
- root = await realpath(root);
69
- const sizes = new Map(files.map(file => [file.path, file.size]));
70
- const uploaded = Object.create(null);
71
- for (const path of wanted) {
72
- if (!safe(path) || !sizes.has(path))
73
- throw new Error("Counterfact requested a file outside the local file list.");
74
- const absolute = resolve(root, path);
75
- // A directory replaced by a symlink after listing must not expose files outside the folder.
76
- if (await realpath(dirname(absolute)) !== dirname(absolute))
77
- throw new Error("A file changed since listing. Run the check again.");
78
- const file = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW);
79
- try {
80
- if (!(await file.stat()).isFile())
81
- throw new Error("Only regular files can be uploaded.");
82
- const bytes = await file.readFile();
83
- let text;
84
- try {
85
- text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
86
- }
87
- catch {
88
- throw new Error(`${path} is not UTF-8 text and cannot be uploaded.`);
89
- }
90
- if (Buffer.byteLength(text, "utf8") !== sizes.get(path))
91
- throw new Error("A file changed since listing. Run the check again.");
92
- uploaded[path] = text;
93
- }
94
- finally {
95
- await file.close();
96
- }
97
- }
98
- return { ...uploaded };
99
- }
package/dist/git.js DELETED
@@ -1,21 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { promisify } from "node:util";
3
- import { basename, relative, resolve, sep } from "node:path";
4
- export async function repositoryFacts(root, git = async (args) => (await promisify(execFile)("git", ["-C", root, ...args], { timeout: 3000, maxBuffer: 16_384 })).stdout) {
5
- const folderName = basename(resolve(root));
6
- let repoRoot;
7
- try {
8
- repoRoot = (await git(["rev-parse", "--show-toplevel"])).trim();
9
- }
10
- catch {
11
- return { remoteUrl: null, relPath: "", folderName };
12
- }
13
- let remoteUrl;
14
- try {
15
- remoteUrl = (await git(["config", "--get", "remote.origin.url"])).trim().replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/]*@/i, "$1") || null;
16
- }
17
- catch {
18
- remoteUrl = null;
19
- }
20
- return { repoRoot, remoteUrl, relPath: relative(repoRoot, root).split(sep).join("/"), folderName };
21
- }
package/dist/gitignore.js DELETED
@@ -1,70 +0,0 @@
1
- const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2
- function glob(pattern) {
3
- let result = "";
4
- for (let i = 0; i < pattern.length; i++) {
5
- const ch = pattern[i];
6
- if (ch === "\\" && i + 1 < pattern.length)
7
- result += escape(pattern[++i]);
8
- else if (ch === "*" && pattern[i + 1] === "*") {
9
- i++;
10
- if (pattern[i + 1] === "/") {
11
- result += "(?:.*/)?";
12
- i++;
13
- }
14
- else
15
- result += ".*";
16
- }
17
- else if (ch === "*")
18
- result += "[^/]*";
19
- else if (ch === "?")
20
- result += "[^/]";
21
- else if (ch === "[") {
22
- const end = pattern.indexOf("]", i + 1);
23
- if (end < 0)
24
- result += "\\[";
25
- else {
26
- const chars = pattern.slice(i + 1, end).replace(/^!/, "^");
27
- result += `[${chars}]`;
28
- i = end;
29
- }
30
- }
31
- else
32
- result += escape(ch);
33
- }
34
- return result;
35
- }
36
- export function parseIgnore(text, base) {
37
- const rules = [];
38
- for (let line of text.split(/\r?\n/)) {
39
- line = line.replace(/(?<!\\) +$/, "");
40
- if (!line || line.startsWith("#"))
41
- continue;
42
- const negate = line.startsWith("!");
43
- if (negate)
44
- line = line.slice(1);
45
- const directory = line.endsWith("/");
46
- if (directory)
47
- line = line.slice(0, -1);
48
- const anchored = line.startsWith("/") || line.includes("/");
49
- if (line.startsWith("/"))
50
- line = line.slice(1);
51
- if (!line)
52
- continue;
53
- try {
54
- rules.push({ base, directory, negate, regex: new RegExp(`${anchored ? "^" : "(?:^|/)"}${glob(line)}$`) });
55
- }
56
- catch {
57
- throw new Error("A .gitignore pattern could not be read.");
58
- }
59
- }
60
- return rules;
61
- }
62
- export function ignored(path, directory, rules) {
63
- let excluded = false;
64
- for (const rule of rules) {
65
- const local = rule.base ? path.startsWith(`${rule.base}/`) ? path.slice(rule.base.length + 1) : null : path;
66
- if (local !== null && (!rule.directory || directory) && rule.regex.test(local))
67
- excluded = !rule.negate;
68
- }
69
- return excluded;
70
- }
package/dist/mcp.js DELETED
@@ -1,165 +0,0 @@
1
- import { createInterface } from "node:readline";
2
- import { once } from "node:events";
3
- import { readFile } from "node:fs/promises";
4
- import { resolve } from "node:path";
5
- import { errorCode, errorText } from "./api.js";
6
- import { commandContext } from "./context.js";
7
- import { confirmPrepared, prepareCheck } from "./commands/check.js";
8
- import { finding } from "./commands/finding.js";
9
- import { balance } from "./commands/balance.js";
10
- const tools = [
11
- {
12
- name: "check_agent",
13
- description: "Preview the files and price of an agent check. Show the user the price and get their agreement "
14
- + "before calling with confirm: true. A confirmation uses the most recent preview for that path in this session. "
15
- + "Suggest a check after edits; wait for the user to ask for it. If the tool call is cancelled or times out, "
16
- + "a confirmed check keeps running on the server and its result will be in the console's Checks page.",
17
- inputSchema: { type: "object", properties: { path: { type: "string", description: "The agent folder on this computer." }, confirm: { type: "boolean", default: false } }, required: ["path"], additionalProperties: false },
18
- },
19
- { 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 } },
20
- { name: "get_balance", description: "Get the balance and top-up link.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
21
- ];
22
- class ProtocolError extends Error {
23
- code;
24
- constructor(code, message) {
25
- super(message);
26
- this.code = code;
27
- }
28
- }
29
- function object(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
30
- export async function serveMcp(input, output, options = {}) {
31
- const { version } = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
32
- const previews = new Map();
33
- const requests = new Map();
34
- let queue = Promise.resolve();
35
- const lines = createInterface({ input, crlfDelay: Infinity });
36
- async function toolCall(params, signal) {
37
- if (!object(params) || typeof params.name !== "string")
38
- throw new ProtocolError(-32602, "Choose a tool and its arguments.");
39
- const args = params.arguments ?? {};
40
- if (!object(args))
41
- throw new ProtocolError(-32602, "Tool arguments must be an object.");
42
- const name = params.name;
43
- const allowed = name === "check_agent" ? ["path", "confirm"] : name === "get_finding" ? ["id"] : [];
44
- if (!tools.some(tool => tool.name === name) || Object.keys(args).some(key => !allowed.includes(key)))
45
- throw new ProtocolError(-32602, "Unknown tool or argument.");
46
- if (name === "check_agent" && (typeof args.path !== "string" || !args.path.trim() || (args.confirm !== undefined && typeof args.confirm !== "boolean")))
47
- throw new ProtocolError(-32602, "Provide a path and an optional boolean confirmation.");
48
- if (name === "get_finding" && (typeof args.id !== "string" || !args.id.trim()))
49
- throw new ProtocolError(-32602, "Provide a finding id.");
50
- let text = "", exitCode = 0;
51
- let findingIds;
52
- let url;
53
- try {
54
- const ctx = commandContext({ ...options, signal, write: chunk => {
55
- // Progress is transient terminal text; only the final lines belong in a tool response.
56
- if (!chunk.startsWith("\r"))
57
- text += chunk;
58
- } });
59
- if (name === "get_finding")
60
- await finding(args.id, ctx);
61
- else if (name === "get_balance")
62
- await balance(ctx);
63
- else {
64
- const path = resolve(args.path);
65
- if (args.confirm === true) {
66
- const prepared = previews.get(path);
67
- previews.delete(path);
68
- if (!prepared)
69
- throw new Error("Get a preview first. Show the user the price and get their agreement before confirming.");
70
- url = prepared.url;
71
- const result = await confirmPrepared(prepared, ctx);
72
- findingIds = result.findingIds;
73
- url = result.taste.url;
74
- }
75
- else {
76
- previews.delete(path);
77
- const prepared = await prepareCheck(args.path, ctx);
78
- previews.set(path, prepared);
79
- url = prepared.url;
80
- }
81
- }
82
- }
83
- catch (error) {
84
- text += errorText(error) + "\n";
85
- exitCode = errorCode(error);
86
- }
87
- // Some clients (Claude Code among them) give the model only the structured result when one is present, so it
88
- // repeats the text: without it the model sees no files, price or results.
89
- return { content: [{ type: "text", text }], isError: exitCode !== 0,
90
- structuredContent: { text, exitCode, ...(findingIds ? { findingIds } : {}), ...(url ? { url } : {}) } };
91
- }
92
- async function respond(message, id, signal) {
93
- if (signal.aborted)
94
- return;
95
- let reply;
96
- try {
97
- let result;
98
- if (message.method === "initialize") {
99
- const requested = object(message.params) ? message.params.protocolVersion : undefined;
100
- result = { protocolVersion: ["2024-11-05", "2025-03-26", "2025-06-18"].includes(requested) ? requested : "2025-06-18",
101
- capabilities: { tools: {} }, serverInfo: { name: "counterfact-labs", version } };
102
- }
103
- else if (message.method === "ping")
104
- result = {};
105
- else if (message.method === "tools/list")
106
- result = { tools };
107
- else if (message.method === "tools/call")
108
- result = await toolCall(message.params, signal);
109
- else
110
- throw new ProtocolError(-32601, "Method not found.");
111
- reply = { jsonrpc: "2.0", id, result };
112
- }
113
- catch (error) {
114
- reply = errorReply(id, error);
115
- }
116
- // The caller stopped waiting; a late result must not look like a completed call.
117
- if (!signal.aborted && !output.write(JSON.stringify(reply) + "\n"))
118
- await once(output, "drain");
119
- }
120
- for await (const line of lines) {
121
- if (!line.trim())
122
- continue;
123
- let id = null;
124
- let reply;
125
- try {
126
- let message;
127
- try {
128
- message = JSON.parse(line);
129
- }
130
- catch {
131
- throw new ProtocolError(-32700, "Invalid JSON.");
132
- }
133
- if (!object(message) || message.jsonrpc !== "2.0" || typeof message.method !== "string"
134
- || (message.id !== undefined && typeof message.id !== "string" && typeof message.id !== "number" && message.id !== null))
135
- throw new ProtocolError(-32600, "Invalid request.");
136
- if (message.id === undefined) {
137
- if (message.method === "notifications/cancelled" && object(message.params)) {
138
- const requestId = message.params.requestId;
139
- if (typeof requestId === "string" || typeof requestId === "number")
140
- requests.get(requestId)?.abort();
141
- }
142
- continue;
143
- }
144
- id = message.id;
145
- const controller = new AbortController();
146
- requests.set(id, controller);
147
- // Keep preview and confirm in order while still reading cancellation notifications.
148
- const request = message;
149
- queue = queue.then(() => respond(request, id, controller.signal)).finally(() => { requests.delete(id); });
150
- continue;
151
- }
152
- catch (error) {
153
- reply = errorReply(id, error);
154
- }
155
- if (!output.write(JSON.stringify(reply) + "\n"))
156
- await once(output, "drain");
157
- }
158
- await queue;
159
- }
160
- function errorReply(id, error) {
161
- return { jsonrpc: "2.0", id, error: {
162
- code: error instanceof ProtocolError ? error.code : -32603,
163
- message: error instanceof ProtocolError ? error.message : "The request could not be completed.",
164
- } };
165
- }
package/dist/output.js DELETED
@@ -1,23 +0,0 @@
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 fileText(path, files) {
8
- const bytes = files.reduce((sum, file) => sum + file.size, 0);
9
- const names = files.slice(0, 3).map(file => file.path).join(", ");
10
- const more = files.length > 3 ? `, ... ${files.length - 3} more` : "";
11
- return `Checking the agent in ${path} (${files.length} files, ${Math.ceil(bytes / 1000)} KB):\n ${names}${more}\n`;
12
- }
13
- export function quoteText(quote) {
14
- if (quote.band === null)
15
- return quote.message + "\n";
16
- if (quote.free)
17
- return "This check is free (your first one).\n";
18
- return `${quote.message ? quote.message + "\n" : ""}This check costs ${money(quote.priceMicro)} (${quote.band} agent).${quote.balanceMicro === null ? "" : ` Balance: ${money(quote.balanceMicro)}.`}\n`;
19
- }
20
- export function tasteText(taste, findingIds) {
21
- return [taste.headline, ...taste.lines.map(line => ` ${line}`),
22
- ...(findingIds.length ? [`Finding ids: ${findingIds.join(", ")}`] : []), `Full results: ${taste.url}`, ""].join("\n");
23
- }
@@ -1,5 +0,0 @@
1
- // Copied from src/lib/conventions/file-admission.ts by the build.
2
- export const SECRET_FILE_RE = /(^|\/)(\.env(?:\.[^/]*)?|\.(?:npmrc|pypirc|netrc)|(?:credentials|secrets)\.(?:json|ya?ml)|secrets\.toml|[^/]+-credentials\.json|service-account[^/]*\.json|id_(?:rsa|dsa|ecdsa|ed25519)|[^/]+\.(?:pem|key|p12|pfx))$/i;
3
- export function isReadableRepoPath(path) {
4
- return !SECRET_FILE_RE.test(path.replace(/\\/g, "/"));
5
- }
File without changes
File without changes
File without changes
File without changes