@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 +3 -3
- package/dist/commands/check.js +4 -2
- package/dist/commands/login.js +2 -1
- package/dist/files.js +2 -2
- package/dist/mcp.js +14 -4
- package/dist/secret-files.js +4 -1
- package/package.json +1 -1
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()`.
|
|
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
|
|
package/dist/commands/check.js
CHANGED
|
@@ -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
|
|
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) {
|
package/dist/commands/login.js
CHANGED
|
@@ -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(`
|
|
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 {
|
|
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("/") &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
101
|
+
capabilities: { tools: {} }, serverInfo: { name: "counterfact-labs", version } };
|
|
92
102
|
}
|
|
93
103
|
else if (message.method === "ping")
|
|
94
104
|
result = {};
|
package/dist/secret-files.js
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
// Copied from src/lib/conventions/file-
|
|
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
|
+
}
|