@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/cli/index.js +907 -93
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/prompts.d.ts +12 -1
- package/dist/db/prompts.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +190 -2
- package/dist/lib/audit.d.ts +16 -0
- package/dist/lib/audit.d.ts.map +1 -0
- package/dist/lib/completion.d.ts +3 -0
- package/dist/lib/completion.d.ts.map +1 -0
- package/dist/lib/diff.d.ts +8 -0
- package/dist/lib/diff.d.ts.map +1 -0
- package/dist/lib/importer.d.ts +8 -0
- package/dist/lib/importer.d.ts.map +1 -1
- package/dist/lib/search.d.ts +3 -1
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/mcp/index.js +562 -24
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +192 -10
- package/dist/types/index.d.ts +52 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
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 ??
|
|
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 ??
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
5356
|
+
project_id = pid;
|
|
4963
5357
|
}
|
|
4964
|
-
|
|
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(
|
|
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: "
|
|
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(
|
|
5036
|
-
project: exports_external.string().optional()
|
|
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
|
-
|
|
5456
|
+
project_id = pid;
|
|
5045
5457
|
}
|
|
5046
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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 =
|
|
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", {
|