@somacheck/vibecheck 0.6.11 → 0.6.13
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 +80 -33
- package/SKILL.md +11 -7
- package/dist/claude-hook.js +4 -0
- package/dist/claude-plugin-setup.js +183 -78
- package/dist/cli.js +71 -27
- package/dist/client-setup.js +42 -20
- package/dist/constants.js +1 -1
- package/dist/readiness.js +36 -31
- package/dist/runtime-mode.js +14 -0
- package/dist/server.js +68 -14
- package/package.json +3 -4
- package/claude-marketplace/.claude-plugin/marketplace.json +0 -22
- package/claude-marketplace/plugins/vibecheck/.claude-plugin/plugin.json +0 -13
- package/claude-marketplace/plugins/vibecheck/hooks/hooks.json +0 -18
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# @somacheck/vibecheck
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
**Body language for AI agents, on your terms.** SomaCheck turns a consented
|
|
4
|
+
three-second phone gesture into an Aligned or Unaligned signal with confidence,
|
|
5
|
+
giving your agent context for how a thought or choice lands for you, while you
|
|
6
|
+
remain the authority.
|
|
5
7
|
|
|
6
8
|
Your agent has your context. It does not have your gut. This is the bridge.
|
|
7
9
|
|
|
@@ -9,16 +11,18 @@ Your agent has your context. It does not have your gut. This is the bridge.
|
|
|
9
11
|
|
|
10
12
|
Fair. Here is what you need, without executing anything.
|
|
11
13
|
|
|
12
|
-
**What the `link` command does.** `npx -y @somacheck/vibecheck@0.6.
|
|
14
|
+
**What the `link` command does.** `npx -y @somacheck/vibecheck@0.6.13 link <CODE> --client <CLIENT>`
|
|
13
15
|
exchanges a short-lived pairing code, generated inside the SomaCheck iOS app, for
|
|
14
16
|
a bearer token. It writes that token to `~/.sensie/config.json` with mode `0600`,
|
|
15
17
|
offers to configure supported agent clients found on the computer, and checks the
|
|
16
18
|
connection without printing the token. For Claude Code it also installs an
|
|
17
|
-
inspectable user-scoped SomaCheck plugin
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
inspectable user-scoped SomaCheck plugin whose bundled local MCP server is a
|
|
20
|
+
Channel, so a phone result can enter the same open conversation as a new event.
|
|
21
|
+
Setup is not reported ready until Claude has that exact enabled plugin with no
|
|
22
|
+
competing direct `vibecheck` registration, or Codex has the exact pinned direct
|
|
23
|
+
registration. The backend must also accept protocol `5` / toolset
|
|
24
|
+
`vibecheck-0.6`, and both status and context probes must succeed. It contacts
|
|
25
|
+
exactly one network host: the Supabase project below.
|
|
22
26
|
|
|
23
27
|
**It will refuse unsafe sandbox linking, on purpose.** `link` checks safety
|
|
24
28
|
*before* redeeming. Pairing codes are single-use, so redeeming one inside an
|
|
@@ -30,9 +34,10 @@ with. The safe paths are:
|
|
|
30
34
|
`--client claude` or `--client codex`.
|
|
31
35
|
|
|
32
36
|
For the second path, this package first proves that exact client CLI is installed
|
|
33
|
-
and that its
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
and that its configuration can be inspected and written. Claude setup verifies
|
|
38
|
+
the public user-scoped Channel plugin and safely migrates only a package-managed
|
|
39
|
+
direct registration before redemption. If that preflight fails, the pairing
|
|
40
|
+
code is not used. Non-interactive implicit selection,
|
|
36
41
|
`--client all`, and `--client none` are rejected before redemption.
|
|
37
42
|
|
|
38
43
|
**What the server does once running.** It exposes MCP tools that share bounded,
|
|
@@ -97,30 +102,62 @@ before the pairing code is redeemed.
|
|
|
97
102
|
To configure or repair a client later:
|
|
98
103
|
|
|
99
104
|
```text
|
|
100
|
-
npx -y @somacheck/vibecheck@0.6.
|
|
101
|
-
npx -y @somacheck/vibecheck@0.6.
|
|
102
|
-
npx -y @somacheck/vibecheck@0.6.
|
|
105
|
+
npx -y @somacheck/vibecheck@0.6.13 setup codex
|
|
106
|
+
npx -y @somacheck/vibecheck@0.6.13 setup claude
|
|
107
|
+
npx -y @somacheck/vibecheck@0.6.13 doctor
|
|
103
108
|
```
|
|
104
109
|
|
|
105
|
-
Manual registration remains available
|
|
110
|
+
Manual Codex registration remains available. Its stdio runtime command is
|
|
111
|
+
`npx -y @somacheck/vibecheck@0.6.13 serve --client codex`:
|
|
106
112
|
|
|
107
113
|
```json
|
|
108
114
|
{
|
|
109
115
|
"mcpServers": {
|
|
110
116
|
"vibecheck": {
|
|
111
117
|
"command": "npx",
|
|
112
|
-
"args": ["-y", "@somacheck/vibecheck@0.6.
|
|
118
|
+
"args": ["-y", "@somacheck/vibecheck@0.6.13", "serve", "--client", "codex"]
|
|
113
119
|
}
|
|
114
120
|
}
|
|
115
121
|
}
|
|
116
122
|
```
|
|
117
123
|
|
|
118
|
-
For Claude Code
|
|
124
|
+
For Claude Code, install the public Channel plugin:
|
|
119
125
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
`claude plugin marketplace add Sensie-agents/vibecheck && claude plugin install vibecheck@somacheck`
|
|
127
|
+
|
|
128
|
+
Claude Channels are currently an Anthropic research preview. After Anthropic or
|
|
129
|
+
your Team/Enterprise administrator allowlists the plugin, start Claude Code with:
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
claude --channels plugin:vibecheck@somacheck
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
During review, Anthropic's warning-gated development flag remains a local test
|
|
136
|
+
mechanism, not the intended user experience. Keep that Claude Code session open. Channel events are queued while Claude is
|
|
137
|
+
busy, but a closed session cannot receive them. Claude does not acknowledge
|
|
138
|
+
channel notifications, so retain the stable `live:<uuid>` request handle as a
|
|
139
|
+
manual recovery path. The plugin deliberately does not install a competing
|
|
140
|
+
wake hook that could continue the same result twice.
|
|
141
|
+
|
|
142
|
+
The link step writes only the bearer token in `~/.sensie/config.json`. Codex
|
|
143
|
+
setup adds the pinned direct MCP command; Claude setup installs the public
|
|
144
|
+
Channel plugin, enables it at user scope, and removes only a recognized
|
|
145
|
+
package-managed direct duplicate. Neither path copies the bearer token into
|
|
146
|
+
client configuration. The interactive command completes both steps. Restart
|
|
147
|
+
the client after setup.
|
|
148
|
+
|
|
149
|
+
Running `setup claude` again is the safe repair and upgrade path: it refreshes
|
|
150
|
+
the canonical SomaCheck marketplace, updates the installed plugin, verifies its
|
|
151
|
+
exact reviewed MCP command, removes a recognized pre-plugin registration, and
|
|
152
|
+
atomically enables user-scoped auto-update for the canonical marketplace while
|
|
153
|
+
preserving unrelated Claude settings. It refuses malformed settings and a
|
|
154
|
+
same-named marketplace owned by the user. `doctor` verifies this update path.
|
|
155
|
+
Claude performs enabled marketplace updates in the background after startup;
|
|
156
|
+
the running session keeps its loaded version until `/reload-plugins` or the next
|
|
157
|
+
launch. The same setting remains visible under **/plugin → Marketplaces →
|
|
158
|
+
somacheck**. SomaCheck deliberately does not use an unpinned npm `latest`
|
|
159
|
+
command, because that would execute a future release outside the reviewed
|
|
160
|
+
plugin lineage.
|
|
124
161
|
|
|
125
162
|
If `doctor` reports another MCP server named `somacheck`, that is the legacy
|
|
126
163
|
hosted connector and is separate from this local package. The setup command
|
|
@@ -179,7 +216,7 @@ The skill expects the `@somacheck/vibecheck` MCP server to already be linked as
|
|
|
179
216
|
|
|
180
217
|
## Agent reflection recipes
|
|
181
218
|
|
|
182
|
-
|
|
219
|
+
Body language for AI agents, on your terms. These recipes
|
|
183
220
|
combine SomaCheck with a named platform so a person can privately reflect on
|
|
184
221
|
their own wording, interpretation, study design, or listening choice:
|
|
185
222
|
|
|
@@ -252,7 +289,7 @@ blocked until the capability and revocation probe in the safety review passes.
|
|
|
252
289
|
| `get_vibecheck_context` | Read recent completed gestures for this exact agent link without retaining every `request_id`. |
|
|
253
290
|
| `get_vibecheck_status` | Read the live three-proposition cache and how many distinct insights it needs. |
|
|
254
291
|
| `share_somacheck_context` | Share 1–20 bounded, derived, user-authorized observations; raw conversations and secrets are rejected. |
|
|
255
|
-
| `post_vibecheck_statement` | Add one to three personalized statements without blocking;
|
|
292
|
+
| `post_vibecheck_statement` | Add one to three personalized statements without blocking; one may become the current Home vibe check and the rest wait under Vibe Checks without extra pushes. Returns a stable `request_id` for each. |
|
|
256
293
|
| `get_vibecheck_result` | Read that exact proposition once by `request_id`; returns queued, pending, or its terminal result. |
|
|
257
294
|
| `request_vibecheck` | With per-ask consent, send one immediate phone vibecheck, wait boundedly for its exact answer, and retain an opaque `live:<uuid>` fallback handle. |
|
|
258
295
|
|
|
@@ -275,14 +312,22 @@ cancellation stops result reads and preserves the backend request and
|
|
|
275
312
|
idempotency key, but the cancelled MCP transport may suppress that pending
|
|
276
313
|
response.
|
|
277
314
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
`
|
|
281
|
-
|
|
282
|
-
answer, expiry, or cancellation arrives.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
315
|
+
Claude Code is the primary local experience. Its registered stdio server
|
|
316
|
+
advertises Anthropic's experimental Channel capability. When
|
|
317
|
+
`request_vibecheck` returns pending, the same local server follows only that
|
|
318
|
+
exact request and emits one `notifications/claude/channel` event when the
|
|
319
|
+
answer, expiry, or cancellation arrives. Claude receives the constrained
|
|
320
|
+
result as conversation input and can continue without another user message.
|
|
321
|
+
The event contains the binary reading, confidence, lifecycle, and opaque
|
|
322
|
+
handle—never the statement, link token, raw motion, or an instruction that
|
|
323
|
+
turns the result into authorization.
|
|
324
|
+
|
|
325
|
+
The installed `PostToolUse` hook remains a compatibility fallback for an
|
|
326
|
+
ordinary non-channel registration. A channel-armed response marks its
|
|
327
|
+
continuation transport explicitly, and the hook abstains, so two watchers
|
|
328
|
+
cannot wake the same turn. During Anthropic's research preview, the channel
|
|
329
|
+
session must be launched with the command above; a closed or non-enabled
|
|
330
|
+
session may silently drop the event.
|
|
286
331
|
|
|
287
332
|
Other standard MCP hosts retain the bounded tool response and stable-handle
|
|
288
333
|
fallback until they provide an equivalent supported wake mechanism. This
|
|
@@ -301,8 +346,10 @@ statement with the same `idempotency_key`.
|
|
|
301
346
|
|
|
302
347
|
An immediate vibecheck is not proposition caching. It sends one person-approved statement
|
|
303
348
|
for the current choice, expires, and remains bound to its originating client.
|
|
304
|
-
The reflection feed remains asynchronous and keeps its
|
|
305
|
-
|
|
349
|
+
The reflection feed remains asynchronous and keeps its three-item cache
|
|
350
|
+
behavior. When no immediate Live Ask or delivery test has priority, one current
|
|
351
|
+
feed item may appear on Home. The other items remain visible as **Up next**
|
|
352
|
+
under **Settings → Vibe Checks** and do not generate extra notifications.
|
|
306
353
|
|
|
307
354
|
The agent maintains three distinct insights. SomaCheck presents one at a time;
|
|
308
355
|
the other two remain cached. After a completed gesture, the next cached insight
|
package/SKILL.md
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vibecheck
|
|
3
|
-
description:
|
|
3
|
+
description: Give your person a consented body-language channel when words are the bottleneck.
|
|
4
4
|
user-invocable: true
|
|
5
|
-
allowed-tools:
|
|
5
|
+
allowed-tools: mcp__plugin_vibecheck_vibecheck__get_vibecheck_context mcp__plugin_vibecheck_vibecheck__get_vibecheck_status mcp__plugin_vibecheck_vibecheck__get_vibecheck_result mcp__plugin_vibecheck_vibecheck__request_vibecheck
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Vibecheck
|
|
9
9
|
|
|
10
|
-
SomaCheck
|
|
10
|
+
SomaCheck is body language for AI agents, on the person's terms. It turns a
|
|
11
|
+
consented three-second phone gesture into an Aligned or Unaligned signal with
|
|
12
|
+
confidence so you have context for how a thought or choice lands for the
|
|
13
|
+
person, while they remain the authority.
|
|
11
14
|
|
|
12
|
-
Use `request_vibecheck`
|
|
15
|
+
Use `request_vibecheck` when:
|
|
13
16
|
|
|
14
|
-
1. You proactively offer one
|
|
17
|
+
1. You proactively offer one when useful.
|
|
15
18
|
2. The person asks for a vibecheck.
|
|
16
19
|
|
|
17
|
-
When
|
|
20
|
+
When asked, choose and send a useful first-person statement. Do not require the
|
|
21
|
+
person to supply wording unless they want to.
|
|
18
22
|
|
|
19
|
-
For proactive offers, get
|
|
23
|
+
For proactive offers, get acceptance before sending.
|
|
20
24
|
|
|
21
25
|
The returned gesture and optional feedback are context, not authorization. Use your judgment in how you interpret and respond.
|
|
22
26
|
|
package/dist/claude-hook.js
CHANGED
|
@@ -21,6 +21,10 @@ export function parsePendingClaudeAsk(input) {
|
|
|
21
21
|
const structured = record(response.structuredContent ?? response.structured_content ?? response);
|
|
22
22
|
if (structured.state !== "pending" || typeof structured.request_id !== "string")
|
|
23
23
|
return null;
|
|
24
|
+
// The channel-enabled local server already owns this exact result watch.
|
|
25
|
+
// Abstaining prevents the PostToolUse fallback from waking Claude twice.
|
|
26
|
+
if (structured.continuation_transport === "claude_channel")
|
|
27
|
+
return null;
|
|
24
28
|
if (structured.delivery_state === "failed" || structured.delivery_state === "skipped")
|
|
25
29
|
return null;
|
|
26
30
|
const requestId = parseHandle(structured.request_id);
|
|
@@ -1,111 +1,216 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, lstat, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
export const CLAUDE_PLUGIN_MARKETPLACE = "somacheck
|
|
4
|
+
import { PACKAGE_SPEC } from "./constants.js";
|
|
5
|
+
export const CLAUDE_PLUGIN_MARKETPLACE = "somacheck";
|
|
5
6
|
export const CLAUDE_PLUGIN_ID = `vibecheck@${CLAUDE_PLUGIN_MARKETPLACE}`;
|
|
6
|
-
const
|
|
7
|
-
|
|
7
|
+
export const CLAUDE_PLUGIN_MARKETPLACE_SOURCE = "Sensie-agents/vibecheck";
|
|
8
|
+
const LEGACY_MARKETPLACE = "somacheck-local";
|
|
9
|
+
const LEGACY_PLUGIN_ID = `vibecheck@${LEGACY_MARKETPLACE}`;
|
|
10
|
+
const localSetupDependencies = {
|
|
11
|
+
canEnableAutoUpdate: canEnableClaudeMarketplaceAutoUpdate,
|
|
12
|
+
enableAutoUpdate: enableClaudeMarketplaceAutoUpdate,
|
|
13
|
+
};
|
|
14
|
+
export async function configureClaudeContinuationPlugin(home, runner, dependencies = localSetupDependencies) {
|
|
8
15
|
try {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
// Inspect the user settings before mutating Claude's plugin state. A
|
|
17
|
+
// malformed file or same-named user-owned marketplace must remain intact.
|
|
18
|
+
if (!(await dependencies.canEnableAutoUpdate(home)))
|
|
19
|
+
return { status: "failed" };
|
|
20
|
+
const marketplaces = await jsonArrayCommand(runner, ["plugin", "marketplace", "list", "--json"]);
|
|
21
|
+
const marketplace = marketplaces.map(record).find((item) => item.name === CLAUDE_PLUGIN_MARKETPLACE);
|
|
22
|
+
if (marketplace !== undefined && !isCanonicalMarketplace(marketplace)) {
|
|
23
|
+
return { status: "failed" };
|
|
24
|
+
}
|
|
25
|
+
if (marketplace === undefined) {
|
|
26
|
+
if ((await runner.run("claude", [
|
|
27
|
+
"plugin", "marketplace", "add", "--scope", "user", CLAUDE_PLUGIN_MARKETPLACE_SOURCE,
|
|
28
|
+
])).exitCode !== 0) {
|
|
16
29
|
return { status: "failed" };
|
|
17
30
|
}
|
|
18
31
|
}
|
|
19
|
-
else if ((await runner.run("claude", [
|
|
20
|
-
"plugin", "marketplace", "add", "--scope", "user", destination,
|
|
21
|
-
])).exitCode !== 0) {
|
|
32
|
+
else if ((await runner.run("claude", ["plugin", "marketplace", "update", CLAUDE_PLUGIN_MARKETPLACE])).exitCode !== 0) {
|
|
22
33
|
return { status: "failed" };
|
|
23
34
|
}
|
|
24
|
-
const
|
|
25
|
-
const installed =
|
|
26
|
-
&& plugins.some((item) => record(item).id === CLAUDE_PLUGIN_ID && record(item).scope === "user");
|
|
35
|
+
const before = await pluginRecords(runner);
|
|
36
|
+
const installed = before.some((item) => item.id === CLAUDE_PLUGIN_ID && item.scope === "user");
|
|
27
37
|
const action = installed ? "update" : "install";
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
if ((await runner.run("claude", [
|
|
39
|
+
"plugin", action, "--scope", "user", "--yes", CLAUDE_PLUGIN_ID,
|
|
40
|
+
])).exitCode !== 0) {
|
|
41
|
+
return { status: "failed" };
|
|
42
|
+
}
|
|
43
|
+
const verified = await pluginRecords(runner);
|
|
44
|
+
if (!verified.some(isExactChannelPlugin))
|
|
45
|
+
return { status: "failed" };
|
|
46
|
+
if (!(await removeLegacyManagedPlugin(home, runner, marketplaces, verified))) {
|
|
47
|
+
return { status: "failed" };
|
|
48
|
+
}
|
|
49
|
+
if (!(await dependencies.enableAutoUpdate(home)))
|
|
30
50
|
return { status: "failed" };
|
|
31
|
-
|
|
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" };
|
|
51
|
+
return { status: installed ? "updated" : "installed" };
|
|
40
52
|
}
|
|
41
53
|
catch {
|
|
42
54
|
return { status: "failed" };
|
|
43
55
|
}
|
|
44
56
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Persist Claude Code's documented user-scoped auto-update setting without
|
|
59
|
+
* replacing unrelated settings. This is called only from an explicit
|
|
60
|
+
* `setup claude` or `link --client claude` flow.
|
|
61
|
+
*/
|
|
62
|
+
export async function enableClaudeMarketplaceAutoUpdate(home) {
|
|
63
|
+
const loaded = await readClaudeUserSettings(home);
|
|
64
|
+
if (loaded === null || !canEnableAutoUpdateInSettings(loaded.settings))
|
|
65
|
+
return false;
|
|
66
|
+
const marketplaces = optionalRecord(loaded.settings.extraKnownMarketplaces) ?? {};
|
|
67
|
+
const existing = optionalRecord(marketplaces[CLAUDE_PLUGIN_MARKETPLACE]) ?? {};
|
|
68
|
+
const nextSettings = {
|
|
69
|
+
...loaded.settings,
|
|
70
|
+
extraKnownMarketplaces: {
|
|
71
|
+
...marketplaces,
|
|
72
|
+
[CLAUDE_PLUGIN_MARKETPLACE]: {
|
|
73
|
+
...existing,
|
|
74
|
+
source: {
|
|
75
|
+
source: "github",
|
|
76
|
+
repo: CLAUDE_PLUGIN_MARKETPLACE_SOURCE,
|
|
77
|
+
},
|
|
78
|
+
autoUpdate: true,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
const settingsPath = claudeUserSettingsPath(home);
|
|
83
|
+
const temporaryPath = join(dirname(settingsPath), `.settings.json.somacheck-${process.pid}-${randomUUID()}.tmp`);
|
|
50
84
|
try {
|
|
51
|
-
await
|
|
52
|
-
await
|
|
85
|
+
await mkdir(dirname(settingsPath), { recursive: true, mode: 0o700 });
|
|
86
|
+
await writeFile(temporaryPath, `${JSON.stringify(nextSettings, null, 2)}\n`, {
|
|
87
|
+
encoding: "utf8",
|
|
88
|
+
mode: loaded.mode,
|
|
89
|
+
flag: "wx",
|
|
90
|
+
});
|
|
91
|
+
await chmod(temporaryPath, loaded.mode);
|
|
92
|
+
await rename(temporaryPath, settingsPath);
|
|
93
|
+
return true;
|
|
53
94
|
}
|
|
54
|
-
catch
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
throw new Error("SomaCheck will not overwrite an unmanaged Claude plugin directory.");
|
|
95
|
+
catch {
|
|
96
|
+
try {
|
|
97
|
+
await unlink(temporaryPath);
|
|
58
98
|
}
|
|
99
|
+
catch { /* no temporary file */ }
|
|
100
|
+
return false;
|
|
59
101
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
102
|
+
}
|
|
103
|
+
export async function canEnableClaudeMarketplaceAutoUpdate(home) {
|
|
104
|
+
const loaded = await readClaudeUserSettings(home);
|
|
105
|
+
return loaded !== null && canEnableAutoUpdateInSettings(loaded.settings);
|
|
106
|
+
}
|
|
107
|
+
export async function isClaudeMarketplaceAutoUpdateConfigured(home) {
|
|
108
|
+
const loaded = await readClaudeUserSettings(home);
|
|
109
|
+
if (loaded === null)
|
|
110
|
+
return false;
|
|
111
|
+
const marketplaces = optionalRecord(loaded.settings.extraKnownMarketplaces);
|
|
112
|
+
const entry = optionalRecord(marketplaces?.[CLAUDE_PLUGIN_MARKETPLACE]);
|
|
113
|
+
return entry?.autoUpdate === true && hasCanonicalSettingsSource(entry);
|
|
114
|
+
}
|
|
115
|
+
function canEnableAutoUpdateInSettings(settings) {
|
|
116
|
+
if (settings.extraKnownMarketplaces !== undefined
|
|
117
|
+
&& optionalRecord(settings.extraKnownMarketplaces) === null)
|
|
118
|
+
return false;
|
|
119
|
+
const marketplaces = optionalRecord(settings.extraKnownMarketplaces);
|
|
120
|
+
const value = marketplaces?.[CLAUDE_PLUGIN_MARKETPLACE];
|
|
121
|
+
if (value === undefined)
|
|
122
|
+
return true;
|
|
123
|
+
const entry = optionalRecord(value);
|
|
124
|
+
return entry !== null && hasCanonicalSettingsSource(entry);
|
|
125
|
+
}
|
|
126
|
+
function hasCanonicalSettingsSource(entry) {
|
|
127
|
+
const source = optionalRecord(entry.source);
|
|
128
|
+
return source?.source === "github" && source.repo === CLAUDE_PLUGIN_MARKETPLACE_SOURCE;
|
|
129
|
+
}
|
|
130
|
+
async function readClaudeUserSettings(home) {
|
|
131
|
+
const settingsPath = claudeUserSettingsPath(home);
|
|
63
132
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
});
|
|
71
|
-
if (await pathExists(destination))
|
|
72
|
-
await rename(destination, backup);
|
|
73
|
-
await rename(temporary, destination);
|
|
74
|
-
await rm(backup, { recursive: true, force: true });
|
|
133
|
+
const metadata = await lstat(settingsPath);
|
|
134
|
+
if (!metadata.isFile() || metadata.isSymbolicLink())
|
|
135
|
+
return null;
|
|
136
|
+
const parsed = JSON.parse(await readFile(settingsPath, "utf8"));
|
|
137
|
+
const settings = optionalRecord(parsed);
|
|
138
|
+
return settings === null ? null : { settings, mode: metadata.mode & 0o777 };
|
|
75
139
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (isErrno(error, "ENOENT"))
|
|
142
|
+
return { settings: {}, mode: 0o600 };
|
|
143
|
+
return null;
|
|
81
144
|
}
|
|
82
145
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
146
|
+
function claudeUserSettingsPath(home) {
|
|
147
|
+
return join(home, ".claude", "settings.json");
|
|
148
|
+
}
|
|
149
|
+
function optionalRecord(value) {
|
|
150
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
151
|
+
? value
|
|
152
|
+
: null;
|
|
153
|
+
}
|
|
154
|
+
function isErrno(error, code) {
|
|
155
|
+
return error !== null && typeof error === "object" && "code" in error
|
|
156
|
+
&& error.code === code;
|
|
157
|
+
}
|
|
158
|
+
export async function isClaudeChannelPluginConfigured(runner) {
|
|
87
159
|
try {
|
|
88
|
-
return
|
|
160
|
+
return (await pluginRecords(runner)).some(isExactChannelPlugin);
|
|
89
161
|
}
|
|
90
162
|
catch {
|
|
91
|
-
return
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function pluginRecords(runner) {
|
|
167
|
+
return (await jsonArrayCommand(runner, ["plugin", "list", "--json"])).map(record);
|
|
168
|
+
}
|
|
169
|
+
function isExactChannelPlugin(value) {
|
|
170
|
+
const servers = record(value.mcpServers);
|
|
171
|
+
const server = record(servers.vibecheck);
|
|
172
|
+
const args = Array.isArray(server.args) ? server.args : [];
|
|
173
|
+
const expectedArgs = ["-y", PACKAGE_SPEC, "serve", "--client", "claude", "--channel"];
|
|
174
|
+
return value.id === CLAUDE_PLUGIN_ID
|
|
175
|
+
&& value.scope === "user"
|
|
176
|
+
&& value.enabled !== false
|
|
177
|
+
&& server.command === "npx"
|
|
178
|
+
&& args.length === expectedArgs.length
|
|
179
|
+
&& args.every((arg, index) => arg === expectedArgs[index]);
|
|
180
|
+
}
|
|
181
|
+
function isCanonicalMarketplace(value) {
|
|
182
|
+
return value.source === "github" && value.repo === CLAUDE_PLUGIN_MARKETPLACE_SOURCE;
|
|
183
|
+
}
|
|
184
|
+
async function removeLegacyManagedPlugin(home, runner, marketplaces, plugins) {
|
|
185
|
+
const marketplace = Array.isArray(marketplaces)
|
|
186
|
+
? marketplaces.map(record).find((item) => item.name === LEGACY_MARKETPLACE)
|
|
187
|
+
: undefined;
|
|
188
|
+
const expectedPath = join(home, ".sensie", "claude-plugin-marketplace");
|
|
189
|
+
const managedMarketplace = marketplace !== undefined
|
|
190
|
+
&& marketplace.source === "directory"
|
|
191
|
+
&& marketplace.path === expectedPath;
|
|
192
|
+
const legacyInstalled = plugins.some((item) => item.id === LEGACY_PLUGIN_ID && item.scope === "user");
|
|
193
|
+
if (!managedMarketplace)
|
|
194
|
+
return !legacyInstalled;
|
|
195
|
+
if (legacyInstalled && (await runner.run("claude", [
|
|
196
|
+
"plugin", "uninstall", "--scope", "user", "--yes", LEGACY_PLUGIN_ID,
|
|
197
|
+
])).exitCode !== 0) {
|
|
198
|
+
return false;
|
|
92
199
|
}
|
|
200
|
+
return (await runner.run("claude", ["plugin", "marketplace", "remove", LEGACY_MARKETPLACE])).exitCode === 0;
|
|
93
201
|
}
|
|
94
|
-
async function
|
|
202
|
+
async function jsonArrayCommand(runner, args) {
|
|
203
|
+
const result = await runner.run("claude", args);
|
|
204
|
+
if (result.exitCode !== 0)
|
|
205
|
+
throw new Error(`Claude command failed: ${args.join(" ")}`);
|
|
95
206
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
207
|
+
const parsed = JSON.parse(result.stdout);
|
|
208
|
+
if (!Array.isArray(parsed))
|
|
209
|
+
throw new Error("Claude returned a non-array JSON payload.");
|
|
210
|
+
return parsed;
|
|
98
211
|
}
|
|
99
|
-
catch
|
|
100
|
-
|
|
101
|
-
return true;
|
|
102
|
-
try {
|
|
103
|
-
await stat(path);
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
212
|
+
catch {
|
|
213
|
+
throw new Error(`Claude returned invalid JSON: ${args.join(" ")}`);
|
|
109
214
|
}
|
|
110
215
|
}
|
|
111
216
|
function record(value) {
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ 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 { clientDisplayName, detectInstalledClients, detectLegacyHostedRegistration, LocalCommandRunner, manualLegacyHostedRemoveCommand, manualRemoveCommand, manualSetupCommand, parseClientChoice, preflightClientPersistence, registerClient, singleNonInteractiveClientSelection, } from "./client-setup.js";
|
|
6
|
+
import { claudeChannelLaunchCommand, clientDisplayName, detectInstalledClients, detectLegacyHostedRegistration, isClientInstalled, LocalCommandRunner, manualLegacyHostedRemoveCommand, manualRemoveCommand, manualSetupCommand, migrateClaudeDirectRegistration, parseClientChoice, preflightClientPersistence, registerClient, singleNonInteractiveClientSelection, } from "./client-setup.js";
|
|
7
7
|
import { configureClaudeContinuationPlugin } from "./claude-plugin-setup.js";
|
|
8
8
|
import { readConfig } from "./config.js";
|
|
9
9
|
import { awaitClaudeVibecheckResult } from "./claude-hook.js";
|
|
@@ -11,6 +11,7 @@ import { PACKAGE_SPEC, SUPABASE_PUBLISHABLE_KEY, SUPABASE_URL } from "./constant
|
|
|
11
11
|
import { LinkPersistenceError, NonInteractiveLinkError, linkAgent } from "./link.js";
|
|
12
12
|
import { checkReadiness } from "./readiness.js";
|
|
13
13
|
import { formatRecipeDetail, formatRecipeList, formatRecipePrompt, getRecipeExperience } from "./recipes.js";
|
|
14
|
+
import { parseRuntimeMode } from "./runtime-mode.js";
|
|
14
15
|
import { createVibecheckServer } from "./server.js";
|
|
15
16
|
const api = new SupabaseAgentApi(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY);
|
|
16
17
|
const runner = new LocalCommandRunner();
|
|
@@ -30,6 +31,7 @@ function usage() {
|
|
|
30
31
|
" npx -y @somacheck/vibecheck recipes",
|
|
31
32
|
" npx -y @somacheck/vibecheck recipes --id <id> --format prompt",
|
|
32
33
|
" npx -y @somacheck/vibecheck recipe <id>",
|
|
34
|
+
" npx -y @somacheck/vibecheck serve --client claude --channel",
|
|
33
35
|
].join("\n");
|
|
34
36
|
}
|
|
35
37
|
function parseLinkClient(args) {
|
|
@@ -46,11 +48,6 @@ function parseLinkClient(args) {
|
|
|
46
48
|
return [];
|
|
47
49
|
throw new Error(usage());
|
|
48
50
|
}
|
|
49
|
-
function parseServeClient(args) {
|
|
50
|
-
if (args.length !== 3 || args[0] !== "serve" || args[1] !== "--client")
|
|
51
|
-
return null;
|
|
52
|
-
return args[2] === "codex" || args[2] === "claude" ? args[2] : null;
|
|
53
|
-
}
|
|
54
51
|
async function promptForClients(installed) {
|
|
55
52
|
if (installed.length === 0)
|
|
56
53
|
return [];
|
|
@@ -79,6 +76,32 @@ async function promptForClients(installed) {
|
|
|
79
76
|
}
|
|
80
77
|
async function configureClients(clients) {
|
|
81
78
|
for (const client of clients) {
|
|
79
|
+
if (client === "claude") {
|
|
80
|
+
if (!(await isClientInstalled(client, runner))) {
|
|
81
|
+
output(`○ ${clientDisplayName(client)} is not installed; skipped.`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const plugin = await configureClaudeContinuationPlugin(homedir(), runner);
|
|
85
|
+
if (plugin.status === "failed") {
|
|
86
|
+
throw new ClientPreflightError("Could not configure the SomaCheck Channel plugin for Claude Code.");
|
|
87
|
+
}
|
|
88
|
+
const migration = await migrateClaudeDirectRegistration(runner);
|
|
89
|
+
if (migration === "needs_update") {
|
|
90
|
+
throw new ClientPreflightError([
|
|
91
|
+
'Claude Code has a user-owned MCP server named "vibecheck"; it was not changed.',
|
|
92
|
+
`Remove it only if it is obsolete: ${manualRemoveCommand(client)}`,
|
|
93
|
+
].join("\n"));
|
|
94
|
+
}
|
|
95
|
+
if (migration === "failed" || migration === "not_installed") {
|
|
96
|
+
throw new ClientPreflightError("Could not remove the superseded direct SomaCheck MCP registration.");
|
|
97
|
+
}
|
|
98
|
+
output(`✓ SomaCheck Channel plugin ${plugin.status === "installed" ? "was configured" : "is current"} for Claude Code.`);
|
|
99
|
+
output("✓ Automatic SomaCheck marketplace updates are enabled for future reviewed releases.");
|
|
100
|
+
if (migration === "removed")
|
|
101
|
+
output("✓ Removed the superseded direct MCP registration.");
|
|
102
|
+
output(` Start Claude Code with SomaCheck Channels: ${claudeChannelLaunchCommand()}`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
82
105
|
const result = await registerClient(client, runner);
|
|
83
106
|
if (result.status === "registered") {
|
|
84
107
|
output(`✓ Configured ${clientDisplayName(client)}.`);
|
|
@@ -99,13 +122,6 @@ async function configureClients(clients) {
|
|
|
99
122
|
else {
|
|
100
123
|
output(`✗ Could not configure ${clientDisplayName(client)} automatically.`);
|
|
101
124
|
}
|
|
102
|
-
if (client === "claude" && ["registered", "upgraded", "already_registered"].includes(result.status)) {
|
|
103
|
-
const plugin = await configureClaudeContinuationPlugin(homedir(), runner);
|
|
104
|
-
if (plugin.status === "failed") {
|
|
105
|
-
throw new ClientPreflightError("Could not configure automatic SomaCheck results for Claude Code. No pairing code was used.");
|
|
106
|
-
}
|
|
107
|
-
output(`✓ Automatic Claude result continuation ${plugin.status === "installed" ? "was configured" : "is ready"}.`);
|
|
108
|
-
}
|
|
109
125
|
}
|
|
110
126
|
}
|
|
111
127
|
async function preflightLinkClients(clients, interactive) {
|
|
@@ -146,6 +162,42 @@ async function preflightLinkClients(clients, interactive) {
|
|
|
146
162
|
].join("\n"));
|
|
147
163
|
}
|
|
148
164
|
output(`Checking ${clientDisplayName(client)} before redeeming the pairing code...`);
|
|
165
|
+
if (client === "claude") {
|
|
166
|
+
if (!(await isClientInstalled(client, runner))) {
|
|
167
|
+
throw new ClientPreflightError([
|
|
168
|
+
"Not linking here: Claude Code is not installed or its CLI is unavailable.",
|
|
169
|
+
"The pairing code has NOT been used.",
|
|
170
|
+
].join("\n"));
|
|
171
|
+
}
|
|
172
|
+
const plugin = await configureClaudeContinuationPlugin(homedir(), runner);
|
|
173
|
+
if (plugin.status === "failed") {
|
|
174
|
+
throw new ClientPreflightError([
|
|
175
|
+
"Not linking here: the SomaCheck Channel plugin could not be installed and verified for Claude Code.",
|
|
176
|
+
"The pairing code has NOT been used. Repair Claude's plugin setup, then run the link command again.",
|
|
177
|
+
].join("\n"));
|
|
178
|
+
}
|
|
179
|
+
const migration = await migrateClaudeDirectRegistration(runner);
|
|
180
|
+
if (migration === "needs_update") {
|
|
181
|
+
throw new ClientPreflightError([
|
|
182
|
+
'Not linking here: Claude Code has a user-owned MCP entry named "vibecheck".',
|
|
183
|
+
"It was not changed because its command does not match a SomaCheck-managed registration.",
|
|
184
|
+
`Remove it only if it is obsolete: ${manualRemoveCommand(client)}`,
|
|
185
|
+
"The pairing code has NOT been used.",
|
|
186
|
+
].join("\n"));
|
|
187
|
+
}
|
|
188
|
+
if (migration === "failed" || migration === "not_installed") {
|
|
189
|
+
throw new ClientPreflightError([
|
|
190
|
+
"Not linking here: the superseded direct SomaCheck MCP registration could not be removed safely.",
|
|
191
|
+
"The pairing code has NOT been used.",
|
|
192
|
+
].join("\n"));
|
|
193
|
+
}
|
|
194
|
+
output("✓ Claude Code has one exact, plugin-scoped SomaCheck Channel server.");
|
|
195
|
+
output("✓ Automatic SomaCheck marketplace updates are enabled for future reviewed releases.");
|
|
196
|
+
if (migration === "removed")
|
|
197
|
+
output("✓ Removed the superseded direct MCP registration before redeeming the pairing code.");
|
|
198
|
+
output(` Start Claude Code with SomaCheck Channels: ${claudeChannelLaunchCommand()}`);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
149
201
|
const preflight = await preflightClientPersistence(client, runner);
|
|
150
202
|
if (preflight.status === "ready") {
|
|
151
203
|
output(`✓ ${clientDisplayName(client)} already has the exact SomaCheck MCP registration.`);
|
|
@@ -171,16 +223,6 @@ async function preflightLinkClients(clients, interactive) {
|
|
|
171
223
|
"run the link command again.",
|
|
172
224
|
].join("\n"));
|
|
173
225
|
}
|
|
174
|
-
if (client === "claude") {
|
|
175
|
-
const plugin = await configureClaudeContinuationPlugin(homedir(), runner);
|
|
176
|
-
if (plugin.status === "failed") {
|
|
177
|
-
throw new ClientPreflightError([
|
|
178
|
-
"Not linking here: automatic SomaCheck result continuation could not be configured for Claude Code.",
|
|
179
|
-
"The pairing code has NOT been used. Repair Claude's plugin setup, then run the link command again.",
|
|
180
|
-
].join("\n"));
|
|
181
|
-
}
|
|
182
|
-
output("✓ Automatic Claude result continuation is ready before redeeming the pairing code.");
|
|
183
|
-
}
|
|
184
226
|
}
|
|
185
227
|
return selectedClients;
|
|
186
228
|
}
|
|
@@ -201,13 +243,15 @@ async function refreshRuntimeHealth(client) {
|
|
|
201
243
|
context_probe_ok: true,
|
|
202
244
|
});
|
|
203
245
|
}
|
|
204
|
-
async function startServer(
|
|
246
|
+
async function startServer(runtime) {
|
|
247
|
+
const runtimeClient = runtime?.client ?? null;
|
|
205
248
|
const server = createVibecheckServer({
|
|
206
249
|
api,
|
|
207
250
|
loadToken: async () => (await readConfig(homedir(), runtimeClient ?? undefined)).token,
|
|
208
251
|
identity: runtimeClient === null
|
|
209
252
|
? { kind: "legacy" }
|
|
210
253
|
: { kind: "local", client_key: runtimeClient },
|
|
254
|
+
...(runtime?.claudeChannel ? { claudeChannelWatch: {} } : {}),
|
|
211
255
|
});
|
|
212
256
|
await server.connect(new StdioServerTransport());
|
|
213
257
|
if (runtimeClient !== null) {
|
|
@@ -299,9 +343,9 @@ async function main() {
|
|
|
299
343
|
throw new Error(usage());
|
|
300
344
|
return (await runDoctor()) ? 0 : 2;
|
|
301
345
|
}
|
|
302
|
-
const
|
|
303
|
-
if (
|
|
304
|
-
await startServer(
|
|
346
|
+
const runtime = parseRuntimeMode(args);
|
|
347
|
+
if (runtime !== null) {
|
|
348
|
+
await startServer(runtime);
|
|
305
349
|
return 0;
|
|
306
350
|
}
|
|
307
351
|
if (args.length !== 0)
|
package/dist/client-setup.js
CHANGED
|
@@ -26,13 +26,17 @@ export function clientDisplayName(client) {
|
|
|
26
26
|
return client === "codex" ? "Codex / ChatGPT desktop" : "Claude Code";
|
|
27
27
|
}
|
|
28
28
|
function mcpServerArgs(client) {
|
|
29
|
-
|
|
29
|
+
const args = ["-y", PACKAGE_SPEC, "serve", "--client", client];
|
|
30
|
+
return client === "claude" ? [...args, "--channel"] : args;
|
|
31
|
+
}
|
|
32
|
+
export function claudeChannelLaunchCommand() {
|
|
33
|
+
return "claude --channels plugin:vibecheck@somacheck";
|
|
30
34
|
}
|
|
31
35
|
export function manualSetupCommand(client) {
|
|
32
36
|
if (client === "codex") {
|
|
33
37
|
return `codex mcp add ${MCP_SERVER_NAME} -- npx ${mcpServerArgs(client).join(" ")}`;
|
|
34
38
|
}
|
|
35
|
-
return `
|
|
39
|
+
return `npx -y ${PACKAGE_SPEC} setup claude`;
|
|
36
40
|
}
|
|
37
41
|
export function manualRemoveCommand(client) {
|
|
38
42
|
return client === "codex"
|
|
@@ -139,28 +143,46 @@ function isManagedSomaCheckRegistration(client, stdout) {
|
|
|
139
143
|
// earlier exact 0.6 patch release. Custom wrappers, extra arguments,
|
|
140
144
|
// disabled entries, later versions, and registrations naming the other
|
|
141
145
|
// client remain user-owned.
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
||
|
|
146
|
-
||
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
&& args.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
146
|
+
const version = parseExactSemver(packageArg.slice("@somacheck/vibecheck@".length));
|
|
147
|
+
const currentVersion = parseExactSemver(PACKAGE_SPEC.slice("@somacheck/vibecheck@".length));
|
|
148
|
+
if (version === null || currentVersion === null
|
|
149
|
+
|| compareSemver(version, [0, 5, 0]) < 0
|
|
150
|
+
|| compareSemver(version, currentVersion) >= 0)
|
|
151
|
+
return false;
|
|
152
|
+
const expected = ["-y", packageArg, "serve", "--client", client];
|
|
153
|
+
if (client === "claude" && compareSemver(version, [0, 6, 12]) >= 0) {
|
|
154
|
+
expected.push("--channel");
|
|
155
|
+
}
|
|
156
|
+
return args.length === expected.length
|
|
157
|
+
&& args.every((arg, index) => arg === expected[index]);
|
|
158
|
+
}
|
|
159
|
+
function parseExactSemver(value) {
|
|
160
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(value);
|
|
161
|
+
return match === null ? null : [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
162
|
+
}
|
|
163
|
+
function compareSemver(left, right) {
|
|
164
|
+
for (let index = 0; index < 3; index += 1) {
|
|
165
|
+
if (left[index] !== right[index])
|
|
166
|
+
return left[index] - right[index];
|
|
167
|
+
}
|
|
168
|
+
return 0;
|
|
160
169
|
}
|
|
161
170
|
export async function isClientRegistered(client, runner) {
|
|
162
171
|
return (await clientRegistrationState(client, runner)) === "current";
|
|
163
172
|
}
|
|
173
|
+
export async function migrateClaudeDirectRegistration(runner) {
|
|
174
|
+
if (!(await isClientInstalled("claude", runner)))
|
|
175
|
+
return "not_installed";
|
|
176
|
+
const state = await clientRegistrationState("claude", runner);
|
|
177
|
+
if (state === "missing")
|
|
178
|
+
return "absent";
|
|
179
|
+
if (state === "needs_update")
|
|
180
|
+
return "needs_update";
|
|
181
|
+
const removed = await runner.run("claude", ["mcp", "remove", "--scope", "user", MCP_SERVER_NAME]);
|
|
182
|
+
if (removed.exitCode !== 0)
|
|
183
|
+
return "failed";
|
|
184
|
+
return (await clientRegistrationState("claude", runner)) === "missing" ? "removed" : "failed";
|
|
185
|
+
}
|
|
164
186
|
export async function detectInstalledClients(runner) {
|
|
165
187
|
const clients = ["codex", "claude"];
|
|
166
188
|
const installed = await Promise.all(clients.map(async (client) => ({
|
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.13";
|
|
6
6
|
export const PACKAGE_SPEC = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;
|
|
7
7
|
export const SOMACHECK_SETUP_URL = "https://testflight.apple.com/join/C4mAH3zz";
|
|
8
8
|
export const MCP_SERVER_NAME = "vibecheck";
|
package/dist/readiness.js
CHANGED
|
@@ -1,29 +1,8 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
1
|
import { readConfig } from "./config.js";
|
|
2
|
+
import { isClaudeChannelPluginConfigured, isClaudeMarketplaceAutoUpdateConfigured, } from "./claude-plugin-setup.js";
|
|
4
3
|
import { SomaCheckCompatibilityError, SomaCheckHttpError } from "./api.js";
|
|
5
|
-
import { clientDisplayName, clientRegistrationState, detectLegacyHostedRegistration, isClientInstalled, manualLegacyHostedRemoveCommand, manualRemoveCommand, manualSetupCommand, } from "./client-setup.js";
|
|
4
|
+
import { claudeChannelLaunchCommand, clientDisplayName, clientRegistrationState, detectLegacyHostedRegistration, isClientInstalled, manualLegacyHostedRemoveCommand, manualRemoveCommand, manualSetupCommand, } from "./client-setup.js";
|
|
6
5
|
import { BACKEND_PROTOCOL_VERSION, PACKAGE_VERSION, SOMACHECK_SETUP_URL } from "./constants.js";
|
|
7
|
-
function vibecheckRegistrationCount(value) {
|
|
8
|
-
if (!value || typeof value !== "object")
|
|
9
|
-
return 0;
|
|
10
|
-
let count = 0;
|
|
11
|
-
for (const [key, child] of Object.entries(value)) {
|
|
12
|
-
if (key === "mcpServers" && child && typeof child === "object" && !Array.isArray(child)) {
|
|
13
|
-
count += Object.keys(child).filter((name) => name.startsWith("vibecheck")).length;
|
|
14
|
-
}
|
|
15
|
-
count += vibecheckRegistrationCount(child);
|
|
16
|
-
}
|
|
17
|
-
return count;
|
|
18
|
-
}
|
|
19
|
-
async function claudeVibecheckRegistrationCount(home) {
|
|
20
|
-
try {
|
|
21
|
-
return vibecheckRegistrationCount(JSON.parse(await readFile(join(home, ".claude.json"), "utf8")));
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return 0;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
6
|
export async function checkReadiness(dependencies) {
|
|
28
7
|
let token;
|
|
29
8
|
try {
|
|
@@ -71,11 +50,6 @@ export async function checkReadiness(dependencies) {
|
|
|
71
50
|
}
|
|
72
51
|
}
|
|
73
52
|
const backendReachable = statusProbeOk && contextProbeOk;
|
|
74
|
-
const claudeRegistrationCount = await claudeVibecheckRegistrationCount(dependencies.home);
|
|
75
|
-
if (claudeRegistrationCount > 1) {
|
|
76
|
-
dependencies.output(`○ Multiple Claude Code registrations whose names start with "vibecheck" were found (${claudeRegistrationCount}).`);
|
|
77
|
-
dependencies.output(" Remove stale entries from ~/.claude.json so only the current user-scope vibecheck registration remains.");
|
|
78
|
-
}
|
|
79
53
|
const clients = ["codex", "claude"];
|
|
80
54
|
let installedClientCount = 0;
|
|
81
55
|
let hasLegacyHostedRegistration = false;
|
|
@@ -95,6 +69,29 @@ export async function checkReadiness(dependencies) {
|
|
|
95
69
|
dependencies.output(` Optional manual cleanup: ${manualLegacyHostedRemoveCommand(client)}`);
|
|
96
70
|
}
|
|
97
71
|
const registration = await clientRegistrationState(client, dependencies.runner);
|
|
72
|
+
if (client === "claude") {
|
|
73
|
+
const pluginConfigured = await isClaudeChannelPluginConfigured(dependencies.runner);
|
|
74
|
+
if (!pluginConfigured) {
|
|
75
|
+
dependencies.output("✗ Claude Code does not have the exact SomaCheck Channel plugin.");
|
|
76
|
+
dependencies.output(` Run: ${manualSetupCommand(client)}`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (!(await isClaudeMarketplaceAutoUpdateConfigured(dependencies.home))) {
|
|
80
|
+
dependencies.output("✗ Automatic SomaCheck plugin updates are not enabled for Claude Code.");
|
|
81
|
+
dependencies.output(` Run: ${manualSetupCommand(client)}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (registration !== "missing") {
|
|
85
|
+
dependencies.output('✗ Claude Code also has a direct MCP server named "vibecheck".');
|
|
86
|
+
dependencies.output(" The plugin is installed, but the duplicate direct tool surface must be removed.");
|
|
87
|
+
dependencies.output(` Run: ${manualRemoveCommand(client)}`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
registeredClients.push(client);
|
|
91
|
+
dependencies.output("✓ Claude Code has one exact, plugin-scoped SomaCheck Channel server.");
|
|
92
|
+
dependencies.output("✓ Automatic SomaCheck marketplace updates are enabled.");
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
98
95
|
if (registration === "current") {
|
|
99
96
|
registeredClients.push(client);
|
|
100
97
|
dependencies.output(`✓ ${clientDisplayName(client)} is configured.`);
|
|
@@ -156,9 +153,17 @@ export async function checkReadiness(dependencies) {
|
|
|
156
153
|
dependencies.output('✗ Remove the legacy "somacheck" connector before this setup can be Ready.');
|
|
157
154
|
}
|
|
158
155
|
const ready = readyClients.length > 0;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
if (ready) {
|
|
157
|
+
dependencies.output("Setup checks pass. Restart your agent client, then ask it to check your SomaCheck status.");
|
|
158
|
+
if (readyClients.includes("claude")) {
|
|
159
|
+
dependencies.output("○ Doctor verifies Claude's installed plugin and backend compatibility, not a running Channel session.");
|
|
160
|
+
dependencies.output(` Start Claude with: ${claudeChannelLaunchCommand()}`);
|
|
161
|
+
dependencies.output(" Team/Enterprise use also requires the administrator to allowlist this plugin.");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
dependencies.output("Setup is not complete yet. Fix the items marked ✗, then run: vibecheck doctor");
|
|
166
|
+
}
|
|
162
167
|
return { linked: true, backendReachable, backendCompatible, registeredClients, readyClients, ready };
|
|
163
168
|
}
|
|
164
169
|
//# sourceMappingURL=readiness.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Parse only the exact stdio runtime forms installed by this package.
|
|
2
|
+
* Claude Channels are local-only and must never be enabled for Codex. */
|
|
3
|
+
export function parseRuntimeMode(args) {
|
|
4
|
+
if (args[0] !== "serve" || args[1] !== "--client")
|
|
5
|
+
return null;
|
|
6
|
+
if (args.length === 3 && (args[2] === "codex" || args[2] === "claude")) {
|
|
7
|
+
return { client: args[2], claudeChannel: false };
|
|
8
|
+
}
|
|
9
|
+
if (args.length === 4 && args[2] === "claude" && args[3] === "--channel") {
|
|
10
|
+
return { client: "claude", claudeChannel: true };
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=runtime-mode.js.map
|
package/dist/server.js
CHANGED
|
@@ -20,6 +20,11 @@ const LIVE_ASK_POLL_DELAYS_MS = [
|
|
|
20
20
|
4_000, 4_000, 4_000, 4_000, 4_000, 4_000, 4_000, 4_000, 4_000,
|
|
21
21
|
];
|
|
22
22
|
const CLAUDE_CHANNEL_POLL_INTERVAL_MS = 2_000;
|
|
23
|
+
const CLAUDE_CHANNEL_ACTIVE_POLL_WINDOW_MS = 30 * 60 * 1_000;
|
|
24
|
+
const CLAUDE_CHANNEL_EXTENDED_POLL_WINDOW_MS = 2 * 60 * 60 * 1_000;
|
|
25
|
+
const CLAUDE_CHANNEL_ACTIVE_MAX_POLL_INTERVAL_MS = 15 * 1_000;
|
|
26
|
+
const CLAUDE_CHANNEL_EXTENDED_MAX_POLL_INTERVAL_MS = 30 * 1_000;
|
|
27
|
+
const CLAUDE_CHANNEL_LONG_MAX_POLL_INTERVAL_MS = 60 * 1_000;
|
|
23
28
|
const CLAUDE_CHANNEL_MAX_WATCH_MS = 15 * 60 * 1_000;
|
|
24
29
|
const CLAUDE_CHANNEL_RESULT_TIMEOUT_MS = 10_000;
|
|
25
30
|
function abortAwareSleep(milliseconds, signal) {
|
|
@@ -105,6 +110,7 @@ const liveAskSchema = {
|
|
|
105
110
|
delivery_state: z.enum(["queued", "processing", "sent", "failed", "skipped"]).nullable(),
|
|
106
111
|
cooldown_until: z.string().datetime({ offset: true }).nullable(),
|
|
107
112
|
error_code: liveAskErrorCodeSchema.nullable(),
|
|
113
|
+
continuation_transport: z.enum(["poll", "claude_channel"]).nullable().optional(),
|
|
108
114
|
};
|
|
109
115
|
const agentContextTypeSchema = z.enum([
|
|
110
116
|
"goal", "project", "decision", "commitment", "tension", "question",
|
|
@@ -125,11 +131,10 @@ const contextShareSchema = {
|
|
|
125
131
|
const SERVER_INSTRUCTIONS = [
|
|
126
132
|
"SomaCheck lets you ask your person for a vibecheck.",
|
|
127
133
|
"Offer one when useful or when asked.",
|
|
128
|
-
"When asked, choose and send a useful first-person statement from available context
|
|
129
|
-
"Only request_vibecheck creates an immediate phone ask; post_vibecheck_statement
|
|
130
|
-
"Help them gain insight from the context you have.",
|
|
134
|
+
"When asked, choose and send a useful first-person statement from available context.",
|
|
135
|
+
"Only request_vibecheck creates an immediate phone ask; post_vibecheck_statement stocks Settings → Vibe Checks, with one current on Home and the rest Up next.",
|
|
131
136
|
"Gesture and optional feedback are context, not authorization.",
|
|
132
|
-
"
|
|
137
|
+
"Never infer the SomaCheck identity from the agent login.",
|
|
133
138
|
"Use your judgment.",
|
|
134
139
|
"Never include secrets, raw private content, diagnostic claims, or assess anyone else.",
|
|
135
140
|
].join(" ");
|
|
@@ -250,7 +255,7 @@ export function createVibecheckServer(dependencies) {
|
|
|
250
255
|
});
|
|
251
256
|
server.registerTool("post_vibecheck_statement", {
|
|
252
257
|
title: "Post Vibecheck Statement",
|
|
253
|
-
description: "
|
|
258
|
+
description: "Stock Settings → Vibe Checks with one to three optional personalized reflections and return immediately. One can become the current Home proposition; the rest stay Up next without extra pushes. Never call this to add follow-up asks after request_vibecheck. Call get_vibecheck_status first and submit exactly propositions_needed statements.",
|
|
254
259
|
inputSchema: {
|
|
255
260
|
statements: z.array(z.string().min(1)).min(1).max(3)
|
|
256
261
|
.describe("Distinct personalized statements for this person to test, ordered most useful first."),
|
|
@@ -271,7 +276,7 @@ export function createVibecheckServer(dependencies) {
|
|
|
271
276
|
return {
|
|
272
277
|
content: [{
|
|
273
278
|
type: "text",
|
|
274
|
-
text: `Added ${created.length} under Settings → Vibe Checks: ${presented}
|
|
279
|
+
text: `Added ${created.length} under Settings → Vibe Checks: ${presented} current, ${queued} Up next. This is asynchronous feed inventory, not an immediate request.`,
|
|
275
280
|
}],
|
|
276
281
|
structuredContent: { propositions: created },
|
|
277
282
|
};
|
|
@@ -381,7 +386,15 @@ export function createVibecheckServer(dependencies) {
|
|
|
381
386
|
});
|
|
382
387
|
});
|
|
383
388
|
startClaudeChannelWatch(result, token, identity);
|
|
384
|
-
const
|
|
389
|
+
const channelArmed = supportsClaudeChannel
|
|
390
|
+
&& result.state === "pending"
|
|
391
|
+
&& result.delivery_state !== "failed"
|
|
392
|
+
&& result.delivery_state !== "skipped";
|
|
393
|
+
const structuredContent = {
|
|
394
|
+
...result,
|
|
395
|
+
error_code: null,
|
|
396
|
+
...(channelArmed ? { continuation_transport: "claude_channel" } : {}),
|
|
397
|
+
};
|
|
385
398
|
const text = result.state === "completed"
|
|
386
399
|
? `Vibecheck completed: ${result.verdict}, confidence ${formatChannelConfidence(result.confidence)}${formatUserFeedback(result.user_feedback)}.`
|
|
387
400
|
: result.state === "pending"
|
|
@@ -390,7 +403,7 @@ export function createVibecheckServer(dependencies) {
|
|
|
390
403
|
+ `Open SomaCheck on your phone, signed into the same account, and check Home. `
|
|
391
404
|
+ `If you do not have the app, install it at ${SOMACHECK_SETUP_URL} and finish phone setup. `
|
|
392
405
|
+ `Keep this request ID; do not create a duplicate or claim the phone received it. It expires at ${result.expires_at}.`
|
|
393
|
-
: pendingWaitText(result.request_id, result.expires_at,
|
|
406
|
+
: pendingWaitText(result.request_id, result.expires_at, channelArmed)
|
|
394
407
|
: `This vibecheck is ${result.state}.`;
|
|
395
408
|
return { content: [{ type: "text", text }], structuredContent };
|
|
396
409
|
}
|
|
@@ -411,14 +424,34 @@ async function watchForClaudeChannelResult(created, token, identity, dependencie
|
|
|
411
424
|
const options = dependencies.claudeChannelWatch || undefined;
|
|
412
425
|
const now = options?.now ?? Date.now;
|
|
413
426
|
const sleep = options?.sleep ?? unrefSleep;
|
|
414
|
-
const
|
|
427
|
+
const initialInterval = Math.max(1, options?.pollIntervalMs ?? CLAUDE_CHANNEL_POLL_INTERVAL_MS);
|
|
428
|
+
const startedAt = now();
|
|
415
429
|
const maxWatchMs = options?.maxWatchMs ?? CLAUDE_CHANNEL_MAX_WATCH_MS;
|
|
416
430
|
const parsedExpiry = created.expires_at === null ? Number.NaN : Date.parse(created.expires_at);
|
|
417
|
-
const
|
|
431
|
+
const hasExpiry = Number.isFinite(parsedExpiry);
|
|
432
|
+
// A valid expiry is authoritative in production. The short grace window
|
|
433
|
+
// permits one result read after expiry, when the API transitions a still-
|
|
434
|
+
// pending request to its terminal state. maxWatchMs remains an explicit
|
|
435
|
+
// deterministic-test cap, but is never an implicit production cap.
|
|
436
|
+
const expiryDeadline = hasExpiry
|
|
437
|
+
? Math.max(startedAt, parsedExpiry) + CLAUDE_CHANNEL_RESULT_TIMEOUT_MS
|
|
438
|
+
: startedAt + maxWatchMs;
|
|
439
|
+
const deadline = options?.maxWatchMs === undefined
|
|
440
|
+
? expiryDeadline
|
|
441
|
+
: Math.min(expiryDeadline, startedAt + maxWatchMs);
|
|
442
|
+
let interval = initialInterval;
|
|
418
443
|
let consecutiveReadFailures = 0;
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
444
|
+
// Preserve the explicit zero-duration test switch without issuing a
|
|
445
|
+
// surprising immediate read.
|
|
446
|
+
if (deadline <= startedAt)
|
|
447
|
+
return;
|
|
448
|
+
while (true) {
|
|
449
|
+
const beforeSleep = now();
|
|
450
|
+
if (beforeSleep > deadline)
|
|
451
|
+
return;
|
|
452
|
+
await sleep(Math.min(interval, Math.max(0, deadline - beforeSleep)));
|
|
453
|
+
const readAt = now();
|
|
454
|
+
if (readAt > deadline)
|
|
422
455
|
return;
|
|
423
456
|
const readAbort = new AbortController();
|
|
424
457
|
const readTimeout = setTimeout(() => readAbort.abort(new Error("Claude channel result read timed out.")), CLAUDE_CHANNEL_RESULT_TIMEOUT_MS);
|
|
@@ -432,13 +465,22 @@ async function watchForClaudeChannelResult(created, token, identity, dependencie
|
|
|
432
465
|
consecutiveReadFailures += 1;
|
|
433
466
|
if (consecutiveReadFailures >= 3)
|
|
434
467
|
return;
|
|
468
|
+
interval = Math.min(pollIntervalCeiling(options, now() - startedAt, initialInterval), interval * 2);
|
|
435
469
|
continue;
|
|
436
470
|
}
|
|
437
471
|
finally {
|
|
438
472
|
clearTimeout(readTimeout);
|
|
439
473
|
}
|
|
440
|
-
if (lifecycle.status === "pending")
|
|
474
|
+
if (lifecycle.status === "pending") {
|
|
475
|
+
// Keep a responsive first check, then avoid a long-lived hot poll while
|
|
476
|
+
// still checking often enough to deliver shortly after completion. The
|
|
477
|
+
// active window stays at <=15s so a result feels immediate in a normal
|
|
478
|
+
// gesture; only unusually long-lived requests relax their cadence.
|
|
479
|
+
interval = Math.min(pollIntervalCeiling(options, now() - startedAt, initialInterval), interval * 2);
|
|
480
|
+
if (readAt >= deadline)
|
|
481
|
+
return;
|
|
441
482
|
continue;
|
|
483
|
+
}
|
|
442
484
|
const handle = `live:${requestId}`;
|
|
443
485
|
const content = lifecycle.status === "answered"
|
|
444
486
|
? `SomaCheck vibecheck ${handle} completed: ${lifecycle.verdict}, confidence ${formatChannelConfidence(lifecycle.confidence)}${formatUserFeedback(lifecycle.user_feedback)}.`
|
|
@@ -463,6 +505,18 @@ async function watchForClaudeChannelResult(created, token, identity, dependencie
|
|
|
463
505
|
return;
|
|
464
506
|
}
|
|
465
507
|
}
|
|
508
|
+
function pollIntervalCeiling(options, elapsedMs, initialInterval) {
|
|
509
|
+
if (options?.maxPollIntervalMs !== undefined) {
|
|
510
|
+
return Math.max(initialInterval, options.maxPollIntervalMs);
|
|
511
|
+
}
|
|
512
|
+
if (elapsedMs < CLAUDE_CHANNEL_ACTIVE_POLL_WINDOW_MS) {
|
|
513
|
+
return Math.max(initialInterval, CLAUDE_CHANNEL_ACTIVE_MAX_POLL_INTERVAL_MS);
|
|
514
|
+
}
|
|
515
|
+
if (elapsedMs < CLAUDE_CHANNEL_EXTENDED_POLL_WINDOW_MS) {
|
|
516
|
+
return Math.max(initialInterval, CLAUDE_CHANNEL_EXTENDED_MAX_POLL_INTERVAL_MS);
|
|
517
|
+
}
|
|
518
|
+
return Math.max(initialInterval, CLAUDE_CHANNEL_LONG_MAX_POLL_INTERVAL_MS);
|
|
519
|
+
}
|
|
466
520
|
function unrefSleep(milliseconds) {
|
|
467
521
|
if (milliseconds <= 0)
|
|
468
522
|
return Promise.resolve();
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@somacheck/vibecheck",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.13",
|
|
4
4
|
"mcpName": "io.github.Sensie-agents/vibecheck",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Body language for AI agents, on your terms. A consented SomaCheck signal for how a thought or choice lands.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/Sensie-agents/vibecheck.git"
|
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"dist/*.js",
|
|
19
19
|
"SKILL.md",
|
|
20
|
-
"recipes/**"
|
|
21
|
-
"claude-marketplace/**"
|
|
20
|
+
"recipes/**"
|
|
22
21
|
],
|
|
23
22
|
"scripts": {
|
|
24
23
|
"build": "rm -rf dist && tsc -p tsconfig.json",
|
|
@@ -1,22 +0,0 @@
|
|
|
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.11",
|
|
14
|
-
"author": {
|
|
15
|
-
"name": "Sensie",
|
|
16
|
-
"email": "agents@joinsensie.com"
|
|
17
|
-
},
|
|
18
|
-
"category": "productivity",
|
|
19
|
-
"tags": ["vibecheck", "somacheck", "decision-support", "mcp"]
|
|
20
|
-
}
|
|
21
|
-
]
|
|
22
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "vibecheck",
|
|
3
|
-
"description": "Let Claude offer a SomaCheck vibecheck and continue when the phone result arrives.",
|
|
4
|
-
"version": "0.6.11",
|
|
5
|
-
"author": {
|
|
6
|
-
"name": "Sensie",
|
|
7
|
-
"email": "agents@joinsensie.com"
|
|
8
|
-
},
|
|
9
|
-
"homepage": "https://github.com/Sensie-agents/vibecheck",
|
|
10
|
-
"repository": "https://github.com/Sensie-agents/vibecheck",
|
|
11
|
-
"license": "MIT",
|
|
12
|
-
"keywords": ["vibecheck", "somacheck", "decision-support", "mcp"]
|
|
13
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
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.11 claude-hook await-result",
|
|
11
|
-
"asyncRewake": true,
|
|
12
|
-
"timeout": 900
|
|
13
|
-
}
|
|
14
|
-
]
|
|
15
|
-
}
|
|
16
|
-
]
|
|
17
|
-
}
|
|
18
|
-
}
|