@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 +31 -31
- package/dist/cli/index.js +272 -35
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/prompts.d.ts +3 -1
- package/dist/db/prompts.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +78 -18
- package/dist/lib/duplicates.d.ts +7 -0
- package/dist/lib/duplicates.d.ts.map +1 -0
- package/dist/lib/ids.d.ts.map +1 -1
- package/dist/lib/lint.d.ts +16 -0
- package/dist/lib/lint.d.ts.map +1 -0
- package/dist/lib/mementos.d.ts +13 -0
- package/dist/lib/mementos.d.ts.map +1 -0
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/mcp/index.js +234 -29
- package/dist/server/index.js +74 -22
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -12,19 +12,19 @@ bun install -g @hasna/prompts
|
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
# Save a prompt
|
|
15
|
-
|
|
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
|
-
|
|
21
|
+
prompts use typescript-code-review
|
|
22
22
|
|
|
23
23
|
# Render a template
|
|
24
|
-
|
|
24
|
+
prompts render typescript-code-review --var code="$(cat myfile.ts)"
|
|
25
25
|
|
|
26
26
|
# Search
|
|
27
|
-
|
|
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": "
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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"}
|
package/dist/db/prompts.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/db/prompts.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../src/db/prompts.ts"],"names":[],"mappings":"
|
|
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|