@hasna/prompts 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,19 +12,19 @@ bun install -g @hasna/prompts
12
12
 
13
13
  ```bash
14
14
  # Save a prompt
15
- open-prompts save "TypeScript Code Review" \
15
+ prompts save "TypeScript Code Review" \
16
16
  --body "Review this TypeScript code for correctness, types, and style:\n\n{{code}}" \
17
17
  --tags "code,review,typescript" \
18
18
  --collection "code"
19
19
 
20
20
  # Use it (prints body, increments counter)
21
- open-prompts use typescript-code-review
21
+ prompts use typescript-code-review
22
22
 
23
23
  # Render a template
24
- open-prompts render typescript-code-review --var code="$(cat myfile.ts)"
24
+ prompts render typescript-code-review --var code="$(cat myfile.ts)"
25
25
 
26
26
  # Search
27
- open-prompts search "code review"
27
+ prompts search "code review"
28
28
  ```
29
29
 
30
30
  ---
@@ -37,7 +37,7 @@ Add to your Claude/agent config:
37
37
  {
38
38
  "mcpServers": {
39
39
  "prompts": {
40
- "command": "open-prompts-mcp"
40
+ "command": "prompts-mcp"
41
41
  }
42
42
  }
43
43
  }
@@ -86,28 +86,28 @@ Later, in any session:
86
86
  ## CLI Reference
87
87
 
88
88
  ```bash
89
- open-prompts save <title> # Save a prompt (--body, --file, or stdin)
90
- open-prompts use <id|slug> # Get body, increment counter
91
- open-prompts get <id|slug> # Get details without incrementing
92
- open-prompts list # List all prompts
93
- open-prompts search <query> # Full-text search
94
- open-prompts render <id> -v k=v # Render template with variables
95
- open-prompts templates # List templates
96
- open-prompts inspect <id> # Show template variables
97
- open-prompts update <id> # Update fields
98
- open-prompts delete <id> # Delete
99
- open-prompts history <id> # Version history
100
- open-prompts restore <id> <v> # Restore version
101
- open-prompts collections # List collections
102
- open-prompts move <id> <col> # Move to collection
103
- open-prompts export # Export as JSON
104
- open-prompts import <file> # Import from JSON
105
- open-prompts stats # Usage statistics
89
+ prompts save <title> # Save a prompt (--body, --file, or stdin)
90
+ prompts use <id|slug> # Get body, increment counter
91
+ prompts get <id|slug> # Get details without incrementing
92
+ prompts list # List all prompts
93
+ prompts search <query> # Full-text search
94
+ prompts render <id> -v k=v # Render template with variables
95
+ prompts templates # List templates
96
+ prompts inspect <id> # Show template variables
97
+ prompts update <id> # Update fields
98
+ prompts delete <id> # Delete
99
+ prompts history <id> # Version history
100
+ prompts restore <id> <v> # Restore version
101
+ prompts collections # List collections
102
+ prompts move <id> <col> # Move to collection
103
+ prompts export # Export as JSON
104
+ prompts import <file> # Import from JSON
105
+ prompts stats # Usage statistics
106
106
 
107
107
  # Global flags
108
- open-prompts list --json # Machine-readable output
109
- open-prompts list -c code # Filter by collection
110
- open-prompts list -t review,ts # Filter by tags
108
+ prompts list --json # Machine-readable output
109
+ prompts list -c code # Filter by collection
110
+ prompts list -t review,ts # Filter by tags
111
111
  ```
112
112
 
113
113
  ---
@@ -118,18 +118,18 @@ Prompts with `{{variable}}` syntax are automatically detected as templates.
118
118
 
119
119
  ```bash
120
120
  # Save a template
121
- open-prompts save "PR Description" \
121
+ prompts save "PR Description" \
122
122
  --body "Write a PR description for this {{language|TypeScript}} change:\n\n{{diff}}\n\nFocus on: {{focus|what changed and why}}"
123
123
 
124
124
  # Inspect variables
125
- open-prompts inspect pr-description
125
+ prompts inspect pr-description
126
126
  # Variables for pr-description:
127
127
  # language optional (default: "TypeScript")
128
128
  # diff required
129
129
  # focus optional (default: "what changed and why")
130
130
 
131
131
  # Render
132
- open-prompts render pr-description \
132
+ prompts render pr-description \
133
133
  --var diff="$(git diff main)" \
134
134
  --var language=Go
135
135
  ```
@@ -183,7 +183,7 @@ importFromClaudeCommands([
183
183
  ## REST API
184
184
 
185
185
  ```bash
186
- open-prompts-serve # starts on port 19430
186
+ prompts-serve # starts on port 19430
187
187
  ```
188
188
 
189
189
  | Method | Endpoint | Description |
@@ -212,7 +212,7 @@ open-prompts-serve # starts on port 19430
212
212
  ## Web Dashboard
213
213
 
214
214
  ```bash
215
- open-prompts-serve # start API on :19430
215
+ prompts-serve # start API on :19430
216
216
  # open dashboard/dist/index.html or run dashboard dev server
217
217
  ```
218
218
 
@@ -256,7 +256,7 @@ Priority order:
256
256
  # Export existing slash commands as prompts
257
257
  for f in .claude/commands/*.md; do
258
258
  name=$(basename "$f" .md)
259
- open-prompts save "$name" --file "$f" --collection claude-commands --tags "slash-command"
259
+ prompts save "$name" --file "$f" --collection claude-commands --tags "slash-command"
260
260
  done
261
261
  ```
262
262
 
package/dist/cli/index.js CHANGED
@@ -2183,6 +2183,10 @@ function runMigrations(db) {
2183
2183
  CREATE INDEX IF NOT EXISTS idx_versions_prompt_id ON prompt_versions(prompt_id);
2184
2184
  `
2185
2185
  },
2186
+ {
2187
+ name: "003_pinned",
2188
+ sql: `ALTER TABLE prompts ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;`
2189
+ },
2186
2190
  {
2187
2191
  name: "002_fts5",
2188
2192
  sql: `
@@ -2236,6 +2240,15 @@ function resolvePrompt(db, idOrSlug) {
2236
2240
  const byPrefix = db.query("SELECT id FROM prompts WHERE id LIKE ? LIMIT 2").all(`${idOrSlug}%`);
2237
2241
  if (byPrefix.length === 1 && byPrefix[0])
2238
2242
  return byPrefix[0].id;
2243
+ const bySlugPrefix = db.query("SELECT id FROM prompts WHERE slug LIKE ? LIMIT 2").all(`${idOrSlug}%`);
2244
+ if (bySlugPrefix.length === 1 && bySlugPrefix[0])
2245
+ return bySlugPrefix[0].id;
2246
+ const bySlugSub = db.query("SELECT id FROM prompts WHERE slug LIKE ? LIMIT 2").all(`%${idOrSlug}%`);
2247
+ if (bySlugSub.length === 1 && bySlugSub[0])
2248
+ return bySlugSub[0].id;
2249
+ const byTitle = db.query("SELECT id FROM prompts WHERE lower(title) LIKE ? LIMIT 2").all(`%${idOrSlug.toLowerCase()}%`);
2250
+ if (byTitle.length === 1 && byTitle[0])
2251
+ return byTitle[0].id;
2239
2252
  return null;
2240
2253
  }
2241
2254
 
@@ -2329,6 +2342,36 @@ function movePrompt(promptIdOrSlug, targetCollection) {
2329
2342
  ]);
2330
2343
  }
2331
2344
 
2345
+ // src/lib/duplicates.ts
2346
+ function tokenize(text) {
2347
+ return new Set(text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2));
2348
+ }
2349
+ function similarity(a, b) {
2350
+ const ta = tokenize(a);
2351
+ const tb = tokenize(b);
2352
+ if (ta.size === 0 || tb.size === 0)
2353
+ return 0;
2354
+ let shared = 0;
2355
+ for (const word of ta) {
2356
+ if (tb.has(word))
2357
+ shared++;
2358
+ }
2359
+ return shared / Math.max(ta.size, tb.size);
2360
+ }
2361
+ function findDuplicates(body, threshold = 0.8, excludeSlug) {
2362
+ const all = listPrompts({ limit: 1e4 });
2363
+ const matches = [];
2364
+ for (const p of all) {
2365
+ if (excludeSlug && p.slug === excludeSlug)
2366
+ continue;
2367
+ const score = similarity(body, p.body);
2368
+ if (score >= threshold) {
2369
+ matches.push({ prompt: p, score });
2370
+ }
2371
+ }
2372
+ return matches.sort((a, b) => b.score - a.score);
2373
+ }
2374
+
2332
2375
  // src/lib/template.ts
2333
2376
  var VAR_PATTERN = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\|\s*(.*?)\s*)?\}\}/g;
2334
2377
  function extractVariables(body) {
@@ -2406,6 +2449,7 @@ function rowToPrompt(row) {
2406
2449
  collection: row["collection"],
2407
2450
  tags: JSON.parse(row["tags"] || "[]"),
2408
2451
  variables: JSON.parse(row["variables"] || "[]"),
2452
+ pinned: Boolean(row["pinned"]),
2409
2453
  is_template: Boolean(row["is_template"]),
2410
2454
  source: row["source"],
2411
2455
  version: row["version"],
@@ -2480,7 +2524,7 @@ function listPrompts(filter = {}) {
2480
2524
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2481
2525
  const limit = filter.limit ?? 100;
2482
2526
  const offset = filter.offset ?? 0;
2483
- const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY use_count DESC, updated_at DESC LIMIT ? OFFSET ?`).all([...params, limit, offset]);
2527
+ const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY pinned DESC, use_count DESC, updated_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
2484
2528
  return rows.map(rowToPrompt);
2485
2529
  }
2486
2530
  function updatePrompt(idOrSlug, input) {
@@ -2530,7 +2574,13 @@ function usePrompt(idOrSlug) {
2530
2574
  db.run("UPDATE prompts SET use_count = use_count + 1, last_used_at = datetime('now') WHERE id = ?", [prompt.id]);
2531
2575
  return requirePrompt(prompt.id);
2532
2576
  }
2533
- function upsertPrompt(input) {
2577
+ function pinPrompt(idOrSlug, pinned) {
2578
+ const db = getDatabase();
2579
+ const prompt = requirePrompt(idOrSlug);
2580
+ db.run("UPDATE prompts SET pinned = ?, updated_at = datetime('now') WHERE id = ?", [pinned ? 1 : 0, prompt.id]);
2581
+ return requirePrompt(prompt.id);
2582
+ }
2583
+ function upsertPrompt(input, force = false) {
2534
2584
  const db = getDatabase();
2535
2585
  const slug = input.slug || generateSlug(input.title);
2536
2586
  const existing = db.query("SELECT id FROM prompts WHERE slug = ?").get(slug);
@@ -2545,8 +2595,16 @@ function upsertPrompt(input) {
2545
2595
  });
2546
2596
  return { prompt: prompt2, created: false };
2547
2597
  }
2598
+ let duplicate_warning;
2599
+ if (!force && input.body) {
2600
+ const dupes = findDuplicates(input.body, 0.8, slug);
2601
+ if (dupes.length > 0) {
2602
+ const top = dupes[0];
2603
+ duplicate_warning = `Similar prompt already exists: "${top.prompt.slug}" (${Math.round(top.score * 100)}% match). Use --force to save anyway.`;
2604
+ }
2605
+ }
2548
2606
  const prompt = createPrompt({ ...input, slug });
2549
- return { prompt, created: true };
2607
+ return { prompt, created: true, duplicate_warning };
2550
2608
  }
2551
2609
  function getPromptStats() {
2552
2610
  const db = getDatabase();
@@ -2612,6 +2670,7 @@ function rowToSearchResult(row, snippet) {
2612
2670
  collection: row["collection"],
2613
2671
  tags: JSON.parse(row["tags"] || "[]"),
2614
2672
  variables: JSON.parse(row["variables"] || "[]"),
2673
+ pinned: Boolean(row["pinned"]),
2615
2674
  is_template: Boolean(row["is_template"]),
2616
2675
  source: row["source"],
2617
2676
  version: row["version"],
@@ -2666,7 +2725,7 @@ function searchPrompts(query, filter = {}) {
2666
2725
  WHERE prompts_fts MATCH ?
2667
2726
  ${where}
2668
2727
  ORDER BY bm25(prompts_fts)
2669
- LIMIT ? OFFSET ?`).all([ftsQuery, ...params, limit, offset]);
2728
+ LIMIT ? OFFSET ?`).all(ftsQuery, ...params, limit, offset);
2670
2729
  return rows2.map((r) => rowToSearchResult(r, r["snippet"]));
2671
2730
  } catch {}
2672
2731
  }
@@ -2674,7 +2733,7 @@ function searchPrompts(query, filter = {}) {
2674
2733
  const rows = db.query(`SELECT *, 1 as score FROM prompts
2675
2734
  WHERE (name LIKE ? OR slug LIKE ? OR title LIKE ? OR body LIKE ? OR description LIKE ? OR tags LIKE ?)
2676
2735
  ORDER BY use_count DESC, updated_at DESC
2677
- LIMIT ? OFFSET ?`).all([like, like, like, like, like, like, filter.limit ?? 50, filter.offset ?? 0]);
2736
+ LIMIT ? OFFSET ?`).all(like, like, like, like, like, like, filter.limit ?? 50, filter.offset ?? 0);
2678
2737
  return rows.map((r) => rowToSearchResult(r));
2679
2738
  }
2680
2739
 
@@ -2711,10 +2770,44 @@ function exportToJson(collection) {
2711
2770
  return { prompts, exported_at: new Date().toISOString(), collection };
2712
2771
  }
2713
2772
 
2773
+ // src/lib/lint.ts
2774
+ function lintPrompt(p) {
2775
+ const issues = [];
2776
+ const issue = (severity, rule, message) => ({
2777
+ prompt_id: p.id,
2778
+ slug: p.slug,
2779
+ severity,
2780
+ rule,
2781
+ message
2782
+ });
2783
+ if (!p.description) {
2784
+ issues.push(issue("warn", "missing-description", "No description provided"));
2785
+ }
2786
+ if (p.body.trim().length < 10) {
2787
+ issues.push(issue("error", "body-too-short", `Body is only ${p.body.trim().length} characters`));
2788
+ }
2789
+ if (p.tags.length === 0) {
2790
+ issues.push(issue("info", "no-tags", "No tags \u2014 prompt will be harder to discover"));
2791
+ }
2792
+ if (p.is_template) {
2793
+ const undocumented = p.variables.filter((v) => !v.description || v.description.trim() === "");
2794
+ if (undocumented.length > 0) {
2795
+ issues.push(issue("warn", "undocumented-vars", `Template variables without description: ${undocumented.map((v) => v.name).join(", ")}`));
2796
+ }
2797
+ }
2798
+ if (p.collection === "default" && p.use_count === 0) {
2799
+ issues.push(issue("info", "uncollected", "In default collection and never used \u2014 consider organizing"));
2800
+ }
2801
+ return issues;
2802
+ }
2803
+ function lintAll(prompts) {
2804
+ return prompts.map((p) => ({ prompt: p, issues: lintPrompt(p) })).filter((r) => r.issues.length > 0);
2805
+ }
2806
+
2714
2807
  // src/cli/index.tsx
2715
2808
  var require2 = createRequire(import.meta.url);
2716
2809
  var pkg = require2("../../package.json");
2717
- var program2 = new Command().name("open-prompts").version(pkg.version).description("Reusable prompt library \u2014 save, search, render prompts from any AI session").option("--json", "Output as JSON");
2810
+ 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");
2718
2811
  function isJson() {
2719
2812
  return Boolean(program2.opts()["json"]);
2720
2813
  }
@@ -2737,9 +2830,10 @@ function handleError(e) {
2737
2830
  function fmtPrompt(p) {
2738
2831
  const tags = p.tags.length > 0 ? chalk.gray(` [${p.tags.join(", ")}]`) : "";
2739
2832
  const template = p.is_template ? chalk.cyan(" \u25C7") : "";
2740
- return `${chalk.bold(p.id)} ${chalk.green(p.slug)}${template} ${p.title}${tags} ${chalk.gray(p.collection)}`;
2833
+ const pin = p.pinned ? chalk.yellow(" \uD83D\uDCCC") : "";
2834
+ return `${chalk.bold(p.id)} ${chalk.green(p.slug)}${template}${pin} ${p.title}${tags} ${chalk.gray(p.collection)}`;
2741
2835
  }
2742
- 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)").action(async (title, opts) => {
2836
+ 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) => {
2743
2837
  try {
2744
2838
  let body = opts["body"] ?? "";
2745
2839
  if (opts["file"]) {
@@ -2753,7 +2847,7 @@ program2.command("save <title>").description("Save a new prompt (or update exist
2753
2847
  }
2754
2848
  if (!body)
2755
2849
  handleError("No body provided. Use --body, --file, or pipe via stdin.");
2756
- const { prompt, created } = upsertPrompt({
2850
+ const { prompt, created, duplicate_warning } = upsertPrompt({
2757
2851
  title,
2758
2852
  body,
2759
2853
  slug: opts["slug"],
@@ -2762,7 +2856,10 @@ program2.command("save <title>").description("Save a new prompt (or update exist
2762
2856
  tags: opts["tags"] ? opts["tags"].split(",").map((t) => t.trim()) : [],
2763
2857
  source: opts["source"] || "manual",
2764
2858
  changed_by: opts["agent"]
2765
- });
2859
+ }, Boolean(opts["force"]));
2860
+ if (duplicate_warning && !isJson()) {
2861
+ console.warn(chalk.yellow(`Warning: ${duplicate_warning}`));
2862
+ }
2766
2863
  if (isJson()) {
2767
2864
  output(prompt);
2768
2865
  } else {
@@ -2801,14 +2898,17 @@ program2.command("get <id>").description("Get prompt details without incrementin
2801
2898
  handleError(e);
2802
2899
  }
2803
2900
  });
2804
- 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("-n, --limit <n>", "Max results", "50").action((opts) => {
2901
+ 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) => {
2805
2902
  try {
2806
- const prompts = listPrompts({
2903
+ let prompts = listPrompts({
2807
2904
  collection: opts["collection"],
2808
2905
  tags: opts["tags"] ? opts["tags"].split(",").map((t) => t.trim()) : undefined,
2809
2906
  is_template: opts["templates"] ? true : undefined,
2810
2907
  limit: parseInt(opts["limit"]) || 50
2811
2908
  });
2909
+ if (opts["recent"]) {
2910
+ prompts = prompts.filter((p) => p.last_used_at !== null).sort((a, b) => (b.last_used_at ?? "").localeCompare(a.last_used_at ?? ""));
2911
+ }
2812
2912
  if (isJson()) {
2813
2913
  output(prompts);
2814
2914
  } else if (prompts.length === 0) {
@@ -2828,7 +2928,7 @@ program2.command("search <query>").description("Full-text search across prompts
2828
2928
  const results = searchPrompts(query, {
2829
2929
  collection: opts["collection"],
2830
2930
  tags: opts["tags"] ? opts["tags"].split(",").map((t) => t.trim()) : undefined,
2831
- limit: parseInt(opts["limit"]) || 20
2931
+ limit: parseInt(opts["limit"] ?? "20") || 20
2832
2932
  });
2833
2933
  if (isJson()) {
2834
2934
  output(results);
@@ -2837,8 +2937,10 @@ program2.command("search <query>").description("Full-text search across prompts
2837
2937
  } else {
2838
2938
  for (const r of results) {
2839
2939
  console.log(fmtPrompt(r.prompt));
2840
- if (r.snippet)
2841
- console.log(chalk.gray(" " + r.snippet));
2940
+ if (r.snippet) {
2941
+ const highlighted = r.snippet.replace(/\[([^\]]+)\]/g, (_m, word) => chalk.yellowBright(word));
2942
+ console.log(chalk.gray(" ") + chalk.gray(highlighted));
2943
+ }
2842
2944
  }
2843
2945
  console.log(chalk.gray(`
2844
2946
  ${results.length} result(s)`));
@@ -2915,12 +3017,12 @@ program2.command("inspect <id>").description("Show a prompt's variables (for tem
2915
3017
  program2.command("update <id>").description("Update a prompt's fields").option("--title <title>").option("-b, --body <body>").option("-d, --description <desc>").option("-c, --collection <name>").option("-t, --tags <tags>").option("--agent <name>").action((id, opts) => {
2916
3018
  try {
2917
3019
  const prompt = updatePrompt(id, {
2918
- title: opts["title"],
2919
- body: opts["body"],
2920
- description: opts["description"],
2921
- collection: opts["collection"],
3020
+ title: opts["title"] ?? undefined,
3021
+ body: opts["body"] ?? undefined,
3022
+ description: opts["description"] ?? undefined,
3023
+ collection: opts["collection"] ?? undefined,
2922
3024
  tags: opts["tags"] ? opts["tags"].split(",").map((t) => t.trim()) : undefined,
2923
- changed_by: opts["agent"]
3025
+ changed_by: opts["agent"] ?? undefined
2924
3026
  });
2925
3027
  if (isJson())
2926
3028
  output(prompt);
@@ -3079,4 +3181,140 @@ By collection:`));
3079
3181
  handleError(e);
3080
3182
  }
3081
3183
  });
3184
+ program2.command("recent [n]").description("Show recently used prompts (default: 10)").action((n) => {
3185
+ try {
3186
+ const limit = parseInt(n ?? "10") || 10;
3187
+ const prompts = listPrompts({ limit }).filter((p) => p.last_used_at !== null).sort((a, b) => (b.last_used_at ?? "").localeCompare(a.last_used_at ?? "")).slice(0, limit);
3188
+ if (isJson()) {
3189
+ output(prompts);
3190
+ return;
3191
+ }
3192
+ if (prompts.length === 0) {
3193
+ console.log(chalk.gray("No recently used prompts."));
3194
+ return;
3195
+ }
3196
+ for (const p of prompts) {
3197
+ const ago = chalk.gray(new Date(p.last_used_at).toLocaleString());
3198
+ console.log(`${chalk.bold(p.id)} ${chalk.green(p.slug)} ${p.title} ${ago}`);
3199
+ }
3200
+ } catch (e) {
3201
+ handleError(e);
3202
+ }
3203
+ });
3204
+ program2.command("lint").description("Check prompt quality: missing descriptions, undocumented vars, short bodies, no tags").option("-c, --collection <name>", "Lint only this collection").action((opts) => {
3205
+ try {
3206
+ const prompts = listPrompts({ collection: opts["collection"], limit: 1e4 });
3207
+ const results = lintAll(prompts);
3208
+ if (isJson()) {
3209
+ output(results);
3210
+ return;
3211
+ }
3212
+ if (results.length === 0) {
3213
+ console.log(chalk.green("\u2713 All prompts pass lint."));
3214
+ return;
3215
+ }
3216
+ let errors = 0, warns = 0, infos = 0;
3217
+ for (const { prompt: p, issues } of results) {
3218
+ console.log(`
3219
+ ${chalk.bold(p.slug)} ${chalk.gray(p.id)}`);
3220
+ for (const issue of issues) {
3221
+ if (issue.severity === "error") {
3222
+ console.log(chalk.red(` \u2717 [${issue.rule}] ${issue.message}`));
3223
+ errors++;
3224
+ } else if (issue.severity === "warn") {
3225
+ console.log(chalk.yellow(` \u26A0 [${issue.rule}] ${issue.message}`));
3226
+ warns++;
3227
+ } else {
3228
+ console.log(chalk.gray(` \u2139 [${issue.rule}] ${issue.message}`));
3229
+ infos++;
3230
+ }
3231
+ }
3232
+ }
3233
+ console.log(chalk.bold(`
3234
+ ${results.length} prompt(s) with issues \u2014 ${errors} errors, ${warns} warnings, ${infos} info`));
3235
+ if (errors > 0)
3236
+ process.exit(1);
3237
+ } catch (e) {
3238
+ handleError(e);
3239
+ }
3240
+ });
3241
+ program2.command("stale [days]").description("List prompts not used in N days (default: 30)").action((days) => {
3242
+ try {
3243
+ const threshold = parseInt(days ?? "30") || 30;
3244
+ const cutoff = new Date(Date.now() - threshold * 24 * 60 * 60 * 1000).toISOString();
3245
+ const all = listPrompts({ limit: 1e4 });
3246
+ 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 ?? ""));
3247
+ if (isJson()) {
3248
+ output(stale);
3249
+ return;
3250
+ }
3251
+ if (stale.length === 0) {
3252
+ console.log(chalk.green(`No stale prompts (threshold: ${threshold} days).`));
3253
+ return;
3254
+ }
3255
+ console.log(chalk.bold(`Stale prompts (not used in ${threshold}+ days):`));
3256
+ for (const p of stale) {
3257
+ const last = p.last_used_at ? chalk.gray(new Date(p.last_used_at).toLocaleDateString()) : chalk.red("never");
3258
+ console.log(` ${chalk.green(p.slug)} ${chalk.gray(`${p.use_count}\xD7`)} last used: ${last}`);
3259
+ }
3260
+ console.log(chalk.gray(`
3261
+ ${stale.length} stale prompt(s)`));
3262
+ } catch (e) {
3263
+ handleError(e);
3264
+ }
3265
+ });
3266
+ program2.command("pin <id>").description("Pin a prompt so it always appears first in lists").action((id) => {
3267
+ try {
3268
+ const p = pinPrompt(id, true);
3269
+ if (isJson())
3270
+ output(p);
3271
+ else
3272
+ console.log(chalk.yellow(`\uD83D\uDCCC Pinned ${chalk.bold(p.slug)}`));
3273
+ } catch (e) {
3274
+ handleError(e);
3275
+ }
3276
+ });
3277
+ program2.command("unpin <id>").description("Unpin a prompt").action((id) => {
3278
+ try {
3279
+ const p = pinPrompt(id, false);
3280
+ if (isJson())
3281
+ output(p);
3282
+ else
3283
+ console.log(chalk.gray(`Unpinned ${chalk.bold(p.slug)}`));
3284
+ } catch (e) {
3285
+ handleError(e);
3286
+ }
3287
+ });
3288
+ program2.command("copy <id>").description("Copy prompt body to clipboard and increment use counter").action(async (id) => {
3289
+ try {
3290
+ const prompt = usePrompt(id);
3291
+ const { platform } = process;
3292
+ if (platform === "darwin") {
3293
+ const proc = Bun.spawn(["pbcopy"], { stdin: "pipe" });
3294
+ proc.stdin.write(prompt.body);
3295
+ proc.stdin.end();
3296
+ await proc.exited;
3297
+ } else if (platform === "linux") {
3298
+ try {
3299
+ const proc = Bun.spawn(["xclip", "-selection", "clipboard"], { stdin: "pipe" });
3300
+ proc.stdin.write(prompt.body);
3301
+ proc.stdin.end();
3302
+ await proc.exited;
3303
+ } catch {
3304
+ const proc = Bun.spawn(["xsel", "--clipboard", "--input"], { stdin: "pipe" });
3305
+ proc.stdin.write(prompt.body);
3306
+ proc.stdin.end();
3307
+ await proc.exited;
3308
+ }
3309
+ } else {
3310
+ handleError("Clipboard not supported on this platform. Use `prompts use` instead.");
3311
+ }
3312
+ if (isJson())
3313
+ output({ copied: true, id: prompt.id, slug: prompt.slug });
3314
+ else
3315
+ console.log(chalk.green(`Copied ${chalk.bold(prompt.slug)} to clipboard`));
3316
+ } catch (e) {
3317
+ handleError(e);
3318
+ }
3319
+ });
3082
3320
  program2.parse();
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAMrC,wBAAgB,SAAS,IAAI,MAAM,CAsBlC;AAED,wBAAgB,WAAW,IAAI,QAAQ,CAmBtC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAKpC;AAGD,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAkHD,wBAAgB,MAAM,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAM5C;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgB3E"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAMrC,wBAAgB,SAAS,IAAI,MAAM,CAsBlC;AAED,wBAAgB,WAAW,IAAI,QAAQ,CAmBtC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAKpC;AAGD,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAsHD,wBAAgB,MAAM,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAM5C;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkC3E"}
@@ -6,9 +6,11 @@ export declare function listPrompts(filter?: ListPromptsFilter): Prompt[];
6
6
  export declare function updatePrompt(idOrSlug: string, input: UpdatePromptInput): Prompt;
7
7
  export declare function deletePrompt(idOrSlug: string): void;
8
8
  export declare function usePrompt(idOrSlug: string): Prompt;
9
- export declare function upsertPrompt(input: CreatePromptInput): {
9
+ export declare function pinPrompt(idOrSlug: string, pinned: boolean): Prompt;
10
+ export declare function upsertPrompt(input: CreatePromptInput, force?: boolean): {
10
11
  prompt: Prompt;
11
12
  created: boolean;
13
+ duplicate_warning?: string;
12
14
  };
13
15
  export declare function getPromptStats(): {
14
16
  total_prompts: number;
@@ -1 +1 @@
1
- {"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../src/db/prompts.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,MAAM,EACN,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EAGlB,MAAM,mBAAmB,CAAA;AAyB1B,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAyC7D;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOzD;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAItD;AAED,wBAAgB,WAAW,CAAC,MAAM,GAAE,iBAAsB,GAAG,MAAM,EAAE,CAmCpE;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,MAAM,CA8C/E;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAInD;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQlD;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAmB3F;AAED,wBAAgB,cAAc;;;;;YAOJ,MAAM;cAAQ,MAAM;cAAQ,MAAM;eAAS,MAAM;mBAAa,MAAM;;;YAGpE,MAAM;cAAQ,MAAM;cAAQ,MAAM;eAAS,MAAM;sBAAgB,MAAM;;;oBAG/D,MAAM;eAAS,MAAM;;;gBAGzB,MAAM;eAAS,MAAM;;EAGlD"}
1
+ {"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../src/db/prompts.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,MAAM,EACN,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EAGlB,MAAM,mBAAmB,CAAA;AA0B1B,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAyC7D;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOzD;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAItD;AAED,wBAAgB,WAAW,CAAC,MAAM,GAAE,iBAAsB,GAAG,MAAM,EAAE,CAmCpE;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,MAAM,CA8C/E;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAInD;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQlD;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAKnE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,EAAE,KAAK,UAAQ,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAE,CA6BtI;AAED,wBAAgB,cAAc;;;;;YAOJ,MAAM;cAAQ,MAAM;cAAQ,MAAM;eAAS,MAAM;mBAAa,MAAM;;;YAGpE,MAAM;cAAQ,MAAM;cAAQ,MAAM;eAAS,MAAM;sBAAgB,MAAM;;;oBAG/D,MAAM;eAAS,MAAM;;;gBAGzB,MAAM;eAAS,MAAM;;EAGlD"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { createPrompt, getPrompt, requirePrompt, listPrompts, updatePrompt, deletePrompt, usePrompt, upsertPrompt, getPromptStats } from "./db/prompts.js";
1
+ export { createPrompt, getPrompt, requirePrompt, listPrompts, updatePrompt, deletePrompt, usePrompt, upsertPrompt, getPromptStats, pinPrompt } from "./db/prompts.js";
2
2
  export { listVersions, getVersion, restoreVersion } from "./db/versions.js";
3
3
  export { listCollections, getCollection, ensureCollection, movePrompt } from "./db/collections.js";
4
4
  export { registerAgent, listAgents } from "./db/agents.js";
@@ -7,6 +7,8 @@ export { searchPrompts, findSimilar } from "./lib/search.js";
7
7
  export { extractVariables, extractVariableInfo, renderTemplate, validateVars } from "./lib/template.js";
8
8
  export type { VariableInfo } from "./lib/template.js";
9
9
  export { importFromJson, exportToJson } from "./lib/importer.js";
10
+ export { findDuplicates } from "./lib/duplicates.js";
11
+ export type { DuplicateMatch } from "./lib/duplicates.js";
10
12
  export { generateSlug, uniqueSlug, generatePromptId } from "./lib/ids.js";
11
13
  export type { Prompt, PromptVersion, Collection, Agent, TemplateVariable, PromptSource, CreatePromptInput, UpdatePromptInput, ListPromptsFilter, SearchResult, RenderResult, PromptStats, } from "./types/index.js";
12
14
  export { PromptNotFoundError, VersionConflictError, DuplicateSlugError, TemplateRenderError, } from "./types/index.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAC1J,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAC3E,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAClG,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAC1D,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAGzD,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAG5D,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACvG,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGrD,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGhE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAGzE,YAAY,EACV,MAAM,EACN,aAAa,EACb,UAAU,EACV,KAAK,EACL,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,WAAW,GACZ,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AACrK,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAC3E,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAClG,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAC1D,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAGzD,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAG5D,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACvG,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGrD,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAGzD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAGzE,YAAY,EACV,MAAM,EACN,aAAa,EACb,UAAU,EACV,KAAK,EACL,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,WAAW,GACZ,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,kBAAkB,CAAA"}