@somacheck/vibecheck 0.2.0 → 0.3.0
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 +140 -0
- package/dist/api.js +176 -83
- package/dist/cli.js +229 -14
- package/dist/client-setup.js +206 -0
- package/dist/config.js +18 -1
- package/dist/constants.js +13 -0
- package/dist/link.js +51 -2
- package/dist/readiness.js +135 -0
- package/dist/server.js +168 -148
- package/dist/vibecheck.js +1 -64
- package/package.json +4 -3
- package/dist/api.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/constants.js.map +0 -1
- package/dist/link.js.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/vibecheck.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,35 +1,250 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
3
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
5
|
import { SupabaseAgentApi } from "./api.js";
|
|
6
|
+
import { clientDisplayName, detectInstalledClients, detectLegacyHostedRegistration, LocalCommandRunner, manualLegacyHostedRemoveCommand, manualRemoveCommand, manualSetupCommand, parseClientChoice, preflightClientPersistence, registerClient, singleNonInteractiveClientSelection, } from "./client-setup.js";
|
|
5
7
|
import { readConfig } from "./config.js";
|
|
6
8
|
import { SUPABASE_PUBLISHABLE_KEY, SUPABASE_URL } from "./constants.js";
|
|
7
|
-
import { linkAgent } from "./link.js";
|
|
9
|
+
import { LinkPersistenceError, NonInteractiveLinkError, linkAgent } from "./link.js";
|
|
10
|
+
import { checkReadiness } from "./readiness.js";
|
|
8
11
|
import { createVibecheckServer } from "./server.js";
|
|
9
12
|
const api = new SupabaseAgentApi(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY);
|
|
13
|
+
const runner = new LocalCommandRunner();
|
|
14
|
+
const output = (line) => process.stdout.write(`${line}\n`);
|
|
15
|
+
class ClientPreflightError extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "ClientPreflightError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function usage() {
|
|
22
|
+
return [
|
|
23
|
+
"Usage:",
|
|
24
|
+
" npx -y @somacheck/vibecheck link <CODE> [--client codex|claude|all|none]",
|
|
25
|
+
" npx -y @somacheck/vibecheck setup <codex|claude|all>",
|
|
26
|
+
" npx -y @somacheck/vibecheck doctor",
|
|
27
|
+
].join("\n");
|
|
28
|
+
}
|
|
29
|
+
function parseLinkClient(args) {
|
|
30
|
+
if (args.length === 0)
|
|
31
|
+
return "prompt";
|
|
32
|
+
if (args.length !== 2 || args[0] !== "--client")
|
|
33
|
+
throw new Error(usage());
|
|
34
|
+
const selection = args[1]?.toLowerCase();
|
|
35
|
+
if (selection === "codex" || selection === "claude")
|
|
36
|
+
return [selection];
|
|
37
|
+
if (selection === "all")
|
|
38
|
+
return ["codex", "claude"];
|
|
39
|
+
if (selection === "none")
|
|
40
|
+
return [];
|
|
41
|
+
throw new Error(usage());
|
|
42
|
+
}
|
|
43
|
+
function parseServeClient(args) {
|
|
44
|
+
if (args.length !== 3 || args[0] !== "serve" || args[1] !== "--client")
|
|
45
|
+
return null;
|
|
46
|
+
return args[2] === "codex" || args[2] === "claude" ? args[2] : null;
|
|
47
|
+
}
|
|
48
|
+
async function promptForClients(installed) {
|
|
49
|
+
if (installed.length === 0)
|
|
50
|
+
return [];
|
|
51
|
+
const labels = installed.map((client) => clientDisplayName(client)).join(" and ");
|
|
52
|
+
output(`Found ${labels}.`);
|
|
53
|
+
const options = installed.length === 1
|
|
54
|
+
? `Configure ${clientDisplayName(installed[0])} now? [Y/n] `
|
|
55
|
+
: "Configure [1] Codex / ChatGPT desktop, [2] Claude Code, [3] both, or [4] later: ";
|
|
56
|
+
const terminal = createInterface({ input: process.stdin, output: process.stdout });
|
|
57
|
+
try {
|
|
58
|
+
while (true) {
|
|
59
|
+
const answer = await terminal.question(options);
|
|
60
|
+
if (installed.length === 1 && (answer.trim() === "" || /^y(es)?$/i.test(answer)))
|
|
61
|
+
return installed;
|
|
62
|
+
if (installed.length === 1 && /^n(o)?$/i.test(answer))
|
|
63
|
+
return [];
|
|
64
|
+
const selected = parseClientChoice(answer, installed);
|
|
65
|
+
if (selected !== null)
|
|
66
|
+
return selected;
|
|
67
|
+
output("Choose one of the listed options.");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
terminal.close();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function configureClients(clients) {
|
|
75
|
+
for (const client of clients) {
|
|
76
|
+
const result = await registerClient(client, runner);
|
|
77
|
+
if (result.status === "registered") {
|
|
78
|
+
output(`✓ Configured ${clientDisplayName(client)}.`);
|
|
79
|
+
}
|
|
80
|
+
else if (result.status === "upgraded") {
|
|
81
|
+
output(`✓ Upgraded ${clientDisplayName(client)} from the older SomaCheck MCP registration.`);
|
|
82
|
+
}
|
|
83
|
+
else if (result.status === "already_registered") {
|
|
84
|
+
output(`✓ ${clientDisplayName(client)} was already configured.`);
|
|
85
|
+
}
|
|
86
|
+
else if (result.status === "needs_update") {
|
|
87
|
+
output(`✗ ${clientDisplayName(client)} already has a different SomaCheck command or version.`);
|
|
88
|
+
output(" Run doctor for safe replacement instructions; the existing entry was not changed.");
|
|
89
|
+
}
|
|
90
|
+
else if (result.status === "not_installed") {
|
|
91
|
+
output(`○ ${clientDisplayName(client)} is not installed; skipped.`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
output(`✗ Could not configure ${clientDisplayName(client)} automatically.`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function preflightLinkClients(clients, interactive) {
|
|
99
|
+
const nonInteractiveClient = interactive ? null : singleNonInteractiveClientSelection(clients);
|
|
100
|
+
const selectedClients = interactive
|
|
101
|
+
? clients === "prompt" ? [] : clients
|
|
102
|
+
: nonInteractiveClient === null ? [] : [nonInteractiveClient];
|
|
103
|
+
if (selectedClients.length === 0) {
|
|
104
|
+
throw new ClientPreflightError([
|
|
105
|
+
interactive
|
|
106
|
+
? "Not linking yet: choose one installed local client before redeeming the pairing code."
|
|
107
|
+
: "Not linking here: non-interactive setup must name one local client explicitly.",
|
|
108
|
+
"",
|
|
109
|
+
"Run the command in Terminal, or paste it into the local agent with exactly one",
|
|
110
|
+
"client selection, for example:",
|
|
111
|
+
" npx -y @somacheck/vibecheck@0.3.0 link <CODE> --client claude",
|
|
112
|
+
" npx -y @somacheck/vibecheck@0.3.0 link <CODE> --client codex",
|
|
113
|
+
"",
|
|
114
|
+
"The pairing code has NOT been used.",
|
|
115
|
+
].join("\n"));
|
|
116
|
+
}
|
|
117
|
+
for (const client of selectedClients) {
|
|
118
|
+
const legacyHosted = await detectLegacyHostedRegistration(client, runner);
|
|
119
|
+
if (legacyHosted !== null) {
|
|
120
|
+
throw new ClientPreflightError([
|
|
121
|
+
`Not linking yet: ${clientDisplayName(client)} still has the separate legacy "somacheck" connector.`,
|
|
122
|
+
"It can make an agent choose the wrong tools and mistake its OAuth prompt for this local link.",
|
|
123
|
+
"",
|
|
124
|
+
`Remove only that legacy entry: ${manualLegacyHostedRemoveCommand(client)}`,
|
|
125
|
+
"Then run the link command again. The pairing code has NOT been used.",
|
|
126
|
+
].join("\n"));
|
|
127
|
+
}
|
|
128
|
+
output(`Checking ${clientDisplayName(client)} before redeeming the pairing code...`);
|
|
129
|
+
const preflight = await preflightClientPersistence(client, runner);
|
|
130
|
+
if (preflight.status === "ready") {
|
|
131
|
+
output(`✓ ${clientDisplayName(client)} already has the exact SomaCheck MCP registration.`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (preflight.status === "registered" || preflight.status === "upgraded") {
|
|
135
|
+
const verb = preflight.status === "upgraded" ? "was safely upgraded" : "was configured";
|
|
136
|
+
output(`✓ ${clientDisplayName(client)} ${verb} before redeeming the pairing code.`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const reason = preflight.status === "not_installed"
|
|
140
|
+
? `${clientDisplayName(client)} is not installed or its CLI is unavailable.`
|
|
141
|
+
: preflight.status === "needs_update"
|
|
142
|
+
? `${clientDisplayName(client)} has an existing "vibecheck" MCP entry with a different command or version.`
|
|
143
|
+
: `${clientDisplayName(client)} configuration could not be inspected and written.`;
|
|
144
|
+
const repair = preflight.status === "needs_update"
|
|
145
|
+
? ["", `Remove the stale entry: ${manualRemoveCommand(client)}`, `Then configure 0.3: ${manualSetupCommand(client)}`]
|
|
146
|
+
: [];
|
|
147
|
+
throw new ClientPreflightError([
|
|
148
|
+
`Not linking here: ${reason}`,
|
|
149
|
+
...repair,
|
|
150
|
+
"",
|
|
151
|
+
"The pairing code has NOT been used. Fix the local client configuration, then",
|
|
152
|
+
"run the link command again.",
|
|
153
|
+
].join("\n"));
|
|
154
|
+
}
|
|
155
|
+
return selectedClients;
|
|
156
|
+
}
|
|
157
|
+
async function runDoctor() {
|
|
158
|
+
output("\nSomaCheck connection check");
|
|
159
|
+
const readiness = await checkReadiness({ home: homedir(), api, runner, output });
|
|
160
|
+
return readiness.ready;
|
|
161
|
+
}
|
|
162
|
+
async function refreshRuntimeHealth(client) {
|
|
163
|
+
const token = (await readConfig(homedir())).token;
|
|
164
|
+
await api.statusRequest(token);
|
|
165
|
+
await api.contextRequest(token);
|
|
166
|
+
await api.clientHandshake(token, {
|
|
167
|
+
client_key: client,
|
|
168
|
+
client_label: clientDisplayName(client),
|
|
169
|
+
registration_exact: true,
|
|
170
|
+
status_probe_ok: true,
|
|
171
|
+
context_probe_ok: true,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async function startServer(runtimeClient) {
|
|
175
|
+
const server = createVibecheckServer({
|
|
176
|
+
api,
|
|
177
|
+
loadToken: async () => (await readConfig(homedir())).token,
|
|
178
|
+
});
|
|
179
|
+
await server.connect(new StdioServerTransport());
|
|
180
|
+
if (runtimeClient !== null) {
|
|
181
|
+
// Never write health-check output to stdout: this process speaks MCP over
|
|
182
|
+
// stdio. Tool calls retain their own categorized diagnostics if refresh
|
|
183
|
+
// fails, and the phone will simply keep the client out of Ready.
|
|
184
|
+
void refreshRuntimeHealth(runtimeClient).catch(() => undefined);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
10
187
|
async function main() {
|
|
11
188
|
const args = process.argv.slice(2);
|
|
12
189
|
if (args[0] === "link") {
|
|
13
|
-
if (args.length
|
|
14
|
-
throw new Error(
|
|
15
|
-
|
|
190
|
+
if (args.length < 2)
|
|
191
|
+
throw new Error(usage());
|
|
192
|
+
const requestedClients = parseLinkClient(args.slice(2));
|
|
193
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
194
|
+
const requestedOrPromptedClients = interactive
|
|
195
|
+
? requestedClients === "prompt"
|
|
196
|
+
? await promptForClients(await detectInstalledClients(runner))
|
|
197
|
+
: requestedClients
|
|
198
|
+
: requestedClients;
|
|
16
199
|
await linkAgent(args[1] ?? "", {
|
|
17
200
|
home: homedir(),
|
|
18
201
|
redeem: (code) => api.redeemLink(code),
|
|
19
|
-
output
|
|
202
|
+
output,
|
|
203
|
+
isInteractive: () => interactive,
|
|
204
|
+
allowNonInteractive: !interactive || process.env.SOMACHECK_ALLOW_NON_INTERACTIVE === "1",
|
|
205
|
+
preflight: async () => {
|
|
206
|
+
await preflightLinkClients(requestedOrPromptedClients, interactive);
|
|
207
|
+
},
|
|
20
208
|
});
|
|
21
|
-
return;
|
|
209
|
+
return (await runDoctor()) ? 0 : 2;
|
|
22
210
|
}
|
|
23
|
-
if (args
|
|
24
|
-
|
|
211
|
+
if (args[0] === "setup") {
|
|
212
|
+
if (args.length !== 2)
|
|
213
|
+
throw new Error(usage());
|
|
214
|
+
const requested = args[1]?.toLowerCase();
|
|
215
|
+
const clients = requested === "all"
|
|
216
|
+
? ["codex", "claude"]
|
|
217
|
+
: requested === "codex" || requested === "claude"
|
|
218
|
+
? [requested]
|
|
219
|
+
: [];
|
|
220
|
+
if (clients.length === 0)
|
|
221
|
+
throw new Error(usage());
|
|
222
|
+
await configureClients(clients);
|
|
223
|
+
return (await runDoctor()) ? 0 : 2;
|
|
25
224
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
225
|
+
if (args[0] === "doctor") {
|
|
226
|
+
if (args.length !== 1)
|
|
227
|
+
throw new Error(usage());
|
|
228
|
+
return (await runDoctor()) ? 0 : 2;
|
|
229
|
+
}
|
|
230
|
+
const runtimeClient = parseServeClient(args);
|
|
231
|
+
if (runtimeClient !== null) {
|
|
232
|
+
await startServer(runtimeClient);
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
if (args.length !== 0)
|
|
236
|
+
throw new Error(usage());
|
|
237
|
+
await startServer(null);
|
|
238
|
+
return 0;
|
|
31
239
|
}
|
|
32
|
-
main().
|
|
240
|
+
main().then((exitCode) => {
|
|
241
|
+
process.exitCode = exitCode;
|
|
242
|
+
}).catch((error) => {
|
|
243
|
+
if (error instanceof NonInteractiveLinkError || error instanceof LinkPersistenceError || error instanceof ClientPreflightError) {
|
|
244
|
+
process.stderr.write(`${error.message}\n`);
|
|
245
|
+
process.exitCode = 1;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
33
248
|
const isUsageError = error instanceof Error && error.message.startsWith("Usage:");
|
|
34
249
|
const message = isUsageError
|
|
35
250
|
? error.message
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { LEGACY_HOSTED_MCP_SERVER_NAME, MCP_SERVER_NAME, PACKAGE_SPEC } from "./constants.js";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
export class LocalCommandRunner {
|
|
6
|
+
async run(command, args) {
|
|
7
|
+
try {
|
|
8
|
+
const result = await execFileAsync(command, args, {
|
|
9
|
+
encoding: "utf8",
|
|
10
|
+
maxBuffer: 1024 * 1024,
|
|
11
|
+
timeout: 20_000,
|
|
12
|
+
});
|
|
13
|
+
return { exitCode: 0, stdout: result.stdout, stderr: result.stderr };
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
const failure = error;
|
|
17
|
+
return {
|
|
18
|
+
exitCode: typeof failure.code === "number" ? failure.code : 127,
|
|
19
|
+
stdout: failure.stdout ?? "",
|
|
20
|
+
stderr: failure.stderr ?? "",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function clientDisplayName(client) {
|
|
26
|
+
return client === "codex" ? "Codex / ChatGPT desktop" : "Claude Code";
|
|
27
|
+
}
|
|
28
|
+
function mcpServerArgs(client) {
|
|
29
|
+
return ["-y", PACKAGE_SPEC, "serve", "--client", client];
|
|
30
|
+
}
|
|
31
|
+
export function manualSetupCommand(client) {
|
|
32
|
+
if (client === "codex") {
|
|
33
|
+
return `codex mcp add ${MCP_SERVER_NAME} -- npx ${mcpServerArgs(client).join(" ")}`;
|
|
34
|
+
}
|
|
35
|
+
return `claude mcp add --scope user ${MCP_SERVER_NAME} -- npx ${mcpServerArgs(client).join(" ")}`;
|
|
36
|
+
}
|
|
37
|
+
export function manualRemoveCommand(client) {
|
|
38
|
+
return client === "codex"
|
|
39
|
+
? `codex mcp remove ${MCP_SERVER_NAME}`
|
|
40
|
+
: `claude mcp remove --scope user ${MCP_SERVER_NAME}`;
|
|
41
|
+
}
|
|
42
|
+
export function manualLegacyHostedRemoveCommand(client) {
|
|
43
|
+
return client === "codex"
|
|
44
|
+
? `codex mcp remove ${LEGACY_HOSTED_MCP_SERVER_NAME}`
|
|
45
|
+
: `claude mcp remove --scope user ${LEGACY_HOSTED_MCP_SERVER_NAME}`;
|
|
46
|
+
}
|
|
47
|
+
export async function isClientInstalled(client, runner) {
|
|
48
|
+
const command = client === "codex" ? "codex" : "claude";
|
|
49
|
+
return (await runner.run(command, ["--version"])).exitCode === 0;
|
|
50
|
+
}
|
|
51
|
+
export async function clientRegistrationState(client, runner) {
|
|
52
|
+
const command = client === "codex" ? "codex" : "claude";
|
|
53
|
+
const args = client === "codex"
|
|
54
|
+
? ["mcp", "get", MCP_SERVER_NAME, "--json"]
|
|
55
|
+
: ["mcp", "get", MCP_SERVER_NAME];
|
|
56
|
+
const result = await runner.run(command, args);
|
|
57
|
+
if (result.exitCode !== 0)
|
|
58
|
+
return "missing";
|
|
59
|
+
if (registrationMatches(client, result.stdout))
|
|
60
|
+
return "current";
|
|
61
|
+
return isManagedSomaCheckRegistration(client, result.stdout) ? "managed_update" : "needs_update";
|
|
62
|
+
}
|
|
63
|
+
export async function detectLegacyHostedRegistration(client, runner) {
|
|
64
|
+
const command = client === "codex" ? "codex" : "claude";
|
|
65
|
+
const args = client === "codex"
|
|
66
|
+
? ["mcp", "get", LEGACY_HOSTED_MCP_SERVER_NAME, "--json"]
|
|
67
|
+
: ["mcp", "get", LEGACY_HOSTED_MCP_SERVER_NAME];
|
|
68
|
+
const result = await runner.run(command, args);
|
|
69
|
+
if (result.exitCode !== 0)
|
|
70
|
+
return null;
|
|
71
|
+
const combined = `${result.stdout}\n${result.stderr}`;
|
|
72
|
+
return {
|
|
73
|
+
client,
|
|
74
|
+
status: /needs\s+authentication|authenticate|oauth/i.test(combined)
|
|
75
|
+
? "needs_authentication"
|
|
76
|
+
: "present",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function registrationMatches(client, stdout) {
|
|
80
|
+
if (client === "codex") {
|
|
81
|
+
try {
|
|
82
|
+
const value = JSON.parse(stdout);
|
|
83
|
+
return value.enabled !== false
|
|
84
|
+
&& value.transport?.type === "stdio"
|
|
85
|
+
&& value.transport.command === "npx"
|
|
86
|
+
&& Array.isArray(value.transport.args)
|
|
87
|
+
&& value.transport.args.length === 5
|
|
88
|
+
&& value.transport.args.every((arg, index) => arg === mcpServerArgs(client)[index]);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const command = /^\s*Command:\s*(\S+)\s*$/im.exec(stdout)?.[1];
|
|
95
|
+
const rawArgs = /^\s*Args:\s*(.*?)\s*$/im.exec(stdout)?.[1] ?? "";
|
|
96
|
+
const parsedArgs = rawArgs.split(/\s+/).filter(Boolean);
|
|
97
|
+
const expectedArgs = mcpServerArgs(client);
|
|
98
|
+
return command === "npx"
|
|
99
|
+
&& parsedArgs.length === expectedArgs.length
|
|
100
|
+
&& parsedArgs.every((arg, index) => arg === expectedArgs[index]);
|
|
101
|
+
}
|
|
102
|
+
function isManagedSomaCheckRegistration(client, stdout) {
|
|
103
|
+
let command;
|
|
104
|
+
let args = [];
|
|
105
|
+
if (client === "codex") {
|
|
106
|
+
try {
|
|
107
|
+
const value = JSON.parse(stdout);
|
|
108
|
+
if (value.enabled === false || value.transport?.type !== "stdio")
|
|
109
|
+
return false;
|
|
110
|
+
command = typeof value.transport.command === "string" ? value.transport.command : undefined;
|
|
111
|
+
args = Array.isArray(value.transport.args) && value.transport.args.every((arg) => typeof arg === "string")
|
|
112
|
+
? value.transport.args
|
|
113
|
+
: [];
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
command = /^\s*Command:\s*(\S+)\s*$/im.exec(stdout)?.[1];
|
|
121
|
+
args = (/^\s*Args:\s*(.*?)\s*$/im.exec(stdout)?.[1] ?? "").split(/\s+/).filter(Boolean);
|
|
122
|
+
}
|
|
123
|
+
if (command !== "npx")
|
|
124
|
+
return false;
|
|
125
|
+
const packageArgs = args.filter((arg) => arg.startsWith("@somacheck/vibecheck@"));
|
|
126
|
+
return packageArgs.length === 1
|
|
127
|
+
&& /^@somacheck\/vibecheck@0\.[0-2]\.\d+$/.test(packageArgs[0])
|
|
128
|
+
&& args.every((arg) => arg === "-y" || arg === "--yes" || arg === packageArgs[0]);
|
|
129
|
+
}
|
|
130
|
+
export async function isClientRegistered(client, runner) {
|
|
131
|
+
return (await clientRegistrationState(client, runner)) === "current";
|
|
132
|
+
}
|
|
133
|
+
export async function detectInstalledClients(runner) {
|
|
134
|
+
const clients = ["codex", "claude"];
|
|
135
|
+
const installed = await Promise.all(clients.map(async (client) => ({
|
|
136
|
+
client,
|
|
137
|
+
installed: await isClientInstalled(client, runner),
|
|
138
|
+
})));
|
|
139
|
+
return installed.filter((item) => item.installed).map((item) => item.client);
|
|
140
|
+
}
|
|
141
|
+
export async function registerClient(client, runner) {
|
|
142
|
+
if (!(await isClientInstalled(client, runner))) {
|
|
143
|
+
return { client, status: "not_installed" };
|
|
144
|
+
}
|
|
145
|
+
const registration = await clientRegistrationState(client, runner);
|
|
146
|
+
if (registration === "current") {
|
|
147
|
+
return { client, status: "already_registered" };
|
|
148
|
+
}
|
|
149
|
+
if (registration === "needs_update")
|
|
150
|
+
return { client, status: "needs_update" };
|
|
151
|
+
const command = client === "codex" ? "codex" : "claude";
|
|
152
|
+
if (registration === "managed_update") {
|
|
153
|
+
const removeArgs = client === "codex"
|
|
154
|
+
? ["mcp", "remove", MCP_SERVER_NAME]
|
|
155
|
+
: ["mcp", "remove", "--scope", "user", MCP_SERVER_NAME];
|
|
156
|
+
if ((await runner.run(command, removeArgs)).exitCode !== 0) {
|
|
157
|
+
return { client, status: "failed" };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const serverArgs = mcpServerArgs(client);
|
|
161
|
+
const args = client === "codex"
|
|
162
|
+
? ["mcp", "add", MCP_SERVER_NAME, "--", "npx", ...serverArgs]
|
|
163
|
+
: ["mcp", "add", "--scope", "user", MCP_SERVER_NAME, "--", "npx", ...serverArgs];
|
|
164
|
+
const result = await runner.run(command, args);
|
|
165
|
+
return {
|
|
166
|
+
client,
|
|
167
|
+
status: result.exitCode === 0
|
|
168
|
+
? registration === "managed_update" ? "upgraded" : "registered"
|
|
169
|
+
: "failed",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
export async function preflightClientPersistence(client, runner) {
|
|
173
|
+
if (!(await isClientInstalled(client, runner))) {
|
|
174
|
+
return { client, status: "not_installed" };
|
|
175
|
+
}
|
|
176
|
+
const registration = await clientRegistrationState(client, runner);
|
|
177
|
+
if (registration === "current") {
|
|
178
|
+
return { client, status: "ready" };
|
|
179
|
+
}
|
|
180
|
+
if (registration === "needs_update") {
|
|
181
|
+
return { client, status: "needs_update" };
|
|
182
|
+
}
|
|
183
|
+
const result = await registerClient(client, runner);
|
|
184
|
+
if (result.status !== "registered" && result.status !== "upgraded" && result.status !== "already_registered") {
|
|
185
|
+
return { client, status: result.status === "not_installed" ? "not_installed" : "failed" };
|
|
186
|
+
}
|
|
187
|
+
return (await clientRegistrationState(client, runner)) === "current"
|
|
188
|
+
? { client, status: result.status === "upgraded" ? "upgraded" : "registered" }
|
|
189
|
+
: { client, status: "failed" };
|
|
190
|
+
}
|
|
191
|
+
export function singleNonInteractiveClientSelection(clients) {
|
|
192
|
+
return clients !== "prompt" && clients.length === 1 ? clients[0] : null;
|
|
193
|
+
}
|
|
194
|
+
export function parseClientChoice(value, installed) {
|
|
195
|
+
const normalized = value.trim().toLowerCase();
|
|
196
|
+
if (normalized === "later" || normalized === "none" || normalized === "4")
|
|
197
|
+
return [];
|
|
198
|
+
if ((normalized === "codex" || normalized === "1") && installed.includes("codex"))
|
|
199
|
+
return ["codex"];
|
|
200
|
+
if ((normalized === "claude" || normalized === "2") && installed.includes("claude"))
|
|
201
|
+
return ["claude"];
|
|
202
|
+
if (normalized === "all" || normalized === "both" || normalized === "3")
|
|
203
|
+
return [...installed];
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=client-setup.js.map
|
package/dist/config.js
CHANGED
|
@@ -3,13 +3,30 @@ import { join } from "node:path";
|
|
|
3
3
|
export function configPath(home) {
|
|
4
4
|
return join(home, ".sensie", "config.json");
|
|
5
5
|
}
|
|
6
|
+
/** Prove the token directory supports the same create/rename/remove sequence
|
|
7
|
+
* used by writeConfig before a single-use pairing code is redeemed. */
|
|
8
|
+
export async function preflightConfigPersistence(home) {
|
|
9
|
+
const directory = join(home, ".sensie");
|
|
10
|
+
const probe = join(directory, `.config-preflight.${process.pid}.${Date.now()}.tmp`);
|
|
11
|
+
const renamedProbe = `${probe}.renamed`;
|
|
12
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
13
|
+
await chmod(directory, 0o700);
|
|
14
|
+
try {
|
|
15
|
+
await writeFile(probe, "", { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
16
|
+
await rename(probe, renamedProbe);
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
await rm(probe, { force: true });
|
|
20
|
+
await rm(renamedProbe, { force: true });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
6
23
|
export async function readConfig(home) {
|
|
7
24
|
let value;
|
|
8
25
|
try {
|
|
9
26
|
value = JSON.parse(await readFile(configPath(home), "utf8"));
|
|
10
27
|
}
|
|
11
28
|
catch {
|
|
12
|
-
throw new Error("SomaCheck is not linked. Run: npx @somacheck/vibecheck link <CODE>");
|
|
29
|
+
throw new Error("SomaCheck is not linked. Run: npx -y @somacheck/vibecheck link <CODE>");
|
|
13
30
|
}
|
|
14
31
|
if (value === null || typeof value !== "object" || !("token" in value)) {
|
|
15
32
|
throw new Error("SomaCheck link configuration is invalid.");
|
package/dist/constants.js
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
// Supabase publishable keys are public client identifiers; authorization remains enforced by RLS/RPCs.
|
|
2
2
|
export const SUPABASE_URL = "https://pbldcmniommltbdwuykk.supabase.co";
|
|
3
3
|
export const SUPABASE_PUBLISHABLE_KEY = "sb_publishable_af-lUNI2FqEcb-oGy-4uxQ_cnm6kY85";
|
|
4
|
+
export const PACKAGE_NAME = "@somacheck/vibecheck";
|
|
5
|
+
export const PACKAGE_VERSION = "0.3.0";
|
|
6
|
+
export const PACKAGE_SPEC = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;
|
|
7
|
+
export const MCP_SERVER_NAME = "vibecheck";
|
|
8
|
+
export const LEGACY_HOSTED_MCP_SERVER_NAME = "somacheck";
|
|
9
|
+
export const BACKEND_PROTOCOL_VERSION = 3;
|
|
10
|
+
export const TOOLSET_VERSION = "vibecheck-0.3";
|
|
11
|
+
export const TOOL_NAMES = [
|
|
12
|
+
"get_vibecheck_context",
|
|
13
|
+
"get_vibecheck_status",
|
|
14
|
+
"post_vibecheck_statement",
|
|
15
|
+
"get_vibecheck_result",
|
|
16
|
+
];
|
|
4
17
|
//# sourceMappingURL=constants.js.map
|
package/dist/link.js
CHANGED
|
@@ -1,11 +1,60 @@
|
|
|
1
|
-
import { writeConfig } from "./config.js";
|
|
1
|
+
import { preflightConfigPersistence, writeConfig } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when linking is attempted somewhere a pairing code should not be spent.
|
|
4
|
+
* Carries guidance rather than a bare failure: the person reading it is mid-setup
|
|
5
|
+
* and needs to know where to go instead.
|
|
6
|
+
*/
|
|
7
|
+
export class NonInteractiveLinkError extends Error {
|
|
8
|
+
constructor() {
|
|
9
|
+
super([
|
|
10
|
+
"Not linking here: this doesn't look like a terminal with a person at it.",
|
|
11
|
+
"",
|
|
12
|
+
"`link` sets up the computer that will run the SomaCheck MCP server. It is",
|
|
13
|
+
"not a task for a chat assistant -- an assistant's sandbox is usually wiped",
|
|
14
|
+
"between sessions, so the pairing code would be spent and the link lost.",
|
|
15
|
+
"",
|
|
16
|
+
"Open Terminal on your own computer and run the command there.",
|
|
17
|
+
"",
|
|
18
|
+
"Your pairing code has NOT been used. It is still valid.",
|
|
19
|
+
"",
|
|
20
|
+
"If you really are provisioning a machine non-interactively, re-run with",
|
|
21
|
+
"SOMACHECK_ALLOW_NON_INTERACTIVE=1.",
|
|
22
|
+
].join("\n"));
|
|
23
|
+
this.name = "NonInteractiveLinkError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class LinkPersistenceError extends Error {
|
|
27
|
+
constructor() {
|
|
28
|
+
super([
|
|
29
|
+
"Not linking yet: SomaCheck cannot safely write its private token configuration on this computer.",
|
|
30
|
+
"Fix permissions for ~/.sensie, then run the link command again.",
|
|
31
|
+
"The pairing code has NOT been used.",
|
|
32
|
+
].join("\n"));
|
|
33
|
+
this.name = "LinkPersistenceError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
2
36
|
export async function linkAgent(code, dependencies) {
|
|
3
37
|
const normalizedCode = code.trim();
|
|
4
38
|
if (!normalizedCode) {
|
|
5
39
|
throw new Error("Pairing code is required.");
|
|
6
40
|
}
|
|
41
|
+
// Guard BEFORE redeem(). Redemption is the irreversible step -- a pairing code
|
|
42
|
+
// is single-use, so an agent running this inside its own sandbox spends the
|
|
43
|
+
// user's code and leaves them nothing to retry with. Checking afterwards would
|
|
44
|
+
// report the problem accurately and still have caused it.
|
|
45
|
+
const interactive = dependencies.isInteractive?.() ?? false;
|
|
46
|
+
if (!interactive && !dependencies.allowNonInteractive) {
|
|
47
|
+
throw new NonInteractiveLinkError();
|
|
48
|
+
}
|
|
49
|
+
await dependencies.preflight?.();
|
|
50
|
+
try {
|
|
51
|
+
await preflightConfigPersistence(dependencies.home);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new LinkPersistenceError();
|
|
55
|
+
}
|
|
7
56
|
const token = await dependencies.redeem(normalizedCode);
|
|
8
57
|
await writeConfig(dependencies.home, { token });
|
|
9
|
-
dependencies.output("SomaCheck
|
|
58
|
+
dependencies.output("SomaCheck pairing code redeemed on this computer.");
|
|
10
59
|
}
|
|
11
60
|
//# sourceMappingURL=link.js.map
|