@massa-ai/claude-plugin 1.7.0 → 1.8.0
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/.claude-plugin/plugin.json +1 -1
- package/hooks/massa-ai-hook.ts +56 -1
- package/install.sh +144 -30
- package/package.json +1 -1
package/hooks/massa-ai-hook.ts
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
import { spawnSync } from "child_process";
|
|
31
31
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, fstatSync } from "fs";
|
|
32
|
+
import { homedir } from "os";
|
|
32
33
|
import path from "path";
|
|
33
34
|
|
|
34
35
|
// ── Event type mapping ──────────────────────────────────────────────────────
|
|
@@ -132,6 +133,60 @@ export function readStdin(): string {
|
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
// ── API key resolution (SEC-06) ─────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Path to the runtime config file, resolved the same way
|
|
140
|
+
* `packages/shared/src/config/xdg.ts` resolves it: `XDG_CONFIG_HOME` when set
|
|
141
|
+
* and non-blank, else `~/.config`.
|
|
142
|
+
*
|
|
143
|
+
* Duplicated rather than imported on purpose. This binary's entire dependency
|
|
144
|
+
* surface is `child_process`/`fs`/`os`/`path`; pulling in `@massa-ai/shared`
|
|
145
|
+
* would drag the config loader, its Prisma-adjacent transitive graph, and its
|
|
146
|
+
* startup cost into a process that runs on every single tool call and is
|
|
147
|
+
* budgeted in milliseconds. The duplication is nine lines and is pinned by
|
|
148
|
+
* tests on both sides.
|
|
149
|
+
*/
|
|
150
|
+
export function getHookConfigPath(): string {
|
|
151
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
152
|
+
const base = xdg && xdg.trim() ? xdg : path.join(homedir(), ".config");
|
|
153
|
+
return path.join(base, "massa-ai", "config.json");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Resolve the Tools API key: `MASSA_AI_API_KEY` → `config.json`
|
|
158
|
+
* `security.apiKey` → none. Returns "" when no key is available.
|
|
159
|
+
*
|
|
160
|
+
* SEC-01 auto-provisions the key into config.json on first API start, and this
|
|
161
|
+
* binary never runs the `env.ts` seeding that puts it in the environment for
|
|
162
|
+
* everything else — so without reading the file it would send unauthenticated
|
|
163
|
+
* POSTs forever. Because the hook silent-degrades by contract, that failure
|
|
164
|
+
* has no symptom: observation capture just stops.
|
|
165
|
+
*
|
|
166
|
+
* Whitespace-only values count as unset, matching `usable()` in
|
|
167
|
+
* `packages/shared/src/config/api-key.ts`. Every failure mode (missing file,
|
|
168
|
+
* unreadable, malformed JSON, wrong shape) degrades to "" rather than throwing:
|
|
169
|
+
* a hook must never block the agent.
|
|
170
|
+
*
|
|
171
|
+
* Deliberately not memoised. A hook process handles exactly one event and
|
|
172
|
+
* exits, so there is no second call to amortise, and a module-level cache would
|
|
173
|
+
* make the resolution untestable without a reset seam.
|
|
174
|
+
*/
|
|
175
|
+
export function resolveHookApiKey(): string {
|
|
176
|
+
const fromEnv = process.env.MASSA_AI_API_KEY;
|
|
177
|
+
if (fromEnv && fromEnv.trim()) return fromEnv.trim();
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const parsed = JSON.parse(readFileSync(getHookConfigPath(), "utf8"));
|
|
181
|
+
const stored = parsed?.security?.apiKey;
|
|
182
|
+
if (typeof stored === "string" && stored.trim()) return stored.trim();
|
|
183
|
+
} catch {
|
|
184
|
+
// Missing, unreadable, or malformed config → no key (silent-degrade).
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return "";
|
|
188
|
+
}
|
|
189
|
+
|
|
135
190
|
// ── POST helper ─────────────────────────────────────────────────────────────
|
|
136
191
|
|
|
137
192
|
export function postObservation(
|
|
@@ -149,7 +204,7 @@ export function postObservation(
|
|
|
149
204
|
const headers: Record<string, string> = {
|
|
150
205
|
"Content-Type": "application/json",
|
|
151
206
|
};
|
|
152
|
-
const apiKey =
|
|
207
|
+
const apiKey = resolveHookApiKey();
|
|
153
208
|
if (apiKey) {
|
|
154
209
|
headers["x-api-key"] = apiKey;
|
|
155
210
|
}
|
package/install.sh
CHANGED
|
@@ -12,6 +12,18 @@
|
|
|
12
12
|
# MCP registration is delegated to scripts/install-agents.sh, the single writer
|
|
13
13
|
# of host MCP config; it merges the massa-ai entry alongside the hooks block.
|
|
14
14
|
#
|
|
15
|
+
# Plugin-registry registration is delegated to the `claude` CLI, which owns the
|
|
16
|
+
# format of known_marketplaces.json / installed_plugins.json / settings.json's
|
|
17
|
+
# enabledPlugins. Writing those files by hand is what this installer used to
|
|
18
|
+
# omit entirely, which is why the plugin never appeared in /plugin despite the
|
|
19
|
+
# files above all being written correctly.
|
|
20
|
+
#
|
|
21
|
+
# The two routes are mutually exclusive by construction. When the CLI route
|
|
22
|
+
# succeeds, the plugin bundle supplies commands, agents and hooks/hooks.json
|
|
23
|
+
# itself, so this script removes its own loose copies instead of adding them —
|
|
24
|
+
# otherwise every lifecycle event fires twice and every command appears twice.
|
|
25
|
+
# When the CLI is absent or too old, the file route runs exactly as before.
|
|
26
|
+
#
|
|
15
27
|
# Idempotent: re-running is a no-op when owned hooks already present.
|
|
16
28
|
# Uninstall removes only ownership-marked hook entries + commands/agents,
|
|
17
29
|
# preserving user keys and user hooks.
|
|
@@ -27,9 +39,15 @@ set -euo pipefail
|
|
|
27
39
|
|
|
28
40
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
29
41
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
42
|
+
# shellcheck source=scripts/lib/installer-shared.sh
|
|
43
|
+
source "$REPO_ROOT/scripts/lib/installer-shared.sh"
|
|
30
44
|
SCOPE="user"
|
|
31
45
|
UNINSTALL=0
|
|
32
46
|
DRY_RUN=0
|
|
47
|
+
# The marketplace root to register. install-harness.sh resolves this once for
|
|
48
|
+
# the whole run (--plugin-source local|copy|auto); a direct invocation of this
|
|
49
|
+
# script defaults to the checkout it lives in.
|
|
50
|
+
PLUGIN_SOURCE_ROOT="${MASSA_AI_PLUGIN_SOURCE_ROOT:-$REPO_ROOT}"
|
|
33
51
|
|
|
34
52
|
for arg in "$@"; do
|
|
35
53
|
case "$arg" in
|
|
@@ -323,9 +341,72 @@ try {
|
|
|
323
341
|
NODE
|
|
324
342
|
}
|
|
325
343
|
|
|
344
|
+
# ── Plugin-registry registration (delegated to the claude CLI) ──────────────
|
|
345
|
+
# The CLI owns the registry format, so it is the only supported way to make the
|
|
346
|
+
# plugin appear in /plugin. Every failure path returns non-zero and the caller
|
|
347
|
+
# falls back to the file route: a missing CLI, a build too old for
|
|
348
|
+
# `plugin marketplace add`, or an unrelated binary named `claude` must degrade,
|
|
349
|
+
# never abort. Scope is deliberately absent — Claude Code records plugin
|
|
350
|
+
# installs at user scope even for --project, the same reason PLUGIN_REGISTRY
|
|
351
|
+
# resolves from $HOME.
|
|
352
|
+
PLUGIN_MARKETPLACE="massa-ai"
|
|
353
|
+
PLUGIN_ID="massa-ai@massa-ai"
|
|
354
|
+
|
|
355
|
+
claude_plugin_registered() {
|
|
356
|
+
[[ -f "$PLUGIN_REGISTRY" ]] || return 1
|
|
357
|
+
grep -q '"massa-ai@' "$PLUGIN_REGISTRY" 2>/dev/null
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
register_claude_plugin() {
|
|
361
|
+
# Explicit opt-out. Also what pins the file-route tests to the file route:
|
|
362
|
+
# without it the suite's outcome would depend on whether the machine running
|
|
363
|
+
# it happens to have the claude CLI installed.
|
|
364
|
+
[[ "${MASSA_AI_SKIP_PLUGIN_REGISTRY:-0}" == "1" ]] && return 1
|
|
365
|
+
installer_host_cli_supports claude plugin marketplace || return 1
|
|
366
|
+
if [[ ! -f "$PLUGIN_SOURCE_ROOT/.claude-plugin/marketplace.json" ]]; then
|
|
367
|
+
vecho " – no marketplace manifest under $PLUGIN_SOURCE_ROOT — keeping file route"
|
|
368
|
+
return 1
|
|
369
|
+
fi
|
|
370
|
+
# Both subcommands are idempotent: a second run reports "already on disk" /
|
|
371
|
+
# "already installed" and exits 0, leaving the registry files byte-identical.
|
|
372
|
+
claude plugin marketplace add "$PLUGIN_SOURCE_ROOT" </dev/null >/dev/null 2>&1 || return 1
|
|
373
|
+
claude plugin install "$PLUGIN_ID" </dev/null >/dev/null 2>&1 || return 1
|
|
374
|
+
claude_plugin_registered
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
unregister_claude_plugin() {
|
|
378
|
+
installer_host_cli_supports claude plugin marketplace || return 0
|
|
379
|
+
claude plugin uninstall "$PLUGIN_ID" </dev/null >/dev/null 2>&1 || true
|
|
380
|
+
claude plugin marketplace remove "$PLUGIN_MARKETPLACE" </dev/null >/dev/null 2>&1 || true
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
# Strip artifacts a previous file-route install left behind. Called on the
|
|
384
|
+
# plugin route, where the bundle supplies all three itself. Without this an
|
|
385
|
+
# upgrading user keeps their old loose commands and merged hooks *and* gains
|
|
386
|
+
# the plugin's copies — double-firing every lifecycle event, which is the exact
|
|
387
|
+
# bug the guard inside merge_settings_hooks exists to prevent.
|
|
388
|
+
remove_file_route_artifacts() {
|
|
389
|
+
if [[ -f "$SETTINGS_JSON" ]]; then
|
|
390
|
+
merge_settings_hooks "$SETTINGS_JSON" "uninstall"
|
|
391
|
+
fi
|
|
392
|
+
if [[ -d "$TARGET/commands" ]]; then
|
|
393
|
+
for src in "$SCRIPT_DIR/commands/"*.md; do
|
|
394
|
+
[[ -f "$src" ]] || continue
|
|
395
|
+
rm -f "$TARGET/commands/massa-ai-$(basename "$src" .md).md"
|
|
396
|
+
done
|
|
397
|
+
fi
|
|
398
|
+
if [[ -d "$TARGET/agents" ]]; then
|
|
399
|
+
for src in "$TARGET/agents/"massa-ai-*.md; do
|
|
400
|
+
[[ -f "$src" ]] || continue
|
|
401
|
+
rm -f "$src"
|
|
402
|
+
done
|
|
403
|
+
fi
|
|
404
|
+
}
|
|
405
|
+
|
|
326
406
|
# ── Uninstall ───────────────────────────────────────────────────────────────
|
|
327
407
|
if [[ "$UNINSTALL" -eq 1 ]]; then
|
|
328
408
|
echo "Uninstalling massa-ai Claude Code plugin (scope: $SCOPE)..."
|
|
409
|
+
unregister_claude_plugin
|
|
329
410
|
uninstall_bundled_skills
|
|
330
411
|
# Remove owned hook entries (preserves user hooks + user keys)
|
|
331
412
|
if [[ -f "$SETTINGS_JSON" ]]; then
|
|
@@ -357,30 +438,53 @@ fi
|
|
|
357
438
|
|
|
358
439
|
# ── Install ──────────────────────────────────────────────────────────────────
|
|
359
440
|
vecho "Installing massa-ai Claude Code plugin to: $TARGET"
|
|
360
|
-
mkdir -p "$TARGET/commands" "$TARGET/agents"
|
|
361
441
|
|
|
362
|
-
#
|
|
363
|
-
|
|
364
|
-
#
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
vecho " + /massa-ai-${name}"
|
|
370
|
-
command_count=$((command_count + 1))
|
|
371
|
-
done
|
|
442
|
+
# Route selection. The plugin route is preferred because it is the only one
|
|
443
|
+
# that puts massa-ai in /plugin; the file route remains the fallback so a host
|
|
444
|
+
# without a usable `claude` CLI still gets a working install.
|
|
445
|
+
PLUGIN_ROUTE=0
|
|
446
|
+
if register_claude_plugin; then
|
|
447
|
+
PLUGIN_ROUTE=1
|
|
448
|
+
fi
|
|
372
449
|
|
|
373
|
-
|
|
374
|
-
# included). The massa-ai- name prefix is the ownership marker used by uninstall.
|
|
450
|
+
command_count=0
|
|
375
451
|
specialist_count=0
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
done
|
|
383
|
-
|
|
452
|
+
|
|
453
|
+
if [[ "$PLUGIN_ROUTE" -eq 1 ]]; then
|
|
454
|
+
# The bundle at $PLUGIN_SOURCE_ROOT/apps/claude-plugin already carries these,
|
|
455
|
+
# so counting them keeps the summary honest without copying anything.
|
|
456
|
+
for src in "$SCRIPT_DIR/commands/"*.md; do
|
|
457
|
+
[[ -f "$src" ]] && command_count=$((command_count + 1))
|
|
458
|
+
done
|
|
459
|
+
for src in "$SCRIPT_DIR/agents/"massa-ai-*.md; do
|
|
460
|
+
[[ -f "$src" ]] && specialist_count=$((specialist_count + 1))
|
|
461
|
+
done
|
|
462
|
+
remove_file_route_artifacts
|
|
463
|
+
vecho " + registered ${PLUGIN_ID} (marketplace root: ${PLUGIN_SOURCE_ROOT})"
|
|
464
|
+
vecho " + commands, subagents and hooks now served by the plugin bundle"
|
|
465
|
+
else
|
|
466
|
+
mkdir -p "$TARGET/commands" "$TARGET/agents"
|
|
467
|
+
|
|
468
|
+
# Slash commands — prefix with 'massa-ai-' to avoid collisions with user commands
|
|
469
|
+
for src in "$SCRIPT_DIR/commands/"*.md; do
|
|
470
|
+
name="$(basename "$src" .md)"
|
|
471
|
+
dest="$TARGET/commands/massa-ai-${name}.md"
|
|
472
|
+
cp "$src" "$dest"
|
|
473
|
+
vecho " + /massa-ai-${name}"
|
|
474
|
+
command_count=$((command_count + 1))
|
|
475
|
+
done
|
|
476
|
+
|
|
477
|
+
# Subagent specialists (generated from skills/agents/*/SKILL.md, navigator
|
|
478
|
+
# included). The massa-ai- name prefix is the ownership marker used by uninstall.
|
|
479
|
+
for src in "$SCRIPT_DIR/agents/"massa-ai-*.md; do
|
|
480
|
+
[[ -f "$src" ]] || continue
|
|
481
|
+
name="$(basename "$src")"
|
|
482
|
+
cp "$src" "$TARGET/agents/$name"
|
|
483
|
+
vecho " + $name"
|
|
484
|
+
specialist_count=$((specialist_count + 1))
|
|
485
|
+
done
|
|
486
|
+
vecho " + ${specialist_count} subagent specialists (generated from skills/agents/*/SKILL.md)"
|
|
487
|
+
fi
|
|
384
488
|
|
|
385
489
|
# Skills bundling (PDO-08, 09): install massa-ai/persona-router into the
|
|
386
490
|
# shared harness skills directory, unless scripts/install-skills.sh already
|
|
@@ -388,15 +492,19 @@ vecho " + ${specialist_count} subagent specialists (generated from skills/agent
|
|
|
388
492
|
vecho ""
|
|
389
493
|
install_bundled_skills
|
|
390
494
|
|
|
391
|
-
# Merge hooks into settings.json (array-append, backup, idempotent)
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if [[
|
|
395
|
-
|
|
396
|
-
|
|
495
|
+
# Merge hooks into settings.json (array-append, backup, idempotent).
|
|
496
|
+
# Skipped on the plugin route, where hooks/hooks.json inside the bundle is
|
|
497
|
+
# already wired by Claude Code itself.
|
|
498
|
+
if [[ "$PLUGIN_ROUTE" -eq 0 ]]; then
|
|
499
|
+
vecho ""
|
|
500
|
+
vecho "Merging hooks into $SETTINGS_JSON..."
|
|
501
|
+
if [[ ! -f "$HOOK_BIN" ]]; then
|
|
502
|
+
echo " ⚠ Warning: hook binary not found at $HOOK_BIN" >&2
|
|
503
|
+
echo " Hooks will not fire until the binary is available." >&2
|
|
504
|
+
fi
|
|
505
|
+
merge_settings_hooks "$SETTINGS_JSON" "install"
|
|
506
|
+
vecho " + 5 massa-ai hook events wired (array-append, user hooks preserved)"
|
|
397
507
|
fi
|
|
398
|
-
merge_settings_hooks "$SETTINGS_JSON" "install"
|
|
399
|
-
vecho " + 5 massa-ai hook events wired (array-append, user hooks preserved)"
|
|
400
508
|
|
|
401
509
|
# ── MCP registration (delegated) ─────────────────────────────────────────────
|
|
402
510
|
# scripts/install-agents.sh is the single writer of host MCP config. It merges
|
|
@@ -421,7 +529,13 @@ fi
|
|
|
421
529
|
|
|
422
530
|
# Summary line in quiet mode
|
|
423
531
|
if [ "${MASSA_AI_VERBOSE:-0}" != "1" ]; then
|
|
424
|
-
|
|
532
|
+
if [[ "$PLUGIN_ROUTE" -eq 1 ]]; then
|
|
533
|
+
ok "claude plugin registered (${command_count} commands, ${specialist_count} specialists, 5 hooks) — shows in /plugin"
|
|
534
|
+
else
|
|
535
|
+
ok "claude plugin installed (${command_count} commands, ${specialist_count} specialists, 5 hooks)"
|
|
536
|
+
warn "claude CLI unavailable — not registered in /plugin. Register it with:"
|
|
537
|
+
warn " claude plugin marketplace add \"$PLUGIN_SOURCE_ROOT\" && claude plugin install $PLUGIN_ID"
|
|
538
|
+
fi
|
|
425
539
|
else
|
|
426
540
|
vecho ""
|
|
427
541
|
vecho "Done. Restart Claude Code to pick up the new commands and hooks."
|