@hasna/prompts 0.2.2 → 0.3.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/dist/mcp/index.js CHANGED
@@ -4118,6 +4118,26 @@ function runMigrations(db) {
4118
4118
  CREATE INDEX IF NOT EXISTS idx_prompts_project_id ON prompts(project_id);
4119
4119
  `
4120
4120
  },
4121
+ {
4122
+ name: "005_chaining",
4123
+ sql: `ALTER TABLE prompts ADD COLUMN next_prompt TEXT;`
4124
+ },
4125
+ {
4126
+ name: "006_expiry",
4127
+ sql: `ALTER TABLE prompts ADD COLUMN expires_at TEXT;`
4128
+ },
4129
+ {
4130
+ name: "007_usage_log",
4131
+ sql: `
4132
+ CREATE TABLE IF NOT EXISTS usage_log (
4133
+ id TEXT PRIMARY KEY,
4134
+ prompt_id TEXT NOT NULL REFERENCES prompts(id) ON DELETE CASCADE,
4135
+ used_at TEXT NOT NULL DEFAULT (datetime('now'))
4136
+ );
4137
+ CREATE INDEX IF NOT EXISTS idx_usage_log_prompt_id ON usage_log(prompt_id);
4138
+ CREATE INDEX IF NOT EXISTS idx_usage_log_used_at ON usage_log(used_at);
4139
+ `
4140
+ },
4121
4141
  {
4122
4142
  name: "002_fts5",
4123
4143
  sql: `
@@ -4401,6 +4421,40 @@ class ProjectNotFoundError extends Error {
4401
4421
  }
4402
4422
 
4403
4423
  // src/db/prompts.ts
4424
+ function rowToSlimPrompt(row) {
4425
+ const variables = JSON.parse(row["variables"] || "[]");
4426
+ return {
4427
+ id: row["id"],
4428
+ slug: row["slug"],
4429
+ title: row["title"],
4430
+ description: row["description"] ?? null,
4431
+ collection: row["collection"],
4432
+ tags: JSON.parse(row["tags"] || "[]"),
4433
+ variable_names: variables.map((v) => v.name),
4434
+ is_template: Boolean(row["is_template"]),
4435
+ source: row["source"],
4436
+ pinned: Boolean(row["pinned"]),
4437
+ next_prompt: row["next_prompt"] ?? null,
4438
+ expires_at: row["expires_at"] ?? null,
4439
+ project_id: row["project_id"] ?? null,
4440
+ use_count: row["use_count"],
4441
+ last_used_at: row["last_used_at"] ?? null,
4442
+ created_at: row["created_at"],
4443
+ updated_at: row["updated_at"]
4444
+ };
4445
+ }
4446
+ function promptToSaveResult(prompt, created, duplicate_warning) {
4447
+ return {
4448
+ id: prompt.id,
4449
+ slug: prompt.slug,
4450
+ title: prompt.title,
4451
+ collection: prompt.collection,
4452
+ is_template: prompt.is_template,
4453
+ variable_names: prompt.variables.map((v) => v.name),
4454
+ created,
4455
+ duplicate_warning: duplicate_warning ?? null
4456
+ };
4457
+ }
4404
4458
  function rowToPrompt(row) {
4405
4459
  return {
4406
4460
  id: row["id"],
@@ -4413,6 +4467,8 @@ function rowToPrompt(row) {
4413
4467
  tags: JSON.parse(row["tags"] || "[]"),
4414
4468
  variables: JSON.parse(row["variables"] || "[]"),
4415
4469
  pinned: Boolean(row["pinned"]),
4470
+ next_prompt: row["next_prompt"] ?? null,
4471
+ expires_at: row["expires_at"] ?? null,
4416
4472
  project_id: row["project_id"] ?? null,
4417
4473
  is_template: Boolean(row["is_template"]),
4418
4474
  source: row["source"],
@@ -4493,11 +4549,45 @@ function listPrompts(filter = {}) {
4493
4549
  orderBy = `(CASE WHEN project_id = '${filter.project_id}' THEN 0 ELSE 1 END), pinned DESC, use_count DESC, updated_at DESC`;
4494
4550
  }
4495
4551
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4496
- const limit = filter.limit ?? 100;
4552
+ const limit = filter.limit ?? 20;
4497
4553
  const offset = filter.offset ?? 0;
4498
4554
  const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
4499
4555
  return rows.map(rowToPrompt);
4500
4556
  }
4557
+ function listPromptsSlim(filter = {}) {
4558
+ const db = getDatabase();
4559
+ const conditions = [];
4560
+ const params = [];
4561
+ if (filter.collection) {
4562
+ conditions.push("collection = ?");
4563
+ params.push(filter.collection);
4564
+ }
4565
+ if (filter.is_template !== undefined) {
4566
+ conditions.push("is_template = ?");
4567
+ params.push(filter.is_template ? 1 : 0);
4568
+ }
4569
+ if (filter.source) {
4570
+ conditions.push("source = ?");
4571
+ params.push(filter.source);
4572
+ }
4573
+ if (filter.tags && filter.tags.length > 0) {
4574
+ const tagConds = filter.tags.map(() => "tags LIKE ?");
4575
+ conditions.push(`(${tagConds.join(" OR ")})`);
4576
+ for (const tag of filter.tags)
4577
+ params.push(`%"${tag}"%`);
4578
+ }
4579
+ let orderBy = "pinned DESC, use_count DESC, updated_at DESC";
4580
+ if (filter.project_id) {
4581
+ conditions.push("(project_id = ? OR project_id IS NULL)");
4582
+ params.push(filter.project_id);
4583
+ orderBy = `(CASE WHEN project_id = '${filter.project_id}' THEN 0 ELSE 1 END), pinned DESC, use_count DESC, updated_at DESC`;
4584
+ }
4585
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4586
+ const limit = filter.limit ?? 20;
4587
+ const offset = filter.offset ?? 0;
4588
+ const rows = db.query(`SELECT id, slug, name, title, description, collection, tags, variables, is_template, source, pinned, next_prompt, expires_at, project_id, use_count, last_used_at, created_at, updated_at FROM prompts ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
4589
+ return rows.map(rowToSlimPrompt);
4590
+ }
4501
4591
  function updatePrompt(idOrSlug, input) {
4502
4592
  const db = getDatabase();
4503
4593
  const prompt = requirePrompt(idOrSlug);
@@ -4511,6 +4601,7 @@ function updatePrompt(idOrSlug, input) {
4511
4601
  description = COALESCE(?, description),
4512
4602
  collection = COALESCE(?, collection),
4513
4603
  tags = COALESCE(?, tags),
4604
+ next_prompt = CASE WHEN ? IS NOT NULL THEN ? ELSE next_prompt END,
4514
4605
  variables = ?,
4515
4606
  is_template = ?,
4516
4607
  version = version + 1,
@@ -4521,6 +4612,8 @@ function updatePrompt(idOrSlug, input) {
4521
4612
  input.description ?? null,
4522
4613
  input.collection ?? null,
4523
4614
  input.tags ? JSON.stringify(input.tags) : null,
4615
+ "next_prompt" in input ? input.next_prompt ?? "" : null,
4616
+ "next_prompt" in input ? input.next_prompt ?? null : null,
4524
4617
  variables,
4525
4618
  is_template,
4526
4619
  prompt.id,
@@ -4543,6 +4636,30 @@ function usePrompt(idOrSlug) {
4543
4636
  const db = getDatabase();
4544
4637
  const prompt = requirePrompt(idOrSlug);
4545
4638
  db.run("UPDATE prompts SET use_count = use_count + 1, last_used_at = datetime('now') WHERE id = ?", [prompt.id]);
4639
+ db.run("INSERT INTO usage_log (id, prompt_id) VALUES (?, ?)", [generateId("UL"), prompt.id]);
4640
+ return requirePrompt(prompt.id);
4641
+ }
4642
+ function getTrending(days = 7, limit = 10) {
4643
+ const db = getDatabase();
4644
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
4645
+ return db.query(`SELECT p.id, p.slug, p.title, COUNT(ul.id) as uses
4646
+ FROM usage_log ul
4647
+ JOIN prompts p ON p.id = ul.prompt_id
4648
+ WHERE ul.used_at >= ?
4649
+ GROUP BY p.id
4650
+ ORDER BY uses DESC
4651
+ LIMIT ?`).all(cutoff, limit);
4652
+ }
4653
+ function setExpiry(idOrSlug, expiresAt) {
4654
+ const db = getDatabase();
4655
+ const prompt = requirePrompt(idOrSlug);
4656
+ db.run("UPDATE prompts SET expires_at = ?, updated_at = datetime('now') WHERE id = ?", [expiresAt, prompt.id]);
4657
+ return requirePrompt(prompt.id);
4658
+ }
4659
+ function setNextPrompt(idOrSlug, nextSlug) {
4660
+ const db = getDatabase();
4661
+ const prompt = requirePrompt(idOrSlug);
4662
+ db.run("UPDATE prompts SET next_prompt = ?, updated_at = datetime('now') WHERE id = ?", [nextSlug, prompt.id]);
4546
4663
  return requirePrompt(prompt.id);
4547
4664
  }
4548
4665
  function pinPrompt(idOrSlug, pinned) {
@@ -4700,6 +4817,22 @@ function deleteProject(idOrSlug) {
4700
4817
  }
4701
4818
 
4702
4819
  // src/lib/search.ts
4820
+ function rowToSlimSearchResult(row, snippet) {
4821
+ const variables = JSON.parse(row["variables"] || "[]");
4822
+ return {
4823
+ id: row["id"],
4824
+ slug: row["slug"],
4825
+ title: row["title"],
4826
+ description: row["description"] ?? null,
4827
+ collection: row["collection"],
4828
+ tags: JSON.parse(row["tags"] || "[]"),
4829
+ variable_names: variables.map((v) => v.name),
4830
+ is_template: Boolean(row["is_template"]),
4831
+ use_count: row["use_count"],
4832
+ score: row["score"] ?? 1,
4833
+ snippet
4834
+ };
4835
+ }
4703
4836
  function rowToSearchResult(row, snippet) {
4704
4837
  return {
4705
4838
  prompt: {
@@ -4713,6 +4846,8 @@ function rowToSearchResult(row, snippet) {
4713
4846
  tags: JSON.parse(row["tags"] || "[]"),
4714
4847
  variables: JSON.parse(row["variables"] || "[]"),
4715
4848
  pinned: Boolean(row["pinned"]),
4849
+ next_prompt: row["next_prompt"] ?? null,
4850
+ expires_at: row["expires_at"] ?? null,
4716
4851
  project_id: row["project_id"] ?? null,
4717
4852
  is_template: Boolean(row["is_template"]),
4718
4853
  source: row["source"],
@@ -4780,9 +4915,75 @@ function searchPrompts(query, filter = {}) {
4780
4915
  const rows = db.query(`SELECT *, 1 as score FROM prompts
4781
4916
  WHERE (name LIKE ? OR slug LIKE ? OR title LIKE ? OR body LIKE ? OR description LIKE ? OR tags LIKE ?)
4782
4917
  ORDER BY use_count DESC, updated_at DESC
4783
- LIMIT ? OFFSET ?`).all(like, like, like, like, like, like, filter.limit ?? 50, filter.offset ?? 0);
4918
+ LIMIT ? OFFSET ?`).all(like, like, like, like, like, like, filter.limit ?? 10, filter.offset ?? 0);
4784
4919
  return rows.map((r) => rowToSearchResult(r));
4785
4920
  }
4921
+ function searchPromptsSlim(query, filter = {}) {
4922
+ const db = getDatabase();
4923
+ if (!query.trim()) {
4924
+ return listPromptsSlim(filter).map((p) => ({
4925
+ id: p.id,
4926
+ slug: p.slug,
4927
+ title: p.title,
4928
+ description: p.description,
4929
+ collection: p.collection,
4930
+ tags: p.tags,
4931
+ variable_names: p.variable_names,
4932
+ is_template: p.is_template,
4933
+ use_count: p.use_count,
4934
+ score: 1
4935
+ }));
4936
+ }
4937
+ if (hasFts(db)) {
4938
+ const ftsQuery = escapeFtsQuery(query);
4939
+ const conditions = [];
4940
+ const params = [];
4941
+ if (filter.collection) {
4942
+ conditions.push("p.collection = ?");
4943
+ params.push(filter.collection);
4944
+ }
4945
+ if (filter.is_template !== undefined) {
4946
+ conditions.push("p.is_template = ?");
4947
+ params.push(filter.is_template ? 1 : 0);
4948
+ }
4949
+ if (filter.source) {
4950
+ conditions.push("p.source = ?");
4951
+ params.push(filter.source);
4952
+ }
4953
+ if (filter.tags && filter.tags.length > 0) {
4954
+ const tagConds = filter.tags.map(() => "p.tags LIKE ?");
4955
+ conditions.push(`(${tagConds.join(" OR ")})`);
4956
+ for (const tag of filter.tags)
4957
+ params.push(`%"${tag}"%`);
4958
+ }
4959
+ if (filter.project_id) {
4960
+ conditions.push("(p.project_id = ? OR p.project_id IS NULL)");
4961
+ params.push(filter.project_id);
4962
+ }
4963
+ const where = conditions.length > 0 ? `AND ${conditions.join(" AND ")}` : "";
4964
+ const limit = filter.limit ?? 10;
4965
+ const offset = filter.offset ?? 0;
4966
+ try {
4967
+ const rows2 = db.query(`SELECT p.id, p.slug, p.name, p.title, p.description, p.collection, p.tags, p.variables,
4968
+ p.is_template, p.use_count, bm25(prompts_fts) as score,
4969
+ snippet(prompts_fts, 2, '[', ']', '...', 10) as snippet
4970
+ FROM prompts p
4971
+ INNER JOIN prompts_fts ON prompts_fts.rowid = p.rowid
4972
+ WHERE prompts_fts MATCH ?
4973
+ ${where}
4974
+ ORDER BY bm25(prompts_fts)
4975
+ LIMIT ? OFFSET ?`).all(ftsQuery, ...params, limit, offset);
4976
+ return rows2.map((r) => rowToSlimSearchResult(r, r["snippet"]));
4977
+ } catch {}
4978
+ }
4979
+ const like = `%${query}%`;
4980
+ const rows = db.query(`SELECT id, slug, name, title, description, collection, tags, variables, is_template, use_count, 1 as score
4981
+ FROM prompts
4982
+ WHERE (name LIKE ? OR slug LIKE ? OR title LIKE ? OR body LIKE ? OR description LIKE ? OR tags LIKE ?)
4983
+ ORDER BY use_count DESC, updated_at DESC
4984
+ LIMIT ? OFFSET ?`).all(like, like, like, like, like, like, filter.limit ?? 10, filter.offset ?? 0);
4985
+ return rows.map((r) => rowToSlimSearchResult(r));
4986
+ }
4786
4987
  function findSimilar(promptId, limit = 5) {
4787
4988
  const db = getDatabase();
4788
4989
  const prompt = db.query("SELECT * FROM prompts WHERE id = ?").get(promptId);
@@ -4836,6 +5037,77 @@ function exportToJson(collection) {
4836
5037
  const prompts = listPrompts({ collection, limit: 1e4 });
4837
5038
  return { prompts, exported_at: new Date().toISOString(), collection };
4838
5039
  }
5040
+ function markdownToImportItem(content, filename) {
5041
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n+([\s\S]*)$/);
5042
+ if (!frontmatterMatch) {
5043
+ if (!filename)
5044
+ return null;
5045
+ const title2 = filename.replace(/\.md$/, "").replace(/-/g, " ");
5046
+ return { title: title2, body: content.trim() };
5047
+ }
5048
+ const frontmatter = frontmatterMatch[1] ?? "";
5049
+ const body = (frontmatterMatch[2] ?? "").trim();
5050
+ const get = (key) => {
5051
+ const m = frontmatter.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
5052
+ return m ? (m[1] ?? "").trim() : null;
5053
+ };
5054
+ const title = get("title") ?? (filename?.replace(/\.md$/, "").replace(/-/g, " ") ?? "Untitled");
5055
+ const slug = get("slug") ?? undefined;
5056
+ const collection = get("collection") ?? undefined;
5057
+ const description = get("description") ?? undefined;
5058
+ const tagsStr = get("tags");
5059
+ let tags;
5060
+ if (tagsStr) {
5061
+ const inner = tagsStr.replace(/^\[/, "").replace(/\]$/, "");
5062
+ tags = inner.split(",").map((t) => t.trim()).filter(Boolean);
5063
+ }
5064
+ return { title, slug, body, collection, tags, description };
5065
+ }
5066
+ function scanAndImportSlashCommands(rootDir, changedBy) {
5067
+ const { existsSync: existsSync2, readdirSync, readFileSync } = __require("fs");
5068
+ const { join: join2 } = __require("path");
5069
+ const home = process.env["HOME"] ?? "~";
5070
+ const sources = [
5071
+ { dir: join2(rootDir, ".claude", "commands"), collection: "claude-commands", tags: ["claude", "slash-command"] },
5072
+ { dir: join2(home, ".claude", "commands"), collection: "claude-commands", tags: ["claude", "slash-command"] },
5073
+ { dir: join2(rootDir, ".codex", "skills"), collection: "codex-skills", tags: ["codex", "skill"] },
5074
+ { dir: join2(home, ".codex", "skills"), collection: "codex-skills", tags: ["codex", "skill"] },
5075
+ { dir: join2(rootDir, ".gemini", "extensions"), collection: "gemini-extensions", tags: ["gemini", "extension"] },
5076
+ { dir: join2(home, ".gemini", "extensions"), collection: "gemini-extensions", tags: ["gemini", "extension"] }
5077
+ ];
5078
+ const files = [];
5079
+ const scanned = [];
5080
+ for (const { dir, collection, tags } of sources) {
5081
+ if (!existsSync2(dir))
5082
+ continue;
5083
+ let entries;
5084
+ try {
5085
+ entries = readdirSync(dir);
5086
+ } catch {
5087
+ continue;
5088
+ }
5089
+ for (const entry of entries) {
5090
+ if (!entry.endsWith(".md"))
5091
+ continue;
5092
+ const filePath = join2(dir, entry);
5093
+ try {
5094
+ const content = readFileSync(filePath, "utf-8");
5095
+ files.push({ filename: entry, content, collection, tags });
5096
+ scanned.push({ source: dir, file: entry });
5097
+ } catch {}
5098
+ }
5099
+ }
5100
+ const items = files.map((f) => {
5101
+ const base = markdownToImportItem(f.content, f.filename);
5102
+ if (base)
5103
+ return { ...base, collection: base.collection ?? f.collection, tags: base.tags ?? f.tags };
5104
+ const name = f.filename.replace(/\.md$/, "");
5105
+ const title = name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
5106
+ return { title, slug: name, body: f.content.trim(), collection: f.collection, tags: f.tags };
5107
+ });
5108
+ const imported = importFromJson(items, changedBy);
5109
+ return { scanned, imported };
5110
+ }
4839
5111
 
4840
5112
  // src/lib/mementos.ts
4841
5113
  async function maybeSaveMemento(opts) {
@@ -4861,6 +5133,48 @@ async function maybeSaveMemento(opts) {
4861
5133
  } catch {}
4862
5134
  }
4863
5135
 
5136
+ // src/lib/diff.ts
5137
+ function diffTexts(a, b) {
5138
+ const aLines = a.split(`
5139
+ `);
5140
+ const bLines = b.split(`
5141
+ `);
5142
+ const m = aLines.length;
5143
+ const n = bLines.length;
5144
+ const lcs = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
5145
+ for (let i2 = 1;i2 <= m; i2++) {
5146
+ for (let j2 = 1;j2 <= n; j2++) {
5147
+ lcs[i2][j2] = aLines[i2 - 1] === bLines[j2 - 1] ? (lcs[i2 - 1][j2 - 1] ?? 0) + 1 : Math.max(lcs[i2 - 1][j2] ?? 0, lcs[i2][j2 - 1] ?? 0);
5148
+ }
5149
+ }
5150
+ const trace = [];
5151
+ let i = m, j = n;
5152
+ while (i > 0 || j > 0) {
5153
+ if (i > 0 && j > 0 && aLines[i - 1] === bLines[j - 1]) {
5154
+ trace.unshift({ type: "unchanged", content: aLines[i - 1] ?? "" });
5155
+ i--;
5156
+ j--;
5157
+ } else if (j > 0 && (i === 0 || (lcs[i][j - 1] ?? 0) >= (lcs[i - 1][j] ?? 0))) {
5158
+ trace.unshift({ type: "added", content: bLines[j - 1] ?? "" });
5159
+ j--;
5160
+ } else {
5161
+ trace.unshift({ type: "removed", content: aLines[i - 1] ?? "" });
5162
+ i--;
5163
+ }
5164
+ }
5165
+ return trace;
5166
+ }
5167
+ function formatDiff(lines) {
5168
+ return lines.map((l) => {
5169
+ if (l.type === "added")
5170
+ return `+ ${l.content}`;
5171
+ if (l.type === "removed")
5172
+ return `- ${l.content}`;
5173
+ return ` ${l.content}`;
5174
+ }).join(`
5175
+ `);
5176
+ }
5177
+
4864
5178
  // src/lib/lint.ts
4865
5179
  function lintPrompt(p) {
4866
5180
  const issues = [];
@@ -4895,6 +5209,83 @@ function lintAll(prompts) {
4895
5209
  return prompts.map((p) => ({ prompt: p, issues: lintPrompt(p) })).filter((r) => r.issues.length > 0);
4896
5210
  }
4897
5211
 
5212
+ // src/lib/audit.ts
5213
+ function runAudit() {
5214
+ const db = getDatabase();
5215
+ const issues = [];
5216
+ const orphaned = db.query(`
5217
+ SELECT p.id, p.slug FROM prompts p
5218
+ WHERE p.project_id IS NOT NULL
5219
+ AND NOT EXISTS (SELECT 1 FROM projects pr WHERE pr.id = p.project_id)
5220
+ `).all();
5221
+ for (const p of orphaned) {
5222
+ issues.push({
5223
+ type: "orphaned-project",
5224
+ severity: "error",
5225
+ prompt_id: p.id,
5226
+ slug: p.slug,
5227
+ message: `Prompt "${p.slug}" references a deleted project`
5228
+ });
5229
+ }
5230
+ const emptyCollections = db.query(`
5231
+ SELECT c.name FROM collections c
5232
+ WHERE NOT EXISTS (SELECT 1 FROM prompts p WHERE p.collection = c.name)
5233
+ AND c.name != 'default'
5234
+ `).all();
5235
+ for (const c of emptyCollections) {
5236
+ issues.push({
5237
+ type: "empty-collection",
5238
+ severity: "info",
5239
+ message: `Collection "${c.name}" has no prompts`
5240
+ });
5241
+ }
5242
+ const missingHistory = db.query(`
5243
+ SELECT p.id, p.slug FROM prompts p
5244
+ WHERE NOT EXISTS (SELECT 1 FROM prompt_versions v WHERE v.prompt_id = p.id)
5245
+ `).all();
5246
+ for (const p of missingHistory) {
5247
+ issues.push({
5248
+ type: "missing-version-history",
5249
+ severity: "warn",
5250
+ prompt_id: p.id,
5251
+ slug: p.slug,
5252
+ message: `Prompt "${p.slug}" has no version history entries`
5253
+ });
5254
+ }
5255
+ const slugs = db.query("SELECT id, slug FROM prompts").all();
5256
+ const seen = new Map;
5257
+ for (const { id, slug } of slugs) {
5258
+ const base = slug.replace(/-\d+$/, "");
5259
+ if (seen.has(base) && seen.get(base) !== id) {
5260
+ issues.push({
5261
+ type: "near-duplicate-slug",
5262
+ severity: "info",
5263
+ slug,
5264
+ message: `"${slug}" looks like a duplicate of "${base}" \u2014 consider merging`
5265
+ });
5266
+ } else {
5267
+ seen.set(base, id);
5268
+ }
5269
+ }
5270
+ const now = new Date().toISOString();
5271
+ const expired = db.query(`
5272
+ SELECT id, slug FROM prompts WHERE expires_at IS NOT NULL AND expires_at < ?
5273
+ `).all(now);
5274
+ for (const p of expired) {
5275
+ issues.push({
5276
+ type: "expired",
5277
+ severity: "warn",
5278
+ prompt_id: p.id,
5279
+ slug: p.slug,
5280
+ message: `Prompt "${p.slug}" has expired`
5281
+ });
5282
+ }
5283
+ const errors2 = issues.filter((i) => i.severity === "error").length;
5284
+ const warnings = issues.filter((i) => i.severity === "warn").length;
5285
+ const info = issues.filter((i) => i.severity === "info").length;
5286
+ return { issues, errors: errors2, warnings, info, checked_at: new Date().toISOString() };
5287
+ }
5288
+
4898
5289
  // src/mcp/index.ts
4899
5290
  var server = new McpServer({ name: "open-prompts", version: "0.1.0" });
4900
5291
  function ok(data) {
@@ -4928,7 +5319,7 @@ server.registerTool("prompts_save", {
4928
5319
  input.project_id = pid;
4929
5320
  }
4930
5321
  const { prompt, created, duplicate_warning } = upsertPrompt(input, force ?? false);
4931
- return ok({ ...prompt, _created: created, _duplicate_warning: duplicate_warning ?? null });
5322
+ return ok(promptToSaveResult(prompt, created, duplicate_warning));
4932
5323
  } catch (e) {
4933
5324
  return err(e instanceof Error ? e.message : String(e));
4934
5325
  }
@@ -4943,25 +5334,44 @@ server.registerTool("prompts_get", {
4943
5334
  return ok(prompt);
4944
5335
  });
4945
5336
  server.registerTool("prompts_list", {
4946
- description: "List prompts with optional filters.",
5337
+ description: "List prompts (slim by default \u2014 no body). Use prompts_use or prompts_body to get the actual body. Pass include_body:true only if you need body text for all results. summary_only:true returns just id+slug+title for maximum token savings.",
4947
5338
  inputSchema: {
4948
5339
  collection: exports_external.string().optional(),
4949
5340
  tags: exports_external.array(exports_external.string()).optional(),
4950
5341
  is_template: exports_external.boolean().optional(),
4951
5342
  source: exports_external.enum(["manual", "ai-session", "imported"]).optional(),
4952
- limit: exports_external.number().optional().default(50),
5343
+ limit: exports_external.number().optional().default(20),
4953
5344
  offset: exports_external.number().optional().default(0),
4954
- project: exports_external.string().optional().describe("Project name, slug, or ID \u2014 shows project prompts first, then globals")
5345
+ project: exports_external.string().optional().describe("Project name, slug, or ID"),
5346
+ include_body: exports_external.boolean().optional().describe("Include full body text (expensive \u2014 avoid unless needed)"),
5347
+ summary_only: exports_external.boolean().optional().describe("Return only id+slug+title \u2014 maximum token savings")
4955
5348
  }
4956
- }, async ({ project, ...args }) => {
5349
+ }, async ({ project, include_body, summary_only, ...args }) => {
5350
+ let project_id;
4957
5351
  if (project) {
4958
5352
  const db = getDatabase();
4959
5353
  const pid = resolveProject(db, project);
4960
5354
  if (!pid)
4961
5355
  return err(`Project not found: ${project}`);
4962
- return ok(listPrompts({ ...args, project_id: pid }));
5356
+ project_id = pid;
4963
5357
  }
4964
- return ok(listPrompts(args));
5358
+ const filter = { ...args, ...project_id ? { project_id } : {} };
5359
+ if (summary_only) {
5360
+ const items = listPromptsSlim(filter);
5361
+ return ok(items.map((p) => ({ id: p.id, slug: p.slug, title: p.title })));
5362
+ }
5363
+ if (include_body)
5364
+ return ok(listPrompts(filter));
5365
+ return ok(listPromptsSlim(filter));
5366
+ });
5367
+ server.registerTool("prompts_body", {
5368
+ description: "Get just the body text of a prompt without incrementing the use counter. Use prompts_use when you want to actually use a prompt (increments counter). Use this just to read/inspect the body.",
5369
+ inputSchema: { id: exports_external.string().describe("Prompt ID or slug") }
5370
+ }, async ({ id }) => {
5371
+ const prompt = getPrompt(id);
5372
+ if (!prompt)
5373
+ return err(`Prompt not found: ${id}`);
5374
+ return ok({ id: prompt.id, slug: prompt.slug, body: prompt.body, is_template: prompt.is_template, variable_names: prompt.variables.map((v) => v.name) });
4965
5375
  });
4966
5376
  server.registerTool("prompts_delete", {
4967
5377
  description: "Delete a prompt by ID or slug.",
@@ -5013,7 +5423,7 @@ server.registerTool("prompts_list_templates", {
5013
5423
  tags: exports_external.array(exports_external.string()).optional(),
5014
5424
  limit: exports_external.number().optional().default(50)
5015
5425
  }
5016
- }, async (args) => ok(listPrompts({ ...args, is_template: true })));
5426
+ }, async (args) => ok(listPromptsSlim({ ...args, is_template: true })));
5017
5427
  server.registerTool("prompts_variables", {
5018
5428
  description: "Inspect what variables a template needs, including defaults and required status.",
5019
5429
  inputSchema: { id: exports_external.string() }
@@ -5025,25 +5435,30 @@ server.registerTool("prompts_variables", {
5025
5435
  return ok({ prompt_id: prompt.id, slug: prompt.slug, variables: vars });
5026
5436
  });
5027
5437
  server.registerTool("prompts_search", {
5028
- description: "Full-text search across prompt name, slug, title, body, description, and tags. Uses FTS5 BM25 ranking.",
5438
+ description: "Search prompts by text (FTS5 BM25). Returns slim results with snippet \u2014 no body. Use prompts_use/prompts_body to get the body of a result.",
5029
5439
  inputSchema: {
5030
5440
  q: exports_external.string().describe("Search query"),
5031
5441
  collection: exports_external.string().optional(),
5032
5442
  tags: exports_external.array(exports_external.string()).optional(),
5033
5443
  is_template: exports_external.boolean().optional(),
5034
5444
  source: exports_external.enum(["manual", "ai-session", "imported"]).optional(),
5035
- limit: exports_external.number().optional().default(20),
5036
- project: exports_external.string().optional().describe("Project name, slug, or ID to scope search")
5445
+ limit: exports_external.number().optional().default(10),
5446
+ project: exports_external.string().optional(),
5447
+ include_body: exports_external.boolean().optional().describe("Include full body in results (expensive)")
5037
5448
  }
5038
- }, async ({ q, project, ...filter }) => {
5449
+ }, async ({ q, project, include_body, ...filter }) => {
5450
+ let project_id;
5039
5451
  if (project) {
5040
5452
  const db = getDatabase();
5041
5453
  const pid = resolveProject(db, project);
5042
5454
  if (!pid)
5043
5455
  return err(`Project not found: ${project}`);
5044
- return ok(searchPrompts(q, { ...filter, project_id: pid }));
5456
+ project_id = pid;
5045
5457
  }
5046
- return ok(searchPrompts(q, filter));
5458
+ const f = { ...filter, ...project_id ? { project_id } : {} };
5459
+ if (include_body)
5460
+ return ok(searchPrompts(q, f));
5461
+ return ok(searchPromptsSlim(q, f));
5047
5462
  });
5048
5463
  server.registerTool("prompts_similar", {
5049
5464
  description: "Find prompts similar to a given prompt (by tag overlap and collection).",
@@ -5128,6 +5543,17 @@ server.registerTool("prompts_import", {
5128
5543
  const results = importFromJson(prompts, changed_by);
5129
5544
  return ok(results);
5130
5545
  });
5546
+ server.registerTool("prompts_import_slash_commands", {
5547
+ description: "Auto-scan .claude/commands, .codex/skills, .gemini/extensions (both project and home dir) and import all .md files as prompts.",
5548
+ inputSchema: {
5549
+ dir: exports_external.string().optional().describe("Root directory to scan (default: cwd)"),
5550
+ changed_by: exports_external.string().optional()
5551
+ }
5552
+ }, async ({ dir, changed_by }) => {
5553
+ const rootDir = dir ?? process.cwd();
5554
+ const result = scanAndImportSlashCommands(rootDir, changed_by);
5555
+ return ok(result);
5556
+ });
5131
5557
  server.registerTool("prompts_update", {
5132
5558
  description: "Update an existing prompt's fields.",
5133
5559
  inputSchema: {
@@ -5142,7 +5568,7 @@ server.registerTool("prompts_update", {
5142
5568
  }, async ({ id, ...updates }) => {
5143
5569
  try {
5144
5570
  const prompt = updatePrompt(id, updates);
5145
- return ok(prompt);
5571
+ return ok(promptToSaveResult(prompt, false));
5146
5572
  } catch (e) {
5147
5573
  return err(e instanceof Error ? e.message : String(e));
5148
5574
  }
@@ -5183,9 +5609,10 @@ server.registerTool("prompts_save_from_session", {
5183
5609
  collection: exports_external.string().optional().describe("Collection to save into (default: 'sessions')"),
5184
5610
  description: exports_external.string().optional().describe("One-line description of what this prompt does"),
5185
5611
  agent: exports_external.string().optional().describe("Agent name saving this prompt"),
5186
- project: exports_external.string().optional().describe("Project name, slug, or ID to scope this prompt to")
5612
+ project: exports_external.string().optional().describe("Project name, slug, or ID to scope this prompt to"),
5613
+ pin: exports_external.boolean().optional().describe("Pin the prompt immediately so it surfaces first in all lists")
5187
5614
  }
5188
- }, async ({ title, body, slug, tags, collection, description, agent, project }) => {
5615
+ }, async ({ title, body, slug, tags, collection, description, agent, project, pin }) => {
5189
5616
  try {
5190
5617
  let project_id;
5191
5618
  if (project) {
@@ -5206,7 +5633,118 @@ server.registerTool("prompts_save_from_session", {
5206
5633
  changed_by: agent,
5207
5634
  project_id
5208
5635
  });
5209
- return ok({ ...prompt, _created: created, _tip: created ? `Saved as "${prompt.slug}". Use prompts_use("${prompt.slug}") to retrieve it.` : `Updated existing prompt "${prompt.slug}".` });
5636
+ if (pin)
5637
+ pinPrompt(prompt.id, true);
5638
+ const result = promptToSaveResult(prompt, created);
5639
+ return ok({ ...result, pinned: pin ?? false, _tip: created ? `Saved as "${prompt.slug}". Use prompts_use("${prompt.slug}") to retrieve it.` : `Updated "${prompt.slug}".` });
5640
+ } catch (e) {
5641
+ return err(e instanceof Error ? e.message : String(e));
5642
+ }
5643
+ });
5644
+ server.registerTool("prompts_audit", {
5645
+ description: "Run a full audit: orphaned project refs, empty collections, missing version history, near-duplicate slugs, expired prompts.",
5646
+ inputSchema: {}
5647
+ }, async () => ok(runAudit()));
5648
+ server.registerTool("prompts_unused", {
5649
+ description: "List prompts with use_count = 0 \u2014 never used. Good for library cleanup.",
5650
+ inputSchema: { collection: exports_external.string().optional(), limit: exports_external.number().optional().default(50) }
5651
+ }, async ({ collection, limit }) => {
5652
+ const all = listPromptsSlim({ collection, limit: 1e4 });
5653
+ const unused = all.filter((p) => p.use_count === 0).slice(0, limit).map((p) => ({ id: p.id, slug: p.slug, title: p.title, collection: p.collection, created_at: p.created_at }));
5654
+ return ok({ unused, count: unused.length });
5655
+ });
5656
+ server.registerTool("prompts_trending", {
5657
+ description: "Get most-used prompts in the last N days based on per-use log.",
5658
+ inputSchema: {
5659
+ days: exports_external.number().optional().default(7),
5660
+ limit: exports_external.number().optional().default(10)
5661
+ }
5662
+ }, async ({ days, limit }) => ok(getTrending(days, limit)));
5663
+ server.registerTool("prompts_set_expiry", {
5664
+ description: "Set or clear an expiry date on a prompt. Pass expires_at=null to clear.",
5665
+ inputSchema: {
5666
+ id: exports_external.string(),
5667
+ expires_at: exports_external.string().nullable().describe("ISO date string (e.g. 2026-12-31) or null to clear")
5668
+ }
5669
+ }, async ({ id, expires_at }) => {
5670
+ try {
5671
+ return ok(setExpiry(id, expires_at));
5672
+ } catch (e) {
5673
+ return err(e instanceof Error ? e.message : String(e));
5674
+ }
5675
+ });
5676
+ server.registerTool("prompts_duplicate", {
5677
+ description: "Clone a prompt with a new slug. Copies body, tags, collection, description. Version resets to 1.",
5678
+ inputSchema: {
5679
+ id: exports_external.string(),
5680
+ slug: exports_external.string().optional().describe("New slug (auto-generated if omitted)"),
5681
+ title: exports_external.string().optional().describe("New title (defaults to 'Copy of <original>')")
5682
+ }
5683
+ }, async ({ id, slug, title }) => {
5684
+ try {
5685
+ const source = getPrompt(id);
5686
+ if (!source)
5687
+ return err(`Prompt not found: ${id}`);
5688
+ const { prompt } = upsertPrompt({
5689
+ title: title ?? `Copy of ${source.title}`,
5690
+ slug,
5691
+ body: source.body,
5692
+ description: source.description ?? undefined,
5693
+ collection: source.collection,
5694
+ tags: source.tags,
5695
+ source: "manual"
5696
+ });
5697
+ return ok(prompt);
5698
+ } catch (e) {
5699
+ return err(e instanceof Error ? e.message : String(e));
5700
+ }
5701
+ });
5702
+ server.registerTool("prompts_diff", {
5703
+ description: "Show a line diff between two versions of a prompt body. v2 defaults to current version.",
5704
+ inputSchema: {
5705
+ id: exports_external.string(),
5706
+ v1: exports_external.number().describe("First version number"),
5707
+ v2: exports_external.number().optional().describe("Second version (default: current)")
5708
+ }
5709
+ }, async ({ id, v1, v2 }) => {
5710
+ try {
5711
+ const prompt = getPrompt(id);
5712
+ if (!prompt)
5713
+ return err(`Prompt not found: ${id}`);
5714
+ const versions = listVersions(prompt.id);
5715
+ const versionA = versions.find((v) => v.version === v1);
5716
+ if (!versionA)
5717
+ return err(`Version ${v1} not found`);
5718
+ const bodyB = v2 ? versions.find((v) => v.version === v2)?.body ?? null : prompt.body;
5719
+ if (bodyB === null)
5720
+ return err(`Version ${v2} not found`);
5721
+ const lines = diffTexts(versionA.body, bodyB);
5722
+ return ok({ lines, formatted: formatDiff(lines), v1, v2: v2 ?? prompt.version });
5723
+ } catch (e) {
5724
+ return err(e instanceof Error ? e.message : String(e));
5725
+ }
5726
+ });
5727
+ server.registerTool("prompts_chain", {
5728
+ description: "Set or get the next prompt in a chain. After using prompt A, the agent is suggested prompt B. Pass next_prompt=null to clear.",
5729
+ inputSchema: {
5730
+ id: exports_external.string().describe("Prompt ID or slug"),
5731
+ next_prompt: exports_external.string().nullable().optional().describe("Slug of the next prompt in the chain, or null to clear")
5732
+ }
5733
+ }, async ({ id, next_prompt }) => {
5734
+ try {
5735
+ if (next_prompt !== undefined) {
5736
+ const p = setNextPrompt(id, next_prompt ?? null);
5737
+ return ok(p);
5738
+ }
5739
+ const chain = [];
5740
+ let cur = getPrompt(id);
5741
+ const seen = new Set;
5742
+ while (cur && !seen.has(cur.id)) {
5743
+ chain.push({ id: cur.id, slug: cur.slug, title: cur.title });
5744
+ seen.add(cur.id);
5745
+ cur = cur.next_prompt ? getPrompt(cur.next_prompt) : null;
5746
+ }
5747
+ return ok(chain);
5210
5748
  } catch (e) {
5211
5749
  return err(e instanceof Error ? e.message : String(e));
5212
5750
  }
@@ -5232,10 +5770,10 @@ server.registerTool("prompts_unpin", {
5232
5770
  }
5233
5771
  });
5234
5772
  server.registerTool("prompts_recent", {
5235
- description: "Get recently used prompts, ordered by last_used_at descending.",
5773
+ description: "Get recently used prompts (slim \u2014 no body). Returns id, slug, title, tags, use_count, last_used_at.",
5236
5774
  inputSchema: { limit: exports_external.number().optional().default(10) }
5237
5775
  }, async ({ limit }) => {
5238
- 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);
5776
+ const prompts = listPromptsSlim({ limit: 500 }).filter((p) => p.last_used_at !== null).sort((a, b) => (b.last_used_at ?? "").localeCompare(a.last_used_at ?? "")).slice(0, limit);
5239
5777
  return ok(prompts);
5240
5778
  });
5241
5779
  server.registerTool("prompts_lint", {
@@ -5259,8 +5797,8 @@ server.registerTool("prompts_stale", {
5259
5797
  inputSchema: { days: exports_external.number().optional().default(30).describe("Inactivity threshold in days") }
5260
5798
  }, async ({ days }) => {
5261
5799
  const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
5262
- const all = listPrompts({ limit: 1e4 });
5263
- 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 ?? ""));
5800
+ const all = listPromptsSlim({ limit: 1e4 });
5801
+ 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 ?? "")).map((p) => ({ id: p.id, slug: p.slug, title: p.title, last_used_at: p.last_used_at, use_count: p.use_count }));
5264
5802
  return ok({ stale, count: stale.length, threshold_days: days });
5265
5803
  });
5266
5804
  server.registerTool("prompts_stats", {