@somacheck/vibecheck 0.6.12 → 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 CHANGED
@@ -1,7 +1,9 @@
1
1
  # @somacheck/vibecheck
2
2
 
3
- An MCP server that lets an AI agent, with a person's consent, send one statement
4
- for that person to test with a physical wrist gesture in the SomaCheck iOS app.
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,17 +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.12 link <CODE> --client <CLIENT>`
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 and registers the local MCP server as
18
- a Channel so a phone result can enter the same open conversation as a new
19
- event. Setup is not reported ready until the
20
- selected client has the exact pinned `vibecheck` MCP registration, the backend
21
- accepts protocol `5` / toolset `vibecheck-0.6`, and both status and context
22
- probes succeed. It contacts exactly one network host: the Supabase project below.
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.
23
26
 
24
27
  **It will refuse unsafe sandbox linking, on purpose.** `link` checks safety
25
28
  *before* redeeming. Pairing codes are single-use, so redeeming one inside an
@@ -31,9 +34,10 @@ with. The safe paths are:
31
34
  `--client claude` or `--client codex`.
32
35
 
33
36
  For the second path, this package first proves that exact client CLI is installed
34
- and that its persistent MCP config can be inspected and written. Claude setup
35
- also verifies the user-scoped continuation plugin before redemption. If that preflight
36
- fails, the pairing code is not used. Non-interactive implicit selection,
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,
37
41
  `--client all`, and `--client none` are rejected before redemption.
38
42
 
39
43
  **What the server does once running.** It exposes MCP tools that share bounded,
@@ -98,42 +102,62 @@ before the pairing code is redeemed.
98
102
  To configure or repair a client later:
99
103
 
100
104
  ```text
101
- npx -y @somacheck/vibecheck@0.6.12 setup codex
102
- npx -y @somacheck/vibecheck@0.6.12 setup claude
103
- npx -y @somacheck/vibecheck@0.6.12 doctor
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
104
108
  ```
105
109
 
106
- 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`:
107
112
 
108
113
  ```json
109
114
  {
110
115
  "mcpServers": {
111
116
  "vibecheck": {
112
117
  "command": "npx",
113
- "args": ["-y", "@somacheck/vibecheck@0.6.12", "serve", "--client", "codex"]
118
+ "args": ["-y", "@somacheck/vibecheck@0.6.13", "serve", "--client", "codex"]
114
119
  }
115
120
  }
116
121
  }
117
122
  ```
118
123
 
119
- For Claude Code: `claude mcp add --scope user vibecheck -- npx -y @somacheck/vibecheck@0.6.12 serve --client claude --channel`
124
+ For Claude Code, install the public Channel plugin:
120
125
 
121
- Claude Channels are currently an Anthropic research preview. Until Anthropic
122
- approves SomaCheck on its channel allowlist, start Claude Code with:
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:
123
130
 
124
131
  ```text
125
- claude --dangerously-load-development-channels server:vibecheck
132
+ claude --channels plugin:vibecheck@somacheck
126
133
  ```
127
134
 
128
- Keep that Claude Code session open. Channel events are queued while Claude is
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
129
137
  busy, but a closed session cannot receive them. Claude does not acknowledge
130
138
  channel notifications, so retain the stable `live:<uuid>` request handle as a
131
- manual recovery path.
132
-
133
- The link step writes only the bearer token in `~/.sensie/config.json`. The client
134
- setup step asks Codex or Claude to add the pinned MCP command to that client's own
135
- configuration; it never copies the bearer token there. The interactive command
136
- completes both steps. Most clients only pick up a new server after restart.
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.
137
161
 
138
162
  If `doctor` reports another MCP server named `somacheck`, that is the legacy
139
163
  hosted connector and is separate from this local package. The setup command
@@ -192,7 +216,7 @@ The skill expects the `@somacheck/vibecheck` MCP server to already be linked as
192
216
 
193
217
  ## Agent reflection recipes
194
218
 
195
- Give your agent a consented signal when words are the bottleneck. These recipes
219
+ Body language for AI agents, on your terms. These recipes
196
220
  combine SomaCheck with a named platform so a person can privately reflect on
197
221
  their own wording, interpretation, study design, or listening choice:
198
222
 
@@ -265,7 +289,7 @@ blocked until the capability and revocation probe in the safety review passes.
265
289
  | `get_vibecheck_context` | Read recent completed gestures for this exact agent link without retaining every `request_id`. |
266
290
  | `get_vibecheck_status` | Read the live three-proposition cache and how many distinct insights it needs. |
267
291
  | `share_somacheck_context` | Share 1–20 bounded, derived, user-authorized observations; raw conversations and secrets are rejected. |
268
- | `post_vibecheck_statement` | Add one to three personalized statements without blocking; returns a stable `request_id` for each. |
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. |
269
293
  | `get_vibecheck_result` | Read that exact proposition once by `request_id`; returns queued, pending, or its terminal result. |
270
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. |
271
295
 
@@ -322,8 +346,10 @@ statement with the same `idempotency_key`.
322
346
 
323
347
  An immediate vibecheck is not proposition caching. It sends one person-approved statement
324
348
  for the current choice, expires, and remains bound to its originating client.
325
- The reflection feed remains asynchronous and keeps its existing three-item
326
- cache behavior.
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.
327
353
 
328
354
  The agent maintains three distinct insights. SomaCheck presents one at a time;
329
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: Ask your person for a SomaCheck vibecheck when it would help.
3
+ description: Give your person a consented body-language channel when words are the bottleneck.
4
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
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 lets you ask your person for a vibecheck.
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` in either case:
15
+ Use `request_vibecheck` when:
13
16
 
14
- 1. You proactively offer one because you believe it would be useful.
17
+ 1. You proactively offer one when useful.
15
18
  2. The person asks for a vibecheck.
16
19
 
17
- When the person asks, choose and send a useful first-person statement from the context available to you. Do not make them supply wording unless they want to. Help the person get an insight about themselves from the context you have.
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 the person's acceptance before sending.
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
 
@@ -1,111 +1,216 @@
1
- import { cp, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
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 { fileURLToPath } from "node:url";
4
- export const CLAUDE_PLUGIN_MARKETPLACE = "somacheck-local";
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 MANAGED_MARKER = ".somacheck-managed-plugin-marketplace";
7
- export async function configureClaudeContinuationPlugin(home, runner, packageRoot = fileURLToPath(new URL("..", import.meta.url))) {
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
- 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
+ // 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 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");
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
- const result = await runner.run("claude", ["plugin", action, "--scope", "user", "--yes", CLAUDE_PLUGIN_ID]);
29
- if (result.exitCode !== 0)
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
- 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" };
51
+ return { status: installed ? "updated" : "installed" };
40
52
  }
41
53
  catch {
42
54
  return { status: "failed" };
43
55
  }
44
56
  }
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);
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 stat(destination);
52
- await stat(join(destination, MANAGED_MARKER));
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 (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.");
95
+ catch {
96
+ try {
97
+ await unlink(temporaryPath);
58
98
  }
99
+ catch { /* no temporary file */ }
100
+ return false;
59
101
  }
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`;
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
- 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 });
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
- finally {
77
- await rm(temporary, { recursive: true, force: true });
78
- if (await pathExists(backup) && !(await pathExists(destination))) {
79
- await rename(backup, destination);
80
- }
140
+ catch (error) {
141
+ if (isErrno(error, "ENOENT"))
142
+ return { settings: {}, mode: 0o600 };
143
+ return null;
81
144
  }
82
145
  }
83
- async function jsonCommand(runner, args) {
84
- const result = await runner.run("claude", args);
85
- if (result.exitCode !== 0)
86
- return null;
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 JSON.parse(result.stdout);
160
+ return (await pluginRecords(runner)).some(isExactChannelPlugin);
89
161
  }
90
162
  catch {
91
- return null;
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 pathExists(path) {
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
- await readFile(path);
97
- return true;
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 (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
- }
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 { claudeChannelLaunchCommand, 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";
@@ -76,6 +76,32 @@ async function promptForClients(installed) {
76
76
  }
77
77
  async function configureClients(clients) {
78
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
+ }
79
105
  const result = await registerClient(client, runner);
80
106
  if (result.status === "registered") {
81
107
  output(`✓ Configured ${clientDisplayName(client)}.`);
@@ -96,14 +122,6 @@ async function configureClients(clients) {
96
122
  else {
97
123
  output(`✗ Could not configure ${clientDisplayName(client)} automatically.`);
98
124
  }
99
- if (client === "claude" && ["registered", "upgraded", "already_registered"].includes(result.status)) {
100
- const plugin = await configureClaudeContinuationPlugin(homedir(), runner);
101
- if (plugin.status === "failed") {
102
- throw new ClientPreflightError("Could not configure automatic SomaCheck results for Claude Code. No pairing code was used.");
103
- }
104
- output(`✓ Automatic Claude result continuation ${plugin.status === "installed" ? "was configured" : "is ready"}.`);
105
- output(` Start Claude Code with SomaCheck Channels: ${claudeChannelLaunchCommand()}`);
106
- }
107
125
  }
108
126
  }
109
127
  async function preflightLinkClients(clients, interactive) {
@@ -144,6 +162,42 @@ async function preflightLinkClients(clients, interactive) {
144
162
  ].join("\n"));
145
163
  }
146
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
+ }
147
201
  const preflight = await preflightClientPersistence(client, runner);
148
202
  if (preflight.status === "ready") {
149
203
  output(`✓ ${clientDisplayName(client)} already has the exact SomaCheck MCP registration.`);
@@ -169,17 +223,6 @@ async function preflightLinkClients(clients, interactive) {
169
223
  "run the link command again.",
170
224
  ].join("\n"));
171
225
  }
172
- if (client === "claude") {
173
- const plugin = await configureClaudeContinuationPlugin(homedir(), runner);
174
- if (plugin.status === "failed") {
175
- throw new ClientPreflightError([
176
- "Not linking here: automatic SomaCheck result continuation could not be configured for Claude Code.",
177
- "The pairing code has NOT been used. Repair Claude's plugin setup, then run the link command again.",
178
- ].join("\n"));
179
- }
180
- output("✓ Automatic Claude result continuation is ready before redeeming the pairing code.");
181
- output(` Start Claude Code with SomaCheck Channels: ${claudeChannelLaunchCommand()}`);
182
- }
183
226
  }
184
227
  return selectedClients;
185
228
  }
@@ -30,13 +30,13 @@ function mcpServerArgs(client) {
30
30
  return client === "claude" ? [...args, "--channel"] : args;
31
31
  }
32
32
  export function claudeChannelLaunchCommand() {
33
- return `claude --dangerously-load-development-channels server:${MCP_SERVER_NAME}`;
33
+ return "claude --channels plugin:vibecheck@somacheck";
34
34
  }
35
35
  export function manualSetupCommand(client) {
36
36
  if (client === "codex") {
37
37
  return `codex mcp add ${MCP_SERVER_NAME} -- npx ${mcpServerArgs(client).join(" ")}`;
38
38
  }
39
- return `claude mcp add --scope user ${MCP_SERVER_NAME} -- npx ${mcpServerArgs(client).join(" ")}`;
39
+ return `npx -y ${PACKAGE_SPEC} setup claude`;
40
40
  }
41
41
  export function manualRemoveCommand(client) {
42
42
  return client === "codex"
@@ -143,30 +143,46 @@ function isManagedSomaCheckRegistration(client, stdout) {
143
143
  // earlier exact 0.6 patch release. Custom wrappers, extra arguments,
144
144
  // disabled entries, later versions, and registrations naming the other
145
145
  // client remain user-owned.
146
- const previousExactClient = (/^@somacheck\/vibecheck@0\.5\.\d+$/.test(packageArg)
147
- || packageArg === "@somacheck/vibecheck@0.6.0"
148
- || packageArg === "@somacheck/vibecheck@0.6.1"
149
- || packageArg === "@somacheck/vibecheck@0.6.2"
150
- || packageArg === "@somacheck/vibecheck@0.6.3"
151
- || packageArg === "@somacheck/vibecheck@0.6.4"
152
- || packageArg === "@somacheck/vibecheck@0.6.5"
153
- || packageArg === "@somacheck/vibecheck@0.6.6"
154
- || packageArg === "@somacheck/vibecheck@0.6.7"
155
- || packageArg === "@somacheck/vibecheck@0.6.8"
156
- || packageArg === "@somacheck/vibecheck@0.6.9"
157
- || packageArg === "@somacheck/vibecheck@0.6.10"
158
- || packageArg === "@somacheck/vibecheck@0.6.11")
159
- && args.length === 5
160
- && args[0] === "-y"
161
- && args[1] === packageArg
162
- && args[2] === "serve"
163
- && args[3] === "--client"
164
- && args[4] === client;
165
- return previousExactClient;
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;
166
169
  }
167
170
  export async function isClientRegistered(client, runner) {
168
171
  return (await clientRegistrationState(client, runner)) === "current";
169
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
+ }
170
186
  export async function detectInstalledClients(runner) {
171
187
  const clients = ["codex", "claude"];
172
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.12";
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
- dependencies.output(ready
160
- ? "Ready. Restart your agent client, then ask it to check your SomaCheck status."
161
- : "Setup is not complete yet. Fix the items marked ✗, then run: vibecheck doctor");
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
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) {
@@ -126,11 +131,10 @@ const contextShareSchema = {
126
131
  const SERVER_INSTRUCTIONS = [
127
132
  "SomaCheck lets you ask your person for a vibecheck.",
128
133
  "Offer one when useful or when asked.",
129
- "When asked, choose and send a useful first-person statement from available context; do not ask for wording unless they want to.",
130
- "Only request_vibecheck creates an immediate phone ask; post_vibecheck_statement adds optional reflections under Settings → Vibe Checks, never Home.",
131
- "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.",
132
136
  "Gesture and optional feedback are context, not authorization.",
133
- "The SomaCheck account can differ from the host account; never infer its identity from the agent login.",
137
+ "Never infer the SomaCheck identity from the agent login.",
134
138
  "Use your judgment.",
135
139
  "Never include secrets, raw private content, diagnostic claims, or assess anyone else.",
136
140
  ].join(" ");
@@ -251,7 +255,7 @@ export function createVibecheckServer(dependencies) {
251
255
  });
252
256
  server.registerTool("post_vibecheck_statement", {
253
257
  title: "Post Vibecheck Statement",
254
- description: "Fill Settings → Vibe Checks with one to three optional personalized reflections and return immediately. These are quiet feed inventory, never additional Home asks after request_vibecheck. Call get_vibecheck_status first and submit exactly propositions_needed statements.",
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.",
255
259
  inputSchema: {
256
260
  statements: z.array(z.string().min(1)).min(1).max(3)
257
261
  .describe("Distinct personalized statements for this person to test, ordered most useful first."),
@@ -272,7 +276,7 @@ export function createVibecheckServer(dependencies) {
272
276
  return {
273
277
  content: [{
274
278
  type: "text",
275
- text: `Added ${created.length} under Settings → Vibe Checks: ${presented} ready, ${queued} cached. No additional Home ask was created.`,
279
+ text: `Added ${created.length} under Settings → Vibe Checks: ${presented} current, ${queued} Up next. This is asynchronous feed inventory, not an immediate request.`,
276
280
  }],
277
281
  structuredContent: { propositions: created },
278
282
  };
@@ -420,14 +424,34 @@ async function watchForClaudeChannelResult(created, token, identity, dependencie
420
424
  const options = dependencies.claudeChannelWatch || undefined;
421
425
  const now = options?.now ?? Date.now;
422
426
  const sleep = options?.sleep ?? unrefSleep;
423
- const interval = options?.pollIntervalMs ?? CLAUDE_CHANNEL_POLL_INTERVAL_MS;
427
+ const initialInterval = Math.max(1, options?.pollIntervalMs ?? CLAUDE_CHANNEL_POLL_INTERVAL_MS);
428
+ const startedAt = now();
424
429
  const maxWatchMs = options?.maxWatchMs ?? CLAUDE_CHANNEL_MAX_WATCH_MS;
425
430
  const parsedExpiry = created.expires_at === null ? Number.NaN : Date.parse(created.expires_at);
426
- const deadline = Math.min(Number.isFinite(parsedExpiry) ? parsedExpiry : now() + maxWatchMs, now() + maxWatchMs);
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;
427
443
  let consecutiveReadFailures = 0;
428
- while (now() < deadline) {
429
- await sleep(Math.min(interval, Math.max(0, deadline - now())));
430
- if (now() >= deadline)
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)
431
455
  return;
432
456
  const readAbort = new AbortController();
433
457
  const readTimeout = setTimeout(() => readAbort.abort(new Error("Claude channel result read timed out.")), CLAUDE_CHANNEL_RESULT_TIMEOUT_MS);
@@ -441,13 +465,22 @@ async function watchForClaudeChannelResult(created, token, identity, dependencie
441
465
  consecutiveReadFailures += 1;
442
466
  if (consecutiveReadFailures >= 3)
443
467
  return;
468
+ interval = Math.min(pollIntervalCeiling(options, now() - startedAt, initialInterval), interval * 2);
444
469
  continue;
445
470
  }
446
471
  finally {
447
472
  clearTimeout(readTimeout);
448
473
  }
449
- 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;
450
482
  continue;
483
+ }
451
484
  const handle = `live:${requestId}`;
452
485
  const content = lifecycle.status === "answered"
453
486
  ? `SomaCheck vibecheck ${handle} completed: ${lifecycle.verdict}, confidence ${formatChannelConfidence(lifecycle.confidence)}${formatUserFeedback(lifecycle.user_feedback)}.`
@@ -472,6 +505,18 @@ async function watchForClaudeChannelResult(created, token, identity, dependencie
472
505
  return;
473
506
  }
474
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
+ }
475
520
  function unrefSleep(milliseconds) {
476
521
  if (milliseconds <= 0)
477
522
  return Promise.resolve();
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@somacheck/vibecheck",
3
- "version": "0.6.12",
3
+ "version": "0.6.13",
4
4
  "mcpName": "io.github.Sensie-agents/vibecheck",
5
- "description": "Send a consented vibecheck to SomaCheck and use the result as a signal.",
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.12",
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.12",
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.12 claude-hook await-result",
11
- "asyncRewake": true,
12
- "timeout": 900
13
- }
14
- ]
15
- }
16
- ]
17
- }
18
- }