@hasna/prompts 0.2.2 → 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"];
@@ -2202,6 +2191,26 @@ function runMigrations(db) {
2202
2191
  CREATE INDEX IF NOT EXISTS idx_prompts_project_id ON prompts(project_id);
2203
2192
  `
2204
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
+ },
2205
2214
  {
2206
2215
  name: "002_fts5",
2207
2216
  sql: `
@@ -2284,6 +2293,8 @@ function resolvePrompt(db, idOrSlug) {
2284
2293
  return byTitle[0].id;
2285
2294
  return null;
2286
2295
  }
2296
+ var _db = null;
2297
+ var init_database = () => {};
2287
2298
 
2288
2299
  // src/lib/ids.ts
2289
2300
  function generateSlug(title) {
@@ -2299,7 +2310,6 @@ function uniqueSlug(baseSlug) {
2299
2310
  }
2300
2311
  return slug;
2301
2312
  }
2302
- var CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
2303
2313
  function nanoid(len) {
2304
2314
  let id = "";
2305
2315
  for (let i = 0;i < len; i++) {
@@ -2318,6 +2328,10 @@ function generatePromptId() {
2318
2328
  function generateId(prefix) {
2319
2329
  return `${prefix}-${nanoid(8)}`;
2320
2330
  }
2331
+ var CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
2332
+ var init_ids = __esm(() => {
2333
+ init_database();
2334
+ });
2321
2335
 
2322
2336
  // src/db/collections.ts
2323
2337
  function rowToCollection(row) {
@@ -2373,6 +2387,10 @@ function movePrompt(promptIdOrSlug, targetCollection) {
2373
2387
  row.id
2374
2388
  ]);
2375
2389
  }
2390
+ var init_collections = __esm(() => {
2391
+ init_database();
2392
+ init_ids();
2393
+ });
2376
2394
 
2377
2395
  // src/lib/duplicates.ts
2378
2396
  function tokenize(text) {
@@ -2403,9 +2421,11 @@ function findDuplicates(body, threshold = 0.8, excludeSlug) {
2403
2421
  }
2404
2422
  return matches.sort((a, b) => b.score - a.score);
2405
2423
  }
2424
+ var init_duplicates = __esm(() => {
2425
+ init_prompts();
2426
+ });
2406
2427
 
2407
2428
  // src/lib/template.ts
2408
- var VAR_PATTERN = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\|\s*(.*?)\s*)?\}\}/g;
2409
2429
  function extractVariables(body) {
2410
2430
  const vars = new Set;
2411
2431
  const pattern = new RegExp(VAR_PATTERN.source, "g");
@@ -2446,34 +2466,39 @@ function renderTemplate(body, vars) {
2446
2466
  });
2447
2467
  return { rendered, missing_vars: missing, used_defaults: usedDefaults };
2448
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
+ });
2449
2473
 
2450
2474
  // src/types/index.ts
2451
- class PromptNotFoundError extends Error {
2452
- constructor(id) {
2453
- super(`Prompt not found: ${id}`);
2454
- this.name = "PromptNotFoundError";
2455
- }
2456
- }
2457
-
2458
- class VersionConflictError extends Error {
2459
- constructor(id) {
2460
- super(`Version conflict on prompt: ${id}`);
2461
- this.name = "VersionConflictError";
2462
- }
2463
- }
2464
-
2465
- class DuplicateSlugError extends Error {
2466
- constructor(slug) {
2467
- super(`A prompt with slug "${slug}" already exists`);
2468
- this.name = "DuplicateSlugError";
2469
- }
2470
- }
2471
- class ProjectNotFoundError extends Error {
2472
- constructor(id) {
2473
- super(`Project not found: ${id}`);
2474
- this.name = "ProjectNotFoundError";
2475
- }
2476
- }
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
+ });
2477
2502
 
2478
2503
  // src/db/prompts.ts
2479
2504
  function rowToPrompt(row) {
@@ -2488,6 +2513,8 @@ function rowToPrompt(row) {
2488
2513
  tags: JSON.parse(row["tags"] || "[]"),
2489
2514
  variables: JSON.parse(row["variables"] || "[]"),
2490
2515
  pinned: Boolean(row["pinned"]),
2516
+ next_prompt: row["next_prompt"] ?? null,
2517
+ expires_at: row["expires_at"] ?? null,
2491
2518
  project_id: row["project_id"] ?? null,
2492
2519
  is_template: Boolean(row["is_template"]),
2493
2520
  source: row["source"],
@@ -2586,6 +2613,7 @@ function updatePrompt(idOrSlug, input) {
2586
2613
  description = COALESCE(?, description),
2587
2614
  collection = COALESCE(?, collection),
2588
2615
  tags = COALESCE(?, tags),
2616
+ next_prompt = CASE WHEN ? IS NOT NULL THEN ? ELSE next_prompt END,
2589
2617
  variables = ?,
2590
2618
  is_template = ?,
2591
2619
  version = version + 1,
@@ -2596,6 +2624,8 @@ function updatePrompt(idOrSlug, input) {
2596
2624
  input.description ?? null,
2597
2625
  input.collection ?? null,
2598
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,
2599
2629
  variables,
2600
2630
  is_template,
2601
2631
  prompt.id,
@@ -2618,6 +2648,30 @@ function usePrompt(idOrSlug) {
2618
2648
  const db = getDatabase();
2619
2649
  const prompt = requirePrompt(idOrSlug);
2620
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]);
2621
2675
  return requirePrompt(prompt.id);
2622
2676
  }
2623
2677
  function pinPrompt(idOrSlug, pinned) {
@@ -2663,8 +2717,198 @@ function getPromptStats() {
2663
2717
  const bySource = db.query("SELECT source, COUNT(*) as count FROM prompts GROUP BY source ORDER BY count DESC").all();
2664
2718
  return { total_prompts: total, total_templates: templates, total_collections: collections, most_used: mostUsed, recently_used: recentlyUsed, by_collection: byCollection, by_source: bySource };
2665
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";
2666
2907
 
2667
2908
  // src/db/versions.ts
2909
+ init_database();
2910
+ init_ids();
2911
+ init_types();
2668
2912
  function rowToVersion(row) {
2669
2913
  return {
2670
2914
  id: row["id"],
@@ -2703,7 +2947,13 @@ function restoreVersion(promptId, version, changedBy) {
2703
2947
  VALUES (?, ?, ?, ?, ?)`, [generateId("VER"), promptId, ver.body, newVersion, changedBy ?? null]);
2704
2948
  }
2705
2949
 
2950
+ // src/cli/index.tsx
2951
+ init_collections();
2952
+
2706
2953
  // src/db/projects.ts
2954
+ init_database();
2955
+ init_ids();
2956
+ init_types();
2707
2957
  function rowToProject(row, promptCount) {
2708
2958
  return {
2709
2959
  id: row["id"],
@@ -2749,7 +2999,12 @@ function deleteProject(idOrSlug) {
2749
2999
  db.run("DELETE FROM projects WHERE id = ?", [id]);
2750
3000
  }
2751
3001
 
3002
+ // src/cli/index.tsx
3003
+ init_database();
3004
+
2752
3005
  // src/lib/search.ts
3006
+ init_database();
3007
+ init_prompts();
2753
3008
  function rowToSearchResult(row, snippet) {
2754
3009
  return {
2755
3010
  prompt: {
@@ -2763,6 +3018,8 @@ function rowToSearchResult(row, snippet) {
2763
3018
  tags: JSON.parse(row["tags"] || "[]"),
2764
3019
  variables: JSON.parse(row["variables"] || "[]"),
2765
3020
  pinned: Boolean(row["pinned"]),
3021
+ next_prompt: row["next_prompt"] ?? null,
3022
+ expires_at: row["expires_at"] ?? null,
2766
3023
  project_id: row["project_id"] ?? null,
2767
3024
  is_template: Boolean(row["is_template"]),
2768
3025
  source: row["source"],
@@ -2833,40 +3090,31 @@ function searchPrompts(query, filter = {}) {
2833
3090
  LIMIT ? OFFSET ?`).all(like, like, like, like, like, like, filter.limit ?? 50, filter.offset ?? 0);
2834
3091
  return rows.map((r) => rowToSearchResult(r));
2835
3092
  }
2836
-
2837
- // src/lib/importer.ts
2838
- function importFromJson(items, changedBy) {
2839
- let created = 0;
2840
- let updated = 0;
2841
- const errors = [];
2842
- for (const item of items) {
2843
- try {
2844
- const input = {
2845
- title: item.title,
2846
- body: item.body,
2847
- slug: item.slug,
2848
- description: item.description,
2849
- collection: item.collection,
2850
- tags: item.tags,
2851
- source: "imported",
2852
- changed_by: changedBy
2853
- };
2854
- const { created: wasCreated } = upsertPrompt(input);
2855
- if (wasCreated)
2856
- created++;
2857
- else
2858
- updated++;
2859
- } catch (e) {
2860
- errors.push({ item: item.title, error: e instanceof Error ? e.message : String(e) });
2861
- }
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));
2862
3103
  }
2863
- return { created, updated, errors };
2864
- }
2865
- function exportToJson(collection) {
2866
- const prompts = listPrompts({ collection, limit: 1e4 });
2867
- 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));
2868
3112
  }
2869
3113
 
3114
+ // src/cli/index.tsx
3115
+ init_template();
3116
+ init_importer();
3117
+
2870
3118
  // src/lib/lint.ts
2871
3119
  function lintPrompt(p) {
2872
3120
  const issues = [];
@@ -2901,6 +3149,271 @@ function lintAll(prompts) {
2901
3149
  return prompts.map((p) => ({ prompt: p, issues: lintPrompt(p) })).filter((r) => r.issues.length > 0);
2902
3150
  }
2903
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
+
2904
3417
  // src/cli/index.tsx
2905
3418
  var require2 = createRequire(import.meta.url);
2906
3419
  var pkg = require2("../../package.json");
@@ -2937,7 +3450,7 @@ function fmtPrompt(p) {
2937
3450
  const pin = p.pinned ? chalk.yellow(" \uD83D\uDCCC") : "";
2938
3451
  return `${chalk.bold(p.id)} ${chalk.green(p.slug)}${template}${pin} ${p.title}${tags} ${chalk.gray(p.collection)}`;
2939
3452
  }
2940
- 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) => {
2941
3454
  try {
2942
3455
  let body = opts["body"] ?? "";
2943
3456
  if (opts["file"]) {
@@ -2966,13 +3479,17 @@ program2.command("save <title>").description("Save a new prompt (or update exist
2966
3479
  if (duplicate_warning && !isJson()) {
2967
3480
  console.warn(chalk.yellow(`Warning: ${duplicate_warning}`));
2968
3481
  }
3482
+ if (opts["pin"])
3483
+ pinPrompt(prompt.id, true);
2969
3484
  if (isJson()) {
2970
- output(prompt);
3485
+ output(opts["pin"] ? { ...prompt, pinned: true } : prompt);
2971
3486
  } else {
2972
3487
  const action = created ? chalk.green("Created") : chalk.yellow("Updated");
2973
3488
  console.log(`${action} ${chalk.bold(prompt.id)} \u2014 ${chalk.green(prompt.slug)}`);
2974
3489
  console.log(chalk.gray(` Title: ${prompt.title}`));
2975
3490
  console.log(chalk.gray(` Collection: ${prompt.collection}`));
3491
+ if (opts["pin"])
3492
+ console.log(chalk.yellow(" \uD83D\uDCCC Pinned"));
2976
3493
  if (prompt.is_template) {
2977
3494
  const vars = extractVariableInfo(prompt.body);
2978
3495
  console.log(chalk.cyan(` Template vars: ${vars.map((v) => v.name).join(", ")}`));
@@ -2982,13 +3499,33 @@ program2.command("save <title>").description("Save a new prompt (or update exist
2982
3499
  handleError(e);
2983
3500
  }
2984
3501
  });
2985
- 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) => {
2986
3503
  try {
2987
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
+ }
2988
3521
  if (isJson()) {
2989
- output(prompt);
3522
+ output({ ...prompt, body });
2990
3523
  } else {
2991
- 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
+ }
2992
3529
  }
2993
3530
  } catch (e) {
2994
3531
  handleError(e);
@@ -3354,18 +3891,29 @@ program2.command("stale [days]").description("List prompts not used in N days (d
3354
3891
  const cutoff = new Date(Date.now() - threshold * 24 * 60 * 60 * 1000).toISOString();
3355
3892
  const all = listPrompts({ limit: 1e4 });
3356
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);
3357
3896
  if (isJson()) {
3358
- output(stale);
3897
+ output({ stale, expired });
3359
3898
  return;
3360
3899
  }
3361
- 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) {
3362
3907
  console.log(chalk.green(`No stale prompts (threshold: ${threshold} days).`));
3363
3908
  return;
3364
3909
  }
3365
- console.log(chalk.bold(`Stale prompts (not used in ${threshold}+ days):`));
3366
- for (const p of stale) {
3367
- const last = p.last_used_at ? chalk.gray(new Date(p.last_used_at).toLocaleDateString()) : chalk.red("never");
3368
- 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
+ }
3369
3917
  }
3370
3918
  console.log(chalk.gray(`
3371
3919
  ${stale.length} stale prompt(s)`));
@@ -3472,6 +4020,31 @@ projectCmd.command("get <id>").description("Get project details").action((id) =>
3472
4020
  handleError(e);
3473
4021
  }
3474
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
+ });
3475
4048
  projectCmd.command("delete <id>").description("Delete a project (prompts become global)").option("-y, --yes", "Skip confirmation").action(async (id, opts) => {
3476
4049
  try {
3477
4050
  const project = getProject(id);
@@ -3500,4 +4073,245 @@ projectCmd.command("delete <id>").description("Delete a project (prompts become
3500
4073
  handleError(e);
3501
4074
  }
3502
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
+ });
3503
4317
  program2.parse();