@hasna/prompts 0.1.0 → 0.2.1

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
 
@@ -2253,25 +2266,24 @@ function uniqueSlug(baseSlug) {
2253
2266
  }
2254
2267
  return slug;
2255
2268
  }
2269
+ var CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
2270
+ function nanoid(len) {
2271
+ let id = "";
2272
+ for (let i = 0;i < len; i++) {
2273
+ id += CHARS[Math.floor(Math.random() * CHARS.length)];
2274
+ }
2275
+ return id;
2276
+ }
2256
2277
  function generatePromptId() {
2257
2278
  const db = getDatabase();
2258
- const row = db.query("SELECT id FROM prompts ORDER BY rowid DESC LIMIT 1").get();
2259
- let next = 1;
2260
- if (row) {
2261
- const match = row.id.match(/PRMT-(\d+)/);
2262
- if (match && match[1]) {
2263
- next = parseInt(match[1], 10) + 1;
2264
- }
2265
- }
2266
- return `PRMT-${String(next).padStart(5, "0")}`;
2279
+ let id;
2280
+ do {
2281
+ id = `prmt-${nanoid(8)}`;
2282
+ } while (db.query("SELECT 1 FROM prompts WHERE id = ?").get(id));
2283
+ return id;
2267
2284
  }
2268
2285
  function generateId(prefix) {
2269
- const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
2270
- let id = prefix + "-";
2271
- for (let i = 0;i < 8; i++) {
2272
- id += chars[Math.floor(Math.random() * chars.length)];
2273
- }
2274
- return id;
2286
+ return `${prefix}-${nanoid(8)}`;
2275
2287
  }
2276
2288
 
2277
2289
  // src/db/collections.ts
@@ -2329,6 +2341,36 @@ function movePrompt(promptIdOrSlug, targetCollection) {
2329
2341
  ]);
2330
2342
  }
2331
2343
 
2344
+ // src/lib/duplicates.ts
2345
+ function tokenize(text) {
2346
+ return new Set(text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2));
2347
+ }
2348
+ function similarity(a, b) {
2349
+ const ta = tokenize(a);
2350
+ const tb = tokenize(b);
2351
+ if (ta.size === 0 || tb.size === 0)
2352
+ return 0;
2353
+ let shared = 0;
2354
+ for (const word of ta) {
2355
+ if (tb.has(word))
2356
+ shared++;
2357
+ }
2358
+ return shared / Math.max(ta.size, tb.size);
2359
+ }
2360
+ function findDuplicates(body, threshold = 0.8, excludeSlug) {
2361
+ const all = listPrompts({ limit: 1e4 });
2362
+ const matches = [];
2363
+ for (const p of all) {
2364
+ if (excludeSlug && p.slug === excludeSlug)
2365
+ continue;
2366
+ const score = similarity(body, p.body);
2367
+ if (score >= threshold) {
2368
+ matches.push({ prompt: p, score });
2369
+ }
2370
+ }
2371
+ return matches.sort((a, b) => b.score - a.score);
2372
+ }
2373
+
2332
2374
  // src/lib/template.ts
2333
2375
  var VAR_PATTERN = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\|\s*(.*?)\s*)?\}\}/g;
2334
2376
  function extractVariables(body) {
@@ -2406,6 +2448,7 @@ function rowToPrompt(row) {
2406
2448
  collection: row["collection"],
2407
2449
  tags: JSON.parse(row["tags"] || "[]"),
2408
2450
  variables: JSON.parse(row["variables"] || "[]"),
2451
+ pinned: Boolean(row["pinned"]),
2409
2452
  is_template: Boolean(row["is_template"]),
2410
2453
  source: row["source"],
2411
2454
  version: row["version"],
@@ -2480,7 +2523,7 @@ function listPrompts(filter = {}) {
2480
2523
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2481
2524
  const limit = filter.limit ?? 100;
2482
2525
  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]);
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);
2484
2527
  return rows.map(rowToPrompt);
2485
2528
  }
2486
2529
  function updatePrompt(idOrSlug, input) {
@@ -2530,7 +2573,13 @@ function usePrompt(idOrSlug) {
2530
2573
  db.run("UPDATE prompts SET use_count = use_count + 1, last_used_at = datetime('now') WHERE id = ?", [prompt.id]);
2531
2574
  return requirePrompt(prompt.id);
2532
2575
  }
2533
- function upsertPrompt(input) {
2576
+ function pinPrompt(idOrSlug, pinned) {
2577
+ const db = getDatabase();
2578
+ const prompt = requirePrompt(idOrSlug);
2579
+ db.run("UPDATE prompts SET pinned = ?, updated_at = datetime('now') WHERE id = ?", [pinned ? 1 : 0, prompt.id]);
2580
+ return requirePrompt(prompt.id);
2581
+ }
2582
+ function upsertPrompt(input, force = false) {
2534
2583
  const db = getDatabase();
2535
2584
  const slug = input.slug || generateSlug(input.title);
2536
2585
  const existing = db.query("SELECT id FROM prompts WHERE slug = ?").get(slug);
@@ -2545,8 +2594,16 @@ function upsertPrompt(input) {
2545
2594
  });
2546
2595
  return { prompt: prompt2, created: false };
2547
2596
  }
2597
+ let duplicate_warning;
2598
+ if (!force && input.body) {
2599
+ const dupes = findDuplicates(input.body, 0.8, slug);
2600
+ if (dupes.length > 0) {
2601
+ const top = dupes[0];
2602
+ duplicate_warning = `Similar prompt already exists: "${top.prompt.slug}" (${Math.round(top.score * 100)}% match). Use --force to save anyway.`;
2603
+ }
2604
+ }
2548
2605
  const prompt = createPrompt({ ...input, slug });
2549
- return { prompt, created: true };
2606
+ return { prompt, created: true, duplicate_warning };
2550
2607
  }
2551
2608
  function getPromptStats() {
2552
2609
  const db = getDatabase();
@@ -2612,6 +2669,7 @@ function rowToSearchResult(row, snippet) {
2612
2669
  collection: row["collection"],
2613
2670
  tags: JSON.parse(row["tags"] || "[]"),
2614
2671
  variables: JSON.parse(row["variables"] || "[]"),
2672
+ pinned: Boolean(row["pinned"]),
2615
2673
  is_template: Boolean(row["is_template"]),
2616
2674
  source: row["source"],
2617
2675
  version: row["version"],
@@ -2666,7 +2724,7 @@ function searchPrompts(query, filter = {}) {
2666
2724
  WHERE prompts_fts MATCH ?
2667
2725
  ${where}
2668
2726
  ORDER BY bm25(prompts_fts)
2669
- LIMIT ? OFFSET ?`).all([ftsQuery, ...params, limit, offset]);
2727
+ LIMIT ? OFFSET ?`).all(ftsQuery, ...params, limit, offset);
2670
2728
  return rows2.map((r) => rowToSearchResult(r, r["snippet"]));
2671
2729
  } catch {}
2672
2730
  }
@@ -2674,7 +2732,7 @@ function searchPrompts(query, filter = {}) {
2674
2732
  const rows = db.query(`SELECT *, 1 as score FROM prompts
2675
2733
  WHERE (name LIKE ? OR slug LIKE ? OR title LIKE ? OR body LIKE ? OR description LIKE ? OR tags LIKE ?)
2676
2734
  ORDER BY use_count DESC, updated_at DESC
2677
- LIMIT ? OFFSET ?`).all([like, like, like, like, like, like, filter.limit ?? 50, filter.offset ?? 0]);
2735
+ LIMIT ? OFFSET ?`).all(like, like, like, like, like, like, filter.limit ?? 50, filter.offset ?? 0);
2678
2736
  return rows.map((r) => rowToSearchResult(r));
2679
2737
  }
2680
2738
 
@@ -2711,10 +2769,44 @@ function exportToJson(collection) {
2711
2769
  return { prompts, exported_at: new Date().toISOString(), collection };
2712
2770
  }
2713
2771
 
2772
+ // src/lib/lint.ts
2773
+ function lintPrompt(p) {
2774
+ const issues = [];
2775
+ const issue = (severity, rule, message) => ({
2776
+ prompt_id: p.id,
2777
+ slug: p.slug,
2778
+ severity,
2779
+ rule,
2780
+ message
2781
+ });
2782
+ if (!p.description) {
2783
+ issues.push(issue("warn", "missing-description", "No description provided"));
2784
+ }
2785
+ if (p.body.trim().length < 10) {
2786
+ issues.push(issue("error", "body-too-short", `Body is only ${p.body.trim().length} characters`));
2787
+ }
2788
+ if (p.tags.length === 0) {
2789
+ issues.push(issue("info", "no-tags", "No tags \u2014 prompt will be harder to discover"));
2790
+ }
2791
+ if (p.is_template) {
2792
+ const undocumented = p.variables.filter((v) => !v.description || v.description.trim() === "");
2793
+ if (undocumented.length > 0) {
2794
+ issues.push(issue("warn", "undocumented-vars", `Template variables without description: ${undocumented.map((v) => v.name).join(", ")}`));
2795
+ }
2796
+ }
2797
+ if (p.collection === "default" && p.use_count === 0) {
2798
+ issues.push(issue("info", "uncollected", "In default collection and never used \u2014 consider organizing"));
2799
+ }
2800
+ return issues;
2801
+ }
2802
+ function lintAll(prompts) {
2803
+ return prompts.map((p) => ({ prompt: p, issues: lintPrompt(p) })).filter((r) => r.issues.length > 0);
2804
+ }
2805
+
2714
2806
  // src/cli/index.tsx
2715
2807
  var require2 = createRequire(import.meta.url);
2716
2808
  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");
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");
2718
2810
  function isJson() {
2719
2811
  return Boolean(program2.opts()["json"]);
2720
2812
  }
@@ -2737,9 +2829,10 @@ function handleError(e) {
2737
2829
  function fmtPrompt(p) {
2738
2830
  const tags = p.tags.length > 0 ? chalk.gray(` [${p.tags.join(", ")}]`) : "";
2739
2831
  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)}`;
2832
+ const pin = p.pinned ? chalk.yellow(" \uD83D\uDCCC") : "";
2833
+ return `${chalk.bold(p.id)} ${chalk.green(p.slug)}${template}${pin} ${p.title}${tags} ${chalk.gray(p.collection)}`;
2741
2834
  }
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) => {
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) => {
2743
2836
  try {
2744
2837
  let body = opts["body"] ?? "";
2745
2838
  if (opts["file"]) {
@@ -2753,7 +2846,7 @@ program2.command("save <title>").description("Save a new prompt (or update exist
2753
2846
  }
2754
2847
  if (!body)
2755
2848
  handleError("No body provided. Use --body, --file, or pipe via stdin.");
2756
- const { prompt, created } = upsertPrompt({
2849
+ const { prompt, created, duplicate_warning } = upsertPrompt({
2757
2850
  title,
2758
2851
  body,
2759
2852
  slug: opts["slug"],
@@ -2762,7 +2855,10 @@ program2.command("save <title>").description("Save a new prompt (or update exist
2762
2855
  tags: opts["tags"] ? opts["tags"].split(",").map((t) => t.trim()) : [],
2763
2856
  source: opts["source"] || "manual",
2764
2857
  changed_by: opts["agent"]
2765
- });
2858
+ }, Boolean(opts["force"]));
2859
+ if (duplicate_warning && !isJson()) {
2860
+ console.warn(chalk.yellow(`Warning: ${duplicate_warning}`));
2861
+ }
2766
2862
  if (isJson()) {
2767
2863
  output(prompt);
2768
2864
  } else {
@@ -2801,14 +2897,17 @@ program2.command("get <id>").description("Get prompt details without incrementin
2801
2897
  handleError(e);
2802
2898
  }
2803
2899
  });
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) => {
2900
+ 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
2901
  try {
2806
- const prompts = listPrompts({
2902
+ let prompts = listPrompts({
2807
2903
  collection: opts["collection"],
2808
2904
  tags: opts["tags"] ? opts["tags"].split(",").map((t) => t.trim()) : undefined,
2809
2905
  is_template: opts["templates"] ? true : undefined,
2810
2906
  limit: parseInt(opts["limit"]) || 50
2811
2907
  });
2908
+ if (opts["recent"]) {
2909
+ prompts = prompts.filter((p) => p.last_used_at !== null).sort((a, b) => (b.last_used_at ?? "").localeCompare(a.last_used_at ?? ""));
2910
+ }
2812
2911
  if (isJson()) {
2813
2912
  output(prompts);
2814
2913
  } else if (prompts.length === 0) {
@@ -2828,7 +2927,7 @@ program2.command("search <query>").description("Full-text search across prompts
2828
2927
  const results = searchPrompts(query, {
2829
2928
  collection: opts["collection"],
2830
2929
  tags: opts["tags"] ? opts["tags"].split(",").map((t) => t.trim()) : undefined,
2831
- limit: parseInt(opts["limit"]) || 20
2930
+ limit: parseInt(opts["limit"] ?? "20") || 20
2832
2931
  });
2833
2932
  if (isJson()) {
2834
2933
  output(results);
@@ -2837,8 +2936,10 @@ program2.command("search <query>").description("Full-text search across prompts
2837
2936
  } else {
2838
2937
  for (const r of results) {
2839
2938
  console.log(fmtPrompt(r.prompt));
2840
- if (r.snippet)
2841
- console.log(chalk.gray(" " + r.snippet));
2939
+ if (r.snippet) {
2940
+ const highlighted = r.snippet.replace(/\[([^\]]+)\]/g, (_m, word) => chalk.yellowBright(word));
2941
+ console.log(chalk.gray(" ") + chalk.gray(highlighted));
2942
+ }
2842
2943
  }
2843
2944
  console.log(chalk.gray(`
2844
2945
  ${results.length} result(s)`));
@@ -2915,12 +3016,12 @@ program2.command("inspect <id>").description("Show a prompt's variables (for tem
2915
3016
  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
3017
  try {
2917
3018
  const prompt = updatePrompt(id, {
2918
- title: opts["title"],
2919
- body: opts["body"],
2920
- description: opts["description"],
2921
- collection: opts["collection"],
3019
+ title: opts["title"] ?? undefined,
3020
+ body: opts["body"] ?? undefined,
3021
+ description: opts["description"] ?? undefined,
3022
+ collection: opts["collection"] ?? undefined,
2922
3023
  tags: opts["tags"] ? opts["tags"].split(",").map((t) => t.trim()) : undefined,
2923
- changed_by: opts["agent"]
3024
+ changed_by: opts["agent"] ?? undefined
2924
3025
  });
2925
3026
  if (isJson())
2926
3027
  output(prompt);
@@ -3079,4 +3180,140 @@ By collection:`));
3079
3180
  handleError(e);
3080
3181
  }
3081
3182
  });
3183
+ program2.command("recent [n]").description("Show recently used prompts (default: 10)").action((n) => {
3184
+ try {
3185
+ const limit = parseInt(n ?? "10") || 10;
3186
+ 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);
3187
+ if (isJson()) {
3188
+ output(prompts);
3189
+ return;
3190
+ }
3191
+ if (prompts.length === 0) {
3192
+ console.log(chalk.gray("No recently used prompts."));
3193
+ return;
3194
+ }
3195
+ for (const p of prompts) {
3196
+ const ago = chalk.gray(new Date(p.last_used_at).toLocaleString());
3197
+ console.log(`${chalk.bold(p.id)} ${chalk.green(p.slug)} ${p.title} ${ago}`);
3198
+ }
3199
+ } catch (e) {
3200
+ handleError(e);
3201
+ }
3202
+ });
3203
+ 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) => {
3204
+ try {
3205
+ const prompts = listPrompts({ collection: opts["collection"], limit: 1e4 });
3206
+ const results = lintAll(prompts);
3207
+ if (isJson()) {
3208
+ output(results);
3209
+ return;
3210
+ }
3211
+ if (results.length === 0) {
3212
+ console.log(chalk.green("\u2713 All prompts pass lint."));
3213
+ return;
3214
+ }
3215
+ let errors = 0, warns = 0, infos = 0;
3216
+ for (const { prompt: p, issues } of results) {
3217
+ console.log(`
3218
+ ${chalk.bold(p.slug)} ${chalk.gray(p.id)}`);
3219
+ for (const issue of issues) {
3220
+ if (issue.severity === "error") {
3221
+ console.log(chalk.red(` \u2717 [${issue.rule}] ${issue.message}`));
3222
+ errors++;
3223
+ } else if (issue.severity === "warn") {
3224
+ console.log(chalk.yellow(` \u26A0 [${issue.rule}] ${issue.message}`));
3225
+ warns++;
3226
+ } else {
3227
+ console.log(chalk.gray(` \u2139 [${issue.rule}] ${issue.message}`));
3228
+ infos++;
3229
+ }
3230
+ }
3231
+ }
3232
+ console.log(chalk.bold(`
3233
+ ${results.length} prompt(s) with issues \u2014 ${errors} errors, ${warns} warnings, ${infos} info`));
3234
+ if (errors > 0)
3235
+ process.exit(1);
3236
+ } catch (e) {
3237
+ handleError(e);
3238
+ }
3239
+ });
3240
+ program2.command("stale [days]").description("List prompts not used in N days (default: 30)").action((days) => {
3241
+ try {
3242
+ const threshold = parseInt(days ?? "30") || 30;
3243
+ const cutoff = new Date(Date.now() - threshold * 24 * 60 * 60 * 1000).toISOString();
3244
+ const all = listPrompts({ limit: 1e4 });
3245
+ 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 ?? ""));
3246
+ if (isJson()) {
3247
+ output(stale);
3248
+ return;
3249
+ }
3250
+ if (stale.length === 0) {
3251
+ console.log(chalk.green(`No stale prompts (threshold: ${threshold} days).`));
3252
+ return;
3253
+ }
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}`);
3258
+ }
3259
+ console.log(chalk.gray(`
3260
+ ${stale.length} stale prompt(s)`));
3261
+ } catch (e) {
3262
+ handleError(e);
3263
+ }
3264
+ });
3265
+ program2.command("pin <id>").description("Pin a prompt so it always appears first in lists").action((id) => {
3266
+ try {
3267
+ const p = pinPrompt(id, true);
3268
+ if (isJson())
3269
+ output(p);
3270
+ else
3271
+ console.log(chalk.yellow(`\uD83D\uDCCC Pinned ${chalk.bold(p.slug)}`));
3272
+ } catch (e) {
3273
+ handleError(e);
3274
+ }
3275
+ });
3276
+ program2.command("unpin <id>").description("Unpin a prompt").action((id) => {
3277
+ try {
3278
+ const p = pinPrompt(id, false);
3279
+ if (isJson())
3280
+ output(p);
3281
+ else
3282
+ console.log(chalk.gray(`Unpinned ${chalk.bold(p.slug)}`));
3283
+ } catch (e) {
3284
+ handleError(e);
3285
+ }
3286
+ });
3287
+ program2.command("copy <id>").description("Copy prompt body to clipboard and increment use counter").action(async (id) => {
3288
+ try {
3289
+ const prompt = usePrompt(id);
3290
+ const { platform } = process;
3291
+ if (platform === "darwin") {
3292
+ const proc = Bun.spawn(["pbcopy"], { stdin: "pipe" });
3293
+ proc.stdin.write(prompt.body);
3294
+ proc.stdin.end();
3295
+ await proc.exited;
3296
+ } else if (platform === "linux") {
3297
+ try {
3298
+ const proc = Bun.spawn(["xclip", "-selection", "clipboard"], { stdin: "pipe" });
3299
+ proc.stdin.write(prompt.body);
3300
+ proc.stdin.end();
3301
+ await proc.exited;
3302
+ } catch {
3303
+ const proc = Bun.spawn(["xsel", "--clipboard", "--input"], { stdin: "pipe" });
3304
+ proc.stdin.write(prompt.body);
3305
+ proc.stdin.end();
3306
+ await proc.exited;
3307
+ }
3308
+ } else {
3309
+ handleError("Clipboard not supported on this platform. Use `prompts use` instead.");
3310
+ }
3311
+ if (isJson())
3312
+ output({ copied: true, id: prompt.id, slug: prompt.slug });
3313
+ else
3314
+ console.log(chalk.green(`Copied ${chalk.bold(prompt.slug)} to clipboard`));
3315
+ } catch (e) {
3316
+ handleError(e);
3317
+ }
3318
+ });
3082
3319
  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"}