@codyswann/lisa 2.151.0 → 2.152.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 (65) hide show
  1. package/dist/core/lisa.d.ts.map +1 -1
  2. package/dist/core/lisa.js +23 -11
  3. package/dist/core/lisa.js.map +1 -1
  4. package/dist/opencode/hooks-installer.d.ts +54 -0
  5. package/dist/opencode/hooks-installer.d.ts.map +1 -0
  6. package/dist/opencode/hooks-installer.js +300 -0
  7. package/dist/opencode/hooks-installer.js.map +1 -0
  8. package/dist/opencode/plugin-templates/lisa-block-migration-edits.ts +44 -0
  9. package/dist/opencode/plugin-templates/lisa-block-suppress-directives.ts +57 -0
  10. package/dist/opencode/plugin-templates/lisa-lint-on-edit.ts +53 -0
  11. package/dist/opencode/plugin-templates/lisa-rubocop-on-edit.ts +54 -0
  12. package/dist/opencode/plugin-templates/lisa-session-bootstrap.ts +76 -0
  13. package/dist/opencode/plugin-templates/lisa-sg-scan-on-edit.ts +50 -0
  14. package/package.json +2 -2
  15. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  17. package/plugins/lisa/skills/repair-intake/SKILL.md +57 -21
  18. package/plugins/lisa-agy/plugin.json +1 -1
  19. package/plugins/lisa-agy/skills/repair-intake/SKILL.md +57 -21
  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 -1
  26. package/plugins/lisa-copilot/skills/repair-intake/SKILL.md +57 -21
  27. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-cursor/skills/repair-intake/SKILL.md +57 -21
  29. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo-agy/plugin.json +1 -1
  32. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  42. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  47. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-rails-agy/plugin.json +1 -1
  52. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  57. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  62. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  64. package/plugins/src/base/skills/repair-intake/SKILL.md +57 -21
  65. package/scripts/copy-opencode-plugin-templates.mjs +29 -0
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Install Lisa-managed OpenCode hooks into a host project.
3
+ *
4
+ * Unlike Codex (which drives every hook through shell scripts wired into
5
+ * `.codex/hooks.json`), OpenCode is mapped to its NATIVE surfaces first, then
6
+ * falls back to runtime plugins only where genuine behavior is required:
7
+ *
8
+ * - block-no-verify → `permission.bash` deny rules in `opencode.json`. Cheaper
9
+ * and more robust than a hook: OpenCode evaluates the parsed command against
10
+ * glob patterns and rejects matches before they run (verified-by-run on
11
+ * opencode 1.16.2: `git commit … --no-verify` and `HUSKY=0 …` are denied).
12
+ * - format-on-edit → OpenCode's BUILT-IN prettier formatter already formats on
13
+ * edit, so Lisa emits no formatter config (overriding it would be worse than
14
+ * the default). This is why there is no format plugin below.
15
+ * - inject-rules → not needed; rules ship via the canonical `AGENTS.md`, which
16
+ * OpenCode reads natively (handled by the skills/AGENTS.md emit).
17
+ * - everything that needs runtime behavior (blocking suppression directives /
18
+ * migration edits, linting / scanning just-edited files, session bootstrap)
19
+ * → a `.opencode/plugin/lisa-*.ts` module. OpenCode loads project plugins
20
+ * and fires their `tool.execute.before` / `tool.execute.after` hooks under
21
+ * `opencode run` headless (verified-by-run; unlike agy / Copilot).
22
+ *
23
+ * The plugin templates live in `src/opencode/plugin-templates/` and are copied
24
+ * verbatim into the host's `.opencode/plugin/`. Stack-specific plugins are gated
25
+ * by `forProjectTypes` so they ship only when the relevant project type is
26
+ * detected, exactly like the Codex hook catalog.
27
+ * @module opencode/hooks-installer
28
+ */
29
+ import * as fse from "fs-extra";
30
+ import { copyFile, readFile, readdir, rm, writeFile } from "node:fs/promises";
31
+ import * as path from "node:path";
32
+ import { fileURLToPath } from "node:url";
33
+ import { applyEdits, modify, parse as parseJsonc, } from "jsonc-parser";
34
+ import { OPENCODE_CONFIG_DIR } from "./manifest.js";
35
+ import { OPENCODE_SCHEMA_URL } from "./settings-installer.js";
36
+ /** Subdirectory inside `.opencode/` where OpenCode discovers project plugins */
37
+ export const OPENCODE_PLUGIN_SUBDIR = "plugin";
38
+ /** Filename of the OpenCode project config merged at the host root */
39
+ export const OPENCODE_CONFIG_FILENAME = "opencode.json";
40
+ /** Prefix every Lisa-managed plugin filename carries (used for stale cleanup) */
41
+ const LISA_PLUGIN_PREFIX = "lisa-";
42
+ /**
43
+ * `permission.bash` deny patterns that replace Lisa's `block-no-verify` hook.
44
+ * Each glob is matched against the parsed shell command; `*` matches any run of
45
+ * characters. Deny-only (no catch-all allow) so non-matching commands fall
46
+ * through to the host's / OpenCode's default posture.
47
+ */
48
+ const NO_VERIFY_DENY_PATTERNS = {
49
+ "*--no-verify*": "deny",
50
+ "*HUSKY=0*": "deny",
51
+ "*HUSKY_SKIP_HOOKS=*": "deny",
52
+ "*core.hooksPath*/dev/null*": "deny",
53
+ };
54
+ /**
55
+ * Plugin catalog — the OpenCode counterpart of the Codex HOOK_CATALOG. Adding a
56
+ * plugin? Drop a template in `plugin-templates/`, add an entry here, add tests.
57
+ */
58
+ const PLUGIN_CATALOG = [
59
+ {
60
+ id: "session-bootstrap",
61
+ templateFilename: "lisa-session-bootstrap.ts",
62
+ forProjectTypes: ["*"],
63
+ },
64
+ {
65
+ id: "block-suppress-directives",
66
+ templateFilename: "lisa-block-suppress-directives.ts",
67
+ forProjectTypes: ["typescript"],
68
+ },
69
+ {
70
+ id: "lint-on-edit",
71
+ templateFilename: "lisa-lint-on-edit.ts",
72
+ forProjectTypes: ["typescript"],
73
+ },
74
+ {
75
+ id: "sg-scan-on-edit",
76
+ templateFilename: "lisa-sg-scan-on-edit.ts",
77
+ forProjectTypes: ["typescript", "rails"],
78
+ },
79
+ {
80
+ id: "block-migration-edits",
81
+ templateFilename: "lisa-block-migration-edits.ts",
82
+ forProjectTypes: ["nestjs"],
83
+ },
84
+ {
85
+ id: "rubocop-on-edit",
86
+ templateFilename: "lisa-rubocop-on-edit.ts",
87
+ forProjectTypes: ["rails"],
88
+ },
89
+ ];
90
+ /**
91
+ * Install Lisa's OpenCode hooks: merge `permission.bash` deny rules into
92
+ * `opencode.json` and emit the applicable `.opencode/plugin/lisa-*.ts` modules.
93
+ *
94
+ * `opencode.json` lives at the host ROOT (outside `.opencode/`), is a shared
95
+ * merged file, and is intentionally NOT returned in `managedFiles` — the
96
+ * `.opencode/`-relative manifest never deletes it. Plugin files under
97
+ * `.opencode/plugin/` ARE tracked so renames clean up stale modules.
98
+ * @param destDir - Absolute path to the host project root.
99
+ * @param detectedTypes - Project types Lisa detected; gates stack-specific
100
+ * plugins. Universal plugins/config install regardless.
101
+ * @param previousManagedFiles - Files Lisa managed last run (relative to
102
+ * `.opencode/`); used to detect stale plugin modules.
103
+ * @returns Result describing what was written + removed.
104
+ */
105
+ export async function installHooks(destDir, detectedTypes, previousManagedFiles) {
106
+ const configCreated = await mergeOpencodeConfig(destDir);
107
+ const pluginDir = path.join(destDir, OPENCODE_CONFIG_DIR, OPENCODE_PLUGIN_SUBDIR);
108
+ await fse.ensureDir(pluginDir);
109
+ const applicable = filterCatalogByTypes(detectedTypes);
110
+ const managedFiles = await Promise.all(applicable.map(async (entry) => {
111
+ const source = resolveTemplate(entry.templateFilename);
112
+ const dest = path.join(pluginDir, entry.templateFilename);
113
+ await copyFile(source, dest);
114
+ return path.join(OPENCODE_PLUGIN_SUBDIR, entry.templateFilename);
115
+ }));
116
+ const currentFilenames = new Set(applicable.map(entry => entry.templateFilename));
117
+ const deleted = await deleteStalePlugins(previousManagedFiles, currentFilenames, destDir);
118
+ return {
119
+ managedFiles: Object.freeze(managedFiles),
120
+ pluginCount: applicable.length,
121
+ configCreated,
122
+ deleted,
123
+ };
124
+ }
125
+ /**
126
+ * Filter the catalog by detected project types. Universal plugins (`"*"`)
127
+ * always pass; stack-specific plugins pass only if their type is detected.
128
+ * @param detectedTypes - Project types Lisa detected for the host.
129
+ * @returns The catalog entries that apply to this host.
130
+ */
131
+ function filterCatalogByTypes(detectedTypes) {
132
+ const detectedSet = new Set(detectedTypes);
133
+ return PLUGIN_CATALOG.filter(entry => entry.forProjectTypes.some(t => t === "*" || detectedSet.has(t)));
134
+ }
135
+ /**
136
+ * Resolve a bundled plugin template path. Templates ship alongside the compiled
137
+ * installer at `dist/opencode/plugin-templates/<name>` (copied there by
138
+ * `scripts/copy-opencode-plugin-templates.mjs`).
139
+ * @param filename - Template filename (e.g. "lisa-lint-on-edit.ts").
140
+ * @returns Absolute path to the bundled template.
141
+ */
142
+ function resolveTemplate(filename) {
143
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
144
+ return path.join(moduleDir, "plugin-templates", filename);
145
+ }
146
+ /**
147
+ * Delete Lisa plugin modules that were managed last run but aren't shipped this
148
+ * run (e.g. a project type stopped being detected, or a template was renamed).
149
+ * Only files under `.opencode/plugin/` with the `lisa-` prefix are eligible, so
150
+ * host-authored plugins are never touched.
151
+ * @param previousManagedFiles - Files Lisa managed last run (relative to
152
+ * `.opencode/`).
153
+ * @param currentFilenames - Plugin filenames Lisa is shipping this run.
154
+ * @param destDir - Absolute path to the host project root.
155
+ * @returns Sorted list of stale plugin filenames that were deleted.
156
+ */
157
+ async function deleteStalePlugins(previousManagedFiles, currentFilenames, destDir) {
158
+ const prefix = `${OPENCODE_PLUGIN_SUBDIR}${path.sep}`;
159
+ const stale = previousManagedFiles
160
+ .filter(file => file.startsWith(prefix))
161
+ .map(file => file.slice(prefix.length))
162
+ .filter(name => name.startsWith(LISA_PLUGIN_PREFIX) &&
163
+ !name.includes(path.sep) &&
164
+ !currentFilenames.has(name));
165
+ await Promise.all(stale.map(async (name) => {
166
+ const absPath = path.join(destDir, OPENCODE_CONFIG_DIR, OPENCODE_PLUGIN_SUBDIR, name);
167
+ if (await fse.pathExists(absPath)) {
168
+ await rm(absPath, { force: true });
169
+ }
170
+ }));
171
+ return Object.freeze([...new Set(stale)].sort((a, b) => a.localeCompare(b)));
172
+ }
173
+ /** JSONC edit formatting — 2-space indent, matching the repo's JSON style. */
174
+ const FORMATTING_OPTIONS = {
175
+ formattingOptions: { tabSize: 2, insertSpaces: true },
176
+ };
177
+ /**
178
+ * Merge Lisa's `permission.bash` deny rules into the host's `opencode.json`,
179
+ * preserving every other key, comment, and formatting choice. Creates the file
180
+ * (with `$schema`) when absent.
181
+ *
182
+ * Edits the document surgically via `jsonc-parser` — the same host-preserving
183
+ * approach the sibling OpenCode settings/MCP installers use, so the three
184
+ * writers compose into one `opencode.json` without clobbering each other or a
185
+ * host's JSONC comments.
186
+ * @param destDir - Absolute path to the host project root.
187
+ * @returns Whether the config file was created (vs merged into an existing one).
188
+ */
189
+ async function mergeOpencodeConfig(destDir) {
190
+ const configPath = path.join(destDir, OPENCODE_CONFIG_FILENAME);
191
+ const exists = await fse.pathExists(configPath);
192
+ const existing = exists ? await readFile(configPath, "utf8") : "";
193
+ await writeFile(configPath, mergeNoVerifyDenyRules(existing), "utf8");
194
+ return !exists;
195
+ }
196
+ /**
197
+ * Merge the `block-no-verify` deny globs into an `opencode.json` (JSONC) body.
198
+ * Pure function for testability; `mergeOpencodeConfig` is the I/O wrapper.
199
+ *
200
+ * Empty input yields a clean Lisa-authored document. A host `permission.bash`
201
+ * set to a bare string (e.g. `"allow"`) is re-seeded as a `{ "*": <string> }`
202
+ * catch-all so the host's posture survives alongside the deny rules. Otherwise
203
+ * each deny glob is added as its own key, preserving host bash patterns. `$schema`
204
+ * is set only when absent. Throws on invalid JSONC so a corrupt host file is
205
+ * surfaced, not silently overwritten.
206
+ * @param existingJsonc - Current contents of `opencode.json` (or "").
207
+ * @returns Merged JSON/JSONC string with host content preserved.
208
+ */
209
+ export function mergeNoVerifyDenyRules(existingJsonc) {
210
+ if (existingJsonc.trim().length === 0) {
211
+ const fresh = {
212
+ $schema: OPENCODE_SCHEMA_URL,
213
+ permission: { bash: { ...NO_VERIFY_DENY_PATTERNS } },
214
+ };
215
+ return `${JSON.stringify(fresh, null, 2)}\n`;
216
+ }
217
+ const current = parseJsoncOrThrow(existingJsonc);
218
+ const permission = isPlainObject(current["permission"])
219
+ ? current["permission"]
220
+ : {};
221
+ const existingBash = permission["bash"];
222
+ const withBash = typeof existingBash === "string"
223
+ ? // Replace the whole string posture with an object that keeps it as the
224
+ // catch-all and adds the deny globs.
225
+ upsertKey(existingJsonc, ["permission", "bash"], { "*": existingBash, ...NO_VERIFY_DENY_PATTERNS }, undefined)
226
+ : addDenyGlobs(existingJsonc, isPlainObject(existingBash) ? existingBash : undefined);
227
+ const withSchema = current["$schema"] === undefined
228
+ ? upsertKey(withBash, ["$schema"], OPENCODE_SCHEMA_URL, undefined)
229
+ : withBash;
230
+ return withSchema.endsWith("\n") ? withSchema : `${withSchema}\n`;
231
+ }
232
+ /**
233
+ * Add each deny glob under `permission.bash` as an individual key, preserving
234
+ * any host bash patterns (and their comments) already present.
235
+ * @param text - Current document text.
236
+ * @param currentBash - The host's `permission.bash` object, if any.
237
+ * @returns The edited document text.
238
+ */
239
+ function addDenyGlobs(text, currentBash) {
240
+ return Object.entries(NO_VERIFY_DENY_PATTERNS).reduce((acc, [pattern, value]) => upsertKey(acc, ["permission", "bash", pattern], value, currentBash?.[pattern]), text);
241
+ }
242
+ /**
243
+ * Set `value` at `keyPath` via a surgical JSONC edit, skipping the write when
244
+ * the document already holds that value (keeps re-runs no-op-clean).
245
+ * @param text - Current document text.
246
+ * @param keyPath - JSON path to the key.
247
+ * @param value - Value to set.
248
+ * @param currentValue - The value already present at `keyPath` (or undefined).
249
+ * @returns The edited document text.
250
+ */
251
+ function upsertKey(text, keyPath, value, currentValue) {
252
+ if (currentValue === value) {
253
+ return text;
254
+ }
255
+ const edits = modify(text, [...keyPath], value, FORMATTING_OPTIONS);
256
+ return applyEdits(text, edits);
257
+ }
258
+ /**
259
+ * Parse JSONC, throwing on the first syntax error so a corrupt host config is
260
+ * surfaced rather than silently clobbered. Comments and trailing commas are
261
+ * tolerated (OpenCode permits both).
262
+ * @param jsonc - Raw `opencode.json` contents.
263
+ * @returns The parsed object (empty object if the root is a non-object).
264
+ */
265
+ function parseJsoncOrThrow(jsonc) {
266
+ const errors = [];
267
+ const parsed = parseJsonc(jsonc, errors, {
268
+ allowTrailingComma: true,
269
+ disallowComments: false,
270
+ });
271
+ if (errors.length > 0) {
272
+ throw new Error(`Invalid ${OPENCODE_CONFIG_FILENAME} (JSONC syntax error at offset ${errors[0]?.offset ?? 0}); refusing to overwrite host config`);
273
+ }
274
+ return isPlainObject(parsed) ? parsed : {};
275
+ }
276
+ /**
277
+ * Narrow an unknown value to a plain (non-array, non-null) object record.
278
+ * @param value - The value to test.
279
+ * @returns Whether `value` is a plain object.
280
+ */
281
+ function isPlainObject(value) {
282
+ return typeof value === "object" && value !== null && !Array.isArray(value);
283
+ }
284
+ /**
285
+ * Enumerate the Lisa plugin filenames currently present in a host's
286
+ * `.opencode/plugin/` directory. Exposed for tests/diagnostics.
287
+ * @param destDir - Absolute path to the host project root.
288
+ * @returns Sorted list of `lisa-*.ts` filenames (empty if the dir is absent).
289
+ */
290
+ export async function listInstalledPluginFiles(destDir) {
291
+ const pluginDir = path.join(destDir, OPENCODE_CONFIG_DIR, OPENCODE_PLUGIN_SUBDIR);
292
+ if (!(await fse.pathExists(pluginDir))) {
293
+ return [];
294
+ }
295
+ const entries = await readdir(pluginDir);
296
+ return entries
297
+ .filter(name => name.startsWith(LISA_PLUGIN_PREFIX) && name.endsWith(".ts"))
298
+ .sort((a, b) => a.localeCompare(b));
299
+ }
300
+ //# sourceMappingURL=hooks-installer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks-installer.js","sourceRoot":"","sources":["../../src/opencode/hooks-installer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Lisa-managed OpenCode plugin (tool.execute.before).
3
+ *
4
+ * Blocks edits to TypeORM migration files. Use `bun run migration:generate`
5
+ * to regenerate from entity diffs instead — hand-written migrations drift from
6
+ * entity metadata and break the schema/migration contract.
7
+ *
8
+ * Port of Lisa's Codex hook `block-migration-edits.sh`. OpenCode exposes only
9
+ * `edit` / `write` filesystem tools (no `apply_patch`), so the file path comes
10
+ * straight from `output.args.filePath`. Throwing in `tool.execute.before`
11
+ * cancels the tool call and surfaces the message to the agent (verified-by-run
12
+ * on opencode 1.16.2).
13
+ *
14
+ * NOTE: This file is a template Lisa copies verbatim into a host project's
15
+ * `.opencode/plugin/`. It is intentionally excluded from this repo's tsconfig
16
+ * and eslint config — it runs under OpenCode's Bun runtime, not here.
17
+ */
18
+ export /**
19
+ *
20
+ */
21
+ const LisaBlockMigrationEdits = async () => {
22
+ const MIGRATION_RE = /\/migrations\/[^/]*\d[^/]*-[^/]*\.ts$/;
23
+ return {
24
+ "tool.execute.before": async (
25
+ input: { tool: string },
26
+ output: { args?: { filePath?: string } }
27
+ ) => {
28
+ if (input.tool !== "edit" && input.tool !== "write") return;
29
+ const filePath = String(output.args?.filePath ?? "");
30
+ if (!filePath || !MIGRATION_RE.test(filePath)) return;
31
+ throw new Error(
32
+ [
33
+ `block-migration-edits: refusing to modify ${filePath}.`,
34
+ "",
35
+ "TypeORM migrations must be regenerated from entity diffs:",
36
+ " bun run migration:generate -- src/database/migrations/<descriptive-name>",
37
+ "",
38
+ "Hand-written migrations drift from entity metadata and break the",
39
+ "schema contract. Modify the entity, run the generator, then commit.",
40
+ ].join("\n")
41
+ );
42
+ },
43
+ };
44
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Lisa-managed OpenCode plugin (tool.execute.before).
3
+ *
4
+ * Blocks adding error-suppression directives (@ts-ignore, @ts-nocheck,
5
+ * eslint-disable, biome-ignore, prettier-ignore) to JS/TS source. Suppressing
6
+ * the type checker, linter, or formatter hides real defects — fix the
7
+ * underlying error. @ts-expect-error is intentionally NOT matched (it is the
8
+ * safer alternative).
9
+ *
10
+ * Port of Lisa's Codex hook `block-suppress-directives.sh`. OpenCode exposes
11
+ * `edit` (oldString/newString) and `write` (content) tools — there is no
12
+ * multi-file apply_patch — so the new text comes straight from the tool args.
13
+ * Throwing in `tool.execute.before` cancels the call and surfaces the message
14
+ * to the agent (verified-by-run on opencode 1.16.2).
15
+ *
16
+ * NOTE: This file is a template Lisa copies verbatim into a host project's
17
+ * `.opencode/plugin/`. It is intentionally excluded from this repo's tsconfig
18
+ * and eslint config — it runs under OpenCode's Bun runtime, not here.
19
+ */
20
+ export const LisaBlockSuppressDirectives = async () => {
21
+ // Comment-syntax-only match: // or /* opener, optional whitespace, directive.
22
+ const DIRECTIVE_RE =
23
+ /(\/\/|\/\*)\s*(@ts-(ignore|nocheck)|eslint-disable|biome-ignore|prettier-ignore)/;
24
+ const JS_TS = new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs"]);
25
+ const extOf = (p: string) => p.split(".").pop()?.toLowerCase() ?? "";
26
+ return {
27
+ "tool.execute.before": async (
28
+ input: { tool: string },
29
+ output: {
30
+ args?: { filePath?: string; content?: string; newString?: string };
31
+ }
32
+ ) => {
33
+ if (input.tool !== "edit" && input.tool !== "write") return;
34
+ const filePath = String(output.args?.filePath ?? "");
35
+ if (!filePath || !JS_TS.has(extOf(filePath))) return;
36
+ const newText =
37
+ input.tool === "write"
38
+ ? String(output.args?.content ?? "")
39
+ : String(output.args?.newString ?? "");
40
+ if (!DIRECTIVE_RE.test(newText)) return;
41
+ throw new Error(
42
+ [
43
+ `block-suppress-directives: refusing to add an error-suppression directive to ${filePath}.`,
44
+ "",
45
+ "@ts-ignore / @ts-nocheck / eslint-disable / biome-ignore / prettier-ignore",
46
+ "silence the type checker, linter, or formatter instead of fixing the problem.",
47
+ "Fix the underlying type/lint error — add the missing annotation, narrow the",
48
+ "type, or restructure the code so the rule passes.",
49
+ "",
50
+ "Suppression is a last resort. If there is genuinely no other way, STOP and get",
51
+ "the user's approval first, prefer @ts-expect-error over @ts-ignore, scope the",
52
+ 'disable to one line and one rule, and add a "-- <reason>" description.',
53
+ ].join("\n")
54
+ );
55
+ },
56
+ };
57
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Lisa-managed OpenCode plugin (tool.execute.after).
3
+ *
4
+ * Runs ESLint --fix on every just-edited JS/TS file, then re-checks. If
5
+ * unfixable problems remain, throws so OpenCode marks the tool call failed and
6
+ * the agent self-corrects (the equivalent of the Codex hook's non-zero exit).
7
+ * Fails open when ESLint isn't installed.
8
+ *
9
+ * Port of Lisa's Codex hook `lint-on-edit.sh`. OpenCode passes the edited file
10
+ * via `input.args.filePath`; `tool.execute.after` runs after a successful
11
+ * edit/write (verified-by-run on opencode 1.16.2: a throw here surfaces the
12
+ * message to the agent).
13
+ *
14
+ * NOTE: This file is a template Lisa copies verbatim into a host project's
15
+ * `.opencode/plugin/`. It is intentionally excluded from this repo's tsconfig
16
+ * and eslint config — it runs under OpenCode's Bun runtime, not here.
17
+ */
18
+ export const LisaLintOnEdit = async ({
19
+ $,
20
+ directory,
21
+ }: {
22
+ $: (strings: TemplateStringsArray, ...exprs: unknown[]) => any;
23
+ directory: string;
24
+ }) => {
25
+ const EXTS = new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs"]);
26
+ const extOf = (p: string) => p.split(".").pop()?.toLowerCase() ?? "";
27
+ const { existsSync } = await import("node:fs");
28
+ const resolveBin = (name: string): string | null => {
29
+ const local = `${directory}/node_modules/.bin/${name}`;
30
+ if (existsSync(local)) return local;
31
+ return Bun.which(name);
32
+ };
33
+ return {
34
+ "tool.execute.after": async (input: {
35
+ tool: string;
36
+ args?: { filePath?: string };
37
+ }) => {
38
+ if (input.tool !== "edit" && input.tool !== "write") return;
39
+ const filePath = String(input.args?.filePath ?? "");
40
+ if (!filePath || !EXTS.has(extOf(filePath))) return;
41
+ const eslint = resolveBin("eslint");
42
+ if (!eslint) return; // fail open — no ESLint installed
43
+ await $`${eslint} --fix ${filePath}`.quiet().nothrow();
44
+ const res = await $`${eslint} --quiet ${filePath}`.quiet().nothrow();
45
+ if (res.exitCode === 0) return;
46
+ const out =
47
+ `${res.stdout?.toString() ?? ""}${res.stderr?.toString() ?? ""}`.trim();
48
+ throw new Error(
49
+ `lint-on-edit: ESLint reported unfixable problems in ${filePath}:\n${out}`
50
+ );
51
+ },
52
+ };
53
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Lisa-managed OpenCode plugin (tool.execute.after).
3
+ *
4
+ * Runs RuboCop -a (safe autocorrect) on every just-edited Ruby file, then
5
+ * re-checks for remaining unfixable offenses. If any remain, throws so OpenCode
6
+ * marks the tool call failed and the agent fixes them. Prefers `bundle exec
7
+ * rubocop` when a Gemfile is present. Fails open when RuboCop isn't installed.
8
+ *
9
+ * Port of Lisa's Codex hook `rubocop-on-edit.sh`.
10
+ *
11
+ * NOTE: This file is a template Lisa copies verbatim into a host project's
12
+ * `.opencode/plugin/`. It is intentionally excluded from this repo's tsconfig
13
+ * and eslint config — it runs under OpenCode's Bun runtime, not here.
14
+ */
15
+ export const LisaRubocopOnEdit = async ({
16
+ $,
17
+ directory,
18
+ }: {
19
+ $: (strings: TemplateStringsArray, ...exprs: unknown[]) => any;
20
+ directory: string;
21
+ }) => {
22
+ const EXTS = new Set(["rb", "rake"]);
23
+ const extOf = (p: string) => p.split(".").pop()?.toLowerCase() ?? "";
24
+ const { existsSync } = await import("node:fs");
25
+ const resolveRunner = (): string[] | null => {
26
+ const bundle = Bun.which("bundle");
27
+ if (bundle && existsSync(`${directory}/Gemfile`)) {
28
+ return [bundle, "exec", "rubocop"];
29
+ }
30
+ const rubocop = Bun.which("rubocop");
31
+ return rubocop ? [rubocop] : null;
32
+ };
33
+ return {
34
+ "tool.execute.after": async (input: {
35
+ tool: string;
36
+ args?: { filePath?: string };
37
+ }) => {
38
+ if (input.tool !== "edit" && input.tool !== "write") return;
39
+ const filePath = String(input.args?.filePath ?? "");
40
+ if (!filePath || !EXTS.has(extOf(filePath))) return;
41
+ const runner = resolveRunner();
42
+ if (!runner) return; // fail open — no RuboCop installed
43
+ const [bin, ...rest] = runner;
44
+ await $`${bin} ${rest} -a ${filePath}`.quiet().nothrow();
45
+ const res = await $`${bin} ${rest} ${filePath}`.quiet().nothrow();
46
+ if (res.exitCode === 0) return;
47
+ const out =
48
+ `${res.stdout?.toString() ?? ""}${res.stderr?.toString() ?? ""}`.trim();
49
+ throw new Error(
50
+ `rubocop-on-edit: RuboCop reported unfixable offenses in ${filePath}:\n${out}`
51
+ );
52
+ },
53
+ };
54
+ };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Lisa-managed OpenCode plugin (session bootstrap).
3
+ *
4
+ * OpenCode runs a plugin's factory function once when it loads the plugin at
5
+ * session start, which is the natural home for Lisa's Codex SessionStart hooks:
6
+ * - install-pkgs.sh → install dependencies when node_modules is missing
7
+ * - setup-jira-cli.sh → write jira-cli config from environment variables
8
+ *
9
+ * Both are fully fail-open (wrapped in try/catch) so a package-manager or
10
+ * filesystem hiccup never bricks OpenCode startup, mirroring the Codex scripts.
11
+ * install only runs on the first session of a fresh checkout (node_modules
12
+ * absent), so the common case is a cheap no-op.
13
+ *
14
+ * NOTE: This file is a template Lisa copies verbatim into a host project's
15
+ * `.opencode/plugin/`. It is intentionally excluded from this repo's tsconfig
16
+ * and eslint config — it runs under OpenCode's Bun runtime, not here.
17
+ */
18
+ export const LisaSessionBootstrap = async ({
19
+ $,
20
+ worktree,
21
+ }: {
22
+ $: (strings: TemplateStringsArray, ...exprs: unknown[]) => any;
23
+ worktree: string;
24
+ }) => {
25
+ const root = worktree;
26
+ const { existsSync, mkdirSync, writeFileSync } = await import("node:fs");
27
+
28
+ // install-pkgs: bootstrap dependencies when they're missing.
29
+ try {
30
+ if (
31
+ existsSync(`${root}/package.json`) &&
32
+ !existsSync(`${root}/node_modules`)
33
+ ) {
34
+ const has = (f: string) => existsSync(`${root}/${f}`);
35
+ const install = async (cmd: string) => {
36
+ if (Bun.which(cmd)) {
37
+ await $`${cmd} install`.cwd(root).quiet().nothrow();
38
+ }
39
+ };
40
+ if (has("bun.lockb") || has("bun.lock")) await install("bun");
41
+ else if (has("pnpm-lock.yaml")) await install("pnpm");
42
+ else if (has("yarn.lock")) await install("yarn");
43
+ else await install("npm");
44
+ }
45
+ } catch {
46
+ // fail open — never block startup on a dependency-install error
47
+ }
48
+
49
+ // setup-jira-cli: write jira-cli config from environment variables.
50
+ try {
51
+ const server = process.env.JIRA_SERVER;
52
+ const login = process.env.JIRA_LOGIN;
53
+ const home = process.env.HOME;
54
+ if (server && login && home) {
55
+ const dir = `${home}/.config/.jira`;
56
+ mkdirSync(dir, { recursive: true });
57
+ const config = [
58
+ `installation: ${process.env.JIRA_INSTALLATION ?? "cloud"}`,
59
+ `server: ${server}`,
60
+ `login: ${login}`,
61
+ `project: ${process.env.JIRA_PROJECT ?? ""}`,
62
+ `board: "${process.env.JIRA_BOARD ?? ""}"`,
63
+ "auth_type: basic",
64
+ "epic:",
65
+ " name: Epic Name",
66
+ " link: Epic Link",
67
+ "",
68
+ ].join("\n");
69
+ writeFileSync(`${dir}/.config.yml`, config);
70
+ }
71
+ } catch {
72
+ // fail open — never block startup on a jira-cli config error
73
+ }
74
+
75
+ return {};
76
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Lisa-managed OpenCode plugin (tool.execute.after).
3
+ *
4
+ * Runs `ast-grep scan` on every just-edited source file (TypeScript/JS or
5
+ * Ruby) when the project ships an `sgconfig.yml`. If ast-grep reports findings,
6
+ * throws so OpenCode marks the tool call failed and the agent fixes them.
7
+ * Fails open when ast-grep or sgconfig.yml is absent.
8
+ *
9
+ * Port of Lisa's Codex hook `sg-scan-on-edit.sh`.
10
+ *
11
+ * NOTE: This file is a template Lisa copies verbatim into a host project's
12
+ * `.opencode/plugin/`. It is intentionally excluded from this repo's tsconfig
13
+ * and eslint config — it runs under OpenCode's Bun runtime, not here.
14
+ */
15
+ export const LisaSgScanOnEdit = async ({
16
+ $,
17
+ directory,
18
+ }: {
19
+ $: (strings: TemplateStringsArray, ...exprs: unknown[]) => any;
20
+ directory: string;
21
+ }) => {
22
+ const EXTS = new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "rb", "rake"]);
23
+ const extOf = (p: string) => p.split(".").pop()?.toLowerCase() ?? "";
24
+ const { existsSync } = await import("node:fs");
25
+ const resolveBin = (): string | null => {
26
+ const local = `${directory}/node_modules/.bin/ast-grep`;
27
+ if (existsSync(local)) return local;
28
+ return Bun.which("ast-grep") ?? Bun.which("sg");
29
+ };
30
+ return {
31
+ "tool.execute.after": async (input: {
32
+ tool: string;
33
+ args?: { filePath?: string };
34
+ }) => {
35
+ if (input.tool !== "edit" && input.tool !== "write") return;
36
+ const filePath = String(input.args?.filePath ?? "");
37
+ if (!filePath || !EXTS.has(extOf(filePath))) return;
38
+ if (!existsSync(`${directory}/sgconfig.yml`)) return; // fail open
39
+ const astGrep = resolveBin();
40
+ if (!astGrep) return; // fail open — no ast-grep installed
41
+ const res = await $`${astGrep} scan ${filePath}`.quiet().nothrow();
42
+ if (res.exitCode === 0) return;
43
+ const out =
44
+ `${res.stdout?.toString() ?? ""}${res.stderr?.toString() ?? ""}`.trim();
45
+ throw new Error(
46
+ `sg-scan-on-edit: ast-grep reported findings in ${filePath}:\n${out}`
47
+ );
48
+ },
49
+ };
50
+ };