@hasna/prompts 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -31
- package/dist/cli/index.js +272 -35
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/prompts.d.ts +3 -1
- package/dist/db/prompts.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +78 -18
- package/dist/lib/duplicates.d.ts +7 -0
- package/dist/lib/duplicates.d.ts.map +1 -0
- package/dist/lib/ids.d.ts.map +1 -1
- package/dist/lib/lint.d.ts +16 -0
- package/dist/lib/lint.d.ts.map +1 -0
- package/dist/lib/mementos.d.ts +13 -0
- package/dist/lib/mementos.d.ts.map +1 -0
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/mcp/index.js +234 -29
- package/dist/server/index.js +74 -22
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -109,6 +109,10 @@ function runMigrations(db) {
|
|
|
109
109
|
CREATE INDEX IF NOT EXISTS idx_versions_prompt_id ON prompt_versions(prompt_id);
|
|
110
110
|
`
|
|
111
111
|
},
|
|
112
|
+
{
|
|
113
|
+
name: "003_pinned",
|
|
114
|
+
sql: `ALTER TABLE prompts ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;`
|
|
115
|
+
},
|
|
112
116
|
{
|
|
113
117
|
name: "002_fts5",
|
|
114
118
|
sql: `
|
|
@@ -162,6 +166,15 @@ function resolvePrompt(db, idOrSlug) {
|
|
|
162
166
|
const byPrefix = db.query("SELECT id FROM prompts WHERE id LIKE ? LIMIT 2").all(`${idOrSlug}%`);
|
|
163
167
|
if (byPrefix.length === 1 && byPrefix[0])
|
|
164
168
|
return byPrefix[0].id;
|
|
169
|
+
const bySlugPrefix = db.query("SELECT id FROM prompts WHERE slug LIKE ? LIMIT 2").all(`${idOrSlug}%`);
|
|
170
|
+
if (bySlugPrefix.length === 1 && bySlugPrefix[0])
|
|
171
|
+
return bySlugPrefix[0].id;
|
|
172
|
+
const bySlugSub = db.query("SELECT id FROM prompts WHERE slug LIKE ? LIMIT 2").all(`%${idOrSlug}%`);
|
|
173
|
+
if (bySlugSub.length === 1 && bySlugSub[0])
|
|
174
|
+
return bySlugSub[0].id;
|
|
175
|
+
const byTitle = db.query("SELECT id FROM prompts WHERE lower(title) LIKE ? LIMIT 2").all(`%${idOrSlug.toLowerCase()}%`);
|
|
176
|
+
if (byTitle.length === 1 && byTitle[0])
|
|
177
|
+
return byTitle[0].id;
|
|
165
178
|
return null;
|
|
166
179
|
}
|
|
167
180
|
|
|
@@ -179,25 +192,24 @@ function uniqueSlug(baseSlug) {
|
|
|
179
192
|
}
|
|
180
193
|
return slug;
|
|
181
194
|
}
|
|
195
|
+
var CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
196
|
+
function nanoid(len) {
|
|
197
|
+
let id = "";
|
|
198
|
+
for (let i = 0;i < len; i++) {
|
|
199
|
+
id += CHARS[Math.floor(Math.random() * CHARS.length)];
|
|
200
|
+
}
|
|
201
|
+
return id;
|
|
202
|
+
}
|
|
182
203
|
function generatePromptId() {
|
|
183
204
|
const db = getDatabase();
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
next = parseInt(match[1], 10) + 1;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return `PRMT-${String(next).padStart(5, "0")}`;
|
|
205
|
+
let id;
|
|
206
|
+
do {
|
|
207
|
+
id = `prmt-${nanoid(8)}`;
|
|
208
|
+
} while (db.query("SELECT 1 FROM prompts WHERE id = ?").get(id));
|
|
209
|
+
return id;
|
|
193
210
|
}
|
|
194
211
|
function generateId(prefix) {
|
|
195
|
-
|
|
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;
|
|
212
|
+
return `${prefix}-${nanoid(8)}`;
|
|
201
213
|
}
|
|
202
214
|
|
|
203
215
|
// src/db/collections.ts
|
|
@@ -255,6 +267,36 @@ function movePrompt(promptIdOrSlug, targetCollection) {
|
|
|
255
267
|
]);
|
|
256
268
|
}
|
|
257
269
|
|
|
270
|
+
// src/lib/duplicates.ts
|
|
271
|
+
function tokenize(text) {
|
|
272
|
+
return new Set(text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2));
|
|
273
|
+
}
|
|
274
|
+
function similarity(a, b) {
|
|
275
|
+
const ta = tokenize(a);
|
|
276
|
+
const tb = tokenize(b);
|
|
277
|
+
if (ta.size === 0 || tb.size === 0)
|
|
278
|
+
return 0;
|
|
279
|
+
let shared = 0;
|
|
280
|
+
for (const word of ta) {
|
|
281
|
+
if (tb.has(word))
|
|
282
|
+
shared++;
|
|
283
|
+
}
|
|
284
|
+
return shared / Math.max(ta.size, tb.size);
|
|
285
|
+
}
|
|
286
|
+
function findDuplicates(body, threshold = 0.8, excludeSlug) {
|
|
287
|
+
const all = listPrompts({ limit: 1e4 });
|
|
288
|
+
const matches = [];
|
|
289
|
+
for (const p of all) {
|
|
290
|
+
if (excludeSlug && p.slug === excludeSlug)
|
|
291
|
+
continue;
|
|
292
|
+
const score = similarity(body, p.body);
|
|
293
|
+
if (score >= threshold) {
|
|
294
|
+
matches.push({ prompt: p, score });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return matches.sort((a, b) => b.score - a.score);
|
|
298
|
+
}
|
|
299
|
+
|
|
258
300
|
// src/lib/template.ts
|
|
259
301
|
var VAR_PATTERN = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\|\s*(.*?)\s*)?\}\}/g;
|
|
260
302
|
function extractVariables(body) {
|
|
@@ -348,6 +390,7 @@ function rowToPrompt(row) {
|
|
|
348
390
|
collection: row["collection"],
|
|
349
391
|
tags: JSON.parse(row["tags"] || "[]"),
|
|
350
392
|
variables: JSON.parse(row["variables"] || "[]"),
|
|
393
|
+
pinned: Boolean(row["pinned"]),
|
|
351
394
|
is_template: Boolean(row["is_template"]),
|
|
352
395
|
source: row["source"],
|
|
353
396
|
version: row["version"],
|
|
@@ -422,7 +465,7 @@ function listPrompts(filter = {}) {
|
|
|
422
465
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
423
466
|
const limit = filter.limit ?? 100;
|
|
424
467
|
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);
|
|
468
|
+
const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY pinned DESC, use_count DESC, updated_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
426
469
|
return rows.map(rowToPrompt);
|
|
427
470
|
}
|
|
428
471
|
function updatePrompt(idOrSlug, input) {
|
|
@@ -472,7 +515,13 @@ function usePrompt(idOrSlug) {
|
|
|
472
515
|
db.run("UPDATE prompts SET use_count = use_count + 1, last_used_at = datetime('now') WHERE id = ?", [prompt.id]);
|
|
473
516
|
return requirePrompt(prompt.id);
|
|
474
517
|
}
|
|
475
|
-
function
|
|
518
|
+
function pinPrompt(idOrSlug, pinned) {
|
|
519
|
+
const db = getDatabase();
|
|
520
|
+
const prompt = requirePrompt(idOrSlug);
|
|
521
|
+
db.run("UPDATE prompts SET pinned = ?, updated_at = datetime('now') WHERE id = ?", [pinned ? 1 : 0, prompt.id]);
|
|
522
|
+
return requirePrompt(prompt.id);
|
|
523
|
+
}
|
|
524
|
+
function upsertPrompt(input, force = false) {
|
|
476
525
|
const db = getDatabase();
|
|
477
526
|
const slug = input.slug || generateSlug(input.title);
|
|
478
527
|
const existing = db.query("SELECT id FROM prompts WHERE slug = ?").get(slug);
|
|
@@ -487,8 +536,16 @@ function upsertPrompt(input) {
|
|
|
487
536
|
});
|
|
488
537
|
return { prompt: prompt2, created: false };
|
|
489
538
|
}
|
|
539
|
+
let duplicate_warning;
|
|
540
|
+
if (!force && input.body) {
|
|
541
|
+
const dupes = findDuplicates(input.body, 0.8, slug);
|
|
542
|
+
if (dupes.length > 0) {
|
|
543
|
+
const top = dupes[0];
|
|
544
|
+
duplicate_warning = `Similar prompt already exists: "${top.prompt.slug}" (${Math.round(top.score * 100)}% match). Use --force to save anyway.`;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
490
547
|
const prompt = createPrompt({ ...input, slug });
|
|
491
|
-
return { prompt, created: true };
|
|
548
|
+
return { prompt, created: true, duplicate_warning };
|
|
492
549
|
}
|
|
493
550
|
function getPromptStats() {
|
|
494
551
|
const db = getDatabase();
|
|
@@ -581,6 +638,7 @@ function rowToSearchResult(row, snippet) {
|
|
|
581
638
|
collection: row["collection"],
|
|
582
639
|
tags: JSON.parse(row["tags"] || "[]"),
|
|
583
640
|
variables: JSON.parse(row["variables"] || "[]"),
|
|
641
|
+
pinned: Boolean(row["pinned"]),
|
|
584
642
|
is_template: Boolean(row["is_template"]),
|
|
585
643
|
source: row["source"],
|
|
586
644
|
version: row["version"],
|
|
@@ -709,6 +767,7 @@ export {
|
|
|
709
767
|
requirePrompt,
|
|
710
768
|
renderTemplate,
|
|
711
769
|
registerAgent,
|
|
770
|
+
pinPrompt,
|
|
712
771
|
movePrompt,
|
|
713
772
|
listVersions,
|
|
714
773
|
listPrompts,
|
|
@@ -724,6 +783,7 @@ export {
|
|
|
724
783
|
generateSlug,
|
|
725
784
|
generatePromptId,
|
|
726
785
|
findSimilar,
|
|
786
|
+
findDuplicates,
|
|
727
787
|
extractVariables,
|
|
728
788
|
extractVariableInfo,
|
|
729
789
|
exportToJson,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Prompt } from "../types/index.js";
|
|
2
|
+
export interface DuplicateMatch {
|
|
3
|
+
prompt: Prompt;
|
|
4
|
+
score: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function findDuplicates(body: string, threshold?: number, excludeSlug?: string): DuplicateMatch[];
|
|
7
|
+
//# sourceMappingURL=duplicates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duplicates.d.ts","sourceRoot":"","sources":["../../src/lib/duplicates.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAuB/C,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;CACd;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,CAapG"}
|
package/dist/lib/ids.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ids.d.ts","sourceRoot":"","sources":["../../src/lib/ids.ts"],"names":[],"mappings":"AAEA,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQlD;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CASnD;
|
|
1
|
+
{"version":3,"file":"ids.d.ts","sourceRoot":"","sources":["../../src/lib/ids.ts"],"names":[],"mappings":"AAEA,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQlD;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CASnD;AAYD,wBAAgB,gBAAgB,IAAI,MAAM,CAOzC;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEjD"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Prompt } from "../types/index.js";
|
|
2
|
+
export type LintSeverity = "error" | "warn" | "info";
|
|
3
|
+
export interface LintIssue {
|
|
4
|
+
prompt_id: string;
|
|
5
|
+
slug: string;
|
|
6
|
+
severity: LintSeverity;
|
|
7
|
+
rule: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
export interface LintResult {
|
|
11
|
+
prompt: Prompt;
|
|
12
|
+
issues: LintIssue[];
|
|
13
|
+
}
|
|
14
|
+
export declare function lintPrompt(p: Prompt): LintIssue[];
|
|
15
|
+
export declare function lintAll(prompts: Prompt[]): LintResult[];
|
|
16
|
+
//# sourceMappingURL=lint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../../src/lib/lint.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAE/C,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAA;AAEpD,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,YAAY,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,SAAS,EAAE,CAAA;CACpB;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,EAAE,CA2CjD;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,UAAU,EAAE,CAIvD"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional open-mementos integration.
|
|
3
|
+
* Saves a memory when a prompt is used/rendered, if PROMPTS_SAVE_MEMENTOS=1
|
|
4
|
+
* and @hasna/mementos is installed. Gracefully no-ops if not available.
|
|
5
|
+
*/
|
|
6
|
+
export interface MementoSaveOptions {
|
|
7
|
+
slug: string;
|
|
8
|
+
body: string;
|
|
9
|
+
rendered?: string;
|
|
10
|
+
agentId?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function maybeSaveMemento(opts: MementoSaveOptions): Promise<void>;
|
|
13
|
+
//# sourceMappingURL=mementos.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mementos.d.ts","sourceRoot":"","sources":["../../src/lib/mementos.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B9E"}
|
package/dist/lib/search.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/lib/search.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/lib/search.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAuCxE,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,IAAI,CAAC,iBAAiB,EAAE,GAAG,CAAM,GACxC,YAAY,EAAE,CAoEhB;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,YAAY,EAAE,CAmCvE"}
|
package/dist/mcp/index.js
CHANGED
|
@@ -10,6 +10,7 @@ var __export = (target, all) => {
|
|
|
10
10
|
set: (newValue) => all[name] = () => newValue
|
|
11
11
|
});
|
|
12
12
|
};
|
|
13
|
+
var __require = import.meta.require;
|
|
13
14
|
|
|
14
15
|
// src/mcp/index.ts
|
|
15
16
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -4098,6 +4099,10 @@ function runMigrations(db) {
|
|
|
4098
4099
|
CREATE INDEX IF NOT EXISTS idx_versions_prompt_id ON prompt_versions(prompt_id);
|
|
4099
4100
|
`
|
|
4100
4101
|
},
|
|
4102
|
+
{
|
|
4103
|
+
name: "003_pinned",
|
|
4104
|
+
sql: `ALTER TABLE prompts ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;`
|
|
4105
|
+
},
|
|
4101
4106
|
{
|
|
4102
4107
|
name: "002_fts5",
|
|
4103
4108
|
sql: `
|
|
@@ -4151,6 +4156,15 @@ function resolvePrompt(db, idOrSlug) {
|
|
|
4151
4156
|
const byPrefix = db.query("SELECT id FROM prompts WHERE id LIKE ? LIMIT 2").all(`${idOrSlug}%`);
|
|
4152
4157
|
if (byPrefix.length === 1 && byPrefix[0])
|
|
4153
4158
|
return byPrefix[0].id;
|
|
4159
|
+
const bySlugPrefix = db.query("SELECT id FROM prompts WHERE slug LIKE ? LIMIT 2").all(`${idOrSlug}%`);
|
|
4160
|
+
if (bySlugPrefix.length === 1 && bySlugPrefix[0])
|
|
4161
|
+
return bySlugPrefix[0].id;
|
|
4162
|
+
const bySlugSub = db.query("SELECT id FROM prompts WHERE slug LIKE ? LIMIT 2").all(`%${idOrSlug}%`);
|
|
4163
|
+
if (bySlugSub.length === 1 && bySlugSub[0])
|
|
4164
|
+
return bySlugSub[0].id;
|
|
4165
|
+
const byTitle = db.query("SELECT id FROM prompts WHERE lower(title) LIKE ? LIMIT 2").all(`%${idOrSlug.toLowerCase()}%`);
|
|
4166
|
+
if (byTitle.length === 1 && byTitle[0])
|
|
4167
|
+
return byTitle[0].id;
|
|
4154
4168
|
return null;
|
|
4155
4169
|
}
|
|
4156
4170
|
|
|
@@ -4168,25 +4182,24 @@ function uniqueSlug(baseSlug) {
|
|
|
4168
4182
|
}
|
|
4169
4183
|
return slug;
|
|
4170
4184
|
}
|
|
4185
|
+
var CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
4186
|
+
function nanoid(len) {
|
|
4187
|
+
let id = "";
|
|
4188
|
+
for (let i = 0;i < len; i++) {
|
|
4189
|
+
id += CHARS[Math.floor(Math.random() * CHARS.length)];
|
|
4190
|
+
}
|
|
4191
|
+
return id;
|
|
4192
|
+
}
|
|
4171
4193
|
function generatePromptId() {
|
|
4172
4194
|
const db = getDatabase();
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
next = parseInt(match[1], 10) + 1;
|
|
4179
|
-
}
|
|
4180
|
-
}
|
|
4181
|
-
return `PRMT-${String(next).padStart(5, "0")}`;
|
|
4195
|
+
let id;
|
|
4196
|
+
do {
|
|
4197
|
+
id = `prmt-${nanoid(8)}`;
|
|
4198
|
+
} while (db.query("SELECT 1 FROM prompts WHERE id = ?").get(id));
|
|
4199
|
+
return id;
|
|
4182
4200
|
}
|
|
4183
4201
|
function generateId(prefix) {
|
|
4184
|
-
|
|
4185
|
-
let id = prefix + "-";
|
|
4186
|
-
for (let i = 0;i < 8; i++) {
|
|
4187
|
-
id += chars[Math.floor(Math.random() * chars.length)];
|
|
4188
|
-
}
|
|
4189
|
-
return id;
|
|
4202
|
+
return `${prefix}-${nanoid(8)}`;
|
|
4190
4203
|
}
|
|
4191
4204
|
|
|
4192
4205
|
// src/db/collections.ts
|
|
@@ -4244,6 +4257,36 @@ function movePrompt(promptIdOrSlug, targetCollection) {
|
|
|
4244
4257
|
]);
|
|
4245
4258
|
}
|
|
4246
4259
|
|
|
4260
|
+
// src/lib/duplicates.ts
|
|
4261
|
+
function tokenize(text) {
|
|
4262
|
+
return new Set(text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2));
|
|
4263
|
+
}
|
|
4264
|
+
function similarity(a, b) {
|
|
4265
|
+
const ta = tokenize(a);
|
|
4266
|
+
const tb = tokenize(b);
|
|
4267
|
+
if (ta.size === 0 || tb.size === 0)
|
|
4268
|
+
return 0;
|
|
4269
|
+
let shared = 0;
|
|
4270
|
+
for (const word of ta) {
|
|
4271
|
+
if (tb.has(word))
|
|
4272
|
+
shared++;
|
|
4273
|
+
}
|
|
4274
|
+
return shared / Math.max(ta.size, tb.size);
|
|
4275
|
+
}
|
|
4276
|
+
function findDuplicates(body, threshold = 0.8, excludeSlug) {
|
|
4277
|
+
const all = listPrompts({ limit: 1e4 });
|
|
4278
|
+
const matches = [];
|
|
4279
|
+
for (const p of all) {
|
|
4280
|
+
if (excludeSlug && p.slug === excludeSlug)
|
|
4281
|
+
continue;
|
|
4282
|
+
const score = similarity(body, p.body);
|
|
4283
|
+
if (score >= threshold) {
|
|
4284
|
+
matches.push({ prompt: p, score });
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
return matches.sort((a, b) => b.score - a.score);
|
|
4288
|
+
}
|
|
4289
|
+
|
|
4247
4290
|
// src/lib/template.ts
|
|
4248
4291
|
var VAR_PATTERN = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\|\s*(.*?)\s*)?\}\}/g;
|
|
4249
4292
|
function extractVariables(body) {
|
|
@@ -4330,6 +4373,7 @@ function rowToPrompt(row) {
|
|
|
4330
4373
|
collection: row["collection"],
|
|
4331
4374
|
tags: JSON.parse(row["tags"] || "[]"),
|
|
4332
4375
|
variables: JSON.parse(row["variables"] || "[]"),
|
|
4376
|
+
pinned: Boolean(row["pinned"]),
|
|
4333
4377
|
is_template: Boolean(row["is_template"]),
|
|
4334
4378
|
source: row["source"],
|
|
4335
4379
|
version: row["version"],
|
|
@@ -4404,7 +4448,7 @@ function listPrompts(filter = {}) {
|
|
|
4404
4448
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
4405
4449
|
const limit = filter.limit ?? 100;
|
|
4406
4450
|
const offset = filter.offset ?? 0;
|
|
4407
|
-
const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY use_count DESC, updated_at DESC LIMIT ? OFFSET ?`).all(
|
|
4451
|
+
const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY pinned DESC, use_count DESC, updated_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
4408
4452
|
return rows.map(rowToPrompt);
|
|
4409
4453
|
}
|
|
4410
4454
|
function updatePrompt(idOrSlug, input) {
|
|
@@ -4454,7 +4498,13 @@ function usePrompt(idOrSlug) {
|
|
|
4454
4498
|
db.run("UPDATE prompts SET use_count = use_count + 1, last_used_at = datetime('now') WHERE id = ?", [prompt.id]);
|
|
4455
4499
|
return requirePrompt(prompt.id);
|
|
4456
4500
|
}
|
|
4457
|
-
function
|
|
4501
|
+
function pinPrompt(idOrSlug, pinned) {
|
|
4502
|
+
const db = getDatabase();
|
|
4503
|
+
const prompt = requirePrompt(idOrSlug);
|
|
4504
|
+
db.run("UPDATE prompts SET pinned = ?, updated_at = datetime('now') WHERE id = ?", [pinned ? 1 : 0, prompt.id]);
|
|
4505
|
+
return requirePrompt(prompt.id);
|
|
4506
|
+
}
|
|
4507
|
+
function upsertPrompt(input, force = false) {
|
|
4458
4508
|
const db = getDatabase();
|
|
4459
4509
|
const slug = input.slug || generateSlug(input.title);
|
|
4460
4510
|
const existing = db.query("SELECT id FROM prompts WHERE slug = ?").get(slug);
|
|
@@ -4469,8 +4519,16 @@ function upsertPrompt(input) {
|
|
|
4469
4519
|
});
|
|
4470
4520
|
return { prompt: prompt2, created: false };
|
|
4471
4521
|
}
|
|
4522
|
+
let duplicate_warning;
|
|
4523
|
+
if (!force && input.body) {
|
|
4524
|
+
const dupes = findDuplicates(input.body, 0.8, slug);
|
|
4525
|
+
if (dupes.length > 0) {
|
|
4526
|
+
const top = dupes[0];
|
|
4527
|
+
duplicate_warning = `Similar prompt already exists: "${top.prompt.slug}" (${Math.round(top.score * 100)}% match). Use --force to save anyway.`;
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4472
4530
|
const prompt = createPrompt({ ...input, slug });
|
|
4473
|
-
return { prompt, created: true };
|
|
4531
|
+
return { prompt, created: true, duplicate_warning };
|
|
4474
4532
|
}
|
|
4475
4533
|
function getPromptStats() {
|
|
4476
4534
|
const db = getDatabase();
|
|
@@ -4561,6 +4619,7 @@ function rowToSearchResult(row, snippet) {
|
|
|
4561
4619
|
collection: row["collection"],
|
|
4562
4620
|
tags: JSON.parse(row["tags"] || "[]"),
|
|
4563
4621
|
variables: JSON.parse(row["variables"] || "[]"),
|
|
4622
|
+
pinned: Boolean(row["pinned"]),
|
|
4564
4623
|
is_template: Boolean(row["is_template"]),
|
|
4565
4624
|
source: row["source"],
|
|
4566
4625
|
version: row["version"],
|
|
@@ -4615,7 +4674,7 @@ function searchPrompts(query, filter = {}) {
|
|
|
4615
4674
|
WHERE prompts_fts MATCH ?
|
|
4616
4675
|
${where}
|
|
4617
4676
|
ORDER BY bm25(prompts_fts)
|
|
4618
|
-
LIMIT ? OFFSET ?`).all(
|
|
4677
|
+
LIMIT ? OFFSET ?`).all(ftsQuery, ...params, limit, offset);
|
|
4619
4678
|
return rows2.map((r) => rowToSearchResult(r, r["snippet"]));
|
|
4620
4679
|
} catch {}
|
|
4621
4680
|
}
|
|
@@ -4623,7 +4682,7 @@ function searchPrompts(query, filter = {}) {
|
|
|
4623
4682
|
const rows = db.query(`SELECT *, 1 as score FROM prompts
|
|
4624
4683
|
WHERE (name LIKE ? OR slug LIKE ? OR title LIKE ? OR body LIKE ? OR description LIKE ? OR tags LIKE ?)
|
|
4625
4684
|
ORDER BY use_count DESC, updated_at DESC
|
|
4626
|
-
LIMIT ? OFFSET ?`).all(
|
|
4685
|
+
LIMIT ? OFFSET ?`).all(like, like, like, like, like, like, filter.limit ?? 50, filter.offset ?? 0);
|
|
4627
4686
|
return rows.map((r) => rowToSearchResult(r));
|
|
4628
4687
|
}
|
|
4629
4688
|
function findSimilar(promptId, limit = 5) {
|
|
@@ -4634,10 +4693,10 @@ function findSimilar(promptId, limit = 5) {
|
|
|
4634
4693
|
const tags = JSON.parse(prompt["tags"] || "[]");
|
|
4635
4694
|
const collection = prompt["collection"];
|
|
4636
4695
|
if (tags.length === 0) {
|
|
4637
|
-
const rows = db.query("SELECT *, 1 as score FROM prompts WHERE collection = ? AND id != ? ORDER BY use_count DESC LIMIT ?").all(
|
|
4696
|
+
const rows = db.query("SELECT *, 1 as score FROM prompts WHERE collection = ? AND id != ? ORDER BY use_count DESC LIMIT ?").all(collection, promptId, limit);
|
|
4638
4697
|
return rows.map((r) => rowToSearchResult(r));
|
|
4639
4698
|
}
|
|
4640
|
-
const allRows = db.query("SELECT * FROM prompts WHERE id != ?").all(
|
|
4699
|
+
const allRows = db.query("SELECT * FROM prompts WHERE id != ?").all(promptId);
|
|
4641
4700
|
const scored = allRows.map((row) => {
|
|
4642
4701
|
const rowTags = JSON.parse(row["tags"] || "[]");
|
|
4643
4702
|
const overlap = rowTags.filter((t) => tags.includes(t)).length;
|
|
@@ -4680,6 +4739,64 @@ function exportToJson(collection) {
|
|
|
4680
4739
|
return { prompts, exported_at: new Date().toISOString(), collection };
|
|
4681
4740
|
}
|
|
4682
4741
|
|
|
4742
|
+
// src/lib/mementos.ts
|
|
4743
|
+
async function maybeSaveMemento(opts) {
|
|
4744
|
+
if (process.env["PROMPTS_SAVE_MEMENTOS"] !== "1")
|
|
4745
|
+
return;
|
|
4746
|
+
try {
|
|
4747
|
+
const mod = await import("@hasna/mementos").catch(() => null);
|
|
4748
|
+
if (!mod)
|
|
4749
|
+
return;
|
|
4750
|
+
const key = `prompts/used/${opts.slug}`;
|
|
4751
|
+
const value = opts.rendered ?? opts.body;
|
|
4752
|
+
const save = mod["createMemory"] ?? mod["saveMemory"];
|
|
4753
|
+
if (typeof save !== "function")
|
|
4754
|
+
return;
|
|
4755
|
+
await save({
|
|
4756
|
+
key,
|
|
4757
|
+
value,
|
|
4758
|
+
scope: "private",
|
|
4759
|
+
agent_id: opts.agentId,
|
|
4760
|
+
tags: ["prompts", opts.slug],
|
|
4761
|
+
summary: `Used prompt: ${opts.slug}`
|
|
4762
|
+
});
|
|
4763
|
+
} catch {}
|
|
4764
|
+
}
|
|
4765
|
+
|
|
4766
|
+
// src/lib/lint.ts
|
|
4767
|
+
function lintPrompt(p) {
|
|
4768
|
+
const issues = [];
|
|
4769
|
+
const issue = (severity, rule, message) => ({
|
|
4770
|
+
prompt_id: p.id,
|
|
4771
|
+
slug: p.slug,
|
|
4772
|
+
severity,
|
|
4773
|
+
rule,
|
|
4774
|
+
message
|
|
4775
|
+
});
|
|
4776
|
+
if (!p.description) {
|
|
4777
|
+
issues.push(issue("warn", "missing-description", "No description provided"));
|
|
4778
|
+
}
|
|
4779
|
+
if (p.body.trim().length < 10) {
|
|
4780
|
+
issues.push(issue("error", "body-too-short", `Body is only ${p.body.trim().length} characters`));
|
|
4781
|
+
}
|
|
4782
|
+
if (p.tags.length === 0) {
|
|
4783
|
+
issues.push(issue("info", "no-tags", "No tags \u2014 prompt will be harder to discover"));
|
|
4784
|
+
}
|
|
4785
|
+
if (p.is_template) {
|
|
4786
|
+
const undocumented = p.variables.filter((v) => !v.description || v.description.trim() === "");
|
|
4787
|
+
if (undocumented.length > 0) {
|
|
4788
|
+
issues.push(issue("warn", "undocumented-vars", `Template variables without description: ${undocumented.map((v) => v.name).join(", ")}`));
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
4791
|
+
if (p.collection === "default" && p.use_count === 0) {
|
|
4792
|
+
issues.push(issue("info", "uncollected", "In default collection and never used \u2014 consider organizing"));
|
|
4793
|
+
}
|
|
4794
|
+
return issues;
|
|
4795
|
+
}
|
|
4796
|
+
function lintAll(prompts) {
|
|
4797
|
+
return prompts.map((p) => ({ prompt: p, issues: lintPrompt(p) })).filter((r) => r.issues.length > 0);
|
|
4798
|
+
}
|
|
4799
|
+
|
|
4683
4800
|
// src/mcp/index.ts
|
|
4684
4801
|
var server = new McpServer({ name: "open-prompts", version: "0.1.0" });
|
|
4685
4802
|
function ok(data) {
|
|
@@ -4698,12 +4815,14 @@ server.registerTool("prompts_save", {
|
|
|
4698
4815
|
collection: exports_external.string().optional().describe("Collection/namespace (default: 'default')"),
|
|
4699
4816
|
tags: exports_external.array(exports_external.string()).optional().describe("Tags for filtering and search"),
|
|
4700
4817
|
source: exports_external.enum(["manual", "ai-session", "imported"]).optional().describe("Where this prompt came from"),
|
|
4701
|
-
changed_by: exports_external.string().optional().describe("Agent name making this change")
|
|
4818
|
+
changed_by: exports_external.string().optional().describe("Agent name making this change"),
|
|
4819
|
+
force: exports_external.boolean().optional().describe("Save even if a similar prompt already exists")
|
|
4702
4820
|
}
|
|
4703
4821
|
}, async (args) => {
|
|
4704
4822
|
try {
|
|
4705
|
-
const {
|
|
4706
|
-
|
|
4823
|
+
const { force, ...input } = args;
|
|
4824
|
+
const { prompt, created, duplicate_warning } = upsertPrompt(input, force ?? false);
|
|
4825
|
+
return ok({ ...prompt, _created: created, _duplicate_warning: duplicate_warning ?? null });
|
|
4707
4826
|
} catch (e) {
|
|
4708
4827
|
return err(e instanceof Error ? e.message : String(e));
|
|
4709
4828
|
}
|
|
@@ -4741,10 +4860,14 @@ server.registerTool("prompts_delete", {
|
|
|
4741
4860
|
});
|
|
4742
4861
|
server.registerTool("prompts_use", {
|
|
4743
4862
|
description: "Get a prompt's body and increment its use counter. This is the primary way to retrieve a prompt for actual use.",
|
|
4744
|
-
inputSchema: {
|
|
4745
|
-
|
|
4863
|
+
inputSchema: {
|
|
4864
|
+
id: exports_external.string().describe("Prompt ID or slug"),
|
|
4865
|
+
agent: exports_external.string().optional().describe("Agent ID for mementos integration")
|
|
4866
|
+
}
|
|
4867
|
+
}, async ({ id, agent }) => {
|
|
4746
4868
|
try {
|
|
4747
4869
|
const prompt = usePrompt(id);
|
|
4870
|
+
await maybeSaveMemento({ slug: prompt.slug, body: prompt.body, agentId: agent });
|
|
4748
4871
|
return ok({ body: prompt.body, prompt });
|
|
4749
4872
|
} catch (e) {
|
|
4750
4873
|
return err(e instanceof Error ? e.message : String(e));
|
|
@@ -4754,12 +4877,14 @@ server.registerTool("prompts_render", {
|
|
|
4754
4877
|
description: "Render a template prompt by filling in {{variables}}. Returns rendered body plus info on missing/defaulted vars.",
|
|
4755
4878
|
inputSchema: {
|
|
4756
4879
|
id: exports_external.string().describe("Prompt ID or slug"),
|
|
4757
|
-
vars: exports_external.record(exports_external.string()).describe("Variable values as key-value pairs")
|
|
4880
|
+
vars: exports_external.record(exports_external.string()).describe("Variable values as key-value pairs"),
|
|
4881
|
+
agent: exports_external.string().optional().describe("Agent ID for mementos integration")
|
|
4758
4882
|
}
|
|
4759
|
-
}, async ({ id, vars }) => {
|
|
4883
|
+
}, async ({ id, vars, agent }) => {
|
|
4760
4884
|
try {
|
|
4761
4885
|
const prompt = usePrompt(id);
|
|
4762
4886
|
const result = renderTemplate(prompt.body, vars);
|
|
4887
|
+
await maybeSaveMemento({ slug: prompt.slug, body: prompt.body, rendered: result.rendered, agentId: agent });
|
|
4763
4888
|
return ok(result);
|
|
4764
4889
|
} catch (e) {
|
|
4765
4890
|
return err(e instanceof Error ? e.message : String(e));
|
|
@@ -4922,6 +5047,86 @@ server.registerTool("prompts_ensure_collection", {
|
|
|
4922
5047
|
description: exports_external.string().optional()
|
|
4923
5048
|
}
|
|
4924
5049
|
}, async ({ name, description }) => ok(ensureCollection(name, description)));
|
|
5050
|
+
server.registerTool("prompts_save_from_session", {
|
|
5051
|
+
description: "Minimal frictionless save for AI agents mid-conversation. The agent is expected to derive title, slug, and tags from the body before calling this. Automatically sets source=ai-session. Perfect for 'save this as a reusable prompt' moments.",
|
|
5052
|
+
inputSchema: {
|
|
5053
|
+
title: exports_external.string().describe("A short descriptive title for this prompt"),
|
|
5054
|
+
body: exports_external.string().describe("The prompt content to save"),
|
|
5055
|
+
slug: exports_external.string().optional().describe("URL-friendly identifier (auto-generated from title if omitted)"),
|
|
5056
|
+
tags: exports_external.array(exports_external.string()).optional().describe("Relevant tags extracted from the prompt context"),
|
|
5057
|
+
collection: exports_external.string().optional().describe("Collection to save into (default: 'sessions')"),
|
|
5058
|
+
description: exports_external.string().optional().describe("One-line description of what this prompt does"),
|
|
5059
|
+
agent: exports_external.string().optional().describe("Agent name saving this prompt")
|
|
5060
|
+
}
|
|
5061
|
+
}, async ({ title, body, slug, tags, collection, description, agent }) => {
|
|
5062
|
+
try {
|
|
5063
|
+
const { prompt, created } = upsertPrompt({
|
|
5064
|
+
title,
|
|
5065
|
+
body,
|
|
5066
|
+
slug,
|
|
5067
|
+
tags,
|
|
5068
|
+
collection: collection ?? "sessions",
|
|
5069
|
+
description,
|
|
5070
|
+
source: "ai-session",
|
|
5071
|
+
changed_by: agent
|
|
5072
|
+
});
|
|
5073
|
+
return ok({ ...prompt, _created: created, _tip: created ? `Saved as "${prompt.slug}". Use prompts_use("${prompt.slug}") to retrieve it.` : `Updated existing prompt "${prompt.slug}".` });
|
|
5074
|
+
} catch (e) {
|
|
5075
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
5076
|
+
}
|
|
5077
|
+
});
|
|
5078
|
+
server.registerTool("prompts_pin", {
|
|
5079
|
+
description: "Pin a prompt so it always appears first in lists.",
|
|
5080
|
+
inputSchema: { id: exports_external.string() }
|
|
5081
|
+
}, async ({ id }) => {
|
|
5082
|
+
try {
|
|
5083
|
+
return ok(pinPrompt(id, true));
|
|
5084
|
+
} catch (e) {
|
|
5085
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
5086
|
+
}
|
|
5087
|
+
});
|
|
5088
|
+
server.registerTool("prompts_unpin", {
|
|
5089
|
+
description: "Unpin a previously pinned prompt.",
|
|
5090
|
+
inputSchema: { id: exports_external.string() }
|
|
5091
|
+
}, async ({ id }) => {
|
|
5092
|
+
try {
|
|
5093
|
+
return ok(pinPrompt(id, false));
|
|
5094
|
+
} catch (e) {
|
|
5095
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
5096
|
+
}
|
|
5097
|
+
});
|
|
5098
|
+
server.registerTool("prompts_recent", {
|
|
5099
|
+
description: "Get recently used prompts, ordered by last_used_at descending.",
|
|
5100
|
+
inputSchema: { limit: exports_external.number().optional().default(10) }
|
|
5101
|
+
}, async ({ limit }) => {
|
|
5102
|
+
const prompts = listPrompts({ limit: 500 }).filter((p) => p.last_used_at !== null).sort((a, b) => (b.last_used_at ?? "").localeCompare(a.last_used_at ?? "")).slice(0, limit);
|
|
5103
|
+
return ok(prompts);
|
|
5104
|
+
});
|
|
5105
|
+
server.registerTool("prompts_lint", {
|
|
5106
|
+
description: "Check prompt quality: missing descriptions, undocumented template vars, short bodies, no tags.",
|
|
5107
|
+
inputSchema: { collection: exports_external.string().optional() }
|
|
5108
|
+
}, async ({ collection }) => {
|
|
5109
|
+
const prompts = listPrompts({ collection, limit: 1e4 });
|
|
5110
|
+
const results = lintAll(prompts);
|
|
5111
|
+
const summary = {
|
|
5112
|
+
total_checked: prompts.length,
|
|
5113
|
+
prompts_with_issues: results.length,
|
|
5114
|
+
errors: results.flatMap((r) => r.issues).filter((i) => i.severity === "error").length,
|
|
5115
|
+
warnings: results.flatMap((r) => r.issues).filter((i) => i.severity === "warn").length,
|
|
5116
|
+
info: results.flatMap((r) => r.issues).filter((i) => i.severity === "info").length,
|
|
5117
|
+
results
|
|
5118
|
+
};
|
|
5119
|
+
return ok(summary);
|
|
5120
|
+
});
|
|
5121
|
+
server.registerTool("prompts_stale", {
|
|
5122
|
+
description: "List prompts not used in N days. Useful for library hygiene.",
|
|
5123
|
+
inputSchema: { days: exports_external.number().optional().default(30).describe("Inactivity threshold in days") }
|
|
5124
|
+
}, async ({ days }) => {
|
|
5125
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
5126
|
+
const all = listPrompts({ limit: 1e4 });
|
|
5127
|
+
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 ?? ""));
|
|
5128
|
+
return ok({ stale, count: stale.length, threshold_days: days });
|
|
5129
|
+
});
|
|
4925
5130
|
server.registerTool("prompts_stats", {
|
|
4926
5131
|
description: "Get usage statistics: most used prompts, recently used, counts by collection and source.",
|
|
4927
5132
|
inputSchema: {}
|