@codyswann/lisa 2.124.0 → 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.
- package/dist/agy/mcp-installer.d.ts +22 -5
- package/dist/agy/mcp-installer.d.ts.map +1 -1
- package/dist/agy/mcp-installer.js +52 -12
- package/dist/agy/mcp-installer.js.map +1 -1
- package/dist/codex/hooks-installer.d.ts.map +1 -1
- package/dist/codex/hooks-installer.js +0 -7
- package/dist/codex/hooks-installer.js.map +1 -1
- package/dist/core/lisa.d.ts +19 -11
- package/dist/core/lisa.d.ts.map +1 -1
- package/dist/core/lisa.js +44 -12
- package/dist/core/lisa.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -10
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/block-no-verify.agy.sh +45 -0
- package/plugins/lisa/hooks/hooks.json +0 -11
- package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +45 -0
- package/plugins/lisa-agy/hooks.json +15 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -12
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -12
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/.claude-plugin/plugin.json +0 -1
- package/plugins/src/base/hooks/block-no-verify.agy.sh +45 -0
- package/scripts/generate-agy-plugin-artifacts.mjs +158 -19
- package/scripts/generate-copilot-plugin-artifacts.mjs +1 -1
- package/scripts/lib/per-agent-hook-filter.mjs +29 -18
- package/dist/codex/scripts/notify-ntfy.sh +0 -18
- package/plugins/lisa/hooks/notify-ntfy.sh +0 -183
- package/plugins/lisa-copilot/hooks/notify-ntfy.sh +0 -183
- package/plugins/lisa-cursor/hooks/notify-ntfy.sh +0 -183
- package/plugins/lisa-expo-agy/.mcp.json +0 -8
- 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`).
|
|
8
|
-
* plugin-bundled hooks
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
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/,
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
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,
|
|
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
|
-
//
|
|
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),
|
|
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:
|
|
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:
|
|
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, //
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
*
|
|
199
|
-
*
|
|
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
|