@hasna/prompts 0.2.1 → 0.3.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/dist/cli/index.js CHANGED
@@ -17,6 +17,16 @@ var __toESM = (mod, isNodeMode, target) => {
17
17
  return to;
18
18
  };
19
19
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
20
+ var __export = (target, all) => {
21
+ for (var name in all)
22
+ __defProp(target, name, {
23
+ get: all[name],
24
+ enumerable: true,
25
+ configurable: true,
26
+ set: (newValue) => all[name] = () => newValue
27
+ });
28
+ };
29
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
20
30
  var __require = import.meta.require;
21
31
 
22
32
  // node_modules/commander/lib/error.js
@@ -2053,31 +2063,10 @@ var require_commander = __commonJS((exports) => {
2053
2063
  exports.InvalidOptionArgumentError = InvalidArgumentError;
2054
2064
  });
2055
2065
 
2056
- // node_modules/commander/esm.mjs
2057
- var import__ = __toESM(require_commander(), 1);
2058
- var {
2059
- program,
2060
- createCommand,
2061
- createArgument,
2062
- createOption,
2063
- CommanderError,
2064
- InvalidArgumentError,
2065
- InvalidOptionArgumentError,
2066
- Command,
2067
- Argument,
2068
- Option,
2069
- Help
2070
- } = import__.default;
2071
-
2072
- // src/cli/index.tsx
2073
- import chalk from "chalk";
2074
- import { createRequire } from "module";
2075
-
2076
2066
  // src/db/database.ts
2077
2067
  import { Database } from "bun:sqlite";
2078
2068
  import { join } from "path";
2079
2069
  import { existsSync, mkdirSync } from "fs";
2080
- var _db = null;
2081
2070
  function getDbPath() {
2082
2071
  if (process.env["PROMPTS_DB_PATH"]) {
2083
2072
  return process.env["PROMPTS_DB_PATH"];
@@ -2187,6 +2176,41 @@ function runMigrations(db) {
2187
2176
  name: "003_pinned",
2188
2177
  sql: `ALTER TABLE prompts ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;`
2189
2178
  },
2179
+ {
2180
+ name: "004_projects",
2181
+ sql: `
2182
+ CREATE TABLE IF NOT EXISTS projects (
2183
+ id TEXT PRIMARY KEY,
2184
+ name TEXT NOT NULL UNIQUE,
2185
+ slug TEXT NOT NULL UNIQUE,
2186
+ description TEXT,
2187
+ path TEXT,
2188
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2189
+ );
2190
+ ALTER TABLE prompts ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
2191
+ CREATE INDEX IF NOT EXISTS idx_prompts_project_id ON prompts(project_id);
2192
+ `
2193
+ },
2194
+ {
2195
+ name: "005_chaining",
2196
+ sql: `ALTER TABLE prompts ADD COLUMN next_prompt TEXT;`
2197
+ },
2198
+ {
2199
+ name: "006_expiry",
2200
+ sql: `ALTER TABLE prompts ADD COLUMN expires_at TEXT;`
2201
+ },
2202
+ {
2203
+ name: "007_usage_log",
2204
+ sql: `
2205
+ CREATE TABLE IF NOT EXISTS usage_log (
2206
+ id TEXT PRIMARY KEY,
2207
+ prompt_id TEXT NOT NULL REFERENCES prompts(id) ON DELETE CASCADE,
2208
+ used_at TEXT NOT NULL DEFAULT (datetime('now'))
2209
+ );
2210
+ CREATE INDEX IF NOT EXISTS idx_usage_log_prompt_id ON usage_log(prompt_id);
2211
+ CREATE INDEX IF NOT EXISTS idx_usage_log_used_at ON usage_log(used_at);
2212
+ `
2213
+ },
2190
2214
  {
2191
2215
  name: "002_fts5",
2192
2216
  sql: `
@@ -2227,6 +2251,24 @@ function runMigrations(db) {
2227
2251
  db.run("INSERT INTO _migrations (name) VALUES (?)", [migration.name]);
2228
2252
  }
2229
2253
  }
2254
+ function resolveProject(db, idOrSlug) {
2255
+ const byId = db.query("SELECT id FROM projects WHERE id = ?").get(idOrSlug);
2256
+ if (byId)
2257
+ return byId.id;
2258
+ const bySlug = db.query("SELECT id FROM projects WHERE slug = ?").get(idOrSlug);
2259
+ if (bySlug)
2260
+ return bySlug.id;
2261
+ const byName = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(idOrSlug.toLowerCase());
2262
+ if (byName)
2263
+ return byName.id;
2264
+ const byPrefix = db.query("SELECT id FROM projects WHERE id LIKE ? LIMIT 2").all(`${idOrSlug}%`);
2265
+ if (byPrefix.length === 1 && byPrefix[0])
2266
+ return byPrefix[0].id;
2267
+ const bySlugPrefix = db.query("SELECT id FROM projects WHERE slug LIKE ? LIMIT 2").all(`${idOrSlug}%`);
2268
+ if (bySlugPrefix.length === 1 && bySlugPrefix[0])
2269
+ return bySlugPrefix[0].id;
2270
+ return null;
2271
+ }
2230
2272
  function hasFts(db) {
2231
2273
  return db.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='prompts_fts'").get() !== null;
2232
2274
  }
@@ -2251,6 +2293,8 @@ function resolvePrompt(db, idOrSlug) {
2251
2293
  return byTitle[0].id;
2252
2294
  return null;
2253
2295
  }
2296
+ var _db = null;
2297
+ var init_database = () => {};
2254
2298
 
2255
2299
  // src/lib/ids.ts
2256
2300
  function generateSlug(title) {
@@ -2266,7 +2310,6 @@ function uniqueSlug(baseSlug) {
2266
2310
  }
2267
2311
  return slug;
2268
2312
  }
2269
- var CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
2270
2313
  function nanoid(len) {
2271
2314
  let id = "";
2272
2315
  for (let i = 0;i < len; i++) {
@@ -2285,6 +2328,10 @@ function generatePromptId() {
2285
2328
  function generateId(prefix) {
2286
2329
  return `${prefix}-${nanoid(8)}`;
2287
2330
  }
2331
+ var CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
2332
+ var init_ids = __esm(() => {
2333
+ init_database();
2334
+ });
2288
2335
 
2289
2336
  // src/db/collections.ts
2290
2337
  function rowToCollection(row) {
@@ -2340,6 +2387,10 @@ function movePrompt(promptIdOrSlug, targetCollection) {
2340
2387
  row.id
2341
2388
  ]);
2342
2389
  }
2390
+ var init_collections = __esm(() => {
2391
+ init_database();
2392
+ init_ids();
2393
+ });
2343
2394
 
2344
2395
  // src/lib/duplicates.ts
2345
2396
  function tokenize(text) {
@@ -2370,9 +2421,11 @@ function findDuplicates(body, threshold = 0.8, excludeSlug) {
2370
2421
  }
2371
2422
  return matches.sort((a, b) => b.score - a.score);
2372
2423
  }
2424
+ var init_duplicates = __esm(() => {
2425
+ init_prompts();
2426
+ });
2373
2427
 
2374
2428
  // src/lib/template.ts
2375
- var VAR_PATTERN = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\|\s*(.*?)\s*)?\}\}/g;
2376
2429
  function extractVariables(body) {
2377
2430
  const vars = new Set;
2378
2431
  const pattern = new RegExp(VAR_PATTERN.source, "g");
@@ -2413,28 +2466,39 @@ function renderTemplate(body, vars) {
2413
2466
  });
2414
2467
  return { rendered, missing_vars: missing, used_defaults: usedDefaults };
2415
2468
  }
2469
+ var VAR_PATTERN;
2470
+ var init_template = __esm(() => {
2471
+ VAR_PATTERN = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\|\s*(.*?)\s*)?\}\}/g;
2472
+ });
2416
2473
 
2417
2474
  // src/types/index.ts
2418
- class PromptNotFoundError extends Error {
2419
- constructor(id) {
2420
- super(`Prompt not found: ${id}`);
2421
- this.name = "PromptNotFoundError";
2422
- }
2423
- }
2424
-
2425
- class VersionConflictError extends Error {
2426
- constructor(id) {
2427
- super(`Version conflict on prompt: ${id}`);
2428
- this.name = "VersionConflictError";
2429
- }
2430
- }
2431
-
2432
- class DuplicateSlugError extends Error {
2433
- constructor(slug) {
2434
- super(`A prompt with slug "${slug}" already exists`);
2435
- this.name = "DuplicateSlugError";
2436
- }
2437
- }
2475
+ var PromptNotFoundError, VersionConflictError, DuplicateSlugError, ProjectNotFoundError;
2476
+ var init_types = __esm(() => {
2477
+ PromptNotFoundError = class PromptNotFoundError extends Error {
2478
+ constructor(id) {
2479
+ super(`Prompt not found: ${id}`);
2480
+ this.name = "PromptNotFoundError";
2481
+ }
2482
+ };
2483
+ VersionConflictError = class VersionConflictError extends Error {
2484
+ constructor(id) {
2485
+ super(`Version conflict on prompt: ${id}`);
2486
+ this.name = "VersionConflictError";
2487
+ }
2488
+ };
2489
+ DuplicateSlugError = class DuplicateSlugError extends Error {
2490
+ constructor(slug) {
2491
+ super(`A prompt with slug "${slug}" already exists`);
2492
+ this.name = "DuplicateSlugError";
2493
+ }
2494
+ };
2495
+ ProjectNotFoundError = class ProjectNotFoundError extends Error {
2496
+ constructor(id) {
2497
+ super(`Project not found: ${id}`);
2498
+ this.name = "ProjectNotFoundError";
2499
+ }
2500
+ };
2501
+ });
2438
2502
 
2439
2503
  // src/db/prompts.ts
2440
2504
  function rowToPrompt(row) {
@@ -2449,6 +2513,9 @@ function rowToPrompt(row) {
2449
2513
  tags: JSON.parse(row["tags"] || "[]"),
2450
2514
  variables: JSON.parse(row["variables"] || "[]"),
2451
2515
  pinned: Boolean(row["pinned"]),
2516
+ next_prompt: row["next_prompt"] ?? null,
2517
+ expires_at: row["expires_at"] ?? null,
2518
+ project_id: row["project_id"] ?? null,
2452
2519
  is_template: Boolean(row["is_template"]),
2453
2520
  source: row["source"],
2454
2521
  version: row["version"],
@@ -2472,11 +2539,12 @@ function createPrompt(input) {
2472
2539
  ensureCollection(collection);
2473
2540
  const tags = JSON.stringify(input.tags || []);
2474
2541
  const source = input.source || "manual";
2542
+ const project_id = input.project_id ?? null;
2475
2543
  const vars = extractVariables(input.body);
2476
2544
  const variables = JSON.stringify(vars.map((v) => ({ name: v, required: true })));
2477
2545
  const is_template = vars.length > 0 ? 1 : 0;
2478
- db.run(`INSERT INTO prompts (id, name, slug, title, body, description, collection, tags, variables, is_template, source)
2479
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, name, slug, input.title, input.body, input.description ?? null, collection, tags, variables, is_template, source]);
2546
+ db.run(`INSERT INTO prompts (id, name, slug, title, body, description, collection, tags, variables, is_template, source, project_id)
2547
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, name, slug, input.title, input.body, input.description ?? null, collection, tags, variables, is_template, source, project_id]);
2480
2548
  db.run(`INSERT INTO prompt_versions (id, prompt_id, body, version, changed_by)
2481
2549
  VALUES (?, ?, ?, 1, ?)`, [generateId("VER"), id, input.body, input.changed_by ?? null]);
2482
2550
  return getPrompt(id);
@@ -2520,10 +2588,16 @@ function listPrompts(filter = {}) {
2520
2588
  params.push(`%"${tag}"%`);
2521
2589
  }
2522
2590
  }
2591
+ let orderBy = "pinned DESC, use_count DESC, updated_at DESC";
2592
+ if (filter.project_id !== undefined && filter.project_id !== null) {
2593
+ conditions.push("(project_id = ? OR project_id IS NULL)");
2594
+ params.push(filter.project_id);
2595
+ orderBy = `(CASE WHEN project_id = '${filter.project_id}' THEN 0 ELSE 1 END), pinned DESC, use_count DESC, updated_at DESC`;
2596
+ }
2523
2597
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2524
2598
  const limit = filter.limit ?? 100;
2525
2599
  const offset = filter.offset ?? 0;
2526
- const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY pinned DESC, use_count DESC, updated_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
2600
+ const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
2527
2601
  return rows.map(rowToPrompt);
2528
2602
  }
2529
2603
  function updatePrompt(idOrSlug, input) {
@@ -2539,6 +2613,7 @@ function updatePrompt(idOrSlug, input) {
2539
2613
  description = COALESCE(?, description),
2540
2614
  collection = COALESCE(?, collection),
2541
2615
  tags = COALESCE(?, tags),
2616
+ next_prompt = CASE WHEN ? IS NOT NULL THEN ? ELSE next_prompt END,
2542
2617
  variables = ?,
2543
2618
  is_template = ?,
2544
2619
  version = version + 1,
@@ -2549,6 +2624,8 @@ function updatePrompt(idOrSlug, input) {
2549
2624
  input.description ?? null,
2550
2625
  input.collection ?? null,
2551
2626
  input.tags ? JSON.stringify(input.tags) : null,
2627
+ "next_prompt" in input ? input.next_prompt ?? "" : null,
2628
+ "next_prompt" in input ? input.next_prompt ?? null : null,
2552
2629
  variables,
2553
2630
  is_template,
2554
2631
  prompt.id,
@@ -2571,6 +2648,30 @@ function usePrompt(idOrSlug) {
2571
2648
  const db = getDatabase();
2572
2649
  const prompt = requirePrompt(idOrSlug);
2573
2650
  db.run("UPDATE prompts SET use_count = use_count + 1, last_used_at = datetime('now') WHERE id = ?", [prompt.id]);
2651
+ db.run("INSERT INTO usage_log (id, prompt_id) VALUES (?, ?)", [generateId("UL"), prompt.id]);
2652
+ return requirePrompt(prompt.id);
2653
+ }
2654
+ function getTrending(days = 7, limit = 10) {
2655
+ const db = getDatabase();
2656
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
2657
+ return db.query(`SELECT p.id, p.slug, p.title, COUNT(ul.id) as uses
2658
+ FROM usage_log ul
2659
+ JOIN prompts p ON p.id = ul.prompt_id
2660
+ WHERE ul.used_at >= ?
2661
+ GROUP BY p.id
2662
+ ORDER BY uses DESC
2663
+ LIMIT ?`).all(cutoff, limit);
2664
+ }
2665
+ function setExpiry(idOrSlug, expiresAt) {
2666
+ const db = getDatabase();
2667
+ const prompt = requirePrompt(idOrSlug);
2668
+ db.run("UPDATE prompts SET expires_at = ?, updated_at = datetime('now') WHERE id = ?", [expiresAt, prompt.id]);
2669
+ return requirePrompt(prompt.id);
2670
+ }
2671
+ function setNextPrompt(idOrSlug, nextSlug) {
2672
+ const db = getDatabase();
2673
+ const prompt = requirePrompt(idOrSlug);
2674
+ db.run("UPDATE prompts SET next_prompt = ?, updated_at = datetime('now') WHERE id = ?", [nextSlug, prompt.id]);
2574
2675
  return requirePrompt(prompt.id);
2575
2676
  }
2576
2677
  function pinPrompt(idOrSlug, pinned) {
@@ -2616,8 +2717,198 @@ function getPromptStats() {
2616
2717
  const bySource = db.query("SELECT source, COUNT(*) as count FROM prompts GROUP BY source ORDER BY count DESC").all();
2617
2718
  return { total_prompts: total, total_templates: templates, total_collections: collections, most_used: mostUsed, recently_used: recentlyUsed, by_collection: byCollection, by_source: bySource };
2618
2719
  }
2720
+ var init_prompts = __esm(() => {
2721
+ init_database();
2722
+ init_ids();
2723
+ init_collections();
2724
+ init_duplicates();
2725
+ init_template();
2726
+ init_types();
2727
+ init_ids();
2728
+ });
2729
+
2730
+ // src/lib/importer.ts
2731
+ var exports_importer = {};
2732
+ __export(exports_importer, {
2733
+ scanAndImportSlashCommands: () => scanAndImportSlashCommands,
2734
+ promptToMarkdown: () => promptToMarkdown,
2735
+ markdownToImportItem: () => markdownToImportItem,
2736
+ importFromMarkdown: () => importFromMarkdown,
2737
+ importFromJson: () => importFromJson,
2738
+ importFromClaudeCommands: () => importFromClaudeCommands,
2739
+ exportToMarkdownFiles: () => exportToMarkdownFiles,
2740
+ exportToJson: () => exportToJson
2741
+ });
2742
+ function importFromJson(items, changedBy) {
2743
+ let created = 0;
2744
+ let updated = 0;
2745
+ const errors = [];
2746
+ for (const item of items) {
2747
+ try {
2748
+ const input = {
2749
+ title: item.title,
2750
+ body: item.body,
2751
+ slug: item.slug,
2752
+ description: item.description,
2753
+ collection: item.collection,
2754
+ tags: item.tags,
2755
+ source: "imported",
2756
+ changed_by: changedBy
2757
+ };
2758
+ const { created: wasCreated } = upsertPrompt(input);
2759
+ if (wasCreated)
2760
+ created++;
2761
+ else
2762
+ updated++;
2763
+ } catch (e) {
2764
+ errors.push({ item: item.title, error: e instanceof Error ? e.message : String(e) });
2765
+ }
2766
+ }
2767
+ return { created, updated, errors };
2768
+ }
2769
+ function exportToJson(collection) {
2770
+ const prompts = listPrompts({ collection, limit: 1e4 });
2771
+ return { prompts, exported_at: new Date().toISOString(), collection };
2772
+ }
2773
+ function promptToMarkdown(prompt) {
2774
+ const tags = prompt.tags.length > 0 ? `[${prompt.tags.join(", ")}]` : "[]";
2775
+ const desc = prompt.description ? `
2776
+ description: ${prompt.description}` : "";
2777
+ return `---
2778
+ title: ${prompt.title}
2779
+ slug: ${prompt.slug}
2780
+ collection: ${prompt.collection}
2781
+ tags: ${tags}${desc}
2782
+ ---
2783
+
2784
+ ${prompt.body}
2785
+ `;
2786
+ }
2787
+ function exportToMarkdownFiles(collection) {
2788
+ const prompts = listPrompts({ collection, limit: 1e4 });
2789
+ return prompts.map((p) => ({
2790
+ filename: `${p.slug}.md`,
2791
+ content: promptToMarkdown(p)
2792
+ }));
2793
+ }
2794
+ function markdownToImportItem(content, filename) {
2795
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n+([\s\S]*)$/);
2796
+ if (!frontmatterMatch) {
2797
+ if (!filename)
2798
+ return null;
2799
+ const title2 = filename.replace(/\.md$/, "").replace(/-/g, " ");
2800
+ return { title: title2, body: content.trim() };
2801
+ }
2802
+ const frontmatter = frontmatterMatch[1] ?? "";
2803
+ const body = (frontmatterMatch[2] ?? "").trim();
2804
+ const get = (key) => {
2805
+ const m = frontmatter.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
2806
+ return m ? (m[1] ?? "").trim() : null;
2807
+ };
2808
+ const title = get("title") ?? (filename?.replace(/\.md$/, "").replace(/-/g, " ") ?? "Untitled");
2809
+ const slug = get("slug") ?? undefined;
2810
+ const collection = get("collection") ?? undefined;
2811
+ const description = get("description") ?? undefined;
2812
+ const tagsStr = get("tags");
2813
+ let tags;
2814
+ if (tagsStr) {
2815
+ const inner = tagsStr.replace(/^\[/, "").replace(/\]$/, "");
2816
+ tags = inner.split(",").map((t) => t.trim()).filter(Boolean);
2817
+ }
2818
+ return { title, slug, body, collection, tags, description };
2819
+ }
2820
+ function importFromMarkdown(files, changedBy) {
2821
+ const items = files.map((f) => markdownToImportItem(f.content, f.filename)).filter((item) => item !== null);
2822
+ return importFromJson(items, changedBy);
2823
+ }
2824
+ function scanAndImportSlashCommands(rootDir, changedBy) {
2825
+ const { existsSync: existsSync2, readdirSync, readFileSync } = __require("fs");
2826
+ const { join: join2 } = __require("path");
2827
+ const home = process.env["HOME"] ?? "~";
2828
+ const sources = [
2829
+ { dir: join2(rootDir, ".claude", "commands"), collection: "claude-commands", tags: ["claude", "slash-command"] },
2830
+ { dir: join2(home, ".claude", "commands"), collection: "claude-commands", tags: ["claude", "slash-command"] },
2831
+ { dir: join2(rootDir, ".codex", "skills"), collection: "codex-skills", tags: ["codex", "skill"] },
2832
+ { dir: join2(home, ".codex", "skills"), collection: "codex-skills", tags: ["codex", "skill"] },
2833
+ { dir: join2(rootDir, ".gemini", "extensions"), collection: "gemini-extensions", tags: ["gemini", "extension"] },
2834
+ { dir: join2(home, ".gemini", "extensions"), collection: "gemini-extensions", tags: ["gemini", "extension"] }
2835
+ ];
2836
+ const files = [];
2837
+ const scanned = [];
2838
+ for (const { dir, collection, tags } of sources) {
2839
+ if (!existsSync2(dir))
2840
+ continue;
2841
+ let entries;
2842
+ try {
2843
+ entries = readdirSync(dir);
2844
+ } catch {
2845
+ continue;
2846
+ }
2847
+ for (const entry of entries) {
2848
+ if (!entry.endsWith(".md"))
2849
+ continue;
2850
+ const filePath = join2(dir, entry);
2851
+ try {
2852
+ const content = readFileSync(filePath, "utf-8");
2853
+ files.push({ filename: entry, content, collection, tags });
2854
+ scanned.push({ source: dir, file: entry });
2855
+ } catch {}
2856
+ }
2857
+ }
2858
+ const items = files.map((f) => {
2859
+ const base = markdownToImportItem(f.content, f.filename);
2860
+ if (base)
2861
+ return { ...base, collection: base.collection ?? f.collection, tags: base.tags ?? f.tags };
2862
+ const name = f.filename.replace(/\.md$/, "");
2863
+ const title = name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2864
+ return { title, slug: name, body: f.content.trim(), collection: f.collection, tags: f.tags };
2865
+ });
2866
+ const imported = importFromJson(items, changedBy);
2867
+ return { scanned, imported };
2868
+ }
2869
+ function importFromClaudeCommands(files, changedBy) {
2870
+ const items = files.map((f) => {
2871
+ const name = f.filename.replace(/\.md$/, "");
2872
+ const title = name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2873
+ return {
2874
+ title,
2875
+ slug: name,
2876
+ body: f.content.trim(),
2877
+ collection: "claude-commands",
2878
+ tags: ["claude", "slash-command"]
2879
+ };
2880
+ });
2881
+ return importFromJson(items, changedBy);
2882
+ }
2883
+ var init_importer = __esm(() => {
2884
+ init_prompts();
2885
+ });
2886
+
2887
+ // node_modules/commander/esm.mjs
2888
+ var import__ = __toESM(require_commander(), 1);
2889
+ var {
2890
+ program,
2891
+ createCommand,
2892
+ createArgument,
2893
+ createOption,
2894
+ CommanderError,
2895
+ InvalidArgumentError,
2896
+ InvalidOptionArgumentError,
2897
+ Command,
2898
+ Argument,
2899
+ Option,
2900
+ Help
2901
+ } = import__.default;
2902
+
2903
+ // src/cli/index.tsx
2904
+ init_prompts();
2905
+ import chalk from "chalk";
2906
+ import { createRequire } from "module";
2619
2907
 
2620
2908
  // src/db/versions.ts
2909
+ init_database();
2910
+ init_ids();
2911
+ init_types();
2621
2912
  function rowToVersion(row) {
2622
2913
  return {
2623
2914
  id: row["id"],
@@ -2656,7 +2947,64 @@ function restoreVersion(promptId, version, changedBy) {
2656
2947
  VALUES (?, ?, ?, ?, ?)`, [generateId("VER"), promptId, ver.body, newVersion, changedBy ?? null]);
2657
2948
  }
2658
2949
 
2950
+ // src/cli/index.tsx
2951
+ init_collections();
2952
+
2953
+ // src/db/projects.ts
2954
+ init_database();
2955
+ init_ids();
2956
+ init_types();
2957
+ function rowToProject(row, promptCount) {
2958
+ return {
2959
+ id: row["id"],
2960
+ name: row["name"],
2961
+ slug: row["slug"],
2962
+ description: row["description"] ?? null,
2963
+ path: row["path"] ?? null,
2964
+ prompt_count: promptCount,
2965
+ created_at: row["created_at"]
2966
+ };
2967
+ }
2968
+ function createProject(input) {
2969
+ const db = getDatabase();
2970
+ const id = generateId("proj");
2971
+ const slug = generateSlug(input.name);
2972
+ db.run(`INSERT INTO projects (id, name, slug, description, path) VALUES (?, ?, ?, ?, ?)`, [id, input.name, slug, input.description ?? null, input.path ?? null]);
2973
+ return getProject(id);
2974
+ }
2975
+ function getProject(idOrSlug) {
2976
+ const db = getDatabase();
2977
+ const id = resolveProject(db, idOrSlug);
2978
+ if (!id)
2979
+ return null;
2980
+ const row = db.query("SELECT * FROM projects WHERE id = ?").get(id);
2981
+ if (!row)
2982
+ return null;
2983
+ const countRow = db.query("SELECT COUNT(*) as n FROM prompts WHERE project_id = ?").get(id);
2984
+ return rowToProject(row, countRow.n);
2985
+ }
2986
+ function listProjects() {
2987
+ const db = getDatabase();
2988
+ const rows = db.query("SELECT * FROM projects ORDER BY name ASC").all();
2989
+ return rows.map((row) => {
2990
+ const countRow = db.query("SELECT COUNT(*) as n FROM prompts WHERE project_id = ?").get(row["id"]);
2991
+ return rowToProject(row, countRow.n);
2992
+ });
2993
+ }
2994
+ function deleteProject(idOrSlug) {
2995
+ const db = getDatabase();
2996
+ const id = resolveProject(db, idOrSlug);
2997
+ if (!id)
2998
+ throw new ProjectNotFoundError(idOrSlug);
2999
+ db.run("DELETE FROM projects WHERE id = ?", [id]);
3000
+ }
3001
+
3002
+ // src/cli/index.tsx
3003
+ init_database();
3004
+
2659
3005
  // src/lib/search.ts
3006
+ init_database();
3007
+ init_prompts();
2660
3008
  function rowToSearchResult(row, snippet) {
2661
3009
  return {
2662
3010
  prompt: {
@@ -2670,6 +3018,9 @@ function rowToSearchResult(row, snippet) {
2670
3018
  tags: JSON.parse(row["tags"] || "[]"),
2671
3019
  variables: JSON.parse(row["variables"] || "[]"),
2672
3020
  pinned: Boolean(row["pinned"]),
3021
+ next_prompt: row["next_prompt"] ?? null,
3022
+ expires_at: row["expires_at"] ?? null,
3023
+ project_id: row["project_id"] ?? null,
2673
3024
  is_template: Boolean(row["is_template"]),
2674
3025
  source: row["source"],
2675
3026
  version: row["version"],
@@ -2713,6 +3064,10 @@ function searchPrompts(query, filter = {}) {
2713
3064
  for (const tag of filter.tags)
2714
3065
  params.push(`%"${tag}"%`);
2715
3066
  }
3067
+ if (filter.project_id !== undefined && filter.project_id !== null) {
3068
+ conditions.push("(p.project_id = ? OR p.project_id IS NULL)");
3069
+ params.push(filter.project_id);
3070
+ }
2716
3071
  const where = conditions.length > 0 ? `AND ${conditions.join(" AND ")}` : "";
2717
3072
  const limit = filter.limit ?? 50;
2718
3073
  const offset = filter.offset ?? 0;
@@ -2735,40 +3090,31 @@ function searchPrompts(query, filter = {}) {
2735
3090
  LIMIT ? OFFSET ?`).all(like, like, like, like, like, like, filter.limit ?? 50, filter.offset ?? 0);
2736
3091
  return rows.map((r) => rowToSearchResult(r));
2737
3092
  }
2738
-
2739
- // src/lib/importer.ts
2740
- function importFromJson(items, changedBy) {
2741
- let created = 0;
2742
- let updated = 0;
2743
- const errors = [];
2744
- for (const item of items) {
2745
- try {
2746
- const input = {
2747
- title: item.title,
2748
- body: item.body,
2749
- slug: item.slug,
2750
- description: item.description,
2751
- collection: item.collection,
2752
- tags: item.tags,
2753
- source: "imported",
2754
- changed_by: changedBy
2755
- };
2756
- const { created: wasCreated } = upsertPrompt(input);
2757
- if (wasCreated)
2758
- created++;
2759
- else
2760
- updated++;
2761
- } catch (e) {
2762
- errors.push({ item: item.title, error: e instanceof Error ? e.message : String(e) });
2763
- }
3093
+ function findSimilar(promptId, limit = 5) {
3094
+ const db = getDatabase();
3095
+ const prompt = db.query("SELECT * FROM prompts WHERE id = ?").get(promptId);
3096
+ if (!prompt)
3097
+ return [];
3098
+ const tags = JSON.parse(prompt["tags"] || "[]");
3099
+ const collection = prompt["collection"];
3100
+ if (tags.length === 0) {
3101
+ const rows = db.query("SELECT *, 1 as score FROM prompts WHERE collection = ? AND id != ? ORDER BY use_count DESC LIMIT ?").all(collection, promptId, limit);
3102
+ return rows.map((r) => rowToSearchResult(r));
2764
3103
  }
2765
- return { created, updated, errors };
2766
- }
2767
- function exportToJson(collection) {
2768
- const prompts = listPrompts({ collection, limit: 1e4 });
2769
- return { prompts, exported_at: new Date().toISOString(), collection };
3104
+ const allRows = db.query("SELECT * FROM prompts WHERE id != ?").all(promptId);
3105
+ const scored = allRows.map((row) => {
3106
+ const rowTags = JSON.parse(row["tags"] || "[]");
3107
+ const overlap = rowTags.filter((t) => tags.includes(t)).length;
3108
+ const sameCollection = row["collection"] === collection ? 1 : 0;
3109
+ return { row, score: overlap * 2 + sameCollection };
3110
+ });
3111
+ return scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score).slice(0, limit).map((s) => rowToSearchResult(s.row, undefined));
2770
3112
  }
2771
3113
 
3114
+ // src/cli/index.tsx
3115
+ init_template();
3116
+ init_importer();
3117
+
2772
3118
  // src/lib/lint.ts
2773
3119
  function lintPrompt(p) {
2774
3120
  const issues = [];
@@ -2803,13 +3149,285 @@ function lintAll(prompts) {
2803
3149
  return prompts.map((p) => ({ prompt: p, issues: lintPrompt(p) })).filter((r) => r.issues.length > 0);
2804
3150
  }
2805
3151
 
3152
+ // src/lib/audit.ts
3153
+ init_database();
3154
+ function runAudit() {
3155
+ const db = getDatabase();
3156
+ const issues = [];
3157
+ const orphaned = db.query(`
3158
+ SELECT p.id, p.slug FROM prompts p
3159
+ WHERE p.project_id IS NOT NULL
3160
+ AND NOT EXISTS (SELECT 1 FROM projects pr WHERE pr.id = p.project_id)
3161
+ `).all();
3162
+ for (const p of orphaned) {
3163
+ issues.push({
3164
+ type: "orphaned-project",
3165
+ severity: "error",
3166
+ prompt_id: p.id,
3167
+ slug: p.slug,
3168
+ message: `Prompt "${p.slug}" references a deleted project`
3169
+ });
3170
+ }
3171
+ const emptyCollections = db.query(`
3172
+ SELECT c.name FROM collections c
3173
+ WHERE NOT EXISTS (SELECT 1 FROM prompts p WHERE p.collection = c.name)
3174
+ AND c.name != 'default'
3175
+ `).all();
3176
+ for (const c of emptyCollections) {
3177
+ issues.push({
3178
+ type: "empty-collection",
3179
+ severity: "info",
3180
+ message: `Collection "${c.name}" has no prompts`
3181
+ });
3182
+ }
3183
+ const missingHistory = db.query(`
3184
+ SELECT p.id, p.slug FROM prompts p
3185
+ WHERE NOT EXISTS (SELECT 1 FROM prompt_versions v WHERE v.prompt_id = p.id)
3186
+ `).all();
3187
+ for (const p of missingHistory) {
3188
+ issues.push({
3189
+ type: "missing-version-history",
3190
+ severity: "warn",
3191
+ prompt_id: p.id,
3192
+ slug: p.slug,
3193
+ message: `Prompt "${p.slug}" has no version history entries`
3194
+ });
3195
+ }
3196
+ const slugs = db.query("SELECT id, slug FROM prompts").all();
3197
+ const seen = new Map;
3198
+ for (const { id, slug } of slugs) {
3199
+ const base = slug.replace(/-\d+$/, "");
3200
+ if (seen.has(base) && seen.get(base) !== id) {
3201
+ issues.push({
3202
+ type: "near-duplicate-slug",
3203
+ severity: "info",
3204
+ slug,
3205
+ message: `"${slug}" looks like a duplicate of "${base}" \u2014 consider merging`
3206
+ });
3207
+ } else {
3208
+ seen.set(base, id);
3209
+ }
3210
+ }
3211
+ const now = new Date().toISOString();
3212
+ const expired = db.query(`
3213
+ SELECT id, slug FROM prompts WHERE expires_at IS NOT NULL AND expires_at < ?
3214
+ `).all(now);
3215
+ for (const p of expired) {
3216
+ issues.push({
3217
+ type: "expired",
3218
+ severity: "warn",
3219
+ prompt_id: p.id,
3220
+ slug: p.slug,
3221
+ message: `Prompt "${p.slug}" has expired`
3222
+ });
3223
+ }
3224
+ const errors = issues.filter((i) => i.severity === "error").length;
3225
+ const warnings = issues.filter((i) => i.severity === "warn").length;
3226
+ const info = issues.filter((i) => i.severity === "info").length;
3227
+ return { issues, errors, warnings, info, checked_at: new Date().toISOString() };
3228
+ }
3229
+
3230
+ // src/lib/completion.ts
3231
+ var SUBCOMMANDS = [
3232
+ "save",
3233
+ "use",
3234
+ "get",
3235
+ "list",
3236
+ "search",
3237
+ "render",
3238
+ "templates",
3239
+ "inspect",
3240
+ "update",
3241
+ "delete",
3242
+ "history",
3243
+ "restore",
3244
+ "collections",
3245
+ "move",
3246
+ "pin",
3247
+ "unpin",
3248
+ "copy",
3249
+ "recent",
3250
+ "stale",
3251
+ "unused",
3252
+ "lint",
3253
+ "stats",
3254
+ "export",
3255
+ "import",
3256
+ "import-slash-commands",
3257
+ "watch",
3258
+ "similar",
3259
+ "diff",
3260
+ "duplicate",
3261
+ "trending",
3262
+ "audit",
3263
+ "completion",
3264
+ "project"
3265
+ ];
3266
+ var GLOBAL_OPTIONS = ["--json", "--project"];
3267
+ var SOURCES = ["manual", "ai-session", "imported"];
3268
+ function generateZshCompletion() {
3269
+ return `#compdef prompts
3270
+
3271
+ _prompts() {
3272
+ local state line
3273
+ typeset -A opt_args
3274
+
3275
+ _arguments \\
3276
+ '--json[Output as JSON]' \\
3277
+ '--project[Active project]:project:->projects' \\
3278
+ '1:command:->commands' \\
3279
+ '*::args:->args'
3280
+
3281
+ case $state in
3282
+ commands)
3283
+ local commands=(${SUBCOMMANDS.map((c) => `'${c}'`).join(" ")})
3284
+ _describe 'command' commands
3285
+ ;;
3286
+ projects)
3287
+ local projects=($(prompts project list --json 2>/dev/null | command grep -o '"slug":"[^"]*"' | cut -d'"' -f4))
3288
+ _describe 'project' projects
3289
+ ;;
3290
+ args)
3291
+ case $line[1] in
3292
+ use|get|copy|pin|unpin|inspect|history|diff|duplicate|similar)
3293
+ local slugs=($(prompts list --json 2>/dev/null | command grep -o '"slug":"[^"]*"' | cut -d'"' -f4))
3294
+ _describe 'prompt' slugs
3295
+ ;;
3296
+ save|update)
3297
+ _arguments \\
3298
+ '-b[Body]:body:' \\
3299
+ '-f[File]:file:_files' \\
3300
+ '-s[Slug]:slug:' \\
3301
+ '-d[Description]:description:' \\
3302
+ '-c[Collection]:collection:($(prompts collections --json 2>/dev/null | command grep -o '"name":"[^"]*"' | cut -d'"' -f4))' \\
3303
+ '-t[Tags]:tags:' \\
3304
+ '--source[Source]:source:(${SOURCES.join(" ")})' \\
3305
+ '--pin[Pin immediately]' \\
3306
+ '--force[Force save]'
3307
+ ;;
3308
+ list|search)
3309
+ _arguments \\
3310
+ '-c[Collection]:collection:($(prompts collections --json 2>/dev/null | command grep -o '"name":"[^"]*"' | cut -d'"' -f4))' \\
3311
+ '-t[Tags]:tags:' \\
3312
+ '--templates[Templates only]' \\
3313
+ '--recent[Sort by recent]' \\
3314
+ '-n[Limit]:number:'
3315
+ ;;
3316
+ move)
3317
+ local slugs=($(prompts list --json 2>/dev/null | command grep -o '"slug":"[^"]*"' | cut -d'"' -f4))
3318
+ _describe 'prompt' slugs
3319
+ ;;
3320
+ restore)
3321
+ local slugs=($(prompts list --json 2>/dev/null | command grep -o '"slug":"[^"]*"' | cut -d'"' -f4))
3322
+ _describe 'prompt' slugs
3323
+ ;;
3324
+ completion)
3325
+ _arguments '1:shell:(zsh bash)'
3326
+ ;;
3327
+ esac
3328
+ ;;
3329
+ esac
3330
+ }
3331
+
3332
+ _prompts
3333
+ `;
3334
+ }
3335
+ function generateBashCompletion() {
3336
+ return `_prompts_completions() {
3337
+ local cur prev words
3338
+ COMPREPLY=()
3339
+ cur="\${COMP_WORDS[COMP_CWORD]}"
3340
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
3341
+
3342
+ local subcommands="${SUBCOMMANDS.join(" ")}"
3343
+
3344
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
3345
+ COMPREPLY=($(compgen -W "\${subcommands}" -- "\${cur}"))
3346
+ return 0
3347
+ fi
3348
+
3349
+ case "\${prev}" in
3350
+ --project)
3351
+ local projects=$(prompts project list --json 2>/dev/null | grep -o '"slug":"[^"]*"' | cut -d'"' -f4)
3352
+ COMPREPLY=($(compgen -W "\${projects}" -- "\${cur}"))
3353
+ return 0
3354
+ ;;
3355
+ -c)
3356
+ local cols=$(prompts collections --json 2>/dev/null | grep -o '"name":"[^"]*"' | cut -d'"' -f4)
3357
+ COMPREPLY=($(compgen -W "\${cols}" -- "\${cur}"))
3358
+ return 0
3359
+ ;;
3360
+ --source)
3361
+ COMPREPLY=($(compgen -W "${SOURCES.join(" ")}" -- "\${cur}"))
3362
+ return 0
3363
+ ;;
3364
+ esac
3365
+
3366
+ local cmd="\${COMP_WORDS[1]}"
3367
+ case "\${cmd}" in
3368
+ use|get|copy|pin|unpin|inspect|history|diff|duplicate|similar|render|restore|move|update|delete)
3369
+ local slugs=$(prompts list --json 2>/dev/null | grep -o '"slug":"[^"]*"' | cut -d'"' -f4)
3370
+ COMPREPLY=($(compgen -W "\${slugs}" -- "\${cur}"))
3371
+ ;;
3372
+ completion)
3373
+ COMPREPLY=($(compgen -W "zsh bash" -- "\${cur}"))
3374
+ ;;
3375
+ *)
3376
+ COMPREPLY=($(compgen -W "${GLOBAL_OPTIONS.join(" ")}" -- "\${cur}"))
3377
+ ;;
3378
+ esac
3379
+ }
3380
+
3381
+ complete -F _prompts_completions prompts
3382
+ `;
3383
+ }
3384
+
3385
+ // src/lib/diff.ts
3386
+ function diffTexts(a, b) {
3387
+ const aLines = a.split(`
3388
+ `);
3389
+ const bLines = b.split(`
3390
+ `);
3391
+ const m = aLines.length;
3392
+ const n = bLines.length;
3393
+ const lcs = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
3394
+ for (let i2 = 1;i2 <= m; i2++) {
3395
+ for (let j2 = 1;j2 <= n; j2++) {
3396
+ lcs[i2][j2] = aLines[i2 - 1] === bLines[j2 - 1] ? (lcs[i2 - 1][j2 - 1] ?? 0) + 1 : Math.max(lcs[i2 - 1][j2] ?? 0, lcs[i2][j2 - 1] ?? 0);
3397
+ }
3398
+ }
3399
+ const trace = [];
3400
+ let i = m, j = n;
3401
+ while (i > 0 || j > 0) {
3402
+ if (i > 0 && j > 0 && aLines[i - 1] === bLines[j - 1]) {
3403
+ trace.unshift({ type: "unchanged", content: aLines[i - 1] ?? "" });
3404
+ i--;
3405
+ j--;
3406
+ } else if (j > 0 && (i === 0 || (lcs[i][j - 1] ?? 0) >= (lcs[i - 1][j] ?? 0))) {
3407
+ trace.unshift({ type: "added", content: bLines[j - 1] ?? "" });
3408
+ j--;
3409
+ } else {
3410
+ trace.unshift({ type: "removed", content: aLines[i - 1] ?? "" });
3411
+ i--;
3412
+ }
3413
+ }
3414
+ return trace;
3415
+ }
3416
+
2806
3417
  // src/cli/index.tsx
2807
3418
  var require2 = createRequire(import.meta.url);
2808
3419
  var pkg = require2("../../package.json");
2809
- var program2 = new Command().name("prompts").version(pkg.version).description("Reusable prompt library \u2014 save, search, render prompts from any AI session").option("--json", "Output as JSON");
3420
+ var program2 = new Command().name("prompts").version(pkg.version).description("Reusable prompt library \u2014 save, search, render prompts from any AI session").option("--json", "Output as JSON").option("--project <name>", "Active project (name, slug, or ID) for scoped operations");
2810
3421
  function isJson() {
2811
3422
  return Boolean(program2.opts()["json"]);
2812
3423
  }
3424
+ function getActiveProjectId() {
3425
+ const projectName = program2.opts()["project"] ?? process.env["PROMPTS_PROJECT"];
3426
+ if (!projectName)
3427
+ return null;
3428
+ const db = getDatabase();
3429
+ return resolveProject(db, projectName);
3430
+ }
2813
3431
  function output(data) {
2814
3432
  if (isJson()) {
2815
3433
  console.log(JSON.stringify(data, null, 2));
@@ -2832,7 +3450,7 @@ function fmtPrompt(p) {
2832
3450
  const pin = p.pinned ? chalk.yellow(" \uD83D\uDCCC") : "";
2833
3451
  return `${chalk.bold(p.id)} ${chalk.green(p.slug)}${template}${pin} ${p.title}${tags} ${chalk.gray(p.collection)}`;
2834
3452
  }
2835
- program2.command("save <title>").description("Save a new prompt (or update existing by slug)").option("-b, --body <body>", "Prompt body (use - to read from stdin)").option("-f, --file <path>", "Read body from file").option("-s, --slug <slug>", "Custom slug").option("-d, --description <desc>", "Short description").option("-c, --collection <name>", "Collection", "default").option("-t, --tags <tags>", "Comma-separated tags").option("--source <source>", "Source: manual|ai-session|imported", "manual").option("--agent <name>", "Agent name (for attribution)").option("--force", "Save even if a similar prompt already exists").action(async (title, opts) => {
3453
+ program2.command("save <title>").description("Save a new prompt (or update existing by slug)").option("-b, --body <body>", "Prompt body (use - to read from stdin)").option("-f, --file <path>", "Read body from file").option("-s, --slug <slug>", "Custom slug").option("-d, --description <desc>", "Short description").option("-c, --collection <name>", "Collection", "default").option("-t, --tags <tags>", "Comma-separated tags").option("--source <source>", "Source: manual|ai-session|imported", "manual").option("--agent <name>", "Agent name (for attribution)").option("--force", "Save even if a similar prompt already exists").option("--pin", "Pin immediately so it appears first in all lists").action(async (title, opts) => {
2836
3454
  try {
2837
3455
  let body = opts["body"] ?? "";
2838
3456
  if (opts["file"]) {
@@ -2846,6 +3464,7 @@ program2.command("save <title>").description("Save a new prompt (or update exist
2846
3464
  }
2847
3465
  if (!body)
2848
3466
  handleError("No body provided. Use --body, --file, or pipe via stdin.");
3467
+ const project_id = getActiveProjectId();
2849
3468
  const { prompt, created, duplicate_warning } = upsertPrompt({
2850
3469
  title,
2851
3470
  body,
@@ -2854,18 +3473,23 @@ program2.command("save <title>").description("Save a new prompt (or update exist
2854
3473
  collection: opts["collection"],
2855
3474
  tags: opts["tags"] ? opts["tags"].split(",").map((t) => t.trim()) : [],
2856
3475
  source: opts["source"] || "manual",
2857
- changed_by: opts["agent"]
3476
+ changed_by: opts["agent"],
3477
+ project_id
2858
3478
  }, Boolean(opts["force"]));
2859
3479
  if (duplicate_warning && !isJson()) {
2860
3480
  console.warn(chalk.yellow(`Warning: ${duplicate_warning}`));
2861
3481
  }
3482
+ if (opts["pin"])
3483
+ pinPrompt(prompt.id, true);
2862
3484
  if (isJson()) {
2863
- output(prompt);
3485
+ output(opts["pin"] ? { ...prompt, pinned: true } : prompt);
2864
3486
  } else {
2865
3487
  const action = created ? chalk.green("Created") : chalk.yellow("Updated");
2866
3488
  console.log(`${action} ${chalk.bold(prompt.id)} \u2014 ${chalk.green(prompt.slug)}`);
2867
3489
  console.log(chalk.gray(` Title: ${prompt.title}`));
2868
3490
  console.log(chalk.gray(` Collection: ${prompt.collection}`));
3491
+ if (opts["pin"])
3492
+ console.log(chalk.yellow(" \uD83D\uDCCC Pinned"));
2869
3493
  if (prompt.is_template) {
2870
3494
  const vars = extractVariableInfo(prompt.body);
2871
3495
  console.log(chalk.cyan(` Template vars: ${vars.map((v) => v.name).join(", ")}`));
@@ -2875,13 +3499,33 @@ program2.command("save <title>").description("Save a new prompt (or update exist
2875
3499
  handleError(e);
2876
3500
  }
2877
3501
  });
2878
- program2.command("use <id>").description("Get a prompt's body and increment its use counter").action((id) => {
3502
+ program2.command("use <id>").description("Get a prompt's body and increment its use counter").option("--edit", "Open in $EDITOR for quick tweaks before printing").action(async (id, opts) => {
2879
3503
  try {
2880
3504
  const prompt = usePrompt(id);
3505
+ let body = prompt.body;
3506
+ if (opts.edit) {
3507
+ const editor = process.env["EDITOR"] ?? process.env["VISUAL"] ?? "nano";
3508
+ const { writeFileSync, readFileSync, unlinkSync } = await import("fs");
3509
+ const { tmpdir } = await import("os");
3510
+ const { join: join2 } = await import("path");
3511
+ const tmp = join2(tmpdir(), `prompts-${prompt.id}-${Date.now()}.md`);
3512
+ writeFileSync(tmp, body);
3513
+ const proc = Bun.spawnSync([editor, tmp], { stdio: ["inherit", "inherit", "inherit"] });
3514
+ if (proc.exitCode === 0) {
3515
+ body = readFileSync(tmp, "utf-8");
3516
+ }
3517
+ try {
3518
+ unlinkSync(tmp);
3519
+ } catch {}
3520
+ }
2881
3521
  if (isJson()) {
2882
- output(prompt);
3522
+ output({ ...prompt, body });
2883
3523
  } else {
2884
- console.log(prompt.body);
3524
+ console.log(body);
3525
+ if (prompt.next_prompt) {
3526
+ console.error(chalk.gray(`
3527
+ \u2192 next: ${chalk.bold(prompt.next_prompt)}`));
3528
+ }
2885
3529
  }
2886
3530
  } catch (e) {
2887
3531
  handleError(e);
@@ -2899,11 +3543,13 @@ program2.command("get <id>").description("Get prompt details without incrementin
2899
3543
  });
2900
3544
  program2.command("list").description("List prompts").option("-c, --collection <name>", "Filter by collection").option("-t, --tags <tags>", "Filter by tags (comma-separated)").option("--templates", "Show only templates").option("--recent", "Sort by recently used").option("-n, --limit <n>", "Max results", "50").action((opts) => {
2901
3545
  try {
3546
+ const project_id = getActiveProjectId();
2902
3547
  let prompts = listPrompts({
2903
3548
  collection: opts["collection"],
2904
3549
  tags: opts["tags"] ? opts["tags"].split(",").map((t) => t.trim()) : undefined,
2905
3550
  is_template: opts["templates"] ? true : undefined,
2906
- limit: parseInt(opts["limit"]) || 50
3551
+ limit: parseInt(opts["limit"]) || 50,
3552
+ ...project_id !== null ? { project_id } : {}
2907
3553
  });
2908
3554
  if (opts["recent"]) {
2909
3555
  prompts = prompts.filter((p) => p.last_used_at !== null).sort((a, b) => (b.last_used_at ?? "").localeCompare(a.last_used_at ?? ""));
@@ -2924,10 +3570,12 @@ ${prompts.length} prompt(s)`));
2924
3570
  });
2925
3571
  program2.command("search <query>").description("Full-text search across prompts (FTS5)").option("-c, --collection <name>").option("-t, --tags <tags>").option("-n, --limit <n>", "Max results", "20").action((query, opts) => {
2926
3572
  try {
3573
+ const project_id = getActiveProjectId();
2927
3574
  const results = searchPrompts(query, {
2928
3575
  collection: opts["collection"],
2929
3576
  tags: opts["tags"] ? opts["tags"].split(",").map((t) => t.trim()) : undefined,
2930
- limit: parseInt(opts["limit"] ?? "20") || 20
3577
+ limit: parseInt(opts["limit"] ?? "20") || 20,
3578
+ ...project_id !== null ? { project_id } : {}
2931
3579
  });
2932
3580
  if (isJson()) {
2933
3581
  output(results);
@@ -3243,18 +3891,29 @@ program2.command("stale [days]").description("List prompts not used in N days (d
3243
3891
  const cutoff = new Date(Date.now() - threshold * 24 * 60 * 60 * 1000).toISOString();
3244
3892
  const all = listPrompts({ limit: 1e4 });
3245
3893
  const stale = all.filter((p) => p.last_used_at === null || p.last_used_at < cutoff).sort((a, b) => (a.last_used_at ?? "").localeCompare(b.last_used_at ?? ""));
3894
+ const now = new Date().toISOString();
3895
+ const expired = all.filter((p) => p.expires_at !== null && p.expires_at < now);
3246
3896
  if (isJson()) {
3247
- output(stale);
3897
+ output({ stale, expired });
3248
3898
  return;
3249
3899
  }
3250
- if (stale.length === 0) {
3900
+ if (expired.length > 0) {
3901
+ console.log(chalk.red(`
3902
+ Expired (${expired.length}):`));
3903
+ for (const p of expired)
3904
+ console.log(chalk.red(` \u2717 ${p.slug}`) + chalk.gray(` expired ${new Date(p.expires_at).toLocaleDateString()}`));
3905
+ }
3906
+ if (stale.length === 0 && expired.length === 0) {
3251
3907
  console.log(chalk.green(`No stale prompts (threshold: ${threshold} days).`));
3252
3908
  return;
3253
3909
  }
3254
- console.log(chalk.bold(`Stale prompts (not used in ${threshold}+ days):`));
3255
- for (const p of stale) {
3256
- const last = p.last_used_at ? chalk.gray(new Date(p.last_used_at).toLocaleDateString()) : chalk.red("never");
3257
- console.log(` ${chalk.green(p.slug)} ${chalk.gray(`${p.use_count}\xD7`)} last used: ${last}`);
3910
+ if (stale.length > 0) {
3911
+ console.log(chalk.bold(`
3912
+ Stale prompts (not used in ${threshold}+ days):`));
3913
+ for (const p of stale) {
3914
+ const last = p.last_used_at ? chalk.gray(new Date(p.last_used_at).toLocaleDateString()) : chalk.red("never");
3915
+ console.log(` ${chalk.green(p.slug)} ${chalk.gray(`${p.use_count}\xD7`)} last used: ${last}`);
3916
+ }
3258
3917
  }
3259
3918
  console.log(chalk.gray(`
3260
3919
  ${stale.length} stale prompt(s)`));
@@ -3316,4 +3975,343 @@ program2.command("copy <id>").description("Copy prompt body to clipboard and inc
3316
3975
  handleError(e);
3317
3976
  }
3318
3977
  });
3978
+ var projectCmd = program2.command("project").description("Manage projects");
3979
+ projectCmd.command("create <name>").description("Create a new project").option("-d, --description <desc>", "Short description").option("--path <path>", "Filesystem path this project maps to").action((name, opts) => {
3980
+ try {
3981
+ const project = createProject({ name, description: opts["description"], path: opts["path"] });
3982
+ if (isJson())
3983
+ output(project);
3984
+ else {
3985
+ console.log(`${chalk.green("Created")} project ${chalk.bold(project.name)} \u2014 ${chalk.gray(project.slug)}`);
3986
+ if (project.description)
3987
+ console.log(chalk.gray(` ${project.description}`));
3988
+ }
3989
+ } catch (e) {
3990
+ handleError(e);
3991
+ }
3992
+ });
3993
+ projectCmd.command("list").description("List all projects").action(() => {
3994
+ try {
3995
+ const projects = listProjects();
3996
+ if (isJson()) {
3997
+ output(projects);
3998
+ return;
3999
+ }
4000
+ if (projects.length === 0) {
4001
+ console.log(chalk.gray("No projects."));
4002
+ return;
4003
+ }
4004
+ for (const p of projects) {
4005
+ console.log(`${chalk.bold(p.name)} ${chalk.gray(p.slug)} ${chalk.cyan(`${p.prompt_count} prompt(s)`)}`);
4006
+ if (p.description)
4007
+ console.log(chalk.gray(` ${p.description}`));
4008
+ }
4009
+ } catch (e) {
4010
+ handleError(e);
4011
+ }
4012
+ });
4013
+ projectCmd.command("get <id>").description("Get project details").action((id) => {
4014
+ try {
4015
+ const project = getProject(id);
4016
+ if (!project)
4017
+ handleError(`Project not found: ${id}`);
4018
+ output(isJson() ? project : `${chalk.bold(project.name)} ${chalk.gray(project.slug)} ${chalk.cyan(`${project.prompt_count} prompt(s)`)}`);
4019
+ } catch (e) {
4020
+ handleError(e);
4021
+ }
4022
+ });
4023
+ projectCmd.command("prompts <id>").description("List all prompts for a project (project-scoped + globals)").option("-n, --limit <n>", "Max results", "100").action((id, opts) => {
4024
+ try {
4025
+ const project = getProject(id);
4026
+ if (!project)
4027
+ handleError(`Project not found: ${id}`);
4028
+ const prompts = listPrompts({ project_id: project.id, limit: parseInt(opts["limit"] ?? "100") || 100 });
4029
+ if (isJson()) {
4030
+ output(prompts);
4031
+ return;
4032
+ }
4033
+ if (prompts.length === 0) {
4034
+ console.log(chalk.gray("No prompts."));
4035
+ return;
4036
+ }
4037
+ console.log(chalk.bold(`Prompts for project: ${project.name}`));
4038
+ for (const p of prompts) {
4039
+ const scope = p.project_id ? chalk.cyan(" [project]") : chalk.gray(" [global]");
4040
+ console.log(fmtPrompt(p) + scope);
4041
+ }
4042
+ console.log(chalk.gray(`
4043
+ ${prompts.length} prompt(s)`));
4044
+ } catch (e) {
4045
+ handleError(e);
4046
+ }
4047
+ });
4048
+ projectCmd.command("delete <id>").description("Delete a project (prompts become global)").option("-y, --yes", "Skip confirmation").action(async (id, opts) => {
4049
+ try {
4050
+ const project = getProject(id);
4051
+ if (!project)
4052
+ handleError(`Project not found: ${id}`);
4053
+ if (!opts.yes && !isJson()) {
4054
+ const { createInterface } = await import("readline");
4055
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
4056
+ await new Promise((resolve) => {
4057
+ rl.question(chalk.yellow(`Delete project "${project.name}"? Prompts will become global. [y/N] `), (ans) => {
4058
+ rl.close();
4059
+ if (ans.toLowerCase() !== "y") {
4060
+ console.log("Cancelled.");
4061
+ process.exit(0);
4062
+ }
4063
+ resolve();
4064
+ });
4065
+ });
4066
+ }
4067
+ deleteProject(id);
4068
+ if (isJson())
4069
+ output({ deleted: true, id: project.id });
4070
+ else
4071
+ console.log(chalk.red(`Deleted project ${project.name}`));
4072
+ } catch (e) {
4073
+ handleError(e);
4074
+ }
4075
+ });
4076
+ program2.command("audit").description("Check for orphaned project refs, empty collections, missing history, near-duplicate slugs, expired prompts").action(() => {
4077
+ try {
4078
+ const report = runAudit();
4079
+ if (isJson()) {
4080
+ output(report);
4081
+ return;
4082
+ }
4083
+ if (report.issues.length === 0) {
4084
+ console.log(chalk.green("\u2713 No audit issues found."));
4085
+ return;
4086
+ }
4087
+ for (const issue of report.issues) {
4088
+ const sym = issue.severity === "error" ? chalk.red("\u2717") : issue.severity === "warn" ? chalk.yellow("\u26A0") : chalk.gray("\u2139");
4089
+ const slug = issue.slug ? chalk.green(` ${issue.slug}`) : "";
4090
+ console.log(`${sym}${slug} ${issue.message}`);
4091
+ }
4092
+ console.log(chalk.bold(`
4093
+ ${report.issues.length} issue(s) \u2014 ${report.errors} errors, ${report.warnings} warnings, ${report.info} info`));
4094
+ if (report.errors > 0)
4095
+ process.exit(1);
4096
+ } catch (e) {
4097
+ handleError(e);
4098
+ }
4099
+ });
4100
+ program2.command("unused").description("List prompts that have never been used (use_count = 0)").option("-c, --collection <name>").option("-n, --limit <n>", "Max results", "50").action((opts) => {
4101
+ try {
4102
+ const all = listPrompts({ collection: opts["collection"], limit: parseInt(opts["limit"] ?? "50") || 50 });
4103
+ const unused = all.filter((p) => p.use_count === 0).sort((a, b) => a.created_at.localeCompare(b.created_at));
4104
+ if (isJson()) {
4105
+ output(unused);
4106
+ return;
4107
+ }
4108
+ if (unused.length === 0) {
4109
+ console.log(chalk.green("All prompts have been used at least once."));
4110
+ return;
4111
+ }
4112
+ console.log(chalk.bold(`Unused prompts (${unused.length}):`));
4113
+ for (const p of unused) {
4114
+ console.log(` ${fmtPrompt(p)} ${chalk.gray(`created ${new Date(p.created_at).toLocaleDateString()}`)}`);
4115
+ }
4116
+ } catch (e) {
4117
+ handleError(e);
4118
+ }
4119
+ });
4120
+ program2.command("trending").description("Most used prompts in the last N days").option("--days <n>", "Lookback window in days", "7").option("-n, --limit <n>", "Max results", "10").action((opts) => {
4121
+ try {
4122
+ const results = getTrending(parseInt(opts["days"] ?? "7") || 7, parseInt(opts["limit"] ?? "10") || 10);
4123
+ if (isJson()) {
4124
+ output(results);
4125
+ return;
4126
+ }
4127
+ if (results.length === 0) {
4128
+ console.log(chalk.gray("No usage data yet."));
4129
+ return;
4130
+ }
4131
+ console.log(chalk.bold(`Trending (last ${opts["days"] ?? "7"} days):`));
4132
+ for (const r of results) {
4133
+ console.log(` ${chalk.green(r.slug)} ${chalk.bold(String(r.uses))}\xD7 ${chalk.gray(r.title)}`);
4134
+ }
4135
+ } catch (e) {
4136
+ handleError(e);
4137
+ }
4138
+ });
4139
+ program2.command("expire <id> [date]").description("Set expiry date for a prompt (ISO date, e.g. 2026-12-31). Use 'none' to clear.").action((id, date) => {
4140
+ try {
4141
+ const expiresAt = !date || date === "none" ? null : new Date(date).toISOString();
4142
+ const p = setExpiry(id, expiresAt);
4143
+ if (isJson())
4144
+ output(p);
4145
+ else
4146
+ console.log(expiresAt ? chalk.yellow(`Expires ${p.slug} on ${new Date(expiresAt).toLocaleDateString()}`) : chalk.gray(`Cleared expiry for ${p.slug}`));
4147
+ } catch (e) {
4148
+ handleError(e);
4149
+ }
4150
+ });
4151
+ program2.command("duplicate <id>").description("Clone a prompt with a new slug").option("-s, --to <slug>", "New slug (auto-generated if omitted)").option("--title <title>", "New title (defaults to 'Copy of <original>')").action((id, opts) => {
4152
+ try {
4153
+ const source = getPrompt(id);
4154
+ if (!source)
4155
+ handleError(`Prompt not found: ${id}`);
4156
+ const p = source;
4157
+ const { prompt } = upsertPrompt({
4158
+ title: opts["title"] ?? `Copy of ${p.title}`,
4159
+ slug: opts["to"],
4160
+ body: p.body,
4161
+ description: p.description ?? undefined,
4162
+ collection: p.collection,
4163
+ tags: p.tags,
4164
+ source: "manual"
4165
+ });
4166
+ if (isJson())
4167
+ output(prompt);
4168
+ else
4169
+ console.log(`${chalk.green("Duplicated")} ${chalk.bold(p.slug)} \u2192 ${chalk.bold(prompt.slug)}`);
4170
+ } catch (e) {
4171
+ handleError(e);
4172
+ }
4173
+ });
4174
+ program2.command("diff <id> <v1> [v2]").description("Show diff between two versions of a prompt (v2 defaults to current)").action((id, v1, v2) => {
4175
+ try {
4176
+ const prompt = getPrompt(id);
4177
+ if (!prompt)
4178
+ handleError(`Prompt not found: ${id}`);
4179
+ const versions = listVersions(prompt.id);
4180
+ const versionA = versions.find((v) => v.version === parseInt(v1));
4181
+ if (!versionA)
4182
+ handleError(`Version ${v1} not found`);
4183
+ const bodyB = v2 ? versions.find((v) => v.version === parseInt(v2))?.body ?? null : prompt.body;
4184
+ if (bodyB === null)
4185
+ handleError(`Version ${v2} not found`);
4186
+ const lines = diffTexts(versionA.body, bodyB);
4187
+ if (isJson()) {
4188
+ output(lines);
4189
+ return;
4190
+ }
4191
+ const label2 = v2 ? `v${v2}` : "current";
4192
+ console.log(chalk.bold(`${prompt.slug}: v${v1} \u2192 ${label2}`));
4193
+ for (const l of lines) {
4194
+ if (l.type === "added")
4195
+ console.log(chalk.green(`+ ${l.content}`));
4196
+ else if (l.type === "removed")
4197
+ console.log(chalk.red(`- ${l.content}`));
4198
+ else
4199
+ console.log(chalk.gray(` ${l.content}`));
4200
+ }
4201
+ } catch (e) {
4202
+ handleError(e);
4203
+ }
4204
+ });
4205
+ program2.command("chain <id> [next]").description("Set the next prompt in a chain, or show the chain for a prompt. Use 'none' to clear.").action((id, next) => {
4206
+ try {
4207
+ if (next !== undefined) {
4208
+ const nextSlug = next === "none" ? null : next;
4209
+ const p = setNextPrompt(id, nextSlug);
4210
+ if (isJson())
4211
+ output(p);
4212
+ else
4213
+ console.log(nextSlug ? `${chalk.green(p.slug)} \u2192 ${chalk.bold(nextSlug)}` : chalk.gray(`Cleared chain for ${p.slug}`));
4214
+ return;
4215
+ }
4216
+ const prompt = getPrompt(id);
4217
+ if (!prompt)
4218
+ handleError(`Prompt not found: ${id}`);
4219
+ const chain = [];
4220
+ let cur = prompt;
4221
+ const seen = new Set;
4222
+ while (cur && !seen.has(cur.id)) {
4223
+ chain.push({ slug: cur.slug, title: cur.title });
4224
+ seen.add(cur.id);
4225
+ cur = cur.next_prompt ? getPrompt(cur.next_prompt) : null;
4226
+ }
4227
+ if (isJson()) {
4228
+ output(chain);
4229
+ return;
4230
+ }
4231
+ console.log(chain.map((c) => chalk.green(c.slug)).join(chalk.gray(" \u2192 ")));
4232
+ } catch (e) {
4233
+ handleError(e);
4234
+ }
4235
+ });
4236
+ program2.command("similar <id>").description("Find prompts similar to a given prompt (by tag overlap and collection)").option("-n, --limit <n>", "Max results", "5").action((id, opts) => {
4237
+ try {
4238
+ const prompt = getPrompt(id);
4239
+ if (!prompt)
4240
+ handleError(`Prompt not found: ${id}`);
4241
+ const results = findSimilar(prompt.id, parseInt(opts["limit"] ?? "5") || 5);
4242
+ if (isJson()) {
4243
+ output(results);
4244
+ return;
4245
+ }
4246
+ if (results.length === 0) {
4247
+ console.log(chalk.gray("No similar prompts found."));
4248
+ return;
4249
+ }
4250
+ for (const r of results) {
4251
+ const score = chalk.gray(`${Math.round(r.score * 100)}%`);
4252
+ console.log(`${fmtPrompt(r.prompt)} ${score}`);
4253
+ }
4254
+ } catch (e) {
4255
+ handleError(e);
4256
+ }
4257
+ });
4258
+ program2.command("completion [shell]").description("Output shell completion script (zsh or bash)").action((shell) => {
4259
+ const sh = shell ?? (process.env["SHELL"]?.includes("zsh") ? "zsh" : "bash");
4260
+ if (sh === "zsh") {
4261
+ console.log(generateZshCompletion());
4262
+ } else if (sh === "bash") {
4263
+ console.log(generateBashCompletion());
4264
+ } else {
4265
+ handleError(`Unknown shell: ${sh}. Use 'zsh' or 'bash'.`);
4266
+ }
4267
+ });
4268
+ program2.command("watch [dir]").description("Watch a directory for .md changes and auto-import prompts (default: .prompts/)").option("-c, --collection <name>", "Collection to import into", "watched").option("--agent <name>", "Attribution").action(async (dir, opts) => {
4269
+ const { existsSync: existsSync2, mkdirSync: mkdirSync2 } = await import("fs");
4270
+ const { resolve, join: join2 } = await import("path");
4271
+ const watchDir = resolve(dir ?? join2(process.cwd(), ".prompts"));
4272
+ if (!existsSync2(watchDir))
4273
+ mkdirSync2(watchDir, { recursive: true });
4274
+ console.log(chalk.bold(`Watching ${watchDir} for .md changes\u2026`) + chalk.gray(" (Ctrl+C to stop)"));
4275
+ const { importFromMarkdown: importFromMarkdown2 } = await Promise.resolve().then(() => (init_importer(), exports_importer));
4276
+ const { readFileSync } = await import("fs");
4277
+ const fsWatch = (await import("fs")).watch;
4278
+ fsWatch(watchDir, { persistent: true }, async (_event, filename) => {
4279
+ if (!filename?.endsWith(".md"))
4280
+ return;
4281
+ const filePath = join2(watchDir, filename);
4282
+ try {
4283
+ const content = readFileSync(filePath, "utf-8");
4284
+ const result = importFromMarkdown2([{ filename, content }], opts["agent"]);
4285
+ const action = result.created > 0 ? chalk.green("Created") : chalk.yellow("Updated");
4286
+ console.log(`${action}: ${chalk.bold(filename.replace(".md", ""))} ${chalk.gray(new Date().toLocaleTimeString())}`);
4287
+ } catch {
4288
+ console.error(chalk.red(`Failed to import: ${filename}`));
4289
+ }
4290
+ });
4291
+ await new Promise(() => {});
4292
+ });
4293
+ program2.command("import-slash-commands").description("Auto-scan .claude/commands, .codex/skills, .gemini/extensions and import all prompts").option("--dir <path>", "Root dir to scan (default: cwd)", process.cwd()).option("--agent <name>", "Attribution").action((opts) => {
4294
+ try {
4295
+ const { scanned, imported } = scanAndImportSlashCommands(opts["dir"] ?? process.cwd(), opts["agent"]);
4296
+ if (isJson()) {
4297
+ output({ scanned, imported });
4298
+ return;
4299
+ }
4300
+ if (scanned.length === 0) {
4301
+ console.log(chalk.gray("No slash command files found."));
4302
+ return;
4303
+ }
4304
+ console.log(chalk.bold(`Scanned ${scanned.length} file(s):`));
4305
+ for (const s of scanned)
4306
+ console.log(chalk.gray(` ${s.source}/${s.file}`));
4307
+ console.log(`
4308
+ ${chalk.green(`Created: ${imported.created}`)} ${chalk.yellow(`Updated: ${imported.updated}`)}`);
4309
+ if (imported.errors.length > 0) {
4310
+ for (const e of imported.errors)
4311
+ console.error(chalk.red(` \u2717 ${e.item}: ${e.error}`));
4312
+ }
4313
+ } catch (e) {
4314
+ handleError(e);
4315
+ }
4316
+ });
3319
4317
  program2.parse();