@somacheck/vibecheck 0.6.4 → 0.6.5

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,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.4 link <CODE> --client <CLIENT>`
12
+ **What the `link` command does.** `npx -y @somacheck/vibecheck@0.6.5 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. Setup is not reported ready until the
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. If that preflight
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.4 setup codex
82
- npx -y @somacheck/vibecheck@0.6.4 setup claude
83
- npx -y @somacheck/vibecheck@0.6.4 doctor
84
+ npx -y @somacheck/vibecheck@0.6.5 setup codex
85
+ npx -y @somacheck/vibecheck@0.6.5 setup claude
86
+ npx -y @somacheck/vibecheck@0.6.5 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.4", "serve", "--client", "codex"]
96
+ "args": ["-y", "@somacheck/vibecheck@0.6.5", "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.4 serve --client claude`
102
+ For Claude Code: `claude mcp add --scope user vibecheck -- npx -y @somacheck/vibecheck@0.6.5 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
@@ -191,34 +194,26 @@ idempotency key, but the cancelled MCP transport may suppress that pending
191
194
  response. Do not continuously poll or re-ask the same decision to obtain a
192
195
  preferred result.
193
196
 
194
- Standard MCP cannot start a new agent turn after that bounded tool call has
195
- ended. The local Claude Code runtime therefore also advertises Anthropic's
196
- opt-in `claude/channel` capability. When Claude Code is launched with the
197
- SomaCheck server enabled as a channel, one local background watcher follows
198
- only the exact pending request. It emits one hardcoded, secret-free channel
199
- event when that request becomes answered, expired, or cancelled. The event
200
- contains the constrained lifecycle result and opaque handle, never the private
201
- statement or link token, and it cannot create another phone request.
202
-
203
- Claude Code channels are currently a research preview and only receive events
204
- while the opted-in session remains open. For local QA of this candidate, launch
205
- Claude Code with `--dangerously-load-development-channels server:vibecheck`.
206
- Production distribution requires the SomaCheck channel plugin to be accepted by
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
197
+ Standard MCP cannot start a new agent turn after a tool call has ended. The
198
+ Claude setup therefore installs a normal user-scoped plugin with an async
199
+ `PostToolUse` hook. When `request_vibecheck` returns pending, that hook follows
200
+ only the exact request and wakes the same open Claude Code session when the
201
+ answer, expiry, or cancellation arrives. It passes the constrained result and
202
+ opaque handle, never the statement or link token, and cannot create another
203
+ phone request. No experimental launch flag is required. Restart Claude Code
204
+ once after setup so it loads the plugin.
205
+
206
+ Other standard MCP hosts retain the bounded tool response and stable-handle
207
+ fallback until they provide an equivalent supported wake mechanism. This
208
+ Claude-specific continuation must never be presented as provider-neutral.
209
+
210
+ The 10-second whole-call deadline also covers the one create request. If that
213
211
  network request is aborted after an ambiguous commit, retry the exact same
214
212
  statement with the same `idempotency_key`; never create a replacement ask.
215
213
 
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.
214
+ After a completed result, use it as context and respond naturally in the
215
+ current conversation. The result is not authorization and does not establish a
216
+ diagnosis or certainty. Never say that the person's body made the decision.
222
217
 
223
218
  An immediate vibecheck is not proposition caching. It sends one person-approved statement
224
219
  for the current choice, expires, and remains bound to its originating client.
package/SKILL.md ADDED
@@ -0,0 +1,157 @@
1
+ ---
2
+ name: vibecheck
3
+ description: Request or offer a SomaCheck vibecheck at useful moments without nagging.
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
+ Use SomaCheck through one of two entry points:
11
+
12
+ 1. **The person asks for a vibecheck.** Select and send one useful statement immediately. Their current request is consent for one ask. Do not make them supply wording or confirm again.
13
+ 2. **You notice one of the three trigger events below.** Offer one exact statement and wait for acceptance before sending it.
14
+
15
+ A vibecheck tests a first-person statement through the linked `@somacheck/vibecheck` MCP server. It does not diagnose the person, decide for them, or authorize an action.
16
+
17
+ ## Direct requests
18
+
19
+ When the current message explicitly asks for a SomaCheck vibecheck or check-in (including phrasing such as "give me a check-in, a vibe check"), call `request_vibecheck` in that same turn with `consent_basis: "user_requested_vibecheck"`.
20
+
21
+ - If the person supplied exact wording, preserve it.
22
+ - Otherwise choose one present-tense, first-person statement about a current experience, choice, or tension.
23
+ - First use reliable context already available to the host: the current conversation, current goal, and user-approved memory or project context when those capabilities exist. Do not ask the person to restate context you can retrieve.
24
+ - Treat remembered project status, task lists, and summaries as potentially stale. They may identify a general tension, but the statement must not name a project, build, branch, wave, ticket, task status, or supposedly open or closed work.
25
+ - Corroborate remembered status against the current conversation. Do not reuse a stock template or recent SomaCheck statement. If context remains thin, create an original exploratory statement that can reveal something useful without pretending to know the person.
26
+ - Generate one fresh UUID for `idempotency_key`. Reuse it only to retry the same exact statement.
27
+ - Do not list candidates, present a draft, ask what they want to test, or ask "Want me to send it?" The request already supplied one-ask consent.
28
+
29
+ ## When to offer
30
+
31
+ A trigger is an event, not every message that matches a keyword. Once you have offered for an event, treat it as consumed until the task, decision, or message pattern meaningfully changes.
32
+
33
+ ### Trigger 1: Drift
34
+
35
+ Offer when at least one of these transcript-grounded conditions is true:
36
+
37
+ - Transcript timestamps show 90 minutes or more on the same task or thread without reaching the stated outcome.
38
+ - The same or materially equivalent fix has been attempted three or more times.
39
+ - The current work no longer advances the human's stated goal, and the human has not explicitly changed that goal.
40
+
41
+ Stay quiet during productive deep work, after an explicit goal change, or when duration cannot be established. A long message by itself is not drift.
42
+
43
+ ### Trigger 2: Major or irreversible decision ahead
44
+
45
+ Offer immediately before a consequential action when the transcript shows that action is imminent. Examples include deploying to production, sending an important email or offer, signing an agreement, deleting data, making a material purchase, hiring or firing, or saying "just do it" about a consequential step.
46
+
47
+ Planning or discussing a possible decision is not enough. The action must be close enough that a pause now could still affect it. This trigger may bypass a decline cooldown from triggers 1 or 3, but it still must pass the live status gate.
48
+
49
+ ### Trigger 3: Stress signals in messages
50
+
51
+ Offer when the recent messages contain either two or more signals in a short span, or one strong pattern repeated across retries:
52
+
53
+ - Cursing about the task or situation.
54
+ - Fragmented, rushed, or unusually incoherent instructions.
55
+ - All-caps urgency that is not an acronym, log, or quoted text.
56
+ - Repeated retry commands with no meaningful change in approach.
57
+ - Phrases such as "whatever, just make it work."
58
+
59
+ Describe only the observable message pattern. Do not claim to know how the person feels. One casual swear, terse normal writing, pasted logs, or quoted all-caps text is not enough.
60
+
61
+ ### Multiple triggers
62
+
63
+ Choose one primary trigger per event. Use trigger 2 first when an imminent consequential action exists, then trigger 3, then trigger 1. Do not stack or repeat offers.
64
+
65
+ ## Proactive offer sequence
66
+
67
+ 1. If no trigger fired, do nothing.
68
+ 2. Check the local event and cooldown record. If this event was already offered, stay quiet. If a trigger 1 or 3 offer was declined within the last two hours, stay quiet. A new trigger 2 event may bypass that cooldown.
69
+ 3. Call `get_vibecheck_status` before showing an offer.
70
+ 4. Stay quiet when `has_pending_request` is true. Reflection-feed cadence and capacity, including `cadence.may_create_now` and `propositions_needed`, do not authorize or block a live vibecheck.
71
+ 5. Make one short offer containing one statement to test. Do not post yet.
72
+ 6. If the human declines or does not accept, record the event as offered. For a decline, start a two-hour cooldown for triggers 1 and 3.
73
+ 7. If the human accepts, call `request_vibecheck` once with the exact offered statement, a fresh UUID in `idempotency_key`, and `consent_basis: "user_approved_statement"`.
74
+ 8. If the call must be retried, preserve both the exact statement and the same idempotency key.
75
+
76
+ `get_vibecheck_context` is optional before phrasing an offer. Use it only to avoid repeating a recent statement. Do not call `share_somacheck_context` or `post_vibecheck_statement` in this workflow. Live vibechecks and the asynchronous reflection feed are separate product behaviors.
77
+
78
+ ## How to phrase the offer
79
+
80
+ Use a statement to test, never a claim about the person. Keep it first-person, present-tense, neutral, and focused on one decision or next step. Do not include raw transcript text, names, secrets, identifiers, customer data, or private details.
81
+
82
+ Good offer forms:
83
+
84
+ - "Before you ship, want to test: 'I am ready to deploy this to production'?"
85
+ - "Quick 3-second check: 'I trust this decision'?"
86
+ - "Before another retry, want to test: 'This next attempt is meaningfully different from the last one'?"
87
+
88
+ Do not say that the person is distracted, stressed, dysregulated, afraid, or certain. Do not use medical language. Do not say "vibecheck confirms" or "vibecheck proves." Do not use an em dash in any human-facing string.
89
+
90
+ ### Drift statement templates
91
+
92
+ 1. "I am still working on the right problem."
93
+ 2. "This next attempt is meaningfully different from the last one."
94
+ 3. "I know what outcome I am trying to reach."
95
+ 4. "Continuing this task now is the best use of my attention."
96
+ 5. "I am ready to pause and restate the goal."
97
+ 6. "The current approach is moving me closer to done."
98
+ 7. "I have enough context to keep going."
99
+ 8. "I want to keep working on this thread."
100
+ 9. "A short reset would help me choose the next step."
101
+
102
+ ### Major or irreversible decision statement templates
103
+
104
+ 1. "I am ready to deploy this to production."
105
+ 2. "I trust this decision."
106
+ 3. "I am ready to send this message as written."
107
+ 4. "I understand the consequences of this action."
108
+ 5. "I have the information I need to commit."
109
+ 6. "This purchase is the right choice right now."
110
+ 7. "I am ready to make this offer."
111
+ 8. "I am comfortable making this change irreversible."
112
+ 9. "I want to act on this now."
113
+
114
+ ### Stress-signal statement templates
115
+
116
+ 1. "I want to keep pushing on this right now."
117
+ 2. "I trust the next step I am about to take."
118
+ 3. "I have enough clarity to choose the next step."
119
+ 4. "I am ready to slow down for one minute."
120
+ 5. "This approach still feels right to me."
121
+ 6. "I want to send this message in this tone."
122
+ 7. "I am comfortable continuing without another review."
123
+ 8. "I know what I need from this task."
124
+ 9. "I want to make this decision now."
125
+
126
+ Adapt a template only enough to make it relevant. Keep the statement free of sensitive specifics.
127
+
128
+ ## Cooldown rules
129
+
130
+ - One offer per trigger event, whether accepted, declined, or ignored.
131
+ - After a decline, suppress trigger 1 and trigger 3 offers for two hours.
132
+ - A new imminent trigger 2 event may bypass that two-hour cooldown.
133
+ - The one-pending gate always wins. A completed, expired, or cancelled request has no server cooldown; a new direct user request may be sent immediately.
134
+ - A continued conversation about the same task or decision is not a new event. A materially new goal, a different consequential action, or a new message-pattern episode can be.
135
+
136
+ ## What to do with the result
137
+
138
+ `request_vibecheck` waits boundedly for the exact result. If it returns `completed`, use the result as context and respond naturally in the current conversation. The result is not authorization and does not establish a diagnosis or certainty. If the tool returns `pending`, retain the exact `live:<uuid>` handle. Do not promise that you will check back automatically unless the current host supports automatic result delivery. Use `get_vibecheck_result` once when the person asks or at a natural follow-up. Never create a replacement request and never poll in a tight loop.
139
+
140
+ For `expired` or `cancelled`, do not infer an answer and do not immediately repost.
141
+
142
+ A result never grants permission to deploy, send, sign, delete, spend, hire, fire, publish, or take any other consequential action.
143
+
144
+ ## Do not
145
+
146
+ - Do not offer without one of the three transcript-grounded triggers.
147
+ - Do not ignore or reconfirm an explicit request for one vibecheck.
148
+ - Do not claim to know the person's internal state.
149
+ - Do not nag, stack offers, or re-offer for the same event.
150
+ - Do not proactively offer when a request is pending.
151
+ - Do not send an unsolicited offer before the human accepts the exact statement.
152
+ - Do not use `post_vibecheck_statement` for a live ask.
153
+ - Do not send raw conversation text, secrets, identifiers, or private details to the server.
154
+ - Do not call `share_somacheck_context` in this workflow.
155
+ - Do not treat any outcome as fact, diagnosis, proof, or authorization.
156
+ - Do not continuously poll for a result.
157
+ - Do not use medical language or an em dash in a human-facing string.
@@ -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.5",
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.5",
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.5 claude-hook await-result",
11
+ "asyncRewake": true,
12
+ "timeout": 900
13
+ }
14
+ ]
15
+ }
16
+ ]
17
+ }
18
+ }
@@ -0,0 +1,152 @@
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 = record(envelope.tool_response);
21
+ const structured = record(response.structuredContent ?? response.structured_content);
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 result for ${handle}: ${result.verdict} (confidence ${formatConfidence(result.confidence)}). Use it as context in the current conversation. It is not authorization and does not establish a diagnosis or certainty.`
82
+ : `SomaCheck request ${handle} is ${result.status}. Do not infer an answer or create a replacement request for the same decision.`;
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
+ })}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
126
+ await rename(temporary, path);
127
+ }
128
+ function record(value) {
129
+ return value !== null && typeof value === "object" ? value : {};
130
+ }
131
+ function parseHandle(handle) {
132
+ if (!handle.startsWith("live:"))
133
+ return null;
134
+ const requestId = handle.slice(5);
135
+ return UUID_PATTERN.test(requestId) ? requestId : null;
136
+ }
137
+ function formatConfidence(value) {
138
+ return value === null ? "unavailable" : value.toFixed(2);
139
+ }
140
+ async function exists(path) {
141
+ try {
142
+ await readFile(path);
143
+ return true;
144
+ }
145
+ catch {
146
+ return false;
147
+ }
148
+ }
149
+ function delay(milliseconds) {
150
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
151
+ }
152
+ //# 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 { automaticContinuationCommand, clientDisplayName, detectInstalledClients, detectLegacyHostedRegistration, LocalCommandRunner, manualLegacyHostedRemoveCommand, manualRemoveCommand, manualSetupCommand, parseClientChoice, preflightClientPersistence, registerClient, singleNonInteractiveClientSelection, } from "./client-setup.js";
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
- const continuationCommand = automaticContinuationCommand(client);
97
- if (continuationCommand !== null && ["registered", "upgraded", "already_registered"].includes(result.status)) {
98
- output(" For automatic result continuation, start Claude Code with:");
99
- output(` ${continuationCommand}`);
100
- output(" Standard Claude Code keeps the request safe but cannot wake itself after the tool call ends.");
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
- const reason = preflight.status === "not_installed"
153
- ? `${clientDisplayName(client)} is not installed or its CLI is unavailable.`
154
- : preflight.status === "needs_update"
155
- ? `${clientDisplayName(client)} has an existing "vibecheck" MCP entry with a different command or version.`
156
- : `${clientDisplayName(client)} configuration could not be inspected and written.`;
157
- const repair = preflight.status === "needs_update"
158
- ? ["", `Remove the stale entry: ${manualRemoveCommand(client)}`, `Then configure the current version: ${manualSetupCommand(client)}`]
159
- : [];
160
- throw new ClientPreflightError([
161
- `Not linking here: ${reason}`,
162
- ...repair,
163
- "",
164
- "The pairing code has NOT been used. Fix the local client configuration, then",
165
- "run the link command again.",
166
- ].join("\n"));
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());
@@ -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,8 @@ 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")
152
148
  && args.length === 5
153
149
  && args[0] === "-y"
154
150
  && 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.4";
5
+ export const PACKAGE_VERSION = "0.6.5";
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 = 45_000;
6
+ const LIVE_ASK_WAIT_BUDGET_MS = 10_000;
7
7
  const LIVE_ASK_POLL_DELAYS_MS = [
8
- 1_000, 1_000, 2_000, 2_000, 3_000, 3_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;
@@ -124,9 +123,9 @@ const SERVER_INSTRUCTIONS = [
124
123
  "Use at most one vibecheck per decision. Never re-ask to obtain a preferred answer.",
125
124
  "An immediate vibecheck is separate from the asynchronous reflection feed. Do not use it to replenish cached propositions.",
126
125
  "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.",
126
+ "request_vibecheck waits boundedly for the exact answer. If it returns completed, use the result as context in the current conversation. If it returns pending, retain that exact handle and read it once later; never create a replacement ask.",
127
+ "If request_vibecheck returns pending, never promise that you will check back on your own unless the host has automatic result delivery. Continue automatically only after an actual result event arrives for that exact request.",
128
+ "After a completed result, use it as context in the current conversation and respond naturally. It is not authorization and does not establish a diagnosis or certainty. Do not claim the person's body decided anything.",
130
129
  "When you have meaningful user-authorized context, call share_somacheck_context with concise derived observations before creating propositions.",
131
130
  "Never share raw conversation text, photos, credentials, private identifiers, or diagnostic claims.",
132
131
  "Maintain three distinct propositions drawn from your own context about this person.",
@@ -306,7 +305,7 @@ export function createVibecheckServer(dependencies) {
306
305
  const text = result.status === "answered"
307
306
  ? liveRequestId === null
308
307
  ? `Answered: ${Math.round((result.confidence ?? 0) * 100)}% ${result.verdict}.`
309
- : `Vibecheck completed: ${Math.round((result.confidence ?? 0) * 100)}% ${result.verdict}. Do not stop at restating the result or generic signal language. Reconcile it with the current goal, state one observable change, and continue with the next reversible in-scope action when already authorized. If new permission is required, name the exact prepared next step. A typed response from the person always overrides this signal.`
308
+ : `Vibecheck completed: ${Math.round((result.confidence ?? 0) * 100)}% ${result.verdict}. Use it as context in the current conversation. It is not authorization and does not establish a diagnosis or certainty.`
310
309
  : result.status === "queued"
311
310
  ? "Queued. The proposition is cached until the person advances their feed."
312
311
  : result.status === "pending"
@@ -376,11 +375,11 @@ export function createVibecheckServer(dependencies) {
376
375
  startClaudeChannelWatch(result, token, identity);
377
376
  const structuredContent = { ...result, error_code: null };
378
377
  const text = result.state === "completed"
379
- ? `Vibecheck completed: ${Math.round((result.confidence ?? 0) * 100)}% ${result.verdict}. Do not stop at restating the result or generic signal language. Reconcile it with the current goal, state one observable change, and continue with the next reversible in-scope action when already authorized. If new permission is required, name the exact prepared next step. A typed response from the person always overrides this signal.`
378
+ ? `Vibecheck completed: ${Math.round((result.confidence ?? 0) * 100)}% ${result.verdict}. Use it as context in the current conversation. It is not authorization and does not establish a diagnosis or certainty.`
380
379
  : result.state === "pending"
381
380
  ? result.delivery_state === "failed" || result.delivery_state === "skipped"
382
381
  ? `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.`
383
- : `Vibecheck ${result.request_id} is still pending after a bounded wait. Keep this exact handle and check it once later with get_vibecheck_result. It expires at ${result.expires_at}; do not poll continuously, re-ask for this decision, or promise an automatic follow-up unless an actual SomaCheck channel event arrives.`
382
+ : `Vibecheck ${result.request_id} is still pending after a bounded wait. Keep this exact handle and check it once later with get_vibecheck_result. It expires at ${result.expires_at}; do not poll continuously, re-ask for this decision, or promise an automatic follow-up unless the host delivers an actual result event.`
384
383
  : `This vibecheck is ${result.state}. Do not create another ask for the same decision.`;
385
384
  return { content: [{ type: "text", text }], structuredContent };
386
385
  }
@@ -431,7 +430,7 @@ async function watchForClaudeChannelResult(created, token, identity, dependencie
431
430
  continue;
432
431
  const handle = `live:${requestId}`;
433
432
  const content = lifecycle.status === "answered"
434
- ? `A SomaCheck vibecheck requested by this Claude connection has completed. The exact result is ${lifecycle.verdict} with confidence ${formatChannelConfidence(lifecycle.confidence)}. Continue the current conversation now. Do not merely restate the result or give generic signal-not-verdict commentary. Reconcile it with the current goal, state one observable change in your next action, and perform the 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. The exact opaque request handle is ${handle}. Do not request another vibecheck for this decision.`
433
+ ? `A SomaCheck vibecheck requested by this Claude connection has completed. The exact result is ${lifecycle.verdict} with confidence ${formatChannelConfidence(lifecycle.confidence)}. Use it as context in the current conversation. It is not authorization and does not establish a diagnosis or certainty. The exact opaque request handle is ${handle}. Do not request another vibecheck for this decision.`
435
434
  : `A SomaCheck vibecheck requested by this Claude connection is now ${lifecycle.status}. The exact opaque request handle is ${handle}. Do not create a replacement request for the same decision.`;
436
435
  try {
437
436
  await notify({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@somacheck/vibecheck",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
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",