@somacheck/vibecheck 0.6.12 → 0.6.14
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 +73 -41
- package/SKILL.md +11 -7
- package/dist/claude-plugin-setup.js +183 -78
- package/dist/cli.js +63 -20
- package/dist/client-setup.js +38 -22
- package/dist/constants.js +1 -1
- package/dist/readiness.js +36 -31
- package/dist/recipes.js +7 -7
- package/dist/server.js +65 -17
- package/package.json +3 -4
- package/recipes/chattermill-research-reflection.md +11 -10
- package/recipes/dovetail-research-reflection.md +2 -2
- package/recipes/great-question-research-reflection.md +5 -4
- package/recipes/maze-research-reflection.md +9 -9
- package/recipes/prolific-research-reflection.md +2 -2
- package/recipes/questionpro-research-reflection.md +6 -3
- package/recipes/typeform-research-reflection.md +3 -3
- package/recipes/user-interviews-research-reflection.md +9 -7
- 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,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.
|
|
14
|
+
**What the `link` command does.** `npx -y @somacheck/vibecheck@0.6.14 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
|
-
|
|
22
|
-
|
|
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,15 +34,16 @@ 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
|
|
35
|
-
|
|
36
|
-
|
|
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,
|
|
40
44
|
user-authorized context summaries, create a statement for the user to test,
|
|
41
|
-
read its outcome, recover recent completed gesture context, and
|
|
42
|
-
three
|
|
45
|
+
read its outcome, recover recent completed gesture context, and optionally stock
|
|
46
|
+
up to three feed items when the person asks. Version 0.6 adds `request_vibecheck` for a
|
|
43
47
|
person-approved, immediate phone check-in. That path is deliberately separate
|
|
44
48
|
from the reflection feed.
|
|
45
49
|
|
|
@@ -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.
|
|
102
|
-
npx -y @somacheck/vibecheck@0.6.
|
|
103
|
-
npx -y @somacheck/vibecheck@0.6.
|
|
105
|
+
npx -y @somacheck/vibecheck@0.6.14 setup codex
|
|
106
|
+
npx -y @somacheck/vibecheck@0.6.14 setup claude
|
|
107
|
+
npx -y @somacheck/vibecheck@0.6.14 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.14 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.
|
|
118
|
+
"args": ["-y", "@somacheck/vibecheck@0.6.14", "serve", "--client", "codex"]
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
```
|
|
118
123
|
|
|
119
|
-
For Claude Code
|
|
124
|
+
For Claude Code, install the public Channel plugin:
|
|
120
125
|
|
|
121
|
-
|
|
122
|
-
|
|
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 --
|
|
132
|
+
claude --channels plugin:vibecheck@somacheck
|
|
126
133
|
```
|
|
127
134
|
|
|
128
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -263,9 +287,9 @@ blocked until the capability and revocation probe in the safety review passes.
|
|
|
263
287
|
| Tool | What it does |
|
|
264
288
|
|---|---|
|
|
265
289
|
| `get_vibecheck_context` | Read recent completed gestures for this exact agent link without retaining every `request_id`. |
|
|
266
|
-
| `get_vibecheck_status` | Read the
|
|
290
|
+
| `get_vibecheck_status` | Read the three-proposition database cache and its available capacity; this does not verify phone display. |
|
|
267
291
|
| `share_somacheck_context` | Share 1–20 bounded, derived, user-authorized observations; raw conversations and secrets are rejected. |
|
|
268
|
-
| `post_vibecheck_statement` |
|
|
292
|
+
| `post_vibecheck_statement` | When the person asks to stock reflections for later or has explicitly authorized scheduled stocking, add one to the reported available maximum without blocking. One may become database-current and the rest queue under Vibe Checks without extra pushes. Returns a stable `request_id` for each created row; it does not verify phone display. |
|
|
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,18 +346,26 @@ 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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
349
|
+
Handle multiple immediate requests one at a time; do not turn the reflection feed
|
|
350
|
+
into a surrogate queue of immediate asks.
|
|
351
|
+
The reflection feed remains asynchronous and keeps its three-item cache
|
|
352
|
+
behavior. When no immediate Live Ask or delivery test has priority, one current
|
|
353
|
+
feed item may appear on Home. The other items remain visible as **Up next**
|
|
354
|
+
under **Settings → Vibe Checks** and do not generate extra notifications.
|
|
355
|
+
|
|
356
|
+
The database feed has capacity for three distinct insights, but capacity is not
|
|
357
|
+
a quota. Stock only the genuinely useful items the person asked to save for later,
|
|
358
|
+
from one up to the available maximum; never invent filler. SomaCheck can present
|
|
359
|
+
one at a time while other created items remain cached. After a completed gesture, the next cached insight
|
|
330
360
|
is promoted and the open slot is marked for replenishment automatically. Cached insights
|
|
331
361
|
expire sooner than a presented check-in so stale context does not masquerade as
|
|
332
362
|
a timely observation.
|
|
333
363
|
|
|
334
|
-
For a
|
|
335
|
-
|
|
336
|
-
`propositions_needed`
|
|
364
|
+
For a scheduled loop, call `get_vibecheck_context` and `get_vibecheck_status`.
|
|
365
|
+
Only when the person has asked to stock later reflections, post between one and
|
|
366
|
+
the reported `propositions_needed` maximum; it is available capacity, not a
|
|
367
|
+
required count. An explicit, still-active opt-in to scheduled stocking covers
|
|
368
|
+
later scheduled runs without asking again each time. MCP supplies the tools and durable
|
|
337
369
|
context, but it does not itself wake an idle model. Immediate event-driven runs
|
|
338
370
|
require a host webhook/agent trigger; scheduled runs provide the portable path
|
|
339
371
|
across local MCP clients.
|
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
|
|
|
@@ -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) {
|