@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.
@@ -111,6 +111,10 @@ function runMigrations(db) {
111
111
  CREATE INDEX IF NOT EXISTS idx_versions_prompt_id ON prompt_versions(prompt_id);
112
112
  `
113
113
  },
114
+ {
115
+ name: "003_pinned",
116
+ sql: `ALTER TABLE prompts ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;`
117
+ },
114
118
  {
115
119
  name: "002_fts5",
116
120
  sql: `
@@ -164,6 +168,15 @@ function resolvePrompt(db, idOrSlug) {
164
168
  const byPrefix = db.query("SELECT id FROM prompts WHERE id LIKE ? LIMIT 2").all(`${idOrSlug}%`);
165
169
  if (byPrefix.length === 1 && byPrefix[0])
166
170
  return byPrefix[0].id;
171
+ const bySlugPrefix = db.query("SELECT id FROM prompts WHERE slug LIKE ? LIMIT 2").all(`${idOrSlug}%`);
172
+ if (bySlugPrefix.length === 1 && bySlugPrefix[0])
173
+ return bySlugPrefix[0].id;
174
+ const bySlugSub = db.query("SELECT id FROM prompts WHERE slug LIKE ? LIMIT 2").all(`%${idOrSlug}%`);
175
+ if (bySlugSub.length === 1 && bySlugSub[0])
176
+ return bySlugSub[0].id;
177
+ const byTitle = db.query("SELECT id FROM prompts WHERE lower(title) LIKE ? LIMIT 2").all(`%${idOrSlug.toLowerCase()}%`);
178
+ if (byTitle.length === 1 && byTitle[0])
179
+ return byTitle[0].id;
167
180
  return null;
168
181
  }
169
182
 
@@ -257,6 +270,36 @@ function movePrompt(promptIdOrSlug, targetCollection) {
257
270
  ]);
258
271
  }
259
272
 
273
+ // src/lib/duplicates.ts
274
+ function tokenize(text) {
275
+ return new Set(text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2));
276
+ }
277
+ function similarity(a, b) {
278
+ const ta = tokenize(a);
279
+ const tb = tokenize(b);
280
+ if (ta.size === 0 || tb.size === 0)
281
+ return 0;
282
+ let shared = 0;
283
+ for (const word of ta) {
284
+ if (tb.has(word))
285
+ shared++;
286
+ }
287
+ return shared / Math.max(ta.size, tb.size);
288
+ }
289
+ function findDuplicates(body, threshold = 0.8, excludeSlug) {
290
+ const all = listPrompts({ limit: 1e4 });
291
+ const matches = [];
292
+ for (const p of all) {
293
+ if (excludeSlug && p.slug === excludeSlug)
294
+ continue;
295
+ const score = similarity(body, p.body);
296
+ if (score >= threshold) {
297
+ matches.push({ prompt: p, score });
298
+ }
299
+ }
300
+ return matches.sort((a, b) => b.score - a.score);
301
+ }
302
+
260
303
  // src/lib/template.ts
261
304
  var VAR_PATTERN = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\|\s*(.*?)\s*)?\}\}/g;
262
305
  function extractVariables(body) {
@@ -334,6 +377,7 @@ function rowToPrompt(row) {
334
377
  collection: row["collection"],
335
378
  tags: JSON.parse(row["tags"] || "[]"),
336
379
  variables: JSON.parse(row["variables"] || "[]"),
380
+ pinned: Boolean(row["pinned"]),
337
381
  is_template: Boolean(row["is_template"]),
338
382
  source: row["source"],
339
383
  version: row["version"],
@@ -408,7 +452,7 @@ function listPrompts(filter = {}) {
408
452
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
409
453
  const limit = filter.limit ?? 100;
410
454
  const offset = filter.offset ?? 0;
411
- const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY use_count DESC, updated_at DESC LIMIT ? OFFSET ?`).all([...params, limit, offset]);
455
+ const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY pinned DESC, use_count DESC, updated_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
412
456
  return rows.map(rowToPrompt);
413
457
  }
414
458
  function updatePrompt(idOrSlug, input) {
@@ -458,7 +502,7 @@ function usePrompt(idOrSlug) {
458
502
  db.run("UPDATE prompts SET use_count = use_count + 1, last_used_at = datetime('now') WHERE id = ?", [prompt.id]);
459
503
  return requirePrompt(prompt.id);
460
504
  }
461
- function upsertPrompt(input) {
505
+ function upsertPrompt(input, force = false) {
462
506
  const db = getDatabase();
463
507
  const slug = input.slug || generateSlug(input.title);
464
508
  const existing = db.query("SELECT id FROM prompts WHERE slug = ?").get(slug);
@@ -473,8 +517,16 @@ function upsertPrompt(input) {
473
517
  });
474
518
  return { prompt: prompt2, created: false };
475
519
  }
520
+ let duplicate_warning;
521
+ if (!force && input.body) {
522
+ const dupes = findDuplicates(input.body, 0.8, slug);
523
+ if (dupes.length > 0) {
524
+ const top = dupes[0];
525
+ duplicate_warning = `Similar prompt already exists: "${top.prompt.slug}" (${Math.round(top.score * 100)}% match). Use --force to save anyway.`;
526
+ }
527
+ }
476
528
  const prompt = createPrompt({ ...input, slug });
477
- return { prompt, created: true };
529
+ return { prompt, created: true, duplicate_warning };
478
530
  }
479
531
  function getPromptStats() {
480
532
  const db = getDatabase();
@@ -540,6 +592,7 @@ function rowToSearchResult(row, snippet) {
540
592
  collection: row["collection"],
541
593
  tags: JSON.parse(row["tags"] || "[]"),
542
594
  variables: JSON.parse(row["variables"] || "[]"),
595
+ pinned: Boolean(row["pinned"]),
543
596
  is_template: Boolean(row["is_template"]),
544
597
  source: row["source"],
545
598
  version: row["version"],
@@ -594,7 +647,7 @@ function searchPrompts(query, filter = {}) {
594
647
  WHERE prompts_fts MATCH ?
595
648
  ${where}
596
649
  ORDER BY bm25(prompts_fts)
597
- LIMIT ? OFFSET ?`).all([ftsQuery, ...params, limit, offset]);
650
+ LIMIT ? OFFSET ?`).all(ftsQuery, ...params, limit, offset);
598
651
  return rows2.map((r) => rowToSearchResult(r, r["snippet"]));
599
652
  } catch {}
600
653
  }
@@ -602,7 +655,7 @@ function searchPrompts(query, filter = {}) {
602
655
  const rows = db.query(`SELECT *, 1 as score FROM prompts
603
656
  WHERE (name LIKE ? OR slug LIKE ? OR title LIKE ? OR body LIKE ? OR description LIKE ? OR tags LIKE ?)
604
657
  ORDER BY use_count DESC, updated_at DESC
605
- LIMIT ? OFFSET ?`).all([like, like, like, like, like, like, filter.limit ?? 50, filter.offset ?? 0]);
658
+ LIMIT ? OFFSET ?`).all(like, like, like, like, like, like, filter.limit ?? 50, filter.offset ?? 0);
606
659
  return rows.map((r) => rowToSearchResult(r));
607
660
  }
608
661
  function findSimilar(promptId, limit = 5) {
@@ -613,10 +666,10 @@ function findSimilar(promptId, limit = 5) {
613
666
  const tags = JSON.parse(prompt["tags"] || "[]");
614
667
  const collection = prompt["collection"];
615
668
  if (tags.length === 0) {
616
- const rows = db.query("SELECT *, 1 as score FROM prompts WHERE collection = ? AND id != ? ORDER BY use_count DESC LIMIT ?").all([collection, promptId, limit]);
669
+ const rows = db.query("SELECT *, 1 as score FROM prompts WHERE collection = ? AND id != ? ORDER BY use_count DESC LIMIT ?").all(collection, promptId, limit);
617
670
  return rows.map((r) => rowToSearchResult(r));
618
671
  }
619
- const allRows = db.query("SELECT * FROM prompts WHERE id != ?").all([promptId]);
672
+ const allRows = db.query("SELECT * FROM prompts WHERE id != ?").all(promptId);
620
673
  const scored = allRows.map((row) => {
621
674
  const rowTags = JSON.parse(row["tags"] || "[]");
622
675
  const overlap = rowTags.filter((t) => tags.includes(t)).length;
@@ -10,6 +10,7 @@ export interface Prompt {
10
10
  variables: TemplateVariable[];
11
11
  is_template: boolean;
12
12
  source: PromptSource;
13
+ pinned: boolean;
13
14
  version: number;
14
15
  use_count: number;
15
16
  last_used_at: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,SAAS,EAAE,gBAAgB,EAAE,CAAA;IAC7B,WAAW,EAAE,OAAO,CAAA;IACpB,MAAM,EAAE,YAAY,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,YAAY,GAAG,UAAU,CAAA;AAE/D,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,CAAC,CAAC,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,aAAa,EAAE,MAAM,EAAE,CAAA;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAA;IACrB,eAAe,EAAE,MAAM,CAAA;IACvB,iBAAiB,EAAE,MAAM,CAAA;IACzB,SAAS,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC9F,aAAa,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACrG,aAAa,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC3D,SAAS,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACpD;AAED,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,EAAE,EAAE,MAAM;CAIvB;AAED,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,EAAE,EAAE,MAAM;CAIvB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,IAAI,EAAE,MAAM;CAIzB;AAED,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@hasna/prompts",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Reusable prompt library for AI agents — CLI + MCP server + REST API + web dashboard",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "bin": {
9
- "open-prompts": "dist/cli/index.js",
10
- "open-prompts-mcp": "dist/mcp/index.js",
11
- "open-prompts-serve": "dist/server/index.js"
9
+ "prompts": "dist/cli/index.js",
10
+ "prompts-mcp": "dist/mcp/index.js",
11
+ "prompts-serve": "dist/server/index.js"
12
12
  },
13
13
  "exports": {
14
14
  ".": {