@codyswann/lisa 2.124.1 → 2.124.2

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.
Files changed (72) hide show
  1. package/dist/agy/mcp-installer.d.ts +22 -5
  2. package/dist/agy/mcp-installer.d.ts.map +1 -1
  3. package/dist/agy/mcp-installer.js +52 -12
  4. package/dist/agy/mcp-installer.js.map +1 -1
  5. package/dist/codex/hooks-installer.d.ts.map +1 -1
  6. package/dist/codex/hooks-installer.js +0 -7
  7. package/dist/codex/hooks-installer.js.map +1 -1
  8. package/dist/core/lisa.d.ts +19 -11
  9. package/dist/core/lisa.d.ts.map +1 -1
  10. package/dist/core/lisa.js +44 -12
  11. package/dist/core/lisa.js.map +1 -1
  12. package/package.json +1 -1
  13. package/plugins/lisa/.claude-plugin/plugin.json +1 -10
  14. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  15. package/plugins/lisa/hooks/block-no-verify.agy.sh +45 -0
  16. package/plugins/lisa/hooks/hooks.json +0 -11
  17. package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +45 -0
  18. package/plugins/lisa-agy/hooks.json +15 -0
  19. package/plugins/lisa-agy/plugin.json +1 -1
  20. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  23. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -12
  26. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -12
  27. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo-agy/plugin.json +1 -1
  30. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  40. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  45. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails-agy/plugin.json +1 -1
  50. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  55. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  60. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/src/base/.claude-plugin/plugin.json +0 -1
  63. package/plugins/src/base/hooks/block-no-verify.agy.sh +45 -0
  64. package/scripts/generate-agy-plugin-artifacts.mjs +158 -19
  65. package/scripts/generate-copilot-plugin-artifacts.mjs +1 -1
  66. package/scripts/lib/per-agent-hook-filter.mjs +29 -18
  67. package/dist/codex/scripts/notify-ntfy.sh +0 -18
  68. package/plugins/lisa/hooks/notify-ntfy.sh +0 -183
  69. package/plugins/lisa-copilot/hooks/notify-ntfy.sh +0 -183
  70. package/plugins/lisa-cursor/hooks/notify-ntfy.sh +0 -183
  71. package/plugins/lisa-expo-agy/.mcp.json +0 -8
  72. package/plugins/src/base/hooks/notify-ntfy.sh +0 -183
@@ -4,12 +4,33 @@
4
4
  * Claude artifact.
5
5
  *
6
6
  * agy's plugin manifest is a bare `plugin.json` at the plugin root (NOT
7
- * `.claude-plugin/plugin.json`). The Wave 1 audit also established that agy
8
- * plugin-bundled hooks DO NOT FIRE in `-p` headless mode, so the agy variant
9
- * ships no hooks at all — the manifest's `hooks` field is dropped and the
10
- * `hooks/` directory is omitted. Rules-injection for agy uses the AGENTS.md
11
- * bake-in alternative implemented in `src/agy/rules-bake.ts` (per the parity
12
- * research artifact's Cluster 4-agy / Option α).
7
+ * `.claude-plugin/plugin.json`). This generator copies + reshapes the artifact
8
+ * AND emits a plugin-bundled hooks config.
9
+ *
10
+ * HOOKS (plugin-bundled, ROOT-level): a runtime probe of agy 1.0.3 (ticket-1054)
11
+ * proved agy loads a plugin's hooks ONLY from a `hooks.json` at the plugin ROOT
12
+ * of an installed global plugin (`~/.gemini/config/plugins/<variant>/hooks.json`)
13
+ * — a `hooks/` SUBDIR hooks.json (the earlier attempt) is NOT scanned. Lisa
14
+ * already `agy plugin install`s these variants there, so this generator emits a
15
+ * root `hooks.json` in agy's schema (top-level HOOK NAME → event → handlers),
16
+ * matcher `run_command` (agy's shell tool), and ships the agy-protocol script
17
+ * into the variant's `hooks/` subdir (scripts in a subdir are fine — only
18
+ * hooks.json must be at root; the command points at the absolute installed path
19
+ * via `$HOME`). Only events agy supports map: PreToolUse / PostToolUse /
20
+ * PreInvocation / PostInvocation / Stop. SessionStart is NOT supported, so
21
+ * install-pkgs / setup-jira-cli CANNOT ship as agy hooks — only block-no-verify
22
+ * (PreToolUse) maps. Only the BASE plugin manifest carries the universal hooks,
23
+ * so only `lisa-agy` gets a hooks.json; stack variants emit none.
24
+ *
25
+ * MCP (user-global, NOT plugin-bundled): agy ignores plugin-bundled MCP and only
26
+ * reads the user-global `~/.gemini/config/mcp_config.json`, so MCP is delivered
27
+ * by the runtime installer (`src/agy/mcp-installer.ts`), and this generator
28
+ * drops `.mcp.json`. Rules use the AGENTS.md bake (rules-once invariant).
29
+ *
30
+ * Net: the agy variant ships a root `hooks.json` (base only) + its agy-protocol
31
+ * script under `hooks/`, but NO `mcp_config.json`, NO `.mcp.json`, NO `rules/`,
32
+ * and NO `hooks/hooks.json` subdir. The manifest carries neither `hooks` nor
33
+ * `mcpServers`.
13
34
  *
14
35
  * Usage: node scripts/generate-agy-plugin-artifacts.mjs <source-plugin-dir> <out-dir> <version>
15
36
  *
@@ -74,12 +95,16 @@ function copyDir(src, dst, keep = () => true) {
74
95
  /**
75
96
  * Generate the agy variant.
76
97
  *
77
- * Transformation steps (from Wave 2 pattern-b-fan-out-spec.md):
98
+ * Transformation steps:
78
99
  * 0. Filter skills/ against scripts/internal-agy-skill-policy.json.
79
- * 1. Copy source to outDir minus filtered skills, .codex-plugin/, hooks/, and rules/.
100
+ * 1. Copy source to outDir minus filtered skills, .codex-plugin/, hooks/,
101
+ * rules/, and the untranslated .mcp.json.
80
102
  * 2. Move .claude-plugin/plugin.json to bare plugin.json at root; drop .claude-plugin/.
81
- * 3. Drop the hooks field from the manifest (agy plugin hooks don't fire in -p).
103
+ * 3. Drop the hooks + mcpServers fields from the manifest (delivered by the
104
+ * root hooks.json / runtime MCP installer, not as manifest components).
82
105
  * 4. Inject the version.
106
+ * 5. Emit the plugin-bundled root hooks.json (base variant only) + copy the
107
+ * agy-protocol script(s) into the variant's hooks/ subdir.
83
108
  *
84
109
  * @param {string} srcDir Built Claude plugin directory (input).
85
110
  * @param {string} outDir agy variant output directory.
@@ -101,8 +126,14 @@ export function generateAgyVariant(srcDir, outDir, version) {
101
126
  if (relPath.startsWith(".codex-plugin/") || relPath === ".codex-plugin") {
102
127
  return false;
103
128
  }
104
- if (relPath.startsWith("hooks/") || relPath === "hooks") return false; // hooks don't fire on agy
105
- if (relPath.startsWith("rules/") || relPath === "rules") return false; // rules not a plugin component on agy
129
+ // Drop the source hooks/ (Claude scripts + stale codex hooks.json). The agy
130
+ // hooks.json (root) + the agy-protocol script are re-emitted by
131
+ // emitAgyPluginHooks below.
132
+ if (relPath.startsWith("hooks/") || relPath === "hooks") return false;
133
+ if (relPath.startsWith("rules/") || relPath === "rules") return false; // rules delivered via AGENTS.md bake
134
+ // Drop the untranslated Claude .mcp.json — agy ignores it (and the agy
135
+ // MCP shape differs); MCP is delivered by the user-global runtime MCP installer.
136
+ if (relPath === ".mcp.json") return false;
106
137
  // Drop Codex-specific per-skill openai.yaml artifacts.
107
138
  if (/^skills\/[^/]+\/agents\/openai\.ya?ml$/.test(relPath)) return false;
108
139
  // Apply skill denylist.
@@ -132,26 +163,134 @@ export function generateAgyVariant(srcDir, outDir, version) {
132
163
  }
133
164
  }
134
165
 
135
- // 2. Read the Claude manifest, drop hooks, rename to bare plugin.json.
166
+ // 2. Read the Claude manifest, drop hooks + mcpServers, write bare plugin.json.
167
+ // agy reads hooks from a root hooks.json (emitted below), not the manifest;
168
+ // MCP is user-global. So the bare manifest carries neither field.
136
169
  const manifest = JSON.parse(fs.readFileSync(claudeManifest, "utf8"));
137
170
  manifest.version = version;
171
+ const sourceHooks = manifest.hooks ?? {};
138
172
  delete manifest.hooks;
139
-
140
- // 3. Drop any pointer fields that agy doesn't understand.
141
- // agy reads bare plugin.json with components: skills, agents, commands,
142
- // mcpServers, hooks. We omit hooks above. MCP is not a plugin component on
143
- // agy, so drop any `mcpServers` if present (Lisa's base today does not
144
- // emit one, but be defensive).
145
173
  delete manifest.mcpServers;
146
174
 
147
175
  const bareManifestPath = path.join(outDir, "plugin.json");
148
176
  fs.writeFileSync(bareManifestPath, JSON.stringify(manifest, null, 2) + "\n");
149
177
 
150
- // 4. Ensure no .claude-plugin/ directory survives.
178
+ // 3. Ensure no .claude-plugin/ directory survives.
151
179
  const ghostDir = path.join(outDir, ".claude-plugin");
152
180
  if (fs.existsSync(ghostDir)) {
153
181
  fs.rmSync(ghostDir, { recursive: true, force: true });
154
182
  }
183
+
184
+ // 4. Emit the plugin-bundled root hooks.json + agy-protocol script (base only).
185
+ // `agy plugin install` names the install dir by the manifest `name`
186
+ // (`~/.gemini/config/plugins/<name>/`, verified-by-run per
187
+ // reference_agy_plugin_capabilities), NOT the source dir basename — so the
188
+ // hook command path must use manifest.name (e.g. "lisa"), falling back to the
189
+ // dir basename only if a manifest somehow omits name.
190
+ const installDirName = manifest.name ?? path.basename(outDir);
191
+ emitAgyPluginHooks(srcDir, outDir, sourceHooks, installDirName);
192
+ }
193
+
194
+ /**
195
+ * agy-portable hook map. Only events agy supports + scripts whose protocol has
196
+ * an agy variant. Each entry emits one top-level hook-name key in the root
197
+ * hooks.json. `sourceScript` is what the BASE Claude manifest references (used
198
+ * to detect whether this variant should carry the hook); `agyScript` is the
199
+ * agy-protocol script copied into the variant's hooks/ and referenced by the
200
+ * command. NOTE: install-pkgs / setup-jira-cli are SessionStart-only, which agy
201
+ * hooks don't support, so they are intentionally absent. inject-rules is absent
202
+ * too (rules-once via the AGENTS.md bake).
203
+ */
204
+ const AGY_PLUGIN_HOOKS = [
205
+ {
206
+ sourceScript: "block-no-verify.sh",
207
+ hookName: "lisa-block-no-verify",
208
+ event: "PreToolUse",
209
+ matcher: "run_command",
210
+ agyScript: "block-no-verify.agy.sh",
211
+ },
212
+ ];
213
+
214
+ /**
215
+ * Whether the source manifest hook block references `scriptName` anywhere. Used
216
+ * to ship a hook only for the variant whose manifest carries it (the base
217
+ * plugin); stack variants have empty manifest hooks and emit no hooks.json.
218
+ * @param {Record<string, Array<{ hooks?: Array<{ command?: string }> }>>} sourceHooks
219
+ * @param {string} scriptName
220
+ * @returns {boolean}
221
+ */
222
+ function sourceReferencesScript(sourceHooks, scriptName) {
223
+ return Object.values(sourceHooks ?? {}).some(
224
+ entries =>
225
+ Array.isArray(entries) &&
226
+ entries.some(
227
+ e =>
228
+ Array.isArray(e?.hooks) &&
229
+ e.hooks.some(
230
+ h =>
231
+ typeof h?.command === "string" && h.command.includes(scriptName)
232
+ )
233
+ )
234
+ );
235
+ }
236
+
237
+ /**
238
+ * Emit the plugin-bundled root `hooks.json` (agy schema) and copy the
239
+ * agy-protocol script(s) into the variant's `hooks/` subdir. No-op for variants
240
+ * whose source manifest carries none of the mapped hooks (e.g. stack variants).
241
+ * @param {string} srcDir Built Claude plugin directory (input).
242
+ * @param {string} outDir agy variant output directory.
243
+ * @param {Record<string, unknown>} sourceHooks Source manifest hook block.
244
+ * @param {string} installDirName Name agy installs the plugin under in
245
+ * `~/.gemini/config/plugins/<installDirName>/` (the manifest `name`); baked
246
+ * into the hook command path so it resolves to the installed script.
247
+ * @returns {void}
248
+ */
249
+ function emitAgyPluginHooks(srcDir, outDir, sourceHooks, installDirName) {
250
+ const applicable = AGY_PLUGIN_HOOKS.filter(h => {
251
+ if (!sourceReferencesScript(sourceHooks, h.sourceScript)) return false;
252
+ const scriptSource = path.join(srcDir, "hooks", h.agyScript);
253
+ if (!fs.existsSync(scriptSource)) {
254
+ throw new Error(
255
+ `Missing agy hook script for ${h.sourceScript}: ${scriptSource}`
256
+ );
257
+ }
258
+ return true;
259
+ });
260
+ if (applicable.length === 0) return;
261
+
262
+ const hooksConfig = Object.fromEntries(
263
+ applicable.map(h => [
264
+ h.hookName,
265
+ {
266
+ [h.event]: [
267
+ {
268
+ matcher: h.matcher,
269
+ hooks: [
270
+ {
271
+ type: "command",
272
+ command: `bash "$HOME/.gemini/config/plugins/${installDirName}/hooks/${h.agyScript}"`,
273
+ },
274
+ ],
275
+ },
276
+ ],
277
+ },
278
+ ])
279
+ );
280
+ fs.writeFileSync(
281
+ path.join(outDir, "hooks.json"),
282
+ JSON.stringify(hooksConfig, null, 2) + "\n"
283
+ );
284
+
285
+ // Copy the agy-protocol scripts into the variant's hooks/ subdir.
286
+ const hooksDir = path.join(outDir, "hooks");
287
+ fs.mkdirSync(hooksDir, { recursive: true });
288
+ for (const h of applicable) {
289
+ const scriptSource = path.join(srcDir, "hooks", h.agyScript);
290
+ const scriptDest = path.join(hooksDir, h.agyScript);
291
+ fs.copyFileSync(scriptSource, scriptDest);
292
+ fs.chmodSync(scriptDest, 0o755);
293
+ }
155
294
  }
156
295
 
157
296
  // CLI entrypoint.
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * Per the Wave 1 audit, Copilot ships:
14
14
  * block-no-verify.sh, inject-rules.sh (conservative default — conditional on
15
- * the rules-auto-load probe), notify-ntfy.sh, install-pkgs.sh, setup-jira-cli.sh.
15
+ * the rules-auto-load probe), install-pkgs.sh, setup-jira-cli.sh.
16
16
  *
17
17
  * Per the Wave 2 pattern-b-fan-out-spec.md, this generator runs four pre-flight
18
18
  * probes when `copilot` is on PATH and caches the results. When `copilot` is
@@ -15,7 +15,15 @@
15
15
  * generate-codex-plugin-artifacts.mjs path)
16
16
  * - Cursor: strip inject-rules.sh (Cursor auto-loads rules/ natively), strip
17
17
  * Claude-team-specific scripts and `entire hooks claude-code *` calls
18
- * - agy: ship NOTHING (agy plugin hooks do not fire in -p mode per verified-by-run)
18
+ * - agy: this filter is NOT consumed for agy. agy hooks ship as a
19
+ * plugin-bundled ROOT hooks.json emitted by
20
+ * generate-agy-plugin-artifacts.mjs (its own AGY_PLUGIN_HOOKS map is the
21
+ * source of truth), and only `block-no-verify` (PreToolUse) is portable —
22
+ * agy doesn't support SessionStart, so install-pkgs / setup-jira-cli can't
23
+ * ship as agy hooks. The agy column below is retained only as conceptual
24
+ * ship-list documentation (block-no-verify.sh, install-pkgs.sh,
25
+ * setup-jira-cli.sh; strips inject-rules.sh — rules-once via AGENTS.md bake
26
+ * — enforce-team-first.sh, inject-flow-context.sh, and `entire ...` calls).
19
27
  * - Copilot: strip SubagentStart hooks (event missing), strip Claude-team-specific
20
28
  * scripts, conditionally strip inject-rules.sh if the rules-auto-load probe is
21
29
  * positive (caller passes copilotRulesAutoLoads via options)
@@ -29,7 +37,7 @@ const SCRIPT_RULES = {
29
37
  claude: true,
30
38
  codex: true,
31
39
  cursor: true,
32
- agy: false,
40
+ agy: true,
33
41
  copilot: true,
34
42
  },
35
43
  "enforce-team-first.sh": {
@@ -43,35 +51,28 @@ const SCRIPT_RULES = {
43
51
  claude: true,
44
52
  codex: true,
45
53
  cursor: false, // collision: Cursor auto-loads rules/ natively
46
- agy: false, // hooks don't fire in -p
54
+ agy: false, // rules delivered via AGENTS.md bake, not a hook (rules-once invariant)
47
55
  copilot: true, // conservative default; conditionally stripped if rules-auto-load probe positive
48
56
  },
49
57
  "inject-flow-context.sh": {
50
58
  claude: true,
51
59
  codex: true,
52
60
  cursor: false, // SubagentStart unverified on Cursor; conservative default
53
- agy: false,
61
+ agy: false, // SubagentStart-only; not in agy's universal ship-list
54
62
  copilot: false, // Copilot lacks SubagentStart event
55
63
  },
56
64
  "install-pkgs.sh": {
57
65
  claude: true,
58
66
  codex: true,
59
67
  cursor: true,
60
- agy: false,
61
- copilot: true,
62
- },
63
- "notify-ntfy.sh": {
64
- claude: true,
65
- codex: true,
66
- cursor: true,
67
- agy: false,
68
+ agy: true,
68
69
  copilot: true,
69
70
  },
70
71
  "setup-jira-cli.sh": {
71
72
  claude: true,
72
73
  codex: true,
73
74
  cursor: true,
74
- agy: false,
75
+ agy: true,
75
76
  copilot: true,
76
77
  },
77
78
  // Unregistered scripts — exclude by default until classified.
@@ -92,7 +93,11 @@ const SCRIPT_RULES = {
92
93
  };
93
94
 
94
95
  /** Universal exclude pattern: development helpers. */
95
- const SCRIPT_EXCLUDE_PATTERNS = [/debug/i];
96
+ // `.agy.sh` scripts are agy-protocol variants emitted into the agy plugin
97
+ // artifact by generate-agy-plugin-artifacts.mjs (not via this filter); exclude
98
+ // them from every other agent's ship-list so they never leak into cursor /
99
+ // copilot / codex variants.
100
+ const SCRIPT_EXCLUDE_PATTERNS = [/debug/i, /\.agy\.sh$/];
96
101
 
97
102
  /** Hook command shape: { type: "command", command: "..." } */
98
103
  const isEntireClaudeCodeCommand = cmd =>
@@ -178,6 +183,10 @@ export function shouldShipHook(hook, _eventName, agent, opts = {}) {
178
183
  // Cursor collision rule for rules + Copilot conditional rules strip
179
184
  if (scriptName === "inject-rules.sh") {
180
185
  if (agent === "cursor") return false;
186
+ // Belt-and-suspenders rules-once guard: agy gets rules via the AGENTS.md
187
+ // bake, not a hook (rules-once invariant). The SCRIPT_RULES table already
188
+ // sets agy:false, but keep this explicit so the invariant survives a
189
+ // future table edit.
181
190
  if (agent === "agy") return false;
182
191
  if (agent === "copilot" && opts.copilotRulesAutoLoads === true)
183
192
  return false;
@@ -195,8 +204,12 @@ export function shouldShipHook(hook, _eventName, agent, opts = {}) {
195
204
  * Returns the new hook block (or undefined when the block ends up empty after
196
205
  * filtering, which means the manifest should omit the hooks field entirely).
197
206
  *
198
- * For agy this function returns undefined regardless of input because agy
199
- * variants ship no hooks.
207
+ * This function is invoked only for cursor/copilot. The "agy" branch still
208
+ * works (3 universal scripts survive, PascalCase events) and is exercised by
209
+ * unit tests as conceptual ship-list documentation, but agy hooks are NOT
210
+ * emitted through this path — they ship as a plugin-bundled root hooks.json
211
+ * built by generate-agy-plugin-artifacts.mjs (only block-no-verify is portable;
212
+ * agy lacks SessionStart).
200
213
  *
201
214
  * @param {Record<string, Array<{ matcher?: string, hooks: Array<object> }>>} hookBlock
202
215
  * The Claude-format hook block from .claude-plugin/plugin.json.
@@ -205,7 +218,6 @@ export function shouldShipHook(hook, _eventName, agent, opts = {}) {
205
218
  * @returns {Record<string, Array<{ matcher?: string, hooks: Array<object> }>> | undefined}
206
219
  */
207
220
  export function filterHooksForAgent(hookBlock, agent, opts = {}) {
208
- if (agent === "agy") return undefined;
209
221
  if (!hookBlock || typeof hookBlock !== "object") return undefined;
210
222
 
211
223
  /** @type {Record<string, Array<{ matcher?: string, hooks: Array<object> }>>} */
@@ -244,6 +256,5 @@ export function filterHooksForAgent(hookBlock, agent, opts = {}) {
244
256
  * @returns {string[]}
245
257
  */
246
258
  export function filterScriptsForAgent(scriptFilenames, agent) {
247
- if (agent === "agy") return [];
248
259
  return scriptFilenames.filter(name => shouldShipScript(name, agent));
249
260
  }
@@ -1,18 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Lisa-managed Codex hook script (Stop event).
3
- # Sends a desktop/push notification when a Codex session completes.
4
- # No-op if NTFY_TOPIC is not set or curl is unavailable.
5
- set -euo pipefail
6
-
7
- [ -n "${NTFY_TOPIC:-}" ] || exit 0
8
- command -v curl >/dev/null 2>&1 || exit 0
9
-
10
- TITLE="${NTFY_TITLE:-Codex session complete}"
11
- MESSAGE="${NTFY_MESSAGE:-Your Codex session has finished. Check the terminal for results.}"
12
-
13
- curl -s -m 5 \
14
- -H "Title: ${TITLE}" \
15
- -H "Priority: default" \
16
- -d "${MESSAGE}" \
17
- "https://ntfy.sh/${NTFY_TOPIC}" \
18
- >/dev/null 2>&1 || true
@@ -1,183 +0,0 @@
1
- #!/bin/bash
2
- # This file is managed by Lisa.
3
- # Do not edit directly — changes will be overwritten on the next `lisa` run.
4
- # =============================================================================
5
- # ntfy.sh Notification Hook for Claude Code
6
- # =============================================================================
7
- # Sends desktop and mobile notifications via ntfy.sh when Claude needs
8
- # attention or finishes a task.
9
- #
10
- # Setup:
11
- # 1. Install ntfy app on mobile (iOS App Store / Android Play Store)
12
- # 2. Subscribe to your unique topic in the app
13
- # 3. Set NTFY_TOPIC environment variable (e.g., in ~/.bashrc or ~/.zshrc):
14
- # export NTFY_TOPIC="my-claude-alerts-xyz123"
15
- #
16
- # @see https://ntfy.sh
17
- # =============================================================================
18
-
19
- # Read JSON input from stdin
20
- INPUT=$(cat)
21
-
22
- # Extract hook event name
23
- HOOK_EVENT=$(echo "$INPUT" | grep -o '"hook_event_name"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: *"//' | sed 's/"$//')
24
-
25
- # Extract notification type (for Notification hooks)
26
- NOTIFICATION_TYPE=$(echo "$INPUT" | grep -o '"notification_type"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: *"//' | sed 's/"$//')
27
-
28
- # Extract message if available
29
- MESSAGE=$(echo "$INPUT" | grep -o '"message"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: *"//' | sed 's/"$//')
30
-
31
- # Extract session ID (first 8 chars for brevity)
32
- FULL_SESSION_ID=$(echo "$INPUT" | grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: *"//' | sed 's/"$//')
33
- SESSION_ID="${FULL_SESSION_ID:0:8}"
34
-
35
- # Extract transcript path for task summary
36
- TRANSCRIPT_PATH=$(echo "$INPUT" | grep -o '"transcript_path"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: *"//' | sed 's/"$//')
37
-
38
- # Determine source (Web vs Local)
39
- if [ "$CLAUDE_CODE_REMOTE" = "true" ]; then
40
- SOURCE="Web"
41
- else
42
- SOURCE="Local"
43
- fi
44
-
45
- # Get project name from current directory
46
- PROJECT_NAME=$(basename "$CLAUDE_PROJECT_DIR" 2>/dev/null || basename "$(pwd)")
47
-
48
- # Load NTFY_TOPIC from local config if not already set
49
- if [ -z "$NTFY_TOPIC" ]; then
50
- # Check for project-local config (gitignored)
51
- if [ -f "$CLAUDE_PROJECT_DIR/.claude/env.local" ]; then
52
- # shellcheck source=/dev/null
53
- source "$CLAUDE_PROJECT_DIR/.claude/env.local"
54
- fi
55
- # Check for user-global config
56
- if [ -z "$NTFY_TOPIC" ] && [ -f "$HOME/.claude/env.local" ]; then
57
- # shellcheck source=/dev/null
58
- source "$HOME/.claude/env.local"
59
- fi
60
- fi
61
-
62
- # Exit silently if still not configured
63
- if [ -z "$NTFY_TOPIC" ]; then
64
- exit 0
65
- fi
66
-
67
- # Extract task summary from transcript (last assistant message, truncated)
68
- TASK_SUMMARY=""
69
- if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
70
- # Get the last assistant message from the JSONL transcript
71
- # The transcript contains lines with "type":"assistant" and "message" content
72
- # Use awk for cross-platform compatibility (tac is not available on macOS)
73
- LAST_ASSISTANT=$(awk '/"type"[[:space:]]*:[[:space:]]*"assistant"/{line=$0} END{if(line) print line}' "$TRANSCRIPT_PATH" 2>/dev/null)
74
- if [ -n "$LAST_ASSISTANT" ]; then
75
- # Extract the message content - look for text content in the message
76
- # Format: {"message":{"content":[{"type":"text","text":"..."}]}}
77
- # Use jq for robust JSON parsing when available, fallback to grep/sed
78
- if command -v jq >/dev/null 2>&1; then
79
- RAW_SUMMARY=$(echo "$LAST_ASSISTANT" | jq -r '.message.content[] | select(.type == "text") | .text' 2>/dev/null | head -1)
80
- else
81
- # Fallback: simple regex extraction (may fail on escaped quotes)
82
- RAW_SUMMARY=$(echo "$LAST_ASSISTANT" | grep -o '"text"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*: *"//' | sed 's/"$//')
83
- fi
84
- if [ -n "$RAW_SUMMARY" ]; then
85
- # Truncate to 100 chars and clean up newlines
86
- TASK_SUMMARY=$(echo "$RAW_SUMMARY" | tr '\n' ' ' | cut -c1-100)
87
- # Add ellipsis if truncated
88
- if [ ${#RAW_SUMMARY} -gt 100 ]; then
89
- TASK_SUMMARY="${TASK_SUMMARY}..."
90
- fi
91
- fi
92
- fi
93
- fi
94
-
95
- # Build session info string (shown in body)
96
- SESSION_INFO=""
97
- if [ -n "$SESSION_ID" ]; then
98
- SESSION_INFO="Session: $SESSION_ID"
99
- fi
100
-
101
- # Determine notification title and body based on hook type
102
- case "$HOOK_EVENT" in
103
- "Notification")
104
- case "$NOTIFICATION_TYPE" in
105
- "permission_prompt")
106
- TITLE="Claude [$SOURCE] - Permission Required"
107
- BODY="${MESSAGE:-Claude needs your permission to continue}"
108
- if [ -n "$SESSION_INFO" ]; then
109
- BODY="$SESSION_INFO
110
- $BODY"
111
- fi
112
- PRIORITY="high"
113
- TAGS="warning"
114
- ;;
115
- "idle_prompt")
116
- TITLE="Claude [$SOURCE] - Waiting"
117
- BODY="${MESSAGE:-Claude is waiting for your input}"
118
- if [ -n "$SESSION_INFO" ]; then
119
- BODY="$SESSION_INFO
120
- $BODY"
121
- fi
122
- PRIORITY="default"
123
- TAGS="hourglass"
124
- ;;
125
- *)
126
- TITLE="Claude [$SOURCE] - Attention"
127
- BODY="${MESSAGE:-Claude needs your attention}"
128
- if [ -n "$SESSION_INFO" ]; then
129
- BODY="$SESSION_INFO
130
- $BODY"
131
- fi
132
- PRIORITY="default"
133
- TAGS="bell"
134
- ;;
135
- esac
136
- ;;
137
- "Stop")
138
- TITLE="Claude [$SOURCE] - Finished"
139
- BODY="$PROJECT_NAME"
140
- if [ -n "$SESSION_INFO" ]; then
141
- BODY="$SESSION_INFO | $BODY"
142
- fi
143
- if [ -n "$TASK_SUMMARY" ]; then
144
- BODY="$BODY
145
- $TASK_SUMMARY"
146
- fi
147
- PRIORITY="default"
148
- TAGS="white_check_mark"
149
- ;;
150
- "SubagentStop")
151
- TITLE="Claude [$SOURCE] - Subagent Done"
152
- BODY="$PROJECT_NAME"
153
- if [ -n "$SESSION_INFO" ]; then
154
- BODY="$SESSION_INFO | $BODY"
155
- fi
156
- if [ -n "$TASK_SUMMARY" ]; then
157
- BODY="$BODY
158
- $TASK_SUMMARY"
159
- fi
160
- PRIORITY="low"
161
- TAGS="checkered_flag"
162
- ;;
163
- *)
164
- TITLE="Claude [$SOURCE]"
165
- BODY="${MESSAGE:-Event: $HOOK_EVENT}"
166
- if [ -n "$SESSION_INFO" ]; then
167
- BODY="$SESSION_INFO
168
- $BODY"
169
- fi
170
- PRIORITY="default"
171
- TAGS="robot"
172
- ;;
173
- esac
174
-
175
- # Send notification via ntfy.sh
176
- curl -s \
177
- -H "Title: $TITLE" \
178
- -H "Priority: $PRIORITY" \
179
- -H "Tags: $TAGS" \
180
- -d "$BODY" \
181
- "https://ntfy.sh/$NTFY_TOPIC" > /dev/null 2>&1
182
-
183
- exit 0