@massa-ai/claude-plugin 1.53.0 → 1.54.1

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.53.0",
3
+ "version": "1.54.1",
4
4
  "description": "massa-ai — semantic code search, durable memory, symbol graph, and context compression",
5
5
  "author": {
6
6
  "name": "Luiz Massa",
package/install.sh CHANGED
@@ -185,8 +185,54 @@ try {
185
185
  }
186
186
  }
187
187
 
188
+ // Ownership test. The `_massaAiOwned` marker is the precise signal, but it is
189
+ // NOT the only way our hook entries reach settings.json: this plugin also ships
190
+ // settings.json.template, a hand-merge guide whose blocks carry no marker and
191
+ // whose command the reader is told to rewrite to an absolute path. Entries that
192
+ // arrived that way were invisible to both directions of this merge — never
193
+ // deduped on install, never removed when the marketplace route took over — so
194
+ // they fired alongside the bundle's own hooks/hooks.json. Measured live
195
+ // 2026-08-17: 5 unmarked massa-ai blocks in ~/.claude/settings.json beside a
196
+ // registered marketplace plugin, and every event ingested twice.
197
+ //
198
+ // So recognise our entries by WHAT THEY ARE as well. Referencing the
199
+ // massa-ai-hook binary is unambiguous — it is this project's own file name, in
200
+ // every shape it is documented in (`bun run "<abs>/massa-ai-hook.ts" <sub>`,
201
+ // `bun run "${CLAUDE_PLUGIN_ROOT}/hooks/massa-ai-hook.ts" <sub>`). A user hook
202
+ // that merely mentions massa-ai elsewhere (`rtk hook claude`, an MCP tool name)
203
+ // does not match and is preserved.
204
+ const OWNED_COMMAND = /massa-ai-hook/;
205
+
206
+ function commandIsOwned(h) {
207
+ return Boolean(h) && typeof h.command === "string" && OWNED_COMMAND.test(h.command);
208
+ }
209
+
210
+ // A matcher-group block is ours when it is marked, or when every command it
211
+ // carries is ours. A block MIXING user and massa-ai commands is not owned
212
+ // wholesale — stripOwnedHooks below removes only our commands from it.
213
+ function blockIsOwned(b) {
214
+ if (!b || typeof b !== "object") return false;
215
+ if (b._massaAiOwned === true) return true;
216
+ return Array.isArray(b.hooks) && b.hooks.length > 0 && b.hooks.every(commandIsOwned);
217
+ }
218
+
188
219
  function hasOwned(arr) {
189
- return Array.isArray(arr) && arr.some((e) => e && e._massaAiOwned === true);
220
+ return Array.isArray(arr) && arr.some(blockIsOwned);
221
+ }
222
+
223
+ // Returns the event array with our entries removed: whole blocks that are ours,
224
+ // and our individual commands out of blocks we share with the user.
225
+ function stripOwnedHooks(arr) {
226
+ const kept = [];
227
+ for (const b of arr) {
228
+ if (blockIsOwned(b)) continue;
229
+ if (b && Array.isArray(b.hooks) && b.hooks.some(commandIsOwned)) {
230
+ kept.push({ ...b, hooks: b.hooks.filter((h) => !commandIsOwned(h)) });
231
+ continue;
232
+ }
233
+ kept.push(b);
234
+ }
235
+ return kept;
190
236
  }
191
237
 
192
238
  // Double-fire guard: true when massa-ai is already installed as a Claude Code
@@ -223,20 +269,54 @@ function pluginAlreadyInstalled() {
223
269
  if (mode === "uninstall") {
224
270
  const hooks = cfg.hooks;
225
271
  if (hooks && typeof hooks === "object" && !Array.isArray(hooks)) {
226
- for (const [evt] of EVENTS) {
272
+ // Back up before removing. The install path has always done this; removal
273
+ // did not, and now that it deletes unmarked entries it did not necessarily
274
+ // write, the asymmetry is not defensible.
275
+ if (existed) {
276
+ fs.copyFileSync(file, `${file}.massa-ai.bak-${ts}`);
277
+ }
278
+ // Every event, not just the 5 we write. The predicate identifies our
279
+ // commands rather than a location, so an entry parked under an event this
280
+ // release does not use is still ours, and a user's is still not.
281
+ for (const evt of Object.keys(hooks)) {
227
282
  if (Array.isArray(hooks[evt])) {
228
- hooks[evt] = hooks[evt].filter((e) => !(e && e._massaAiOwned === true));
283
+ hooks[evt] = stripOwnedHooks(hooks[evt]);
229
284
  if (hooks[evt].length === 0) delete hooks[evt];
230
285
  }
231
286
  }
232
287
  if (Object.keys(hooks).length === 0) delete cfg.hooks;
233
288
  }
234
289
  } else if (pluginAlreadyInstalled()) {
235
- console.error(
236
- " ↷ massa-ai is installed as a Claude Code plugin — skipping settings.json" +
237
- " hooks (the plugin already provides them; wiring both would double-fire)",
238
- );
239
- process.exit(0);
290
+ // Not writing is only half the guard. If settings.json ALREADY carries our
291
+ // entries — from an older release, or from a hand-merge of
292
+ // settings.json.template — declining to add more still leaves both sources
293
+ // live. This branch is reached with the plugin registered but no usable
294
+ // `claude` CLI, so the marketplace route (which removes them) never runs.
295
+ // Strip them here and fall through to the write.
296
+ const hooks = cfg.hooks;
297
+ let removed = 0;
298
+ if (hooks && typeof hooks === "object" && !Array.isArray(hooks)) {
299
+ for (const evt of Object.keys(hooks)) {
300
+ if (!Array.isArray(hooks[evt])) continue;
301
+ const before = JSON.stringify(hooks[evt]);
302
+ hooks[evt] = stripOwnedHooks(hooks[evt]);
303
+ if (JSON.stringify(hooks[evt]) !== before) removed++;
304
+ if (hooks[evt].length === 0) delete hooks[evt];
305
+ }
306
+ if (Object.keys(hooks).length === 0) delete cfg.hooks;
307
+ }
308
+ if (removed > 0) {
309
+ if (existed) fs.copyFileSync(file, `${file}.massa-ai.bak-${ts}`);
310
+ console.error(
311
+ ` ↷ massa-ai is installed as a Claude Code plugin — removed ${removed} stale` +
312
+ " settings.json hook event(s) that would have fired alongside the plugin's own",
313
+ );
314
+ } else {
315
+ console.error(
316
+ " ↷ massa-ai is installed as a Claude Code plugin — skipping settings.json" +
317
+ " hooks (the plugin already provides them; wiring both would double-fire)",
318
+ );
319
+ }
240
320
  } else {
241
321
  // install: backup before first write if file existed
242
322
  if (existed) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massa-ai/claude-plugin",
3
- "version": "1.53.0",
3
+ "version": "1.54.1",
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",