@somacheck/vibecheck 0.6.4 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -71
- package/SKILL.md +25 -0
- package/claude-marketplace/.claude-plugin/marketplace.json +22 -0
- package/claude-marketplace/plugins/vibecheck/.claude-plugin/plugin.json +13 -0
- package/claude-marketplace/plugins/vibecheck/hooks/hooks.json +18 -0
- package/dist/api.js +19 -2
- package/dist/claude-hook.js +167 -0
- package/dist/claude-plugin-setup.js +114 -0
- package/dist/cli.js +50 -24
- package/dist/client-setup.js +3 -6
- package/dist/constants.js +1 -1
- package/dist/server.js +29 -43
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -9,11 +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.6.
|
|
12
|
+
**What the `link` command does.** `npx -y @somacheck/vibecheck@0.6.6 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
|
-
connection without printing the token.
|
|
16
|
+
connection without printing the token. For Claude Code it also installs an
|
|
17
|
+
inspectable user-scoped SomaCheck plugin that can wake the same open session when
|
|
18
|
+
the phone result arrives. Setup is not reported ready until the
|
|
17
19
|
selected client has the exact pinned `vibecheck` MCP registration, the backend
|
|
18
20
|
accepts protocol `5` / toolset `vibecheck-0.6`, and both status and context
|
|
19
21
|
probes succeed. It contacts exactly one network host: the Supabase project below.
|
|
@@ -28,7 +30,8 @@ with. The safe paths are:
|
|
|
28
30
|
`--client claude` or `--client codex`.
|
|
29
31
|
|
|
30
32
|
For the second path, this package first proves that exact client CLI is installed
|
|
31
|
-
and that its persistent MCP config can be inspected and written.
|
|
33
|
+
and that its persistent MCP config can be inspected and written. Claude setup
|
|
34
|
+
also verifies the user-scoped continuation plugin before redemption. If that preflight
|
|
32
35
|
fails, the pairing code is not used. Non-interactive implicit selection,
|
|
33
36
|
`--client all`, and `--client none` are rejected before redemption.
|
|
34
37
|
|
|
@@ -78,9 +81,9 @@ before the pairing code is redeemed.
|
|
|
78
81
|
To configure or repair a client later:
|
|
79
82
|
|
|
80
83
|
```text
|
|
81
|
-
npx -y @somacheck/vibecheck@0.6.
|
|
82
|
-
npx -y @somacheck/vibecheck@0.6.
|
|
83
|
-
npx -y @somacheck/vibecheck@0.6.
|
|
84
|
+
npx -y @somacheck/vibecheck@0.6.6 setup codex
|
|
85
|
+
npx -y @somacheck/vibecheck@0.6.6 setup claude
|
|
86
|
+
npx -y @somacheck/vibecheck@0.6.6 doctor
|
|
84
87
|
```
|
|
85
88
|
|
|
86
89
|
Manual registration remains available:
|
|
@@ -90,13 +93,13 @@ Manual registration remains available:
|
|
|
90
93
|
"mcpServers": {
|
|
91
94
|
"vibecheck": {
|
|
92
95
|
"command": "npx",
|
|
93
|
-
"args": ["-y", "@somacheck/vibecheck@0.6.
|
|
96
|
+
"args": ["-y", "@somacheck/vibecheck@0.6.6", "serve", "--client", "codex"]
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
```
|
|
98
101
|
|
|
99
|
-
For Claude Code: `claude mcp add --scope user vibecheck -- npx -y @somacheck/vibecheck@0.6.
|
|
102
|
+
For Claude Code: `claude mcp add --scope user vibecheck -- npx -y @somacheck/vibecheck@0.6.6 serve --client claude`
|
|
100
103
|
|
|
101
104
|
The link step writes only the bearer token in `~/.sensie/config.json`. The client
|
|
102
105
|
setup step asks Codex or Claude to add the pinned MCP command to that client's own
|
|
@@ -107,15 +110,15 @@ If `doctor` reports another MCP server named `somacheck`, that is the legacy
|
|
|
107
110
|
hosted connector and is separate from this local package. The setup command
|
|
108
111
|
will explain it and leave it alone. The canonical local MCP key is `vibecheck`.
|
|
109
112
|
|
|
110
|
-
## Teach your agent when to
|
|
113
|
+
## Teach your agent when to use SomaCheck
|
|
111
114
|
|
|
112
|
-
[`SKILL.md`](SKILL.md) teaches compatible agents
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
[`SKILL.md`](SKILL.md) teaches compatible agents one simple affordance:
|
|
116
|
+
SomaCheck lets them ask their person for a vibecheck. The agent can offer one
|
|
117
|
+
when it believes the read would help, or send one when the person asks. The
|
|
118
|
+
skill keeps only the essential boundaries: ask the person, choose a useful
|
|
119
|
+
first-person statement from available context, treat the returned gesture and
|
|
120
|
+
optional feedback as context rather than authorization, and never include
|
|
121
|
+
secrets or assess anyone else.
|
|
119
122
|
|
|
120
123
|
For Claude Code, add this repository as a marketplace and install the skill in
|
|
121
124
|
one terminal command:
|
|
@@ -153,31 +156,11 @@ The skill expects the `@somacheck/vibecheck` MCP server to already be linked as
|
|
|
153
156
|
|
|
154
157
|
## How to use it well
|
|
155
158
|
|
|
156
|
-
Ask when
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
Setup and linking are not permission for future asks. If the person's current
|
|
163
|
-
message explicitly asks for a SomaCheck vibecheck or check-in, that is consent for one ask.
|
|
164
|
-
Use their exact statement when supplied. If they delegated the choice, select
|
|
165
|
-
the most useful first-person statement from the current conversation, goal,
|
|
166
|
-
decision, or tension and send it immediately—do not list candidates, ask them
|
|
167
|
-
for wording, or seek a second confirmation. Remembered context may identify a general tension, but an
|
|
168
|
-
agent-selected statement must not name a project, build, branch, wave, ticket,
|
|
169
|
-
task status, or supposedly open/closed work. Abstract it into a present-tense
|
|
170
|
-
first-person experience or choice, such as “I want to keep consolidating what is
|
|
171
|
-
already in motion rather than start something new.” Preserve exact wording the
|
|
172
|
-
person supplied. If meaningful context is unavailable, use the neutral fallback
|
|
173
|
-
“I feel clear about what deserves my attention right now.”
|
|
174
|
-
|
|
175
|
-
If their message did not explicitly request a vibecheck, first show the exact
|
|
176
|
-
statement and invite the person plainly:
|
|
177
|
-
|
|
178
|
-
> Want a quick vibecheck before I continue? I'll send this statement to your
|
|
179
|
-
> phone; you answer with your wrist gesture, and I'll use the result as a
|
|
180
|
-
> signal—not a verdict.
|
|
159
|
+
Ask when the person's felt response would be useful context. A good prompt is a
|
|
160
|
+
first-person statement for the person to test, not a claim about them. When the
|
|
161
|
+
person asks for a vibecheck, choose and send a useful statement from the context
|
|
162
|
+
available to you. For proactive offers, get the person's acceptance before
|
|
163
|
+
sending.
|
|
181
164
|
|
|
182
165
|
Call `request_vibecheck` with `consent_basis: "user_requested_vibecheck"` for a
|
|
183
166
|
direct request or `consent_basis: "user_approved_statement"` after approval of
|
|
@@ -188,37 +171,24 @@ On timeout or a result-read failure it returns the same pending `live:<uuid>`
|
|
|
188
171
|
handle; pass that handle to `get_vibecheck_result` once later. Caller
|
|
189
172
|
cancellation stops result reads and preserves the backend request and
|
|
190
173
|
idempotency key, but the cancelled MCP transport may suppress that pending
|
|
191
|
-
response.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
Anthropic's channel allowlist (or explicitly allowed by an organization).
|
|
208
|
-
Claude Desktop, hosted connectors, Codex, and other standard MCP hosts retain
|
|
209
|
-
the bounded tool response and stable-handle fallback; this Claude-specific
|
|
210
|
-
channel must never be presented as provider-neutral behavior.
|
|
211
|
-
|
|
212
|
-
The 45-second whole-call deadline also covers the one create request. If that
|
|
174
|
+
response.
|
|
175
|
+
|
|
176
|
+
Standard MCP cannot start a new agent turn after a tool call has ended. The
|
|
177
|
+
Claude setup therefore installs a normal user-scoped plugin with an async
|
|
178
|
+
`PostToolUse` hook. When `request_vibecheck` returns pending, that hook follows
|
|
179
|
+
only the exact request and wakes the same open Claude Code session when the
|
|
180
|
+
answer, expiry, or cancellation arrives. It passes the constrained result and
|
|
181
|
+
opaque handle, never the statement or link token, and cannot create another
|
|
182
|
+
phone request. No experimental launch flag is required. Restart Claude Code
|
|
183
|
+
once after setup so it loads the plugin.
|
|
184
|
+
|
|
185
|
+
Other standard MCP hosts retain the bounded tool response and stable-handle
|
|
186
|
+
fallback until they provide an equivalent supported wake mechanism. This
|
|
187
|
+
Claude-specific continuation must never be presented as provider-neutral.
|
|
188
|
+
|
|
189
|
+
The 10-second whole-call deadline also covers the one create request. If that
|
|
213
190
|
network request is aborted after an ambiguous commit, retry the exact same
|
|
214
|
-
statement with the same `idempotency_key
|
|
215
|
-
|
|
216
|
-
After a completed result, reconcile it with the current goal, state one
|
|
217
|
-
observable change, and continue with the next reversible in-scope action when
|
|
218
|
-
it is already authorized. Do not stop at repeating the verdict, confidence, or
|
|
219
|
-
generic signal language.
|
|
220
|
-
Never treat it as authorization or say that the person's body made the
|
|
221
|
-
decision. A typed response from the person always overrides the signal.
|
|
191
|
+
statement with the same `idempotency_key`.
|
|
222
192
|
|
|
223
193
|
An immediate vibecheck is not proposition caching. It sends one person-approved statement
|
|
224
194
|
for the current choice, expires, and remains bound to its originating client.
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vibecheck
|
|
3
|
+
description: Ask your person for a SomaCheck vibecheck when it would help.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
allowed-tools: mcp__vibecheck__get_vibecheck_context mcp__vibecheck__get_vibecheck_status mcp__vibecheck__get_vibecheck_result mcp__vibecheck__request_vibecheck
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Vibecheck
|
|
9
|
+
|
|
10
|
+
SomaCheck lets you ask your person for a vibecheck.
|
|
11
|
+
|
|
12
|
+
Use `request_vibecheck` in either case:
|
|
13
|
+
|
|
14
|
+
1. You proactively offer one because you believe it would be useful.
|
|
15
|
+
2. The person asks for a vibecheck.
|
|
16
|
+
|
|
17
|
+
When the person asks, choose and send a useful first-person statement from the context available to you. Do not make them supply wording unless they want to. Help the person get an insight about themselves from the context you have.
|
|
18
|
+
|
|
19
|
+
For proactive offers, get the person's acceptance before sending.
|
|
20
|
+
|
|
21
|
+
The returned gesture and optional feedback are context, not authorization. Use your judgment in how you interpret and respond.
|
|
22
|
+
|
|
23
|
+
Never include secrets, raw private content, diagnostic claims, or use SomaCheck to assess anyone other than the person you are interacting with.
|
|
24
|
+
|
|
25
|
+
If a request is pending, keep the returned `live:<uuid>` handle instead of creating a replacement.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "somacheck-local",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Sensie",
|
|
5
|
+
"email": "agents@joinsensie.com"
|
|
6
|
+
},
|
|
7
|
+
"description": "SomaCheck integration installed by the official vibecheck package.",
|
|
8
|
+
"plugins": [
|
|
9
|
+
{
|
|
10
|
+
"name": "vibecheck",
|
|
11
|
+
"source": "./plugins/vibecheck",
|
|
12
|
+
"description": "Let Claude offer a vibecheck and continue when the phone result arrives.",
|
|
13
|
+
"version": "0.6.6",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Sensie",
|
|
16
|
+
"email": "agents@joinsensie.com"
|
|
17
|
+
},
|
|
18
|
+
"category": "productivity",
|
|
19
|
+
"tags": ["vibecheck", "somacheck", "decision-support", "mcp"]
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vibecheck",
|
|
3
|
+
"description": "Let Claude offer a SomaCheck vibecheck and continue when the phone result arrives.",
|
|
4
|
+
"version": "0.6.6",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Sensie",
|
|
7
|
+
"email": "agents@joinsensie.com"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/sensie-app/Somacheck/tree/main/packages/vibecheck",
|
|
10
|
+
"repository": "https://github.com/sensie-app/Somacheck",
|
|
11
|
+
"license": "UNLICENSED",
|
|
12
|
+
"keywords": ["vibecheck", "somacheck", "decision-support", "mcp"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Continue an ordinary Claude Code session when a SomaCheck result arrives.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"PostToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "mcp__vibecheck__request_vibecheck",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "npx -y @somacheck/vibecheck@0.6.6 claude-hook await-result",
|
|
11
|
+
"asyncRewake": true,
|
|
12
|
+
"timeout": 900
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
package/dist/api.js
CHANGED
|
@@ -8,6 +8,7 @@ const LIVE_ASK_CLIENT_REQUIRED_MARKER = "SC_VC_VIBECHECK_CLIENT_REQUIRED";
|
|
|
8
8
|
const LIVE_ASK_CLIENT_NOT_READY_MARKER = "SC_VC_VIBECHECK_CLIENT_NOT_READY";
|
|
9
9
|
const STATUSES = new Set(["queued", "pending", "answered", "expired", "cancelled"]);
|
|
10
10
|
const VERDICTS = new Set(["aligned", "unaligned"]);
|
|
11
|
+
const USER_FEEDBACKS = new Set(["agreed", "disagreed", "not_provided"]);
|
|
11
12
|
const HANDSHAKE_READINESS_STATUSES = new Set([
|
|
12
13
|
"ready",
|
|
13
14
|
"upgrade_required",
|
|
@@ -222,6 +223,7 @@ export function decodePollResponse(value) {
|
|
|
222
223
|
const verdict = row.verdict;
|
|
223
224
|
const confidence = row.confidence;
|
|
224
225
|
const latency = row.latency_s;
|
|
226
|
+
const userFeedback = row.user_feedback ?? null;
|
|
225
227
|
if (verdict !== null && (typeof verdict !== "string" || !VERDICTS.has(verdict))) {
|
|
226
228
|
throw new Error("SomaCheck returned an invalid verdict.");
|
|
227
229
|
}
|
|
@@ -231,6 +233,9 @@ export function decodePollResponse(value) {
|
|
|
231
233
|
if (latency !== null && (typeof latency !== "number" || !Number.isFinite(latency) || latency < 0)) {
|
|
232
234
|
throw new Error("SomaCheck returned an invalid latency.");
|
|
233
235
|
}
|
|
236
|
+
if (userFeedback !== null && (typeof userFeedback !== "string" || !USER_FEEDBACKS.has(userFeedback))) {
|
|
237
|
+
throw new Error("SomaCheck returned invalid user feedback.");
|
|
238
|
+
}
|
|
234
239
|
if (status === "answered") {
|
|
235
240
|
if (verdict === null || confidence === null || confidence < 0 || confidence > 1 || latency === null) {
|
|
236
241
|
throw new Error("SomaCheck returned an incomplete answered result.");
|
|
@@ -239,12 +244,21 @@ export function decodePollResponse(value) {
|
|
|
239
244
|
else if (verdict !== null || confidence !== null || latency !== null) {
|
|
240
245
|
throw new Error("SomaCheck returned an inconsistent pending result.");
|
|
241
246
|
}
|
|
242
|
-
|
|
247
|
+
if (status !== "answered" && userFeedback !== null) {
|
|
248
|
+
throw new Error("SomaCheck returned inconsistent feedback for an unanswered result.");
|
|
249
|
+
}
|
|
250
|
+
const decoded = {
|
|
243
251
|
status: status,
|
|
244
252
|
verdict: verdict,
|
|
245
253
|
confidence: confidence,
|
|
246
254
|
latency_s: latency,
|
|
247
255
|
};
|
|
256
|
+
if (userFeedback === null)
|
|
257
|
+
return decoded;
|
|
258
|
+
return {
|
|
259
|
+
...decoded,
|
|
260
|
+
user_feedback: userFeedback,
|
|
261
|
+
};
|
|
248
262
|
}
|
|
249
263
|
export function decodeLiveVibecheckResult(value, expectedRequestId) {
|
|
250
264
|
const row = firstRow(value);
|
|
@@ -347,7 +361,7 @@ export class SupabaseAgentApi {
|
|
|
347
361
|
const lifecycle = created.status === "answered"
|
|
348
362
|
? await this.liveVibecheckResult(token, identity, created.request_id, signal)
|
|
349
363
|
: { status: created.status, verdict: null, confidence: null, latency_s: null };
|
|
350
|
-
|
|
364
|
+
const liveRequest = {
|
|
351
365
|
request_id: `live:${created.request_id}`,
|
|
352
366
|
state: lifecycle.status === "answered" ? "completed" : lifecycle.status,
|
|
353
367
|
verdict: lifecycle.verdict,
|
|
@@ -357,6 +371,9 @@ export class SupabaseAgentApi {
|
|
|
357
371
|
delivery_state: created.push_status,
|
|
358
372
|
cooldown_until: created.cooldown_until,
|
|
359
373
|
};
|
|
374
|
+
if (lifecycle.user_feedback === undefined || lifecycle.user_feedback === null)
|
|
375
|
+
return liveRequest;
|
|
376
|
+
return { ...liveRequest, user_feedback: lifecycle.user_feedback };
|
|
360
377
|
}
|
|
361
378
|
async createRequests(token, statements) {
|
|
362
379
|
return decodeCreatedRequests(await this.#rpcJson("agent_proposition_batch_create", { token, statements }));
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readConfig } from "./config.js";
|
|
4
|
+
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;
|
|
5
|
+
const POLL_INTERVAL_MS = 2_000;
|
|
6
|
+
const MAX_WATCH_MS = 15 * 60 * 1_000;
|
|
7
|
+
const READ_TIMEOUT_MS = 10_000;
|
|
8
|
+
const STALE_LEASE_MS = 20 * 60 * 1_000;
|
|
9
|
+
export function parsePendingClaudeAsk(input) {
|
|
10
|
+
let envelope;
|
|
11
|
+
try {
|
|
12
|
+
envelope = JSON.parse(input);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
if (typeof envelope.tool_name !== "string"
|
|
18
|
+
|| !envelope.tool_name.endsWith("__request_vibecheck"))
|
|
19
|
+
return null;
|
|
20
|
+
const response = parseToolResponse(envelope.tool_response);
|
|
21
|
+
const structured = record(response.structuredContent ?? response.structured_content ?? response);
|
|
22
|
+
if (structured.state !== "pending" || typeof structured.request_id !== "string")
|
|
23
|
+
return null;
|
|
24
|
+
if (structured.delivery_state === "failed" || structured.delivery_state === "skipped")
|
|
25
|
+
return null;
|
|
26
|
+
const requestId = parseHandle(structured.request_id);
|
|
27
|
+
if (requestId === null)
|
|
28
|
+
return null;
|
|
29
|
+
return {
|
|
30
|
+
requestId,
|
|
31
|
+
expiresAt: typeof structured.expires_at === "string" ? structured.expires_at : null,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export async function awaitClaudeVibecheckResult(input, dependencies) {
|
|
35
|
+
const pending = parsePendingClaudeAsk(input);
|
|
36
|
+
if (pending === null)
|
|
37
|
+
return { exitCode: 0, message: null };
|
|
38
|
+
const receipts = join(dependencies.home, ".sensie", "claude-result-hooks");
|
|
39
|
+
await mkdir(receipts, { recursive: true, mode: 0o700 });
|
|
40
|
+
const donePath = join(receipts, `${pending.requestId}.done.json`);
|
|
41
|
+
const leasePath = join(receipts, `${pending.requestId}.lease`);
|
|
42
|
+
if (await exists(donePath))
|
|
43
|
+
return { exitCode: 0, message: null };
|
|
44
|
+
if (!(await claimLease(leasePath, dependencies.now ?? Date.now))) {
|
|
45
|
+
return { exitCode: 0, message: null };
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const token = (await readConfig(dependencies.home, "claude")).token;
|
|
49
|
+
const now = dependencies.now ?? Date.now;
|
|
50
|
+
const sleep = dependencies.sleep ?? delay;
|
|
51
|
+
const maxWatchMs = dependencies.maxWatchMs ?? MAX_WATCH_MS;
|
|
52
|
+
const pollIntervalMs = dependencies.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
53
|
+
const parsedExpiry = pending.expiresAt === null ? Number.NaN : Date.parse(pending.expiresAt);
|
|
54
|
+
const deadline = Math.min(Number.isFinite(parsedExpiry) ? parsedExpiry : now() + maxWatchMs, now() + maxWatchMs);
|
|
55
|
+
let consecutiveFailures = 0;
|
|
56
|
+
while (now() < deadline) {
|
|
57
|
+
await sleep(Math.min(pollIntervalMs, Math.max(0, deadline - now())));
|
|
58
|
+
if (now() >= deadline)
|
|
59
|
+
break;
|
|
60
|
+
let result;
|
|
61
|
+
const abort = new AbortController();
|
|
62
|
+
const timeout = setTimeout(() => abort.abort(new Error("SomaCheck result read timed out.")), READ_TIMEOUT_MS);
|
|
63
|
+
timeout.unref?.();
|
|
64
|
+
try {
|
|
65
|
+
result = await dependencies.api.liveVibecheckResult(token, { kind: "local", client_key: "claude" }, pending.requestId, abort.signal);
|
|
66
|
+
consecutiveFailures = 0;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
consecutiveFailures += 1;
|
|
70
|
+
if (consecutiveFailures >= 3)
|
|
71
|
+
return { exitCode: 0, message: null };
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
}
|
|
77
|
+
if (result.status === "pending")
|
|
78
|
+
continue;
|
|
79
|
+
const handle = `live:${pending.requestId}`;
|
|
80
|
+
const message = result.status === "answered"
|
|
81
|
+
? `SomaCheck vibecheck ${handle} completed: ${result.verdict}, confidence ${formatConfidence(result.confidence)}${formatUserFeedback(result.user_feedback)}.`
|
|
82
|
+
: `SomaCheck vibecheck ${handle} is ${result.status}.`;
|
|
83
|
+
await writeTerminalReceipt(donePath, result);
|
|
84
|
+
return { exitCode: 2, message };
|
|
85
|
+
}
|
|
86
|
+
return { exitCode: 0, message: null };
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
await rm(leasePath, { force: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function claimLease(path, now) {
|
|
93
|
+
try {
|
|
94
|
+
const handle = await open(path, "wx", 0o600);
|
|
95
|
+
await handle.writeFile(`${JSON.stringify({ started_at_ms: now() })}\n`, "utf8");
|
|
96
|
+
await handle.close();
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error.code !== "EEXIST")
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const metadata = await stat(path);
|
|
105
|
+
if (now() - metadata.mtimeMs <= STALE_LEASE_MS)
|
|
106
|
+
return false;
|
|
107
|
+
await rm(path, { force: true });
|
|
108
|
+
const handle = await open(path, "wx", 0o600);
|
|
109
|
+
await handle.writeFile(`${JSON.stringify({ started_at_ms: now() })}\n`, "utf8");
|
|
110
|
+
await handle.close();
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error.code === "EEXIST")
|
|
115
|
+
return false;
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function writeTerminalReceipt(path, result) {
|
|
120
|
+
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
121
|
+
await writeFile(temporary, `${JSON.stringify({
|
|
122
|
+
status: result.status,
|
|
123
|
+
verdict: result.verdict,
|
|
124
|
+
confidence: result.confidence,
|
|
125
|
+
user_feedback: result.user_feedback ?? null,
|
|
126
|
+
})}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
127
|
+
await rename(temporary, path);
|
|
128
|
+
}
|
|
129
|
+
function parseToolResponse(value) {
|
|
130
|
+
if (typeof value === "string") {
|
|
131
|
+
try {
|
|
132
|
+
return record(JSON.parse(value));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return record(value);
|
|
139
|
+
}
|
|
140
|
+
function record(value) {
|
|
141
|
+
return value !== null && typeof value === "object" ? value : {};
|
|
142
|
+
}
|
|
143
|
+
function parseHandle(handle) {
|
|
144
|
+
if (!handle.startsWith("live:"))
|
|
145
|
+
return null;
|
|
146
|
+
const requestId = handle.slice(5);
|
|
147
|
+
return UUID_PATTERN.test(requestId) ? requestId : null;
|
|
148
|
+
}
|
|
149
|
+
function formatConfidence(value) {
|
|
150
|
+
return value === null ? "unavailable" : value.toFixed(2);
|
|
151
|
+
}
|
|
152
|
+
function formatUserFeedback(feedback) {
|
|
153
|
+
return feedback === undefined || feedback === null ? "" : `, user feedback ${feedback}`;
|
|
154
|
+
}
|
|
155
|
+
async function exists(path) {
|
|
156
|
+
try {
|
|
157
|
+
await readFile(path);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function delay(milliseconds) {
|
|
165
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=claude-hook.js.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { cp, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
export const CLAUDE_PLUGIN_MARKETPLACE = "somacheck-local";
|
|
5
|
+
export const CLAUDE_PLUGIN_ID = `vibecheck@${CLAUDE_PLUGIN_MARKETPLACE}`;
|
|
6
|
+
const MANAGED_MARKER = ".somacheck-managed-plugin-marketplace";
|
|
7
|
+
export async function configureClaudeContinuationPlugin(home, runner, packageRoot = fileURLToPath(new URL("..", import.meta.url))) {
|
|
8
|
+
try {
|
|
9
|
+
const destination = join(home, ".sensie", "claude-plugin-marketplace");
|
|
10
|
+
await installManagedMarketplace(packageRoot, destination);
|
|
11
|
+
const marketplaces = await jsonCommand(runner, ["plugin", "marketplace", "list", "--json"]);
|
|
12
|
+
const marketplaceExists = Array.isArray(marketplaces)
|
|
13
|
+
&& marketplaces.some((item) => record(item).name === CLAUDE_PLUGIN_MARKETPLACE);
|
|
14
|
+
if (marketplaceExists) {
|
|
15
|
+
if ((await runner.run("claude", ["plugin", "marketplace", "update", CLAUDE_PLUGIN_MARKETPLACE])).exitCode !== 0) {
|
|
16
|
+
return { status: "failed" };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
else if ((await runner.run("claude", [
|
|
20
|
+
"plugin", "marketplace", "add", "--scope", "user", destination,
|
|
21
|
+
])).exitCode !== 0) {
|
|
22
|
+
return { status: "failed" };
|
|
23
|
+
}
|
|
24
|
+
const plugins = await jsonCommand(runner, ["plugin", "list", "--json"]);
|
|
25
|
+
const installed = Array.isArray(plugins)
|
|
26
|
+
&& plugins.some((item) => record(item).id === CLAUDE_PLUGIN_ID && record(item).scope === "user");
|
|
27
|
+
const action = installed ? "update" : "install";
|
|
28
|
+
const result = await runner.run("claude", ["plugin", action, "--scope", "user", "--yes", CLAUDE_PLUGIN_ID]);
|
|
29
|
+
if (result.exitCode !== 0)
|
|
30
|
+
return { status: "failed" };
|
|
31
|
+
const verified = await jsonCommand(runner, ["plugin", "list", "--json"]);
|
|
32
|
+
const current = Array.isArray(verified)
|
|
33
|
+
&& verified.some((item) => {
|
|
34
|
+
const value = record(item);
|
|
35
|
+
return value.id === CLAUDE_PLUGIN_ID && value.scope === "user" && value.enabled !== false;
|
|
36
|
+
});
|
|
37
|
+
return current
|
|
38
|
+
? { status: installed ? "updated" : "installed" }
|
|
39
|
+
: { status: "failed" };
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return { status: "failed" };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function installManagedMarketplace(packageRoot, destination) {
|
|
46
|
+
const template = join(packageRoot, "claude-marketplace");
|
|
47
|
+
const skill = join(packageRoot, "SKILL.md");
|
|
48
|
+
await stat(join(template, ".claude-plugin", "marketplace.json"));
|
|
49
|
+
await stat(skill);
|
|
50
|
+
try {
|
|
51
|
+
await stat(destination);
|
|
52
|
+
await stat(join(destination, MANAGED_MARKER));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const missing = error.code === "ENOENT";
|
|
56
|
+
if (!missing || await pathExists(destination)) {
|
|
57
|
+
throw new Error("SomaCheck will not overwrite an unmanaged Claude plugin directory.");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
|
|
61
|
+
const temporary = `${destination}.${process.pid}.${Date.now()}.tmp`;
|
|
62
|
+
const backup = `${destination}.${process.pid}.${Date.now()}.backup`;
|
|
63
|
+
try {
|
|
64
|
+
await cp(template, temporary, { recursive: true, errorOnExist: true, force: false });
|
|
65
|
+
const skillDestination = join(temporary, "plugins", "vibecheck", "skills", "vibecheck");
|
|
66
|
+
await mkdir(skillDestination, { recursive: true, mode: 0o700 });
|
|
67
|
+
await cp(skill, join(skillDestination, "SKILL.md"), { errorOnExist: true, force: false });
|
|
68
|
+
await writeFile(join(temporary, MANAGED_MARKER), "managed by @somacheck/vibecheck\n", {
|
|
69
|
+
encoding: "utf8", flag: "wx", mode: 0o600,
|
|
70
|
+
});
|
|
71
|
+
if (await pathExists(destination))
|
|
72
|
+
await rename(destination, backup);
|
|
73
|
+
await rename(temporary, destination);
|
|
74
|
+
await rm(backup, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
await rm(temporary, { recursive: true, force: true });
|
|
78
|
+
if (await pathExists(backup) && !(await pathExists(destination))) {
|
|
79
|
+
await rename(backup, destination);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function jsonCommand(runner, args) {
|
|
84
|
+
const result = await runner.run("claude", args);
|
|
85
|
+
if (result.exitCode !== 0)
|
|
86
|
+
return null;
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(result.stdout);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function pathExists(path) {
|
|
95
|
+
try {
|
|
96
|
+
await readFile(path);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error.code === "EISDIR")
|
|
101
|
+
return true;
|
|
102
|
+
try {
|
|
103
|
+
await stat(path);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function record(value) {
|
|
112
|
+
return value !== null && typeof value === "object" ? value : {};
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=claude-plugin-setup.js.map
|
package/dist/cli.js
CHANGED
|
@@ -3,8 +3,10 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { SupabaseAgentApi } from "./api.js";
|
|
6
|
-
import {
|
|
6
|
+
import { clientDisplayName, detectInstalledClients, detectLegacyHostedRegistration, LocalCommandRunner, manualLegacyHostedRemoveCommand, manualRemoveCommand, manualSetupCommand, parseClientChoice, preflightClientPersistence, registerClient, singleNonInteractiveClientSelection, } from "./client-setup.js";
|
|
7
|
+
import { configureClaudeContinuationPlugin } from "./claude-plugin-setup.js";
|
|
7
8
|
import { readConfig } from "./config.js";
|
|
9
|
+
import { awaitClaudeVibecheckResult } from "./claude-hook.js";
|
|
8
10
|
import { PACKAGE_SPEC, SUPABASE_PUBLISHABLE_KEY, SUPABASE_URL } from "./constants.js";
|
|
9
11
|
import { LinkPersistenceError, NonInteractiveLinkError, linkAgent } from "./link.js";
|
|
10
12
|
import { checkReadiness } from "./readiness.js";
|
|
@@ -93,11 +95,12 @@ async function configureClients(clients) {
|
|
|
93
95
|
else {
|
|
94
96
|
output(`✗ Could not configure ${clientDisplayName(client)} automatically.`);
|
|
95
97
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
if (client === "claude" && ["registered", "upgraded", "already_registered"].includes(result.status)) {
|
|
99
|
+
const plugin = await configureClaudeContinuationPlugin(homedir(), runner);
|
|
100
|
+
if (plugin.status === "failed") {
|
|
101
|
+
throw new ClientPreflightError("Could not configure automatic SomaCheck results for Claude Code. No pairing code was used.");
|
|
102
|
+
}
|
|
103
|
+
output(`✓ Automatic Claude result continuation ${plugin.status === "installed" ? "was configured" : "is ready"}.`);
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
}
|
|
@@ -142,28 +145,38 @@ async function preflightLinkClients(clients, interactive) {
|
|
|
142
145
|
const preflight = await preflightClientPersistence(client, runner);
|
|
143
146
|
if (preflight.status === "ready") {
|
|
144
147
|
output(`✓ ${clientDisplayName(client)} already has the exact SomaCheck MCP registration.`);
|
|
145
|
-
continue;
|
|
146
148
|
}
|
|
147
|
-
if (preflight.status === "registered" || preflight.status === "upgraded") {
|
|
149
|
+
else if (preflight.status === "registered" || preflight.status === "upgraded") {
|
|
148
150
|
const verb = preflight.status === "upgraded" ? "was safely upgraded" : "was configured";
|
|
149
151
|
output(`✓ ${clientDisplayName(client)} ${verb} before redeeming the pairing code.`);
|
|
150
|
-
continue;
|
|
151
152
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
153
|
+
else {
|
|
154
|
+
const reason = preflight.status === "not_installed"
|
|
155
|
+
? `${clientDisplayName(client)} is not installed or its CLI is unavailable.`
|
|
156
|
+
: preflight.status === "needs_update"
|
|
157
|
+
? `${clientDisplayName(client)} has an existing "vibecheck" MCP entry with a different command or version.`
|
|
158
|
+
: `${clientDisplayName(client)} configuration could not be inspected and written.`;
|
|
159
|
+
const repair = preflight.status === "needs_update"
|
|
160
|
+
? ["", `Remove the stale entry: ${manualRemoveCommand(client)}`, `Then configure the current version: ${manualSetupCommand(client)}`]
|
|
161
|
+
: [];
|
|
162
|
+
throw new ClientPreflightError([
|
|
163
|
+
`Not linking here: ${reason}`,
|
|
164
|
+
...repair,
|
|
165
|
+
"",
|
|
166
|
+
"The pairing code has NOT been used. Fix the local client configuration, then",
|
|
167
|
+
"run the link command again.",
|
|
168
|
+
].join("\n"));
|
|
169
|
+
}
|
|
170
|
+
if (client === "claude") {
|
|
171
|
+
const plugin = await configureClaudeContinuationPlugin(homedir(), runner);
|
|
172
|
+
if (plugin.status === "failed") {
|
|
173
|
+
throw new ClientPreflightError([
|
|
174
|
+
"Not linking here: automatic SomaCheck result continuation could not be configured for Claude Code.",
|
|
175
|
+
"The pairing code has NOT been used. Repair Claude's plugin setup, then run the link command again.",
|
|
176
|
+
].join("\n"));
|
|
177
|
+
}
|
|
178
|
+
output("✓ Automatic Claude result continuation is ready before redeeming the pairing code.");
|
|
179
|
+
}
|
|
167
180
|
}
|
|
168
181
|
return selectedClients;
|
|
169
182
|
}
|
|
@@ -200,8 +213,21 @@ async function startServer(runtimeClient) {
|
|
|
200
213
|
void refreshRuntimeHealth(runtimeClient).catch(() => undefined);
|
|
201
214
|
}
|
|
202
215
|
}
|
|
216
|
+
async function runClaudeResultHook() {
|
|
217
|
+
const chunks = [];
|
|
218
|
+
for await (const chunk of process.stdin) {
|
|
219
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
220
|
+
}
|
|
221
|
+
const result = await awaitClaudeVibecheckResult(Buffer.concat(chunks).toString("utf8"), { home: homedir(), api });
|
|
222
|
+
if (result.message !== null)
|
|
223
|
+
process.stderr.write(`${result.message}\n`);
|
|
224
|
+
return result.exitCode;
|
|
225
|
+
}
|
|
203
226
|
async function main() {
|
|
204
227
|
const args = process.argv.slice(2);
|
|
228
|
+
if (args.length === 2 && args[0] === "claude-hook" && args[1] === "await-result") {
|
|
229
|
+
return runClaudeResultHook();
|
|
230
|
+
}
|
|
205
231
|
if (args[0] === "link") {
|
|
206
232
|
if (args.length < 2)
|
|
207
233
|
throw new Error(usage());
|
package/dist/client-setup.js
CHANGED
|
@@ -34,11 +34,6 @@ export function manualSetupCommand(client) {
|
|
|
34
34
|
}
|
|
35
35
|
return `claude mcp add --scope user ${MCP_SERVER_NAME} -- npx ${mcpServerArgs(client).join(" ")}`;
|
|
36
36
|
}
|
|
37
|
-
export function automaticContinuationCommand(client) {
|
|
38
|
-
return client === "claude"
|
|
39
|
-
? "claude --dangerously-load-development-channels server:vibecheck"
|
|
40
|
-
: null;
|
|
41
|
-
}
|
|
42
37
|
export function manualRemoveCommand(client) {
|
|
43
38
|
return client === "codex"
|
|
44
39
|
? `codex mcp remove ${MCP_SERVER_NAME}`
|
|
@@ -148,7 +143,9 @@ function isManagedSomaCheckRegistration(client, stdout) {
|
|
|
148
143
|
|| packageArg === "@somacheck/vibecheck@0.6.0"
|
|
149
144
|
|| packageArg === "@somacheck/vibecheck@0.6.1"
|
|
150
145
|
|| packageArg === "@somacheck/vibecheck@0.6.2"
|
|
151
|
-
|| packageArg === "@somacheck/vibecheck@0.6.3"
|
|
146
|
+
|| packageArg === "@somacheck/vibecheck@0.6.3"
|
|
147
|
+
|| packageArg === "@somacheck/vibecheck@0.6.4"
|
|
148
|
+
|| packageArg === "@somacheck/vibecheck@0.6.5")
|
|
152
149
|
&& args.length === 5
|
|
153
150
|
&& args[0] === "-y"
|
|
154
151
|
&& args[1] === packageArg
|
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.6.
|
|
5
|
+
export const PACKAGE_VERSION = "0.6.6";
|
|
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/server.js
CHANGED
|
@@ -3,10 +3,9 @@ import { z } from "zod";
|
|
|
3
3
|
import { StatementPendingError, } from "./vibecheck.js";
|
|
4
4
|
import { SomaCheckCompatibilityError, SomaCheckHttpError, SomaCheckLiveAskClientError, SomaCheckLiveAskConflictError, SomaCheckLiveAskPendingError, } from "./api.js";
|
|
5
5
|
import { PACKAGE_NAME, PACKAGE_SPEC, PACKAGE_VERSION } from "./constants.js";
|
|
6
|
-
const LIVE_ASK_WAIT_BUDGET_MS =
|
|
6
|
+
const LIVE_ASK_WAIT_BUDGET_MS = 10_000;
|
|
7
7
|
const LIVE_ASK_POLL_DELAYS_MS = [
|
|
8
|
-
1_000,
|
|
9
|
-
4_000, 4_000, 5_000, 5_000, 5_000, 5_000,
|
|
8
|
+
1_000, 1_500, 2_500, 4_000,
|
|
10
9
|
];
|
|
11
10
|
const CLAUDE_CHANNEL_POLL_INTERVAL_MS = 2_000;
|
|
12
11
|
const CLAUDE_CHANNEL_MAX_WATCH_MS = 15 * 60 * 1_000;
|
|
@@ -62,6 +61,7 @@ const resultSchema = {
|
|
|
62
61
|
verdict: z.enum(["aligned", "unaligned"]).nullable(),
|
|
63
62
|
confidence: z.number().nullable(),
|
|
64
63
|
latency_s: z.number().nullable(),
|
|
64
|
+
user_feedback: z.enum(["agreed", "disagreed", "not_provided"]).nullable().optional(),
|
|
65
65
|
};
|
|
66
66
|
const contextItemSchema = z.object({
|
|
67
67
|
request_id: z.string(),
|
|
@@ -87,6 +87,7 @@ const liveAskSchema = {
|
|
|
87
87
|
request_id: z.string().nullable(),
|
|
88
88
|
verdict: z.enum(["aligned", "unaligned"]).nullable(),
|
|
89
89
|
confidence: z.number().min(0).max(1).nullable(),
|
|
90
|
+
user_feedback: z.enum(["agreed", "disagreed", "not_provided"]).nullable().optional(),
|
|
90
91
|
expires_at: z.string().datetime({ offset: true }).nullable(),
|
|
91
92
|
idempotent_replay: z.boolean().nullable(),
|
|
92
93
|
delivery_state: z.enum(["queued", "processing", "sent", "failed", "skipped"]).nullable(),
|
|
@@ -111,34 +112,18 @@ const contextShareSchema = {
|
|
|
111
112
|
captured_at: z.string().datetime({ offset: true }),
|
|
112
113
|
};
|
|
113
114
|
const SERVER_INSTRUCTIONS = [
|
|
114
|
-
"SomaCheck lets you
|
|
115
|
-
"
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
"
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"DELEGATED SELECTION RULE: before choosing wording, use the reliable context already available to your host: the current conversation, current working goal, and user-approved memory or project context when those capabilities exist. Do not make the person restate context you can already retrieve. Remembered task status may identify a general tension, but the statement itself must not name a project, build, branch, wave, ticket, task status, or supposedly open/closed work. Abstract it into an original present-tense first-person experience or choice. This restriction does not alter exact wording supplied by the person.",
|
|
122
|
-
"When choosing the statement, prefer the most consequential unresolved first-person decision or tension you can support with current evidence. Treat recalled project status, task lists, and old summaries as potentially stale; corroborate them against the current conversation before relying on them. Do not reuse a stock example or recent SomaCheck statement. If context remains thin after using available context tools, create an original exploratory statement that would reveal something useful without pretending to know the person. Never diagnose, judge another person, or claim certainty about the user.",
|
|
123
|
-
"Otherwise, 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.",
|
|
124
|
-
"Use at most one vibecheck per decision. Never re-ask to obtain a preferred answer.",
|
|
125
|
-
"An immediate vibecheck is separate from the asynchronous reflection feed. Do not use it to replenish cached propositions.",
|
|
126
|
-
"Use a fresh idempotency_key for each new vibecheck, and reuse that exact key only when retrying the same statement.",
|
|
127
|
-
"request_vibecheck waits boundedly for the exact answer. If it returns completed, continue in the same turn and explain what changes. If it returns pending, retain that exact handle and read it once later; never create a replacement ask.",
|
|
128
|
-
"If request_vibecheck returns pending, never promise that you will check back on your own. Only continue automatically after an actual SomaCheck channel event arrives for that exact request.",
|
|
129
|
-
"After a completed result, do not merely repeat the verdict, confidence, or generic signal-not-verdict language. Reconcile the result with the current goal and state one observable change in your next action. Continue with that next reversible in-scope action when it is already authorized; if new permission is required, name the exact prepared next step and ask only for that permission. Never say 'your body decided'; a typed human response always overrides the signal.",
|
|
130
|
-
"When you have meaningful user-authorized context, call share_somacheck_context with concise derived observations before creating propositions.",
|
|
131
|
-
"Never share raw conversation text, photos, credentials, private identifiers, or diagnostic claims.",
|
|
132
|
-
"Maintain three distinct propositions drawn from your own context about this person.",
|
|
133
|
-
"When propositions_needed is greater than zero, post exactly that many in one call.",
|
|
134
|
-
"Offer each statement as something to test, never as a claim of fact about the user.",
|
|
135
|
-
"Retain every request_id returned by post_vibecheck_statement and call get_vibecheck_result later.",
|
|
136
|
-
"Do not poll continuously. Queued means the proposition is cached until the person advances their feed.",
|
|
115
|
+
"SomaCheck lets you ask your person for a vibecheck.",
|
|
116
|
+
"You can proactively offer one when you believe it would be useful, or send one when the person asks.",
|
|
117
|
+
"When the person asks, choose and send a useful first-person statement from the context available to you; do not make them supply wording unless they want to.",
|
|
118
|
+
"Help the person get an insight about themselves from the context you have.",
|
|
119
|
+
"The returned gesture and optional feedback are context, not authorization.",
|
|
120
|
+
"Use your judgment in how you interpret and respond.",
|
|
121
|
+
"Never include secrets, raw private content, diagnostic claims, or use SomaCheck to assess anyone other than the person you are interacting with.",
|
|
137
122
|
].join(" ");
|
|
138
|
-
const DIRECT_REQUEST_NEXT_ACTION = "DIRECT REQUEST RULE: If the person's current message asks for a SomaCheck vibecheck or check-in, first use reliable context already available to the host, including current conversation and user-approved memory when supported. Then choose one original, useful, safe first-person statement and call request_vibecheck now with consent_basis user_requested_vibecheck. Do not list choices, ask for wording, or seek another confirmation. Ignore reflection-feed propositions_needed for this direct request. Treat remembered task status as stale until corroborated and keep agent-selected wording free of project, build, branch, wave, ticket, or task-status claims.";
|
|
139
123
|
export function createVibecheckServer(dependencies) {
|
|
140
124
|
const supportsClaudeChannel = dependencies.identity?.kind === "local"
|
|
141
|
-
&& dependencies.identity.client_key === "claude"
|
|
125
|
+
&& dependencies.identity.client_key === "claude"
|
|
126
|
+
&& typeof dependencies.claudeChannelWatch === "object";
|
|
142
127
|
const server = new McpServer({ name: PACKAGE_NAME, version: PACKAGE_VERSION }, {
|
|
143
128
|
instructions: SERVER_INSTRUCTIONS,
|
|
144
129
|
...(supportsClaudeChannel
|
|
@@ -181,12 +166,10 @@ export function createVibecheckServer(dependencies) {
|
|
|
181
166
|
const token = await dependencies.loadToken();
|
|
182
167
|
const checkins = await dependencies.api.contextRequest(token);
|
|
183
168
|
const text = checkins.length === 0
|
|
184
|
-
?
|
|
169
|
+
? "No completed SomaCheck gestures yet."
|
|
185
170
|
: [
|
|
186
|
-
`Loaded ${checkins.length} completed gesture${checkins.length === 1 ? "" : "s"}
|
|
171
|
+
`Loaded ${checkins.length} completed gesture${checkins.length === 1 ? "" : "s"}:`,
|
|
187
172
|
...checkins.map((item) => `${Math.round(item.confidence * 100)}% ${item.verdict} — ${JSON.stringify(item.statement)}`),
|
|
188
|
-
"",
|
|
189
|
-
DIRECT_REQUEST_NEXT_ACTION,
|
|
190
173
|
].join("\n");
|
|
191
174
|
return { content: [{ type: "text", text }], structuredContent: { checkins } };
|
|
192
175
|
}
|
|
@@ -212,7 +195,7 @@ export function createVibecheckServer(dependencies) {
|
|
|
212
195
|
return {
|
|
213
196
|
content: [{
|
|
214
197
|
type: "text",
|
|
215
|
-
text:
|
|
198
|
+
text: summarise(status),
|
|
216
199
|
}],
|
|
217
200
|
structuredContent: { ...status },
|
|
218
201
|
};
|
|
@@ -306,7 +289,7 @@ export function createVibecheckServer(dependencies) {
|
|
|
306
289
|
const text = result.status === "answered"
|
|
307
290
|
? liveRequestId === null
|
|
308
291
|
? `Answered: ${Math.round((result.confidence ?? 0) * 100)}% ${result.verdict}.`
|
|
309
|
-
: `Vibecheck completed: ${
|
|
292
|
+
: `Vibecheck completed: ${result.verdict}, confidence ${formatChannelConfidence(result.confidence)}${formatUserFeedback(result.user_feedback)}.`
|
|
310
293
|
: result.status === "queued"
|
|
311
294
|
? "Queued. The proposition is cached until the person advances their feed."
|
|
312
295
|
: result.status === "pending"
|
|
@@ -320,14 +303,14 @@ export function createVibecheckServer(dependencies) {
|
|
|
320
303
|
});
|
|
321
304
|
server.registerTool("request_vibecheck", {
|
|
322
305
|
title: "Request a Vibecheck",
|
|
323
|
-
description: "Send one
|
|
306
|
+
description: "Send one statement to the person's phone for a SomaCheck vibecheck. If the person asks for a vibecheck, choose a useful first-person statement from your available context and send it. For proactive offers, call only after the person accepts. The result is context, not authorization.",
|
|
324
307
|
inputSchema: {
|
|
325
308
|
statement: z.string().trim().min(1).max(1000)
|
|
326
|
-
.describe("One plain-language statement for the person to test
|
|
309
|
+
.describe("One plain-language first-person statement for the person to test. Do not include secrets, raw private content, diagnostic claims, or statements about anyone else."),
|
|
327
310
|
idempotency_key: z.string().uuid()
|
|
328
311
|
.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."),
|
|
329
312
|
consent_basis: z.enum(["user_requested_vibecheck", "user_approved_statement"])
|
|
330
|
-
.describe("
|
|
313
|
+
.describe("Use user_requested_vibecheck when the person asks for a vibecheck. Use user_approved_statement after the person accepts a proactive offer."),
|
|
331
314
|
},
|
|
332
315
|
outputSchema: liveAskSchema,
|
|
333
316
|
annotations: {
|
|
@@ -376,12 +359,12 @@ export function createVibecheckServer(dependencies) {
|
|
|
376
359
|
startClaudeChannelWatch(result, token, identity);
|
|
377
360
|
const structuredContent = { ...result, error_code: null };
|
|
378
361
|
const text = result.state === "completed"
|
|
379
|
-
? `Vibecheck completed: ${
|
|
362
|
+
? `Vibecheck completed: ${result.verdict}, confidence ${formatChannelConfidence(result.confidence)}${formatUserFeedback(result.user_feedback)}.`
|
|
380
363
|
: result.state === "pending"
|
|
381
364
|
? result.delivery_state === "failed" || result.delivery_state === "skipped"
|
|
382
|
-
? `Vibecheck ${result.request_id} was stored, but phone delivery is ${result.delivery_state}. It expires at ${result.expires_at}
|
|
383
|
-
: `Vibecheck ${result.request_id} is
|
|
384
|
-
: `This vibecheck is ${result.state}
|
|
365
|
+
? `Vibecheck ${result.request_id} was stored, but phone delivery is ${result.delivery_state}. It expires at ${result.expires_at}.`
|
|
366
|
+
: `Vibecheck ${result.request_id} is pending. It expires at ${result.expires_at}.`
|
|
367
|
+
: `This vibecheck is ${result.state}.`;
|
|
385
368
|
return { content: [{ type: "text", text }], structuredContent };
|
|
386
369
|
}
|
|
387
370
|
catch (error) {
|
|
@@ -431,8 +414,8 @@ async function watchForClaudeChannelResult(created, token, identity, dependencie
|
|
|
431
414
|
continue;
|
|
432
415
|
const handle = `live:${requestId}`;
|
|
433
416
|
const content = lifecycle.status === "answered"
|
|
434
|
-
? `
|
|
435
|
-
: `
|
|
417
|
+
? `SomaCheck vibecheck ${handle} completed: ${lifecycle.verdict}, confidence ${formatChannelConfidence(lifecycle.confidence)}${formatUserFeedback(lifecycle.user_feedback)}.`
|
|
418
|
+
: `SomaCheck vibecheck ${handle} is ${lifecycle.status}.`;
|
|
436
419
|
try {
|
|
437
420
|
await notify({
|
|
438
421
|
method: "notifications/claude/channel",
|
|
@@ -464,6 +447,9 @@ function unrefSleep(milliseconds) {
|
|
|
464
447
|
function formatChannelConfidence(confidence) {
|
|
465
448
|
return confidence === null ? "unavailable" : confidence.toFixed(2);
|
|
466
449
|
}
|
|
450
|
+
function formatUserFeedback(feedback) {
|
|
451
|
+
return feedback === undefined || feedback === null ? "" : `, user feedback ${feedback}`;
|
|
452
|
+
}
|
|
467
453
|
async function waitForLiveVibecheck(created, token, identity, dependencies, signal, deadline, progress) {
|
|
468
454
|
if (created.state !== "pending"
|
|
469
455
|
|| created.delivery_state === "failed"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@somacheck/vibecheck",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "Send a consented vibecheck to SomaCheck and use the result as a signal.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
".": "./dist/server.js"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
|
-
"dist/*.js"
|
|
18
|
+
"dist/*.js",
|
|
19
|
+
"SKILL.md",
|
|
20
|
+
"claude-marketplace/**"
|
|
19
21
|
],
|
|
20
22
|
"scripts": {
|
|
21
23
|
"build": "rm -rf dist && tsc -p tsconfig.json",
|