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