@burdenoff/vibe-plugin-ai 2.0.0 → 3.0.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.
Files changed (2) hide show
  1. package/dist/index.js +546 -20
  2. package/package.json +11 -4
package/dist/index.js CHANGED
@@ -1,7 +1,420 @@
1
1
  // @bun
2
2
  // src/index.ts
3
- import { existsSync, mkdirSync, writeFileSync } from "fs";
3
+ import { mkdirSync as mkdirSync2 } from "fs";
4
+ import { join as join2 } from "path";
5
+ import { Elysia as Elysia2 } from "elysia";
6
+
7
+ // src/db/prompts.ts
8
+ import { Database } from "bun:sqlite";
4
9
  import { join } from "path";
10
+ import os from "os";
11
+ import { existsSync, mkdirSync } from "fs";
12
+ var VALID_CATEGORIES = [
13
+ "GENERAL",
14
+ "CODING",
15
+ "DEBUGGING",
16
+ "REVIEW",
17
+ "DOCUMENTATION",
18
+ "TESTING",
19
+ "DEPLOYMENT",
20
+ "CUSTOM"
21
+ ];
22
+
23
+ class PromptDatabase {
24
+ db;
25
+ constructor(dbPath) {
26
+ const dir = join(os.homedir(), ".vibecontrols");
27
+ if (!existsSync(dir)) {
28
+ mkdirSync(dir, { recursive: true });
29
+ }
30
+ const path = dbPath || join(dir, "ai-prompts.db");
31
+ this.db = new Database(path);
32
+ this.db.exec("PRAGMA journal_mode = WAL");
33
+ this.db.exec("PRAGMA busy_timeout = 5000");
34
+ this.db.exec("PRAGMA synchronous = NORMAL");
35
+ this.initializeSchema();
36
+ }
37
+ initializeSchema() {
38
+ this.db.exec(`
39
+ CREATE TABLE IF NOT EXISTS prompts (
40
+ id TEXT PRIMARY KEY,
41
+ name TEXT NOT NULL,
42
+ content TEXT NOT NULL,
43
+ category TEXT CHECK(category IS NULL OR category IN ('GENERAL','CODING','DEBUGGING','REVIEW','DOCUMENTATION','TESTING','DEPLOYMENT','CUSTOM')),
44
+ tags TEXT DEFAULT '[]',
45
+ variables TEXT DEFAULT '[]',
46
+ is_shared INTEGER DEFAULT 0,
47
+ created_by TEXT NOT NULL DEFAULT 'local',
48
+ usage_count INTEGER DEFAULT 0,
49
+ last_used TEXT,
50
+ metadata TEXT DEFAULT '{}',
51
+ created_at TEXT DEFAULT (datetime('now')),
52
+ updated_at TEXT DEFAULT (datetime('now')),
53
+ deleted_at TEXT
54
+ );
55
+
56
+ CREATE INDEX IF NOT EXISTS idx_prompts_category ON prompts(category);
57
+ CREATE INDEX IF NOT EXISTS idx_prompts_is_shared ON prompts(is_shared);
58
+ CREATE INDEX IF NOT EXISTS idx_prompts_created_by ON prompts(created_by);
59
+ CREATE INDEX IF NOT EXISTS idx_prompts_deleted_at ON prompts(deleted_at);
60
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_prompts_name ON prompts(name) WHERE deleted_at IS NULL;
61
+ `);
62
+ }
63
+ generateId() {
64
+ return crypto.randomUUID();
65
+ }
66
+ rowToPrompt(row) {
67
+ return {
68
+ id: row.id,
69
+ name: row.name,
70
+ content: row.content,
71
+ category: row.category,
72
+ tags: JSON.parse(row.tags || "[]"),
73
+ variables: JSON.parse(row.variables || "[]"),
74
+ isShared: row.is_shared === 1,
75
+ createdBy: row.created_by,
76
+ usageCount: row.usage_count,
77
+ lastUsed: row.last_used,
78
+ metadata: JSON.parse(row.metadata || "{}"),
79
+ createdAt: row.created_at,
80
+ updatedAt: row.updated_at,
81
+ deletedAt: row.deleted_at
82
+ };
83
+ }
84
+ extractVariables(content) {
85
+ const variableRegex = /\{\{(\w+)\}\}/g;
86
+ const variables = [];
87
+ let match;
88
+ while ((match = variableRegex.exec(content)) !== null) {
89
+ if (match[1] && !variables.includes(match[1])) {
90
+ variables.push(match[1]);
91
+ }
92
+ }
93
+ return variables;
94
+ }
95
+ create(input) {
96
+ const id = this.generateId();
97
+ const now = new Date().toISOString();
98
+ const variables = input.variables || this.extractVariables(input.content);
99
+ const category = input.category && VALID_CATEGORIES.includes(input.category) ? input.category : null;
100
+ const stmt = this.db.prepare(`
101
+ INSERT INTO prompts (id, name, content, category, tags, variables, is_shared, created_by, usage_count, metadata, created_at, updated_at)
102
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'local', 0, ?, ?, ?)
103
+ `);
104
+ stmt.run(id, input.name, input.content, category, JSON.stringify(input.tags || []), JSON.stringify(variables), input.isShared ? 1 : 0, JSON.stringify(input.metadata || {}), now, now);
105
+ return this.getById(id);
106
+ }
107
+ getById(id) {
108
+ const row = this.db.prepare("SELECT * FROM prompts WHERE id = ? AND deleted_at IS NULL").get(id);
109
+ return row ? this.rowToPrompt(row) : null;
110
+ }
111
+ list(filter, pagination) {
112
+ const conditions = ["deleted_at IS NULL"];
113
+ const params = [];
114
+ if (filter?.category) {
115
+ conditions.push("category = ?");
116
+ params.push(filter.category);
117
+ }
118
+ if (filter?.isShared !== undefined) {
119
+ conditions.push("is_shared = ?");
120
+ params.push(filter.isShared ? 1 : 0);
121
+ }
122
+ if (filter?.createdBy) {
123
+ conditions.push("created_by = ?");
124
+ params.push(filter.createdBy);
125
+ }
126
+ const whereClause = conditions.join(" AND ");
127
+ const limit = pagination?.limit || 50;
128
+ const offset = pagination?.offset || 0;
129
+ const countRow = this.db.prepare(`SELECT COUNT(*) as count FROM prompts WHERE ${whereClause}`).get(...params);
130
+ const rows = this.db.prepare(`SELECT * FROM prompts WHERE ${whereClause} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
131
+ let items = rows.map((r) => this.rowToPrompt(r));
132
+ let total = countRow.count;
133
+ if (filter?.tags && filter.tags.length > 0) {
134
+ items = items.filter((prompt) => filter.tags.some((tag) => prompt.tags.includes(tag)));
135
+ total = items.length;
136
+ }
137
+ return {
138
+ items,
139
+ total,
140
+ hasMore: offset + items.length < total
141
+ };
142
+ }
143
+ search(query, category, limit) {
144
+ const conditions = [
145
+ "deleted_at IS NULL",
146
+ "(LOWER(name) LIKE ? OR LOWER(content) LIKE ?)"
147
+ ];
148
+ const pattern = `%${query.toLowerCase()}%`;
149
+ const params = [pattern, pattern];
150
+ if (category) {
151
+ conditions.push("category = ?");
152
+ params.push(category);
153
+ }
154
+ const rows = this.db.prepare(`SELECT * FROM prompts WHERE ${conditions.join(" AND ")} ORDER BY usage_count DESC LIMIT ?`).all(...params, limit || 20);
155
+ return rows.map((r) => this.rowToPrompt(r));
156
+ }
157
+ getPopular(limit) {
158
+ const rows = this.db.prepare("SELECT * FROM prompts WHERE deleted_at IS NULL ORDER BY usage_count DESC LIMIT ?").all(limit || 10);
159
+ return rows.map((r) => this.rowToPrompt(r));
160
+ }
161
+ update(id, input) {
162
+ const existing = this.getById(id);
163
+ if (!existing)
164
+ return null;
165
+ const sets = ["updated_at = ?"];
166
+ const now = new Date().toISOString();
167
+ const params = [now];
168
+ if (input.name !== undefined) {
169
+ sets.push("name = ?");
170
+ params.push(input.name);
171
+ }
172
+ if (input.content !== undefined) {
173
+ sets.push("content = ?");
174
+ params.push(input.content);
175
+ const variables = input.variables || this.extractVariables(input.content);
176
+ sets.push("variables = ?");
177
+ params.push(JSON.stringify(variables));
178
+ }
179
+ if (input.category !== undefined) {
180
+ sets.push("category = ?");
181
+ params.push(input.category);
182
+ }
183
+ if (input.tags !== undefined) {
184
+ sets.push("tags = ?");
185
+ params.push(JSON.stringify(input.tags));
186
+ }
187
+ if (input.variables !== undefined && input.content === undefined) {
188
+ sets.push("variables = ?");
189
+ params.push(JSON.stringify(input.variables));
190
+ }
191
+ if (input.isShared !== undefined) {
192
+ sets.push("is_shared = ?");
193
+ params.push(input.isShared ? 1 : 0);
194
+ }
195
+ if (input.metadata !== undefined) {
196
+ sets.push("metadata = ?");
197
+ params.push(JSON.stringify(input.metadata));
198
+ }
199
+ params.push(id);
200
+ this.db.prepare(`UPDATE prompts SET ${sets.join(", ")} WHERE id = ? AND deleted_at IS NULL`).run(...params);
201
+ return this.getById(id);
202
+ }
203
+ delete(id) {
204
+ const result = this.db.prepare("UPDATE prompts SET deleted_at = datetime('now'), updated_at = datetime('now') WHERE id = ? AND deleted_at IS NULL").run(id);
205
+ return result.changes > 0;
206
+ }
207
+ use(id) {
208
+ const existing = this.getById(id);
209
+ if (!existing)
210
+ return null;
211
+ this.db.prepare("UPDATE prompts SET usage_count = usage_count + 1, last_used = datetime('now'), updated_at = datetime('now') WHERE id = ?").run(id);
212
+ return this.getById(id);
213
+ }
214
+ duplicate(id, newName) {
215
+ const original = this.getById(id);
216
+ if (!original)
217
+ return null;
218
+ return this.create({
219
+ name: newName,
220
+ content: original.content,
221
+ category: original.category || undefined,
222
+ tags: original.tags,
223
+ variables: original.variables,
224
+ isShared: original.isShared,
225
+ metadata: original.metadata
226
+ });
227
+ }
228
+ renderPrompt(content, variables) {
229
+ let rendered = content;
230
+ for (const [key, value] of Object.entries(variables)) {
231
+ const placeholder = `{{${key}}}`;
232
+ while (rendered.includes(placeholder)) {
233
+ rendered = rendered.replace(placeholder, String(value));
234
+ }
235
+ }
236
+ return rendered;
237
+ }
238
+ renderById(id, variables) {
239
+ const prompt = this.getById(id);
240
+ if (!prompt)
241
+ return null;
242
+ return this.renderPrompt(prompt.content, variables);
243
+ }
244
+ close() {
245
+ this.db.close();
246
+ }
247
+ }
248
+
249
+ // src/routes/prompts.ts
250
+ import { Elysia, t } from "elysia";
251
+ var PromptCategoryEnum = t.Optional(t.Union([
252
+ t.Literal("GENERAL"),
253
+ t.Literal("CODING"),
254
+ t.Literal("DEBUGGING"),
255
+ t.Literal("REVIEW"),
256
+ t.Literal("DOCUMENTATION"),
257
+ t.Literal("TESTING"),
258
+ t.Literal("DEPLOYMENT"),
259
+ t.Literal("CUSTOM")
260
+ ]));
261
+ function createPromptRoutes(promptDb) {
262
+ return new Elysia({ prefix: "/prompts" }).get("/", ({ query }) => {
263
+ const filter = {};
264
+ if (query.category) {
265
+ filter.category = query.category;
266
+ }
267
+ if (query.tags) {
268
+ filter.tags = query.tags.split(",").map((t2) => t2.trim());
269
+ }
270
+ if (query.isShared !== undefined) {
271
+ filter.isShared = query.isShared === "true";
272
+ }
273
+ if (query.createdBy) {
274
+ filter.createdBy = query.createdBy;
275
+ }
276
+ const pagination = {
277
+ limit: query.limit ? parseInt(query.limit, 10) : 50,
278
+ offset: query.offset ? parseInt(query.offset, 10) : 0
279
+ };
280
+ return promptDb.list(Object.keys(filter).length > 0 ? filter : undefined, pagination);
281
+ }, {
282
+ query: t.Object({
283
+ category: t.Optional(t.String()),
284
+ tags: t.Optional(t.String()),
285
+ isShared: t.Optional(t.String()),
286
+ createdBy: t.Optional(t.String()),
287
+ limit: t.Optional(t.String()),
288
+ offset: t.Optional(t.String())
289
+ })
290
+ }).get("/search", ({ query }) => {
291
+ const results = promptDb.search(query.q, query.category, query.limit ? parseInt(query.limit, 10) : undefined);
292
+ return { items: results, total: results.length };
293
+ }, {
294
+ query: t.Object({
295
+ q: t.String(),
296
+ category: t.Optional(t.String()),
297
+ limit: t.Optional(t.String())
298
+ })
299
+ }).get("/popular", ({ query }) => {
300
+ const results = promptDb.getPopular(query.limit ? parseInt(query.limit, 10) : undefined);
301
+ return { items: results, total: results.length };
302
+ }, {
303
+ query: t.Object({
304
+ limit: t.Optional(t.String())
305
+ })
306
+ }).get("/:id", ({ params, set }) => {
307
+ const prompt = promptDb.getById(params.id);
308
+ if (!prompt) {
309
+ set.status = 404;
310
+ return { error: "Prompt not found" };
311
+ }
312
+ return prompt;
313
+ }, {
314
+ params: t.Object({ id: t.String() })
315
+ }).post("/", ({ body, set }) => {
316
+ try {
317
+ const prompt = promptDb.create(body);
318
+ set.status = 201;
319
+ return prompt;
320
+ } catch (err) {
321
+ set.status = 400;
322
+ return {
323
+ error: err instanceof Error ? err.message : "Failed to create prompt"
324
+ };
325
+ }
326
+ }, {
327
+ body: t.Object({
328
+ name: t.String({ minLength: 1 }),
329
+ content: t.String({ minLength: 1 }),
330
+ category: PromptCategoryEnum,
331
+ tags: t.Optional(t.Array(t.String())),
332
+ variables: t.Optional(t.Array(t.String())),
333
+ isShared: t.Optional(t.Boolean()),
334
+ metadata: t.Optional(t.Record(t.String(), t.Any()))
335
+ })
336
+ }).put("/:id", ({ params, body, set }) => {
337
+ const updated = promptDb.update(params.id, body);
338
+ if (!updated) {
339
+ set.status = 404;
340
+ return { error: "Prompt not found" };
341
+ }
342
+ return updated;
343
+ }, {
344
+ params: t.Object({ id: t.String() }),
345
+ body: t.Object({
346
+ name: t.Optional(t.String({ minLength: 1 })),
347
+ content: t.Optional(t.String({ minLength: 1 })),
348
+ category: t.Optional(t.Union([
349
+ t.Literal("GENERAL"),
350
+ t.Literal("CODING"),
351
+ t.Literal("DEBUGGING"),
352
+ t.Literal("REVIEW"),
353
+ t.Literal("DOCUMENTATION"),
354
+ t.Literal("TESTING"),
355
+ t.Literal("DEPLOYMENT"),
356
+ t.Literal("CUSTOM"),
357
+ t.Null()
358
+ ])),
359
+ tags: t.Optional(t.Array(t.String())),
360
+ variables: t.Optional(t.Array(t.String())),
361
+ isShared: t.Optional(t.Boolean()),
362
+ metadata: t.Optional(t.Record(t.String(), t.Any()))
363
+ })
364
+ }).delete("/:id", ({ params, set }) => {
365
+ const deleted = promptDb.delete(params.id);
366
+ if (!deleted) {
367
+ set.status = 404;
368
+ return { error: "Prompt not found" };
369
+ }
370
+ return { success: true };
371
+ }, {
372
+ params: t.Object({ id: t.String() })
373
+ }).post("/:id/use", ({ params, set }) => {
374
+ const prompt = promptDb.use(params.id);
375
+ if (!prompt) {
376
+ set.status = 404;
377
+ return { error: "Prompt not found" };
378
+ }
379
+ return prompt;
380
+ }, {
381
+ params: t.Object({ id: t.String() })
382
+ }).post("/:id/duplicate", ({ params, body, set }) => {
383
+ try {
384
+ const prompt = promptDb.duplicate(params.id, body.name);
385
+ if (!prompt) {
386
+ set.status = 404;
387
+ return { error: "Prompt not found" };
388
+ }
389
+ set.status = 201;
390
+ return prompt;
391
+ } catch (err) {
392
+ set.status = 400;
393
+ return {
394
+ error: err instanceof Error ? err.message : "Failed to duplicate prompt"
395
+ };
396
+ }
397
+ }, {
398
+ params: t.Object({ id: t.String() }),
399
+ body: t.Object({
400
+ name: t.String({ minLength: 1 })
401
+ })
402
+ }).post("/:id/render", ({ params, body, set }) => {
403
+ const rendered = promptDb.renderById(params.id, body.variables);
404
+ if (rendered === null) {
405
+ set.status = 404;
406
+ return { error: "Prompt not found" };
407
+ }
408
+ return { rendered };
409
+ }, {
410
+ params: t.Object({ id: t.String() }),
411
+ body: t.Object({
412
+ variables: t.Record(t.String(), t.Any())
413
+ })
414
+ });
415
+ }
416
+
417
+ // src/index.ts
5
418
  var AI_TOOLS = [
6
419
  {
7
420
  name: "claude-code",
@@ -69,16 +482,62 @@ function runInstallCommand(command) {
69
482
  return proc.exitCode === 0;
70
483
  }
71
484
  function findConfigFiles(tool, directory) {
72
- return tool.configFiles.filter((f) => existsSync(join(directory, f)));
485
+ return tool.configFiles.filter((f) => {
486
+ try {
487
+ return Bun.file(join2(directory, f)).size > 0;
488
+ } catch {
489
+ return false;
490
+ }
491
+ });
492
+ }
493
+ var promptDb = null;
494
+ function getPromptDb() {
495
+ if (!promptDb) {
496
+ promptDb = new PromptDatabase;
497
+ }
498
+ return promptDb;
73
499
  }
74
500
  var vibePlugin = {
75
501
  name: "ai",
76
- version: "2.0.0",
77
- description: "AI tool management and integration",
78
- tags: ["cli", "integration"],
502
+ version: "3.0.0",
503
+ description: "AI tool management, prompt templates, and integration",
504
+ tags: ["backend", "cli", "integration"],
79
505
  cliCommand: "ai",
506
+ apiPrefix: "/api/ai",
507
+ createRoutes() {
508
+ const db = getPromptDb();
509
+ return new Elysia2().get("/tools", () => {
510
+ return {
511
+ tools: AI_TOOLS.map((tool) => {
512
+ const { installed, version } = isToolInstalled(tool);
513
+ const configs = findConfigFiles(tool, process.cwd());
514
+ return {
515
+ name: tool.name,
516
+ displayName: tool.displayName,
517
+ description: tool.description,
518
+ installed,
519
+ version: installed ? version.split(`
520
+ `)[0] : "",
521
+ configFiles: tool.configFiles,
522
+ foundConfigs: configs,
523
+ installCommand: tool.installCommand
524
+ };
525
+ })
526
+ };
527
+ }).use(createPromptRoutes(db));
528
+ },
529
+ onServerStart(_app, hostServices) {
530
+ getPromptDb();
531
+ hostServices?.logger?.info("ai-plugin", "AI plugin started \u2014 prompt database initialized");
532
+ },
533
+ onServerStop() {
534
+ if (promptDb) {
535
+ promptDb.close();
536
+ promptDb = null;
537
+ }
538
+ },
80
539
  onCliSetup(program, _hostServices) {
81
- const aiCmd = program.command("ai").description("Manage AI coding tools and configurations");
540
+ const aiCmd = program.command("ai").description("Manage AI coding tools and prompt templates");
82
541
  aiCmd.command("list").description("List all supported AI tools and their status").option("--cwd <dir>", "Project directory to check configs", process.cwd()).action((options) => {
83
542
  console.log(`
84
543
  \x1B[1m\u2500\u2500 AI Tools \u2500\u2500\x1B[0m
@@ -99,10 +558,10 @@ var vibePlugin = {
99
558
  console.log();
100
559
  }
101
560
  });
102
- aiCmd.command("install").description("Install an AI tool").argument("<tool>", `Tool name (${AI_TOOLS.map((t) => t.name).join(", ")})`).action((toolName) => {
103
- const tool = AI_TOOLS.find((t) => t.name === toolName);
561
+ aiCmd.command("install").description("Install an AI tool").argument("<tool>", `Tool name (${AI_TOOLS.map((t2) => t2.name).join(", ")})`).action((toolName) => {
562
+ const tool = AI_TOOLS.find((t2) => t2.name === toolName);
104
563
  if (!tool) {
105
- console.error(`\x1B[31mError:\x1B[0m Unknown tool '${toolName}'. Available: ${AI_TOOLS.map((t) => t.name).join(", ")}`);
564
+ console.error(`\x1B[31mError:\x1B[0m Unknown tool '${toolName}'. Available: ${AI_TOOLS.map((t2) => t2.name).join(", ")}`);
106
565
  process.exit(1);
107
566
  }
108
567
  if (!tool.installCommand) {
@@ -128,26 +587,28 @@ var vibePlugin = {
128
587
  process.exit(1);
129
588
  }
130
589
  });
131
- aiCmd.command("init").description("Initialize AI tool config in the current project").argument("<tool>", "Tool name").option("--cwd <dir>", "Project directory", process.cwd()).action((toolName, options) => {
132
- const tool = AI_TOOLS.find((t) => t.name === toolName);
590
+ aiCmd.command("init").description("Initialize AI tool config in the current project").argument("<tool>", "Tool name").option("--cwd <dir>", "Project directory", process.cwd()).action(async (toolName, options) => {
591
+ const tool = AI_TOOLS.find((t2) => t2.name === toolName);
133
592
  if (!tool) {
134
- console.error(`\x1B[31mError:\x1B[0m Unknown tool '${toolName}'. Available: ${AI_TOOLS.map((t) => t.name).join(", ")}`);
593
+ console.error(`\x1B[31mError:\x1B[0m Unknown tool '${toolName}'. Available: ${AI_TOOLS.map((t2) => t2.name).join(", ")}`);
135
594
  process.exit(1);
136
595
  }
137
596
  const dir = options.cwd;
138
597
  const primaryConfig = tool.configFiles[0];
139
- const configPath = join(dir, primaryConfig);
140
- if (existsSync(configPath)) {
141
- console.log(` \x1B[33m\u26A0\x1B[0m ${primaryConfig} already exists in ${dir}`);
142
- return;
143
- }
598
+ const configPath = join2(dir, primaryConfig);
599
+ try {
600
+ if (Bun.file(configPath).size >= 0) {
601
+ console.log(` \x1B[33m\u26A0\x1B[0m ${primaryConfig} already exists in ${dir}`);
602
+ return;
603
+ }
604
+ } catch {}
144
605
  const segments = primaryConfig.split("/");
145
606
  if (segments.length > 1) {
146
- const parentDir = join(dir, ...segments.slice(0, -1));
147
- mkdirSync(parentDir, { recursive: true });
607
+ const parentDir = join2(dir, ...segments.slice(0, -1));
608
+ mkdirSync2(parentDir, { recursive: true });
148
609
  }
149
610
  const content = generateStarterConfig(tool);
150
- writeFileSync(configPath, content);
611
+ await Bun.write(configPath, content);
151
612
  console.log(`
152
613
  \x1B[32m\u2713 Created ${primaryConfig}\x1B[0m in ${dir}
153
614
  `);
@@ -171,6 +632,71 @@ var vibePlugin = {
171
632
  `);
172
633
  }
173
634
  });
635
+ const promptsCmd = aiCmd.command("prompts").description("Manage prompt templates");
636
+ promptsCmd.command("list").description("List all prompt templates").option("--shared", "Show only shared prompts").option("--category <cat>", "Filter by category").option("--limit <n>", "Max results", "20").action((options) => {
637
+ const db = getPromptDb();
638
+ const result = db.list({
639
+ isShared: options.shared !== undefined ? options.shared : undefined,
640
+ category: options.category
641
+ }, { limit: parseInt(options.limit, 10) });
642
+ if (result.items.length === 0) {
643
+ console.log(`
644
+ No prompts found.
645
+ `);
646
+ return;
647
+ }
648
+ console.log(`
649
+ \x1B[1m\u2500\u2500 Prompts (${result.total}) \u2500\u2500\x1B[0m
650
+ `);
651
+ for (const prompt of result.items) {
652
+ const shared = prompt.isShared ? " \x1B[36m[shared]\x1B[0m" : "";
653
+ const tags = prompt.tags.length > 0 ? ` \x1B[33m[${prompt.tags.join(", ")}]\x1B[0m` : "";
654
+ const uses = prompt.usageCount > 0 ? ` \x1B[90m(${prompt.usageCount} uses)\x1B[0m` : "";
655
+ console.log(` \x1B[1m${prompt.name}\x1B[0m${shared}${tags}${uses}`);
656
+ const preview = prompt.content.replace(/\n/g, " ").slice(0, 60).trim();
657
+ console.log(` ${preview}${prompt.content.length > 60 ? "..." : ""}`);
658
+ console.log();
659
+ }
660
+ });
661
+ promptsCmd.command("search").description("Search prompts by name or content").argument("<query>", "Search query").option("--limit <n>", "Max results", "10").action((query, options) => {
662
+ const db = getPromptDb();
663
+ const results = db.search(query, undefined, parseInt(options.limit, 10));
664
+ if (results.length === 0) {
665
+ console.log(`
666
+ No prompts matching "${query}".
667
+ `);
668
+ return;
669
+ }
670
+ console.log(`
671
+ \x1B[1m\u2500\u2500 Search: "${query}" (${results.length} results) \u2500\u2500\x1B[0m
672
+ `);
673
+ for (const prompt of results) {
674
+ console.log(` \x1B[1m${prompt.name}\x1B[0m \x1B[90m(${prompt.usageCount} uses)\x1B[0m`);
675
+ const preview = prompt.content.replace(/\n/g, " ").slice(0, 60).trim();
676
+ console.log(` ${preview}${prompt.content.length > 60 ? "..." : ""}`);
677
+ console.log();
678
+ }
679
+ });
680
+ promptsCmd.command("show").description("Show a prompt by ID").argument("<id>", "Prompt ID").action((id) => {
681
+ const db = getPromptDb();
682
+ const prompt = db.getById(id);
683
+ if (!prompt) {
684
+ console.error(`\x1B[31mError:\x1B[0m Prompt not found: ${id}`);
685
+ process.exit(1);
686
+ }
687
+ console.log(`
688
+ \x1B[1m${prompt.name}\x1B[0m`);
689
+ if (prompt.tags.length > 0) {
690
+ console.log(` Tags: ${prompt.tags.join(", ")}`);
691
+ }
692
+ if (prompt.variables.length > 0) {
693
+ console.log(` Variables: ${prompt.variables.map((v) => `{{${v}}}`).join(", ")}`);
694
+ }
695
+ console.log(` Shared: ${prompt.isShared ? "yes" : "no"} | Uses: ${prompt.usageCount}`);
696
+ console.log(`
697
+ ${prompt.content}
698
+ `);
699
+ });
174
700
  }
175
701
  };
176
702
  function generateStarterConfig(tool) {
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@burdenoff/vibe-plugin-ai",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "main": "./dist/index.js",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.3.0"
8
8
  },
9
9
  "scripts": {
10
- "build": "bun build ./src/index.ts --outdir ./dist --target bun",
10
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun --external elysia",
11
11
  "lint": "eslint ./src",
12
12
  "format": "bunx prettier . --write",
13
13
  "format:check": "bunx prettier . --check",
@@ -35,14 +35,21 @@
35
35
  "url": "https://github.com/tvvignesh"
36
36
  },
37
37
  "license": "SEE LICENSE IN LICENSE",
38
- "description": "AI tool management plugin for VibeControls Agent — manage Claude Code, OpenCode, Codex, Copilot, and more (Bun runtime)",
38
+ "description": "AI tool management and prompt templates plugin for VibeControls Agent — manage AI tools, prompt templates with variables (Bun runtime)",
39
+ "dependencies": {
40
+ "elysia": "^1.3.0"
41
+ },
39
42
  "devDependencies": {
43
+ "@eslint/js": "^10.0.1",
40
44
  "@types/bun": "^1.2.16",
45
+ "bun-types": "^1.3.9",
41
46
  "commander": "^14.0.3",
42
47
  "eslint": "^9.30.1",
48
+ "globals": "^17.3.0",
43
49
  "prettier": "^3.6.2",
44
50
  "rimraf": "^6.0.1",
45
- "typescript": "^5.8.3"
51
+ "typescript": "^5.8.3",
52
+ "typescript-eslint": "^8.56.0"
46
53
  },
47
54
  "peerDependencies": {
48
55
  "@burdenoff/vibe-agent": ">=2.0.0"