@counterfact-labs/cli 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,7 +25,7 @@ The console is at `https://app.counterfactlabs.com`. Sign in again after moving
25
25
  Add the local MCP server to Codex:
26
26
 
27
27
  ```sh
28
- codex mcp add counterfact -- npx -y @counterfact-labs/cli mcp
28
+ codex mcp add counterfact -- npx -y @counterfact-labs/cli@latest mcp
29
29
  ```
30
30
 
31
31
  Checks can take several minutes. Codex has a [60-second tool timeout by default](https://learn.chatgpt.com/docs/extend/mcp?surface=cli). In `~/.codex/config.toml`, add `tool_timeout_sec = 900` to the existing `[mcp_servers.counterfact]` section. It should look like this; keep any other settings already in that section:
@@ -33,7 +33,7 @@ Checks can take several minutes. Codex has a [60-second tool timeout by default]
33
33
  ```toml
34
34
  [mcp_servers.counterfact]
35
35
  command = "npx"
36
- args = ["-y", "@counterfact-labs/cli", "mcp"]
36
+ args = ["-y", "@counterfact-labs/cli@latest", "mcp"]
37
37
  tool_timeout_sec = 900
38
38
  ```
39
39
 
@@ -51,7 +51,7 @@ After editing prompts, skills or tool definitions, suggest a check and wait for
51
51
  me to ask for it.
52
52
  ```
53
53
 
54
- The MCP server reads local files and communicates over stdin and stdout. Its tools are `check_agent(path, confirm?)`, `get_finding(id)` and `get_balance()`. Finding ids accompany completed checks in the tool's structured result. Keep the preview and confirmation in the same MCP session.
54
+ The MCP server reads local files and communicates over stdin and stdout. Its tools are `check_agent(path, confirm?)`, `get_finding(id)` and `get_balance()`. Each result's structured content repeats the tool's text, and completed checks add their finding ids. Keep the preview and confirmation in the same MCP session.
55
55
 
56
56
  Privacy: only files requested by the server are uploaded. The tool skips `.git`, `node_modules`, `.gitignore` matches, symlinks and secret file names such as `.env` and private keys. Uploaded files are kept with the check while the workspace exists, and deleted with the check or workspace. Sign-in is saved in `~/.config/counterfact-labs/credentials` with owner-only permissions. `logout` removes that local sign-in; revoke the token from your account to disable it elsewhere.
57
57
 
@@ -35,9 +35,11 @@ export async function prepareCheck(path, ctx) {
35
35
  throw new Error("No agent files were found in this folder.");
36
36
  ctx.write(fileText(path, wanted));
37
37
  const upload = await readUpload(root, files, plan.wanted);
38
- const response = await awaitQuote(await ctx.api.request("checks", { ...facts, files: upload }), ctx);
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);
39
41
  ctx.write(quoteText(response.quote));
40
- return { root, response, files: upload };
42
+ return { root, response, files: upload, url: created.url };
41
43
  }
42
44
  /** A confirmed check can keep running and charge while the connection is down. */
43
45
  async function pollConfirmed(id, ctx) {
@@ -1,7 +1,8 @@
1
1
  import { saveCredentials } from "../credentials.js";
2
2
  export async function login(ctx) {
3
3
  const device = await ctx.api.request("device", {}, false);
4
- ctx.write(`To sign in, open ${device.verificationUri} and enter ${device.userCode}\n`);
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`);
5
6
  const until = ctx.now() + device.expiresIn * 1000;
6
7
  while (ctx.now() < until) {
7
8
  await ctx.sleep(device.interval * 1000);
package/dist/files.js CHANGED
@@ -2,10 +2,10 @@ import { constants } from "node:fs";
2
2
  import { lstat, open, readdir, realpath } from "node:fs/promises";
3
3
  import { dirname, join, relative, resolve, sep } from "node:path";
4
4
  import { ignored, parseIgnore } from "./gitignore.js";
5
- import { SECRET_FILE_RE } from "./secret-files.js";
5
+ import { isReadableRepoPath } from "./secret-files.js";
6
6
  const slash = (path) => path.split(sep).join("/");
7
7
  function safe(path) {
8
- return !!path && !path.includes("\\") && !path.includes("\0") && !path.startsWith("/") && !SECRET_FILE_RE.test(path)
8
+ return !!path && !path.includes("\\") && !path.includes("\0") && !path.startsWith("/") && isReadableRepoPath(path)
9
9
  && path.split("/").every(part => !["", ".", "..", ".git", "node_modules"].includes(part));
10
10
  }
11
11
  async function ignoreFile(directory, base) {
package/dist/mcp.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createInterface } from "node:readline";
2
2
  import { once } from "node:events";
3
+ import { readFile } from "node:fs/promises";
3
4
  import { resolve } from "node:path";
4
5
  import { errorCode, errorText } from "./api.js";
5
6
  import { commandContext } from "./context.js";
@@ -27,6 +28,7 @@ class ProtocolError extends Error {
27
28
  }
28
29
  function object(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
29
30
  export async function serveMcp(input, output, options = {}) {
31
+ const { version } = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
30
32
  const previews = new Map();
31
33
  const requests = new Map();
32
34
  let queue = Promise.resolve();
@@ -47,6 +49,7 @@ export async function serveMcp(input, output, options = {}) {
47
49
  throw new ProtocolError(-32602, "Provide a finding id.");
48
50
  let text = "", exitCode = 0;
49
51
  let findingIds;
52
+ let url;
50
53
  try {
51
54
  const ctx = commandContext({ ...options, signal, write: chunk => {
52
55
  // Progress is transient terminal text; only the final lines belong in a tool response.
@@ -64,11 +67,16 @@ export async function serveMcp(input, output, options = {}) {
64
67
  previews.delete(path);
65
68
  if (!prepared)
66
69
  throw new Error("Get a preview first. Show the user the price and get their agreement before confirming.");
67
- findingIds = (await confirmPrepared(prepared, ctx)).findingIds;
70
+ url = prepared.url;
71
+ const result = await confirmPrepared(prepared, ctx);
72
+ findingIds = result.findingIds;
73
+ url = result.taste.url;
68
74
  }
69
75
  else {
70
76
  previews.delete(path);
71
- previews.set(path, await prepareCheck(args.path, ctx));
77
+ const prepared = await prepareCheck(args.path, ctx);
78
+ previews.set(path, prepared);
79
+ url = prepared.url;
72
80
  }
73
81
  }
74
82
  }
@@ -76,8 +84,10 @@ export async function serveMcp(input, output, options = {}) {
76
84
  text += errorText(error) + "\n";
77
85
  exitCode = errorCode(error);
78
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.
79
89
  return { content: [{ type: "text", text }], isError: exitCode !== 0,
80
- structuredContent: { exitCode, ...(findingIds ? { findingIds } : {}) } };
90
+ structuredContent: { text, exitCode, ...(findingIds ? { findingIds } : {}), ...(url ? { url } : {}) } };
81
91
  }
82
92
  async function respond(message, id, signal) {
83
93
  if (signal.aborted)
@@ -88,7 +98,7 @@ export async function serveMcp(input, output, options = {}) {
88
98
  if (message.method === "initialize") {
89
99
  const requested = object(message.params) ? message.params.protocolVersion : undefined;
90
100
  result = { protocolVersion: ["2024-11-05", "2025-03-26", "2025-06-18"].includes(requested) ? requested : "2025-06-18",
91
- capabilities: { tools: {} }, serverInfo: { name: "counterfact-labs", version: "0.1.0" } };
101
+ capabilities: { tools: {} }, serverInfo: { name: "counterfact-labs", version } };
92
102
  }
93
103
  else if (message.method === "ping")
94
104
  result = {};
@@ -1,2 +1,5 @@
1
- // Copied from src/lib/conventions/file-patterns.ts by the build.
1
+ // Copied from src/lib/conventions/file-admission.ts by the build.
2
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@counterfact-labs/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "license": "UNLICENSED",
5
5
  "description": "Check an agent from your terminal or coding agent",
6
6
  "type": "module",