@somacheck/vibecheck 0.3.0 → 0.4.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 CHANGED
@@ -9,7 +9,7 @@ Your agent has your context. It does not have your gut. This is the bridge.
9
9
 
10
10
  Fair. Here is what you need, without executing anything.
11
11
 
12
- **What the `link` command does.** `npx -y @somacheck/vibecheck@0.3.0 link <CODE> --client <CLIENT>`
12
+ **What the `link` command does.** `npx -y @somacheck/vibecheck@0.4.0 link <CODE> --client <CLIENT>`
13
13
  exchanges a short-lived pairing code, generated inside the SomaCheck iOS app, for
14
14
  a bearer token. It writes that token to `~/.sensie/config.json` with mode `0600`,
15
15
  offers to configure supported agent clients found on the computer, and checks the
@@ -72,9 +72,9 @@ before the pairing code is redeemed.
72
72
  To configure or repair a client later:
73
73
 
74
74
  ```text
75
- npx -y @somacheck/vibecheck@0.3.0 setup codex
76
- npx -y @somacheck/vibecheck@0.3.0 setup claude
77
- npx -y @somacheck/vibecheck@0.3.0 doctor
75
+ npx -y @somacheck/vibecheck@0.4.0 setup codex
76
+ npx -y @somacheck/vibecheck@0.4.0 setup claude
77
+ npx -y @somacheck/vibecheck@0.4.0 doctor
78
78
  ```
79
79
 
80
80
  Manual registration remains available:
@@ -84,13 +84,13 @@ Manual registration remains available:
84
84
  "mcpServers": {
85
85
  "vibecheck": {
86
86
  "command": "npx",
87
- "args": ["-y", "@somacheck/vibecheck@0.3.0", "serve", "--client", "codex"]
87
+ "args": ["-y", "@somacheck/vibecheck@0.4.0", "serve", "--client", "codex"]
88
88
  }
89
89
  }
90
90
  }
91
91
  ```
92
92
 
93
- For Claude Code: `claude mcp add --scope user vibecheck -- npx -y @somacheck/vibecheck@0.3.0 serve --client claude`
93
+ For Claude Code: `claude mcp add --scope user vibecheck -- npx -y @somacheck/vibecheck@0.4.0 serve --client claude`
94
94
 
95
95
  The link step writes only the bearer token in `~/.sensie/config.json`. The client
96
96
  setup step asks Codex or Claude to add the pinned MCP command to that client's own
package/dist/api.js CHANGED
@@ -128,6 +128,75 @@ function parseStatus(row) {
128
128
  },
129
129
  };
130
130
  }
131
+ /** Shared strict decoders used by both local stdio and hosted OAuth MCP. */
132
+ export function decodeVibecheckStatus(value) {
133
+ return parseStatus(asObject(value));
134
+ }
135
+ export function decodeCreatedRequests(value) {
136
+ return allRows(value).map((row) => {
137
+ const presentationState = requiredString(row, "presentation_state");
138
+ if (presentationState !== "presented" && presentationState !== "queued") {
139
+ throw new Error("SomaCheck returned an incomplete response.");
140
+ }
141
+ return {
142
+ request_id: requiredString(row, "request_id"),
143
+ presentation_state: presentationState,
144
+ stale_at: nullableString(row, "expires_at"),
145
+ };
146
+ });
147
+ }
148
+ export function decodePollResponse(value) {
149
+ const row = asObject(value);
150
+ const status = row.status;
151
+ if (typeof status !== "string" || !STATUSES.has(status)) {
152
+ throw new Error("SomaCheck returned an invalid request status.");
153
+ }
154
+ const verdict = row.verdict;
155
+ const confidence = row.confidence;
156
+ const latency = row.latency_s;
157
+ if (verdict !== null && (typeof verdict !== "string" || !VERDICTS.has(verdict))) {
158
+ throw new Error("SomaCheck returned an invalid verdict.");
159
+ }
160
+ if (confidence !== null && (typeof confidence !== "number" || !Number.isFinite(confidence))) {
161
+ throw new Error("SomaCheck returned an invalid confidence.");
162
+ }
163
+ if (latency !== null && (typeof latency !== "number" || !Number.isFinite(latency) || latency < 0)) {
164
+ throw new Error("SomaCheck returned an invalid latency.");
165
+ }
166
+ if (status === "answered") {
167
+ if (verdict === null || confidence === null || confidence < 0 || confidence > 1 || latency === null) {
168
+ throw new Error("SomaCheck returned an incomplete answered result.");
169
+ }
170
+ }
171
+ else if (verdict !== null || confidence !== null || latency !== null) {
172
+ throw new Error("SomaCheck returned an inconsistent pending result.");
173
+ }
174
+ return {
175
+ status: status,
176
+ verdict: verdict,
177
+ confidence: confidence,
178
+ latency_s: latency,
179
+ };
180
+ }
181
+ export function decodeVibecheckContext(value) {
182
+ return allRows(value).map((row) => {
183
+ const verdict = requiredString(row, "verdict");
184
+ const confidence = requiredNumber(row, "confidence");
185
+ if (!VERDICTS.has(verdict)) {
186
+ throw new Error("SomaCheck returned an invalid verdict.");
187
+ }
188
+ if (confidence < 0 || confidence > 1) {
189
+ throw new Error("SomaCheck returned an invalid confidence.");
190
+ }
191
+ return {
192
+ request_id: requiredString(row, "request_id"),
193
+ statement: requiredString(row, "statement"),
194
+ verdict: verdict,
195
+ confidence,
196
+ answered_at: requiredTimestamp(row, "answered_at"),
197
+ };
198
+ });
199
+ }
131
200
  function parseHandshake(row) {
132
201
  const readinessStatus = requiredString(row, "readiness_status");
133
202
  if (!HANDSHAKE_READINESS_STATUSES.has(readinessStatus)) {
@@ -165,74 +234,16 @@ export class SupabaseAgentApi {
165
234
  return requiredString(row, "token");
166
235
  }
167
236
  async createRequests(token, statements) {
168
- const rows = await this.#rpcRows("agent_proposition_batch_create", { token, statements });
169
- return rows.map((row) => {
170
- const presentationState = requiredString(row, "presentation_state");
171
- if (presentationState !== "presented" && presentationState !== "queued") {
172
- throw new Error("SomaCheck returned an incomplete response.");
173
- }
174
- return {
175
- request_id: requiredString(row, "request_id"),
176
- presentation_state: presentationState,
177
- stale_at: nullableString(row, "expires_at"),
178
- };
179
- });
237
+ return decodeCreatedRequests(await this.#rpcJson("agent_proposition_batch_create", { token, statements }));
180
238
  }
181
239
  async pollRequest(token, requestId) {
182
- const row = await this.#rpc("agent_proposition_result", { token, proposition_id: requestId });
183
- const status = row.status;
184
- if (typeof status !== "string" || !STATUSES.has(status)) {
185
- throw new Error("SomaCheck returned an invalid request status.");
186
- }
187
- const verdict = row.verdict;
188
- const confidence = row.confidence;
189
- const latency = row.latency_s;
190
- if (verdict !== null && (typeof verdict !== "string" || !VERDICTS.has(verdict))) {
191
- throw new Error("SomaCheck returned an invalid verdict.");
192
- }
193
- if (confidence !== null && (typeof confidence !== "number" || !Number.isFinite(confidence))) {
194
- throw new Error("SomaCheck returned an invalid confidence.");
195
- }
196
- if (latency !== null && (typeof latency !== "number" || !Number.isFinite(latency) || latency < 0)) {
197
- throw new Error("SomaCheck returned an invalid latency.");
198
- }
199
- if (status === "answered") {
200
- if (verdict === null || confidence === null || confidence < 0 || confidence > 1 || latency === null) {
201
- throw new Error("SomaCheck returned an incomplete answered result.");
202
- }
203
- }
204
- else if (verdict !== null || confidence !== null || latency !== null) {
205
- throw new Error("SomaCheck returned an inconsistent pending result.");
206
- }
207
- return {
208
- status: status,
209
- verdict: verdict,
210
- confidence: confidence,
211
- latency_s: latency,
212
- };
240
+ return decodePollResponse(await this.#rpc("agent_proposition_result", { token, proposition_id: requestId }));
213
241
  }
214
242
  async statusRequest(token) {
215
- return parseStatus(await this.#rpc("agent_proposition_cache_status", { token }));
243
+ return decodeVibecheckStatus(await this.#rpc("agent_proposition_cache_status", { token }));
216
244
  }
217
245
  async contextRequest(token, limit = 20) {
218
- const rows = await this.#rpcRows("agent_proposition_context", { token, p_limit: limit });
219
- return rows.map((row) => {
220
- const verdict = requiredString(row, "verdict");
221
- const confidence = requiredNumber(row, "confidence");
222
- if (!VERDICTS.has(verdict)) {
223
- throw new Error("SomaCheck returned an invalid verdict.");
224
- }
225
- if (confidence < 0 || confidence > 1) {
226
- throw new Error("SomaCheck returned an invalid confidence.");
227
- }
228
- return {
229
- request_id: requiredString(row, "request_id"),
230
- statement: requiredString(row, "statement"),
231
- verdict: verdict,
232
- confidence,
233
- answered_at: requiredTimestamp(row, "answered_at"),
234
- };
235
- });
246
+ return decodeVibecheckContext(await this.#rpcJson("agent_proposition_context", { token, p_limit: limit }));
236
247
  }
237
248
  async clientHandshake(token, input) {
238
249
  const row = await this.#rpc("agent_client_handshake", {
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
5
5
  import { SupabaseAgentApi } from "./api.js";
6
6
  import { clientDisplayName, detectInstalledClients, detectLegacyHostedRegistration, LocalCommandRunner, manualLegacyHostedRemoveCommand, manualRemoveCommand, manualSetupCommand, parseClientChoice, preflightClientPersistence, registerClient, singleNonInteractiveClientSelection, } from "./client-setup.js";
7
7
  import { readConfig } from "./config.js";
8
- import { SUPABASE_PUBLISHABLE_KEY, SUPABASE_URL } from "./constants.js";
8
+ import { PACKAGE_SPEC, SUPABASE_PUBLISHABLE_KEY, SUPABASE_URL } from "./constants.js";
9
9
  import { LinkPersistenceError, NonInteractiveLinkError, linkAgent } from "./link.js";
10
10
  import { checkReadiness } from "./readiness.js";
11
11
  import { createVibecheckServer } from "./server.js";
@@ -100,6 +100,13 @@ async function preflightLinkClients(clients, interactive) {
100
100
  const selectedClients = interactive
101
101
  ? clients === "prompt" ? [] : clients
102
102
  : nonInteractiveClient === null ? [] : [nonInteractiveClient];
103
+ if (selectedClients.length > 1) {
104
+ throw new ClientPreflightError([
105
+ "Not linking yet: each local agent needs its own independently revocable pairing code.",
106
+ "Link Claude and Codex separately so one credential is never shared between them.",
107
+ "The pairing code has NOT been used.",
108
+ ].join("\n"));
109
+ }
103
110
  if (selectedClients.length === 0) {
104
111
  throw new ClientPreflightError([
105
112
  interactive
@@ -108,8 +115,8 @@ async function preflightLinkClients(clients, interactive) {
108
115
  "",
109
116
  "Run the command in Terminal, or paste it into the local agent with exactly one",
110
117
  "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",
118
+ ` npx -y ${PACKAGE_SPEC} link <CODE> --client claude`,
119
+ ` npx -y ${PACKAGE_SPEC} link <CODE> --client codex`,
113
120
  "",
114
121
  "The pairing code has NOT been used.",
115
122
  ].join("\n"));
@@ -142,7 +149,7 @@ async function preflightLinkClients(clients, interactive) {
142
149
  ? `${clientDisplayName(client)} has an existing "vibecheck" MCP entry with a different command or version.`
143
150
  : `${clientDisplayName(client)} configuration could not be inspected and written.`;
144
151
  const repair = preflight.status === "needs_update"
145
- ? ["", `Remove the stale entry: ${manualRemoveCommand(client)}`, `Then configure 0.3: ${manualSetupCommand(client)}`]
152
+ ? ["", `Remove the stale entry: ${manualRemoveCommand(client)}`, `Then configure the current version: ${manualSetupCommand(client)}`]
146
153
  : [];
147
154
  throw new ClientPreflightError([
148
155
  `Not linking here: ${reason}`,
@@ -160,7 +167,7 @@ async function runDoctor() {
160
167
  return readiness.ready;
161
168
  }
162
169
  async function refreshRuntimeHealth(client) {
163
- const token = (await readConfig(homedir())).token;
170
+ const token = (await readConfig(homedir(), client)).token;
164
171
  await api.statusRequest(token);
165
172
  await api.contextRequest(token);
166
173
  await api.clientHandshake(token, {
@@ -174,7 +181,7 @@ async function refreshRuntimeHealth(client) {
174
181
  async function startServer(runtimeClient) {
175
182
  const server = createVibecheckServer({
176
183
  api,
177
- loadToken: async () => (await readConfig(homedir())).token,
184
+ loadToken: async () => (await readConfig(homedir(), runtimeClient ?? undefined)).token,
178
185
  });
179
186
  await server.connect(new StdioServerTransport());
180
187
  if (runtimeClient !== null) {
@@ -196,15 +203,14 @@ async function main() {
196
203
  ? await promptForClients(await detectInstalledClients(runner))
197
204
  : requestedClients
198
205
  : requestedClients;
206
+ const selectedClients = await preflightLinkClients(requestedOrPromptedClients, interactive);
199
207
  await linkAgent(args[1] ?? "", {
200
208
  home: homedir(),
201
209
  redeem: (code) => api.redeemLink(code),
202
210
  output,
203
211
  isInteractive: () => interactive,
204
212
  allowNonInteractive: !interactive || process.env.SOMACHECK_ALLOW_NON_INTERACTIVE === "1",
205
- preflight: async () => {
206
- await preflightLinkClients(requestedOrPromptedClients, interactive);
207
- },
213
+ client: selectedClients[0],
208
214
  });
209
215
  return (await runDoctor()) ? 0 : 2;
210
216
  }
@@ -124,7 +124,7 @@ function isManagedSomaCheckRegistration(client, stdout) {
124
124
  return false;
125
125
  const packageArgs = args.filter((arg) => arg.startsWith("@somacheck/vibecheck@"));
126
126
  return packageArgs.length === 1
127
- && /^@somacheck\/vibecheck@0\.[0-2]\.\d+$/.test(packageArgs[0])
127
+ && /^@somacheck\/vibecheck@0\.[0-3]\.\d+$/.test(packageArgs[0])
128
128
  && args.every((arg) => arg === "-y" || arg === "--yes" || arg === packageArgs[0]);
129
129
  }
130
130
  export async function isClientRegistered(client, runner) {
package/dist/config.js CHANGED
@@ -20,7 +20,7 @@ export async function preflightConfigPersistence(home) {
20
20
  await rm(renamedProbe, { force: true });
21
21
  }
22
22
  }
23
- export async function readConfig(home) {
23
+ export async function readConfig(home, client) {
24
24
  let value;
25
25
  try {
26
26
  value = JSON.parse(await readFile(configPath(home), "utf8"));
@@ -28,23 +28,41 @@ export async function readConfig(home) {
28
28
  catch {
29
29
  throw new Error("SomaCheck is not linked. Run: npx -y @somacheck/vibecheck link <CODE>");
30
30
  }
31
- if (value === null || typeof value !== "object" || !("token" in value)) {
31
+ if (value === null || typeof value !== "object") {
32
32
  throw new Error("SomaCheck link configuration is invalid.");
33
33
  }
34
- const token = value.token;
34
+ // 0.3.0 legacy config. Keep reading it so existing installations do not
35
+ // break, but every new link is written in the client-scoped v2 shape.
36
+ if ("token" in value) {
37
+ return validatedToken(value.token);
38
+ }
39
+ const stored = value;
40
+ if (stored.version !== 2 || stored.connections === null || typeof stored.connections !== "object") {
41
+ throw new Error("SomaCheck link configuration is invalid.");
42
+ }
43
+ const selected = client ?? stored.default_client;
44
+ if (selected !== "codex" && selected !== "claude") {
45
+ throw new Error("SomaCheck link configuration is invalid.");
46
+ }
47
+ return validatedToken(stored.connections[selected]?.token);
48
+ }
49
+ function validatedToken(token) {
35
50
  if (typeof token !== "string" || token.length === 0) {
36
51
  throw new Error("SomaCheck link configuration is invalid.");
37
52
  }
38
53
  return { token };
39
54
  }
40
- export async function writeConfig(home, config) {
55
+ export async function writeConfig(home, config, client) {
41
56
  const directory = join(home, ".sensie");
42
57
  const destination = configPath(home);
43
58
  const temporary = join(directory, `.config.json.${process.pid}.${Date.now()}.tmp`);
44
59
  await mkdir(directory, { recursive: true, mode: 0o700 });
45
60
  await chmod(directory, 0o700);
46
61
  try {
47
- await writeFile(temporary, `${JSON.stringify(config, null, 2)}\n`, {
62
+ const stored = client === undefined
63
+ ? config
64
+ : await mergedClientConfig(home, client, config);
65
+ await writeFile(temporary, `${JSON.stringify(stored, null, 2)}\n`, {
48
66
  encoding: "utf8",
49
67
  flag: "wx",
50
68
  mode: 0o600,
@@ -56,4 +74,27 @@ export async function writeConfig(home, config) {
56
74
  await rm(temporary, { force: true });
57
75
  }
58
76
  }
77
+ async function mergedClientConfig(home, client, config) {
78
+ let connections = {};
79
+ try {
80
+ const parsed = JSON.parse(await readFile(configPath(home), "utf8"));
81
+ if (parsed !== null && typeof parsed === "object" && "version" in parsed && parsed.version === 2
82
+ && "connections" in parsed && parsed.connections !== null && typeof parsed.connections === "object") {
83
+ const existing = parsed.connections;
84
+ for (const key of ["codex", "claude"]) {
85
+ const candidate = existing[key]?.token;
86
+ if (typeof candidate === "string" && candidate.length > 0) {
87
+ connections[key] = { token: candidate };
88
+ }
89
+ }
90
+ }
91
+ }
92
+ catch {
93
+ // Missing or legacy config is intentionally migrated by the new link.
94
+ // A legacy token cannot be attributed safely to a client, so it remains
95
+ // readable until the first scoped link and is then replaced.
96
+ }
97
+ connections = { ...connections, [client]: config };
98
+ return { version: 2, default_client: client, connections };
99
+ }
59
100
  //# sourceMappingURL=config.js.map
package/dist/constants.js CHANGED
@@ -2,7 +2,7 @@
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
4
  export const PACKAGE_NAME = "@somacheck/vibecheck";
5
- export const PACKAGE_VERSION = "0.3.0";
5
+ export const PACKAGE_VERSION = "0.4.0";
6
6
  export const PACKAGE_SPEC = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;
7
7
  export const MCP_SERVER_NAME = "vibecheck";
8
8
  export const LEGACY_HOSTED_MCP_SERVER_NAME = "somacheck";
package/dist/link.js CHANGED
@@ -54,7 +54,7 @@ export async function linkAgent(code, dependencies) {
54
54
  throw new LinkPersistenceError();
55
55
  }
56
56
  const token = await dependencies.redeem(normalizedCode);
57
- await writeConfig(dependencies.home, { token });
57
+ await writeConfig(dependencies.home, { token }, dependencies.client);
58
58
  dependencies.output("SomaCheck pairing code redeemed on this computer.");
59
59
  }
60
60
  //# sourceMappingURL=link.js.map
package/dist/server.js CHANGED
@@ -2,7 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { StatementPendingError } from "./vibecheck.js";
4
4
  import { SomaCheckCompatibilityError, SomaCheckHttpError } from "./api.js";
5
- import { PACKAGE_NAME, PACKAGE_VERSION } from "./constants.js";
5
+ import { PACKAGE_NAME, PACKAGE_SPEC, PACKAGE_VERSION } from "./constants.js";
6
6
  const cadenceSchema = z.object({
7
7
  reason: z.string(),
8
8
  create_due_at: z.string().nullable(),
@@ -202,7 +202,7 @@ function failureMessage(operation, error) {
202
202
  return failure(operationalFailureText(operation, error));
203
203
  }
204
204
  function operationalFailureText(operation, error) {
205
- const doctor = "Run: npx -y @somacheck/vibecheck@0.3.0 doctor";
205
+ const doctor = `Run: npx -y ${PACKAGE_SPEC} doctor`;
206
206
  if (error instanceof SomaCheckCompatibilityError) {
207
207
  return `Could not ${operation}: this MCP package and the SomaCheck backend are incompatible. ${doctor}`;
208
208
  }
package/package.json CHANGED
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "name": "@somacheck/vibecheck",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Ask your embodied sense for an answer through SomaCheck.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/sensie-app/Somacheck.git",
8
+ "directory": "packages/vibecheck"
9
+ },
5
10
  "type": "module",
6
11
  "bin": {
7
12
  "vibecheck": "dist/cli.js"
@@ -13,8 +18,8 @@
13
18
  "dist/*.js"
14
19
  ],
15
20
  "scripts": {
16
- "build": "tsc -p tsconfig.json",
17
- "test": "npm run build && node --import tsx --test test/*.test.ts && node --test test/server.test.mjs",
21
+ "build": "rm -rf dist && tsc -p tsconfig.json",
22
+ "test": "npm run build && node --import tsx --test test/*.test.ts && node --test test/*.test.mjs",
18
23
  "test:release-artifacts": "bash ./verify-release-artifacts.sh",
19
24
  "typecheck": "tsc -p tsconfig.json --noEmit",
20
25
  "prepack": "npm run build"