@somacheck/vibecheck 0.5.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 +32 -8
- package/dist/api.js +116 -0
- package/dist/cli.js +3 -0
- package/dist/client-setup.js +19 -3
- package/dist/constants.js +4 -3
- package/dist/server.js +149 -6
- package/package.json +2 -2
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.
|
|
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 `
|
|
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
|
|
@@ -35,7 +35,9 @@ fails, the pairing code is not used. Non-interactive implicit selection,
|
|
|
35
35
|
**What the server does once running.** It exposes MCP tools that share bounded,
|
|
36
36
|
user-authorized context summaries, create a statement for the user to test,
|
|
37
37
|
read its outcome, recover recent completed gesture context, and keep a
|
|
38
|
-
three-statement feed stocked.
|
|
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.
|
|
39
41
|
|
|
40
42
|
**What is sent.** A statement string, bounded derived context observations the
|
|
41
43
|
agent explicitly shares, and a bearer token identifying the link. Tool
|
|
@@ -76,9 +78,9 @@ before the pairing code is redeemed.
|
|
|
76
78
|
To configure or repair a client later:
|
|
77
79
|
|
|
78
80
|
```text
|
|
79
|
-
npx -y @somacheck/vibecheck@0.
|
|
80
|
-
npx -y @somacheck/vibecheck@0.
|
|
81
|
-
npx -y @somacheck/vibecheck@0.
|
|
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
|
|
82
84
|
```
|
|
83
85
|
|
|
84
86
|
Manual registration remains available:
|
|
@@ -88,13 +90,13 @@ Manual registration remains available:
|
|
|
88
90
|
"mcpServers": {
|
|
89
91
|
"vibecheck": {
|
|
90
92
|
"command": "npx",
|
|
91
|
-
"args": ["-y", "@somacheck/vibecheck@0.
|
|
93
|
+
"args": ["-y", "@somacheck/vibecheck@0.6.0", "serve", "--client", "codex"]
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
```
|
|
96
98
|
|
|
97
|
-
For Claude Code: `claude mcp add --scope user vibecheck -- npx -y @somacheck/vibecheck@0.
|
|
99
|
+
For Claude Code: `claude mcp add --scope user vibecheck -- npx -y @somacheck/vibecheck@0.6.0 serve --client claude`
|
|
98
100
|
|
|
99
101
|
The link step writes only the bearer token in `~/.sensie/config.json`. The client
|
|
100
102
|
setup step asks Codex or Claude to add the pinned MCP command to that client's own
|
|
@@ -114,6 +116,7 @@ will explain it and leave it alone. The canonical local MCP key is `vibecheck`.
|
|
|
114
116
|
| `share_somacheck_context` | Share 1–20 bounded, derived, user-authorized observations; raw conversations and secrets are rejected. |
|
|
115
117
|
| `post_vibecheck_statement` | Add one to three personalized statements without blocking; returns a stable `request_id` for each. |
|
|
116
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. |
|
|
117
120
|
|
|
118
121
|
## How to use it well
|
|
119
122
|
|
|
@@ -123,6 +126,27 @@ prompt is **a statement for the person to test**, not a question about facts:
|
|
|
123
126
|
something to test, never as a claim about them. If it is wrong, their body says
|
|
124
127
|
`unaligned`, and that is a useful answer rather than a failure.
|
|
125
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
|
+
|
|
126
150
|
The agent maintains three distinct insights. SomaCheck presents one at a time;
|
|
127
151
|
the other two remain cached. After a completed gesture, the next cached insight
|
|
128
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,9 @@ 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
|
+
]);
|
|
13
20
|
const CONTEXT_CATEGORY = {
|
|
14
21
|
goal: "goals_projects", project: "goals_projects",
|
|
15
22
|
decision: "decisions_commitments", commitment: "decisions_commitments",
|
|
@@ -40,6 +47,24 @@ export class SomaCheckCompatibilityError extends Error {
|
|
|
40
47
|
this.name = "SomaCheckCompatibilityError";
|
|
41
48
|
}
|
|
42
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
|
+
}
|
|
43
68
|
function firstRow(value) {
|
|
44
69
|
const row = Array.isArray(value) ? value[0] : value;
|
|
45
70
|
if (row === null || typeof row !== "object" || Array.isArray(row)) {
|
|
@@ -155,6 +180,39 @@ export function decodeCreatedRequests(value) {
|
|
|
155
180
|
};
|
|
156
181
|
});
|
|
157
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
|
+
}
|
|
158
216
|
export function decodePollResponse(value) {
|
|
159
217
|
const row = asObject(value);
|
|
160
218
|
const status = row.status;
|
|
@@ -188,6 +246,21 @@ export function decodePollResponse(value) {
|
|
|
188
246
|
latency_s: latency,
|
|
189
247
|
};
|
|
190
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
|
+
}
|
|
191
264
|
export function decodeVibecheckContext(value) {
|
|
192
265
|
return allRows(value).map((row) => {
|
|
193
266
|
const verdict = requiredString(row, "verdict");
|
|
@@ -261,12 +334,45 @@ export class SupabaseAgentApi {
|
|
|
261
334
|
const row = await this.#rpc("agent_link_redeem", { code });
|
|
262
335
|
return requiredString(row, "token");
|
|
263
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
|
+
}
|
|
264
361
|
async createRequests(token, statements) {
|
|
265
362
|
return decodeCreatedRequests(await this.#rpcJson("agent_proposition_batch_create", { token, statements }));
|
|
266
363
|
}
|
|
267
364
|
async pollRequest(token, requestId) {
|
|
268
365
|
return decodePollResponse(await this.#rpc("agent_proposition_result", { token, proposition_id: requestId }));
|
|
269
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
|
+
}
|
|
270
376
|
async statusRequest(token) {
|
|
271
377
|
return decodeVibecheckStatus(await this.#rpc("agent_proposition_cache_status", { token }));
|
|
272
378
|
}
|
|
@@ -329,6 +435,16 @@ export class SupabaseAgentApi {
|
|
|
329
435
|
if (response.status === 426 || responseBody.includes(UPGRADE_REQUIRED_MARKER)) {
|
|
330
436
|
throw new SomaCheckCompatibilityError(null, null, null);
|
|
331
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
|
+
}
|
|
332
448
|
if (response.status === 409 || responseBody.includes(STATEMENT_PENDING_MARKER)) {
|
|
333
449
|
throw new StatementPendingError();
|
|
334
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) {
|
package/dist/client-setup.js
CHANGED
|
@@ -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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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,17 +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.
|
|
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 =
|
|
10
|
-
export const TOOLSET_VERSION = "vibecheck-0.
|
|
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
14
|
"share_somacheck_context",
|
|
15
15
|
"post_vibecheck_statement",
|
|
16
16
|
"get_vibecheck_result",
|
|
17
|
+
"request_vibecheck",
|
|
17
18
|
];
|
|
18
19
|
//# sourceMappingURL=constants.js.map
|
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,6 +48,28 @@ 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
|
+
};
|
|
51
73
|
const agentContextTypeSchema = z.enum([
|
|
52
74
|
"goal", "project", "decision", "commitment", "tension", "question",
|
|
53
75
|
"blindSpot", "relationship", "situation", "change", "deadline",
|
|
@@ -69,6 +91,12 @@ const SERVER_INSTRUCTIONS = [
|
|
|
69
91
|
"SomaCheck lets you offer the person a statement to test through a quick phone check-in.",
|
|
70
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.",
|
|
71
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.",
|
|
72
100
|
"When you have meaningful user-authorized context, call share_somacheck_context with concise derived observations before creating propositions.",
|
|
73
101
|
"Never share raw conversation text, photos, credentials, private identifiers, or diagnostic claims.",
|
|
74
102
|
"Maintain three distinct propositions drawn from your own context about this person.",
|
|
@@ -189,9 +217,9 @@ export function createVibecheckServer(dependencies) {
|
|
|
189
217
|
});
|
|
190
218
|
server.registerTool("get_vibecheck_result", {
|
|
191
219
|
title: "Get Vibecheck Result",
|
|
192
|
-
description: "Read one proposition by request_id. This is a single non-blocking read: queued is cached, and pending is
|
|
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.",
|
|
193
221
|
inputSchema: {
|
|
194
|
-
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."),
|
|
195
223
|
},
|
|
196
224
|
outputSchema: resultSchema,
|
|
197
225
|
annotations: {
|
|
@@ -203,10 +231,19 @@ export function createVibecheckServer(dependencies) {
|
|
|
203
231
|
}, async ({ request_id }) => {
|
|
204
232
|
try {
|
|
205
233
|
const token = await dependencies.loadToken();
|
|
206
|
-
const
|
|
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);
|
|
207
242
|
const structuredContent = { request_id, ...result };
|
|
208
243
|
const text = result.status === "answered"
|
|
209
|
-
?
|
|
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.`
|
|
210
247
|
: result.status === "queued"
|
|
211
248
|
? "Queued. The proposition is cached until the person advances their feed."
|
|
212
249
|
: result.status === "pending"
|
|
@@ -218,6 +255,46 @@ export function createVibecheckServer(dependencies) {
|
|
|
218
255
|
return failureMessage("read that SomaCheck result", error);
|
|
219
256
|
}
|
|
220
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
|
+
});
|
|
221
298
|
return server;
|
|
222
299
|
}
|
|
223
300
|
function summarise(status) {
|
|
@@ -237,6 +314,16 @@ function formatDuration(seconds) {
|
|
|
237
314
|
const hours = seconds / 3600;
|
|
238
315
|
return Number.isInteger(hours) ? `${hours}-hour` : `${Math.round(seconds / 60)}-minute`;
|
|
239
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
|
+
}
|
|
240
327
|
function failure(text) {
|
|
241
328
|
return {
|
|
242
329
|
isError: true,
|
|
@@ -246,6 +333,62 @@ function failure(text) {
|
|
|
246
333
|
function failureMessage(operation, error) {
|
|
247
334
|
return failure(operationalFailureText(operation, error));
|
|
248
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
|
+
}
|
|
249
392
|
function operationalFailureText(operation, error) {
|
|
250
393
|
const doctor = `Run: npx -y ${PACKAGE_SPEC} doctor`;
|
|
251
394
|
if (error instanceof SomaCheckCompatibilityError) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@somacheck/vibecheck",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
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",
|