@somacheck/vibecheck 0.4.0 → 0.6.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,13 +9,13 @@ 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.4.0 link <CODE> --client <CLIENT>`
12
+ **What the `link` command does.** `npx -y @somacheck/vibecheck@0.6.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
16
16
  connection without printing the token. Setup is not reported ready until the
17
17
  selected client has the exact pinned `vibecheck` MCP registration, the backend
18
- accepts protocol `3` / toolset `vibecheck-0.3`, and both status and context
18
+ accepts protocol `5` / toolset `vibecheck-0.6`, and both status and context
19
19
  probes succeed. It contacts exactly one network host: the Supabase project below.
20
20
 
21
21
  **It will refuse unsafe sandbox linking, on purpose.** `link` checks safety
@@ -32,13 +32,19 @@ and that its persistent MCP config can be inspected and written. If that preflig
32
32
  fails, the pairing code is not used. Non-interactive implicit selection,
33
33
  `--client all`, and `--client none` are rejected before redemption.
34
34
 
35
- **What the server does once running.** It exposes MCP tools that create a
36
- statement for the user to test, read its outcome, recover recent completed
37
- gesture context, and keep a three-statement feed stocked. That is the whole
38
- surface.
39
-
40
- **What is sent.** A question string you supply, and a bearer token identifying
41
- the link. **What is never sent: raw sensor data.** The agent receives a verdict
35
+ **What the server does once running.** It exposes MCP tools that share bounded,
36
+ user-authorized context summaries, create a statement for the user to test,
37
+ read its outcome, recover recent completed gesture context, and keep a
38
+ three-statement feed stocked. Version 0.6 adds `request_vibecheck` for a
39
+ person-approved, immediate phone check-in. That path is deliberately separate
40
+ from the reflection feed.
41
+
42
+ **What is sent.** A statement string, bounded derived context observations the
43
+ agent explicitly shares, and a bearer token identifying the link. Tool
44
+ instructions forbid raw conversations, photos, identifiers, and diagnostic
45
+ claims; the backend enforces the bounded capsule shape and rejects
46
+ secret-shaped content. **What is never sent: raw sensor
47
+ data.** The agent receives a verdict
42
48
  (`aligned` / `unaligned`) plus a confidence percentage and latency. Model
43
49
  abstention is a failed capture, never a third user-facing outcome.
44
50
  Accelerometer and gyroscope streams never leave the user's device through this
@@ -72,9 +78,9 @@ before the pairing code is redeemed.
72
78
  To configure or repair a client later:
73
79
 
74
80
  ```text
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
81
+ npx -y @somacheck/vibecheck@0.6.0 setup codex
82
+ npx -y @somacheck/vibecheck@0.6.0 setup claude
83
+ npx -y @somacheck/vibecheck@0.6.0 doctor
78
84
  ```
79
85
 
80
86
  Manual registration remains available:
@@ -84,13 +90,13 @@ Manual registration remains available:
84
90
  "mcpServers": {
85
91
  "vibecheck": {
86
92
  "command": "npx",
87
- "args": ["-y", "@somacheck/vibecheck@0.4.0", "serve", "--client", "codex"]
93
+ "args": ["-y", "@somacheck/vibecheck@0.6.0", "serve", "--client", "codex"]
88
94
  }
89
95
  }
90
96
  }
91
97
  ```
92
98
 
93
- For Claude Code: `claude mcp add --scope user vibecheck -- npx -y @somacheck/vibecheck@0.4.0 serve --client claude`
99
+ For Claude Code: `claude mcp add --scope user vibecheck -- npx -y @somacheck/vibecheck@0.6.0 serve --client claude`
94
100
 
95
101
  The link step writes only the bearer token in `~/.sensie/config.json`. The client
96
102
  setup step asks Codex or Claude to add the pinned MCP command to that client's own
@@ -98,7 +104,7 @@ configuration; it never copies the bearer token there. The interactive command
98
104
  completes both steps. Most clients only pick up a new server after restart.
99
105
 
100
106
  If `doctor` reports another MCP server named `somacheck`, that is the legacy
101
- hosted connector and is not part of this local 0.3 release. The setup command
107
+ hosted connector and is separate from this local package. The setup command
102
108
  will explain it and leave it alone. The canonical local MCP key is `vibecheck`.
103
109
 
104
110
  ## Tools
@@ -107,8 +113,10 @@ will explain it and leave it alone. The canonical local MCP key is `vibecheck`.
107
113
  |---|---|
108
114
  | `get_vibecheck_context` | Read recent completed gestures for this exact agent link without retaining every `request_id`. |
109
115
  | `get_vibecheck_status` | Read the live three-proposition cache and how many distinct insights it needs. |
116
+ | `share_somacheck_context` | Share 1–20 bounded, derived, user-authorized observations; raw conversations and secrets are rejected. |
110
117
  | `post_vibecheck_statement` | Add one to three personalized statements without blocking; returns a stable `request_id` for each. |
111
118
  | `get_vibecheck_result` | Read that exact proposition once by `request_id`; returns queued, pending, or its terminal result. |
119
+ | `request_vibecheck` | After the person accepts the exact statement, send one immediate phone vibecheck with a retry-safe UUID and return an opaque `live:<uuid>` handle. |
112
120
 
113
121
  ## How to use it well
114
122
 
@@ -118,6 +126,27 @@ prompt is **a statement for the person to test**, not a question about facts:
118
126
  something to test, never as a claim about them. If it is wrong, their body says
119
127
  `unaligned`, and that is a useful answer rather than a failure.
120
128
 
129
+ For a Live Ask, first show the exact statement and invite the person plainly:
130
+
131
+ > Want a quick vibecheck before I continue? I'll send this statement to your
132
+ > phone; you answer with your wrist gesture, and I'll use the result as a
133
+ > signal—not a verdict.
134
+
135
+ Call `request_vibecheck` only after they accept, with `human_confirmed: true`
136
+ and a new UUID in `idempotency_key`. Reuse that UUID only when retrying the
137
+ same statement. The returned `live:<uuid>` handle can be passed to
138
+ `get_vibecheck_result` later. Each read is non-blocking; do not continuously
139
+ poll, and do not re-ask the same decision to obtain a preferred result.
140
+
141
+ After a completed result, explain what the signal changes in your next action.
142
+ Never treat it as authorization or say that the person's body made the
143
+ decision. A typed response from the person always overrides the signal.
144
+
145
+ An immediate vibecheck is not proposition caching. It sends one person-approved statement
146
+ for the current choice, expires, and remains bound to its originating client.
147
+ The reflection feed remains asynchronous and keeps its existing three-item
148
+ cache behavior.
149
+
121
150
  The agent maintains three distinct insights. SomaCheck presents one at a time;
122
151
  the other two remain cached. After a completed gesture, the next cached insight
123
152
  is promoted and the open slot is marked for replenishment automatically. Cached insights
package/dist/api.js CHANGED
@@ -2,6 +2,10 @@ import { StatementPendingError, } from "./vibecheck.js";
2
2
  import { BACKEND_PROTOCOL_VERSION, PACKAGE_VERSION, TOOLSET_VERSION } from "./constants.js";
3
3
  const STATEMENT_PENDING_MARKER = "SC_VC_STATEMENT_PENDING";
4
4
  const UPGRADE_REQUIRED_MARKER = "SC_VC_UPGRADE_REQUIRED";
5
+ const LIVE_ASK_CONFLICT_MARKER = "SC_VC_VIBECHECK_IDEMPOTENCY_CONFLICT";
6
+ const LIVE_ASK_PENDING_MARKER = "SC_VC_VIBECHECK_PENDING";
7
+ const LIVE_ASK_CLIENT_REQUIRED_MARKER = "SC_VC_VIBECHECK_CLIENT_REQUIRED";
8
+ const LIVE_ASK_CLIENT_NOT_READY_MARKER = "SC_VC_VIBECHECK_CLIENT_NOT_READY";
5
9
  const STATUSES = new Set(["queued", "pending", "answered", "expired", "cancelled"]);
6
10
  const VERDICTS = new Set(["aligned", "unaligned"]);
7
11
  const HANDSHAKE_READINESS_STATUSES = new Set([
@@ -10,6 +14,19 @@ const HANDSHAKE_READINESS_STATUSES = new Set([
10
14
  "setup_incomplete",
11
15
  "health_check_failed",
12
16
  ]);
17
+ const PUSH_STATUSES = new Set([
18
+ "queued", "processing", "sent", "failed", "skipped",
19
+ ]);
20
+ const CONTEXT_CATEGORY = {
21
+ goal: "goals_projects", project: "goals_projects",
22
+ decision: "decisions_commitments", commitment: "decisions_commitments",
23
+ tension: "tensions_questions_blind_spots", question: "tensions_questions_blind_spots",
24
+ blindSpot: "tensions_questions_blind_spots",
25
+ relationship: "relationships_situational", situation: "relationships_situational",
26
+ change: "changes_deadlines_developments", deadline: "changes_deadlines_developments",
27
+ development: "changes_deadlines_developments", moodTheme: "emotional_mood_themes",
28
+ preference: "corrections_dismissals_saves_preferences",
29
+ };
13
30
  export class SomaCheckHttpError extends Error {
14
31
  status;
15
32
  constructor(status) {
@@ -30,6 +47,24 @@ export class SomaCheckCompatibilityError extends Error {
30
47
  this.name = "SomaCheckCompatibilityError";
31
48
  }
32
49
  }
50
+ export class SomaCheckLiveAskConflictError extends Error {
51
+ constructor() {
52
+ super("That Live Ask retry key is already bound to a different statement.");
53
+ this.name = "SomaCheckLiveAskConflictError";
54
+ }
55
+ }
56
+ export class SomaCheckLiveAskPendingError extends Error {
57
+ constructor() {
58
+ super("A vibecheck is already waiting for this connection.");
59
+ this.name = "SomaCheckLiveAskPendingError";
60
+ }
61
+ }
62
+ export class SomaCheckLiveAskClientError extends Error {
63
+ constructor() {
64
+ super("Live Ask requires an exact ready client connection.");
65
+ this.name = "SomaCheckLiveAskClientError";
66
+ }
67
+ }
33
68
  function firstRow(value) {
34
69
  const row = Array.isArray(value) ? value[0] : value;
35
70
  if (row === null || typeof row !== "object" || Array.isArray(row)) {
@@ -145,6 +180,39 @@ export function decodeCreatedRequests(value) {
145
180
  };
146
181
  });
147
182
  }
183
+ /** Strict decoder for agent_vibecheck_create. PostgREST wraps the
184
+ * set-returning function in an array. The backend intentionally uses its
185
+ * canonical `answered` lifecycle name; the provider-neutral adapter below
186
+ * maps that to the tool's clearer `completed` state. */
187
+ export function decodeLiveVibecheckCreation(value) {
188
+ const row = firstRow(value);
189
+ const status = requiredString(row, "status");
190
+ if (status !== "pending" && status !== "answered" && status !== "expired" && status !== "cancelled") {
191
+ throw new Error("SomaCheck returned an invalid vibecheck status.");
192
+ }
193
+ if (requiredString(row, "request_kind") !== "live_ask") {
194
+ throw new Error("SomaCheck returned an invalid vibecheck request kind.");
195
+ }
196
+ const pushStatus = nullableString(row, "push_status");
197
+ if (pushStatus !== null && !PUSH_STATUSES.has(pushStatus)) {
198
+ throw new Error("SomaCheck returned an invalid vibecheck delivery state.");
199
+ }
200
+ return {
201
+ request_id: requiredString(row, "request_id"),
202
+ status,
203
+ expires_at: requiredTimestamp(row, "expires_at"),
204
+ idempotent_replay: requiredBoolean(row, "idempotent_replay"),
205
+ push_status: pushStatus,
206
+ cooldown_until: nullableTimestamp(row, "cooldown_until"),
207
+ };
208
+ }
209
+ function nullableTimestamp(row, key) {
210
+ const value = nullableString(row, key);
211
+ if (value !== null && !Number.isFinite(Date.parse(value))) {
212
+ throw new Error("SomaCheck returned an invalid timestamp.");
213
+ }
214
+ return value;
215
+ }
148
216
  export function decodePollResponse(value) {
149
217
  const row = asObject(value);
150
218
  const status = row.status;
@@ -178,6 +246,21 @@ export function decodePollResponse(value) {
178
246
  latency_s: latency,
179
247
  };
180
248
  }
249
+ export function decodeLiveVibecheckResult(value, expectedRequestId) {
250
+ const row = firstRow(value);
251
+ if (requiredString(row, "request_id") !== expectedRequestId) {
252
+ throw new Error("SomaCheck returned a mismatched vibecheck result.");
253
+ }
254
+ if (requiredString(row, "request_kind") !== "live_ask") {
255
+ throw new Error("SomaCheck returned an invalid vibecheck request kind.");
256
+ }
257
+ const result = decodePollResponse(row);
258
+ if (result.status === "queued") {
259
+ throw new Error("SomaCheck returned an invalid vibecheck status.");
260
+ }
261
+ requiredTimestamp(row, "expires_at");
262
+ return result;
263
+ }
181
264
  export function decodeVibecheckContext(value) {
182
265
  return allRows(value).map((row) => {
183
266
  const verdict = requiredString(row, "verdict");
@@ -220,6 +303,24 @@ function parseHandshake(row) {
220
303
  }
221
304
  return result;
222
305
  }
306
+ export function decodeAgentContextShare(value) {
307
+ const row = firstRow(value);
308
+ const state = requiredString(row, "state");
309
+ if (state !== "ready")
310
+ throw new Error("SomaCheck returned an invalid context state.");
311
+ const observationCount = requiredInteger(row, "observation_count");
312
+ if (observationCount < 1 || observationCount > 20) {
313
+ throw new Error("SomaCheck returned an invalid observation count.");
314
+ }
315
+ return {
316
+ source_id: requiredString(row, "source_id"),
317
+ entry_id: requiredString(row, "entry_id"),
318
+ accepted: requiredBoolean(row, "accepted"),
319
+ state,
320
+ observation_count: observationCount,
321
+ captured_at: requiredTimestamp(row, "captured_at"),
322
+ };
323
+ }
223
324
  export class SupabaseAgentApi {
224
325
  #endpoint;
225
326
  #apiKey;
@@ -233,20 +334,72 @@ export class SupabaseAgentApi {
233
334
  const row = await this.#rpc("agent_link_redeem", { code });
234
335
  return requiredString(row, "token");
235
336
  }
337
+ async requestVibecheck(token, identity, statement, idempotencyKey) {
338
+ if (identity.kind !== "local")
339
+ throw new SomaCheckLiveAskClientError();
340
+ const clientKey = identity.client_key;
341
+ const created = decodeLiveVibecheckCreation(await this.#rpcJson("agent_vibecheck_create", {
342
+ token,
343
+ statement,
344
+ idempotency_key: idempotencyKey,
345
+ client_key: clientKey,
346
+ }));
347
+ const lifecycle = created.status === "answered"
348
+ ? await this.liveVibecheckResult(token, identity, created.request_id)
349
+ : { status: created.status, verdict: null, confidence: null, latency_s: null };
350
+ return {
351
+ request_id: `live:${created.request_id}`,
352
+ state: lifecycle.status === "answered" ? "completed" : lifecycle.status,
353
+ verdict: lifecycle.verdict,
354
+ confidence: lifecycle.confidence,
355
+ expires_at: created.expires_at,
356
+ idempotent_replay: created.idempotent_replay,
357
+ delivery_state: created.push_status,
358
+ cooldown_until: created.cooldown_until,
359
+ };
360
+ }
236
361
  async createRequests(token, statements) {
237
362
  return decodeCreatedRequests(await this.#rpcJson("agent_proposition_batch_create", { token, statements }));
238
363
  }
239
364
  async pollRequest(token, requestId) {
240
365
  return decodePollResponse(await this.#rpc("agent_proposition_result", { token, proposition_id: requestId }));
241
366
  }
367
+ async liveVibecheckResult(token, identity, requestId) {
368
+ if (identity.kind !== "local")
369
+ throw new SomaCheckLiveAskClientError();
370
+ return decodeLiveVibecheckResult(await this.#rpcJson("agent_vibecheck_result", {
371
+ token,
372
+ live_ask_id: requestId,
373
+ client_key: identity.client_key,
374
+ }), requestId);
375
+ }
242
376
  async statusRequest(token) {
243
377
  return decodeVibecheckStatus(await this.#rpc("agent_proposition_cache_status", { token }));
244
378
  }
245
379
  async contextRequest(token, limit = 20) {
246
380
  return decodeVibecheckContext(await this.#rpcJson("agent_proposition_context", { token, p_limit: limit }));
247
381
  }
382
+ async shareContext(token, observations) {
383
+ const capturedAt = new Date().toISOString();
384
+ const payload = {};
385
+ for (const observation of observations) {
386
+ const category = CONTEXT_CATEGORY[observation.type];
387
+ (payload[category] ??= []).push({
388
+ type: observation.type,
389
+ summary: observation.summary.trim(),
390
+ confidence: observation.confidence,
391
+ evidence_count: observation.evidence_count,
392
+ captured_at: capturedAt,
393
+ });
394
+ }
395
+ return decodeAgentContextShare(await this.#rpcJson("agent_context_capsule_share", {
396
+ token,
397
+ p_payload: payload,
398
+ p_confidence: observations.reduce((sum, item) => sum + item.confidence, 0) / observations.length,
399
+ }));
400
+ }
248
401
  async clientHandshake(token, input) {
249
- const row = await this.#rpc("agent_client_handshake", {
402
+ const row = await this.#rpc("agent_client_handshake_v2", {
250
403
  token,
251
404
  p_client_key: input.client_key,
252
405
  p_client_label: input.client_label,
@@ -282,6 +435,16 @@ export class SupabaseAgentApi {
282
435
  if (response.status === 426 || responseBody.includes(UPGRADE_REQUIRED_MARKER)) {
283
436
  throw new SomaCheckCompatibilityError(null, null, null);
284
437
  }
438
+ if (response.status === 409 && responseBody.includes(LIVE_ASK_CONFLICT_MARKER)) {
439
+ throw new SomaCheckLiveAskConflictError();
440
+ }
441
+ if (response.status === 409 && responseBody.includes(LIVE_ASK_PENDING_MARKER)) {
442
+ throw new SomaCheckLiveAskPendingError();
443
+ }
444
+ if (responseBody.includes(LIVE_ASK_CLIENT_REQUIRED_MARKER)
445
+ || responseBody.includes(LIVE_ASK_CLIENT_NOT_READY_MARKER)) {
446
+ throw new SomaCheckLiveAskClientError();
447
+ }
285
448
  if (response.status === 409 || responseBody.includes(STATEMENT_PENDING_MARKER)) {
286
449
  throw new StatementPendingError();
287
450
  }
package/dist/cli.js CHANGED
@@ -182,6 +182,9 @@ async function startServer(runtimeClient) {
182
182
  const server = createVibecheckServer({
183
183
  api,
184
184
  loadToken: async () => (await readConfig(homedir(), runtimeClient ?? undefined)).token,
185
+ identity: runtimeClient === null
186
+ ? { kind: "legacy" }
187
+ : { kind: "local", client_key: runtimeClient },
185
188
  });
186
189
  await server.connect(new StdioServerTransport());
187
190
  if (runtimeClient !== null) {
@@ -123,9 +123,25 @@ function isManagedSomaCheckRegistration(client, stdout) {
123
123
  if (command !== "npx")
124
124
  return false;
125
125
  const packageArgs = args.filter((arg) => arg.startsWith("@somacheck/vibecheck@"));
126
- return packageArgs.length === 1
127
- && /^@somacheck\/vibecheck@0\.[0-3]\.\d+$/.test(packageArgs[0])
128
- && args.every((arg) => arg === "-y" || arg === "--yes" || arg === packageArgs[0]);
126
+ if (packageArgs.length !== 1)
127
+ return false;
128
+ const packageArg = packageArgs[0];
129
+ const legacyWithoutClient = /^@somacheck\/vibecheck@0\.[0-3]\.\d+$/.test(packageArg)
130
+ && args.every((arg) => arg === "-y" || arg === "--yes" || arg === packageArg);
131
+ if (legacyWithoutClient)
132
+ return true;
133
+ // 0.5 introduced the exact local runtime identity required by Live Ask.
134
+ // It is safe to replace only the canonical command generated by that
135
+ // release. Custom wrappers, extra arguments, disabled entries, and a
136
+ // registration naming the other client remain user-owned.
137
+ const previousExactClient = /^@somacheck\/vibecheck@0\.5\.\d+$/.test(packageArg)
138
+ && args.length === 5
139
+ && args[0] === "-y"
140
+ && args[1] === packageArg
141
+ && args[2] === "serve"
142
+ && args[3] === "--client"
143
+ && args[4] === client;
144
+ return previousExactClient;
129
145
  }
130
146
  export async function isClientRegistered(client, runner) {
131
147
  return (await clientRegistrationState(client, runner)) === "current";
package/dist/constants.js CHANGED
@@ -2,16 +2,18 @@
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.4.0";
5
+ export const PACKAGE_VERSION = "0.6.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";
9
- export const BACKEND_PROTOCOL_VERSION = 3;
10
- export const TOOLSET_VERSION = "vibecheck-0.3";
9
+ export const BACKEND_PROTOCOL_VERSION = 5;
10
+ export const TOOLSET_VERSION = "vibecheck-0.6";
11
11
  export const TOOL_NAMES = [
12
12
  "get_vibecheck_context",
13
13
  "get_vibecheck_status",
14
+ "share_somacheck_context",
14
15
  "post_vibecheck_statement",
15
16
  "get_vibecheck_result",
17
+ "request_vibecheck",
16
18
  ];
17
19
  //# sourceMappingURL=constants.js.map
package/dist/readiness.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readConfig } from "./config.js";
2
2
  import { SomaCheckCompatibilityError, SomaCheckHttpError } from "./api.js";
3
3
  import { clientDisplayName, clientRegistrationState, detectLegacyHostedRegistration, isClientInstalled, manualLegacyHostedRemoveCommand, manualRemoveCommand, manualSetupCommand, } from "./client-setup.js";
4
+ import { BACKEND_PROTOCOL_VERSION, PACKAGE_VERSION } from "./constants.js";
4
5
  export async function checkReadiness(dependencies) {
5
6
  let token;
6
7
  try {
@@ -62,7 +63,7 @@ export async function checkReadiness(dependencies) {
62
63
  ? " It currently reports: Needs authentication."
63
64
  : "";
64
65
  dependencies.output(`○ ${clientDisplayName(client)} also has a separate legacy server named "somacheck".${authNote}`);
65
- dependencies.output(' SomaCheck MCP 0.3 uses "vibecheck"; the legacy entry was not changed.');
66
+ dependencies.output(` SomaCheck MCP ${PACKAGE_VERSION} uses "vibecheck"; the legacy entry was not changed.`);
66
67
  dependencies.output(` Optional manual cleanup: ${manualLegacyHostedRemoveCommand(client)}`);
67
68
  }
68
69
  const registration = await clientRegistrationState(client, dependencies.runner);
@@ -81,7 +82,7 @@ export async function checkReadiness(dependencies) {
81
82
  }
82
83
  }
83
84
  if (registeredClients.length === 0) {
84
- dependencies.output("✗ No supported agent client has the current SomaCheck 0.3 configuration.");
85
+ dependencies.output(`✗ No supported agent client has the current SomaCheck ${PACKAGE_VERSION} configuration.`);
85
86
  if (installedClientCount === 0) {
86
87
  dependencies.output(" Install Codex or Claude Code, then run the matching setup command.");
87
88
  }
@@ -101,7 +102,7 @@ export async function checkReadiness(dependencies) {
101
102
  if (handshake.readiness_status === "ready") {
102
103
  backendCompatible = true;
103
104
  readyClients.push(client);
104
- dependencies.output(`✓ ${clientDisplayName(client)} completed the SomaCheck protocol-3 health check.`);
105
+ dependencies.output(`✓ ${clientDisplayName(client)} completed the SomaCheck protocol-${BACKEND_PROTOCOL_VERSION} health check.`);
105
106
  }
106
107
  else {
107
108
  dependencies.output(`✗ ${clientDisplayName(client)} is not ready yet (${handshake.readiness_status}).`);
package/dist/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import { StatementPendingError } from "./vibecheck.js";
4
- import { SomaCheckCompatibilityError, SomaCheckHttpError } from "./api.js";
3
+ import { StatementPendingError, } from "./vibecheck.js";
4
+ import { SomaCheckCompatibilityError, SomaCheckHttpError, SomaCheckLiveAskClientError, SomaCheckLiveAskConflictError, SomaCheckLiveAskPendingError, } from "./api.js";
5
5
  import { PACKAGE_NAME, PACKAGE_SPEC, PACKAGE_VERSION } from "./constants.js";
6
6
  const cadenceSchema = z.object({
7
7
  reason: z.string(),
@@ -48,10 +48,57 @@ const contextItemSchema = z.object({
48
48
  answered_at: z.string().datetime({ offset: true }),
49
49
  });
50
50
  const contextSchema = { checkins: z.array(contextItemSchema) };
51
+ const liveAskErrorCodeSchema = z.enum([
52
+ "link_revoked",
53
+ "upgrade_required",
54
+ "setup_required",
55
+ "request_conflict",
56
+ "vibecheck_pending",
57
+ "rate_limited",
58
+ "live_ask_unavailable",
59
+ "backend_unavailable",
60
+ "connection_failed",
61
+ ]);
62
+ const liveAskSchema = {
63
+ state: z.enum(["pending", "completed", "expired", "cancelled", "error"]),
64
+ request_id: z.string().nullable(),
65
+ verdict: z.enum(["aligned", "unaligned"]).nullable(),
66
+ confidence: z.number().min(0).max(1).nullable(),
67
+ expires_at: z.string().datetime({ offset: true }).nullable(),
68
+ idempotent_replay: z.boolean().nullable(),
69
+ delivery_state: z.enum(["queued", "processing", "sent", "failed", "skipped"]).nullable(),
70
+ cooldown_until: z.string().datetime({ offset: true }).nullable(),
71
+ error_code: liveAskErrorCodeSchema.nullable(),
72
+ };
73
+ const agentContextTypeSchema = z.enum([
74
+ "goal", "project", "decision", "commitment", "tension", "question",
75
+ "blindSpot", "relationship", "situation", "change", "deadline",
76
+ "development", "moodTheme", "preference",
77
+ ]);
78
+ const agentContextObservationSchema = z.object({
79
+ type: agentContextTypeSchema,
80
+ summary: z.string().trim().min(1).max(500)
81
+ .describe("A concise derived theme or fact the person authorized you to share. Never paste raw conversations."),
82
+ confidence: z.number().min(0).max(1).default(0.8),
83
+ evidence_count: z.number().int().min(1).max(100).default(1),
84
+ });
85
+ const contextShareSchema = {
86
+ source_id: z.string(), entry_id: z.string(), accepted: z.boolean(),
87
+ state: z.literal("ready"), observation_count: z.number().int().min(1).max(20),
88
+ captured_at: z.string().datetime({ offset: true }),
89
+ };
51
90
  const SERVER_INSTRUCTIONS = [
52
91
  "SomaCheck lets you offer the person a statement to test through a quick phone check-in.",
53
92
  "Call get_vibecheck_context and get_vibecheck_status at the start of a session or background run, and call status again after a completed check-in.",
54
93
  "Treat gesture outcomes as contextual signals, never fixed facts or blanket authorization.",
94
+ "Use request_vibecheck when a current choice would benefit from the person's own felt response; never use it to evaluate someone else.",
95
+ "First show the exact statement and ask: Want a quick vibecheck before I continue? I'll send this statement to your phone; you answer with your wrist gesture, and I'll use the result as a signal—not a verdict. Call only after the person accepts.",
96
+ "Use at most one vibecheck per decision. Never re-ask to obtain a preferred answer.",
97
+ "An immediate vibecheck is separate from the asynchronous reflection feed. Do not use it to replenish cached propositions.",
98
+ "Use a fresh idempotency_key for each new vibecheck, and reuse that exact key only when retrying the same statement.",
99
+ "After a completed result, say what the signal changes in your next action. Never say 'your body decided'; a typed human response always overrides the signal.",
100
+ "When you have meaningful user-authorized context, call share_somacheck_context with concise derived observations before creating propositions.",
101
+ "Never share raw conversation text, photos, credentials, private identifiers, or diagnostic claims.",
55
102
  "Maintain three distinct propositions drawn from your own context about this person.",
56
103
  "When propositions_needed is greater than zero, post exactly that many in one call.",
57
104
  "Offer each statement as something to test, never as a claim of fact about the user.",
@@ -111,6 +158,32 @@ export function createVibecheckServer(dependencies) {
111
158
  return failureMessage("read SomaCheck status", error);
112
159
  }
113
160
  });
161
+ server.registerTool("share_somacheck_context", {
162
+ title: "Share SomaCheck Context",
163
+ description: "Share 1-20 concise, user-authorized context observations so SomaCheck can prepare richer propositions. Send derived summaries only—never raw conversation text, photos, credentials, identifiers, or diagnostic claims.",
164
+ inputSchema: {
165
+ observations: z.array(agentContextObservationSchema).min(1).max(20),
166
+ },
167
+ outputSchema: contextShareSchema,
168
+ annotations: {
169
+ readOnlyHint: false,
170
+ destructiveHint: false,
171
+ idempotentHint: true,
172
+ openWorldHint: false,
173
+ },
174
+ }, async ({ observations }) => {
175
+ try {
176
+ const token = await dependencies.loadToken();
177
+ const result = await dependencies.api.shareContext(token, observations);
178
+ return {
179
+ content: [{ type: "text", text: `Shared ${result.observation_count} bounded context observation${result.observation_count === 1 ? "" : "s"}. SomaCheck can now prepare contextual propositions.` }],
180
+ structuredContent: { ...result },
181
+ };
182
+ }
183
+ catch (error) {
184
+ return failureMessage("share context with SomaCheck", error);
185
+ }
186
+ });
114
187
  server.registerTool("post_vibecheck_statement", {
115
188
  title: "Post Vibecheck Statement",
116
189
  description: "Fill the person's SomaCheck cache with one to three personalized statements and return immediately. Call get_vibecheck_status first and submit exactly propositions_needed statements.",
@@ -144,9 +217,9 @@ export function createVibecheckServer(dependencies) {
144
217
  });
145
218
  server.registerTool("get_vibecheck_result", {
146
219
  title: "Get Vibecheck Result",
147
- description: "Read one proposition by request_id. This is a single non-blocking read: queued is cached, and pending is presented but not answered.",
220
+ description: "Read one exact proposition or immediate vibecheck by request_id. This is a single non-blocking read: queued is cached, and pending is not answered. Immediate vibecheck handles begin with live: and remain bound to the originating client.",
148
221
  inputSchema: {
149
- request_id: z.string().min(1).describe("The request_id returned by post_vibecheck_statement."),
222
+ request_id: z.string().min(1).describe("The opaque request_id returned by post_vibecheck_statement or request_vibecheck."),
150
223
  },
151
224
  outputSchema: resultSchema,
152
225
  annotations: {
@@ -158,10 +231,19 @@ export function createVibecheckServer(dependencies) {
158
231
  }, async ({ request_id }) => {
159
232
  try {
160
233
  const token = await dependencies.loadToken();
161
- const result = await dependencies.api.pollRequest(token, request_id);
234
+ const liveRequestId = parseLiveVibecheckHandle(request_id);
235
+ const identity = dependencies.identity ?? { kind: "legacy" };
236
+ if (liveRequestId !== null && identity.kind === "legacy") {
237
+ return failure("Could not read that vibecheck: Live Ask needs the exact ready client connection that created it. Run setup and restart the client.");
238
+ }
239
+ const result = liveRequestId === null
240
+ ? await dependencies.api.pollRequest(token, request_id)
241
+ : await dependencies.api.liveVibecheckResult(token, identity, liveRequestId);
162
242
  const structuredContent = { request_id, ...result };
163
243
  const text = result.status === "answered"
164
- ? `Answered: ${Math.round((result.confidence ?? 0) * 100)}% ${result.verdict}.`
244
+ ? liveRequestId === null
245
+ ? `Answered: ${Math.round((result.confidence ?? 0) * 100)}% ${result.verdict}.`
246
+ : `Vibecheck completed: ${Math.round((result.confidence ?? 0) * 100)}% ${result.verdict}. Treat this as a signal, say what it changes in your next action, and defer to any typed response from the person.`
165
247
  : result.status === "queued"
166
248
  ? "Queued. The proposition is cached until the person advances their feed."
167
249
  : result.status === "pending"
@@ -173,6 +255,46 @@ export function createVibecheckServer(dependencies) {
173
255
  return failureMessage("read that SomaCheck result", error);
174
256
  }
175
257
  });
258
+ server.registerTool("request_vibecheck", {
259
+ title: "Request a Vibecheck",
260
+ description: "Send one plain-language statement to the person's phone for a vibecheck answered with a wrist gesture. First show the exact statement and call only after the person accepts. Use this only when their own felt response would help with the current choice, not for routine feed content or to judge another person. Returns a stable request_id immediately; call get_vibecheck_result later once, without continuous polling.",
261
+ inputSchema: {
262
+ statement: z.string().trim().min(1).max(1000)
263
+ .describe("One plain-language statement for the person to test; offer a signal, not a fact, diagnosis, or instruction."),
264
+ idempotency_key: z.string().uuid()
265
+ .describe("A new UUID for this logical ask. Reuse the same UUID only to retry the exact same statement; retries will not create another phone request."),
266
+ human_confirmed: z.literal(true)
267
+ .describe("Confirms the agent showed the exact statement and the person accepted this one vibecheck."),
268
+ },
269
+ outputSchema: liveAskSchema,
270
+ annotations: {
271
+ readOnlyHint: false,
272
+ destructiveHint: false,
273
+ idempotentHint: true,
274
+ openWorldHint: false,
275
+ },
276
+ }, async ({ statement, idempotency_key }) => {
277
+ try {
278
+ const identity = dependencies.identity ?? { kind: "legacy" };
279
+ if (identity.kind === "legacy") {
280
+ return liveAskFailure(new SomaCheckLiveAskClientError());
281
+ }
282
+ const token = await dependencies.loadToken();
283
+ const result = await dependencies.api.requestVibecheck(token, identity, statement.trim(), idempotency_key);
284
+ const structuredContent = { ...result, error_code: null };
285
+ const text = result.state === "completed"
286
+ ? `Vibecheck completed: ${Math.round((result.confidence ?? 0) * 100)}% ${result.verdict}. Treat this as a signal, say what it changes in your next action, and defer to any typed response from the person.`
287
+ : result.state === "pending"
288
+ ? result.delivery_state === "failed" || result.delivery_state === "skipped"
289
+ ? `Vibecheck ${result.request_id} was stored, but phone delivery is ${result.delivery_state}. It expires at ${result.expires_at}. Do not repost it or change the idempotency_key; check the phone setup before a later decision.`
290
+ : `Vibecheck requested. Keep request_id ${result.request_id} and check it later with get_vibecheck_result. It expires at ${result.expires_at}; do not poll continuously or re-ask for this decision.`
291
+ : `This vibecheck is ${result.state}. Do not create another ask for the same decision.`;
292
+ return { content: [{ type: "text", text }], structuredContent };
293
+ }
294
+ catch (error) {
295
+ return liveAskFailure(error);
296
+ }
297
+ });
176
298
  return server;
177
299
  }
178
300
  function summarise(status) {
@@ -192,6 +314,16 @@ function formatDuration(seconds) {
192
314
  const hours = seconds / 3600;
193
315
  return Number.isInteger(hours) ? `${hours}-hour` : `${Math.round(seconds / 60)}-minute`;
194
316
  }
317
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
318
+ function parseLiveVibecheckHandle(handle) {
319
+ if (!handle.startsWith("live:"))
320
+ return null;
321
+ const requestId = handle.slice("live:".length);
322
+ if (!UUID_PATTERN.test(requestId)) {
323
+ throw new Error("The Live Ask request_id is malformed.");
324
+ }
325
+ return requestId;
326
+ }
195
327
  function failure(text) {
196
328
  return {
197
329
  isError: true,
@@ -201,6 +333,62 @@ function failure(text) {
201
333
  function failureMessage(operation, error) {
202
334
  return failure(operationalFailureText(operation, error));
203
335
  }
336
+ function liveAskFailure(error) {
337
+ const doctor = `Run: npx -y ${PACKAGE_SPEC} doctor`;
338
+ let errorCode;
339
+ let text;
340
+ if (error instanceof SomaCheckCompatibilityError) {
341
+ errorCode = "upgrade_required";
342
+ text = `Could not request a vibecheck: this package needs an upgrade before immediate vibechecks can be used. ${doctor}`;
343
+ }
344
+ else if (error instanceof SomaCheckLiveAskClientError) {
345
+ errorCode = "setup_required";
346
+ text = `Could not request a vibecheck: immediate vibechecks need an exact ready agent connection. Run the pinned serve command through setup, then restart the client. ${doctor}`;
347
+ }
348
+ else if (error instanceof SomaCheckLiveAskConflictError) {
349
+ errorCode = "request_conflict";
350
+ text = "Could not request a vibecheck: that retry key already belongs to a different statement. Use the original statement, or use a new UUID for a genuinely new ask.";
351
+ }
352
+ else if (error instanceof SomaCheckLiveAskPendingError) {
353
+ errorCode = "vibecheck_pending";
354
+ text = "Could not request a vibecheck: one is already waiting for this connection. Keep the existing request; do not re-ask or change the retry key.";
355
+ }
356
+ else if (error instanceof SomaCheckHttpError && (error.status === 401 || error.status === 403)) {
357
+ errorCode = "link_revoked";
358
+ text = `Could not request a vibecheck: this SomaCheck link was rejected or revoked. Reconnect in the app. ${doctor}`;
359
+ }
360
+ else if (error instanceof SomaCheckHttpError && error.status === 404) {
361
+ errorCode = "live_ask_unavailable";
362
+ text = `Could not request a vibecheck: immediate vibechecks are not available on this SomaCheck backend yet. ${doctor}`;
363
+ }
364
+ else if (error instanceof SomaCheckHttpError && error.status === 429) {
365
+ errorCode = "rate_limited";
366
+ text = "Could not request a vibecheck: too many vibechecks were requested recently. Wait before making a new ask; do not change the retry key for the same ask.";
367
+ }
368
+ else if (error instanceof SomaCheckHttpError && error.status >= 500) {
369
+ errorCode = "backend_unavailable";
370
+ text = `Could not request a vibecheck: the SomaCheck backend is temporarily unavailable. Retry later with the same idempotency_key. ${doctor}`;
371
+ }
372
+ else {
373
+ errorCode = "connection_failed";
374
+ text = `Could not request a vibecheck: the local link or network check failed. Retry later with the same idempotency_key. ${doctor}`;
375
+ }
376
+ return {
377
+ isError: true,
378
+ content: [{ type: "text", text }],
379
+ structuredContent: {
380
+ state: "error",
381
+ request_id: null,
382
+ verdict: null,
383
+ confidence: null,
384
+ expires_at: null,
385
+ idempotent_replay: null,
386
+ delivery_state: null,
387
+ cooldown_until: null,
388
+ error_code: errorCode,
389
+ },
390
+ };
391
+ }
204
392
  function operationalFailureText(operation, error) {
205
393
  const doctor = `Run: npx -y ${PACKAGE_SPEC} doctor`;
206
394
  if (error instanceof SomaCheckCompatibilityError) {
@@ -208,6 +396,9 @@ function operationalFailureText(operation, error) {
208
396
  }
209
397
  if (error instanceof SomaCheckHttpError) {
210
398
  if (error.status === 401 || error.status === 403) {
399
+ if (operation === "share context with SomaCheck") {
400
+ return `Could not ${operation}: context is not enabled for this link, or the link was rejected or revoked. Open SomaCheck → Connect context, then retry. ${doctor}`;
401
+ }
211
402
  return `Could not ${operation}: this SomaCheck link was rejected or revoked. Reconnect in the app. ${doctor}`;
212
403
  }
213
404
  if (error.status === 404) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@somacheck/vibecheck",
3
- "version": "0.4.0",
4
- "description": "Ask your embodied sense for an answer through SomaCheck.",
3
+ "version": "0.6.0",
4
+ "description": "Send a consented vibecheck to SomaCheck and use the result as a signal.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/sensie-app/Somacheck.git",