@codyswann/lisa 2.121.0 → 2.121.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.
Files changed (67) hide show
  1. package/dist/agy/mcp-installer.d.ts +18 -0
  2. package/dist/agy/mcp-installer.d.ts.map +1 -1
  3. package/dist/agy/mcp-installer.js +21 -0
  4. package/dist/agy/mcp-installer.js.map +1 -1
  5. package/dist/agy/plugin-installer.d.ts.map +1 -1
  6. package/dist/agy/plugin-installer.js +6 -2
  7. package/dist/agy/plugin-installer.js.map +1 -1
  8. package/dist/codex/lisa-plugin-detection.d.ts +18 -19
  9. package/dist/codex/lisa-plugin-detection.d.ts.map +1 -1
  10. package/dist/codex/lisa-plugin-detection.js +26 -22
  11. package/dist/codex/lisa-plugin-detection.js.map +1 -1
  12. package/dist/copilot/plugin-installer.d.ts.map +1 -1
  13. package/dist/copilot/plugin-installer.js +7 -3
  14. package/dist/copilot/plugin-installer.js.map +1 -1
  15. package/dist/core/config.d.ts +20 -0
  16. package/dist/core/config.d.ts.map +1 -1
  17. package/dist/core/config.js +20 -0
  18. package/dist/core/config.js.map +1 -1
  19. package/dist/core/lisa.d.ts.map +1 -1
  20. package/dist/core/lisa.js +10 -6
  21. package/dist/core/lisa.js.map +1 -1
  22. package/package.json +1 -1
  23. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa/.codex-plugin/plugin.json +2 -2
  25. package/plugins/lisa/{.codex-plugin → hooks}/hooks.json +7 -7
  26. package/plugins/lisa-agy/plugin.json +1 -1
  27. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  29. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +2 -2
  35. package/plugins/lisa-harper-fabric/{.codex-plugin → hooks}/hooks.json +2 -2
  36. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +2 -2
  38. package/plugins/lisa-nestjs/{.codex-plugin → hooks}/hooks.json +1 -1
  39. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-rails/.codex-plugin/plugin.json +2 -2
  43. package/plugins/lisa-rails/{.codex-plugin → hooks}/hooks.json +4 -4
  44. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-typescript/.codex-plugin/plugin.json +2 -2
  46. package/plugins/lisa-typescript/{.codex-plugin → hooks}/hooks.json +4 -4
  47. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  49. package/scripts/generate-codex-plugin-artifacts.mjs +28 -48
  50. package/scripts/generate-copilot-plugin-artifacts.mjs +5 -0
  51. package/scripts/generate-cursor-plugin-artifacts.mjs +4 -0
  52. package/scripts/internal-copilot-runtime-probe.json +10 -4
  53. package/plugins/lisa/.codex-plugin/hooks/block-no-verify.sh +0 -37
  54. package/plugins/lisa/.codex-plugin/hooks/inject-flow-context.sh +0 -12
  55. package/plugins/lisa/.codex-plugin/hooks/inject-rules.sh +0 -33
  56. package/plugins/lisa/.codex-plugin/hooks/install-pkgs.sh +0 -69
  57. package/plugins/lisa/.codex-plugin/hooks/notify-ntfy.sh +0 -183
  58. package/plugins/lisa/.codex-plugin/hooks/setup-jira-cli.sh +0 -51
  59. package/plugins/lisa-harper-fabric/.codex-plugin/hooks/inject-rules.sh +0 -16
  60. package/plugins/lisa-nestjs/.codex-plugin/hooks/block-migration-edits.sh +0 -60
  61. package/plugins/lisa-rails/.codex-plugin/hooks/inject-rules.sh +0 -22
  62. package/plugins/lisa-rails/.codex-plugin/hooks/rubocop-on-edit.sh +0 -78
  63. package/plugins/lisa-rails/.codex-plugin/hooks/sg-scan-on-edit.sh +0 -74
  64. package/plugins/lisa-typescript/.codex-plugin/hooks/block-suppress-directives.sh +0 -73
  65. package/plugins/lisa-typescript/.codex-plugin/hooks/format-on-edit.sh +0 -79
  66. package/plugins/lisa-typescript/.codex-plugin/hooks/lint-on-edit.sh +0 -134
  67. package/plugins/lisa-typescript/.codex-plugin/hooks/sg-scan-on-edit.sh +0 -71
@@ -468,24 +468,33 @@ function main() {
468
468
  * @param {object} claudeManifest Parsed contents of .claude-plugin/plugin.json.
469
469
  */
470
470
  function emitCodexHooks(pluginDir, claudeManifest) {
471
- const codexPluginDir = path.join(pluginDir, ".codex-plugin");
472
- const hooksJsonPath = path.join(codexPluginDir, "hooks.json");
473
- const hooksScriptsDir = path.join(codexPluginDir, "hooks");
471
+ // Codex auto-discovers a plugin's hooks at <plugin-root>/hooks/hooks.json and
472
+ // resolves the manifest hooks pointer + ${PLUGIN_ROOT} relative to the plugin
473
+ // root (developers.openai.com/codex/plugins/build). The hook scripts already
474
+ // ship at <plugin-root>/hooks/ (copied from plugins/src by the Claude build),
475
+ // so the Codex hooks.json lives alongside them and no script copy is needed.
476
+ const hooksDir = path.join(pluginDir, "hooks");
477
+ const hooksJsonPath = path.join(hooksDir, "hooks.json");
478
+ // Clean up the pre-2.121 layout (hooks.json + copied scripts under
479
+ // .codex-plugin/) so a rebuilt plugin never ships both.
480
+ const legacyCodexPluginDir = path.join(pluginDir, ".codex-plugin");
481
+ fs.rmSync(path.join(legacyCodexPluginDir, "hooks.json"), { force: true });
482
+ fs.rmSync(path.join(legacyCodexPluginDir, "hooks"), {
483
+ force: true,
484
+ recursive: true,
485
+ });
474
486
  const filtered = filterCodexHooks(claudeManifest.hooks);
475
487
  if (filtered === null) {
476
- // Nothing survived the filter. Remove any stale hooks artifacts from a
477
- // prior build so componentPointers() doesn't keep advertising removed
478
- // hooks via the ./hooks.json pointer.
488
+ // Nothing survived the filter. Remove any stale hooks.json so
489
+ // componentPointers() doesn't keep advertising removed hooks.
479
490
  fs.rmSync(hooksJsonPath, { force: true });
480
- fs.rmSync(hooksScriptsDir, { force: true, recursive: true });
481
491
  return;
482
492
  }
483
- fs.mkdirSync(codexPluginDir, { recursive: true });
493
+ fs.mkdirSync(hooksDir, { recursive: true });
484
494
  fs.writeFileSync(
485
495
  hooksJsonPath,
486
496
  `${JSON.stringify(buildCodexHooksDocument(filtered), null, 2)}\n`
487
497
  );
488
- copyCodexHookScripts(pluginDir, filtered);
489
498
  }
490
499
 
491
500
  /**
@@ -540,8 +549,8 @@ function componentPointers(pluginDir) {
540
549
  ...(fs.existsSync(path.join(pluginDir, ".mcp.json"))
541
550
  ? { mcpServers: "./.mcp.json" }
542
551
  : {}),
543
- ...(fs.existsSync(path.join(pluginDir, ".codex-plugin", "hooks.json"))
544
- ? { hooks: "./hooks.json" }
552
+ ...(fs.existsSync(path.join(pluginDir, "hooks", "hooks.json"))
553
+ ? { hooks: "./hooks/hooks.json" }
545
554
  : {}),
546
555
  };
547
556
  }
@@ -551,8 +560,9 @@ function componentPointers(pluginDir) {
551
560
  * Claude plugin.json hooks block:
552
561
  * - Drop every `entire hooks claude-code *` command (Claude-only).
553
562
  * - Drop every reference to `enforce-team-first.sh` (Claude-team-specific).
554
- * - Rewrite ${CLAUDE_PLUGIN_ROOT}/hooks/<script>.sh to ./hooks/<script>.sh
555
- * (Codex resolves plugin paths relative to .codex-plugin/plugin.json).
563
+ * - Rewrite ${CLAUDE_PLUGIN_ROOT}/hooks/<script>.sh to
564
+ * ${PLUGIN_ROOT}/hooks/<script>.sh (Codex exposes the installed plugin
565
+ * root to hook commands as ${PLUGIN_ROOT}).
556
566
  * - Drop matchers that produce no surviving handlers.
557
567
  *
558
568
  * When the resulting block is empty, no hooks.json is written and the
@@ -590,11 +600,13 @@ export function filterCodexHooks(hooksBlock) {
590
600
  return [
591
601
  {
592
602
  ...h,
593
- // Codex hook commands resolve relative to .codex-plugin/plugin.json;
594
- // rewrite to a path the hooks.json sibling will find.
603
+ // Codex exposes the installed plugin root as ${PLUGIN_ROOT} to hook
604
+ // commands (developers.openai.com/codex/plugins/build). A bare
605
+ // ./hooks/ path would resolve against the session cwd, not the
606
+ // plugin, so rewrite to the plugin-root env var.
595
607
  command: h.command.replaceAll(
596
608
  "${CLAUDE_PLUGIN_ROOT}/hooks/",
597
- "./hooks/"
609
+ "${PLUGIN_ROOT}/hooks/"
598
610
  ),
599
611
  },
600
612
  ];
@@ -613,38 +625,6 @@ export function filterCodexHooks(hooksBlock) {
613
625
  return Object.keys(out).length > 0 ? out : null;
614
626
  }
615
627
 
616
- /**
617
- * Copy hook scripts that survived filterCodexHooks into the Codex artifact.
618
- *
619
- * Scripts land at <pluginDir>/.codex-plugin/hooks/ so the hooks.json pointer
620
- * "./hooks/<n>.sh" resolves correctly when Codex loads the plugin.
621
- *
622
- * @param {string} pluginDir Built Claude plugin directory.
623
- * @param {object} hooks Codex-shaped hooks block from filterCodexHooks.
624
- */
625
- function copyCodexHookScripts(pluginDir, hooks) {
626
- const srcHooksDir = path.join(pluginDir, "hooks");
627
- if (!fs.existsSync(srcHooksDir)) return;
628
- const referenced = new Set();
629
- for (const entries of Object.values(hooks)) {
630
- for (const entry of entries) {
631
- for (const h of entry.hooks ?? []) {
632
- if (typeof h.command !== "string") continue;
633
- const m = /^\.\/hooks\/([^/\s]+\.sh)/.exec(h.command);
634
- if (m) referenced.add(m[1]);
635
- }
636
- }
637
- }
638
- if (referenced.size === 0) return;
639
- const dstHooksDir = path.join(pluginDir, ".codex-plugin", "hooks");
640
- fs.mkdirSync(dstHooksDir, { recursive: true });
641
- for (const name of referenced) {
642
- const src = path.join(srcHooksDir, name);
643
- if (!fs.existsSync(src)) continue;
644
- fs.copyFileSync(src, path.join(dstHooksDir, name));
645
- }
646
- }
647
-
648
628
  function metadataFor(pluginName, claudeManifest) {
649
629
  const map = {
650
630
  lisa: {
@@ -139,6 +139,11 @@ export function generateCopilotVariant(srcDir, outDir, version) {
139
139
  copyDir(srcDir, outDir, relPath => {
140
140
  if (relPath.startsWith(".codex-plugin/") || relPath === ".codex-plugin")
141
141
  return false;
142
+ // Drop the Codex hooks manifest the base build emits at hooks/hooks.json —
143
+ // Copilot reads its (camelCase) hooks from .claude-plugin/plugin.json, not
144
+ // this Codex-shaped (PascalCase, ${PLUGIN_ROOT}) file. Keeping it would ship
145
+ // a spurious, wrong-shaped hooks manifest in the Copilot variant.
146
+ if (relPath === path.join("hooks", "hooks.json")) return false;
142
147
  // Drop Codex-specific per-skill openai.yaml artifacts — Copilot does not use them.
143
148
  if (/^skills\/[^/]+\/agents\/openai\.ya?ml$/.test(relPath)) return false;
144
149
  const skillsPrefix = path.join("skills") + path.sep;
@@ -105,6 +105,10 @@ export function generateCursorVariant(srcDir, outDir, version) {
105
105
  // Drop the `.codex-plugin/` directory — Cursor does not consume it.
106
106
  if (relPath.startsWith(".codex-plugin/") || relPath === ".codex-plugin")
107
107
  return false;
108
+ // Drop the Codex hooks manifest the base build emits at hooks/hooks.json —
109
+ // Cursor reads its (filtered) hooks from .claude-plugin/plugin.json, not this
110
+ // Codex-shaped file. The surviving .sh scripts in hooks/ are kept below.
111
+ if (relPath === path.join("hooks", "hooks.json")) return false;
108
112
  // Drop Codex-specific per-skill openai.yaml artifacts — Cursor does not use them.
109
113
  // These live at skills/<n>/agents/openai.yaml and are generated by the Codex
110
114
  // artifact builder; the per-agent variants ship only the SKILL.md.
@@ -1,7 +1,13 @@
1
1
  {
2
- "_comment": "Cached Copilot runtime probe results. Updated by Wave 3 verification probes (Task #17) and consumed by scripts/generate-copilot-plugin-artifacts.mjs. Missing values fall through to conservative defaults.",
3
- "cliVersion": null,
2
+ "_comment": "Cached Copilot runtime probe results consumed by scripts/generate-copilot-plugin-artifacts.mjs. Populated 2026-05-28 by empirical probes against GitHub Copilot CLI 1.0.55 (see _evidence). Missing/null values fall through to conservative generator defaults.",
3
+ "cliVersion": "1.0.55",
4
4
  "pluginRootEnvVar": null,
5
- "rulesAutoLoads": null,
6
- "agentPathOverrideAccepted": null
5
+ "rulesAutoLoads": false,
6
+ "agentPathOverrideAccepted": false,
7
+ "_evidence": {
8
+ "cliVersion": "[VERIFIED-BY-RUN] `copilot --version` => 'GitHub Copilot CLI 1.0.55.'",
9
+ "rulesAutoLoads": "[VERIFIED-BY-RUN] A plugin's bundled rules/ directory is NOT auto-loaded. Probe: `copilot --plugin-dir <pkg> -p 'plugin codeword?'` with a sentinel rule in <pkg>/rules/sentinel.md and no .github/copilot-instructions.md returned 'UNKNOWN'. (Copilot DOES auto-load .github/copilot-instructions.md — a separate project file — but that template only points at the plugin, so inject-rules.sh must still ship. Hence false: do NOT strip inject-rules.sh.)",
10
+ "agentPathOverrideAccepted": "[PARTIAL] With an explicit manifest 'agents: ./agents/' pointer, `copilot --plugin-dir <pkg> --agent <ns>:<name>` loaded a non-.agent.md file (agents/probeagent.md) and it responded. However that is session-only --plugin-dir loading; the marketplace-install auto-discovery path is unverified and .agent.md loads in every observed context. Kept false so the generator retains the universally-safe rename to <name>.agent.md.",
11
+ "pluginRootEnvVar": "[VERIFIED-DOC] GitHub Copilot CLI plugin reference documents ${COPILOT_PLUGIN_DATA} is also exposed as ${CLAUDE_PLUGIN_DATA}; Copilot aliases the CLAUDE_* plugin env vars, so the ${CLAUDE_PLUGIN_ROOT} form the generator emits in hook commands is supported. null => generator keeps ${CLAUDE_PLUGIN_ROOT}. Direct hook-fire env capture was not achievable via --plugin-dir in -p mode (hook did not fire; firing likely requires a real `copilot plugin install` and/or interactive trust — unverified)."
12
+ }
7
13
  }
@@ -1,37 +0,0 @@
1
- #!/usr/bin/env bash
2
- # PreToolUse hook for Bash: blocks any command containing --no-verify.
3
- # --no-verify on git commit/push (and equivalents) bypasses pre-commit/pre-push
4
- # hooks that exist for a reason. The fix is to address the underlying issue,
5
- # not silence the check. See feedback_never_no_verify in user memory.
6
- #
7
- # Word-boundary match avoids false positives on flags like --no-verify-ssl,
8
- # --no-verify-host, etc.
9
- set -euo pipefail
10
-
11
- input="$(cat)"
12
-
13
- tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty')"
14
- if [ "$tool_name" != "Bash" ]; then
15
- exit 0
16
- fi
17
-
18
- command_str="$(printf '%s' "$input" | jq -r '.tool_input.command // empty')"
19
- if [ -z "$command_str" ]; then
20
- exit 0
21
- fi
22
-
23
- # Match --no-verify bounded by non-token characters (not alphanumeric, _, or -).
24
- # This catches all syntactic positions including subshells (e.g. `(git commit --no-verify)`)
25
- # while excluding longer flags like --no-verify-ssl, --no-verify-host, etc.
26
- if printf '%s' "$command_str" | grep -Eq '(^|[^[:alnum:]_-])--no-verify($|[^[:alnum:]_-])'; then
27
- cat >&2 <<'EOF'
28
- Blocked: --no-verify bypasses pre-commit/pre-push hooks. Fix the underlying
29
- issue (lint error, failing test, formatting) or ask the user before bypassing.
30
-
31
- If the user has explicitly authorized the bypass for this specific command,
32
- re-run after they confirm.
33
- EOF
34
- exit 2
35
- fi
36
-
37
- exit 0
@@ -1,12 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Tells subagents not to ask users for flow selection.
3
- # The parent agent has already determined the flow and work type.
4
- # Used by SubagentStart hook.
5
- set -euo pipefail
6
-
7
- jq -n '{
8
- "hookSpecificOutput": {
9
- "hookEventName": "SubagentStart",
10
- "additionalContext": "You are a subagent operating within an established flow. Your parent agent has already determined the flow and work type. Do NOT ask the user to choose a flow or classify the request. Execute your assigned work within the context provided by your parent agent."
11
- }
12
- }'
@@ -1,33 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Reads all .md files from the plugin's rules/eager/ directory and injects them
3
- # into the session context via additionalContext.
4
- # Used by SessionStart and SubagentStart hooks.
5
- #
6
- # The split between eager and reference rules is documented in
7
- # rules/eager/00-bootstrap.md (or the equivalent README). Reference bodies
8
- # under rules/reference/ are installed alongside but loaded only when the
9
- # eager breadcrumb points to them.
10
- set -euo pipefail
11
-
12
- RULES_DIR="${CLAUDE_PLUGIN_ROOT}/rules/eager"
13
-
14
- # Backward compatibility: if the eager subdir is absent (older Lisa install),
15
- # fall back to the flat rules/ directory so a partial upgrade still ships rules.
16
- if [ ! -d "$RULES_DIR" ]; then
17
- RULES_DIR="${CLAUDE_PLUGIN_ROOT}/rules"
18
- fi
19
-
20
- # Bail silently if no rules directory at all
21
- [ -d "$RULES_DIR" ] || exit 0
22
-
23
- CONTEXT=""
24
- for file in "$RULES_DIR"/*.md; do
25
- [ -f "$file" ] || continue
26
- CONTEXT+="$(cat "$file")"$'\n\n'
27
- done
28
-
29
- # Bail if no rules found
30
- [ -n "$CONTEXT" ] || exit 0
31
-
32
- # Output as JSON — jq handles escaping
33
- jq -n --arg ctx "$CONTEXT" '{"additionalContext": $ctx}'
@@ -1,69 +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
- # Only run package installation when node_modules are missing.
6
- # This covers remote environments, new worktrees, fresh clones, and CI.
7
- if [ -d "node_modules" ]; then
8
- exit 0
9
- fi
10
-
11
- # Detect package manager based on lock file presence
12
- if [ -f "bun.lockb" ] || [ -f "bun.lock" ]; then
13
- bun install
14
- elif [ -f "pnpm-lock.yaml" ]; then
15
- pnpm install
16
- elif [ -f "yarn.lock" ]; then
17
- yarn install
18
- elif [ -f "package-lock.json" ]; then
19
- npm install
20
- else
21
- npm install
22
- fi
23
-
24
- # The tools below use Linux-specific binaries and paths — skip on other platforms.
25
- if [ "$(uname -s)" != "Linux" ]; then
26
- exit 0
27
- fi
28
-
29
- # Install Gitleaks for secret detection (pre-commit hook)
30
- echo "Installing Gitleaks for secret detection..."
31
- GITLEAKS_VERSION="8.18.4"
32
- curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" | tar -xz -C /usr/local/bin gitleaks
33
- echo "Gitleaks installed: $(gitleaks version)"
34
-
35
- # Install jira-cli for JIRA integration
36
- # The tarball nests the binary at jira_VERSION_linux_x86_64/bin/jira,
37
- # so we extract to a temp dir and copy the binary out.
38
- echo "Installing jira-cli for JIRA integration..."
39
- JIRA_CLI_VERSION="1.7.0"
40
- JIRA_TMPDIR=$(mktemp -d)
41
- curl -sSfL "https://github.com/ankitpokhrel/jira-cli/releases/download/v${JIRA_CLI_VERSION}/jira_${JIRA_CLI_VERSION}_linux_x86_64.tar.gz" \
42
- | tar -xz -C "${JIRA_TMPDIR}"
43
- cp "${JIRA_TMPDIR}/jira_${JIRA_CLI_VERSION}_linux_x86_64/bin/jira" /usr/local/bin/jira
44
- chmod +x /usr/local/bin/jira
45
- rm -rf "${JIRA_TMPDIR}"
46
- echo "jira-cli installed: $(jira version)"
47
-
48
- # Install Chromium for Lighthouse CI (pre-push hook)
49
- # Playwright's bundled Chromium works with @lhci/cli
50
- echo "Installing Chromium for Lighthouse CI..."
51
- npx playwright install chromium
52
-
53
- # Find and export CHROME_PATH for Lighthouse CI
54
- # Use sort to ensure deterministic selection of the latest version
55
- CHROME_PATH=$(find ~/.cache/ms-playwright -name "chrome" -type f 2>/dev/null | grep "chrome-linux" | sort | tail -n 1)
56
- if [ -n "$CHROME_PATH" ]; then
57
- # Append to ~/.bashrc for shell sessions (idempotent)
58
- if ! grep -q "export CHROME_PATH=" ~/.bashrc 2>/dev/null; then
59
- echo "export CHROME_PATH=\"$CHROME_PATH\"" >> ~/.bashrc
60
- else
61
- # Update existing CHROME_PATH in bashrc
62
- sed -i "s|^export CHROME_PATH=.*|export CHROME_PATH=\"$CHROME_PATH\"|" ~/.bashrc
63
- fi
64
-
65
- export CHROME_PATH="$CHROME_PATH"
66
- echo "Chromium installed at: $CHROME_PATH"
67
- fi
68
-
69
- exit 0
@@ -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
@@ -1,51 +0,0 @@
1
- #!/usr/bin/env bash
2
- ##
3
- # Writes the JIRA CLI config file from environment variables.
4
- # Runs on SessionStart so the config is available for every session.
5
- #
6
- # Required env vars (must be created in your Claude Code Web environment):
7
- # JIRA_INSTALLATION - cloud or local
8
- # JIRA_SERVER - Atlassian instance URL
9
- # JIRA_LOGIN - login email
10
- # JIRA_PROJECT - default project key
11
- # JIRA_API_TOKEN - already expected by jira-cli natively
12
- #
13
- # Optional env vars:
14
- # JIRA_BOARD - default board name
15
- ##
16
-
17
- set -euo pipefail
18
-
19
- # Fix jira-cli installation if install-pkgs.sh failed to extract correctly.
20
- # Only attempt on Linux — the binary download is Linux-only.
21
- if [[ "$(uname -s)" == "Linux" ]] && ! command -v jira &>/dev/null; then
22
- JIRA_CLI_VERSION="1.7.0"
23
- TMPDIR=$(mktemp -d)
24
- curl -sSfL "https://github.com/ankitpokhrel/jira-cli/releases/download/v${JIRA_CLI_VERSION}/jira_${JIRA_CLI_VERSION}_linux_x86_64.tar.gz" \
25
- | tar -xz -C "${TMPDIR}"
26
- cp "${TMPDIR}/jira_${JIRA_CLI_VERSION}_linux_x86_64/bin/jira" /usr/local/bin/jira
27
- chmod +x /usr/local/bin/jira
28
- rm -rf "${TMPDIR}"
29
- fi
30
-
31
- CONFIG_DIR="${HOME}/.config/.jira"
32
- CONFIG_FILE="${CONFIG_DIR}/.config.yml"
33
-
34
- # Skip config write if required vars are missing
35
- if [[ -z "${JIRA_SERVER:-}" || -z "${JIRA_LOGIN:-}" ]]; then
36
- exit 0
37
- fi
38
-
39
- mkdir -p "${CONFIG_DIR}"
40
-
41
- cat > "${CONFIG_FILE}" << EOF
42
- installation: ${JIRA_INSTALLATION:-cloud}
43
- server: ${JIRA_SERVER}
44
- login: ${JIRA_LOGIN}
45
- project: ${JIRA_PROJECT:-}
46
- board: "${JIRA_BOARD:-}"
47
- auth_type: basic
48
- epic:
49
- name: Epic Name
50
- link: Epic Link
51
- EOF
@@ -1,16 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Reads all .md files from the plugin's rules/ directory and injects them
3
- # into Claude context at session/subagent start.
4
- set -euo pipefail
5
-
6
- ROOT="${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}}"
7
- RULES_DIR="$ROOT/rules"
8
-
9
- [ -d "$RULES_DIR" ] || exit 0
10
-
11
- for rule in "$RULES_DIR"/*.md; do
12
- [ -f "$rule" ] || continue
13
- printf '\n<lisa-harper-fabric-rule path="%s">\n' "$rule"
14
- cat "$rule"
15
- printf '\n</lisa-harper-fabric-rule>\n'
16
- done
@@ -1,60 +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
- # PreToolUse hook: block Write/Edit on TypeORM migration files.
6
- # NestJS projects must use `bun run migration:generate` to create migrations
7
- # from entity diffs. Hand-written migrations drift from entity metadata and
8
- # break the schema/migration contract.
9
- # Reference: https://docs.claude.com/en/docs/claude-code/hooks
10
- # Exit code 2 blocks the tool call and surfaces stderr to Claude.
11
-
12
- JSON_INPUT=$(cat)
13
-
14
- if ! command -v jq >/dev/null 2>&1; then
15
- echo "⚠ block-migration-edits: jq not available, allowing edit" >&2
16
- exit 0
17
- fi
18
-
19
- FILE_PATH=$(echo "$JSON_INPUT" | jq -r '.tool_input.file_path // empty')
20
-
21
- if [ -z "$FILE_PATH" ]; then
22
- exit 0
23
- fi
24
-
25
- case "$FILE_PATH" in
26
- */migrations/*.ts|*/migrations/*.js)
27
- cat >&2 <<EOF
28
- ❌ Blocked: Direct edits to TypeORM migration files are not allowed.
29
-
30
- File: $FILE_PATH
31
-
32
- Entity files (src/database/entities/*.ts) are the single source of
33
- truth for the database schema in this project. Migrations are a derived
34
- artifact — generate them from entity diffs:
35
-
36
- 1. Edit the entity to express the desired schema.
37
- 2. Run: bun run migration:generate --name=<DescriptiveName>
38
- 3. Review the generated migration; commit entity + migration together.
39
-
40
- If a schema change cannot be expressed via the entity model, the entity
41
- model is wrong — fix the entity, do not hand-write the migration.
42
-
43
- OUT-OF-BAND MIGRATIONS (seed data, backfills, data transformations,
44
- one-off cleanup): these genuinely cannot come from entity diffs. They
45
- are legitimate but they bypass the entity-as-source-of-truth contract.
46
-
47
- If you believe this edit is an out-of-band migration:
48
- 1. STOP and tell the user what change is needed and why it cannot
49
- be expressed via the entity model.
50
- 2. Get explicit approval before proceeding.
51
- 3. Document the rationale in the migration's class comment.
52
-
53
- Do NOT silently hand-write a migration. See the nestjs-rules skill
54
- for the full rationale.
55
- EOF
56
- exit 2
57
- ;;
58
- esac
59
-
60
- exit 0
@@ -1,22 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Reads all .md files from the plugin's rules/ directory and injects them
3
- # into the session context via additionalContext.
4
- # Used by SessionStart and SubagentStart hooks.
5
- set -euo pipefail
6
-
7
- RULES_DIR="${CLAUDE_PLUGIN_ROOT}/rules"
8
-
9
- # Bail silently if no rules directory
10
- [ -d "$RULES_DIR" ] || exit 0
11
-
12
- CONTEXT=""
13
- for file in "$RULES_DIR"/*.md; do
14
- [ -f "$file" ] || continue
15
- CONTEXT+="$(cat "$file")"$'\n\n'
16
- done
17
-
18
- # Bail if no rules found
19
- [ -n "$CONTEXT" ] || exit 0
20
-
21
- # Output as JSON — jq handles escaping
22
- jq -n --arg ctx "$CONTEXT" '{"additionalContext": $ctx}'