@hasna/prompts 0.2.1 → 0.3.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/dist/mcp/index.js CHANGED
@@ -4103,6 +4103,41 @@ function runMigrations(db) {
4103
4103
  name: "003_pinned",
4104
4104
  sql: `ALTER TABLE prompts ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;`
4105
4105
  },
4106
+ {
4107
+ name: "004_projects",
4108
+ sql: `
4109
+ CREATE TABLE IF NOT EXISTS projects (
4110
+ id TEXT PRIMARY KEY,
4111
+ name TEXT NOT NULL UNIQUE,
4112
+ slug TEXT NOT NULL UNIQUE,
4113
+ description TEXT,
4114
+ path TEXT,
4115
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
4116
+ );
4117
+ ALTER TABLE prompts ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
4118
+ CREATE INDEX IF NOT EXISTS idx_prompts_project_id ON prompts(project_id);
4119
+ `
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
+ },
4106
4141
  {
4107
4142
  name: "002_fts5",
4108
4143
  sql: `
@@ -4143,6 +4178,24 @@ function runMigrations(db) {
4143
4178
  db.run("INSERT INTO _migrations (name) VALUES (?)", [migration.name]);
4144
4179
  }
4145
4180
  }
4181
+ function resolveProject(db, idOrSlug) {
4182
+ const byId = db.query("SELECT id FROM projects WHERE id = ?").get(idOrSlug);
4183
+ if (byId)
4184
+ return byId.id;
4185
+ const bySlug = db.query("SELECT id FROM projects WHERE slug = ?").get(idOrSlug);
4186
+ if (bySlug)
4187
+ return bySlug.id;
4188
+ const byName = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(idOrSlug.toLowerCase());
4189
+ if (byName)
4190
+ return byName.id;
4191
+ const byPrefix = db.query("SELECT id FROM projects WHERE id LIKE ? LIMIT 2").all(`${idOrSlug}%`);
4192
+ if (byPrefix.length === 1 && byPrefix[0])
4193
+ return byPrefix[0].id;
4194
+ const bySlugPrefix = db.query("SELECT id FROM projects WHERE slug LIKE ? LIMIT 2").all(`${idOrSlug}%`);
4195
+ if (bySlugPrefix.length === 1 && bySlugPrefix[0])
4196
+ return bySlugPrefix[0].id;
4197
+ return null;
4198
+ }
4146
4199
  function hasFts(db) {
4147
4200
  return db.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='prompts_fts'").get() !== null;
4148
4201
  }
@@ -4360,6 +4413,12 @@ class DuplicateSlugError extends Error {
4360
4413
  this.name = "DuplicateSlugError";
4361
4414
  }
4362
4415
  }
4416
+ class ProjectNotFoundError extends Error {
4417
+ constructor(id) {
4418
+ super(`Project not found: ${id}`);
4419
+ this.name = "ProjectNotFoundError";
4420
+ }
4421
+ }
4363
4422
 
4364
4423
  // src/db/prompts.ts
4365
4424
  function rowToPrompt(row) {
@@ -4374,6 +4433,9 @@ function rowToPrompt(row) {
4374
4433
  tags: JSON.parse(row["tags"] || "[]"),
4375
4434
  variables: JSON.parse(row["variables"] || "[]"),
4376
4435
  pinned: Boolean(row["pinned"]),
4436
+ next_prompt: row["next_prompt"] ?? null,
4437
+ expires_at: row["expires_at"] ?? null,
4438
+ project_id: row["project_id"] ?? null,
4377
4439
  is_template: Boolean(row["is_template"]),
4378
4440
  source: row["source"],
4379
4441
  version: row["version"],
@@ -4397,11 +4459,12 @@ function createPrompt(input) {
4397
4459
  ensureCollection(collection);
4398
4460
  const tags = JSON.stringify(input.tags || []);
4399
4461
  const source = input.source || "manual";
4462
+ const project_id = input.project_id ?? null;
4400
4463
  const vars = extractVariables(input.body);
4401
4464
  const variables = JSON.stringify(vars.map((v) => ({ name: v, required: true })));
4402
4465
  const is_template = vars.length > 0 ? 1 : 0;
4403
- db.run(`INSERT INTO prompts (id, name, slug, title, body, description, collection, tags, variables, is_template, source)
4404
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, name, slug, input.title, input.body, input.description ?? null, collection, tags, variables, is_template, source]);
4466
+ db.run(`INSERT INTO prompts (id, name, slug, title, body, description, collection, tags, variables, is_template, source, project_id)
4467
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, name, slug, input.title, input.body, input.description ?? null, collection, tags, variables, is_template, source, project_id]);
4405
4468
  db.run(`INSERT INTO prompt_versions (id, prompt_id, body, version, changed_by)
4406
4469
  VALUES (?, ?, ?, 1, ?)`, [generateId("VER"), id, input.body, input.changed_by ?? null]);
4407
4470
  return getPrompt(id);
@@ -4445,10 +4508,16 @@ function listPrompts(filter = {}) {
4445
4508
  params.push(`%"${tag}"%`);
4446
4509
  }
4447
4510
  }
4511
+ let orderBy = "pinned DESC, use_count DESC, updated_at DESC";
4512
+ if (filter.project_id !== undefined && filter.project_id !== null) {
4513
+ conditions.push("(project_id = ? OR project_id IS NULL)");
4514
+ params.push(filter.project_id);
4515
+ orderBy = `(CASE WHEN project_id = '${filter.project_id}' THEN 0 ELSE 1 END), pinned DESC, use_count DESC, updated_at DESC`;
4516
+ }
4448
4517
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4449
4518
  const limit = filter.limit ?? 100;
4450
4519
  const offset = filter.offset ?? 0;
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);
4520
+ const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
4452
4521
  return rows.map(rowToPrompt);
4453
4522
  }
4454
4523
  function updatePrompt(idOrSlug, input) {
@@ -4464,6 +4533,7 @@ function updatePrompt(idOrSlug, input) {
4464
4533
  description = COALESCE(?, description),
4465
4534
  collection = COALESCE(?, collection),
4466
4535
  tags = COALESCE(?, tags),
4536
+ next_prompt = CASE WHEN ? IS NOT NULL THEN ? ELSE next_prompt END,
4467
4537
  variables = ?,
4468
4538
  is_template = ?,
4469
4539
  version = version + 1,
@@ -4474,6 +4544,8 @@ function updatePrompt(idOrSlug, input) {
4474
4544
  input.description ?? null,
4475
4545
  input.collection ?? null,
4476
4546
  input.tags ? JSON.stringify(input.tags) : null,
4547
+ "next_prompt" in input ? input.next_prompt ?? "" : null,
4548
+ "next_prompt" in input ? input.next_prompt ?? null : null,
4477
4549
  variables,
4478
4550
  is_template,
4479
4551
  prompt.id,
@@ -4496,6 +4568,30 @@ function usePrompt(idOrSlug) {
4496
4568
  const db = getDatabase();
4497
4569
  const prompt = requirePrompt(idOrSlug);
4498
4570
  db.run("UPDATE prompts SET use_count = use_count + 1, last_used_at = datetime('now') WHERE id = ?", [prompt.id]);
4571
+ db.run("INSERT INTO usage_log (id, prompt_id) VALUES (?, ?)", [generateId("UL"), prompt.id]);
4572
+ return requirePrompt(prompt.id);
4573
+ }
4574
+ function getTrending(days = 7, limit = 10) {
4575
+ const db = getDatabase();
4576
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
4577
+ return db.query(`SELECT p.id, p.slug, p.title, COUNT(ul.id) as uses
4578
+ FROM usage_log ul
4579
+ JOIN prompts p ON p.id = ul.prompt_id
4580
+ WHERE ul.used_at >= ?
4581
+ GROUP BY p.id
4582
+ ORDER BY uses DESC
4583
+ LIMIT ?`).all(cutoff, limit);
4584
+ }
4585
+ function setExpiry(idOrSlug, expiresAt) {
4586
+ const db = getDatabase();
4587
+ const prompt = requirePrompt(idOrSlug);
4588
+ db.run("UPDATE prompts SET expires_at = ?, updated_at = datetime('now') WHERE id = ?", [expiresAt, prompt.id]);
4589
+ return requirePrompt(prompt.id);
4590
+ }
4591
+ function setNextPrompt(idOrSlug, nextSlug) {
4592
+ const db = getDatabase();
4593
+ const prompt = requirePrompt(idOrSlug);
4594
+ db.run("UPDATE prompts SET next_prompt = ?, updated_at = datetime('now') WHERE id = ?", [nextSlug, prompt.id]);
4499
4595
  return requirePrompt(prompt.id);
4500
4596
  }
4501
4597
  function pinPrompt(idOrSlug, pinned) {
@@ -4606,6 +4702,52 @@ function registerAgent(name, description) {
4606
4702
  return rowToAgent(db.query("SELECT * FROM agents WHERE id = ?").get(id));
4607
4703
  }
4608
4704
 
4705
+ // src/db/projects.ts
4706
+ function rowToProject(row, promptCount) {
4707
+ return {
4708
+ id: row["id"],
4709
+ name: row["name"],
4710
+ slug: row["slug"],
4711
+ description: row["description"] ?? null,
4712
+ path: row["path"] ?? null,
4713
+ prompt_count: promptCount,
4714
+ created_at: row["created_at"]
4715
+ };
4716
+ }
4717
+ function createProject(input) {
4718
+ const db = getDatabase();
4719
+ const id = generateId("proj");
4720
+ const slug = generateSlug(input.name);
4721
+ db.run(`INSERT INTO projects (id, name, slug, description, path) VALUES (?, ?, ?, ?, ?)`, [id, input.name, slug, input.description ?? null, input.path ?? null]);
4722
+ return getProject(id);
4723
+ }
4724
+ function getProject(idOrSlug) {
4725
+ const db = getDatabase();
4726
+ const id = resolveProject(db, idOrSlug);
4727
+ if (!id)
4728
+ return null;
4729
+ const row = db.query("SELECT * FROM projects WHERE id = ?").get(id);
4730
+ if (!row)
4731
+ return null;
4732
+ const countRow = db.query("SELECT COUNT(*) as n FROM prompts WHERE project_id = ?").get(id);
4733
+ return rowToProject(row, countRow.n);
4734
+ }
4735
+ function listProjects() {
4736
+ const db = getDatabase();
4737
+ const rows = db.query("SELECT * FROM projects ORDER BY name ASC").all();
4738
+ return rows.map((row) => {
4739
+ const countRow = db.query("SELECT COUNT(*) as n FROM prompts WHERE project_id = ?").get(row["id"]);
4740
+ return rowToProject(row, countRow.n);
4741
+ });
4742
+ }
4743
+ function deleteProject(idOrSlug) {
4744
+ const db = getDatabase();
4745
+ const id = resolveProject(db, idOrSlug);
4746
+ if (!id)
4747
+ throw new ProjectNotFoundError(idOrSlug);
4748
+ db.run("DELETE FROM projects WHERE id = ?", [id]);
4749
+ }
4750
+
4609
4751
  // src/lib/search.ts
4610
4752
  function rowToSearchResult(row, snippet) {
4611
4753
  return {
@@ -4620,6 +4762,9 @@ function rowToSearchResult(row, snippet) {
4620
4762
  tags: JSON.parse(row["tags"] || "[]"),
4621
4763
  variables: JSON.parse(row["variables"] || "[]"),
4622
4764
  pinned: Boolean(row["pinned"]),
4765
+ next_prompt: row["next_prompt"] ?? null,
4766
+ expires_at: row["expires_at"] ?? null,
4767
+ project_id: row["project_id"] ?? null,
4623
4768
  is_template: Boolean(row["is_template"]),
4624
4769
  source: row["source"],
4625
4770
  version: row["version"],
@@ -4663,6 +4808,10 @@ function searchPrompts(query, filter = {}) {
4663
4808
  for (const tag of filter.tags)
4664
4809
  params.push(`%"${tag}"%`);
4665
4810
  }
4811
+ if (filter.project_id !== undefined && filter.project_id !== null) {
4812
+ conditions.push("(p.project_id = ? OR p.project_id IS NULL)");
4813
+ params.push(filter.project_id);
4814
+ }
4666
4815
  const where = conditions.length > 0 ? `AND ${conditions.join(" AND ")}` : "";
4667
4816
  const limit = filter.limit ?? 50;
4668
4817
  const offset = filter.offset ?? 0;
@@ -4738,6 +4887,77 @@ function exportToJson(collection) {
4738
4887
  const prompts = listPrompts({ collection, limit: 1e4 });
4739
4888
  return { prompts, exported_at: new Date().toISOString(), collection };
4740
4889
  }
4890
+ function markdownToImportItem(content, filename) {
4891
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n+([\s\S]*)$/);
4892
+ if (!frontmatterMatch) {
4893
+ if (!filename)
4894
+ return null;
4895
+ const title2 = filename.replace(/\.md$/, "").replace(/-/g, " ");
4896
+ return { title: title2, body: content.trim() };
4897
+ }
4898
+ const frontmatter = frontmatterMatch[1] ?? "";
4899
+ const body = (frontmatterMatch[2] ?? "").trim();
4900
+ const get = (key) => {
4901
+ const m = frontmatter.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
4902
+ return m ? (m[1] ?? "").trim() : null;
4903
+ };
4904
+ const title = get("title") ?? (filename?.replace(/\.md$/, "").replace(/-/g, " ") ?? "Untitled");
4905
+ const slug = get("slug") ?? undefined;
4906
+ const collection = get("collection") ?? undefined;
4907
+ const description = get("description") ?? undefined;
4908
+ const tagsStr = get("tags");
4909
+ let tags;
4910
+ if (tagsStr) {
4911
+ const inner = tagsStr.replace(/^\[/, "").replace(/\]$/, "");
4912
+ tags = inner.split(",").map((t) => t.trim()).filter(Boolean);
4913
+ }
4914
+ return { title, slug, body, collection, tags, description };
4915
+ }
4916
+ function scanAndImportSlashCommands(rootDir, changedBy) {
4917
+ const { existsSync: existsSync2, readdirSync, readFileSync } = __require("fs");
4918
+ const { join: join2 } = __require("path");
4919
+ const home = process.env["HOME"] ?? "~";
4920
+ const sources = [
4921
+ { dir: join2(rootDir, ".claude", "commands"), collection: "claude-commands", tags: ["claude", "slash-command"] },
4922
+ { dir: join2(home, ".claude", "commands"), collection: "claude-commands", tags: ["claude", "slash-command"] },
4923
+ { dir: join2(rootDir, ".codex", "skills"), collection: "codex-skills", tags: ["codex", "skill"] },
4924
+ { dir: join2(home, ".codex", "skills"), collection: "codex-skills", tags: ["codex", "skill"] },
4925
+ { dir: join2(rootDir, ".gemini", "extensions"), collection: "gemini-extensions", tags: ["gemini", "extension"] },
4926
+ { dir: join2(home, ".gemini", "extensions"), collection: "gemini-extensions", tags: ["gemini", "extension"] }
4927
+ ];
4928
+ const files = [];
4929
+ const scanned = [];
4930
+ for (const { dir, collection, tags } of sources) {
4931
+ if (!existsSync2(dir))
4932
+ continue;
4933
+ let entries;
4934
+ try {
4935
+ entries = readdirSync(dir);
4936
+ } catch {
4937
+ continue;
4938
+ }
4939
+ for (const entry of entries) {
4940
+ if (!entry.endsWith(".md"))
4941
+ continue;
4942
+ const filePath = join2(dir, entry);
4943
+ try {
4944
+ const content = readFileSync(filePath, "utf-8");
4945
+ files.push({ filename: entry, content, collection, tags });
4946
+ scanned.push({ source: dir, file: entry });
4947
+ } catch {}
4948
+ }
4949
+ }
4950
+ const items = files.map((f) => {
4951
+ const base = markdownToImportItem(f.content, f.filename);
4952
+ if (base)
4953
+ return { ...base, collection: base.collection ?? f.collection, tags: base.tags ?? f.tags };
4954
+ const name = f.filename.replace(/\.md$/, "");
4955
+ const title = name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
4956
+ return { title, slug: name, body: f.content.trim(), collection: f.collection, tags: f.tags };
4957
+ });
4958
+ const imported = importFromJson(items, changedBy);
4959
+ return { scanned, imported };
4960
+ }
4741
4961
 
4742
4962
  // src/lib/mementos.ts
4743
4963
  async function maybeSaveMemento(opts) {
@@ -4763,6 +4983,48 @@ async function maybeSaveMemento(opts) {
4763
4983
  } catch {}
4764
4984
  }
4765
4985
 
4986
+ // src/lib/diff.ts
4987
+ function diffTexts(a, b) {
4988
+ const aLines = a.split(`
4989
+ `);
4990
+ const bLines = b.split(`
4991
+ `);
4992
+ const m = aLines.length;
4993
+ const n = bLines.length;
4994
+ const lcs = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
4995
+ for (let i2 = 1;i2 <= m; i2++) {
4996
+ for (let j2 = 1;j2 <= n; j2++) {
4997
+ 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);
4998
+ }
4999
+ }
5000
+ const trace = [];
5001
+ let i = m, j = n;
5002
+ while (i > 0 || j > 0) {
5003
+ if (i > 0 && j > 0 && aLines[i - 1] === bLines[j - 1]) {
5004
+ trace.unshift({ type: "unchanged", content: aLines[i - 1] ?? "" });
5005
+ i--;
5006
+ j--;
5007
+ } else if (j > 0 && (i === 0 || (lcs[i][j - 1] ?? 0) >= (lcs[i - 1][j] ?? 0))) {
5008
+ trace.unshift({ type: "added", content: bLines[j - 1] ?? "" });
5009
+ j--;
5010
+ } else {
5011
+ trace.unshift({ type: "removed", content: aLines[i - 1] ?? "" });
5012
+ i--;
5013
+ }
5014
+ }
5015
+ return trace;
5016
+ }
5017
+ function formatDiff(lines) {
5018
+ return lines.map((l) => {
5019
+ if (l.type === "added")
5020
+ return `+ ${l.content}`;
5021
+ if (l.type === "removed")
5022
+ return `- ${l.content}`;
5023
+ return ` ${l.content}`;
5024
+ }).join(`
5025
+ `);
5026
+ }
5027
+
4766
5028
  // src/lib/lint.ts
4767
5029
  function lintPrompt(p) {
4768
5030
  const issues = [];
@@ -4797,6 +5059,83 @@ function lintAll(prompts) {
4797
5059
  return prompts.map((p) => ({ prompt: p, issues: lintPrompt(p) })).filter((r) => r.issues.length > 0);
4798
5060
  }
4799
5061
 
5062
+ // src/lib/audit.ts
5063
+ function runAudit() {
5064
+ const db = getDatabase();
5065
+ const issues = [];
5066
+ const orphaned = db.query(`
5067
+ SELECT p.id, p.slug FROM prompts p
5068
+ WHERE p.project_id IS NOT NULL
5069
+ AND NOT EXISTS (SELECT 1 FROM projects pr WHERE pr.id = p.project_id)
5070
+ `).all();
5071
+ for (const p of orphaned) {
5072
+ issues.push({
5073
+ type: "orphaned-project",
5074
+ severity: "error",
5075
+ prompt_id: p.id,
5076
+ slug: p.slug,
5077
+ message: `Prompt "${p.slug}" references a deleted project`
5078
+ });
5079
+ }
5080
+ const emptyCollections = db.query(`
5081
+ SELECT c.name FROM collections c
5082
+ WHERE NOT EXISTS (SELECT 1 FROM prompts p WHERE p.collection = c.name)
5083
+ AND c.name != 'default'
5084
+ `).all();
5085
+ for (const c of emptyCollections) {
5086
+ issues.push({
5087
+ type: "empty-collection",
5088
+ severity: "info",
5089
+ message: `Collection "${c.name}" has no prompts`
5090
+ });
5091
+ }
5092
+ const missingHistory = db.query(`
5093
+ SELECT p.id, p.slug FROM prompts p
5094
+ WHERE NOT EXISTS (SELECT 1 FROM prompt_versions v WHERE v.prompt_id = p.id)
5095
+ `).all();
5096
+ for (const p of missingHistory) {
5097
+ issues.push({
5098
+ type: "missing-version-history",
5099
+ severity: "warn",
5100
+ prompt_id: p.id,
5101
+ slug: p.slug,
5102
+ message: `Prompt "${p.slug}" has no version history entries`
5103
+ });
5104
+ }
5105
+ const slugs = db.query("SELECT id, slug FROM prompts").all();
5106
+ const seen = new Map;
5107
+ for (const { id, slug } of slugs) {
5108
+ const base = slug.replace(/-\d+$/, "");
5109
+ if (seen.has(base) && seen.get(base) !== id) {
5110
+ issues.push({
5111
+ type: "near-duplicate-slug",
5112
+ severity: "info",
5113
+ slug,
5114
+ message: `"${slug}" looks like a duplicate of "${base}" \u2014 consider merging`
5115
+ });
5116
+ } else {
5117
+ seen.set(base, id);
5118
+ }
5119
+ }
5120
+ const now = new Date().toISOString();
5121
+ const expired = db.query(`
5122
+ SELECT id, slug FROM prompts WHERE expires_at IS NOT NULL AND expires_at < ?
5123
+ `).all(now);
5124
+ for (const p of expired) {
5125
+ issues.push({
5126
+ type: "expired",
5127
+ severity: "warn",
5128
+ prompt_id: p.id,
5129
+ slug: p.slug,
5130
+ message: `Prompt "${p.slug}" has expired`
5131
+ });
5132
+ }
5133
+ const errors2 = issues.filter((i) => i.severity === "error").length;
5134
+ const warnings = issues.filter((i) => i.severity === "warn").length;
5135
+ const info = issues.filter((i) => i.severity === "info").length;
5136
+ return { issues, errors: errors2, warnings, info, checked_at: new Date().toISOString() };
5137
+ }
5138
+
4800
5139
  // src/mcp/index.ts
4801
5140
  var server = new McpServer({ name: "open-prompts", version: "0.1.0" });
4802
5141
  function ok(data) {
@@ -4816,11 +5155,19 @@ server.registerTool("prompts_save", {
4816
5155
  tags: exports_external.array(exports_external.string()).optional().describe("Tags for filtering and search"),
4817
5156
  source: exports_external.enum(["manual", "ai-session", "imported"]).optional().describe("Where this prompt came from"),
4818
5157
  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")
5158
+ force: exports_external.boolean().optional().describe("Save even if a similar prompt already exists"),
5159
+ project: exports_external.string().optional().describe("Project name, slug, or ID to scope this prompt to")
4820
5160
  }
4821
5161
  }, async (args) => {
4822
5162
  try {
4823
- const { force, ...input } = args;
5163
+ const { force, project, ...input } = args;
5164
+ if (project) {
5165
+ const db = getDatabase();
5166
+ const pid = resolveProject(db, project);
5167
+ if (!pid)
5168
+ return err(`Project not found: ${project}`);
5169
+ input.project_id = pid;
5170
+ }
4824
5171
  const { prompt, created, duplicate_warning } = upsertPrompt(input, force ?? false);
4825
5172
  return ok({ ...prompt, _created: created, _duplicate_warning: duplicate_warning ?? null });
4826
5173
  } catch (e) {
@@ -4844,9 +5191,19 @@ server.registerTool("prompts_list", {
4844
5191
  is_template: exports_external.boolean().optional(),
4845
5192
  source: exports_external.enum(["manual", "ai-session", "imported"]).optional(),
4846
5193
  limit: exports_external.number().optional().default(50),
4847
- offset: exports_external.number().optional().default(0)
4848
- }
4849
- }, async (args) => ok(listPrompts(args)));
5194
+ offset: exports_external.number().optional().default(0),
5195
+ project: exports_external.string().optional().describe("Project name, slug, or ID \u2014 shows project prompts first, then globals")
5196
+ }
5197
+ }, async ({ project, ...args }) => {
5198
+ if (project) {
5199
+ const db = getDatabase();
5200
+ const pid = resolveProject(db, project);
5201
+ if (!pid)
5202
+ return err(`Project not found: ${project}`);
5203
+ return ok(listPrompts({ ...args, project_id: pid }));
5204
+ }
5205
+ return ok(listPrompts(args));
5206
+ });
4850
5207
  server.registerTool("prompts_delete", {
4851
5208
  description: "Delete a prompt by ID or slug.",
4852
5209
  inputSchema: { id: exports_external.string() }
@@ -4916,9 +5273,19 @@ server.registerTool("prompts_search", {
4916
5273
  tags: exports_external.array(exports_external.string()).optional(),
4917
5274
  is_template: exports_external.boolean().optional(),
4918
5275
  source: exports_external.enum(["manual", "ai-session", "imported"]).optional(),
4919
- limit: exports_external.number().optional().default(20)
4920
- }
4921
- }, async ({ q, ...filter }) => ok(searchPrompts(q, filter)));
5276
+ limit: exports_external.number().optional().default(20),
5277
+ project: exports_external.string().optional().describe("Project name, slug, or ID to scope search")
5278
+ }
5279
+ }, async ({ q, project, ...filter }) => {
5280
+ if (project) {
5281
+ const db = getDatabase();
5282
+ const pid = resolveProject(db, project);
5283
+ if (!pid)
5284
+ return err(`Project not found: ${project}`);
5285
+ return ok(searchPrompts(q, { ...filter, project_id: pid }));
5286
+ }
5287
+ return ok(searchPrompts(q, filter));
5288
+ });
4922
5289
  server.registerTool("prompts_similar", {
4923
5290
  description: "Find prompts similar to a given prompt (by tag overlap and collection).",
4924
5291
  inputSchema: {
@@ -5002,6 +5369,17 @@ server.registerTool("prompts_import", {
5002
5369
  const results = importFromJson(prompts, changed_by);
5003
5370
  return ok(results);
5004
5371
  });
5372
+ server.registerTool("prompts_import_slash_commands", {
5373
+ description: "Auto-scan .claude/commands, .codex/skills, .gemini/extensions (both project and home dir) and import all .md files as prompts.",
5374
+ inputSchema: {
5375
+ dir: exports_external.string().optional().describe("Root directory to scan (default: cwd)"),
5376
+ changed_by: exports_external.string().optional()
5377
+ }
5378
+ }, async ({ dir, changed_by }) => {
5379
+ const rootDir = dir ?? process.cwd();
5380
+ const result = scanAndImportSlashCommands(rootDir, changed_by);
5381
+ return ok(result);
5382
+ });
5005
5383
  server.registerTool("prompts_update", {
5006
5384
  description: "Update an existing prompt's fields.",
5007
5385
  inputSchema: {
@@ -5056,10 +5434,20 @@ server.registerTool("prompts_save_from_session", {
5056
5434
  tags: exports_external.array(exports_external.string()).optional().describe("Relevant tags extracted from the prompt context"),
5057
5435
  collection: exports_external.string().optional().describe("Collection to save into (default: 'sessions')"),
5058
5436
  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")
5437
+ agent: exports_external.string().optional().describe("Agent name saving this prompt"),
5438
+ project: exports_external.string().optional().describe("Project name, slug, or ID to scope this prompt to"),
5439
+ pin: exports_external.boolean().optional().describe("Pin the prompt immediately so it surfaces first in all lists")
5060
5440
  }
5061
- }, async ({ title, body, slug, tags, collection, description, agent }) => {
5441
+ }, async ({ title, body, slug, tags, collection, description, agent, project, pin }) => {
5062
5442
  try {
5443
+ let project_id;
5444
+ if (project) {
5445
+ const db = getDatabase();
5446
+ const pid = resolveProject(db, project);
5447
+ if (!pid)
5448
+ return err(`Project not found: ${project}`);
5449
+ project_id = pid;
5450
+ }
5063
5451
  const { prompt, created } = upsertPrompt({
5064
5452
  title,
5065
5453
  body,
@@ -5068,9 +5456,121 @@ server.registerTool("prompts_save_from_session", {
5068
5456
  collection: collection ?? "sessions",
5069
5457
  description,
5070
5458
  source: "ai-session",
5071
- changed_by: agent
5459
+ changed_by: agent,
5460
+ project_id
5072
5461
  });
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}".` });
5462
+ if (pin)
5463
+ pinPrompt(prompt.id, true);
5464
+ const final = pin ? { ...prompt, pinned: true } : prompt;
5465
+ return ok({ ...final, _created: created, _tip: created ? `Saved as "${prompt.slug}". Use prompts_use("${prompt.slug}") to retrieve it.` : `Updated existing prompt "${prompt.slug}".` });
5466
+ } catch (e) {
5467
+ return err(e instanceof Error ? e.message : String(e));
5468
+ }
5469
+ });
5470
+ server.registerTool("prompts_audit", {
5471
+ description: "Run a full audit: orphaned project refs, empty collections, missing version history, near-duplicate slugs, expired prompts.",
5472
+ inputSchema: {}
5473
+ }, async () => ok(runAudit()));
5474
+ server.registerTool("prompts_unused", {
5475
+ description: "List prompts with use_count = 0 \u2014 never used. Good for library cleanup.",
5476
+ inputSchema: { collection: exports_external.string().optional(), limit: exports_external.number().optional().default(50) }
5477
+ }, async ({ collection, limit }) => {
5478
+ const all = listPrompts({ collection, limit: 1e4 });
5479
+ const unused = all.filter((p) => p.use_count === 0).slice(0, limit);
5480
+ return ok({ unused, count: unused.length });
5481
+ });
5482
+ server.registerTool("prompts_trending", {
5483
+ description: "Get most-used prompts in the last N days based on per-use log.",
5484
+ inputSchema: {
5485
+ days: exports_external.number().optional().default(7),
5486
+ limit: exports_external.number().optional().default(10)
5487
+ }
5488
+ }, async ({ days, limit }) => ok(getTrending(days, limit)));
5489
+ server.registerTool("prompts_set_expiry", {
5490
+ description: "Set or clear an expiry date on a prompt. Pass expires_at=null to clear.",
5491
+ inputSchema: {
5492
+ id: exports_external.string(),
5493
+ expires_at: exports_external.string().nullable().describe("ISO date string (e.g. 2026-12-31) or null to clear")
5494
+ }
5495
+ }, async ({ id, expires_at }) => {
5496
+ try {
5497
+ return ok(setExpiry(id, expires_at));
5498
+ } catch (e) {
5499
+ return err(e instanceof Error ? e.message : String(e));
5500
+ }
5501
+ });
5502
+ server.registerTool("prompts_duplicate", {
5503
+ description: "Clone a prompt with a new slug. Copies body, tags, collection, description. Version resets to 1.",
5504
+ inputSchema: {
5505
+ id: exports_external.string(),
5506
+ slug: exports_external.string().optional().describe("New slug (auto-generated if omitted)"),
5507
+ title: exports_external.string().optional().describe("New title (defaults to 'Copy of <original>')")
5508
+ }
5509
+ }, async ({ id, slug, title }) => {
5510
+ try {
5511
+ const source = getPrompt(id);
5512
+ if (!source)
5513
+ return err(`Prompt not found: ${id}`);
5514
+ const { prompt } = upsertPrompt({
5515
+ title: title ?? `Copy of ${source.title}`,
5516
+ slug,
5517
+ body: source.body,
5518
+ description: source.description ?? undefined,
5519
+ collection: source.collection,
5520
+ tags: source.tags,
5521
+ source: "manual"
5522
+ });
5523
+ return ok(prompt);
5524
+ } catch (e) {
5525
+ return err(e instanceof Error ? e.message : String(e));
5526
+ }
5527
+ });
5528
+ server.registerTool("prompts_diff", {
5529
+ description: "Show a line diff between two versions of a prompt body. v2 defaults to current version.",
5530
+ inputSchema: {
5531
+ id: exports_external.string(),
5532
+ v1: exports_external.number().describe("First version number"),
5533
+ v2: exports_external.number().optional().describe("Second version (default: current)")
5534
+ }
5535
+ }, async ({ id, v1, v2 }) => {
5536
+ try {
5537
+ const prompt = getPrompt(id);
5538
+ if (!prompt)
5539
+ return err(`Prompt not found: ${id}`);
5540
+ const versions = listVersions(prompt.id);
5541
+ const versionA = versions.find((v) => v.version === v1);
5542
+ if (!versionA)
5543
+ return err(`Version ${v1} not found`);
5544
+ const bodyB = v2 ? versions.find((v) => v.version === v2)?.body ?? null : prompt.body;
5545
+ if (bodyB === null)
5546
+ return err(`Version ${v2} not found`);
5547
+ const lines = diffTexts(versionA.body, bodyB);
5548
+ return ok({ lines, formatted: formatDiff(lines), v1, v2: v2 ?? prompt.version });
5549
+ } catch (e) {
5550
+ return err(e instanceof Error ? e.message : String(e));
5551
+ }
5552
+ });
5553
+ server.registerTool("prompts_chain", {
5554
+ 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.",
5555
+ inputSchema: {
5556
+ id: exports_external.string().describe("Prompt ID or slug"),
5557
+ next_prompt: exports_external.string().nullable().optional().describe("Slug of the next prompt in the chain, or null to clear")
5558
+ }
5559
+ }, async ({ id, next_prompt }) => {
5560
+ try {
5561
+ if (next_prompt !== undefined) {
5562
+ const p = setNextPrompt(id, next_prompt ?? null);
5563
+ return ok(p);
5564
+ }
5565
+ const chain = [];
5566
+ let cur = getPrompt(id);
5567
+ const seen = new Set;
5568
+ while (cur && !seen.has(cur.id)) {
5569
+ chain.push({ id: cur.id, slug: cur.slug, title: cur.title });
5570
+ seen.add(cur.id);
5571
+ cur = cur.next_prompt ? getPrompt(cur.next_prompt) : null;
5572
+ }
5573
+ return ok(chain);
5074
5574
  } catch (e) {
5075
5575
  return err(e instanceof Error ? e.message : String(e));
5076
5576
  }
@@ -5131,5 +5631,43 @@ server.registerTool("prompts_stats", {
5131
5631
  description: "Get usage statistics: most used prompts, recently used, counts by collection and source.",
5132
5632
  inputSchema: {}
5133
5633
  }, async () => ok(getPromptStats()));
5634
+ server.registerTool("prompts_project_create", {
5635
+ description: "Create a new project to scope prompts.",
5636
+ inputSchema: {
5637
+ name: exports_external.string().describe("Project name"),
5638
+ description: exports_external.string().optional().describe("Short description"),
5639
+ path: exports_external.string().optional().describe("Optional filesystem path this project maps to")
5640
+ }
5641
+ }, async ({ name, description, path }) => {
5642
+ try {
5643
+ return ok(createProject({ name, description, path }));
5644
+ } catch (e) {
5645
+ return err(e instanceof Error ? e.message : String(e));
5646
+ }
5647
+ });
5648
+ server.registerTool("prompts_project_list", {
5649
+ description: "List all projects with prompt counts.",
5650
+ inputSchema: {}
5651
+ }, async () => ok(listProjects()));
5652
+ server.registerTool("prompts_project_get", {
5653
+ description: "Get a project by ID, slug, or name.",
5654
+ inputSchema: { id: exports_external.string().describe("Project ID, slug, or name") }
5655
+ }, async ({ id }) => {
5656
+ const project = getProject(id);
5657
+ if (!project)
5658
+ return err(`Project not found: ${id}`);
5659
+ return ok(project);
5660
+ });
5661
+ server.registerTool("prompts_project_delete", {
5662
+ description: "Delete a project. Prompts in the project become global (project_id set to null).",
5663
+ inputSchema: { id: exports_external.string().describe("Project ID, slug, or name") }
5664
+ }, async ({ id }) => {
5665
+ try {
5666
+ deleteProject(id);
5667
+ return ok({ deleted: true, id });
5668
+ } catch (e) {
5669
+ return err(e instanceof Error ? e.message : String(e));
5670
+ }
5671
+ });
5134
5672
  var transport = new StdioServerTransport;
5135
5673
  await server.connect(transport);