@hasna/prompts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +269 -0
- package/dashboard/README.md +73 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +3082 -0
- package/dist/db/agents.d.ts +4 -0
- package/dist/db/agents.d.ts.map +1 -0
- package/dist/db/collections.d.ts +6 -0
- package/dist/db/collections.d.ts.map +1 -0
- package/dist/db/database.d.ts +8 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/prompts.d.ts +40 -0
- package/dist/db/prompts.d.ts.map +1 -0
- package/dist/db/versions.d.ts +5 -0
- package/dist/db/versions.d.ts.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +737 -0
- package/dist/lib/ids.d.ts +5 -0
- package/dist/lib/ids.d.ts.map +1 -0
- package/dist/lib/importer.d.ts +38 -0
- package/dist/lib/importer.d.ts.map +1 -0
- package/dist/lib/search.d.ts +4 -0
- package/dist/lib/search.d.ts.map +1 -0
- package/dist/lib/template.d.ts +15 -0
- package/dist/lib/template.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +4930 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +827 -0
- package/dist/types/index.d.ts +125 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +80 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/db/database.ts
|
|
3
|
+
import { Database } from "bun:sqlite";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { existsSync, mkdirSync } from "fs";
|
|
6
|
+
var _db = null;
|
|
7
|
+
function getDbPath() {
|
|
8
|
+
if (process.env["PROMPTS_DB_PATH"]) {
|
|
9
|
+
return process.env["PROMPTS_DB_PATH"];
|
|
10
|
+
}
|
|
11
|
+
if (process.env["PROMPTS_DB_SCOPE"] === "project") {
|
|
12
|
+
let dir = process.cwd();
|
|
13
|
+
while (true) {
|
|
14
|
+
const candidate = join(dir, ".prompts", "prompts.db");
|
|
15
|
+
if (existsSync(join(dir, ".git"))) {
|
|
16
|
+
return candidate;
|
|
17
|
+
}
|
|
18
|
+
const parent = join(dir, "..");
|
|
19
|
+
if (parent === dir)
|
|
20
|
+
break;
|
|
21
|
+
dir = parent;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
25
|
+
return join(home, ".prompts", "prompts.db");
|
|
26
|
+
}
|
|
27
|
+
function getDatabase() {
|
|
28
|
+
if (_db)
|
|
29
|
+
return _db;
|
|
30
|
+
const dbPath = getDbPath();
|
|
31
|
+
if (dbPath !== ":memory:") {
|
|
32
|
+
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
33
|
+
if (dir && !existsSync(dir)) {
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const db = new Database(dbPath, { create: true });
|
|
38
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
39
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
40
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
41
|
+
runMigrations(db);
|
|
42
|
+
_db = db;
|
|
43
|
+
return db;
|
|
44
|
+
}
|
|
45
|
+
function runMigrations(db) {
|
|
46
|
+
db.exec(`
|
|
47
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
48
|
+
id INTEGER PRIMARY KEY,
|
|
49
|
+
name TEXT NOT NULL,
|
|
50
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
51
|
+
)
|
|
52
|
+
`);
|
|
53
|
+
const applied = new Set(db.query("SELECT name FROM _migrations").all().map((r) => r.name));
|
|
54
|
+
const migrations = [
|
|
55
|
+
{
|
|
56
|
+
name: "001_initial",
|
|
57
|
+
sql: `
|
|
58
|
+
CREATE TABLE IF NOT EXISTS collections (
|
|
59
|
+
id TEXT PRIMARY KEY,
|
|
60
|
+
name TEXT NOT NULL UNIQUE,
|
|
61
|
+
description TEXT,
|
|
62
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
INSERT OR IGNORE INTO collections (id, name, description, created_at)
|
|
66
|
+
VALUES ('default', 'default', 'Default collection', datetime('now'));
|
|
67
|
+
|
|
68
|
+
CREATE TABLE IF NOT EXISTS prompts (
|
|
69
|
+
id TEXT PRIMARY KEY,
|
|
70
|
+
name TEXT NOT NULL,
|
|
71
|
+
slug TEXT NOT NULL UNIQUE,
|
|
72
|
+
title TEXT NOT NULL,
|
|
73
|
+
body TEXT NOT NULL,
|
|
74
|
+
description TEXT,
|
|
75
|
+
collection TEXT NOT NULL DEFAULT 'default' REFERENCES collections(name) ON UPDATE CASCADE,
|
|
76
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
77
|
+
variables TEXT NOT NULL DEFAULT '[]',
|
|
78
|
+
is_template INTEGER NOT NULL DEFAULT 0,
|
|
79
|
+
source TEXT NOT NULL DEFAULT 'manual',
|
|
80
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
81
|
+
use_count INTEGER NOT NULL DEFAULT 0,
|
|
82
|
+
last_used_at TEXT,
|
|
83
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
84
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
CREATE TABLE IF NOT EXISTS prompt_versions (
|
|
88
|
+
id TEXT PRIMARY KEY,
|
|
89
|
+
prompt_id TEXT NOT NULL REFERENCES prompts(id) ON DELETE CASCADE,
|
|
90
|
+
body TEXT NOT NULL,
|
|
91
|
+
version INTEGER NOT NULL,
|
|
92
|
+
changed_by TEXT,
|
|
93
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
97
|
+
id TEXT PRIMARY KEY,
|
|
98
|
+
name TEXT NOT NULL UNIQUE,
|
|
99
|
+
description TEXT,
|
|
100
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
101
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_prompts_collection ON prompts(collection);
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_prompts_source ON prompts(source);
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_prompts_is_template ON prompts(is_template);
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_prompts_use_count ON prompts(use_count DESC);
|
|
108
|
+
CREATE INDEX IF NOT EXISTS idx_prompts_last_used ON prompts(last_used_at DESC);
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_versions_prompt_id ON prompt_versions(prompt_id);
|
|
110
|
+
`
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "002_fts5",
|
|
114
|
+
sql: `
|
|
115
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS prompts_fts USING fts5(
|
|
116
|
+
name,
|
|
117
|
+
slug,
|
|
118
|
+
title,
|
|
119
|
+
body,
|
|
120
|
+
description,
|
|
121
|
+
tags,
|
|
122
|
+
content='prompts',
|
|
123
|
+
content_rowid='rowid'
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
CREATE TRIGGER IF NOT EXISTS prompts_fts_insert AFTER INSERT ON prompts BEGIN
|
|
127
|
+
INSERT INTO prompts_fts(rowid, name, slug, title, body, description, tags)
|
|
128
|
+
VALUES (new.rowid, new.name, new.slug, new.title, new.body, COALESCE(new.description,''), new.tags);
|
|
129
|
+
END;
|
|
130
|
+
|
|
131
|
+
CREATE TRIGGER IF NOT EXISTS prompts_fts_update AFTER UPDATE ON prompts BEGIN
|
|
132
|
+
INSERT INTO prompts_fts(prompts_fts, rowid, name, slug, title, body, description, tags)
|
|
133
|
+
VALUES ('delete', old.rowid, old.name, old.slug, old.title, old.body, COALESCE(old.description,''), old.tags);
|
|
134
|
+
INSERT INTO prompts_fts(rowid, name, slug, title, body, description, tags)
|
|
135
|
+
VALUES (new.rowid, new.name, new.slug, new.title, new.body, COALESCE(new.description,''), new.tags);
|
|
136
|
+
END;
|
|
137
|
+
|
|
138
|
+
CREATE TRIGGER IF NOT EXISTS prompts_fts_delete AFTER DELETE ON prompts BEGIN
|
|
139
|
+
INSERT INTO prompts_fts(prompts_fts, rowid, name, slug, title, body, description, tags)
|
|
140
|
+
VALUES ('delete', old.rowid, old.name, old.slug, old.title, old.body, COALESCE(old.description,''), old.tags);
|
|
141
|
+
END;
|
|
142
|
+
`
|
|
143
|
+
}
|
|
144
|
+
];
|
|
145
|
+
for (const migration of migrations) {
|
|
146
|
+
if (applied.has(migration.name))
|
|
147
|
+
continue;
|
|
148
|
+
db.exec(migration.sql);
|
|
149
|
+
db.run("INSERT INTO _migrations (name) VALUES (?)", [migration.name]);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function hasFts(db) {
|
|
153
|
+
return db.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='prompts_fts'").get() !== null;
|
|
154
|
+
}
|
|
155
|
+
function resolvePrompt(db, idOrSlug) {
|
|
156
|
+
const byId = db.query("SELECT id FROM prompts WHERE id = ?").get(idOrSlug);
|
|
157
|
+
if (byId)
|
|
158
|
+
return byId.id;
|
|
159
|
+
const bySlug = db.query("SELECT id FROM prompts WHERE slug = ?").get(idOrSlug);
|
|
160
|
+
if (bySlug)
|
|
161
|
+
return bySlug.id;
|
|
162
|
+
const byPrefix = db.query("SELECT id FROM prompts WHERE id LIKE ? LIMIT 2").all(`${idOrSlug}%`);
|
|
163
|
+
if (byPrefix.length === 1 && byPrefix[0])
|
|
164
|
+
return byPrefix[0].id;
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/lib/ids.ts
|
|
169
|
+
function generateSlug(title) {
|
|
170
|
+
return title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").substring(0, 80);
|
|
171
|
+
}
|
|
172
|
+
function uniqueSlug(baseSlug) {
|
|
173
|
+
const db = getDatabase();
|
|
174
|
+
let slug = baseSlug;
|
|
175
|
+
let i = 2;
|
|
176
|
+
while (db.query("SELECT 1 FROM prompts WHERE slug = ?").get(slug)) {
|
|
177
|
+
slug = `${baseSlug}-${i}`;
|
|
178
|
+
i++;
|
|
179
|
+
}
|
|
180
|
+
return slug;
|
|
181
|
+
}
|
|
182
|
+
function generatePromptId() {
|
|
183
|
+
const db = getDatabase();
|
|
184
|
+
const row = db.query("SELECT id FROM prompts ORDER BY rowid DESC LIMIT 1").get();
|
|
185
|
+
let next = 1;
|
|
186
|
+
if (row) {
|
|
187
|
+
const match = row.id.match(/PRMT-(\d+)/);
|
|
188
|
+
if (match && match[1]) {
|
|
189
|
+
next = parseInt(match[1], 10) + 1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return `PRMT-${String(next).padStart(5, "0")}`;
|
|
193
|
+
}
|
|
194
|
+
function generateId(prefix) {
|
|
195
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
196
|
+
let id = prefix + "-";
|
|
197
|
+
for (let i = 0;i < 8; i++) {
|
|
198
|
+
id += chars[Math.floor(Math.random() * chars.length)];
|
|
199
|
+
}
|
|
200
|
+
return id;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/db/collections.ts
|
|
204
|
+
function rowToCollection(row) {
|
|
205
|
+
return {
|
|
206
|
+
id: row["id"],
|
|
207
|
+
name: row["name"],
|
|
208
|
+
description: row["description"] ?? null,
|
|
209
|
+
prompt_count: row["prompt_count"] ?? 0,
|
|
210
|
+
created_at: row["created_at"]
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function listCollections() {
|
|
214
|
+
const db = getDatabase();
|
|
215
|
+
const rows = db.query(`SELECT c.*, COUNT(p.id) as prompt_count
|
|
216
|
+
FROM collections c
|
|
217
|
+
LEFT JOIN prompts p ON p.collection = c.name
|
|
218
|
+
GROUP BY c.id
|
|
219
|
+
ORDER BY c.name`).all();
|
|
220
|
+
return rows.map(rowToCollection);
|
|
221
|
+
}
|
|
222
|
+
function getCollection(name) {
|
|
223
|
+
const db = getDatabase();
|
|
224
|
+
const row = db.query(`SELECT c.*, COUNT(p.id) as prompt_count
|
|
225
|
+
FROM collections c
|
|
226
|
+
LEFT JOIN prompts p ON p.collection = c.name
|
|
227
|
+
WHERE c.name = ?
|
|
228
|
+
GROUP BY c.id`).get(name);
|
|
229
|
+
if (!row)
|
|
230
|
+
return null;
|
|
231
|
+
return rowToCollection(row);
|
|
232
|
+
}
|
|
233
|
+
function ensureCollection(name, description) {
|
|
234
|
+
const db = getDatabase();
|
|
235
|
+
const existing = db.query("SELECT id FROM collections WHERE name = ?").get(name);
|
|
236
|
+
if (!existing) {
|
|
237
|
+
const id = generateId("COL");
|
|
238
|
+
db.run("INSERT INTO collections (id, name, description) VALUES (?, ?, ?)", [
|
|
239
|
+
id,
|
|
240
|
+
name,
|
|
241
|
+
description ?? null
|
|
242
|
+
]);
|
|
243
|
+
}
|
|
244
|
+
return getCollection(name);
|
|
245
|
+
}
|
|
246
|
+
function movePrompt(promptIdOrSlug, targetCollection) {
|
|
247
|
+
const db = getDatabase();
|
|
248
|
+
ensureCollection(targetCollection);
|
|
249
|
+
const row = db.query("SELECT id FROM prompts WHERE id = ? OR slug = ?").get(promptIdOrSlug, promptIdOrSlug);
|
|
250
|
+
if (!row)
|
|
251
|
+
throw new Error(`Prompt not found: ${promptIdOrSlug}`);
|
|
252
|
+
db.run("UPDATE prompts SET collection = ?, updated_at = datetime('now') WHERE id = ?", [
|
|
253
|
+
targetCollection,
|
|
254
|
+
row.id
|
|
255
|
+
]);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/lib/template.ts
|
|
259
|
+
var VAR_PATTERN = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\|\s*(.*?)\s*)?\}\}/g;
|
|
260
|
+
function extractVariables(body) {
|
|
261
|
+
const vars = new Set;
|
|
262
|
+
const pattern = new RegExp(VAR_PATTERN.source, "g");
|
|
263
|
+
let match;
|
|
264
|
+
while ((match = pattern.exec(body)) !== null) {
|
|
265
|
+
if (match[1])
|
|
266
|
+
vars.add(match[1]);
|
|
267
|
+
}
|
|
268
|
+
return Array.from(vars);
|
|
269
|
+
}
|
|
270
|
+
function extractVariableInfo(body) {
|
|
271
|
+
const seen = new Map;
|
|
272
|
+
const pattern = new RegExp(VAR_PATTERN.source, "g");
|
|
273
|
+
let match;
|
|
274
|
+
while ((match = pattern.exec(body)) !== null) {
|
|
275
|
+
const name = match[1];
|
|
276
|
+
if (!name)
|
|
277
|
+
continue;
|
|
278
|
+
const defaultVal = match[2] !== undefined ? match[2] : null;
|
|
279
|
+
if (!seen.has(name)) {
|
|
280
|
+
seen.set(name, { name, default: defaultVal, required: defaultVal === null });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return Array.from(seen.values());
|
|
284
|
+
}
|
|
285
|
+
function renderTemplate(body, vars) {
|
|
286
|
+
const missing = [];
|
|
287
|
+
const usedDefaults = [];
|
|
288
|
+
const rendered = body.replace(VAR_PATTERN, (_match, name, defaultVal) => {
|
|
289
|
+
if (name in vars)
|
|
290
|
+
return vars[name] ?? "";
|
|
291
|
+
if (defaultVal !== undefined) {
|
|
292
|
+
usedDefaults.push(name);
|
|
293
|
+
return defaultVal;
|
|
294
|
+
}
|
|
295
|
+
missing.push(name);
|
|
296
|
+
return _match;
|
|
297
|
+
});
|
|
298
|
+
return { rendered, missing_vars: missing, used_defaults: usedDefaults };
|
|
299
|
+
}
|
|
300
|
+
function validateVars(body, provided) {
|
|
301
|
+
const infos = extractVariableInfo(body);
|
|
302
|
+
const required = infos.filter((v) => v.required).map((v) => v.name);
|
|
303
|
+
const optional = infos.filter((v) => !v.required).map((v) => v.name);
|
|
304
|
+
const all = infos.map((v) => v.name);
|
|
305
|
+
const missing = required.filter((v) => !(v in provided));
|
|
306
|
+
const extra = Object.keys(provided).filter((v) => !all.includes(v));
|
|
307
|
+
return { missing, extra, optional };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/types/index.ts
|
|
311
|
+
class PromptNotFoundError extends Error {
|
|
312
|
+
constructor(id) {
|
|
313
|
+
super(`Prompt not found: ${id}`);
|
|
314
|
+
this.name = "PromptNotFoundError";
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
class VersionConflictError extends Error {
|
|
319
|
+
constructor(id) {
|
|
320
|
+
super(`Version conflict on prompt: ${id}`);
|
|
321
|
+
this.name = "VersionConflictError";
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
class DuplicateSlugError extends Error {
|
|
326
|
+
constructor(slug) {
|
|
327
|
+
super(`A prompt with slug "${slug}" already exists`);
|
|
328
|
+
this.name = "DuplicateSlugError";
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
class TemplateRenderError extends Error {
|
|
333
|
+
constructor(message) {
|
|
334
|
+
super(message);
|
|
335
|
+
this.name = "TemplateRenderError";
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/db/prompts.ts
|
|
340
|
+
function rowToPrompt(row) {
|
|
341
|
+
return {
|
|
342
|
+
id: row["id"],
|
|
343
|
+
name: row["name"],
|
|
344
|
+
slug: row["slug"],
|
|
345
|
+
title: row["title"],
|
|
346
|
+
body: row["body"],
|
|
347
|
+
description: row["description"] ?? null,
|
|
348
|
+
collection: row["collection"],
|
|
349
|
+
tags: JSON.parse(row["tags"] || "[]"),
|
|
350
|
+
variables: JSON.parse(row["variables"] || "[]"),
|
|
351
|
+
is_template: Boolean(row["is_template"]),
|
|
352
|
+
source: row["source"],
|
|
353
|
+
version: row["version"],
|
|
354
|
+
use_count: row["use_count"],
|
|
355
|
+
last_used_at: row["last_used_at"] ?? null,
|
|
356
|
+
created_at: row["created_at"],
|
|
357
|
+
updated_at: row["updated_at"]
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function createPrompt(input) {
|
|
361
|
+
const db = getDatabase();
|
|
362
|
+
const slug = input.slug ? input.slug : uniqueSlug(generateSlug(input.title));
|
|
363
|
+
if (input.slug) {
|
|
364
|
+
const existing = db.query("SELECT id FROM prompts WHERE slug = ?").get(input.slug);
|
|
365
|
+
if (existing)
|
|
366
|
+
throw new DuplicateSlugError(input.slug);
|
|
367
|
+
}
|
|
368
|
+
const id = generatePromptId();
|
|
369
|
+
const name = input.name || input.title;
|
|
370
|
+
const collection = input.collection || "default";
|
|
371
|
+
ensureCollection(collection);
|
|
372
|
+
const tags = JSON.stringify(input.tags || []);
|
|
373
|
+
const source = input.source || "manual";
|
|
374
|
+
const vars = extractVariables(input.body);
|
|
375
|
+
const variables = JSON.stringify(vars.map((v) => ({ name: v, required: true })));
|
|
376
|
+
const is_template = vars.length > 0 ? 1 : 0;
|
|
377
|
+
db.run(`INSERT INTO prompts (id, name, slug, title, body, description, collection, tags, variables, is_template, source)
|
|
378
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, name, slug, input.title, input.body, input.description ?? null, collection, tags, variables, is_template, source]);
|
|
379
|
+
db.run(`INSERT INTO prompt_versions (id, prompt_id, body, version, changed_by)
|
|
380
|
+
VALUES (?, ?, ?, 1, ?)`, [generateId("VER"), id, input.body, input.changed_by ?? null]);
|
|
381
|
+
return getPrompt(id);
|
|
382
|
+
}
|
|
383
|
+
function getPrompt(idOrSlug) {
|
|
384
|
+
const db = getDatabase();
|
|
385
|
+
const id = resolvePrompt(db, idOrSlug);
|
|
386
|
+
if (!id)
|
|
387
|
+
return null;
|
|
388
|
+
const row = db.query("SELECT * FROM prompts WHERE id = ?").get(id);
|
|
389
|
+
if (!row)
|
|
390
|
+
return null;
|
|
391
|
+
return rowToPrompt(row);
|
|
392
|
+
}
|
|
393
|
+
function requirePrompt(idOrSlug) {
|
|
394
|
+
const prompt = getPrompt(idOrSlug);
|
|
395
|
+
if (!prompt)
|
|
396
|
+
throw new PromptNotFoundError(idOrSlug);
|
|
397
|
+
return prompt;
|
|
398
|
+
}
|
|
399
|
+
function listPrompts(filter = {}) {
|
|
400
|
+
const db = getDatabase();
|
|
401
|
+
const conditions = [];
|
|
402
|
+
const params = [];
|
|
403
|
+
if (filter.collection) {
|
|
404
|
+
conditions.push("collection = ?");
|
|
405
|
+
params.push(filter.collection);
|
|
406
|
+
}
|
|
407
|
+
if (filter.is_template !== undefined) {
|
|
408
|
+
conditions.push("is_template = ?");
|
|
409
|
+
params.push(filter.is_template ? 1 : 0);
|
|
410
|
+
}
|
|
411
|
+
if (filter.source) {
|
|
412
|
+
conditions.push("source = ?");
|
|
413
|
+
params.push(filter.source);
|
|
414
|
+
}
|
|
415
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
416
|
+
const tagConditions = filter.tags.map(() => "tags LIKE ?");
|
|
417
|
+
conditions.push(`(${tagConditions.join(" OR ")})`);
|
|
418
|
+
for (const tag of filter.tags) {
|
|
419
|
+
params.push(`%"${tag}"%`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
423
|
+
const limit = filter.limit ?? 100;
|
|
424
|
+
const offset = filter.offset ?? 0;
|
|
425
|
+
const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY use_count DESC, updated_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
426
|
+
return rows.map(rowToPrompt);
|
|
427
|
+
}
|
|
428
|
+
function updatePrompt(idOrSlug, input) {
|
|
429
|
+
const db = getDatabase();
|
|
430
|
+
const prompt = requirePrompt(idOrSlug);
|
|
431
|
+
const newBody = input.body ?? prompt.body;
|
|
432
|
+
const vars = extractVariables(newBody);
|
|
433
|
+
const variables = JSON.stringify(vars.map((v) => ({ name: v, required: true })));
|
|
434
|
+
const is_template = vars.length > 0 ? 1 : 0;
|
|
435
|
+
const updated = db.run(`UPDATE prompts SET
|
|
436
|
+
title = COALESCE(?, title),
|
|
437
|
+
body = COALESCE(?, body),
|
|
438
|
+
description = COALESCE(?, description),
|
|
439
|
+
collection = COALESCE(?, collection),
|
|
440
|
+
tags = COALESCE(?, tags),
|
|
441
|
+
variables = ?,
|
|
442
|
+
is_template = ?,
|
|
443
|
+
version = version + 1,
|
|
444
|
+
updated_at = datetime('now')
|
|
445
|
+
WHERE id = ? AND version = ?`, [
|
|
446
|
+
input.title ?? null,
|
|
447
|
+
input.body ?? null,
|
|
448
|
+
input.description ?? null,
|
|
449
|
+
input.collection ?? null,
|
|
450
|
+
input.tags ? JSON.stringify(input.tags) : null,
|
|
451
|
+
variables,
|
|
452
|
+
is_template,
|
|
453
|
+
prompt.id,
|
|
454
|
+
prompt.version
|
|
455
|
+
]);
|
|
456
|
+
if (updated.changes === 0)
|
|
457
|
+
throw new VersionConflictError(prompt.id);
|
|
458
|
+
if (input.body && input.body !== prompt.body) {
|
|
459
|
+
db.run(`INSERT INTO prompt_versions (id, prompt_id, body, version, changed_by)
|
|
460
|
+
VALUES (?, ?, ?, ?, ?)`, [generateId("VER"), prompt.id, input.body, prompt.version + 1, input.changed_by ?? null]);
|
|
461
|
+
}
|
|
462
|
+
return requirePrompt(prompt.id);
|
|
463
|
+
}
|
|
464
|
+
function deletePrompt(idOrSlug) {
|
|
465
|
+
const db = getDatabase();
|
|
466
|
+
const prompt = requirePrompt(idOrSlug);
|
|
467
|
+
db.run("DELETE FROM prompts WHERE id = ?", [prompt.id]);
|
|
468
|
+
}
|
|
469
|
+
function usePrompt(idOrSlug) {
|
|
470
|
+
const db = getDatabase();
|
|
471
|
+
const prompt = requirePrompt(idOrSlug);
|
|
472
|
+
db.run("UPDATE prompts SET use_count = use_count + 1, last_used_at = datetime('now') WHERE id = ?", [prompt.id]);
|
|
473
|
+
return requirePrompt(prompt.id);
|
|
474
|
+
}
|
|
475
|
+
function upsertPrompt(input) {
|
|
476
|
+
const db = getDatabase();
|
|
477
|
+
const slug = input.slug || generateSlug(input.title);
|
|
478
|
+
const existing = db.query("SELECT id FROM prompts WHERE slug = ?").get(slug);
|
|
479
|
+
if (existing) {
|
|
480
|
+
const prompt2 = updatePrompt(existing.id, {
|
|
481
|
+
title: input.title,
|
|
482
|
+
body: input.body,
|
|
483
|
+
description: input.description,
|
|
484
|
+
collection: input.collection,
|
|
485
|
+
tags: input.tags,
|
|
486
|
+
changed_by: input.changed_by
|
|
487
|
+
});
|
|
488
|
+
return { prompt: prompt2, created: false };
|
|
489
|
+
}
|
|
490
|
+
const prompt = createPrompt({ ...input, slug });
|
|
491
|
+
return { prompt, created: true };
|
|
492
|
+
}
|
|
493
|
+
function getPromptStats() {
|
|
494
|
+
const db = getDatabase();
|
|
495
|
+
const total = db.query("SELECT COUNT(*) as n FROM prompts").get().n;
|
|
496
|
+
const templates = db.query("SELECT COUNT(*) as n FROM prompts WHERE is_template = 1").get().n;
|
|
497
|
+
const collections = db.query("SELECT COUNT(DISTINCT collection) as n FROM prompts").get().n;
|
|
498
|
+
const mostUsed = db.query("SELECT id, name, slug, title, use_count FROM prompts WHERE use_count > 0 ORDER BY use_count DESC LIMIT 10").all();
|
|
499
|
+
const recentlyUsed = db.query("SELECT id, name, slug, title, last_used_at FROM prompts WHERE last_used_at IS NOT NULL ORDER BY last_used_at DESC LIMIT 10").all();
|
|
500
|
+
const byCollection = db.query("SELECT collection, COUNT(*) as count FROM prompts GROUP BY collection ORDER BY count DESC").all();
|
|
501
|
+
const bySource = db.query("SELECT source, COUNT(*) as count FROM prompts GROUP BY source ORDER BY count DESC").all();
|
|
502
|
+
return { total_prompts: total, total_templates: templates, total_collections: collections, most_used: mostUsed, recently_used: recentlyUsed, by_collection: byCollection, by_source: bySource };
|
|
503
|
+
}
|
|
504
|
+
// src/db/versions.ts
|
|
505
|
+
function rowToVersion(row) {
|
|
506
|
+
return {
|
|
507
|
+
id: row["id"],
|
|
508
|
+
prompt_id: row["prompt_id"],
|
|
509
|
+
body: row["body"],
|
|
510
|
+
version: row["version"],
|
|
511
|
+
changed_by: row["changed_by"] ?? null,
|
|
512
|
+
created_at: row["created_at"]
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function listVersions(promptId) {
|
|
516
|
+
const db = getDatabase();
|
|
517
|
+
const rows = db.query("SELECT * FROM prompt_versions WHERE prompt_id = ? ORDER BY version DESC").all(promptId);
|
|
518
|
+
return rows.map(rowToVersion);
|
|
519
|
+
}
|
|
520
|
+
function getVersion(promptId, version) {
|
|
521
|
+
const db = getDatabase();
|
|
522
|
+
const row = db.query("SELECT * FROM prompt_versions WHERE prompt_id = ? AND version = ?").get(promptId, version);
|
|
523
|
+
if (!row)
|
|
524
|
+
return null;
|
|
525
|
+
return rowToVersion(row);
|
|
526
|
+
}
|
|
527
|
+
function restoreVersion(promptId, version, changedBy) {
|
|
528
|
+
const db = getDatabase();
|
|
529
|
+
const ver = getVersion(promptId, version);
|
|
530
|
+
if (!ver)
|
|
531
|
+
throw new PromptNotFoundError(`${promptId}@v${version}`);
|
|
532
|
+
const current = db.query("SELECT version FROM prompts WHERE id = ?").get(promptId);
|
|
533
|
+
if (!current)
|
|
534
|
+
throw new PromptNotFoundError(promptId);
|
|
535
|
+
const newVersion = current.version + 1;
|
|
536
|
+
db.run(`UPDATE prompts SET body = ?, version = ?, updated_at = datetime('now'),
|
|
537
|
+
is_template = (CASE WHEN body LIKE '%{{%' THEN 1 ELSE 0 END)
|
|
538
|
+
WHERE id = ?`, [ver.body, newVersion, promptId]);
|
|
539
|
+
db.run(`INSERT INTO prompt_versions (id, prompt_id, body, version, changed_by)
|
|
540
|
+
VALUES (?, ?, ?, ?, ?)`, [generateId("VER"), promptId, ver.body, newVersion, changedBy ?? null]);
|
|
541
|
+
}
|
|
542
|
+
// src/db/agents.ts
|
|
543
|
+
function rowToAgent(row) {
|
|
544
|
+
return {
|
|
545
|
+
id: row["id"],
|
|
546
|
+
name: row["name"],
|
|
547
|
+
description: row["description"] ?? null,
|
|
548
|
+
created_at: row["created_at"],
|
|
549
|
+
last_seen_at: row["last_seen_at"]
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function registerAgent(name, description) {
|
|
553
|
+
const db = getDatabase();
|
|
554
|
+
const existing = db.query("SELECT * FROM agents WHERE name = ?").get(name);
|
|
555
|
+
if (existing) {
|
|
556
|
+
db.run("UPDATE agents SET last_seen_at = datetime('now'), description = COALESCE(?, description) WHERE name = ?", [
|
|
557
|
+
description ?? null,
|
|
558
|
+
name
|
|
559
|
+
]);
|
|
560
|
+
return rowToAgent(db.query("SELECT * FROM agents WHERE name = ?").get(name));
|
|
561
|
+
}
|
|
562
|
+
const id = generateId("AGT");
|
|
563
|
+
db.run("INSERT INTO agents (id, name, description) VALUES (?, ?, ?)", [id, name, description ?? null]);
|
|
564
|
+
return rowToAgent(db.query("SELECT * FROM agents WHERE id = ?").get(id));
|
|
565
|
+
}
|
|
566
|
+
function listAgents() {
|
|
567
|
+
const db = getDatabase();
|
|
568
|
+
const rows = db.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
|
|
569
|
+
return rows.map(rowToAgent);
|
|
570
|
+
}
|
|
571
|
+
// src/lib/search.ts
|
|
572
|
+
function rowToSearchResult(row, snippet) {
|
|
573
|
+
return {
|
|
574
|
+
prompt: {
|
|
575
|
+
id: row["id"],
|
|
576
|
+
name: row["name"],
|
|
577
|
+
slug: row["slug"],
|
|
578
|
+
title: row["title"],
|
|
579
|
+
body: row["body"],
|
|
580
|
+
description: row["description"] ?? null,
|
|
581
|
+
collection: row["collection"],
|
|
582
|
+
tags: JSON.parse(row["tags"] || "[]"),
|
|
583
|
+
variables: JSON.parse(row["variables"] || "[]"),
|
|
584
|
+
is_template: Boolean(row["is_template"]),
|
|
585
|
+
source: row["source"],
|
|
586
|
+
version: row["version"],
|
|
587
|
+
use_count: row["use_count"],
|
|
588
|
+
last_used_at: row["last_used_at"] ?? null,
|
|
589
|
+
created_at: row["created_at"],
|
|
590
|
+
updated_at: row["updated_at"]
|
|
591
|
+
},
|
|
592
|
+
score: row["score"] ?? 1,
|
|
593
|
+
snippet
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
function escapeFtsQuery(q) {
|
|
597
|
+
return q.trim().split(/\s+/).filter(Boolean).map((w) => `"${w.replace(/"/g, '""')}"*`).join(" ");
|
|
598
|
+
}
|
|
599
|
+
function searchPrompts(query, filter = {}) {
|
|
600
|
+
const db = getDatabase();
|
|
601
|
+
if (!query.trim()) {
|
|
602
|
+
const prompts = listPrompts(filter);
|
|
603
|
+
return prompts.map((p) => ({ prompt: p, score: 1 }));
|
|
604
|
+
}
|
|
605
|
+
if (hasFts(db)) {
|
|
606
|
+
const ftsQuery = escapeFtsQuery(query);
|
|
607
|
+
const conditions = [];
|
|
608
|
+
const params = [];
|
|
609
|
+
if (filter.collection) {
|
|
610
|
+
conditions.push("p.collection = ?");
|
|
611
|
+
params.push(filter.collection);
|
|
612
|
+
}
|
|
613
|
+
if (filter.is_template !== undefined) {
|
|
614
|
+
conditions.push("p.is_template = ?");
|
|
615
|
+
params.push(filter.is_template ? 1 : 0);
|
|
616
|
+
}
|
|
617
|
+
if (filter.source) {
|
|
618
|
+
conditions.push("p.source = ?");
|
|
619
|
+
params.push(filter.source);
|
|
620
|
+
}
|
|
621
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
622
|
+
const tagConds = filter.tags.map(() => "p.tags LIKE ?");
|
|
623
|
+
conditions.push(`(${tagConds.join(" OR ")})`);
|
|
624
|
+
for (const tag of filter.tags)
|
|
625
|
+
params.push(`%"${tag}"%`);
|
|
626
|
+
}
|
|
627
|
+
const where = conditions.length > 0 ? `AND ${conditions.join(" AND ")}` : "";
|
|
628
|
+
const limit = filter.limit ?? 50;
|
|
629
|
+
const offset = filter.offset ?? 0;
|
|
630
|
+
try {
|
|
631
|
+
const rows2 = db.query(`SELECT p.*, bm25(prompts_fts) as score,
|
|
632
|
+
snippet(prompts_fts, 2, '[', ']', '...', 10) as snippet
|
|
633
|
+
FROM prompts p
|
|
634
|
+
INNER JOIN prompts_fts ON prompts_fts.rowid = p.rowid
|
|
635
|
+
WHERE prompts_fts MATCH ?
|
|
636
|
+
${where}
|
|
637
|
+
ORDER BY bm25(prompts_fts)
|
|
638
|
+
LIMIT ? OFFSET ?`).all(ftsQuery, ...params, limit, offset);
|
|
639
|
+
return rows2.map((r) => rowToSearchResult(r, r["snippet"]));
|
|
640
|
+
} catch {}
|
|
641
|
+
}
|
|
642
|
+
const like = `%${query}%`;
|
|
643
|
+
const rows = db.query(`SELECT *, 1 as score FROM prompts
|
|
644
|
+
WHERE (name LIKE ? OR slug LIKE ? OR title LIKE ? OR body LIKE ? OR description LIKE ? OR tags LIKE ?)
|
|
645
|
+
ORDER BY use_count DESC, updated_at DESC
|
|
646
|
+
LIMIT ? OFFSET ?`).all(like, like, like, like, like, like, filter.limit ?? 50, filter.offset ?? 0);
|
|
647
|
+
return rows.map((r) => rowToSearchResult(r));
|
|
648
|
+
}
|
|
649
|
+
function findSimilar(promptId, limit = 5) {
|
|
650
|
+
const db = getDatabase();
|
|
651
|
+
const prompt = db.query("SELECT * FROM prompts WHERE id = ?").get(promptId);
|
|
652
|
+
if (!prompt)
|
|
653
|
+
return [];
|
|
654
|
+
const tags = JSON.parse(prompt["tags"] || "[]");
|
|
655
|
+
const collection = prompt["collection"];
|
|
656
|
+
if (tags.length === 0) {
|
|
657
|
+
const rows = db.query("SELECT *, 1 as score FROM prompts WHERE collection = ? AND id != ? ORDER BY use_count DESC LIMIT ?").all(collection, promptId, limit);
|
|
658
|
+
return rows.map((r) => rowToSearchResult(r));
|
|
659
|
+
}
|
|
660
|
+
const allRows = db.query("SELECT * FROM prompts WHERE id != ?").all(promptId);
|
|
661
|
+
const scored = allRows.map((row) => {
|
|
662
|
+
const rowTags = JSON.parse(row["tags"] || "[]");
|
|
663
|
+
const overlap = rowTags.filter((t) => tags.includes(t)).length;
|
|
664
|
+
const sameCollection = row["collection"] === collection ? 1 : 0;
|
|
665
|
+
return { row, score: overlap * 2 + sameCollection };
|
|
666
|
+
});
|
|
667
|
+
return scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score).slice(0, limit).map((s) => rowToSearchResult(s.row, undefined));
|
|
668
|
+
}
|
|
669
|
+
// src/lib/importer.ts
|
|
670
|
+
function importFromJson(items, changedBy) {
|
|
671
|
+
let created = 0;
|
|
672
|
+
let updated = 0;
|
|
673
|
+
const errors = [];
|
|
674
|
+
for (const item of items) {
|
|
675
|
+
try {
|
|
676
|
+
const input = {
|
|
677
|
+
title: item.title,
|
|
678
|
+
body: item.body,
|
|
679
|
+
slug: item.slug,
|
|
680
|
+
description: item.description,
|
|
681
|
+
collection: item.collection,
|
|
682
|
+
tags: item.tags,
|
|
683
|
+
source: "imported",
|
|
684
|
+
changed_by: changedBy
|
|
685
|
+
};
|
|
686
|
+
const { created: wasCreated } = upsertPrompt(input);
|
|
687
|
+
if (wasCreated)
|
|
688
|
+
created++;
|
|
689
|
+
else
|
|
690
|
+
updated++;
|
|
691
|
+
} catch (e) {
|
|
692
|
+
errors.push({ item: item.title, error: e instanceof Error ? e.message : String(e) });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return { created, updated, errors };
|
|
696
|
+
}
|
|
697
|
+
function exportToJson(collection) {
|
|
698
|
+
const prompts = listPrompts({ collection, limit: 1e4 });
|
|
699
|
+
return { prompts, exported_at: new Date().toISOString(), collection };
|
|
700
|
+
}
|
|
701
|
+
export {
|
|
702
|
+
validateVars,
|
|
703
|
+
usePrompt,
|
|
704
|
+
upsertPrompt,
|
|
705
|
+
updatePrompt,
|
|
706
|
+
uniqueSlug,
|
|
707
|
+
searchPrompts,
|
|
708
|
+
restoreVersion,
|
|
709
|
+
requirePrompt,
|
|
710
|
+
renderTemplate,
|
|
711
|
+
registerAgent,
|
|
712
|
+
movePrompt,
|
|
713
|
+
listVersions,
|
|
714
|
+
listPrompts,
|
|
715
|
+
listCollections,
|
|
716
|
+
listAgents,
|
|
717
|
+
importFromJson,
|
|
718
|
+
getVersion,
|
|
719
|
+
getPromptStats,
|
|
720
|
+
getPrompt,
|
|
721
|
+
getDbPath,
|
|
722
|
+
getDatabase,
|
|
723
|
+
getCollection,
|
|
724
|
+
generateSlug,
|
|
725
|
+
generatePromptId,
|
|
726
|
+
findSimilar,
|
|
727
|
+
extractVariables,
|
|
728
|
+
extractVariableInfo,
|
|
729
|
+
exportToJson,
|
|
730
|
+
ensureCollection,
|
|
731
|
+
deletePrompt,
|
|
732
|
+
createPrompt,
|
|
733
|
+
VersionConflictError,
|
|
734
|
+
TemplateRenderError,
|
|
735
|
+
PromptNotFoundError,
|
|
736
|
+
DuplicateSlugError
|
|
737
|
+
};
|