crosscheck-cli 0.0.6
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/LICENSE.md +18 -0
- package/README.md +77 -0
- package/dist/activate.js +196 -0
- package/dist/activate.js.map +1 -0
- package/dist/cli.js +186 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +45 -0
- package/dist/config.js.map +1 -0
- package/dist/fingerprint.js +92 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/heartbeat.js +88 -0
- package/dist/heartbeat.js.map +1 -0
- package/dist/install.js +86 -0
- package/dist/install.js.map +1 -0
- package/dist/jwt.js +68 -0
- package/dist/jwt.js.map +1 -0
- package/dist/keychain.js +34 -0
- package/dist/keychain.js.map +1 -0
- package/dist/keys-browser-flow.js +218 -0
- package/dist/keys-browser-flow.js.map +1 -0
- package/dist/keys-setup.js +104 -0
- package/dist/keys-setup.js.map +1 -0
- package/dist/mcp-register.js +74 -0
- package/dist/mcp-register.js.map +1 -0
- package/dist/mcp.js +117 -0
- package/dist/mcp.js.map +1 -0
- package/dist/open-browser.js +25 -0
- package/dist/open-browser.js.map +1 -0
- package/dist/signing.js +37 -0
- package/dist/signing.js.map +1 -0
- package/dist/telemetry.js +80 -0
- package/dist/telemetry.js.map +1 -0
- package/package.json +41 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Crosscheck (XC) CLI License
|
|
2
|
+
|
|
3
|
+
**Copyright © 2026 Vardr Partners, LLC.** All rights reserved.
|
|
4
|
+
|
|
5
|
+
This package, the **Crosscheck (XC) CLI** (the "Software"), is proprietary software of Vardr Partners, LLC. Use is governed by the Crosscheck (XC) End User License Agreement available at https://vardrpartners.com/products/crosscheck/legal/eula (the "EULA"), which you accepted during the install flow. The EULA, and not this file, defines the actual scope of your rights.
|
|
6
|
+
|
|
7
|
+
Notable terms (summary only — the EULA controls):
|
|
8
|
+
|
|
9
|
+
- The Software runs locally on your machine and communicates with vardrpartners.com solely for activation, heartbeat, and signed content-free telemetry.
|
|
10
|
+
- Upstream LLM provider API keys you configure live only in your OS keychain. The Software does not transmit them to Vardr.
|
|
11
|
+
- The Software embeds and distributes portions of `crosscheck-agent`, licensed to Vardr Partners, LLC by Frank Speiser under a separately-executed revocable reseller license.
|
|
12
|
+
- Vardr Partners, LLC provides the Software AS-IS, without warranty of any kind. Vardr expressly disclaims any warranty that the Software will reduce token consumption, improve model output quality, or accelerate any task.
|
|
13
|
+
- You may not copy, modify, reverse engineer, decompile, or redistribute the Software except as expressly permitted by the EULA.
|
|
14
|
+
- This license may be terminated at Vardr's discretion on thirty (30) days' notice, or immediately if the underlying `crosscheck-agent` license is revoked.
|
|
15
|
+
|
|
16
|
+
For the complete and authoritative terms, see the EULA.
|
|
17
|
+
|
|
18
|
+
For questions: crosscheck@vardrpartners.com.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# crosscheck-cli
|
|
2
|
+
|
|
3
|
+
The Crosscheck multi-LLM orchestration **Model Context Protocol (MCP) server**, packaged as a single-machine-activatable CLI. Runs on your laptop, fans structured patterns (`confer`, `debate`, `audit`, `plan`, `panel_pick`, …) across multiple LLM providers using **your own API keys**, and emits signed, content-free telemetry so your dashboard at https://crosscheckagent.com/account can show usage. No Python required — the engine ([`crosscheck-mcp`](https://www.npmjs.com/package/crosscheck-mcp)) is pure TypeScript.
|
|
4
|
+
|
|
5
|
+
## Install (Claude Code in VS Code — recommended)
|
|
6
|
+
|
|
7
|
+
In the Claude Code chat, just say:
|
|
8
|
+
|
|
9
|
+
> **Install Crosscheck**
|
|
10
|
+
|
|
11
|
+
Claude Code will run the one-liner below for you. Or run it yourself in any terminal:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx -y crosscheck-cli@latest install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
That single command:
|
|
18
|
+
|
|
19
|
+
1. **Activates this machine** — opens your browser to sign in, accept the agreement, and subscribe (device-authorization flow, like `gh auth login`). No tokens to copy and paste.
|
|
20
|
+
2. **Sets up your provider keys** — Anthropic, OpenAI, Google (Gemini), xAI (Grok). Browser, terminal, or import from your environment. Keys are stored in your **OS keychain** and never sent to Crosscheck.
|
|
21
|
+
3. **Registers the MCP server with Claude Code** at user scope (`claude mcp add`, with a `~/.claude.json` fallback) — available in every workspace.
|
|
22
|
+
|
|
23
|
+
Then reconnect MCP servers in Claude Code (or restart it) and ask it to *"use crosscheck to confer on …"*.
|
|
24
|
+
|
|
25
|
+
Requires Node 20+.
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
crosscheck install # set everything up (run this first)
|
|
31
|
+
crosscheck activate # (re)authorize this machine via the browser
|
|
32
|
+
crosscheck keys setup # interactive wizard for all provider keys
|
|
33
|
+
crosscheck keys add <provider> [--paste] # add one key (browser by default; --paste for CI)
|
|
34
|
+
crosscheck keys list # list stored provider key names (not values)
|
|
35
|
+
crosscheck keys remove <provider> # remove a key
|
|
36
|
+
crosscheck heartbeat # check license status against the server
|
|
37
|
+
crosscheck serve # start the MCP server (stdio) — Claude Code runs this
|
|
38
|
+
crosscheck deactivate # wipe local credentials
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Providers: `anthropic`, `openai`, `gemini`, `grok` (also `groq`, `deepseek`, `mistral`).
|
|
42
|
+
|
|
43
|
+
## What we never see
|
|
44
|
+
|
|
45
|
+
The Crosscheck server **never** receives the content of your prompts/responses or your upstream LLM API keys.
|
|
46
|
+
|
|
47
|
+
What the server does see (and shows on your dashboard):
|
|
48
|
+
- Token counts per call
|
|
49
|
+
- Estimated cost (re-computed server-side from a public price table)
|
|
50
|
+
- Provider, model id, pattern name
|
|
51
|
+
- Latency, success/error status
|
|
52
|
+
|
|
53
|
+
Telemetry events are HMAC-signed with a per-seat key derived at activation time. Any event carrying a `prompt`, `messages`, `content`, `input`, `text`, `completion`, `response`, or `output` field is rejected at the server boundary.
|
|
54
|
+
|
|
55
|
+
## Architecture
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
Customer machine crosscheckagent.com
|
|
59
|
+
───────────────── ─────────────────────────────
|
|
60
|
+
crosscheck CLI <-- HTTPS --> /api/crosscheck/cli/device-code
|
|
61
|
+
OS keychain /api/crosscheck/cli/token
|
|
62
|
+
upstream LLM keys /api/crosscheck/heartbeat
|
|
63
|
+
Ed25519 JWT /api/crosscheck/telemetry
|
|
64
|
+
seat HMAC key
|
|
65
|
+
└─ spawns crosscheck-mcp (stdio) ← the TypeScript engine (npm dependency)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The CLI:
|
|
69
|
+
1. Activates against the server (browser device-auth → Ed25519 JWT + per-seat HMAC key, stored in the OS keychain).
|
|
70
|
+
2. Stores upstream LLM keys in the OS keychain (Keychain / Credential Manager / libsecret).
|
|
71
|
+
3. Heartbeats every 24h; refreshes the JWT and honors revocation.
|
|
72
|
+
4. Signs and POSTs content-free telemetry on each LLM call.
|
|
73
|
+
5. Spawns `crosscheck-mcp` locally, injecting your keys as env vars, and lets the MCP client talk to it over stdio.
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
See [LICENSE.md](LICENSE.md) and the canonical EULA at https://crosscheckagent.com/legal/eula.
|
package/dist/activate.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activation. Two paths:
|
|
3
|
+
*
|
|
4
|
+
* 1. Device-authorization grant (RFC 8628) — new default. Run with no
|
|
5
|
+
* argument: `crosscheck activate`
|
|
6
|
+
* Prints a URL + short user-code; polls /api/crosscheck/cli/token
|
|
7
|
+
* until the user authorizes in their browser; stores the resulting
|
|
8
|
+
* JWT + signing keys in the OS keychain. This is what `gh auth
|
|
9
|
+
* login` / `vercel login` do; eliminates the "paste a 256-char
|
|
10
|
+
* base64 token into your terminal" friction wall.
|
|
11
|
+
*
|
|
12
|
+
* 2. Legacy single-use token paste — kept alive for backwards
|
|
13
|
+
* compatibility with the old install flow:
|
|
14
|
+
* `crosscheck activate <TOKEN>`
|
|
15
|
+
* Will be removed in a future release.
|
|
16
|
+
*
|
|
17
|
+
* Both paths end with the same artifacts in the keychain:
|
|
18
|
+
* - jwt (Ed25519-signed activation JWT)
|
|
19
|
+
* - jwtJti (revocation handle)
|
|
20
|
+
* - jwtPublicKey (key used to verify the JWT — comes from server)
|
|
21
|
+
* - hmacSigningKey (per-seat key for signing telemetry events)
|
|
22
|
+
*/
|
|
23
|
+
import { request } from "undici";
|
|
24
|
+
import { SERVER_URL } from "./config.js";
|
|
25
|
+
import { setSecret } from "./keychain.js";
|
|
26
|
+
import { openBrowser } from "./open-browser.js";
|
|
27
|
+
import { computeFingerprintHash, detectPlatform, getOrCreateInstallUuid, } from "./fingerprint.js";
|
|
28
|
+
/** Persist the activation artifacts to the OS keychain. */
|
|
29
|
+
async function persistActivation(s) {
|
|
30
|
+
await setSecret("jwt", s.jwt);
|
|
31
|
+
await setSecret("hmacSigningKey", s.hmacSigningKey);
|
|
32
|
+
await setSecret("jwtJti", s.jwtJti);
|
|
33
|
+
if (s.jwtPublicKey) {
|
|
34
|
+
await setSecret("jwtPublicKey", s.jwtPublicKey);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Public entry point. With no token, runs the device flow. */
|
|
38
|
+
export async function activate(token) {
|
|
39
|
+
if (token && token.length >= 16) {
|
|
40
|
+
return activateLegacy(token);
|
|
41
|
+
}
|
|
42
|
+
return activateDeviceFlow();
|
|
43
|
+
}
|
|
44
|
+
/** Legacy: paste-a-token activation. Kept for old install scripts. */
|
|
45
|
+
async function activateLegacy(token) {
|
|
46
|
+
const fingerprintHash = await computeFingerprintHash();
|
|
47
|
+
const installUuid = await getOrCreateInstallUuid();
|
|
48
|
+
const osPlatform = detectPlatform();
|
|
49
|
+
const res = await request(`${SERVER_URL}/api/crosscheck/activate`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "content-type": "application/json" },
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
activationToken: token,
|
|
54
|
+
fingerprintHash,
|
|
55
|
+
installUuid,
|
|
56
|
+
osPlatform,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
const body = (await res.body.json());
|
|
60
|
+
if (res.statusCode !== 200 || !("ok" in body) || !body.ok) {
|
|
61
|
+
const error = "ok" in body && !body.ok
|
|
62
|
+
? `${body.error}${body.reason ? ` (${body.reason})` : ""}`
|
|
63
|
+
: `HTTP ${res.statusCode}`;
|
|
64
|
+
throw new Error(`Activation failed: ${error}`);
|
|
65
|
+
}
|
|
66
|
+
await persistActivation(body);
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
console.log(`✓ Activated. JWT expires ${new Date(body.expiresAt).toLocaleString()}.`);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Device-flow activation. Prints a URL + user-code, polls until the user
|
|
72
|
+
* authorizes in their browser, then stores artifacts in the keychain.
|
|
73
|
+
*
|
|
74
|
+
* Polling resilience (per Matt's panel review): network errors retry with
|
|
75
|
+
* backoff and don't terminate the flow. Only `expired`, `denied`, and
|
|
76
|
+
* `already_consumed` are terminal.
|
|
77
|
+
*/
|
|
78
|
+
async function activateDeviceFlow() {
|
|
79
|
+
// 1. Request a device code pair from the server.
|
|
80
|
+
const startRes = await request(`${SERVER_URL}/api/crosscheck/cli/device-code`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { "content-type": "application/json" },
|
|
83
|
+
body: JSON.stringify({}),
|
|
84
|
+
});
|
|
85
|
+
const start = (await startRes.body.json());
|
|
86
|
+
if (startRes.statusCode !== 200 || !("ok" in start) || !start.ok) {
|
|
87
|
+
const err = "error" in start ? start.error : `HTTP ${startRes.statusCode}`;
|
|
88
|
+
throw new Error(`Could not start activation: ${err}`);
|
|
89
|
+
}
|
|
90
|
+
// 2. Print instructions and auto-open the browser (like `gh auth login`).
|
|
91
|
+
const verifyUrl = `${start.verificationUrl}?code=${start.userCode}`;
|
|
92
|
+
printActivationPrompt(start.userCode, start.verificationUrl, start.expiresIn);
|
|
93
|
+
openBrowser(verifyUrl);
|
|
94
|
+
// Compute the machine fingerprint once and send it on every poll so the
|
|
95
|
+
// device-flow activation carries the same identity as the legacy flow
|
|
96
|
+
// (enables revocation + the server's seat-in-use guard).
|
|
97
|
+
const fingerprintHash = await computeFingerprintHash();
|
|
98
|
+
const installUuid = await getOrCreateInstallUuid();
|
|
99
|
+
const osPlatform = detectPlatform();
|
|
100
|
+
// 3. Poll for the token.
|
|
101
|
+
const intervalMs = Math.max(2_000, start.interval * 1000);
|
|
102
|
+
const deadlineMs = Date.now() + start.expiresIn * 1000;
|
|
103
|
+
let lastStatus = "pending";
|
|
104
|
+
// Backoff for transient network errors only — capped so polling stays
|
|
105
|
+
// responsive once the network recovers.
|
|
106
|
+
let backoffMs = intervalMs;
|
|
107
|
+
while (Date.now() < deadlineMs) {
|
|
108
|
+
await sleep(backoffMs);
|
|
109
|
+
backoffMs = intervalMs; // reset; only widened by network failures below
|
|
110
|
+
let res;
|
|
111
|
+
try {
|
|
112
|
+
res = await request(`${SERVER_URL}/api/crosscheck/cli/token`, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: { "content-type": "application/json" },
|
|
115
|
+
body: JSON.stringify({
|
|
116
|
+
deviceCode: start.deviceCode,
|
|
117
|
+
fingerprintHash,
|
|
118
|
+
installUuid,
|
|
119
|
+
osPlatform,
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
// Transient — back off and retry.
|
|
125
|
+
backoffMs = Math.min(intervalMs * 4, 30_000);
|
|
126
|
+
process.stderr.write(` (network glitch: ${err instanceof Error ? err.message : "unknown"}; retrying)\n`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const body = (await res.body.json());
|
|
130
|
+
if (body.ok && body.status === "ok") {
|
|
131
|
+
await persistActivation(body);
|
|
132
|
+
// eslint-disable-next-line no-console
|
|
133
|
+
console.log(`\n✓ Activated. JWT expires ${new Date(body.expiresAt).toLocaleString()}.`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!body.ok) {
|
|
137
|
+
const s = "status" in body ? body.status : "unknown";
|
|
138
|
+
if (s === "pending") {
|
|
139
|
+
// Show a heartbeat so the user knows we're alive. One dot per poll.
|
|
140
|
+
if (s !== lastStatus) {
|
|
141
|
+
process.stderr.write("Waiting for authorization");
|
|
142
|
+
}
|
|
143
|
+
process.stderr.write(".");
|
|
144
|
+
lastStatus = s;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (s === "seat_in_use") {
|
|
148
|
+
process.stderr.write("\n");
|
|
149
|
+
throw new Error("This seat is already active on another machine. Deactivate it at " +
|
|
150
|
+
`${SERVER_URL}/account, or add a seat, then re-run \`crosscheck activate\`.`);
|
|
151
|
+
}
|
|
152
|
+
if (s === "eula_required") {
|
|
153
|
+
process.stderr.write("\n");
|
|
154
|
+
throw new Error("You need to accept the agreement first. Re-run `crosscheck activate` and complete the browser step.");
|
|
155
|
+
}
|
|
156
|
+
if (s === "expired" || s === "denied" || s === "already_consumed") {
|
|
157
|
+
process.stderr.write("\n");
|
|
158
|
+
throw new Error(`Activation aborted: ${s}. Re-run \`crosscheck activate\` to try again.`);
|
|
159
|
+
}
|
|
160
|
+
if (s === "rate_limited") {
|
|
161
|
+
// Server told us to back off — respect it.
|
|
162
|
+
backoffMs = Math.min(intervalMs * 4, 30_000);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
// Unknown status — log and keep polling rather than crash.
|
|
166
|
+
process.stderr.write(`\n (unexpected status: ${s}; continuing)\n`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
process.stderr.write("\n");
|
|
170
|
+
throw new Error("Activation timed out. Re-run `crosscheck activate` to get a fresh code.");
|
|
171
|
+
}
|
|
172
|
+
function printActivationPrompt(userCode, verificationUrl, expiresIn) {
|
|
173
|
+
const minutes = Math.round(expiresIn / 60);
|
|
174
|
+
// eslint-disable-next-line no-console
|
|
175
|
+
console.log("");
|
|
176
|
+
// eslint-disable-next-line no-console
|
|
177
|
+
console.log(" Open this URL in your browser:");
|
|
178
|
+
// eslint-disable-next-line no-console
|
|
179
|
+
console.log(` ${verificationUrl}?code=${userCode}`);
|
|
180
|
+
// eslint-disable-next-line no-console
|
|
181
|
+
console.log("");
|
|
182
|
+
// eslint-disable-next-line no-console
|
|
183
|
+
console.log(" Or type this code:");
|
|
184
|
+
// eslint-disable-next-line no-console
|
|
185
|
+
console.log(` ${userCode}`);
|
|
186
|
+
// eslint-disable-next-line no-console
|
|
187
|
+
console.log("");
|
|
188
|
+
// eslint-disable-next-line no-console
|
|
189
|
+
console.log(` Code expires in ${minutes} minutes. Press Ctrl-C to cancel.`);
|
|
190
|
+
// eslint-disable-next-line no-console
|
|
191
|
+
console.log("");
|
|
192
|
+
}
|
|
193
|
+
function sleep(ms) {
|
|
194
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=activate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activate.js","sourceRoot":"","sources":["../src/activate.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Crosscheck (XC) CLI entry point. Minimal argument parser — subcommand and
|
|
4
|
+
* positional/named args. We don't pull in a heavyweight CLI library in the
|
|
5
|
+
* skeleton; if richer help text or interactive flows are needed later,
|
|
6
|
+
* commander or yargs slots in here.
|
|
7
|
+
*/
|
|
8
|
+
import { activate } from "./activate.js";
|
|
9
|
+
import { runHeartbeat } from "./heartbeat.js";
|
|
10
|
+
import { deleteSecret, getSecret, listSecrets, setSecret, } from "./keychain.js";
|
|
11
|
+
import { captureKeyViaBrowser } from "./keys-browser-flow.js";
|
|
12
|
+
import { runKeysSetup } from "./keys-setup.js";
|
|
13
|
+
import { runInstall } from "./install.js";
|
|
14
|
+
import { startMcpServer } from "./mcp.js";
|
|
15
|
+
const HELP = `crosscheck — multi-LLM orchestration MCP for the terminal
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
crosscheck install Set it all up: activate, keys, and register with
|
|
19
|
+
Claude Code. Run this first (or just ask Claude
|
|
20
|
+
Code to "Install Crosscheck").
|
|
21
|
+
crosscheck activate Authorize this machine via your browser.
|
|
22
|
+
crosscheck activate <TOKEN> Legacy: paste-a-token activation (deprecated).
|
|
23
|
+
crosscheck login Alias for \`crosscheck activate\`.
|
|
24
|
+
crosscheck deactivate Wipe local credentials.
|
|
25
|
+
crosscheck heartbeat Check status against the server.
|
|
26
|
+
crosscheck keys setup Interactive wizard for all provider keys.
|
|
27
|
+
crosscheck keys add <provider> [--paste] Store one upstream LLM key in your OS keychain
|
|
28
|
+
(default: opens a browser form; --paste falls
|
|
29
|
+
back to stdin for CI / headless environments).
|
|
30
|
+
crosscheck keys list List stored upstream-provider key names (not values).
|
|
31
|
+
crosscheck keys remove <provider> Remove an upstream LLM key.
|
|
32
|
+
crosscheck serve Start the MCP server (stdio).
|
|
33
|
+
crosscheck help Show this help.
|
|
34
|
+
|
|
35
|
+
Providers: anthropic, openai, gemini, grok
|
|
36
|
+
|
|
37
|
+
Documentation: https://crosscheckagent.com/install
|
|
38
|
+
`;
|
|
39
|
+
async function readStdin() {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
let buf = "";
|
|
42
|
+
process.stdin.setEncoding("utf8");
|
|
43
|
+
process.stdin.on("data", (c) => (buf += c));
|
|
44
|
+
process.stdin.on("end", () => resolve(buf.trim()));
|
|
45
|
+
process.stdin.on("error", reject);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function main() {
|
|
49
|
+
const [, , cmd, ...rest] = process.argv;
|
|
50
|
+
switch (cmd) {
|
|
51
|
+
case undefined:
|
|
52
|
+
case "help":
|
|
53
|
+
case "--help":
|
|
54
|
+
case "-h":
|
|
55
|
+
// eslint-disable-next-line no-console
|
|
56
|
+
console.log(HELP);
|
|
57
|
+
return;
|
|
58
|
+
case "install": {
|
|
59
|
+
await runInstall();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
case "activate":
|
|
63
|
+
case "login": {
|
|
64
|
+
// No token argument = device-authorization grant (default).
|
|
65
|
+
// $ crosscheck activate
|
|
66
|
+
// $ crosscheck login (alias)
|
|
67
|
+
// With a token = legacy paste-the-token activation (deprecated).
|
|
68
|
+
// $ crosscheck activate <TOKEN>
|
|
69
|
+
const token = rest[0];
|
|
70
|
+
await activate(token);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
case "deactivate": {
|
|
74
|
+
const items = await listSecrets();
|
|
75
|
+
for (const item of items) {
|
|
76
|
+
await deleteSecret(item.account);
|
|
77
|
+
}
|
|
78
|
+
// eslint-disable-next-line no-console
|
|
79
|
+
console.log(`✓ Removed ${items.length} keychain entries. To set up again, run \`crosscheck install\`.`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
case "heartbeat": {
|
|
83
|
+
const r = await runHeartbeat();
|
|
84
|
+
// eslint-disable-next-line no-console
|
|
85
|
+
console.log(`status=${r.status}${r.reason ? ` reason=${r.reason}` : ""}`);
|
|
86
|
+
if (r.status === "revoked")
|
|
87
|
+
process.exit(2);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
case "keys": {
|
|
91
|
+
const sub = rest[0];
|
|
92
|
+
const provider = rest[1];
|
|
93
|
+
// --paste falls back to the legacy stdin paste flow (useful in
|
|
94
|
+
// CI/headless/SSH environments where the browser flow can't work).
|
|
95
|
+
const paste = rest.includes("--paste");
|
|
96
|
+
if (sub === "setup") {
|
|
97
|
+
await runKeysSetup();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (sub === "list") {
|
|
101
|
+
const items = (await listSecrets()).filter((i) => i.account.startsWith("key:"));
|
|
102
|
+
for (const i of items) {
|
|
103
|
+
// eslint-disable-next-line no-console
|
|
104
|
+
console.log(i.account.slice(4));
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const validProviders = [
|
|
109
|
+
"anthropic",
|
|
110
|
+
"openai",
|
|
111
|
+
"gemini",
|
|
112
|
+
"grok",
|
|
113
|
+
"groq",
|
|
114
|
+
"deepseek",
|
|
115
|
+
"mistral",
|
|
116
|
+
];
|
|
117
|
+
if (!provider || !validProviders.includes(provider)) {
|
|
118
|
+
// eslint-disable-next-line no-console
|
|
119
|
+
console.error(`usage: crosscheck keys (add|remove) (${validProviders.join("|")}) [--paste]`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
if (sub === "add") {
|
|
123
|
+
let value = "";
|
|
124
|
+
if (paste) {
|
|
125
|
+
process.stderr.write(`Paste your ${provider} API key, then press Enter+Ctrl-D:\n`);
|
|
126
|
+
value = await readStdin();
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// Default: browser flow. The CLI opens a Crosscheck (XC)-served form
|
|
130
|
+
// that POSTs the pasted key directly to a loopback HTTP server
|
|
131
|
+
// we spin up here. Value never transits Crosscheck's servers.
|
|
132
|
+
try {
|
|
133
|
+
const captured = await captureKeyViaBrowser({ provider });
|
|
134
|
+
value = captured.value;
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
// eslint-disable-next-line no-console
|
|
138
|
+
console.error(`✗ ${err instanceof Error ? err.message : err}\n To use the legacy paste-into-stdin flow, run \`crosscheck keys add ${provider} --paste\`.`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!value) {
|
|
143
|
+
// eslint-disable-next-line no-console
|
|
144
|
+
console.error("✗ no key entered");
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
await setSecret(`key:${provider}`, value);
|
|
148
|
+
// eslint-disable-next-line no-console
|
|
149
|
+
console.log(`✓ Stored ${provider} key in OS keychain (never sent to Crosscheck).`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (sub === "remove") {
|
|
153
|
+
const ok = await deleteSecret(`key:${provider}`);
|
|
154
|
+
// eslint-disable-next-line no-console
|
|
155
|
+
console.log(ok ? `✓ Removed ${provider} key.` : `(no ${provider} key was stored)`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// eslint-disable-next-line no-console
|
|
159
|
+
console.error("usage: crosscheck keys (add|remove|list) [provider]");
|
|
160
|
+
process.exit(1);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
case "serve": {
|
|
164
|
+
const jwt = await getSecret("jwt");
|
|
165
|
+
if (!jwt) {
|
|
166
|
+
// eslint-disable-next-line no-console
|
|
167
|
+
console.error("Not activated. Run `crosscheck install` first.");
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
await startMcpServer();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
default:
|
|
174
|
+
// eslint-disable-next-line no-console
|
|
175
|
+
console.error(`Unknown command: ${cmd}\n`);
|
|
176
|
+
// eslint-disable-next-line no-console
|
|
177
|
+
console.log(HELP);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
main().catch((err) => {
|
|
182
|
+
// eslint-disable-next-line no-console
|
|
183
|
+
console.error(err instanceof Error ? err.message : err);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
});
|
|
186
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time constants. Override the server base URL via the
|
|
3
|
+
* CROSSCHECK_SERVER environment variable for local development against a
|
|
4
|
+
* preview deployment. The Ed25519 public key is embedded at build time so
|
|
5
|
+
* the CLI can verify JWTs offline.
|
|
6
|
+
*/
|
|
7
|
+
export const SERVER_URL = process.env.CROSSCHECK_SERVER || "https://crosscheckagent.com";
|
|
8
|
+
/** npm package name of this CLI — used when self-registering the MCP server. */
|
|
9
|
+
export const CLI_PACKAGE = "crosscheck-cli";
|
|
10
|
+
/**
|
|
11
|
+
* npm package name of the TypeScript MCP engine the CLI runs. The CLI is the
|
|
12
|
+
* license/onboarding wrapper; crosscheck-mcp is the engine (formerly a bundled
|
|
13
|
+
* Python server — now pure TypeScript, shipped as a normal npm dependency).
|
|
14
|
+
*/
|
|
15
|
+
export const ENGINE_PACKAGE = "crosscheck-mcp";
|
|
16
|
+
/**
|
|
17
|
+
* Ed25519 public key (PEM, SPKI) used to verify activation JWTs. This is
|
|
18
|
+
* replaced at build time by the publish pipeline with the public half of
|
|
19
|
+
* the keypair produced by scripts/generate-cb-secrets.ts on the server.
|
|
20
|
+
*
|
|
21
|
+
* For local dev: set CROSSCHECK_JWT_PUBLIC_KEY in the environment.
|
|
22
|
+
*/
|
|
23
|
+
export const EMBEDDED_PUBLIC_KEY_PEM = process.env.CROSSCHECK_JWT_PUBLIC_KEY ?? "";
|
|
24
|
+
/**
|
|
25
|
+
* OS keychain service identifier. Stored entries:
|
|
26
|
+
* - "jwt" → the activation JWT (rotates ~weekly)
|
|
27
|
+
* - "hmacSigningKey" → per-seat HMAC key from /activate
|
|
28
|
+
* - "key:anthropic" / "key:openai" / "key:gemini" / "key:grok"
|
|
29
|
+
* → user-supplied upstream LLM keys
|
|
30
|
+
* - "installUuid" → stable UUID generated at first run
|
|
31
|
+
*/
|
|
32
|
+
export const KEYCHAIN_SERVICE = "crosscheck";
|
|
33
|
+
/**
|
|
34
|
+
* Heartbeat cadence. Matches the server's 24h refreshAt / 7d exp / 72h
|
|
35
|
+
* fail-open grace. Configurable for tests.
|
|
36
|
+
*/
|
|
37
|
+
export const HEARTBEAT_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
38
|
+
export const HEARTBEAT_FAIL_OPEN_MS = 72 * 60 * 60 * 1000;
|
|
39
|
+
/**
|
|
40
|
+
* Telemetry batch flush. Events accumulate in memory and POST in batches
|
|
41
|
+
* up to 100 events or 30 seconds, whichever comes first.
|
|
42
|
+
*/
|
|
43
|
+
export const TELEMETRY_BATCH_SIZE = 50;
|
|
44
|
+
export const TELEMETRY_FLUSH_MS = 30_000;
|
|
45
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,UAAU,GACrB,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,6BAA6B,CAAC;AAEjE,gFAAgF;AAChF,MAAM,CAAC,MAAM,WAAW,GAAG,gBAAgB,CAAC;AAE5C;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,gBAAgB,CAAC;AAE/C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,OAAO,CAAC,GAAG,CAAC,yBAAyB,IAAI,EAAE,CAAC;AAEnF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,YAAY,CAAC;AAE7C;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AACzD,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE1D;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC;AACvC,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Privacy-preserving machine fingerprint. Argon2id-hashed:
|
|
3
|
+
*
|
|
4
|
+
* fingerprint = argon2id(machine-id || install-uuid || os-platform)
|
|
5
|
+
*
|
|
6
|
+
* The same fingerprint inputs always produce the same hash (deterministic
|
|
7
|
+
* salt). The hash is not reversible to PII — it's only useful as a stable
|
|
8
|
+
* identifier for re-activation flows. NEVER raw MAC address, serial number,
|
|
9
|
+
* or hardware UUID — those are PII and unstable across OS updates.
|
|
10
|
+
*
|
|
11
|
+
* The `installUuid` is generated once at first run and stored in the OS
|
|
12
|
+
* keychain so subsequent invocations on the same install yield the same
|
|
13
|
+
* fingerprint even if the machine-id rotates.
|
|
14
|
+
*/
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
17
|
+
import { execSync } from "node:child_process";
|
|
18
|
+
import * as argon2 from "argon2";
|
|
19
|
+
import { getSecret, setSecret } from "./keychain.js";
|
|
20
|
+
const INSTALL_UUID_KEY = "installUuid";
|
|
21
|
+
const FIXED_SALT = Buffer.from("vardr.crosscheck-fp-v1-salt-2026", "utf8");
|
|
22
|
+
export function detectPlatform() {
|
|
23
|
+
const p = process.platform;
|
|
24
|
+
if (p === "darwin" || p === "linux" || p === "win32")
|
|
25
|
+
return p;
|
|
26
|
+
return "other";
|
|
27
|
+
}
|
|
28
|
+
export async function getOrCreateInstallUuid() {
|
|
29
|
+
const existing = await getSecret(INSTALL_UUID_KEY);
|
|
30
|
+
if (existing)
|
|
31
|
+
return existing;
|
|
32
|
+
const fresh = randomUUID();
|
|
33
|
+
await setSecret(INSTALL_UUID_KEY, fresh);
|
|
34
|
+
return fresh;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Best-effort machine identifier. Falls back to a stable hash of OS +
|
|
38
|
+
* hostname if a true machine-id is unavailable.
|
|
39
|
+
*/
|
|
40
|
+
export function getMachineId() {
|
|
41
|
+
const platform = detectPlatform();
|
|
42
|
+
try {
|
|
43
|
+
if (platform === "darwin") {
|
|
44
|
+
const out = execSync("ioreg -d2 -c IOPlatformExpertDevice", {
|
|
45
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
46
|
+
}).toString();
|
|
47
|
+
const m = out.match(/IOPlatformUUID"\s*=\s*"([^"]+)"/);
|
|
48
|
+
if (m && m[1])
|
|
49
|
+
return `darwin:${m[1]}`;
|
|
50
|
+
}
|
|
51
|
+
else if (platform === "linux") {
|
|
52
|
+
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
53
|
+
if (existsSync(p))
|
|
54
|
+
return `linux:${readFileSync(p, "utf8").trim()}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else if (platform === "win32") {
|
|
58
|
+
const out = execSync("reg query HKLM\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid", { stdio: ["ignore", "pipe", "ignore"] }).toString();
|
|
59
|
+
const m = out.match(/MachineGuid\s+REG_SZ\s+([0-9a-fA-F-]+)/);
|
|
60
|
+
if (m && m[1])
|
|
61
|
+
return `win32:${m[1]}`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Fall through to a stable-enough fallback.
|
|
66
|
+
}
|
|
67
|
+
// Last-resort: not unique across machines but stable per-host.
|
|
68
|
+
return `${platform}:${process.env.HOSTNAME ?? "unknown"}`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Compute the fingerprint hash. Argon2id with the fixed salt so the server
|
|
72
|
+
* doesn't need to know the inputs to verify.
|
|
73
|
+
*/
|
|
74
|
+
export async function computeFingerprintHash() {
|
|
75
|
+
const machineId = getMachineId();
|
|
76
|
+
const installUuid = await getOrCreateInstallUuid();
|
|
77
|
+
const platform = detectPlatform();
|
|
78
|
+
const input = `${machineId}|${installUuid}|${platform}`;
|
|
79
|
+
// Argon2id with modest parameters — fingerprinting is one-shot at install
|
|
80
|
+
// time so the cost only matters once.
|
|
81
|
+
const hash = await argon2.hash(input, {
|
|
82
|
+
type: argon2.argon2id,
|
|
83
|
+
salt: FIXED_SALT,
|
|
84
|
+
timeCost: 3,
|
|
85
|
+
memoryCost: 32 * 1024,
|
|
86
|
+
parallelism: 1,
|
|
87
|
+
raw: true,
|
|
88
|
+
hashLength: 32,
|
|
89
|
+
});
|
|
90
|
+
return Buffer.from(hash).toString("hex");
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=fingerprint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../src/fingerprint.ts"],"names":[],"mappings":"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"}
|