@counterfact-labs/cli 0.1.1 → 0.1.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.
package/README.md CHANGED
@@ -20,6 +20,10 @@ counterfact-labs logout
20
20
 
21
21
  Checks return exit code 0 on success, 1 on error and 2 when the balance is too low. The server chooses the files, sets the price and writes the result summary. The first eligible check is free. Failed checks are not charged.
22
22
 
23
+ By default, results appear as an analysis in a project in your workspace. A matching GitHub repository uses an existing project. Otherwise the check reuses a project from an earlier check of the same folder or creates one. The results link opens the project's findings for that analysis. New and fixed problems are compared with the agents' open findings there, including earlier console results.
24
+
25
+ The tool sends the branch, full commit and whether the checked folder has local edits when Git can report them. Analysis history shows these details with who ran the check. The analysis reads the uploaded files, so its findings show your local copy. Older tool versions that omit branch, commit or local edits still work.
26
+
23
27
  The console is at `https://app.counterfactlabs.com`. Sign in again after moving from the old address; saved sign-in belongs to the server address.
24
28
 
25
29
  Add the local MCP server to Codex:
@@ -37,11 +41,13 @@ args = ["-y", "@counterfact-labs/cli@latest", "mcp"]
37
41
  tool_timeout_sec = 900
38
42
  ```
39
43
 
40
- Cancellation stops the local wait. A confirmed check keeps running on the server after cancellation or a timeout, and its result appears on the console's Checks page.
44
+ Cancellation stops the local wait. A confirmed check keeps running on the server after cancellation or a timeout. Use the project link from the preview to follow it in the console.
41
45
 
42
46
  Add this to your project's `AGENTS.md`:
43
47
 
44
48
  ```markdown
49
+ First call sign_in_status; if not signed in, call sign_in and then sign_in_status with wait: true.
50
+ Always show the returned sign-in link and code, so I can sign in on any device.
45
51
  When I ask to check an agent, use Counterfact's check_agent tool with its folder path.
46
52
  Call it without confirm first. Show me the selected files and price, and get my
47
53
  agreement before calling again with confirm: true. If the files or price change,
@@ -51,9 +57,9 @@ After editing prompts, skills or tool definitions, suggest a check and wait for
51
57
  me to ask for it.
52
58
  ```
53
59
 
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.
60
+ The MCP server reads local files and communicates over stdin and stdout. Its tools are `sign_in_status(wait?)`, `sign_in()`, `check_agent(path, confirm?)`, `get_finding(id)` and `get_balance()`. Sign-in status also reports whether Claude Code settings allow the check tool. Sign-in opens a browser when a local desktop is available and always returns a link and code for signing in on any device. Each result's structured content repeats the tool's text, and completed checks add their finding ids. Keep sign-in and its wait, and the preview and confirmation, in the same MCP session.
55
61
 
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.
62
+ 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 saved with the analysis so findings can show the source that was checked. 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
63
 
58
64
  Set `COUNTERFACT_URL` to use another server. It defaults to `https://app.counterfactlabs.com`. Sign-in is tied to the server address; sign in again when changing it.
59
65
 
@@ -0,0 +1,23 @@
1
+ import { spawn } from "node:child_process";
2
+ export async function openBrowser(url, spawner = spawn, platform = process.platform, env = process.env) {
3
+ try {
4
+ if (!["http:", "https:"].includes(new URL(url).protocol))
5
+ return false;
6
+ // A remote opener cannot reach the user's screen, even when it starts successfully.
7
+ if (env.SSH_CONNECTION || env.SSH_TTY || (platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY))
8
+ return false;
9
+ // Windows parses start's arguments as shell text; these links need the caller's fallback.
10
+ if (platform === "win32" && /[\s"&|<>^()%!]/.test(url))
11
+ return false;
12
+ const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
13
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
14
+ return await new Promise(resolve => {
15
+ const child = spawner(command, args, { detached: true, stdio: "ignore" });
16
+ child.once("error", () => resolve(false));
17
+ child.once("spawn", () => { child.unref(); resolve(true); });
18
+ });
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
@@ -0,0 +1,19 @@
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 CHECK_TOOL_RULE = `${servers[0]}__check_agent`;
5
+ const allowedRules = new Set(servers.flatMap(server => [server, `${server}__*`, `${server}__check_agent`]));
6
+ export async function checkToolAllowed(homeDir, cwd) {
7
+ const paths = [join(homeDir, ".claude/settings.json"), join(cwd, ".claude/settings.json"), join(cwd, ".claude/settings.local.json")];
8
+ const matches = await Promise.all(paths.map(async (path) => {
9
+ try {
10
+ const settings = JSON.parse(await readFile(path, "utf8"));
11
+ const allow = settings?.permissions?.allow;
12
+ return Array.isArray(allow) && allow.some(rule => allowedRules.has(rule));
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }));
18
+ return matches.some(Boolean);
19
+ }
@@ -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,20 +1,9 @@
1
- import { saveCredentials } from "../credentials.js";
1
+ import { startSignIn } from "../sign-in.js";
2
2
  export async function login(ctx) {
3
- const device = await ctx.api.request("device", {}, false);
4
- ctx.write(`To sign in, open ${device.verificationUri} and enter ${device.userCode}\n`);
5
- const until = ctx.now() + device.expiresIn * 1000;
6
- while (ctx.now() < until) {
7
- await ctx.sleep(device.interval * 1000);
8
- const poll = await ctx.api.request("device/token", { deviceCode: device.deviceCode }, false);
9
- if (poll.status === "ok") {
10
- await saveCredentials(ctx.homeDir, ctx.api.baseUrl, poll.token);
11
- ctx.write("Signed in.\n");
12
- return 0;
13
- }
14
- if (poll.status === "expired")
15
- break;
16
- if (poll.status === "denied")
17
- throw new Error("Sign-in was cancelled. Run counterfact-labs login to try again.");
18
- }
19
- throw new Error("The sign-in code expired. Run counterfact-labs login to try again.");
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;
20
9
  }
package/dist/context.js CHANGED
@@ -2,6 +2,7 @@ import { homedir } from "node:os";
2
2
  import { createInterface } from "node:readline/promises";
3
3
  import { setTimeout as delay } from "node:timers/promises";
4
4
  import { ApiClient, DEFAULT_URL } from "./api.js";
5
+ import { openBrowser } from "./browser.js";
5
6
  import { readCredentials } from "./credentials.js";
6
7
  export function commandContext(options = {}) {
7
8
  const homeDir = options.homeDir ?? homedir();
@@ -9,7 +10,7 @@ export function commandContext(options = {}) {
9
10
  const api = new ApiClient(baseUrl, () => readCredentials(homeDir, api.baseUrl), undefined, options.signal);
10
11
  const write = options.write ?? (text => { process.stdout.write(text); });
11
12
  return {
12
- api, homeDir, write,
13
+ api, homeDir, write, openBrowser: options.openBrowser ?? openBrowser,
13
14
  sleep: options.sleep ?? (async (ms) => { await delay(ms, undefined, { signal: options.signal }); }), now: options.now ?? Date.now,
14
15
  ask: options.ask ?? (async (question) => {
15
16
  if (!process.stdin.isTTY)
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/git.js CHANGED
@@ -8,7 +8,7 @@ export async function repositoryFacts(root, git = async (args) => (await promisi
8
8
  repoRoot = (await git(["rev-parse", "--show-toplevel"])).trim();
9
9
  }
10
10
  catch {
11
- return { remoteUrl: null, relPath: "", folderName };
11
+ return { remoteUrl: null, relPath: "", folderName, branch: null, commit: null, dirty: null };
12
12
  }
13
13
  let remoteUrl;
14
14
  try {
@@ -17,5 +17,19 @@ export async function repositoryFacts(root, git = async (args) => (await promisi
17
17
  catch {
18
18
  remoteUrl = null;
19
19
  }
20
- return { repoRoot, remoteUrl, relPath: relative(repoRoot, root).split(sep).join("/"), folderName };
20
+ const read = async (args) => {
21
+ try {
22
+ return (await git(args)).trim();
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ };
28
+ const [branch, commit, status] = await Promise.all([
29
+ read(["rev-parse", "--abbrev-ref", "HEAD"]), read(["rev-parse", "HEAD"]),
30
+ // Include new files even when Git hides them, and exclude sibling folders.
31
+ read(["status", "--porcelain", "--untracked-files=normal", "--", "."]),
32
+ ]);
33
+ return { repoRoot, remoteUrl, relPath: relative(repoRoot, root).split(sep).join("/"), folderName,
34
+ branch: branch === "HEAD" ? null : branch || null, commit: commit || null, dirty: status === null ? null : status.length > 0 };
21
35
  }
package/dist/mcp.js CHANGED
@@ -1,11 +1,14 @@
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";
6
7
  import { confirmPrepared, prepareCheck } from "./commands/check.js";
7
8
  import { finding } from "./commands/finding.js";
8
9
  import { balance } from "./commands/balance.js";
10
+ import { checkToolAllowed } from "./claude-code-permission.js";
11
+ import { signInStatus, startSignIn } from "./sign-in.js";
9
12
  const tools = [
10
13
  {
11
14
  name: "check_agent",
@@ -17,6 +20,8 @@ const tools = [
17
20
  },
18
21
  { 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 } },
19
22
  { name: "get_balance", description: "Get the balance and top-up link.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
23
+ { name: "sign_in_status", description: "Check whether this computer is signed in to Counterfact, and whether the user has allowed the check tool in Claude Code. With wait: true, first wait for a sign-in started by sign_in to finish.", inputSchema: { type: "object", properties: { wait: { type: "boolean" } }, additionalProperties: false } },
24
+ { name: "sign_in", description: "Open the Counterfact sign-in page in the user's browser. Then call sign_in_status with wait: true.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
20
25
  ];
21
26
  class ProtocolError extends Error {
22
27
  code;
@@ -27,7 +32,10 @@ class ProtocolError extends Error {
27
32
  }
28
33
  function object(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
29
34
  export async function serveMcp(input, output, options = {}) {
35
+ const { version } = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
30
36
  const previews = new Map();
37
+ const cwd = options.cwd ?? process.cwd();
38
+ let pendingSignIn;
31
39
  const requests = new Map();
32
40
  let queue = Promise.resolve();
33
41
  const lines = createInterface({ input, crlfDelay: Infinity });
@@ -38,15 +46,19 @@ export async function serveMcp(input, output, options = {}) {
38
46
  if (!object(args))
39
47
  throw new ProtocolError(-32602, "Tool arguments must be an object.");
40
48
  const name = params.name;
41
- const allowed = name === "check_agent" ? ["path", "confirm"] : name === "get_finding" ? ["id"] : [];
49
+ const allowed = name === "check_agent" ? ["path", "confirm"] : name === "get_finding" ? ["id"] : name === "sign_in_status" ? ["wait"] : [];
42
50
  if (!tools.some(tool => tool.name === name) || Object.keys(args).some(key => !allowed.includes(key)))
43
51
  throw new ProtocolError(-32602, "Unknown tool or argument.");
44
52
  if (name === "check_agent" && (typeof args.path !== "string" || !args.path.trim() || (args.confirm !== undefined && typeof args.confirm !== "boolean")))
45
53
  throw new ProtocolError(-32602, "Provide a path and an optional boolean confirmation.");
46
54
  if (name === "get_finding" && (typeof args.id !== "string" || !args.id.trim()))
47
55
  throw new ProtocolError(-32602, "Provide a finding id.");
56
+ if (name === "sign_in_status" && args.wait !== undefined && typeof args.wait !== "boolean")
57
+ throw new ProtocolError(-32602, "Provide an optional boolean wait.");
48
58
  let text = "", exitCode = 0;
49
59
  let findingIds;
60
+ let url;
61
+ let signIn;
50
62
  try {
51
63
  const ctx = commandContext({ ...options, signal, write: chunk => {
52
64
  // Progress is transient terminal text; only the final lines belong in a tool response.
@@ -57,6 +69,55 @@ export async function serveMcp(input, output, options = {}) {
57
69
  await finding(args.id, ctx);
58
70
  else if (name === "get_balance")
59
71
  await balance(ctx);
72
+ else if (name === "sign_in_status") {
73
+ const pending = pendingSignIn;
74
+ const allowed = await checkToolAllowed(ctx.homeDir, cwd);
75
+ if (args.wait === true && pending) {
76
+ const cancel = () => pending.controller.abort();
77
+ signal.addEventListener("abort", cancel, { once: true });
78
+ try {
79
+ if (signal.aborted)
80
+ cancel();
81
+ await pending.flow.done;
82
+ }
83
+ finally {
84
+ signal.removeEventListener("abort", cancel);
85
+ }
86
+ }
87
+ signIn = { ...await signInStatus(ctx), checkToolAllowed: allowed };
88
+ text = signIn.signedIn ? "Signed in." : "Not signed in.";
89
+ }
90
+ else if (name === "sign_in") {
91
+ if ((await signInStatus(ctx)).signedIn)
92
+ text = "Already signed in.";
93
+ else {
94
+ let pending = pendingSignIn;
95
+ if (!pending) {
96
+ // The flow outlives this call; a later wait call can cancel its polling.
97
+ const controller = new AbortController();
98
+ const cancel = () => controller.abort();
99
+ signal.addEventListener("abort", cancel, { once: true });
100
+ try {
101
+ signal.throwIfAborted();
102
+ const flow = await startSignIn(commandContext({ ...options, homeDir: ctx.homeDir, baseUrl: ctx.api.baseUrl, signal: controller.signal }));
103
+ pending = { flow, controller, opened: false };
104
+ pendingSignIn = pending;
105
+ const current = pending;
106
+ const clear = () => { if (pendingSignIn === current)
107
+ pendingSignIn = undefined; };
108
+ // Handle background failures even when the client never calls wait.
109
+ void flow.done.then(clear, clear);
110
+ pending.opened = await ctx.openBrowser(flow.pageUrl);
111
+ }
112
+ finally {
113
+ signal.removeEventListener("abort", cancel);
114
+ }
115
+ }
116
+ signIn = { pageUrl: pending.flow.pageUrl, userCode: pending.flow.userCode };
117
+ text = pending.opened ? `Opening your browser… If nothing opens, go to ${signIn.pageUrl} on any device and enter ${signIn.userCode}.`
118
+ : `Go to ${signIn.pageUrl} on any device and enter ${signIn.userCode}.`;
119
+ }
120
+ }
60
121
  else {
61
122
  const path = resolve(args.path);
62
123
  if (args.confirm === true) {
@@ -64,11 +125,16 @@ export async function serveMcp(input, output, options = {}) {
64
125
  previews.delete(path);
65
126
  if (!prepared)
66
127
  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;
128
+ url = prepared.url;
129
+ const result = await confirmPrepared(prepared, ctx);
130
+ findingIds = result.findingIds;
131
+ url = result.taste.url;
68
132
  }
69
133
  else {
70
134
  previews.delete(path);
71
- previews.set(path, await prepareCheck(args.path, ctx));
135
+ const prepared = await prepareCheck(args.path, ctx);
136
+ previews.set(path, prepared);
137
+ url = prepared.url;
72
138
  }
73
139
  }
74
140
  }
@@ -79,7 +145,7 @@ export async function serveMcp(input, output, options = {}) {
79
145
  // Some clients (Claude Code among them) give the model only the structured result when one is present, so it
80
146
  // repeats the text: without it the model sees no files, price or results.
81
147
  return { content: [{ type: "text", text }], isError: exitCode !== 0,
82
- structuredContent: { text, exitCode, ...(findingIds ? { findingIds } : {}) } };
148
+ structuredContent: { text, exitCode, ...(findingIds ? { findingIds } : {}), ...(url ? { url } : {}), ...signIn } };
83
149
  }
84
150
  async function respond(message, id, signal) {
85
151
  if (signal.aborted)
@@ -90,7 +156,7 @@ export async function serveMcp(input, output, options = {}) {
90
156
  if (message.method === "initialize") {
91
157
  const requested = object(message.params) ? message.params.protocolVersion : undefined;
92
158
  result = { protocolVersion: ["2024-11-05", "2025-03-26", "2025-06-18"].includes(requested) ? requested : "2025-06-18",
93
- capabilities: { tools: {} }, serverInfo: { name: "counterfact-labs", version: "0.1.1" } };
159
+ capabilities: { tools: {} }, serverInfo: { name: "counterfact-labs", version } };
94
160
  }
95
161
  else if (message.method === "ping")
96
162
  result = {};
@@ -147,7 +213,12 @@ export async function serveMcp(input, output, options = {}) {
147
213
  if (!output.write(JSON.stringify(reply) + "\n"))
148
214
  await once(output, "drain");
149
215
  }
150
- await queue;
216
+ try {
217
+ await queue;
218
+ }
219
+ finally {
220
+ pendingSignIn?.controller.abort();
221
+ }
151
222
  }
152
223
  function errorReply(id, error) {
153
224
  return { jsonrpc: "2.0", id, error: {
package/dist/output.js CHANGED
@@ -8,7 +8,7 @@ export function fileText(path, files) {
8
8
  const bytes = files.reduce((sum, file) => sum + file.size, 0);
9
9
  const names = files.slice(0, 3).map(file => file.path).join(", ");
10
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`;
11
+ return `These files would be checked in ${path} (${files.length} files, ${Math.ceil(bytes / 1000)} KB):\n ${names}${more}\n`;
12
12
  }
13
13
  export function quoteText(quote) {
14
14
  if (quote.band === null)
@@ -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
+ }
@@ -0,0 +1,36 @@
1
+ import { ApiError } from "./api.js";
2
+ import { readCredentials, saveCredentials } from "./credentials.js";
3
+ async function pollSignIn(ctx, device) {
4
+ const until = ctx.now() + device.expiresIn * 1000;
5
+ while (ctx.now() < until) {
6
+ await ctx.sleep(device.interval * 1000);
7
+ const poll = await ctx.api.request("device/token", { deviceCode: device.deviceCode }, false);
8
+ if (poll.status === "ok") {
9
+ await saveCredentials(ctx.homeDir, ctx.api.baseUrl, poll.token);
10
+ return "signed_in";
11
+ }
12
+ if (poll.status === "expired")
13
+ break;
14
+ if (poll.status === "denied")
15
+ throw new Error("Sign-in was cancelled. Run counterfact-labs login to try again.");
16
+ }
17
+ throw new Error("The sign-in code expired. Run counterfact-labs login to try again.");
18
+ }
19
+ export async function startSignIn(ctx) {
20
+ const device = await ctx.api.request("device", {}, false);
21
+ return { userCode: device.userCode, verificationUri: device.verificationUri,
22
+ pageUrl: `${device.verificationUri}?code=${encodeURIComponent(device.userCode)}`, done: pollSignIn(ctx, device) };
23
+ }
24
+ export async function signInStatus(ctx) {
25
+ if (!await readCredentials(ctx.homeDir, ctx.api.baseUrl))
26
+ return { signedIn: false };
27
+ try {
28
+ await ctx.api.request("balance");
29
+ return { signedIn: true };
30
+ }
31
+ catch (error) {
32
+ if (error instanceof ApiError && error.status === 401)
33
+ return { signedIn: false };
34
+ throw error;
35
+ }
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@counterfact-labs/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "license": "UNLICENSED",
5
5
  "description": "Check an agent from your terminal or coding agent",
6
6
  "type": "module",