@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "massa-ai",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "massa-ai — semantic code search, durable memory, symbol graph, and context compression",
5
5
  "author": {
6
6
  "name": "Luiz Massa",
@@ -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 = process.env.MASSA_AI_API_KEY;
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
- # Count for summary
363
- command_count=0
364
- # Slash commands — prefix with 'massa-ai-' to avoid collisions with user commands
365
- for src in "$SCRIPT_DIR/commands/"*.md; do
366
- name="$(basename "$src" .md)"
367
- dest="$TARGET/commands/massa-ai-${name}.md"
368
- cp "$src" "$dest"
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
- # Subagent specialists (generated from skills/agents/*/SKILL.md, navigator
374
- # included). The massa-ai- name prefix is the ownership marker used by uninstall.
450
+ command_count=0
375
451
  specialist_count=0
376
- for src in "$SCRIPT_DIR/agents/"massa-ai-*.md; do
377
- [[ -f "$src" ]] || continue
378
- name="$(basename "$src")"
379
- cp "$src" "$TARGET/agents/$name"
380
- vecho " + $name"
381
- specialist_count=$((specialist_count + 1))
382
- done
383
- vecho " + ${specialist_count} subagent specialists (generated from skills/agents/*/SKILL.md)"
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
- vecho ""
393
- vecho "Merging hooks into $SETTINGS_JSON..."
394
- if [[ ! -f "$HOOK_BIN" ]]; then
395
- echo " ⚠ Warning: hook binary not found at $HOOK_BIN" >&2
396
- echo " Hooks will not fire until the binary is available." >&2
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
- ok "claude plugin installed (${command_count} commands, ${specialist_count} specialists, 5 hooks)"
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."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massa-ai/claude-plugin",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "massa-ai plugin for Claude Code — semantic code search, durable memory, symbol graph, and context compression",
5
5
  "files": [
6
6
  "agents",