@hasna/prompts 0.1.0 → 0.2.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 +31 -31
- package/dist/cli/index.js +258 -20
- 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 +64 -3
- package/dist/lib/duplicates.d.ts +7 -0
- package/dist/lib/duplicates.d.ts.map +1 -0
- 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 +220 -14
- package/dist/server/index.js +60 -7
- 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
|
|
|
@@ -255,6 +268,36 @@ function movePrompt(promptIdOrSlug, targetCollection) {
|
|
|
255
268
|
]);
|
|
256
269
|
}
|
|
257
270
|
|
|
271
|
+
// src/lib/duplicates.ts
|
|
272
|
+
function tokenize(text) {
|
|
273
|
+
return new Set(text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2));
|
|
274
|
+
}
|
|
275
|
+
function similarity(a, b) {
|
|
276
|
+
const ta = tokenize(a);
|
|
277
|
+
const tb = tokenize(b);
|
|
278
|
+
if (ta.size === 0 || tb.size === 0)
|
|
279
|
+
return 0;
|
|
280
|
+
let shared = 0;
|
|
281
|
+
for (const word of ta) {
|
|
282
|
+
if (tb.has(word))
|
|
283
|
+
shared++;
|
|
284
|
+
}
|
|
285
|
+
return shared / Math.max(ta.size, tb.size);
|
|
286
|
+
}
|
|
287
|
+
function findDuplicates(body, threshold = 0.8, excludeSlug) {
|
|
288
|
+
const all = listPrompts({ limit: 1e4 });
|
|
289
|
+
const matches = [];
|
|
290
|
+
for (const p of all) {
|
|
291
|
+
if (excludeSlug && p.slug === excludeSlug)
|
|
292
|
+
continue;
|
|
293
|
+
const score = similarity(body, p.body);
|
|
294
|
+
if (score >= threshold) {
|
|
295
|
+
matches.push({ prompt: p, score });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return matches.sort((a, b) => b.score - a.score);
|
|
299
|
+
}
|
|
300
|
+
|
|
258
301
|
// src/lib/template.ts
|
|
259
302
|
var VAR_PATTERN = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\|\s*(.*?)\s*)?\}\}/g;
|
|
260
303
|
function extractVariables(body) {
|
|
@@ -348,6 +391,7 @@ function rowToPrompt(row) {
|
|
|
348
391
|
collection: row["collection"],
|
|
349
392
|
tags: JSON.parse(row["tags"] || "[]"),
|
|
350
393
|
variables: JSON.parse(row["variables"] || "[]"),
|
|
394
|
+
pinned: Boolean(row["pinned"]),
|
|
351
395
|
is_template: Boolean(row["is_template"]),
|
|
352
396
|
source: row["source"],
|
|
353
397
|
version: row["version"],
|
|
@@ -422,7 +466,7 @@ function listPrompts(filter = {}) {
|
|
|
422
466
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
423
467
|
const limit = filter.limit ?? 100;
|
|
424
468
|
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);
|
|
469
|
+
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
470
|
return rows.map(rowToPrompt);
|
|
427
471
|
}
|
|
428
472
|
function updatePrompt(idOrSlug, input) {
|
|
@@ -472,7 +516,13 @@ function usePrompt(idOrSlug) {
|
|
|
472
516
|
db.run("UPDATE prompts SET use_count = use_count + 1, last_used_at = datetime('now') WHERE id = ?", [prompt.id]);
|
|
473
517
|
return requirePrompt(prompt.id);
|
|
474
518
|
}
|
|
475
|
-
function
|
|
519
|
+
function pinPrompt(idOrSlug, pinned) {
|
|
520
|
+
const db = getDatabase();
|
|
521
|
+
const prompt = requirePrompt(idOrSlug);
|
|
522
|
+
db.run("UPDATE prompts SET pinned = ?, updated_at = datetime('now') WHERE id = ?", [pinned ? 1 : 0, prompt.id]);
|
|
523
|
+
return requirePrompt(prompt.id);
|
|
524
|
+
}
|
|
525
|
+
function upsertPrompt(input, force = false) {
|
|
476
526
|
const db = getDatabase();
|
|
477
527
|
const slug = input.slug || generateSlug(input.title);
|
|
478
528
|
const existing = db.query("SELECT id FROM prompts WHERE slug = ?").get(slug);
|
|
@@ -487,8 +537,16 @@ function upsertPrompt(input) {
|
|
|
487
537
|
});
|
|
488
538
|
return { prompt: prompt2, created: false };
|
|
489
539
|
}
|
|
540
|
+
let duplicate_warning;
|
|
541
|
+
if (!force && input.body) {
|
|
542
|
+
const dupes = findDuplicates(input.body, 0.8, slug);
|
|
543
|
+
if (dupes.length > 0) {
|
|
544
|
+
const top = dupes[0];
|
|
545
|
+
duplicate_warning = `Similar prompt already exists: "${top.prompt.slug}" (${Math.round(top.score * 100)}% match). Use --force to save anyway.`;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
490
548
|
const prompt = createPrompt({ ...input, slug });
|
|
491
|
-
return { prompt, created: true };
|
|
549
|
+
return { prompt, created: true, duplicate_warning };
|
|
492
550
|
}
|
|
493
551
|
function getPromptStats() {
|
|
494
552
|
const db = getDatabase();
|
|
@@ -581,6 +639,7 @@ function rowToSearchResult(row, snippet) {
|
|
|
581
639
|
collection: row["collection"],
|
|
582
640
|
tags: JSON.parse(row["tags"] || "[]"),
|
|
583
641
|
variables: JSON.parse(row["variables"] || "[]"),
|
|
642
|
+
pinned: Boolean(row["pinned"]),
|
|
584
643
|
is_template: Boolean(row["is_template"]),
|
|
585
644
|
source: row["source"],
|
|
586
645
|
version: row["version"],
|
|
@@ -709,6 +768,7 @@ export {
|
|
|
709
768
|
requirePrompt,
|
|
710
769
|
renderTemplate,
|
|
711
770
|
registerAgent,
|
|
771
|
+
pinPrompt,
|
|
712
772
|
movePrompt,
|
|
713
773
|
listVersions,
|
|
714
774
|
listPrompts,
|
|
@@ -724,6 +784,7 @@ export {
|
|
|
724
784
|
generateSlug,
|
|
725
785
|
generatePromptId,
|
|
726
786
|
findSimilar,
|
|
787
|
+
findDuplicates,
|
|
727
788
|
extractVariables,
|
|
728
789
|
extractVariableInfo,
|
|
729
790
|
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"}
|
|
@@ -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
|
|
|
@@ -4244,6 +4258,36 @@ function movePrompt(promptIdOrSlug, targetCollection) {
|
|
|
4244
4258
|
]);
|
|
4245
4259
|
}
|
|
4246
4260
|
|
|
4261
|
+
// src/lib/duplicates.ts
|
|
4262
|
+
function tokenize(text) {
|
|
4263
|
+
return new Set(text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2));
|
|
4264
|
+
}
|
|
4265
|
+
function similarity(a, b) {
|
|
4266
|
+
const ta = tokenize(a);
|
|
4267
|
+
const tb = tokenize(b);
|
|
4268
|
+
if (ta.size === 0 || tb.size === 0)
|
|
4269
|
+
return 0;
|
|
4270
|
+
let shared = 0;
|
|
4271
|
+
for (const word of ta) {
|
|
4272
|
+
if (tb.has(word))
|
|
4273
|
+
shared++;
|
|
4274
|
+
}
|
|
4275
|
+
return shared / Math.max(ta.size, tb.size);
|
|
4276
|
+
}
|
|
4277
|
+
function findDuplicates(body, threshold = 0.8, excludeSlug) {
|
|
4278
|
+
const all = listPrompts({ limit: 1e4 });
|
|
4279
|
+
const matches = [];
|
|
4280
|
+
for (const p of all) {
|
|
4281
|
+
if (excludeSlug && p.slug === excludeSlug)
|
|
4282
|
+
continue;
|
|
4283
|
+
const score = similarity(body, p.body);
|
|
4284
|
+
if (score >= threshold) {
|
|
4285
|
+
matches.push({ prompt: p, score });
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
return matches.sort((a, b) => b.score - a.score);
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4247
4291
|
// src/lib/template.ts
|
|
4248
4292
|
var VAR_PATTERN = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\|\s*(.*?)\s*)?\}\}/g;
|
|
4249
4293
|
function extractVariables(body) {
|
|
@@ -4330,6 +4374,7 @@ function rowToPrompt(row) {
|
|
|
4330
4374
|
collection: row["collection"],
|
|
4331
4375
|
tags: JSON.parse(row["tags"] || "[]"),
|
|
4332
4376
|
variables: JSON.parse(row["variables"] || "[]"),
|
|
4377
|
+
pinned: Boolean(row["pinned"]),
|
|
4333
4378
|
is_template: Boolean(row["is_template"]),
|
|
4334
4379
|
source: row["source"],
|
|
4335
4380
|
version: row["version"],
|
|
@@ -4404,7 +4449,7 @@ function listPrompts(filter = {}) {
|
|
|
4404
4449
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
4405
4450
|
const limit = filter.limit ?? 100;
|
|
4406
4451
|
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(
|
|
4452
|
+
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
4453
|
return rows.map(rowToPrompt);
|
|
4409
4454
|
}
|
|
4410
4455
|
function updatePrompt(idOrSlug, input) {
|
|
@@ -4454,7 +4499,13 @@ function usePrompt(idOrSlug) {
|
|
|
4454
4499
|
db.run("UPDATE prompts SET use_count = use_count + 1, last_used_at = datetime('now') WHERE id = ?", [prompt.id]);
|
|
4455
4500
|
return requirePrompt(prompt.id);
|
|
4456
4501
|
}
|
|
4457
|
-
function
|
|
4502
|
+
function pinPrompt(idOrSlug, pinned) {
|
|
4503
|
+
const db = getDatabase();
|
|
4504
|
+
const prompt = requirePrompt(idOrSlug);
|
|
4505
|
+
db.run("UPDATE prompts SET pinned = ?, updated_at = datetime('now') WHERE id = ?", [pinned ? 1 : 0, prompt.id]);
|
|
4506
|
+
return requirePrompt(prompt.id);
|
|
4507
|
+
}
|
|
4508
|
+
function upsertPrompt(input, force = false) {
|
|
4458
4509
|
const db = getDatabase();
|
|
4459
4510
|
const slug = input.slug || generateSlug(input.title);
|
|
4460
4511
|
const existing = db.query("SELECT id FROM prompts WHERE slug = ?").get(slug);
|
|
@@ -4469,8 +4520,16 @@ function upsertPrompt(input) {
|
|
|
4469
4520
|
});
|
|
4470
4521
|
return { prompt: prompt2, created: false };
|
|
4471
4522
|
}
|
|
4523
|
+
let duplicate_warning;
|
|
4524
|
+
if (!force && input.body) {
|
|
4525
|
+
const dupes = findDuplicates(input.body, 0.8, slug);
|
|
4526
|
+
if (dupes.length > 0) {
|
|
4527
|
+
const top = dupes[0];
|
|
4528
|
+
duplicate_warning = `Similar prompt already exists: "${top.prompt.slug}" (${Math.round(top.score * 100)}% match). Use --force to save anyway.`;
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
4472
4531
|
const prompt = createPrompt({ ...input, slug });
|
|
4473
|
-
return { prompt, created: true };
|
|
4532
|
+
return { prompt, created: true, duplicate_warning };
|
|
4474
4533
|
}
|
|
4475
4534
|
function getPromptStats() {
|
|
4476
4535
|
const db = getDatabase();
|
|
@@ -4561,6 +4620,7 @@ function rowToSearchResult(row, snippet) {
|
|
|
4561
4620
|
collection: row["collection"],
|
|
4562
4621
|
tags: JSON.parse(row["tags"] || "[]"),
|
|
4563
4622
|
variables: JSON.parse(row["variables"] || "[]"),
|
|
4623
|
+
pinned: Boolean(row["pinned"]),
|
|
4564
4624
|
is_template: Boolean(row["is_template"]),
|
|
4565
4625
|
source: row["source"],
|
|
4566
4626
|
version: row["version"],
|
|
@@ -4615,7 +4675,7 @@ function searchPrompts(query, filter = {}) {
|
|
|
4615
4675
|
WHERE prompts_fts MATCH ?
|
|
4616
4676
|
${where}
|
|
4617
4677
|
ORDER BY bm25(prompts_fts)
|
|
4618
|
-
LIMIT ? OFFSET ?`).all(
|
|
4678
|
+
LIMIT ? OFFSET ?`).all(ftsQuery, ...params, limit, offset);
|
|
4619
4679
|
return rows2.map((r) => rowToSearchResult(r, r["snippet"]));
|
|
4620
4680
|
} catch {}
|
|
4621
4681
|
}
|
|
@@ -4623,7 +4683,7 @@ function searchPrompts(query, filter = {}) {
|
|
|
4623
4683
|
const rows = db.query(`SELECT *, 1 as score FROM prompts
|
|
4624
4684
|
WHERE (name LIKE ? OR slug LIKE ? OR title LIKE ? OR body LIKE ? OR description LIKE ? OR tags LIKE ?)
|
|
4625
4685
|
ORDER BY use_count DESC, updated_at DESC
|
|
4626
|
-
LIMIT ? OFFSET ?`).all(
|
|
4686
|
+
LIMIT ? OFFSET ?`).all(like, like, like, like, like, like, filter.limit ?? 50, filter.offset ?? 0);
|
|
4627
4687
|
return rows.map((r) => rowToSearchResult(r));
|
|
4628
4688
|
}
|
|
4629
4689
|
function findSimilar(promptId, limit = 5) {
|
|
@@ -4634,10 +4694,10 @@ function findSimilar(promptId, limit = 5) {
|
|
|
4634
4694
|
const tags = JSON.parse(prompt["tags"] || "[]");
|
|
4635
4695
|
const collection = prompt["collection"];
|
|
4636
4696
|
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(
|
|
4697
|
+
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
4698
|
return rows.map((r) => rowToSearchResult(r));
|
|
4639
4699
|
}
|
|
4640
|
-
const allRows = db.query("SELECT * FROM prompts WHERE id != ?").all(
|
|
4700
|
+
const allRows = db.query("SELECT * FROM prompts WHERE id != ?").all(promptId);
|
|
4641
4701
|
const scored = allRows.map((row) => {
|
|
4642
4702
|
const rowTags = JSON.parse(row["tags"] || "[]");
|
|
4643
4703
|
const overlap = rowTags.filter((t) => tags.includes(t)).length;
|
|
@@ -4680,6 +4740,64 @@ function exportToJson(collection) {
|
|
|
4680
4740
|
return { prompts, exported_at: new Date().toISOString(), collection };
|
|
4681
4741
|
}
|
|
4682
4742
|
|
|
4743
|
+
// src/lib/mementos.ts
|
|
4744
|
+
async function maybeSaveMemento(opts) {
|
|
4745
|
+
if (process.env["PROMPTS_SAVE_MEMENTOS"] !== "1")
|
|
4746
|
+
return;
|
|
4747
|
+
try {
|
|
4748
|
+
const mod = await import("@hasna/mementos").catch(() => null);
|
|
4749
|
+
if (!mod)
|
|
4750
|
+
return;
|
|
4751
|
+
const key = `prompts/used/${opts.slug}`;
|
|
4752
|
+
const value = opts.rendered ?? opts.body;
|
|
4753
|
+
const save = mod["createMemory"] ?? mod["saveMemory"];
|
|
4754
|
+
if (typeof save !== "function")
|
|
4755
|
+
return;
|
|
4756
|
+
await save({
|
|
4757
|
+
key,
|
|
4758
|
+
value,
|
|
4759
|
+
scope: "private",
|
|
4760
|
+
agent_id: opts.agentId,
|
|
4761
|
+
tags: ["prompts", opts.slug],
|
|
4762
|
+
summary: `Used prompt: ${opts.slug}`
|
|
4763
|
+
});
|
|
4764
|
+
} catch {}
|
|
4765
|
+
}
|
|
4766
|
+
|
|
4767
|
+
// src/lib/lint.ts
|
|
4768
|
+
function lintPrompt(p) {
|
|
4769
|
+
const issues = [];
|
|
4770
|
+
const issue = (severity, rule, message) => ({
|
|
4771
|
+
prompt_id: p.id,
|
|
4772
|
+
slug: p.slug,
|
|
4773
|
+
severity,
|
|
4774
|
+
rule,
|
|
4775
|
+
message
|
|
4776
|
+
});
|
|
4777
|
+
if (!p.description) {
|
|
4778
|
+
issues.push(issue("warn", "missing-description", "No description provided"));
|
|
4779
|
+
}
|
|
4780
|
+
if (p.body.trim().length < 10) {
|
|
4781
|
+
issues.push(issue("error", "body-too-short", `Body is only ${p.body.trim().length} characters`));
|
|
4782
|
+
}
|
|
4783
|
+
if (p.tags.length === 0) {
|
|
4784
|
+
issues.push(issue("info", "no-tags", "No tags \u2014 prompt will be harder to discover"));
|
|
4785
|
+
}
|
|
4786
|
+
if (p.is_template) {
|
|
4787
|
+
const undocumented = p.variables.filter((v) => !v.description || v.description.trim() === "");
|
|
4788
|
+
if (undocumented.length > 0) {
|
|
4789
|
+
issues.push(issue("warn", "undocumented-vars", `Template variables without description: ${undocumented.map((v) => v.name).join(", ")}`));
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
if (p.collection === "default" && p.use_count === 0) {
|
|
4793
|
+
issues.push(issue("info", "uncollected", "In default collection and never used \u2014 consider organizing"));
|
|
4794
|
+
}
|
|
4795
|
+
return issues;
|
|
4796
|
+
}
|
|
4797
|
+
function lintAll(prompts) {
|
|
4798
|
+
return prompts.map((p) => ({ prompt: p, issues: lintPrompt(p) })).filter((r) => r.issues.length > 0);
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4683
4801
|
// src/mcp/index.ts
|
|
4684
4802
|
var server = new McpServer({ name: "open-prompts", version: "0.1.0" });
|
|
4685
4803
|
function ok(data) {
|
|
@@ -4698,12 +4816,14 @@ server.registerTool("prompts_save", {
|
|
|
4698
4816
|
collection: exports_external.string().optional().describe("Collection/namespace (default: 'default')"),
|
|
4699
4817
|
tags: exports_external.array(exports_external.string()).optional().describe("Tags for filtering and search"),
|
|
4700
4818
|
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")
|
|
4819
|
+
changed_by: exports_external.string().optional().describe("Agent name making this change"),
|
|
4820
|
+
force: exports_external.boolean().optional().describe("Save even if a similar prompt already exists")
|
|
4702
4821
|
}
|
|
4703
4822
|
}, async (args) => {
|
|
4704
4823
|
try {
|
|
4705
|
-
const {
|
|
4706
|
-
|
|
4824
|
+
const { force, ...input } = args;
|
|
4825
|
+
const { prompt, created, duplicate_warning } = upsertPrompt(input, force ?? false);
|
|
4826
|
+
return ok({ ...prompt, _created: created, _duplicate_warning: duplicate_warning ?? null });
|
|
4707
4827
|
} catch (e) {
|
|
4708
4828
|
return err(e instanceof Error ? e.message : String(e));
|
|
4709
4829
|
}
|
|
@@ -4741,10 +4861,14 @@ server.registerTool("prompts_delete", {
|
|
|
4741
4861
|
});
|
|
4742
4862
|
server.registerTool("prompts_use", {
|
|
4743
4863
|
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
|
-
|
|
4864
|
+
inputSchema: {
|
|
4865
|
+
id: exports_external.string().describe("Prompt ID or slug"),
|
|
4866
|
+
agent: exports_external.string().optional().describe("Agent ID for mementos integration")
|
|
4867
|
+
}
|
|
4868
|
+
}, async ({ id, agent }) => {
|
|
4746
4869
|
try {
|
|
4747
4870
|
const prompt = usePrompt(id);
|
|
4871
|
+
await maybeSaveMemento({ slug: prompt.slug, body: prompt.body, agentId: agent });
|
|
4748
4872
|
return ok({ body: prompt.body, prompt });
|
|
4749
4873
|
} catch (e) {
|
|
4750
4874
|
return err(e instanceof Error ? e.message : String(e));
|
|
@@ -4754,12 +4878,14 @@ server.registerTool("prompts_render", {
|
|
|
4754
4878
|
description: "Render a template prompt by filling in {{variables}}. Returns rendered body plus info on missing/defaulted vars.",
|
|
4755
4879
|
inputSchema: {
|
|
4756
4880
|
id: exports_external.string().describe("Prompt ID or slug"),
|
|
4757
|
-
vars: exports_external.record(exports_external.string()).describe("Variable values as key-value pairs")
|
|
4881
|
+
vars: exports_external.record(exports_external.string()).describe("Variable values as key-value pairs"),
|
|
4882
|
+
agent: exports_external.string().optional().describe("Agent ID for mementos integration")
|
|
4758
4883
|
}
|
|
4759
|
-
}, async ({ id, vars }) => {
|
|
4884
|
+
}, async ({ id, vars, agent }) => {
|
|
4760
4885
|
try {
|
|
4761
4886
|
const prompt = usePrompt(id);
|
|
4762
4887
|
const result = renderTemplate(prompt.body, vars);
|
|
4888
|
+
await maybeSaveMemento({ slug: prompt.slug, body: prompt.body, rendered: result.rendered, agentId: agent });
|
|
4763
4889
|
return ok(result);
|
|
4764
4890
|
} catch (e) {
|
|
4765
4891
|
return err(e instanceof Error ? e.message : String(e));
|
|
@@ -4922,6 +5048,86 @@ server.registerTool("prompts_ensure_collection", {
|
|
|
4922
5048
|
description: exports_external.string().optional()
|
|
4923
5049
|
}
|
|
4924
5050
|
}, async ({ name, description }) => ok(ensureCollection(name, description)));
|
|
5051
|
+
server.registerTool("prompts_save_from_session", {
|
|
5052
|
+
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.",
|
|
5053
|
+
inputSchema: {
|
|
5054
|
+
title: exports_external.string().describe("A short descriptive title for this prompt"),
|
|
5055
|
+
body: exports_external.string().describe("The prompt content to save"),
|
|
5056
|
+
slug: exports_external.string().optional().describe("URL-friendly identifier (auto-generated from title if omitted)"),
|
|
5057
|
+
tags: exports_external.array(exports_external.string()).optional().describe("Relevant tags extracted from the prompt context"),
|
|
5058
|
+
collection: exports_external.string().optional().describe("Collection to save into (default: 'sessions')"),
|
|
5059
|
+
description: exports_external.string().optional().describe("One-line description of what this prompt does"),
|
|
5060
|
+
agent: exports_external.string().optional().describe("Agent name saving this prompt")
|
|
5061
|
+
}
|
|
5062
|
+
}, async ({ title, body, slug, tags, collection, description, agent }) => {
|
|
5063
|
+
try {
|
|
5064
|
+
const { prompt, created } = upsertPrompt({
|
|
5065
|
+
title,
|
|
5066
|
+
body,
|
|
5067
|
+
slug,
|
|
5068
|
+
tags,
|
|
5069
|
+
collection: collection ?? "sessions",
|
|
5070
|
+
description,
|
|
5071
|
+
source: "ai-session",
|
|
5072
|
+
changed_by: agent
|
|
5073
|
+
});
|
|
5074
|
+
return ok({ ...prompt, _created: created, _tip: created ? `Saved as "${prompt.slug}". Use prompts_use("${prompt.slug}") to retrieve it.` : `Updated existing prompt "${prompt.slug}".` });
|
|
5075
|
+
} catch (e) {
|
|
5076
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
5077
|
+
}
|
|
5078
|
+
});
|
|
5079
|
+
server.registerTool("prompts_pin", {
|
|
5080
|
+
description: "Pin a prompt so it always appears first in lists.",
|
|
5081
|
+
inputSchema: { id: exports_external.string() }
|
|
5082
|
+
}, async ({ id }) => {
|
|
5083
|
+
try {
|
|
5084
|
+
return ok(pinPrompt(id, true));
|
|
5085
|
+
} catch (e) {
|
|
5086
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
5087
|
+
}
|
|
5088
|
+
});
|
|
5089
|
+
server.registerTool("prompts_unpin", {
|
|
5090
|
+
description: "Unpin a previously pinned prompt.",
|
|
5091
|
+
inputSchema: { id: exports_external.string() }
|
|
5092
|
+
}, async ({ id }) => {
|
|
5093
|
+
try {
|
|
5094
|
+
return ok(pinPrompt(id, false));
|
|
5095
|
+
} catch (e) {
|
|
5096
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
5097
|
+
}
|
|
5098
|
+
});
|
|
5099
|
+
server.registerTool("prompts_recent", {
|
|
5100
|
+
description: "Get recently used prompts, ordered by last_used_at descending.",
|
|
5101
|
+
inputSchema: { limit: exports_external.number().optional().default(10) }
|
|
5102
|
+
}, async ({ limit }) => {
|
|
5103
|
+
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);
|
|
5104
|
+
return ok(prompts);
|
|
5105
|
+
});
|
|
5106
|
+
server.registerTool("prompts_lint", {
|
|
5107
|
+
description: "Check prompt quality: missing descriptions, undocumented template vars, short bodies, no tags.",
|
|
5108
|
+
inputSchema: { collection: exports_external.string().optional() }
|
|
5109
|
+
}, async ({ collection }) => {
|
|
5110
|
+
const prompts = listPrompts({ collection, limit: 1e4 });
|
|
5111
|
+
const results = lintAll(prompts);
|
|
5112
|
+
const summary = {
|
|
5113
|
+
total_checked: prompts.length,
|
|
5114
|
+
prompts_with_issues: results.length,
|
|
5115
|
+
errors: results.flatMap((r) => r.issues).filter((i) => i.severity === "error").length,
|
|
5116
|
+
warnings: results.flatMap((r) => r.issues).filter((i) => i.severity === "warn").length,
|
|
5117
|
+
info: results.flatMap((r) => r.issues).filter((i) => i.severity === "info").length,
|
|
5118
|
+
results
|
|
5119
|
+
};
|
|
5120
|
+
return ok(summary);
|
|
5121
|
+
});
|
|
5122
|
+
server.registerTool("prompts_stale", {
|
|
5123
|
+
description: "List prompts not used in N days. Useful for library hygiene.",
|
|
5124
|
+
inputSchema: { days: exports_external.number().optional().default(30).describe("Inactivity threshold in days") }
|
|
5125
|
+
}, async ({ days }) => {
|
|
5126
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
5127
|
+
const all = listPrompts({ limit: 1e4 });
|
|
5128
|
+
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 ?? ""));
|
|
5129
|
+
return ok({ stale, count: stale.length, threshold_days: days });
|
|
5130
|
+
});
|
|
4925
5131
|
server.registerTool("prompts_stats", {
|
|
4926
5132
|
description: "Get usage statistics: most used prompts, recently used, counts by collection and source.",
|
|
4927
5133
|
inputSchema: {}
|