@helloleo/plugins 0.1.0

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/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # @helloleo/plugins
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To run:
10
+
11
+ ```bash
12
+ bun run src/index.ts
13
+ ```
14
+
15
+ This project was created using `bun init` in bun v1.3.9. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@helloleo/plugins",
3
+ "version": "0.1.0",
4
+ "description": "OpenCode plugins for HelloLeo",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "files": ["src"],
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "scripts": {
15
+ "format": "bunx --bun @biomejs/biome format --write src",
16
+ "lint": "bunx --bun @biomejs/biome lint --write src",
17
+ "check": "bunx --bun @biomejs/biome check --write src"
18
+ },
19
+ "dependencies": {
20
+ "@opencode-ai/plugin": "1.2.27"
21
+ },
22
+ "devDependencies": {
23
+ "@biomejs/biome": "2.4.8",
24
+ "@types/bun": "latest"
25
+ },
26
+ "peerDependencies": {
27
+ "typescript": "^5"
28
+ }
29
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { CreatePlanPlugin } from "./tools/create-plan.ts";
2
+ export { ListPlansPlugin } from "./tools/list-plans.ts";
3
+ export { ModifyPlanPlugin } from "./tools/modify-plan.ts";
4
+ export { RemoveFilePlugin } from "./tools/remove-file.ts";
5
+ export { ScaffoldPlugin } from "./tools/scaffold.ts";
@@ -0,0 +1,61 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { Plugin } from "@opencode-ai/plugin";
4
+ import { tool } from "@opencode-ai/plugin";
5
+
6
+ function slug(title: string) {
7
+ return title
8
+ .toLowerCase()
9
+ .replace(/[^a-z0-9]+/g, "-")
10
+ .replace(/^-|-$/g, "")
11
+ .slice(0, 60);
12
+ }
13
+
14
+ function frontmatter(fields: Record<string, string>) {
15
+ const lines = Object.entries(fields).map(([k, v]) => `${k}: ${v}`);
16
+ return `---\n${lines.join("\n")}\n---`;
17
+ }
18
+
19
+ export const createPlan = tool({
20
+ description:
21
+ "Create a new plan file in .leo/plans/. Plans use markdown with YAML frontmatter " +
22
+ "tracking title, status, and timestamps. The plan ID is a slug derived from the title.",
23
+ args: {
24
+ title: tool.schema.string().describe("Title of the plan"),
25
+ content: tool.schema.string().describe("Markdown body of the plan"),
26
+ },
27
+ async execute(args, context) {
28
+ const dir = path.join(context.directory, ".leo", "plans");
29
+ await mkdir(dir, { recursive: true });
30
+
31
+ let id = slug(args.title);
32
+ let filepath = path.join(dir, `${id}.md`);
33
+ let suffix = 1;
34
+
35
+ while (await Bun.file(filepath).exists()) {
36
+ suffix++;
37
+ id = `${slug(args.title)}-${suffix}`;
38
+ filepath = path.join(dir, `${id}.md`);
39
+ }
40
+
41
+ const now = new Date().toISOString();
42
+ const header = frontmatter({
43
+ title: args.title,
44
+ status: "active",
45
+ created: now,
46
+ modified: now,
47
+ });
48
+
49
+ const body = `${header}\n\n${args.content}\n`;
50
+ await Bun.write(filepath, body);
51
+
52
+ const relative = path.relative(context.directory, filepath);
53
+ return `Created plan "${args.title}" at ${relative}\n\n${body}`;
54
+ },
55
+ });
56
+
57
+ export const CreatePlanPlugin: Plugin = async () => {
58
+ return {
59
+ tool: { "create-plan": createPlan },
60
+ };
61
+ };
@@ -0,0 +1,85 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import type { Plugin } from "@opencode-ai/plugin";
4
+ import { tool } from "@opencode-ai/plugin";
5
+
6
+ const STATUS = ["active", "completed", "cancelled"] as const;
7
+
8
+ function parseFrontmatter(raw: string) {
9
+ const match = raw.match(/^---\n([\s\S]*?)\n---/);
10
+ if (!match) return {} as Record<string, string>;
11
+
12
+ const fields: Record<string, string> = {};
13
+ for (const line of (match[1] ?? "").split("\n")) {
14
+ const idx = line.indexOf(":");
15
+ if (idx === -1) continue;
16
+ fields[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
17
+ }
18
+
19
+ return fields;
20
+ }
21
+
22
+ export const listPlans = tool({
23
+ description:
24
+ "List all plans in .leo/plans/. Returns each plan's slug, title, status, and last modified date. " +
25
+ "Optionally filter by status.",
26
+ args: {
27
+ status: tool.schema
28
+ .enum(STATUS)
29
+ .optional()
30
+ .describe(`Filter plans by status: ${STATUS.join(", ")}`),
31
+ },
32
+ async execute(args, context) {
33
+ const dir = path.join(context.directory, ".leo", "plans");
34
+
35
+ if (!existsSync(dir)) {
36
+ return args.status
37
+ ? `No plans with status "${args.status}"`
38
+ : "No plans found";
39
+ }
40
+
41
+ const glob = new Bun.Glob("*.md");
42
+ const entries: {
43
+ id: string;
44
+ title: string;
45
+ status: string;
46
+ modified: string;
47
+ }[] = [];
48
+
49
+ for await (const match of glob.scan({ cwd: dir, absolute: true })) {
50
+ const raw = await Bun.file(match).text();
51
+ const fields = parseFrontmatter(raw);
52
+ const id = path.basename(match, ".md");
53
+
54
+ if (args.status && fields.status !== args.status) continue;
55
+
56
+ entries.push({
57
+ id,
58
+ title: fields.title ?? id,
59
+ status: fields.status ?? "unknown",
60
+ modified: fields.modified ?? fields.created ?? "unknown",
61
+ });
62
+ }
63
+
64
+ if (entries.length === 0) {
65
+ return args.status
66
+ ? `No plans with status "${args.status}"`
67
+ : "No plans found";
68
+ }
69
+
70
+ entries.sort((a, b) => (b.modified > a.modified ? 1 : -1));
71
+
72
+ const header = `Found ${entries.length} plan${entries.length === 1 ? "" : "s"}:\n`;
73
+ const rows = entries.map(
74
+ (e) => `${e.id} | ${e.status} | ${e.title} | modified ${e.modified}`,
75
+ );
76
+
77
+ return header + rows.join("\n");
78
+ },
79
+ });
80
+
81
+ export const ListPlansPlugin: Plugin = async () => {
82
+ return {
83
+ tool: { "list-plans": listPlans },
84
+ };
85
+ };
@@ -0,0 +1,79 @@
1
+ import path from "node:path";
2
+ import type { Plugin } from "@opencode-ai/plugin";
3
+ import { tool } from "@opencode-ai/plugin";
4
+
5
+ const STATUS = ["active", "completed", "cancelled"] as const;
6
+
7
+ function parseFrontmatter(raw: string) {
8
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
9
+ if (!match) return { fields: {} as Record<string, string>, body: raw };
10
+
11
+ const fields: Record<string, string> = {};
12
+ for (const line of (match[1] ?? "").split("\n")) {
13
+ const idx = line.indexOf(":");
14
+ if (idx === -1) continue;
15
+ fields[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
16
+ }
17
+
18
+ return { fields, body: (match[2] ?? "").trim() };
19
+ }
20
+
21
+ function formatFrontmatter(fields: Record<string, string>, body: string) {
22
+ const lines = Object.entries(fields).map(([k, v]) => `${k}: ${v}`);
23
+ return `---\n${lines.join("\n")}\n---\n\n${body}\n`;
24
+ }
25
+
26
+ export const modifyPlan = tool({
27
+ description:
28
+ "Modify an existing plan in .leo/plans/. You can update the content, the status, or both. " +
29
+ "The modified timestamp is updated automatically.",
30
+ args: {
31
+ planId: tool.schema
32
+ .string()
33
+ .describe("Plan slug ID (e.g. refactor-auth-flow)"),
34
+ content: tool.schema
35
+ .string()
36
+ .optional()
37
+ .describe("New markdown body for the plan"),
38
+ status: tool.schema
39
+ .enum(STATUS)
40
+ .optional()
41
+ .describe("New status for the plan"),
42
+ },
43
+ async execute(args, context) {
44
+ if (!args.content && !args.status) {
45
+ throw new Error("Provide at least one of content or status to modify");
46
+ }
47
+
48
+ const filepath = path.join(
49
+ context.directory,
50
+ ".leo",
51
+ "plans",
52
+ `${args.planId}.md`,
53
+ );
54
+ const file = Bun.file(filepath);
55
+
56
+ if (!(await file.exists())) {
57
+ throw new Error(`Plan not found: ${args.planId}`);
58
+ }
59
+
60
+ const raw = await file.text();
61
+ const parsed = parseFrontmatter(raw);
62
+
63
+ if (args.status) parsed.fields.status = args.status;
64
+ parsed.fields.modified = new Date().toISOString();
65
+
66
+ const body = args.content ?? parsed.body;
67
+ const output = formatFrontmatter(parsed.fields, body);
68
+ await Bun.write(filepath, output);
69
+
70
+ const relative = path.relative(context.directory, filepath);
71
+ return `Updated plan "${args.planId}" at ${relative}\n\n${output}`;
72
+ },
73
+ });
74
+
75
+ export const ModifyPlanPlugin: Plugin = async () => {
76
+ return {
77
+ tool: { "modify-plan": modifyPlan },
78
+ };
79
+ };
@@ -0,0 +1,53 @@
1
+ import { unlink } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { Plugin } from "@opencode-ai/plugin";
4
+ import { tool } from "@opencode-ai/plugin";
5
+
6
+ export const removeFile = tool({
7
+ description:
8
+ "Remove a file from the project. The file path must be relative to the project root. " +
9
+ "This tool will ask for permission before deleting. " +
10
+ "Cannot delete files outside the project boundary.",
11
+ args: {
12
+ filePath: tool.schema
13
+ .string()
14
+ .describe("Relative path to the file to remove (e.g. src/App.tsx)"),
15
+ },
16
+ async execute(args, context) {
17
+ const resolved = path.resolve(context.directory, args.filePath);
18
+ const normalized = path.normalize(resolved);
19
+
20
+ if (
21
+ !normalized.startsWith(context.directory + path.sep) &&
22
+ normalized !== context.directory
23
+ ) {
24
+ throw new Error(
25
+ `Path "${args.filePath}" resolves outside the project boundary. Deletion blocked.`,
26
+ );
27
+ }
28
+
29
+ const file = Bun.file(resolved);
30
+ if (!(await file.exists())) {
31
+ throw new Error(`File not found: ${args.filePath}`);
32
+ }
33
+
34
+ const relative = path.relative(context.directory, resolved);
35
+
36
+ await context.ask({
37
+ permission: "remove-file",
38
+ patterns: [resolved],
39
+ always: [resolved],
40
+ metadata: { filePath: relative },
41
+ });
42
+
43
+ await unlink(resolved);
44
+
45
+ return `Removed ${relative}`;
46
+ },
47
+ });
48
+
49
+ export const RemoveFilePlugin: Plugin = async () => {
50
+ return {
51
+ tool: { "remove-file": removeFile },
52
+ };
53
+ };
@@ -0,0 +1,300 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { Plugin } from "@opencode-ai/plugin";
4
+ import { tool } from "@opencode-ai/plugin";
5
+
6
+ function packageJson(name: string) {
7
+ return JSON.stringify(
8
+ {
9
+ name,
10
+ private: true,
11
+ version: "0.0.0",
12
+ type: "module",
13
+ scripts: {
14
+ dev: "vite",
15
+ start: "vite",
16
+ build: "vite build",
17
+ preview: "vite preview",
18
+ },
19
+ dependencies: {
20
+ react: "^18.3.1",
21
+ "react-dom": "^18.3.1",
22
+ clsx: "^2.1.1",
23
+ "tailwind-merge": "^2.6.0",
24
+ },
25
+ devDependencies: {
26
+ "@types/react": "^18.3.12",
27
+ "@types/react-dom": "^18.3.1",
28
+ "@vitejs/plugin-react": "^4.3.4",
29
+ autoprefixer: "^10.4.20",
30
+ postcss: "^8.4.49",
31
+ tailwindcss: "^3.4.17",
32
+ typescript: "^5.6.3",
33
+ vite: "^6.0.5",
34
+ },
35
+ },
36
+ null,
37
+ 2,
38
+ );
39
+ }
40
+
41
+ function indexHtml(fontFamily: string) {
42
+ const fontParam = fontFamily.replace(/\s+/g, "+");
43
+ return `<!DOCTYPE html>
44
+ <html lang="en">
45
+ <head>
46
+ <meta charset="UTF-8" />
47
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
48
+ <link rel="preconnect" href="https://fonts.googleapis.com">
49
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
50
+ <link href="https://fonts.googleapis.com/css2?family=${fontParam}:wght@400;500;600;700&display=swap" rel="stylesheet">
51
+ <title>App</title>
52
+ </head>
53
+ <body>
54
+ <div id="root"></div>
55
+ <script type="module" src="/src/main.tsx"></script>
56
+ </body>
57
+ </html>`;
58
+ }
59
+
60
+ function viteConfig() {
61
+ return `import { defineConfig } from 'vite'
62
+ import react from '@vitejs/plugin-react'
63
+ import path from 'path'
64
+
65
+ export default defineConfig({
66
+ plugins: [react()],
67
+ server: {
68
+ allowedHosts: true,
69
+ },
70
+ resolve: {
71
+ alias: {
72
+ '@': path.resolve(__dirname, './src'),
73
+ },
74
+ },
75
+ })`;
76
+ }
77
+
78
+ function tailwindConfig() {
79
+ return `export default {
80
+ darkMode: ["class"],
81
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
82
+ theme: {
83
+ extend: {
84
+ colors: {
85
+ background: "hsl(var(--background))",
86
+ foreground: "hsl(var(--foreground))",
87
+ card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },
88
+ primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
89
+ secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },
90
+ muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
91
+ accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },
92
+ destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
93
+ success: { DEFAULT: "hsl(var(--success))", foreground: "hsl(var(--success-foreground))" },
94
+ warning: { DEFAULT: "hsl(var(--warning))", foreground: "hsl(var(--warning-foreground))" },
95
+ border: "hsl(var(--border))",
96
+ input: "hsl(var(--input))",
97
+ ring: "hsl(var(--ring))",
98
+ },
99
+ fontFamily: {
100
+ sans: "var(--font-sans)",
101
+ heading: "var(--font-heading)",
102
+ },
103
+ borderRadius: {
104
+ lg: "var(--radius)",
105
+ md: "calc(var(--radius) - 2px)",
106
+ sm: "calc(var(--radius) - 4px)",
107
+ },
108
+ borderWidth: {
109
+ DEFAULT: "var(--border-width)",
110
+ },
111
+ boxShadow: {
112
+ DEFAULT: "var(--shadow)",
113
+ lg: "var(--shadow-lg)",
114
+ },
115
+ },
116
+ },
117
+ plugins: [],
118
+ }`;
119
+ }
120
+
121
+ function postcssConfig() {
122
+ return `export default {
123
+ plugins: {
124
+ tailwindcss: {},
125
+ autoprefixer: {},
126
+ },
127
+ }`;
128
+ }
129
+
130
+ function indexCss(
131
+ primaryColor: string,
132
+ fontFamily: string,
133
+ borderRadius: string,
134
+ ) {
135
+ const fontValue = `'${fontFamily}', ui-sans-serif, system-ui, sans-serif`;
136
+ return `@tailwind base;
137
+ @tailwind components;
138
+ @tailwind utilities;
139
+
140
+ @layer base {
141
+ :root {
142
+ --background: 0 0% 100%;
143
+ --foreground: 222.2 84% 4.9%;
144
+ --card: 0 0% 100%;
145
+ --card-foreground: 222.2 84% 4.9%;
146
+ --primary: ${primaryColor};
147
+ --primary-foreground: 210 40% 98%;
148
+ --secondary: 210 40% 96.1%;
149
+ --secondary-foreground: 222.2 47.4% 11.2%;
150
+ --muted: 210 40% 96.1%;
151
+ --muted-foreground: 215.4 16.3% 46.9%;
152
+ --accent: 210 40% 96.1%;
153
+ --accent-foreground: 222.2 47.4% 11.2%;
154
+ --destructive: 0 84.2% 60.2%;
155
+ --destructive-foreground: 210 40% 98%;
156
+ --success: 142 76% 36%;
157
+ --success-foreground: 210 40% 98%;
158
+ --warning: 38 92% 50%;
159
+ --warning-foreground: 222.2 47.4% 11.2%;
160
+ --border: 214.3 31.8% 91.4%;
161
+ --input: 214.3 31.8% 91.4%;
162
+ --ring: 221.2 83.2% 53.3%;
163
+ --radius: ${borderRadius};
164
+ --border-width: 1px;
165
+ --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
166
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
167
+ --font-sans: ${fontValue};
168
+ --font-heading: ${fontValue};
169
+ }
170
+
171
+ .dark {
172
+ --background: 222.2 84% 4.9%;
173
+ --foreground: 210 40% 98%;
174
+ --card: 222.2 84% 4.9%;
175
+ --card-foreground: 210 40% 98%;
176
+ --primary: 217.2 91.2% 59.8%;
177
+ --primary-foreground: 222.2 47.4% 11.2%;
178
+ --secondary: 217.2 32.6% 17.5%;
179
+ --secondary-foreground: 210 40% 98%;
180
+ --muted: 217.2 32.6% 17.5%;
181
+ --muted-foreground: 215 20.2% 65.1%;
182
+ --accent: 217.2 32.6% 17.5%;
183
+ --accent-foreground: 210 40% 98%;
184
+ --destructive: 0 62.8% 30.6%;
185
+ --destructive-foreground: 210 40% 98%;
186
+ --success: 142 76% 36%;
187
+ --success-foreground: 210 40% 98%;
188
+ --warning: 38 92% 50%;
189
+ --warning-foreground: 222.2 47.4% 11.2%;
190
+ --border: 217.2 32.6% 17.5%;
191
+ --input: 217.2 32.6% 17.5%;
192
+ --ring: 224.3 76.3% 48%;
193
+ --border-width: 1px;
194
+ --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.3);
195
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.3);
196
+ --font-sans: ${fontValue};
197
+ --font-heading: ${fontValue};
198
+ }
199
+ }
200
+
201
+ body {
202
+ @apply bg-background text-foreground font-sans;
203
+ }`;
204
+ }
205
+
206
+ function utilsTs() {
207
+ return `import { clsx, type ClassValue } from 'clsx'
208
+ import { twMerge } from 'tailwind-merge'
209
+
210
+ export function cn(...inputs: ClassValue[]) {
211
+ return twMerge(clsx(inputs))
212
+ }`;
213
+ }
214
+
215
+ function mainTsx() {
216
+ return `import React from 'react'
217
+ import ReactDOM from 'react-dom/client'
218
+ import App from './App'
219
+ import './index.css'
220
+
221
+ ReactDOM.createRoot(document.getElementById('root')!).render(
222
+ <React.StrictMode>
223
+ <App />
224
+ </React.StrictMode>,
225
+ )`;
226
+ }
227
+
228
+ export const scaffold = tool({
229
+ description:
230
+ "Scaffold a new React + Vite + Tailwind CSS project with the HelloLeo design system. " +
231
+ "Creates all mandatory boilerplate: package.json, index.html (with Google Fonts), " +
232
+ "vite.config.js, tailwind.config.js, postcss.config.js, src/index.css (CSS variables " +
233
+ "light+dark), src/lib/utils.ts (cn helper), and src/main.tsx. " +
234
+ "Call this FIRST for any new project before writing App.tsx and components.",
235
+ args: {
236
+ name: tool.schema
237
+ .string()
238
+ .optional()
239
+ .describe("Project name for package.json (default: leo-project)"),
240
+ primaryColor: tool.schema
241
+ .string()
242
+ .optional()
243
+ .describe(
244
+ 'HSL values for primary color, e.g. "221.2 83.2% 53.3%" (default: blue)',
245
+ ),
246
+ fontFamily: tool.schema
247
+ .string()
248
+ .optional()
249
+ .describe(
250
+ 'Google Font family name, e.g. "Inter", "Poppins" (default: Inter)',
251
+ ),
252
+ borderRadius: tool.schema
253
+ .string()
254
+ .optional()
255
+ .describe(
256
+ 'Base border radius, e.g. "0.5rem", "0.75rem" (default: 0.5rem)',
257
+ ),
258
+ },
259
+ async execute(args, context) {
260
+ const dir = context.directory;
261
+ const name = args.name ?? "leo-project";
262
+ const primary = args.primaryColor ?? "221.2 83.2% 53.3%";
263
+ const font = args.fontFamily ?? "Inter";
264
+ const radius = args.borderRadius ?? "0.5rem";
265
+
266
+ await mkdir(path.join(dir, "src", "lib"), { recursive: true });
267
+
268
+ const files: [string, string][] = [
269
+ ["package.json", packageJson(name)],
270
+ ["index.html", indexHtml(font)],
271
+ ["vite.config.js", viteConfig()],
272
+ ["tailwind.config.js", tailwindConfig()],
273
+ ["postcss.config.js", postcssConfig()],
274
+ [path.join("src", "index.css"), indexCss(primary, font, radius)],
275
+ [path.join("src", "lib", "utils.ts"), utilsTs()],
276
+ [path.join("src", "main.tsx"), mainTsx()],
277
+ ];
278
+
279
+ const created: string[] = [];
280
+ for (const [filePath, content] of files) {
281
+ await Bun.write(path.join(dir, filePath), content);
282
+ created.push(filePath);
283
+ }
284
+
285
+ return [
286
+ `Scaffolded ${created.length} files:`,
287
+ ...created.map((f) => ` - ${f}`),
288
+ "",
289
+ "Next: create src/App.tsx with your application code.",
290
+ "The design system is ready -- use design tokens (bg-primary, text-foreground, etc.)",
291
+ "and create UI components in src/components/ui/.",
292
+ ].join("\n");
293
+ },
294
+ });
295
+
296
+ export const ScaffoldPlugin: Plugin = async () => {
297
+ return {
298
+ tool: { scaffold: scaffold },
299
+ };
300
+ };