@counterfact-labs/cli 0.1.3 → 0.2.4

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 (186) hide show
  1. package/README.md +176 -42
  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 +144 -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 +62 -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 +26 -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/3CKW5JIF.js +1 -0
  39. package/dist/analysis-core/chunks/3GUKUEEJ.js +1 -0
  40. package/dist/analysis-core/chunks/3MTXB4MK.js +6 -0
  41. package/dist/analysis-core/chunks/4F6ZIHXU.js +1 -0
  42. package/dist/analysis-core/chunks/57M4OQJM.js +1 -0
  43. package/dist/analysis-core/chunks/6YCABS5T.js +1 -0
  44. package/dist/analysis-core/chunks/724WZMYW.js +2 -0
  45. package/dist/analysis-core/chunks/74FPPPIG.js +1 -0
  46. package/dist/analysis-core/chunks/7OYIBWX3.js +1 -0
  47. package/dist/analysis-core/chunks/D6AWHLRI.js +1 -0
  48. package/dist/analysis-core/chunks/DRGDIIFA.js +1 -0
  49. package/dist/analysis-core/chunks/E7TFIHC5.js +1 -0
  50. package/dist/analysis-core/chunks/EH3QQ3T4.js +1 -0
  51. package/dist/analysis-core/chunks/FSLWD2PE.js +1 -0
  52. package/dist/analysis-core/chunks/FTODBFQW.js +1 -0
  53. package/dist/analysis-core/chunks/HPIAEQHE.js +37 -0
  54. package/dist/analysis-core/chunks/HTLIRVP7.js +1 -0
  55. package/dist/analysis-core/chunks/IBCFGUHI.js +1 -0
  56. package/dist/analysis-core/chunks/IVDDV6CU.js +14 -0
  57. package/dist/analysis-core/chunks/J4AXGE5W.js +1 -0
  58. package/dist/analysis-core/chunks/JF4GYMOZ.js +1 -0
  59. package/dist/analysis-core/chunks/JKWTHRUR.js +1 -0
  60. package/dist/analysis-core/chunks/K5OTWBTT.js +22 -0
  61. package/dist/analysis-core/chunks/KU5DINLW.js +1 -0
  62. package/dist/analysis-core/chunks/MJGB25S5.js +1 -0
  63. package/dist/analysis-core/chunks/MXXNED6N.js +7 -0
  64. package/dist/analysis-core/chunks/NCWEOJVJ.js +6 -0
  65. package/dist/analysis-core/chunks/O652FQQH.js +1 -0
  66. package/dist/analysis-core/chunks/OFBJNHC6.js +1 -0
  67. package/dist/analysis-core/chunks/PYKAERZZ.js +1 -0
  68. package/dist/analysis-core/chunks/QDYMR6KS.js +1 -0
  69. package/dist/analysis-core/chunks/QMIXCT62.js +1 -0
  70. package/dist/analysis-core/chunks/QV2EOS7U.js +1 -0
  71. package/dist/analysis-core/chunks/SL27GMEN.js +1 -0
  72. package/dist/analysis-core/chunks/STZOC7LN.js +1 -0
  73. package/dist/analysis-core/chunks/T2SAP7OV.js +60 -0
  74. package/dist/analysis-core/chunks/T5TREZCQ.js +1 -0
  75. package/dist/analysis-core/chunks/T66VDFXF.js +1 -0
  76. package/dist/analysis-core/chunks/TDNTU4TF.js +1 -0
  77. package/dist/analysis-core/chunks/U5HOEVPE.js +1 -0
  78. package/dist/analysis-core/chunks/UEWL3GMQ.js +1 -0
  79. package/dist/analysis-core/chunks/V7BFUTM7.js +1 -0
  80. package/dist/analysis-core/chunks/VGYWI4X2.js +1 -0
  81. package/dist/analysis-core/chunks/Y7JGSUOA.js +1 -0
  82. package/dist/analysis-core/chunks/ZAA44GPI.js +1 -0
  83. package/dist/analysis-core/contracts/requests.js +1 -0
  84. package/dist/analysis-core/conventions/analysis-fit.js +1 -0
  85. package/dist/analysis-core/conventions/file-admission.js +1 -0
  86. package/dist/analysis-core/data/finding-types.js +1 -0
  87. package/dist/analysis-core/errors.js +1 -0
  88. package/dist/analysis-core/findings/presentation.js +1 -0
  89. package/dist/analysis-core/github/repo-key.js +1 -0
  90. package/dist/analysis-core/import/outside-loader-sources.js +1 -0
  91. package/dist/analysis-core/index.js +6 -0
  92. package/dist/analysis-core/llm/cache-policy.js +1 -0
  93. package/dist/analysis-core/llm/cost.js +1 -0
  94. package/dist/analysis-core/llm/execution.js +1 -0
  95. package/dist/analysis-core/llm/output-limit.js +1 -0
  96. package/dist/analysis-core/llm/slots.js +1 -0
  97. package/dist/analysis-core/llm/stable-json.js +1 -0
  98. package/dist/analysis-core/llm/structured-output-error.js +1 -0
  99. package/dist/analysis-core/map-limit.js +1 -0
  100. package/dist/analysis-core/requests/answer-formats.js +1 -0
  101. package/dist/analysis-core/requests/check-diagnostics.js +1 -0
  102. package/dist/analysis-core/requests/headline-request.js +1 -0
  103. package/dist/analysis-core/requests/instruction-text.js +1 -0
  104. package/dist/analysis-core/requests/local-plan.js +1 -0
  105. package/dist/analysis-core/requests/registry.js +1 -0
  106. package/dist/analysis-core/requests/schema-descriptions.js +1 -0
  107. package/dist/analysis-core/requests/shape-finding-input.js +1 -0
  108. package/dist/analysis-core/requests/template-version.js +1 -0
  109. package/dist/analysis-core/requests/tools.js +1 -0
  110. package/dist/analysis-core/settings.js +1 -0
  111. package/dist/bundle/seal/deserialise.js +75 -0
  112. package/dist/bundle/seal/encryption.js +25 -0
  113. package/dist/bundle/seal/metadata.js +69 -0
  114. package/dist/bundle/seal/open.js +38 -0
  115. package/dist/bundle/seal/planned-calls.js +20 -0
  116. package/dist/bundle/seal/signature.js +51 -0
  117. package/dist/cli/agent-folder.js +17 -0
  118. package/dist/cli/api-types.js +2 -0
  119. package/dist/cli/api.js +1 -0
  120. package/dist/{claude-code-permission.js → cli/claude-code-permission.js} +4 -3
  121. package/dist/cli/commands/analyze.js +75 -0
  122. package/dist/cli/commands/login.js +51 -0
  123. package/dist/cli/completion-recovery.js +18 -0
  124. package/dist/{context.js → cli/context.js} +8 -14
  125. package/dist/cli/development-relay.js +34 -0
  126. package/dist/cli/email-sign-in.js +56 -0
  127. package/dist/{git.js → cli/git.js} +12 -8
  128. package/dist/cli/intake-payload.js +18 -0
  129. package/dist/{main.js → cli/main.js} +23 -12
  130. package/dist/{mcp.js → cli/mcp.js} +109 -40
  131. package/dist/cli/output.js +10 -0
  132. package/dist/cli/relay-check.js +239 -0
  133. package/dist/cli/relay-preparation.js +33 -0
  134. package/dist/cli/sign-in-input.js +97 -0
  135. package/dist/{sign-in.js → cli/sign-in.js} +2 -1
  136. package/dist/cli/suggested-email.js +10 -0
  137. package/dist/cli/summary.js +30 -0
  138. package/dist/contract/cli-api-messages.js +5 -0
  139. package/dist/contract/cli-refusals.js +24 -0
  140. package/dist/contract/cli-version.js +1 -0
  141. package/dist/contract/console-url.js +8 -0
  142. package/dist/contract/console.js +131 -0
  143. package/dist/contract/email-sign-in-messages.js +18 -0
  144. package/dist/contract/messages.js +252 -0
  145. package/dist/contract/release-key.js +2 -0
  146. package/dist/contract/runtime-environment.js +37 -0
  147. package/dist/core/analysis/finding-quality/config.js +4 -0
  148. package/dist/core/analysis/run-to-end.js +55 -0
  149. package/dist/core/analysis/structural-run.js +16 -0
  150. package/dist/gatekeeper/attestation.js +67 -0
  151. package/dist/gatekeeper/certificate-algorithms.js +18 -0
  152. package/dist/gatekeeper/certificate-path.js +99 -0
  153. package/dist/gatekeeper/cli-api.js +174 -0
  154. package/dist/gatekeeper/client.js +76 -0
  155. package/dist/gatekeeper/connection-exchange.js +61 -0
  156. package/dist/gatekeeper/connection.js +140 -0
  157. package/dist/gatekeeper/console.js +175 -0
  158. package/dist/gatekeeper/loopback.js +5 -0
  159. package/dist/gatekeeper/model.js +70 -0
  160. package/dist/gatekeeper/production-attestation.js +149 -0
  161. package/dist/gatekeeper/trust/hardware.js +21 -0
  162. package/dist/gatekeeper/trust/policy.js +42 -0
  163. package/dist/gatekeeper/trust/production.json +25 -0
  164. package/dist/setup-pings/client.js +31 -0
  165. package/dist/setup-pings/contract.js +2 -0
  166. package/dist/setup-pings/policy.js +3 -0
  167. package/dist/setup-pings/state.js +261 -0
  168. package/dist/setup-pings/worker.js +95 -0
  169. package/dist/transport/base64url.js +9 -0
  170. package/dist/transport/http-response.js +89 -0
  171. package/dist/transport/tls-exporter.js +5 -0
  172. package/package.json +57 -11
  173. package/LICENSE +0 -1
  174. package/dist/api-types.js +0 -2
  175. package/dist/api.js +0 -97
  176. package/dist/commands/check.js +0 -104
  177. package/dist/commands/login.js +0 -9
  178. package/dist/files.js +0 -99
  179. package/dist/gitignore.js +0 -70
  180. package/dist/output.js +0 -23
  181. package/dist/secret-files.js +0 -5
  182. /package/dist/{browser.js → cli/browser.js} +0 -0
  183. /package/dist/{commands → cli/commands}/balance.js +0 -0
  184. /package/dist/{commands → cli/commands}/finding.js +0 -0
  185. /package/dist/{commands → cli/commands}/logout.js +0 -0
  186. /package/dist/{credentials.js → cli/credentials.js} +0 -0
@@ -0,0 +1,95 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { request as httpRequest } from "node:http";
3
+ import { request as httpsRequest } from "node:https";
4
+ import process from "node:process";
5
+ import { clearTimeout, setTimeout } from "node:timers";
6
+ import { fileURLToPath, URL } from "node:url";
7
+ import { SETUP_PING_STEPS } from "./contract.js";
8
+ import { claimLink, claimStep } from "./state.js";
9
+ import { DEFAULT_CONSOLE_URL } from "./client.js";
10
+ function parseOperation(raw) {
11
+ if (!raw || typeof raw !== "object")
12
+ return null;
13
+ const operation = raw;
14
+ if (typeof operation.homeDir !== "string" || typeof operation.origin !== "string")
15
+ return null;
16
+ if (operation.kind !== "ping" && operation.kind !== "link")
17
+ return null;
18
+ if (operation.kind === "ping" && !SETUP_PING_STEPS.includes(operation.step))
19
+ return null;
20
+ if (operation.kind === "link" && !operation.token)
21
+ return null;
22
+ if (operation.token !== undefined && (typeof operation.token !== "string" || !/^[\x21-\x7e]{1,4096}$/.test(operation.token)))
23
+ return null;
24
+ const base = new URL(operation.origin);
25
+ const production = base.origin === DEFAULT_CONSOLE_URL;
26
+ const loopback = operation.allowLoopback === true && base.protocol === "http:" && ["127.0.0.1", "[::1]"].includes(base.hostname);
27
+ if ((!production && !loopback) || base.username || base.password || base.search || base.hash)
28
+ return null;
29
+ const url = new URL(`${base.href.replace(/\/+$/, "")}/api/funnel/step`);
30
+ return { operation, url };
31
+ }
32
+ export async function runWorker(raw, io = {}) {
33
+ let request;
34
+ let httpDeadline;
35
+ let stopped = false;
36
+ let finish;
37
+ const finished = new Promise(resolve => { finish = () => { stopped = true; resolve(); }; });
38
+ const deadline = setTimeout(finish, 750);
39
+ void (async () => {
40
+ try {
41
+ const parsed = parseOperation(raw);
42
+ if (!parsed)
43
+ return;
44
+ const { operation, url } = parsed;
45
+ const claim = operation.kind === "ping"
46
+ ? await (io.claimStep ?? claimStep)(operation.homeDir, operation.step).then(value => value && { ...value, step: operation.step })
47
+ : await (io.claimLink ?? claimLink)(operation.homeDir);
48
+ if (!claim || stopped)
49
+ return;
50
+ const body = JSON.stringify({ installId: claim.installId, step: claim.step });
51
+ const headers = { "content-type": "application/json", "content-length": Buffer.byteLength(body) };
52
+ if (operation.token)
53
+ headers.authorization = `Bearer ${operation.token}`;
54
+ const send = io.request ?? (url.protocol === "https:" ? httpsRequest : httpRequest);
55
+ request = send(url, { method: "POST", headers, agent: false }, response => {
56
+ // Status is final: no redirects, response body, retries or server text.
57
+ response.destroy();
58
+ finish();
59
+ });
60
+ request.on("error", finish);
61
+ // An absolute timer also covers DNS and TLS before a socket is ready.
62
+ httpDeadline = setTimeout(finish, 500);
63
+ request.end(body);
64
+ await finished;
65
+ }
66
+ catch { /* Disk and transport failures are deliberately silent. */ }
67
+ finally {
68
+ finish();
69
+ }
70
+ })();
71
+ await finished;
72
+ clearTimeout(deadline);
73
+ clearTimeout(httpDeadline);
74
+ request?.destroy();
75
+ }
76
+ async function main() {
77
+ // A hard process deadline also covers hung filesystem work and unread stdin.
78
+ const deadline = setTimeout(() => process.exit(0), 750);
79
+ try {
80
+ let input = "";
81
+ for await (const chunk of process.stdin) {
82
+ input += chunk;
83
+ if (Buffer.byteLength(input) > 16384)
84
+ return;
85
+ }
86
+ await runWorker(JSON.parse(input));
87
+ }
88
+ catch { /* No telemetry failure is printed. */ }
89
+ finally {
90
+ clearTimeout(deadline);
91
+ process.exit(0);
92
+ }
93
+ }
94
+ if (process.argv[1] === fileURLToPath(import.meta.url))
95
+ void main();
@@ -0,0 +1,9 @@
1
+ /** Node's decoder ignores invalid characters and trailing bits; signed tokens require canonical bytes. */
2
+ export function decodeBase64url(value) {
3
+ if (!/^[A-Za-z0-9_-]+$/.test(value))
4
+ throw new Error();
5
+ const bytes = Buffer.from(value, "base64url");
6
+ if (bytes.toString("base64url") !== value)
7
+ throw new Error();
8
+ return bytes;
9
+ }
@@ -0,0 +1,89 @@
1
+ /** Owns response bounds and cleanup for direct HTTP clients; callers own destinations and payload formats. */
2
+ export function requestBoundedResponse(start, limits) {
3
+ return new Promise((resolve, reject) => {
4
+ let req, response;
5
+ let done = false;
6
+ let timer;
7
+ const fail = () => {
8
+ if (done)
9
+ return;
10
+ done = true;
11
+ clearTimeout(timer);
12
+ req?.destroy();
13
+ response?.destroy();
14
+ reject(new Error("HTTP response failed"));
15
+ };
16
+ try {
17
+ for (const limit of [limits.maxBytes, limits.timeoutMs, limits.bodyTimeoutMs ?? limits.timeoutMs]) {
18
+ if (!Number.isSafeInteger(limit) || limit < 1)
19
+ throw new Error();
20
+ }
21
+ // This starts before dialing and never resets on data, so trickled bytes cannot prolong it.
22
+ timer = setTimeout(fail, limits.timeoutMs);
23
+ req = start(incoming => {
24
+ incoming.on("error", fail);
25
+ incoming.on("aborted", fail);
26
+ if (done) {
27
+ incoming.destroy();
28
+ return;
29
+ }
30
+ if (response) {
31
+ incoming.destroy();
32
+ fail();
33
+ return;
34
+ }
35
+ response = incoming;
36
+ try {
37
+ if (limits.bodyTimeoutMs !== undefined) {
38
+ clearTimeout(timer);
39
+ timer = setTimeout(fail, limits.bodyTimeoutMs);
40
+ }
41
+ const length = incoming.headers["content-length"];
42
+ const encoding = incoming.headers["content-encoding"];
43
+ if (encoding !== undefined && encoding !== "identity" ||
44
+ length !== undefined && (!/^\d+$/.test(length) || Number(length) > limits.maxBytes))
45
+ throw new Error();
46
+ limits.validate?.(incoming);
47
+ }
48
+ catch {
49
+ fail();
50
+ return;
51
+ }
52
+ const chunks = [];
53
+ let bytes = 0;
54
+ incoming.on("data", (chunk) => {
55
+ if (done)
56
+ return;
57
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
58
+ bytes += buffer.length;
59
+ if (bytes > limits.maxBytes) {
60
+ fail();
61
+ return;
62
+ }
63
+ chunks.push(buffer);
64
+ });
65
+ incoming.on("end", () => {
66
+ if (done)
67
+ return;
68
+ if (!incoming.complete) {
69
+ fail();
70
+ return;
71
+ }
72
+ done = true;
73
+ clearTimeout(timer);
74
+ resolve({ response: incoming, body: Buffer.concat(chunks) });
75
+ });
76
+ incoming.on("close", () => { if (!incoming.complete)
77
+ fail(); });
78
+ });
79
+ req.on("error", fail);
80
+ if (done)
81
+ req.destroy();
82
+ else
83
+ req.end(limits.requestBody);
84
+ }
85
+ catch {
86
+ fail();
87
+ }
88
+ });
89
+ }
@@ -0,0 +1,5 @@
1
+ import { RELAY_TLS_EXPORTER_LABEL } from "../contract/messages.js";
2
+ /** Each peer must derive these bytes independently from its actual socket. */
3
+ export function relayExportedKeyMaterial(socket) {
4
+ return socket.exportKeyingMaterial(32, RELAY_TLS_EXPORTER_LABEL, Buffer.alloc(0));
5
+ }
package/package.json CHANGED
@@ -1,16 +1,62 @@
1
1
  {
2
2
  "name": "@counterfact-labs/cli",
3
- "version": "0.1.3",
4
- "license": "UNLICENSED",
5
- "description": "Check an agent from your terminal or coding agent",
3
+ "version": "0.2.4",
6
4
  "type": "module",
7
- "bin": { "counterfact-labs": "dist/main.js" },
8
- "engines": { "node": ">=20" },
9
- "files": ["dist", "!dist/**/*.test.js", "!dist/test-helpers.js"],
5
+ "files": [
6
+ "dist/**/*.js",
7
+ "dist/gatekeeper/trust/production.json",
8
+ "bundle/sealed.json",
9
+ "bundle/client-release.json"
10
+ ],
11
+ "engines": {
12
+ "node": ">=22"
13
+ },
10
14
  "scripts": {
11
- "prebuild": "node scripts/copy-api-types.mjs",
12
- "build": "node scripts/build.mjs",
13
- "test": "npm run build && node --test dist/*.test.js dist/commands/*.test.js",
14
- "prepack": "npm run build"
15
- }
15
+ "lint": "eslint .",
16
+ "bundle:seal": "tsx bundle/seal/cli.ts",
17
+ "bundle:check": "tsx bundle/seal/cli.ts check",
18
+ "bench:replay": "tsx bench/replay.ts",
19
+ "bench:importer": "tsx bench/importer-coverage.ts",
20
+ "typecheck": "tsc --noEmit",
21
+ "test": "tsx --test ${TEST_SHARD:+--test-shard=$TEST_SHARD} --test-concurrency=4 --test-timeout=600000 \"core/**/*.test.ts\" \"adapters/**/*.test.ts\" \"cli/**/*.test.ts\" \"bench/**/*.test.ts\" \"bundle/**/*.test.ts\" \"contract/**/*.test.ts\" \"gatekeeper/**/*.test.ts\" \"relay/**/*.test.ts\" \"transport/**/*.test.ts\" \"packages/core/src/**/*.test.ts\" \"packages/core/*.test.ts\" \"packages/setup-pings/src/**/*.test.ts\" \"scripts/**/*.test.ts\"",
22
+ "build:core": "npm run build --workspace @counterfact-labs/analysis-core",
23
+ "build:setup-pings": "npm run build --workspace @counterfact-labs/setup-pings",
24
+ "prepare": "npm run build:core && npm run build:setup-pings",
25
+ "pretest": "npm run build:core && npm run build:setup-pings",
26
+ "release:core": "tsx scripts/release-core.ts",
27
+ "build:cli": "npm run build:setup-pings && tsx scripts/build-cli.ts",
28
+ "prepack": "npm run build:core && npm run build:cli",
29
+ "pack:integration": "tsx scripts/integration/pack.ts",
30
+ "release:cli": "tsx scripts/release-cli.ts"
31
+ },
32
+ "dependencies": {
33
+ "@anthropic-ai/sdk": "0.128.0",
34
+ "zod": "4.6.5",
35
+ "asn1js": "3.0.6",
36
+ "pkijs": "3.2.5"
37
+ },
38
+ "devDependencies": {
39
+ "@eslint/js": "9.39.5",
40
+ "@types/node": "24.19.0",
41
+ "esbuild": "0.28.2",
42
+ "eslint": "9.39.5",
43
+ "js-yaml": "4.3.2",
44
+ "tsx": "4.23.15",
45
+ "typescript": "6.0.3",
46
+ "typescript-eslint": "8.64.0",
47
+ "@counterfact-labs/analysis-core": "0.8.0",
48
+ "@counterfact-labs/setup-pings": "0.1.0"
49
+ },
50
+ "workspaces": [
51
+ "packages/*"
52
+ ],
53
+ "bin": {
54
+ "counterfact-labs": "dist/cli/main.js"
55
+ },
56
+ "exports": {
57
+ ".": "./dist/cli/main.js",
58
+ "./gatekeeper": "./dist/gatekeeper/client.js"
59
+ },
60
+ "license": "UNLICENSED",
61
+ "description": "Analyze an agent from your terminal or coding agent"
16
62
  }
package/LICENSE DELETED
@@ -1 +0,0 @@
1
- Copyright (c) 2026 Counterfact Labs, Inc. All rights reserved. This software is provided to access the Counterfact service and may be used only for that purpose. No other rights are granted.
package/dist/api-types.js DELETED
@@ -1,2 +0,0 @@
1
- // This file is copied into the command-line package before its build. Keep it self-contained.
2
- export const CLI_BODY_LIMIT = 2 * 1024 * 1024;
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,9 +0,0 @@
1
- import { startSignIn } from "../sign-in.js";
2
- export async function login(ctx) {
3
- const flow = await startSignIn(ctx);
4
- ctx.write(`1. Open ${flow.verificationUri}. If asked, sign in with the code we email you.\n`
5
- + `2. Then enter this code on that page: ${flow.userCode}\n`);
6
- await flow.done;
7
- ctx.write("Signed in.\n");
8
- return 0;
9
- }
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/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
- }