@massa-ai/codex-plugin 1.7.1 → 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.1",
3
+ "version": "1.8.0",
4
4
  "description": "massa-ai — semantic code search, memory, and context compression for Codex",
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
@@ -10,13 +10,18 @@
10
10
  # MCP registration is delegated to scripts/install-agents.sh, the single writer
11
11
  # of host MCP config. This installer no longer ships a plugin-local .mcp.json.
12
12
  #
13
- # Complementary to the marketplace route, NOT a duplicate of it. Installing via
14
- # `codex plugin add massa-ai@massa-ai` gives Codex the skills and the /plugins
15
- # entry; it CANNOT give it hooks, because a Codex plugin manifest has no hooks
16
- # key (none of the 203 manifests shipped by the bundled/curated/runtime
17
- # marketplaces declares one). Hooks reach Codex only through ~/.codex/hooks.json,
18
- # which this script writes. So there is no double-fire to guard against here —
19
- # unlike apps/claude-plugin/install.sh, whose plugin does ship hooks/hooks.json.
13
+ # This script now drives BOTH halves. `codex plugin marketplace add` +
14
+ # `codex plugin add massa-ai@massa-ai` supply the skills and the /plugins entry;
15
+ # they CANNOT supply hooks, because a Codex plugin manifest has no hooks key
16
+ # (none of the 203 manifests shipped by the bundled/curated/runtime marketplaces
17
+ # declares one). Hooks reach Codex only through ~/.codex/hooks.json, which this
18
+ # script writes. The two halves are therefore additive, not exclusive — unlike
19
+ # apps/claude-plugin/install.sh, whose plugin does ship hooks/hooks.json and so
20
+ # has to pick one route or double-fire.
21
+ #
22
+ # Skills are likewise not duplicated: the marketplace route populates Codex's
23
+ # own plugin skill cache, while the harness skills go to $CODEX_DIR/skills under
24
+ # the skillsOwner coordination described further down. Separate mechanisms.
20
25
  #
21
26
  # Idempotent: re-running is a no-op when owned entries already present.
22
27
  # Uninstall removes only ownership-marked entries + the plugin directory.
@@ -33,6 +38,11 @@ set -euo pipefail
33
38
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
34
39
  REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
35
40
  CLAUDE_PLUGIN_BIN="$REPO_ROOT/apps/claude-plugin/hooks/massa-ai-hook.ts"
41
+ # shellcheck source=scripts/lib/installer-shared.sh
42
+ source "$REPO_ROOT/scripts/lib/installer-shared.sh"
43
+ # Marketplace root to register; install-harness.sh resolves it once per run
44
+ # (--plugin-source local|copy|auto). Direct invocation uses this checkout.
45
+ PLUGIN_SOURCE_ROOT="${MASSA_AI_PLUGIN_SOURCE_ROOT:-$REPO_ROOT}"
36
46
 
37
47
  SCOPE="user"
38
48
  UNINSTALL=0
@@ -335,9 +345,59 @@ try {
335
345
  NODE
336
346
  }
337
347
 
348
+ # ── Plugin-registry registration (delegated to the codex CLI) ───────────────
349
+ # The CLI owns config.toml's [marketplaces.*] / [plugins."x@y"] tables and the
350
+ # plugins/cache layout, so it is the only supported way to reach /plugins.
351
+ # Failure is never fatal: without this the rest of the install (hooks, agents,
352
+ # MCP) is still fully functional, it just does not show in the plugins screen.
353
+ CODEX_MARKETPLACE="massa-ai"
354
+ CODEX_PLUGIN_ID="massa-ai@massa-ai"
355
+
356
+ # Where the CLI is allowed to write. Pinned explicitly, and not left to the
357
+ # CLI's own resolution, for two reasons:
358
+ #
359
+ # 1. The codex CLI resolves its config root from $CODEX_HOME before falling
360
+ # back to $HOME/.codex — and the Codex app exports CODEX_HOME into every
361
+ # shell it spawns. This script resolves CODEX_DIR from $HOME alone, so an
362
+ # install running against an overridden $HOME (tests, --target) would write
363
+ # its files into the sandbox while registering the marketplace into the
364
+ # developer's real ~/.codex. That is not hypothetical: it silently removed
365
+ # a live [marketplaces.massa-ai] table during development of this change.
366
+ # 2. Codex records plugin installs at user scope, so this stays $HOME-derived
367
+ # even for --project, mirroring PLUGIN_REGISTRY in apps/claude-plugin.
368
+ #
369
+ # Note the pre-existing divergence left untouched here: the file-copy half of
370
+ # this installer still ignores CODEX_HOME entirely. Making both halves honour
371
+ # it is a separate change with a much wider blast radius.
372
+ CODEX_CLI_HOME="$HOME/.codex"
373
+
374
+ register_codex_plugin() {
375
+ # Explicit opt-out; see the same knob in apps/claude-plugin/install.sh.
376
+ [[ "${MASSA_AI_SKIP_PLUGIN_REGISTRY:-0}" == "1" ]] && return 1
377
+ installer_host_cli_supports codex plugin marketplace || return 1
378
+ if [[ ! -f "$PLUGIN_SOURCE_ROOT/.agents/plugins/marketplace.json" ]]; then
379
+ vecho " – no marketplace manifest under $PLUGIN_SOURCE_ROOT — skipping /plugins registration"
380
+ return 1
381
+ fi
382
+ # Idempotent: a second run reports "already added" and leaves config.toml
383
+ # byte-identical.
384
+ CODEX_HOME="$CODEX_CLI_HOME" codex plugin marketplace add "$PLUGIN_SOURCE_ROOT" </dev/null >/dev/null 2>&1 || return 1
385
+ CODEX_HOME="$CODEX_CLI_HOME" codex plugin add "$CODEX_PLUGIN_ID" </dev/null >/dev/null 2>&1 || return 1
386
+ }
387
+
388
+ unregister_codex_plugin() {
389
+ installer_host_cli_supports codex plugin marketplace || return 0
390
+ CODEX_HOME="$CODEX_CLI_HOME" codex plugin remove "$CODEX_PLUGIN_ID" </dev/null >/dev/null 2>&1 || true
391
+ # Order matters: the marketplace entry must go last, because removing it
392
+ # first leaves `codex plugin list` unable to resolve the plugin it still has
393
+ # registered — the same cache-miss failure a deleted checkout produces.
394
+ CODEX_HOME="$CODEX_CLI_HOME" codex plugin marketplace remove "$CODEX_MARKETPLACE" </dev/null >/dev/null 2>&1 || true
395
+ }
396
+
338
397
  # ── Uninstall ───────────────────────────────────────────────────────────────
339
398
  if [[ "$UNINSTALL" -eq 1 ]]; then
340
399
  echo "Uninstalling massa-ai Codex plugin (scope: $SCOPE)..."
400
+ unregister_codex_plugin
341
401
  uninstall_bundled_skills
342
402
  # Remove owned hook entries (preserves user hooks)
343
403
  if [[ -f "$HOOKS_JSON" ]]; then
@@ -463,9 +523,24 @@ else
463
523
  echo " ⚠ scripts/install-agents.sh not found — register MCP with: bash scripts/install-agents.sh --agent codex --yes" >&2
464
524
  fi
465
525
 
526
+ # ── Plugin-registry registration ─────────────────────────────────────────────
527
+ # Runs after the file writes above because it is additive here: hooks and agent
528
+ # TOMLs stay owned by this script either way.
529
+ CODEX_PLUGIN_ROUTE=0
530
+ if register_codex_plugin; then
531
+ CODEX_PLUGIN_ROUTE=1
532
+ vecho " + registered ${CODEX_PLUGIN_ID} (marketplace root: ${PLUGIN_SOURCE_ROOT})"
533
+ fi
534
+
466
535
  # Summary line in quiet mode
467
536
  if [ "${MASSA_AI_VERBOSE:-0}" != "1" ]; then
468
- ok "codex plugin installed (${skill_count} skills, ${specialist_count} specialists, 6 hooks)"
537
+ if [[ "$CODEX_PLUGIN_ROUTE" -eq 1 ]]; then
538
+ ok "codex plugin installed (${skill_count} skills, ${specialist_count} specialists, 6 hooks) — shows in /plugins"
539
+ else
540
+ ok "codex plugin installed (${skill_count} skills, ${specialist_count} specialists, 6 hooks)"
541
+ warn "codex CLI unavailable — not registered in /plugins. Register it with:"
542
+ warn " codex plugin marketplace add \"$PLUGIN_SOURCE_ROOT\" && codex plugin add $CODEX_PLUGIN_ID"
543
+ fi
469
544
  else
470
545
  vecho ""
471
546
  vecho "Done. Restart Codex to pick up the plugin."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massa-ai/codex-plugin",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "description": "massa-ai plugin for Codex — semantic code search, memory, and context compression",
5
5
  "files": [
6
6
  "agents",