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
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-register the crosscheck MCP server with Claude Code so the user never
|
|
3
|
+
* hand-edits JSON.
|
|
4
|
+
*
|
|
5
|
+
* Primary path: the `claude` CLI — `claude mcp add --scope user crosscheck --
|
|
6
|
+
* npx -y crosscheck-cli@latest serve`. User scope means it's available in
|
|
7
|
+
* every workspace, which matches a per-seat license.
|
|
8
|
+
*
|
|
9
|
+
* Fallback: if the `claude` CLI isn't on PATH, merge-write the user's
|
|
10
|
+
* ~/.claude.json `mcpServers` map in place — never clobbering existing
|
|
11
|
+
* servers or other settings.
|
|
12
|
+
*/
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { CLI_PACKAGE } from "./config.js";
|
|
18
|
+
const SERVER_NAME = "crosscheck";
|
|
19
|
+
const SERVER_COMMAND = "npx";
|
|
20
|
+
const SERVER_ARGS = ["-y", `${CLI_PACKAGE}@latest`, "serve"];
|
|
21
|
+
function claudeCliAvailable() {
|
|
22
|
+
try {
|
|
23
|
+
const r = spawnSync("claude", ["--version"], { stdio: "ignore" });
|
|
24
|
+
return r.status === 0;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function mcpJsonSnippet() {
|
|
31
|
+
return JSON.stringify({
|
|
32
|
+
mcpServers: {
|
|
33
|
+
[SERVER_NAME]: { command: SERVER_COMMAND, args: SERVER_ARGS },
|
|
34
|
+
},
|
|
35
|
+
}, null, 2);
|
|
36
|
+
}
|
|
37
|
+
/** Merge our server into ~/.claude.json without disturbing anything else. */
|
|
38
|
+
function writeUserConfig() {
|
|
39
|
+
const configPath = path.join(os.homedir(), ".claude.json");
|
|
40
|
+
try {
|
|
41
|
+
let config = {};
|
|
42
|
+
if (existsSync(configPath)) {
|
|
43
|
+
const raw = readFileSync(configPath, "utf8").trim();
|
|
44
|
+
if (raw)
|
|
45
|
+
config = JSON.parse(raw);
|
|
46
|
+
}
|
|
47
|
+
const servers = config.mcpServers ?? {};
|
|
48
|
+
servers[SERVER_NAME] = { command: SERVER_COMMAND, args: SERVER_ARGS };
|
|
49
|
+
config.mcpServers = servers;
|
|
50
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
51
|
+
return { ok: true, configPath };
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
return { ok: false, reason: err instanceof Error ? err.message : "unknown error" };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function registerMcpServer() {
|
|
58
|
+
if (claudeCliAvailable()) {
|
|
59
|
+
const r = spawnSync("claude", ["mcp", "add", "--scope", "user", SERVER_NAME, "--", SERVER_COMMAND, ...SERVER_ARGS], { stdio: "ignore" });
|
|
60
|
+
if (r.status === 0)
|
|
61
|
+
return { method: "cli" };
|
|
62
|
+
// `claude` exists but the add failed (e.g. already registered) — fall
|
|
63
|
+
// through to the file path, which is idempotent.
|
|
64
|
+
}
|
|
65
|
+
const written = writeUserConfig();
|
|
66
|
+
if (written.ok)
|
|
67
|
+
return { method: "file", configPath: written.configPath };
|
|
68
|
+
return {
|
|
69
|
+
method: "manual",
|
|
70
|
+
snippet: mcpJsonSnippet(),
|
|
71
|
+
reason: written.reason,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=mcp-register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-register.js","sourceRoot":"","sources":["../src/mcp-register.ts"],"names":[],"mappings":"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"}
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server façade. Spawns the TypeScript crosscheck-mcp engine as a
|
|
3
|
+
* subprocess and lets the IDE's MCP stdio JSON-RPC traffic flow through it.
|
|
4
|
+
*
|
|
5
|
+
* (Previously this bundled and spawned a Python server. crosscheck-mcp is now
|
|
6
|
+
* pure TypeScript and ships as a normal npm dependency — no Python required.)
|
|
7
|
+
*
|
|
8
|
+
* Pre-flight (before spawning the child):
|
|
9
|
+
* 1. Verify the activation JWT from the OS keychain — refuse to serve if
|
|
10
|
+
* missing, expired, or revoked.
|
|
11
|
+
* 2. Resolve the crosscheck-mcp engine entry from node_modules (falling back
|
|
12
|
+
* to `npx -y crosscheck-mcp` if it isn't installed alongside).
|
|
13
|
+
* 3. Pull every upstream LLM key from the keychain into the child's env
|
|
14
|
+
* (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, XAI_API_KEY,
|
|
15
|
+
* GROQ_API_KEY, DEEPSEEK_API_KEY, MISTRAL_API_KEY). The customer never
|
|
16
|
+
* hand-manages env vars.
|
|
17
|
+
*
|
|
18
|
+
* The child inherits stdio so the IDE's MCP client talks to it directly; we
|
|
19
|
+
* don't proxy or interpret the JSON-RPC traffic. The CLI is the
|
|
20
|
+
* license/onboarding wrapper; crosscheck-mcp is the engine.
|
|
21
|
+
*/
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { createRequire } from "node:module";
|
|
26
|
+
import { ENGINE_PACKAGE, SERVER_URL } from "./config.js";
|
|
27
|
+
import { getSecret } from "./keychain.js";
|
|
28
|
+
import { verifyJwt } from "./jwt.js";
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
/** Keychain key name -> engine env var name. Mirrors crosscheck-mcp's providers. */
|
|
31
|
+
const KEY_BINDINGS = [
|
|
32
|
+
["key:anthropic", "ANTHROPIC_API_KEY"],
|
|
33
|
+
["key:openai", "OPENAI_API_KEY"],
|
|
34
|
+
["key:gemini", "GEMINI_API_KEY"],
|
|
35
|
+
["key:grok", "XAI_API_KEY"],
|
|
36
|
+
["key:groq", "GROQ_API_KEY"],
|
|
37
|
+
["key:deepseek", "DEEPSEEK_API_KEY"],
|
|
38
|
+
["key:mistral", "MISTRAL_API_KEY"],
|
|
39
|
+
];
|
|
40
|
+
async function loadUpstreamKeys() {
|
|
41
|
+
const out = {};
|
|
42
|
+
for (const [name, envVar] of KEY_BINDINGS) {
|
|
43
|
+
const v = await getSecret(name);
|
|
44
|
+
if (v)
|
|
45
|
+
out[envVar] = v;
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* How to launch the engine. Prefer a locally-installed crosscheck-mcp (fast,
|
|
51
|
+
* offline, version-pinned via our dependency). Fall back to `npx -y` so the
|
|
52
|
+
* CLI still works if the dependency tree was flattened oddly.
|
|
53
|
+
*/
|
|
54
|
+
function resolveEngineLauncher() {
|
|
55
|
+
try {
|
|
56
|
+
const pkgJsonPath = require.resolve(`${ENGINE_PACKAGE}/package.json`);
|
|
57
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
58
|
+
const binRel = typeof pkg.bin === "string"
|
|
59
|
+
? pkg.bin
|
|
60
|
+
: (pkg.bin?.[ENGINE_PACKAGE] ?? Object.values(pkg.bin ?? {})[0]);
|
|
61
|
+
if (binRel) {
|
|
62
|
+
const entry = path.join(path.dirname(pkgJsonPath), binRel);
|
|
63
|
+
// Spawn with the same Node that's running us — robust across installs.
|
|
64
|
+
return { command: process.execPath, args: [entry] };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Not resolvable locally — fall through to npx.
|
|
69
|
+
}
|
|
70
|
+
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
71
|
+
return { command: npx, args: ["-y", ENGINE_PACKAGE] };
|
|
72
|
+
}
|
|
73
|
+
export async function startMcpServer() {
|
|
74
|
+
// 1. License gate.
|
|
75
|
+
const jwt = await getSecret("jwt");
|
|
76
|
+
if (!jwt) {
|
|
77
|
+
process.stderr.write("Not activated. Run `crosscheck activate` first.\n");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
const verified = await verifyJwt(jwt);
|
|
81
|
+
if (!verified.ok) {
|
|
82
|
+
process.stderr.write(`Activation is ${verified.reason}. Re-activate by running \`crosscheck activate\` ` +
|
|
83
|
+
`(or manage devices at ${SERVER_URL}/account).\n`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
// 2. Pull upstream LLM keys from keychain into the child's env.
|
|
87
|
+
const keys = await loadUpstreamKeys();
|
|
88
|
+
if (Object.keys(keys).length === 0) {
|
|
89
|
+
process.stderr.write("No upstream LLM keys configured. Run `crosscheck keys setup` first.\n");
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
// 3. Launch the engine.
|
|
93
|
+
const { command, args } = resolveEngineLauncher();
|
|
94
|
+
const env = {
|
|
95
|
+
...process.env,
|
|
96
|
+
...keys,
|
|
97
|
+
CROSSCHECK_DATA_DIR: process.env.CROSSCHECK_DATA_DIR ||
|
|
98
|
+
path.join(process.env.HOME ?? process.env.USERPROFILE ?? "", ".crosscheck"),
|
|
99
|
+
};
|
|
100
|
+
const child = spawn(command, args, { env, stdio: "inherit" });
|
|
101
|
+
child.on("exit", (code, signal) => {
|
|
102
|
+
if (signal) {
|
|
103
|
+
process.stderr.write(`crosscheck-mcp exited on ${signal}\n`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
process.exit(code ?? 0);
|
|
107
|
+
});
|
|
108
|
+
child.on("error", (err) => {
|
|
109
|
+
process.stderr.write(`Failed to start the crosscheck engine (${ENGINE_PACKAGE}): ${err.message}\n` +
|
|
110
|
+
`Try reinstalling: npx -y crosscheck-cli@latest install\n`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
});
|
|
113
|
+
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
114
|
+
process.on(sig, () => child.kill(sig));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=mcp.js.map
|
package/dist/mcp.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort cross-platform browser opener. Failure is silent — callers
|
|
3
|
+
* always print the URL too, so a headless box just falls back to copy-paste.
|
|
4
|
+
* Kept dependency-light (no `open` package) on purpose.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
export function openBrowser(url) {
|
|
8
|
+
const opener = process.platform === "darwin"
|
|
9
|
+
? "open"
|
|
10
|
+
: process.platform === "win32"
|
|
11
|
+
? "cmd"
|
|
12
|
+
: "xdg-open";
|
|
13
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
14
|
+
try {
|
|
15
|
+
const child = spawn(opener, args, { stdio: "ignore", detached: true });
|
|
16
|
+
child.unref();
|
|
17
|
+
child.on("error", () => {
|
|
18
|
+
/* swallow — URL was printed for manual open */
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
/* ignore */
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=open-browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"open-browser.js","sourceRoot":"","sources":["../src/open-browser.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,MAAM,GACV,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAC3B,CAAC,CAAC,MAAM;QACR,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO;YAC5B,CAAC,CAAC,KAAK;YACP,CAAC,CAAC,UAAU,CAAC;IACnB,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAC7E,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACvE,KAAK,CAAC,KAAK,EAAE,CAAC;QACd,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACrB,+CAA+C;QACjD,CAAC,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,YAAY;IACd,CAAC;AACH,CAAC"}
|
package/dist/signing.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical event signing. MUST match the server's
|
|
3
|
+
* src/lib/crosscheck/crypto.ts byte-for-byte. Any drift breaks telemetry
|
|
4
|
+
* ingest because the server re-derives the seat HMAC key and recomputes
|
|
5
|
+
* the signature with the canonical body — if the strings differ, the
|
|
6
|
+
* signature won't verify.
|
|
7
|
+
*
|
|
8
|
+
* If the server adds a field to SignableEvent, this file must change in
|
|
9
|
+
* lockstep. Tests on both sides should sign + verify the same fixture to
|
|
10
|
+
* catch divergence.
|
|
11
|
+
*/
|
|
12
|
+
import crypto from "node:crypto";
|
|
13
|
+
export function canonicalEventBody(e) {
|
|
14
|
+
return [
|
|
15
|
+
e.eventId,
|
|
16
|
+
e.seatId,
|
|
17
|
+
e.orgId,
|
|
18
|
+
e.ts,
|
|
19
|
+
e.provider,
|
|
20
|
+
e.model,
|
|
21
|
+
e.pattern,
|
|
22
|
+
String(e.promptTokens),
|
|
23
|
+
String(e.completionTokens),
|
|
24
|
+
e.costUsdEstimate.toFixed(6),
|
|
25
|
+
String(e.latencyMs),
|
|
26
|
+
e.status,
|
|
27
|
+
e.errorClass ?? "",
|
|
28
|
+
].join("|");
|
|
29
|
+
}
|
|
30
|
+
/** Sign a usage event with the per-seat HMAC key obtained at activation. */
|
|
31
|
+
export function signEvent(seatSigningKey, e) {
|
|
32
|
+
return crypto
|
|
33
|
+
.createHmac("sha256", seatSigningKey)
|
|
34
|
+
.update(canonicalEventBody(e))
|
|
35
|
+
.digest("hex");
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=signing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signing.js","sourceRoot":"","sources":["../src/signing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,MAAM,MAAM,aAAa,CAAC;AAuBjC,MAAM,UAAU,kBAAkB,CAAC,CAAgB;IACjD,OAAO;QACL,CAAC,CAAC,OAAO;QACT,CAAC,CAAC,MAAM;QACR,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,QAAQ;QACV,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,OAAO;QACT,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC;QACtB,MAAM,CAAC,CAAC,CAAC,gBAAgB,CAAC;QAC1B,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;QAC5B,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;QACnB,CAAC,CAAC,MAAM;QACR,CAAC,CAAC,UAAU,IAAI,EAAE;KACnB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,SAAS,CAAC,cAAsB,EAAE,CAAgB;IAChE,OAAO,MAAM;SACV,UAAU,CAAC,QAAQ,EAAE,cAAc,CAAC;SACpC,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC;SAC7B,MAAM,CAAC,KAAK,CAAC,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry emitter. Signs each event with the per-seat HMAC key from
|
|
3
|
+
* activation and flushes batches to /api/crosscheck/telemetry on size or
|
|
4
|
+
* time threshold.
|
|
5
|
+
*
|
|
6
|
+
* Hard rule: nothing prompt-shaped goes in the event. The server's strict
|
|
7
|
+
* Zod schema rejects keys named prompt/messages/content/input/text/
|
|
8
|
+
* completion/response/output, so even an accidental log line never makes
|
|
9
|
+
* it past ingest. Don't add a "prompt_preview" field. Don't.
|
|
10
|
+
*/
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { request } from "undici";
|
|
13
|
+
import { SERVER_URL, TELEMETRY_BATCH_SIZE, TELEMETRY_FLUSH_MS, } from "./config.js";
|
|
14
|
+
import { getSecret } from "./keychain.js";
|
|
15
|
+
import { signEvent } from "./signing.js";
|
|
16
|
+
const queue = [];
|
|
17
|
+
let flushTimer = null;
|
|
18
|
+
export async function emit(args) {
|
|
19
|
+
const hmacKey = await getSecret("hmacSigningKey");
|
|
20
|
+
if (!hmacKey)
|
|
21
|
+
throw new Error("Not activated. No HMAC signing key on file.");
|
|
22
|
+
const base = {
|
|
23
|
+
eventId: randomUUID(),
|
|
24
|
+
seatId: args.seatId,
|
|
25
|
+
orgId: args.orgId,
|
|
26
|
+
ts: new Date().toISOString(),
|
|
27
|
+
provider: args.provider,
|
|
28
|
+
model: args.model,
|
|
29
|
+
pattern: args.pattern,
|
|
30
|
+
promptTokens: args.promptTokens,
|
|
31
|
+
completionTokens: args.completionTokens,
|
|
32
|
+
costUsdEstimate: args.costUsdEstimate,
|
|
33
|
+
latencyMs: args.latencyMs,
|
|
34
|
+
status: args.status,
|
|
35
|
+
errorClass: args.errorClass,
|
|
36
|
+
};
|
|
37
|
+
const signatureHmac = signEvent(hmacKey, base);
|
|
38
|
+
queue.push({ ...base, signatureHmac });
|
|
39
|
+
if (queue.length >= TELEMETRY_BATCH_SIZE) {
|
|
40
|
+
await flush();
|
|
41
|
+
}
|
|
42
|
+
else if (!flushTimer) {
|
|
43
|
+
flushTimer = setTimeout(() => void flush(), TELEMETRY_FLUSH_MS);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function flush() {
|
|
47
|
+
if (flushTimer) {
|
|
48
|
+
clearTimeout(flushTimer);
|
|
49
|
+
flushTimer = null;
|
|
50
|
+
}
|
|
51
|
+
if (queue.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
const batch = queue.splice(0, queue.length);
|
|
54
|
+
try {
|
|
55
|
+
const res = await request(`${SERVER_URL}/api/crosscheck/telemetry`, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { "content-type": "application/json" },
|
|
58
|
+
body: JSON.stringify({ events: batch }),
|
|
59
|
+
});
|
|
60
|
+
if (res.statusCode !== 200) {
|
|
61
|
+
// Re-queue on failure so we retry on next flush. Don't retry on 400 —
|
|
62
|
+
// a malformed event will fail forever; drop it and log.
|
|
63
|
+
if (res.statusCode === 400) {
|
|
64
|
+
// eslint-disable-next-line no-console
|
|
65
|
+
console.error("[telemetry] 400 from server (events dropped):");
|
|
66
|
+
// eslint-disable-next-line no-console
|
|
67
|
+
console.error(await res.body.text());
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
queue.unshift(...batch);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
queue.unshift(...batch);
|
|
76
|
+
// eslint-disable-next-line no-console
|
|
77
|
+
console.error("[telemetry] network error, re-queued:", err instanceof Error ? err.message : err);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=telemetry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "crosscheck-cli",
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"description": "Crosscheck — multi-LLM orchestration MCP for the terminal. Single-machine activation, BYO upstream LLM keys, content-free telemetry. Installs into Claude Code in one command.",
|
|
5
|
+
"license": "SEE LICENSE.md",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"crosscheck": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE.md"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"dev": "tsx src/cli.ts",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"lint": "tsc -p tsconfig.json --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
30
|
+
"argon2": "^0.41.0",
|
|
31
|
+
"crosscheck-mcp": "^0.1.0",
|
|
32
|
+
"keytar": "^7.9.0",
|
|
33
|
+
"undici": "^7.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.10.0",
|
|
37
|
+
"tsx": "^4.19.0",
|
|
38
|
+
"typescript": "^5.7.0",
|
|
39
|
+
"vitest": "^4.1.0"
|
|
40
|
+
}
|
|
41
|
+
}
|