@hasna/prompts 0.2.1 → 0.2.2

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 CHANGED
@@ -2187,6 +2187,21 @@ function runMigrations(db) {
2187
2187
  name: "003_pinned",
2188
2188
  sql: `ALTER TABLE prompts ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;`
2189
2189
  },
2190
+ {
2191
+ name: "004_projects",
2192
+ sql: `
2193
+ CREATE TABLE IF NOT EXISTS projects (
2194
+ id TEXT PRIMARY KEY,
2195
+ name TEXT NOT NULL UNIQUE,
2196
+ slug TEXT NOT NULL UNIQUE,
2197
+ description TEXT,
2198
+ path TEXT,
2199
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2200
+ );
2201
+ ALTER TABLE prompts ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
2202
+ CREATE INDEX IF NOT EXISTS idx_prompts_project_id ON prompts(project_id);
2203
+ `
2204
+ },
2190
2205
  {
2191
2206
  name: "002_fts5",
2192
2207
  sql: `
@@ -2227,6 +2242,24 @@ function runMigrations(db) {
2227
2242
  db.run("INSERT INTO _migrations (name) VALUES (?)", [migration.name]);
2228
2243
  }
2229
2244
  }
2245
+ function resolveProject(db, idOrSlug) {
2246
+ const byId = db.query("SELECT id FROM projects WHERE id = ?").get(idOrSlug);
2247
+ if (byId)
2248
+ return byId.id;
2249
+ const bySlug = db.query("SELECT id FROM projects WHERE slug = ?").get(idOrSlug);
2250
+ if (bySlug)
2251
+ return bySlug.id;
2252
+ const byName = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(idOrSlug.toLowerCase());
2253
+ if (byName)
2254
+ return byName.id;
2255
+ const byPrefix = db.query("SELECT id FROM projects WHERE id LIKE ? LIMIT 2").all(`${idOrSlug}%`);
2256
+ if (byPrefix.length === 1 && byPrefix[0])
2257
+ return byPrefix[0].id;
2258
+ const bySlugPrefix = db.query("SELECT id FROM projects WHERE slug LIKE ? LIMIT 2").all(`${idOrSlug}%`);
2259
+ if (bySlugPrefix.length === 1 && bySlugPrefix[0])
2260
+ return bySlugPrefix[0].id;
2261
+ return null;
2262
+ }
2230
2263
  function hasFts(db) {
2231
2264
  return db.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='prompts_fts'").get() !== null;
2232
2265
  }
@@ -2435,6 +2468,12 @@ class DuplicateSlugError extends Error {
2435
2468
  this.name = "DuplicateSlugError";
2436
2469
  }
2437
2470
  }
2471
+ class ProjectNotFoundError extends Error {
2472
+ constructor(id) {
2473
+ super(`Project not found: ${id}`);
2474
+ this.name = "ProjectNotFoundError";
2475
+ }
2476
+ }
2438
2477
 
2439
2478
  // src/db/prompts.ts
2440
2479
  function rowToPrompt(row) {
@@ -2449,6 +2488,7 @@ function rowToPrompt(row) {
2449
2488
  tags: JSON.parse(row["tags"] || "[]"),
2450
2489
  variables: JSON.parse(row["variables"] || "[]"),
2451
2490
  pinned: Boolean(row["pinned"]),
2491
+ project_id: row["project_id"] ?? null,
2452
2492
  is_template: Boolean(row["is_template"]),
2453
2493
  source: row["source"],
2454
2494
  version: row["version"],
@@ -2472,11 +2512,12 @@ function createPrompt(input) {
2472
2512
  ensureCollection(collection);
2473
2513
  const tags = JSON.stringify(input.tags || []);
2474
2514
  const source = input.source || "manual";
2515
+ const project_id = input.project_id ?? null;
2475
2516
  const vars = extractVariables(input.body);
2476
2517
  const variables = JSON.stringify(vars.map((v) => ({ name: v, required: true })));
2477
2518
  const is_template = vars.length > 0 ? 1 : 0;
2478
- db.run(`INSERT INTO prompts (id, name, slug, title, body, description, collection, tags, variables, is_template, source)
2479
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, name, slug, input.title, input.body, input.description ?? null, collection, tags, variables, is_template, source]);
2519
+ db.run(`INSERT INTO prompts (id, name, slug, title, body, description, collection, tags, variables, is_template, source, project_id)
2520
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, name, slug, input.title, input.body, input.description ?? null, collection, tags, variables, is_template, source, project_id]);
2480
2521
  db.run(`INSERT INTO prompt_versions (id, prompt_id, body, version, changed_by)
2481
2522
  VALUES (?, ?, ?, 1, ?)`, [generateId("VER"), id, input.body, input.changed_by ?? null]);
2482
2523
  return getPrompt(id);
@@ -2520,10 +2561,16 @@ function listPrompts(filter = {}) {
2520
2561
  params.push(`%"${tag}"%`);
2521
2562
  }
2522
2563
  }
2564
+ let orderBy = "pinned DESC, use_count DESC, updated_at DESC";
2565
+ if (filter.project_id !== undefined && filter.project_id !== null) {
2566
+ conditions.push("(project_id = ? OR project_id IS NULL)");
2567
+ params.push(filter.project_id);
2568
+ orderBy = `(CASE WHEN project_id = '${filter.project_id}' THEN 0 ELSE 1 END), pinned DESC, use_count DESC, updated_at DESC`;
2569
+ }
2523
2570
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2524
2571
  const limit = filter.limit ?? 100;
2525
2572
  const offset = filter.offset ?? 0;
2526
- const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY pinned DESC, use_count DESC, updated_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
2573
+ const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
2527
2574
  return rows.map(rowToPrompt);
2528
2575
  }
2529
2576
  function updatePrompt(idOrSlug, input) {
@@ -2656,6 +2703,52 @@ function restoreVersion(promptId, version, changedBy) {
2656
2703
  VALUES (?, ?, ?, ?, ?)`, [generateId("VER"), promptId, ver.body, newVersion, changedBy ?? null]);
2657
2704
  }
2658
2705
 
2706
+ // src/db/projects.ts
2707
+ function rowToProject(row, promptCount) {
2708
+ return {
2709
+ id: row["id"],
2710
+ name: row["name"],
2711
+ slug: row["slug"],
2712
+ description: row["description"] ?? null,
2713
+ path: row["path"] ?? null,
2714
+ prompt_count: promptCount,
2715
+ created_at: row["created_at"]
2716
+ };
2717
+ }
2718
+ function createProject(input) {
2719
+ const db = getDatabase();
2720
+ const id = generateId("proj");
2721
+ const slug = generateSlug(input.name);
2722
+ db.run(`INSERT INTO projects (id, name, slug, description, path) VALUES (?, ?, ?, ?, ?)`, [id, input.name, slug, input.description ?? null, input.path ?? null]);
2723
+ return getProject(id);
2724
+ }
2725
+ function getProject(idOrSlug) {
2726
+ const db = getDatabase();
2727
+ const id = resolveProject(db, idOrSlug);
2728
+ if (!id)
2729
+ return null;
2730
+ const row = db.query("SELECT * FROM projects WHERE id = ?").get(id);
2731
+ if (!row)
2732
+ return null;
2733
+ const countRow = db.query("SELECT COUNT(*) as n FROM prompts WHERE project_id = ?").get(id);
2734
+ return rowToProject(row, countRow.n);
2735
+ }
2736
+ function listProjects() {
2737
+ const db = getDatabase();
2738
+ const rows = db.query("SELECT * FROM projects ORDER BY name ASC").all();
2739
+ return rows.map((row) => {
2740
+ const countRow = db.query("SELECT COUNT(*) as n FROM prompts WHERE project_id = ?").get(row["id"]);
2741
+ return rowToProject(row, countRow.n);
2742
+ });
2743
+ }
2744
+ function deleteProject(idOrSlug) {
2745
+ const db = getDatabase();
2746
+ const id = resolveProject(db, idOrSlug);
2747
+ if (!id)
2748
+ throw new ProjectNotFoundError(idOrSlug);
2749
+ db.run("DELETE FROM projects WHERE id = ?", [id]);
2750
+ }
2751
+
2659
2752
  // src/lib/search.ts
2660
2753
  function rowToSearchResult(row, snippet) {
2661
2754
  return {
@@ -2670,6 +2763,7 @@ function rowToSearchResult(row, snippet) {
2670
2763
  tags: JSON.parse(row["tags"] || "[]"),
2671
2764
  variables: JSON.parse(row["variables"] || "[]"),
2672
2765
  pinned: Boolean(row["pinned"]),
2766
+ project_id: row["project_id"] ?? null,
2673
2767
  is_template: Boolean(row["is_template"]),
2674
2768
  source: row["source"],
2675
2769
  version: row["version"],
@@ -2713,6 +2807,10 @@ function searchPrompts(query, filter = {}) {
2713
2807
  for (const tag of filter.tags)
2714
2808
  params.push(`%"${tag}"%`);
2715
2809
  }
2810
+ if (filter.project_id !== undefined && filter.project_id !== null) {
2811
+ conditions.push("(p.project_id = ? OR p.project_id IS NULL)");
2812
+ params.push(filter.project_id);
2813
+ }
2716
2814
  const where = conditions.length > 0 ? `AND ${conditions.join(" AND ")}` : "";
2717
2815
  const limit = filter.limit ?? 50;
2718
2816
  const offset = filter.offset ?? 0;
@@ -2806,10 +2904,17 @@ function lintAll(prompts) {
2806
2904
  // src/cli/index.tsx
2807
2905
  var require2 = createRequire(import.meta.url);
2808
2906
  var pkg = require2("../../package.json");
2809
- var program2 = new Command().name("prompts").version(pkg.version).description("Reusable prompt library \u2014 save, search, render prompts from any AI session").option("--json", "Output as JSON");
2907
+ var program2 = new Command().name("prompts").version(pkg.version).description("Reusable prompt library \u2014 save, search, render prompts from any AI session").option("--json", "Output as JSON").option("--project <name>", "Active project (name, slug, or ID) for scoped operations");
2810
2908
  function isJson() {
2811
2909
  return Boolean(program2.opts()["json"]);
2812
2910
  }
2911
+ function getActiveProjectId() {
2912
+ const projectName = program2.opts()["project"] ?? process.env["PROMPTS_PROJECT"];
2913
+ if (!projectName)
2914
+ return null;
2915
+ const db = getDatabase();
2916
+ return resolveProject(db, projectName);
2917
+ }
2813
2918
  function output(data) {
2814
2919
  if (isJson()) {
2815
2920
  console.log(JSON.stringify(data, null, 2));
@@ -2846,6 +2951,7 @@ program2.command("save <title>").description("Save a new prompt (or update exist
2846
2951
  }
2847
2952
  if (!body)
2848
2953
  handleError("No body provided. Use --body, --file, or pipe via stdin.");
2954
+ const project_id = getActiveProjectId();
2849
2955
  const { prompt, created, duplicate_warning } = upsertPrompt({
2850
2956
  title,
2851
2957
  body,
@@ -2854,7 +2960,8 @@ program2.command("save <title>").description("Save a new prompt (or update exist
2854
2960
  collection: opts["collection"],
2855
2961
  tags: opts["tags"] ? opts["tags"].split(",").map((t) => t.trim()) : [],
2856
2962
  source: opts["source"] || "manual",
2857
- changed_by: opts["agent"]
2963
+ changed_by: opts["agent"],
2964
+ project_id
2858
2965
  }, Boolean(opts["force"]));
2859
2966
  if (duplicate_warning && !isJson()) {
2860
2967
  console.warn(chalk.yellow(`Warning: ${duplicate_warning}`));
@@ -2899,11 +3006,13 @@ program2.command("get <id>").description("Get prompt details without incrementin
2899
3006
  });
2900
3007
  program2.command("list").description("List prompts").option("-c, --collection <name>", "Filter by collection").option("-t, --tags <tags>", "Filter by tags (comma-separated)").option("--templates", "Show only templates").option("--recent", "Sort by recently used").option("-n, --limit <n>", "Max results", "50").action((opts) => {
2901
3008
  try {
3009
+ const project_id = getActiveProjectId();
2902
3010
  let prompts = listPrompts({
2903
3011
  collection: opts["collection"],
2904
3012
  tags: opts["tags"] ? opts["tags"].split(",").map((t) => t.trim()) : undefined,
2905
3013
  is_template: opts["templates"] ? true : undefined,
2906
- limit: parseInt(opts["limit"]) || 50
3014
+ limit: parseInt(opts["limit"]) || 50,
3015
+ ...project_id !== null ? { project_id } : {}
2907
3016
  });
2908
3017
  if (opts["recent"]) {
2909
3018
  prompts = prompts.filter((p) => p.last_used_at !== null).sort((a, b) => (b.last_used_at ?? "").localeCompare(a.last_used_at ?? ""));
@@ -2924,10 +3033,12 @@ ${prompts.length} prompt(s)`));
2924
3033
  });
2925
3034
  program2.command("search <query>").description("Full-text search across prompts (FTS5)").option("-c, --collection <name>").option("-t, --tags <tags>").option("-n, --limit <n>", "Max results", "20").action((query, opts) => {
2926
3035
  try {
3036
+ const project_id = getActiveProjectId();
2927
3037
  const results = searchPrompts(query, {
2928
3038
  collection: opts["collection"],
2929
3039
  tags: opts["tags"] ? opts["tags"].split(",").map((t) => t.trim()) : undefined,
2930
- limit: parseInt(opts["limit"] ?? "20") || 20
3040
+ limit: parseInt(opts["limit"] ?? "20") || 20,
3041
+ ...project_id !== null ? { project_id } : {}
2931
3042
  });
2932
3043
  if (isJson()) {
2933
3044
  output(results);
@@ -3316,4 +3427,77 @@ program2.command("copy <id>").description("Copy prompt body to clipboard and inc
3316
3427
  handleError(e);
3317
3428
  }
3318
3429
  });
3430
+ var projectCmd = program2.command("project").description("Manage projects");
3431
+ projectCmd.command("create <name>").description("Create a new project").option("-d, --description <desc>", "Short description").option("--path <path>", "Filesystem path this project maps to").action((name, opts) => {
3432
+ try {
3433
+ const project = createProject({ name, description: opts["description"], path: opts["path"] });
3434
+ if (isJson())
3435
+ output(project);
3436
+ else {
3437
+ console.log(`${chalk.green("Created")} project ${chalk.bold(project.name)} \u2014 ${chalk.gray(project.slug)}`);
3438
+ if (project.description)
3439
+ console.log(chalk.gray(` ${project.description}`));
3440
+ }
3441
+ } catch (e) {
3442
+ handleError(e);
3443
+ }
3444
+ });
3445
+ projectCmd.command("list").description("List all projects").action(() => {
3446
+ try {
3447
+ const projects = listProjects();
3448
+ if (isJson()) {
3449
+ output(projects);
3450
+ return;
3451
+ }
3452
+ if (projects.length === 0) {
3453
+ console.log(chalk.gray("No projects."));
3454
+ return;
3455
+ }
3456
+ for (const p of projects) {
3457
+ console.log(`${chalk.bold(p.name)} ${chalk.gray(p.slug)} ${chalk.cyan(`${p.prompt_count} prompt(s)`)}`);
3458
+ if (p.description)
3459
+ console.log(chalk.gray(` ${p.description}`));
3460
+ }
3461
+ } catch (e) {
3462
+ handleError(e);
3463
+ }
3464
+ });
3465
+ projectCmd.command("get <id>").description("Get project details").action((id) => {
3466
+ try {
3467
+ const project = getProject(id);
3468
+ if (!project)
3469
+ handleError(`Project not found: ${id}`);
3470
+ output(isJson() ? project : `${chalk.bold(project.name)} ${chalk.gray(project.slug)} ${chalk.cyan(`${project.prompt_count} prompt(s)`)}`);
3471
+ } catch (e) {
3472
+ handleError(e);
3473
+ }
3474
+ });
3475
+ projectCmd.command("delete <id>").description("Delete a project (prompts become global)").option("-y, --yes", "Skip confirmation").action(async (id, opts) => {
3476
+ try {
3477
+ const project = getProject(id);
3478
+ if (!project)
3479
+ handleError(`Project not found: ${id}`);
3480
+ if (!opts.yes && !isJson()) {
3481
+ const { createInterface } = await import("readline");
3482
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
3483
+ await new Promise((resolve) => {
3484
+ rl.question(chalk.yellow(`Delete project "${project.name}"? Prompts will become global. [y/N] `), (ans) => {
3485
+ rl.close();
3486
+ if (ans.toLowerCase() !== "y") {
3487
+ console.log("Cancelled.");
3488
+ process.exit(0);
3489
+ }
3490
+ resolve();
3491
+ });
3492
+ });
3493
+ }
3494
+ deleteProject(id);
3495
+ if (isJson())
3496
+ output({ deleted: true, id: project.id });
3497
+ else
3498
+ console.log(chalk.red(`Deleted project ${project.name}`));
3499
+ } catch (e) {
3500
+ handleError(e);
3501
+ }
3502
+ });
3319
3503
  program2.parse();
@@ -3,6 +3,7 @@ export declare function getDbPath(): string;
3
3
  export declare function getDatabase(): Database;
4
4
  export declare function closeDatabase(): void;
5
5
  export declare function resetDatabase(): void;
6
+ export declare function resolveProject(db: Database, idOrSlug: string): string | null;
6
7
  export declare function hasFts(db: Database): boolean;
7
8
  export declare function resolvePrompt(db: Database, idOrSlug: string): string | null;
8
9
  //# sourceMappingURL=database.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAMrC,wBAAgB,SAAS,IAAI,MAAM,CAsBlC;AAED,wBAAgB,WAAW,IAAI,QAAQ,CAmBtC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAKpC;AAGD,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAsHD,wBAAgB,MAAM,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAM5C;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkC3E"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAMrC,wBAAgB,SAAS,IAAI,MAAM,CAsBlC;AAED,wBAAgB,WAAW,IAAI,QAAQ,CAmBtC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAKpC;AAGD,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAqID,wBAAgB,cAAc,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA0B5E;AAED,wBAAgB,MAAM,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAM5C;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkC3E"}
@@ -0,0 +1,10 @@
1
+ import type { Project } from "../types/index.js";
2
+ export declare function createProject(input: {
3
+ name: string;
4
+ description?: string;
5
+ path?: string;
6
+ }): Project;
7
+ export declare function getProject(idOrSlug: string): Project | null;
8
+ export declare function listProjects(): Project[];
9
+ export declare function deleteProject(idOrSlug: string): void;
10
+ //# sourceMappingURL=projects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/db/projects.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAehD,wBAAgB,aAAa,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAWnG;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAU3D;AAED,wBAAgB,YAAY,IAAI,OAAO,EAAE,CAOxC;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAKpD"}
@@ -1 +1 @@
1
- {"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../src/db/prompts.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,MAAM,EACN,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EAGlB,MAAM,mBAAmB,CAAA;AA0B1B,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAyC7D;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOzD;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAItD;AAED,wBAAgB,WAAW,CAAC,MAAM,GAAE,iBAAsB,GAAG,MAAM,EAAE,CAmCpE;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,MAAM,CA8C/E;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAInD;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQlD;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAKnE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,EAAE,KAAK,UAAQ,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAE,CA6BtI;AAED,wBAAgB,cAAc;;;;;YAOJ,MAAM;cAAQ,MAAM;cAAQ,MAAM;eAAS,MAAM;mBAAa,MAAM;;;YAGpE,MAAM;cAAQ,MAAM;cAAQ,MAAM;eAAS,MAAM;sBAAgB,MAAM;;;oBAG/D,MAAM;eAAS,MAAM;;;gBAGzB,MAAM;eAAS,MAAM;;EAGlD"}
1
+ {"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../src/db/prompts.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,MAAM,EACN,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EAGlB,MAAM,mBAAmB,CAAA;AA2B1B,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,CA0C7D;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOzD;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAItD;AAED,wBAAgB,WAAW,CAAC,MAAM,GAAE,iBAAsB,GAAG,MAAM,EAAE,CA4CpE;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,MAAM,CA8C/E;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAInD;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQlD;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAKnE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,EAAE,KAAK,UAAQ,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAE,CA6BtI;AAED,wBAAgB,cAAc;;;;;YAOJ,MAAM;cAAQ,MAAM;cAAQ,MAAM;eAAS,MAAM;mBAAa,MAAM;;;YAGpE,MAAM;cAAQ,MAAM;cAAQ,MAAM;eAAS,MAAM;sBAAgB,MAAM;;;oBAG/D,MAAM;eAAS,MAAM;;;gBAGzB,MAAM;eAAS,MAAM;;EAGlD"}
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { listVersions, getVersion, restoreVersion } from "./db/versions.js";
3
3
  export { listCollections, getCollection, ensureCollection, movePrompt } from "./db/collections.js";
4
4
  export { registerAgent, listAgents } from "./db/agents.js";
5
5
  export { getDatabase, getDbPath } from "./db/database.js";
6
+ export { createProject, getProject, listProjects, deleteProject } from "./db/projects.js";
6
7
  export { searchPrompts, findSimilar } from "./lib/search.js";
7
8
  export { extractVariables, extractVariableInfo, renderTemplate, validateVars } from "./lib/template.js";
8
9
  export type { VariableInfo } from "./lib/template.js";
@@ -10,6 +11,6 @@ export { importFromJson, exportToJson } from "./lib/importer.js";
10
11
  export { findDuplicates } from "./lib/duplicates.js";
11
12
  export type { DuplicateMatch } from "./lib/duplicates.js";
12
13
  export { generateSlug, uniqueSlug, generatePromptId } from "./lib/ids.js";
13
- export type { Prompt, PromptVersion, Collection, Agent, TemplateVariable, PromptSource, CreatePromptInput, UpdatePromptInput, ListPromptsFilter, SearchResult, RenderResult, PromptStats, } from "./types/index.js";
14
- export { PromptNotFoundError, VersionConflictError, DuplicateSlugError, TemplateRenderError, } from "./types/index.js";
14
+ export type { Prompt, PromptVersion, Collection, Agent, Project, TemplateVariable, PromptSource, CreatePromptInput, UpdatePromptInput, ListPromptsFilter, SearchResult, RenderResult, PromptStats, } from "./types/index.js";
15
+ export { PromptNotFoundError, VersionConflictError, DuplicateSlugError, TemplateRenderError, ProjectNotFoundError, } from "./types/index.js";
15
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AACrK,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAC3E,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAClG,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAC1D,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAGzD,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAG5D,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACvG,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGrD,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAGzD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAGzE,YAAY,EACV,MAAM,EACN,aAAa,EACb,UAAU,EACV,KAAK,EACL,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,WAAW,GACZ,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AACrK,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAC3E,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAClG,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAC1D,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACzD,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAGzF,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAG5D,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACvG,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGrD,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAGzD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAGzE,YAAY,EACV,MAAM,EACN,aAAa,EACb,UAAU,EACV,KAAK,EACL,OAAO,EACP,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,WAAW,GACZ,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,kBAAkB,CAAA"}
package/dist/index.js CHANGED
@@ -113,6 +113,21 @@ function runMigrations(db) {
113
113
  name: "003_pinned",
114
114
  sql: `ALTER TABLE prompts ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;`
115
115
  },
116
+ {
117
+ name: "004_projects",
118
+ sql: `
119
+ CREATE TABLE IF NOT EXISTS projects (
120
+ id TEXT PRIMARY KEY,
121
+ name TEXT NOT NULL UNIQUE,
122
+ slug TEXT NOT NULL UNIQUE,
123
+ description TEXT,
124
+ path TEXT,
125
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
126
+ );
127
+ ALTER TABLE prompts ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
128
+ CREATE INDEX IF NOT EXISTS idx_prompts_project_id ON prompts(project_id);
129
+ `
130
+ },
116
131
  {
117
132
  name: "002_fts5",
118
133
  sql: `
@@ -153,6 +168,24 @@ function runMigrations(db) {
153
168
  db.run("INSERT INTO _migrations (name) VALUES (?)", [migration.name]);
154
169
  }
155
170
  }
171
+ function resolveProject(db, idOrSlug) {
172
+ const byId = db.query("SELECT id FROM projects WHERE id = ?").get(idOrSlug);
173
+ if (byId)
174
+ return byId.id;
175
+ const bySlug = db.query("SELECT id FROM projects WHERE slug = ?").get(idOrSlug);
176
+ if (bySlug)
177
+ return bySlug.id;
178
+ const byName = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(idOrSlug.toLowerCase());
179
+ if (byName)
180
+ return byName.id;
181
+ const byPrefix = db.query("SELECT id FROM projects WHERE id LIKE ? LIMIT 2").all(`${idOrSlug}%`);
182
+ if (byPrefix.length === 1 && byPrefix[0])
183
+ return byPrefix[0].id;
184
+ const bySlugPrefix = db.query("SELECT id FROM projects WHERE slug LIKE ? LIMIT 2").all(`${idOrSlug}%`);
185
+ if (bySlugPrefix.length === 1 && bySlugPrefix[0])
186
+ return bySlugPrefix[0].id;
187
+ return null;
188
+ }
156
189
  function hasFts(db) {
157
190
  return db.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='prompts_fts'").get() !== null;
158
191
  }
@@ -378,6 +411,13 @@ class TemplateRenderError extends Error {
378
411
  }
379
412
  }
380
413
 
414
+ class ProjectNotFoundError extends Error {
415
+ constructor(id) {
416
+ super(`Project not found: ${id}`);
417
+ this.name = "ProjectNotFoundError";
418
+ }
419
+ }
420
+
381
421
  // src/db/prompts.ts
382
422
  function rowToPrompt(row) {
383
423
  return {
@@ -391,6 +431,7 @@ function rowToPrompt(row) {
391
431
  tags: JSON.parse(row["tags"] || "[]"),
392
432
  variables: JSON.parse(row["variables"] || "[]"),
393
433
  pinned: Boolean(row["pinned"]),
434
+ project_id: row["project_id"] ?? null,
394
435
  is_template: Boolean(row["is_template"]),
395
436
  source: row["source"],
396
437
  version: row["version"],
@@ -414,11 +455,12 @@ function createPrompt(input) {
414
455
  ensureCollection(collection);
415
456
  const tags = JSON.stringify(input.tags || []);
416
457
  const source = input.source || "manual";
458
+ const project_id = input.project_id ?? null;
417
459
  const vars = extractVariables(input.body);
418
460
  const variables = JSON.stringify(vars.map((v) => ({ name: v, required: true })));
419
461
  const is_template = vars.length > 0 ? 1 : 0;
420
- db.run(`INSERT INTO prompts (id, name, slug, title, body, description, collection, tags, variables, is_template, source)
421
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, name, slug, input.title, input.body, input.description ?? null, collection, tags, variables, is_template, source]);
462
+ db.run(`INSERT INTO prompts (id, name, slug, title, body, description, collection, tags, variables, is_template, source, project_id)
463
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, name, slug, input.title, input.body, input.description ?? null, collection, tags, variables, is_template, source, project_id]);
422
464
  db.run(`INSERT INTO prompt_versions (id, prompt_id, body, version, changed_by)
423
465
  VALUES (?, ?, ?, 1, ?)`, [generateId("VER"), id, input.body, input.changed_by ?? null]);
424
466
  return getPrompt(id);
@@ -462,10 +504,16 @@ function listPrompts(filter = {}) {
462
504
  params.push(`%"${tag}"%`);
463
505
  }
464
506
  }
507
+ let orderBy = "pinned DESC, use_count DESC, updated_at DESC";
508
+ if (filter.project_id !== undefined && filter.project_id !== null) {
509
+ conditions.push("(project_id = ? OR project_id IS NULL)");
510
+ params.push(filter.project_id);
511
+ orderBy = `(CASE WHEN project_id = '${filter.project_id}' THEN 0 ELSE 1 END), pinned DESC, use_count DESC, updated_at DESC`;
512
+ }
465
513
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
466
514
  const limit = filter.limit ?? 100;
467
515
  const offset = filter.offset ?? 0;
468
- const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY pinned DESC, use_count DESC, updated_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
516
+ const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
469
517
  return rows.map(rowToPrompt);
470
518
  }
471
519
  function updatePrompt(idOrSlug, input) {
@@ -625,6 +673,51 @@ function listAgents() {
625
673
  const rows = db.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
626
674
  return rows.map(rowToAgent);
627
675
  }
676
+ // src/db/projects.ts
677
+ function rowToProject(row, promptCount) {
678
+ return {
679
+ id: row["id"],
680
+ name: row["name"],
681
+ slug: row["slug"],
682
+ description: row["description"] ?? null,
683
+ path: row["path"] ?? null,
684
+ prompt_count: promptCount,
685
+ created_at: row["created_at"]
686
+ };
687
+ }
688
+ function createProject(input) {
689
+ const db = getDatabase();
690
+ const id = generateId("proj");
691
+ const slug = generateSlug(input.name);
692
+ db.run(`INSERT INTO projects (id, name, slug, description, path) VALUES (?, ?, ?, ?, ?)`, [id, input.name, slug, input.description ?? null, input.path ?? null]);
693
+ return getProject(id);
694
+ }
695
+ function getProject(idOrSlug) {
696
+ const db = getDatabase();
697
+ const id = resolveProject(db, idOrSlug);
698
+ if (!id)
699
+ return null;
700
+ const row = db.query("SELECT * FROM projects WHERE id = ?").get(id);
701
+ if (!row)
702
+ return null;
703
+ const countRow = db.query("SELECT COUNT(*) as n FROM prompts WHERE project_id = ?").get(id);
704
+ return rowToProject(row, countRow.n);
705
+ }
706
+ function listProjects() {
707
+ const db = getDatabase();
708
+ const rows = db.query("SELECT * FROM projects ORDER BY name ASC").all();
709
+ return rows.map((row) => {
710
+ const countRow = db.query("SELECT COUNT(*) as n FROM prompts WHERE project_id = ?").get(row["id"]);
711
+ return rowToProject(row, countRow.n);
712
+ });
713
+ }
714
+ function deleteProject(idOrSlug) {
715
+ const db = getDatabase();
716
+ const id = resolveProject(db, idOrSlug);
717
+ if (!id)
718
+ throw new ProjectNotFoundError(idOrSlug);
719
+ db.run("DELETE FROM projects WHERE id = ?", [id]);
720
+ }
628
721
  // src/lib/search.ts
629
722
  function rowToSearchResult(row, snippet) {
630
723
  return {
@@ -639,6 +732,7 @@ function rowToSearchResult(row, snippet) {
639
732
  tags: JSON.parse(row["tags"] || "[]"),
640
733
  variables: JSON.parse(row["variables"] || "[]"),
641
734
  pinned: Boolean(row["pinned"]),
735
+ project_id: row["project_id"] ?? null,
642
736
  is_template: Boolean(row["is_template"]),
643
737
  source: row["source"],
644
738
  version: row["version"],
@@ -682,6 +776,10 @@ function searchPrompts(query, filter = {}) {
682
776
  for (const tag of filter.tags)
683
777
  params.push(`%"${tag}"%`);
684
778
  }
779
+ if (filter.project_id !== undefined && filter.project_id !== null) {
780
+ conditions.push("(p.project_id = ? OR p.project_id IS NULL)");
781
+ params.push(filter.project_id);
782
+ }
685
783
  const where = conditions.length > 0 ? `AND ${conditions.join(" AND ")}` : "";
686
784
  const limit = filter.limit ?? 50;
687
785
  const offset = filter.offset ?? 0;
@@ -771,12 +869,14 @@ export {
771
869
  movePrompt,
772
870
  listVersions,
773
871
  listPrompts,
872
+ listProjects,
774
873
  listCollections,
775
874
  listAgents,
776
875
  importFromJson,
777
876
  getVersion,
778
877
  getPromptStats,
779
878
  getPrompt,
879
+ getProject,
780
880
  getDbPath,
781
881
  getDatabase,
782
882
  getCollection,
@@ -789,9 +889,12 @@ export {
789
889
  exportToJson,
790
890
  ensureCollection,
791
891
  deletePrompt,
892
+ deleteProject,
792
893
  createPrompt,
894
+ createProject,
793
895
  VersionConflictError,
794
896
  TemplateRenderError,
795
897
  PromptNotFoundError,
898
+ ProjectNotFoundError,
796
899
  DuplicateSlugError
797
900
  };
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/lib/search.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAuCxE,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,IAAI,CAAC,iBAAiB,EAAE,GAAG,CAAM,GACxC,YAAY,EAAE,CAoEhB;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,YAAY,EAAE,CAmCvE"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/lib/search.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAwCxE,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,IAAI,CAAC,iBAAiB,EAAE,GAAG,CAAM,GACxC,YAAY,EAAE,CAwEhB;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,YAAY,EAAE,CAmCvE"}
package/dist/mcp/index.js CHANGED
@@ -4103,6 +4103,21 @@ 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
+ },
4106
4121
  {
4107
4122
  name: "002_fts5",
4108
4123
  sql: `
@@ -4143,6 +4158,24 @@ function runMigrations(db) {
4143
4158
  db.run("INSERT INTO _migrations (name) VALUES (?)", [migration.name]);
4144
4159
  }
4145
4160
  }
4161
+ function resolveProject(db, idOrSlug) {
4162
+ const byId = db.query("SELECT id FROM projects WHERE id = ?").get(idOrSlug);
4163
+ if (byId)
4164
+ return byId.id;
4165
+ const bySlug = db.query("SELECT id FROM projects WHERE slug = ?").get(idOrSlug);
4166
+ if (bySlug)
4167
+ return bySlug.id;
4168
+ const byName = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(idOrSlug.toLowerCase());
4169
+ if (byName)
4170
+ return byName.id;
4171
+ const byPrefix = db.query("SELECT id FROM projects WHERE id LIKE ? LIMIT 2").all(`${idOrSlug}%`);
4172
+ if (byPrefix.length === 1 && byPrefix[0])
4173
+ return byPrefix[0].id;
4174
+ const bySlugPrefix = db.query("SELECT id FROM projects WHERE slug LIKE ? LIMIT 2").all(`${idOrSlug}%`);
4175
+ if (bySlugPrefix.length === 1 && bySlugPrefix[0])
4176
+ return bySlugPrefix[0].id;
4177
+ return null;
4178
+ }
4146
4179
  function hasFts(db) {
4147
4180
  return db.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='prompts_fts'").get() !== null;
4148
4181
  }
@@ -4360,6 +4393,12 @@ class DuplicateSlugError extends Error {
4360
4393
  this.name = "DuplicateSlugError";
4361
4394
  }
4362
4395
  }
4396
+ class ProjectNotFoundError extends Error {
4397
+ constructor(id) {
4398
+ super(`Project not found: ${id}`);
4399
+ this.name = "ProjectNotFoundError";
4400
+ }
4401
+ }
4363
4402
 
4364
4403
  // src/db/prompts.ts
4365
4404
  function rowToPrompt(row) {
@@ -4374,6 +4413,7 @@ function rowToPrompt(row) {
4374
4413
  tags: JSON.parse(row["tags"] || "[]"),
4375
4414
  variables: JSON.parse(row["variables"] || "[]"),
4376
4415
  pinned: Boolean(row["pinned"]),
4416
+ project_id: row["project_id"] ?? null,
4377
4417
  is_template: Boolean(row["is_template"]),
4378
4418
  source: row["source"],
4379
4419
  version: row["version"],
@@ -4397,11 +4437,12 @@ function createPrompt(input) {
4397
4437
  ensureCollection(collection);
4398
4438
  const tags = JSON.stringify(input.tags || []);
4399
4439
  const source = input.source || "manual";
4440
+ const project_id = input.project_id ?? null;
4400
4441
  const vars = extractVariables(input.body);
4401
4442
  const variables = JSON.stringify(vars.map((v) => ({ name: v, required: true })));
4402
4443
  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]);
4444
+ db.run(`INSERT INTO prompts (id, name, slug, title, body, description, collection, tags, variables, is_template, source, project_id)
4445
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, name, slug, input.title, input.body, input.description ?? null, collection, tags, variables, is_template, source, project_id]);
4405
4446
  db.run(`INSERT INTO prompt_versions (id, prompt_id, body, version, changed_by)
4406
4447
  VALUES (?, ?, ?, 1, ?)`, [generateId("VER"), id, input.body, input.changed_by ?? null]);
4407
4448
  return getPrompt(id);
@@ -4445,10 +4486,16 @@ function listPrompts(filter = {}) {
4445
4486
  params.push(`%"${tag}"%`);
4446
4487
  }
4447
4488
  }
4489
+ let orderBy = "pinned DESC, use_count DESC, updated_at DESC";
4490
+ if (filter.project_id !== undefined && filter.project_id !== null) {
4491
+ conditions.push("(project_id = ? OR project_id IS NULL)");
4492
+ params.push(filter.project_id);
4493
+ orderBy = `(CASE WHEN project_id = '${filter.project_id}' THEN 0 ELSE 1 END), pinned DESC, use_count DESC, updated_at DESC`;
4494
+ }
4448
4495
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4449
4496
  const limit = filter.limit ?? 100;
4450
4497
  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);
4498
+ const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
4452
4499
  return rows.map(rowToPrompt);
4453
4500
  }
4454
4501
  function updatePrompt(idOrSlug, input) {
@@ -4606,6 +4653,52 @@ function registerAgent(name, description) {
4606
4653
  return rowToAgent(db.query("SELECT * FROM agents WHERE id = ?").get(id));
4607
4654
  }
4608
4655
 
4656
+ // src/db/projects.ts
4657
+ function rowToProject(row, promptCount) {
4658
+ return {
4659
+ id: row["id"],
4660
+ name: row["name"],
4661
+ slug: row["slug"],
4662
+ description: row["description"] ?? null,
4663
+ path: row["path"] ?? null,
4664
+ prompt_count: promptCount,
4665
+ created_at: row["created_at"]
4666
+ };
4667
+ }
4668
+ function createProject(input) {
4669
+ const db = getDatabase();
4670
+ const id = generateId("proj");
4671
+ const slug = generateSlug(input.name);
4672
+ db.run(`INSERT INTO projects (id, name, slug, description, path) VALUES (?, ?, ?, ?, ?)`, [id, input.name, slug, input.description ?? null, input.path ?? null]);
4673
+ return getProject(id);
4674
+ }
4675
+ function getProject(idOrSlug) {
4676
+ const db = getDatabase();
4677
+ const id = resolveProject(db, idOrSlug);
4678
+ if (!id)
4679
+ return null;
4680
+ const row = db.query("SELECT * FROM projects WHERE id = ?").get(id);
4681
+ if (!row)
4682
+ return null;
4683
+ const countRow = db.query("SELECT COUNT(*) as n FROM prompts WHERE project_id = ?").get(id);
4684
+ return rowToProject(row, countRow.n);
4685
+ }
4686
+ function listProjects() {
4687
+ const db = getDatabase();
4688
+ const rows = db.query("SELECT * FROM projects ORDER BY name ASC").all();
4689
+ return rows.map((row) => {
4690
+ const countRow = db.query("SELECT COUNT(*) as n FROM prompts WHERE project_id = ?").get(row["id"]);
4691
+ return rowToProject(row, countRow.n);
4692
+ });
4693
+ }
4694
+ function deleteProject(idOrSlug) {
4695
+ const db = getDatabase();
4696
+ const id = resolveProject(db, idOrSlug);
4697
+ if (!id)
4698
+ throw new ProjectNotFoundError(idOrSlug);
4699
+ db.run("DELETE FROM projects WHERE id = ?", [id]);
4700
+ }
4701
+
4609
4702
  // src/lib/search.ts
4610
4703
  function rowToSearchResult(row, snippet) {
4611
4704
  return {
@@ -4620,6 +4713,7 @@ function rowToSearchResult(row, snippet) {
4620
4713
  tags: JSON.parse(row["tags"] || "[]"),
4621
4714
  variables: JSON.parse(row["variables"] || "[]"),
4622
4715
  pinned: Boolean(row["pinned"]),
4716
+ project_id: row["project_id"] ?? null,
4623
4717
  is_template: Boolean(row["is_template"]),
4624
4718
  source: row["source"],
4625
4719
  version: row["version"],
@@ -4663,6 +4757,10 @@ function searchPrompts(query, filter = {}) {
4663
4757
  for (const tag of filter.tags)
4664
4758
  params.push(`%"${tag}"%`);
4665
4759
  }
4760
+ if (filter.project_id !== undefined && filter.project_id !== null) {
4761
+ conditions.push("(p.project_id = ? OR p.project_id IS NULL)");
4762
+ params.push(filter.project_id);
4763
+ }
4666
4764
  const where = conditions.length > 0 ? `AND ${conditions.join(" AND ")}` : "";
4667
4765
  const limit = filter.limit ?? 50;
4668
4766
  const offset = filter.offset ?? 0;
@@ -4816,11 +4914,19 @@ server.registerTool("prompts_save", {
4816
4914
  tags: exports_external.array(exports_external.string()).optional().describe("Tags for filtering and search"),
4817
4915
  source: exports_external.enum(["manual", "ai-session", "imported"]).optional().describe("Where this prompt came from"),
4818
4916
  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")
4917
+ force: exports_external.boolean().optional().describe("Save even if a similar prompt already exists"),
4918
+ project: exports_external.string().optional().describe("Project name, slug, or ID to scope this prompt to")
4820
4919
  }
4821
4920
  }, async (args) => {
4822
4921
  try {
4823
- const { force, ...input } = args;
4922
+ const { force, project, ...input } = args;
4923
+ if (project) {
4924
+ const db = getDatabase();
4925
+ const pid = resolveProject(db, project);
4926
+ if (!pid)
4927
+ return err(`Project not found: ${project}`);
4928
+ input.project_id = pid;
4929
+ }
4824
4930
  const { prompt, created, duplicate_warning } = upsertPrompt(input, force ?? false);
4825
4931
  return ok({ ...prompt, _created: created, _duplicate_warning: duplicate_warning ?? null });
4826
4932
  } catch (e) {
@@ -4844,9 +4950,19 @@ server.registerTool("prompts_list", {
4844
4950
  is_template: exports_external.boolean().optional(),
4845
4951
  source: exports_external.enum(["manual", "ai-session", "imported"]).optional(),
4846
4952
  limit: exports_external.number().optional().default(50),
4847
- offset: exports_external.number().optional().default(0)
4848
- }
4849
- }, async (args) => ok(listPrompts(args)));
4953
+ 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")
4955
+ }
4956
+ }, async ({ project, ...args }) => {
4957
+ if (project) {
4958
+ const db = getDatabase();
4959
+ const pid = resolveProject(db, project);
4960
+ if (!pid)
4961
+ return err(`Project not found: ${project}`);
4962
+ return ok(listPrompts({ ...args, project_id: pid }));
4963
+ }
4964
+ return ok(listPrompts(args));
4965
+ });
4850
4966
  server.registerTool("prompts_delete", {
4851
4967
  description: "Delete a prompt by ID or slug.",
4852
4968
  inputSchema: { id: exports_external.string() }
@@ -4916,9 +5032,19 @@ server.registerTool("prompts_search", {
4916
5032
  tags: exports_external.array(exports_external.string()).optional(),
4917
5033
  is_template: exports_external.boolean().optional(),
4918
5034
  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)));
5035
+ limit: exports_external.number().optional().default(20),
5036
+ project: exports_external.string().optional().describe("Project name, slug, or ID to scope search")
5037
+ }
5038
+ }, async ({ q, project, ...filter }) => {
5039
+ if (project) {
5040
+ const db = getDatabase();
5041
+ const pid = resolveProject(db, project);
5042
+ if (!pid)
5043
+ return err(`Project not found: ${project}`);
5044
+ return ok(searchPrompts(q, { ...filter, project_id: pid }));
5045
+ }
5046
+ return ok(searchPrompts(q, filter));
5047
+ });
4922
5048
  server.registerTool("prompts_similar", {
4923
5049
  description: "Find prompts similar to a given prompt (by tag overlap and collection).",
4924
5050
  inputSchema: {
@@ -5056,10 +5182,19 @@ server.registerTool("prompts_save_from_session", {
5056
5182
  tags: exports_external.array(exports_external.string()).optional().describe("Relevant tags extracted from the prompt context"),
5057
5183
  collection: exports_external.string().optional().describe("Collection to save into (default: 'sessions')"),
5058
5184
  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")
5185
+ 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")
5060
5187
  }
5061
- }, async ({ title, body, slug, tags, collection, description, agent }) => {
5188
+ }, async ({ title, body, slug, tags, collection, description, agent, project }) => {
5062
5189
  try {
5190
+ let project_id;
5191
+ if (project) {
5192
+ const db = getDatabase();
5193
+ const pid = resolveProject(db, project);
5194
+ if (!pid)
5195
+ return err(`Project not found: ${project}`);
5196
+ project_id = pid;
5197
+ }
5063
5198
  const { prompt, created } = upsertPrompt({
5064
5199
  title,
5065
5200
  body,
@@ -5068,7 +5203,8 @@ server.registerTool("prompts_save_from_session", {
5068
5203
  collection: collection ?? "sessions",
5069
5204
  description,
5070
5205
  source: "ai-session",
5071
- changed_by: agent
5206
+ changed_by: agent,
5207
+ project_id
5072
5208
  });
5073
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}".` });
5074
5210
  } catch (e) {
@@ -5131,5 +5267,43 @@ server.registerTool("prompts_stats", {
5131
5267
  description: "Get usage statistics: most used prompts, recently used, counts by collection and source.",
5132
5268
  inputSchema: {}
5133
5269
  }, async () => ok(getPromptStats()));
5270
+ server.registerTool("prompts_project_create", {
5271
+ description: "Create a new project to scope prompts.",
5272
+ inputSchema: {
5273
+ name: exports_external.string().describe("Project name"),
5274
+ description: exports_external.string().optional().describe("Short description"),
5275
+ path: exports_external.string().optional().describe("Optional filesystem path this project maps to")
5276
+ }
5277
+ }, async ({ name, description, path }) => {
5278
+ try {
5279
+ return ok(createProject({ name, description, path }));
5280
+ } catch (e) {
5281
+ return err(e instanceof Error ? e.message : String(e));
5282
+ }
5283
+ });
5284
+ server.registerTool("prompts_project_list", {
5285
+ description: "List all projects with prompt counts.",
5286
+ inputSchema: {}
5287
+ }, async () => ok(listProjects()));
5288
+ server.registerTool("prompts_project_get", {
5289
+ description: "Get a project by ID, slug, or name.",
5290
+ inputSchema: { id: exports_external.string().describe("Project ID, slug, or name") }
5291
+ }, async ({ id }) => {
5292
+ const project = getProject(id);
5293
+ if (!project)
5294
+ return err(`Project not found: ${id}`);
5295
+ return ok(project);
5296
+ });
5297
+ server.registerTool("prompts_project_delete", {
5298
+ description: "Delete a project. Prompts in the project become global (project_id set to null).",
5299
+ inputSchema: { id: exports_external.string().describe("Project ID, slug, or name") }
5300
+ }, async ({ id }) => {
5301
+ try {
5302
+ deleteProject(id);
5303
+ return ok({ deleted: true, id });
5304
+ } catch (e) {
5305
+ return err(e instanceof Error ? e.message : String(e));
5306
+ }
5307
+ });
5134
5308
  var transport = new StdioServerTransport;
5135
5309
  await server.connect(transport);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":";;;eAmCmB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;;AAF9C,wBA+KC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":";;;eAsCmB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;;AAF9C,wBAkOC"}
@@ -115,6 +115,21 @@ function runMigrations(db) {
115
115
  name: "003_pinned",
116
116
  sql: `ALTER TABLE prompts ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;`
117
117
  },
118
+ {
119
+ name: "004_projects",
120
+ sql: `
121
+ CREATE TABLE IF NOT EXISTS projects (
122
+ id TEXT PRIMARY KEY,
123
+ name TEXT NOT NULL UNIQUE,
124
+ slug TEXT NOT NULL UNIQUE,
125
+ description TEXT,
126
+ path TEXT,
127
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
128
+ );
129
+ ALTER TABLE prompts ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
130
+ CREATE INDEX IF NOT EXISTS idx_prompts_project_id ON prompts(project_id);
131
+ `
132
+ },
118
133
  {
119
134
  name: "002_fts5",
120
135
  sql: `
@@ -155,6 +170,24 @@ function runMigrations(db) {
155
170
  db.run("INSERT INTO _migrations (name) VALUES (?)", [migration.name]);
156
171
  }
157
172
  }
173
+ function resolveProject(db, idOrSlug) {
174
+ const byId = db.query("SELECT id FROM projects WHERE id = ?").get(idOrSlug);
175
+ if (byId)
176
+ return byId.id;
177
+ const bySlug = db.query("SELECT id FROM projects WHERE slug = ?").get(idOrSlug);
178
+ if (bySlug)
179
+ return bySlug.id;
180
+ const byName = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(idOrSlug.toLowerCase());
181
+ if (byName)
182
+ return byName.id;
183
+ const byPrefix = db.query("SELECT id FROM projects WHERE id LIKE ? LIMIT 2").all(`${idOrSlug}%`);
184
+ if (byPrefix.length === 1 && byPrefix[0])
185
+ return byPrefix[0].id;
186
+ const bySlugPrefix = db.query("SELECT id FROM projects WHERE slug LIKE ? LIMIT 2").all(`${idOrSlug}%`);
187
+ if (bySlugPrefix.length === 1 && bySlugPrefix[0])
188
+ return bySlugPrefix[0].id;
189
+ return null;
190
+ }
158
191
  function hasFts(db) {
159
192
  return db.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='prompts_fts'").get() !== null;
160
193
  }
@@ -363,6 +396,12 @@ class DuplicateSlugError extends Error {
363
396
  this.name = "DuplicateSlugError";
364
397
  }
365
398
  }
399
+ class ProjectNotFoundError extends Error {
400
+ constructor(id) {
401
+ super(`Project not found: ${id}`);
402
+ this.name = "ProjectNotFoundError";
403
+ }
404
+ }
366
405
 
367
406
  // src/db/prompts.ts
368
407
  function rowToPrompt(row) {
@@ -377,6 +416,7 @@ function rowToPrompt(row) {
377
416
  tags: JSON.parse(row["tags"] || "[]"),
378
417
  variables: JSON.parse(row["variables"] || "[]"),
379
418
  pinned: Boolean(row["pinned"]),
419
+ project_id: row["project_id"] ?? null,
380
420
  is_template: Boolean(row["is_template"]),
381
421
  source: row["source"],
382
422
  version: row["version"],
@@ -400,11 +440,12 @@ function createPrompt(input) {
400
440
  ensureCollection(collection);
401
441
  const tags = JSON.stringify(input.tags || []);
402
442
  const source = input.source || "manual";
443
+ const project_id = input.project_id ?? null;
403
444
  const vars = extractVariables(input.body);
404
445
  const variables = JSON.stringify(vars.map((v) => ({ name: v, required: true })));
405
446
  const is_template = vars.length > 0 ? 1 : 0;
406
- db.run(`INSERT INTO prompts (id, name, slug, title, body, description, collection, tags, variables, is_template, source)
407
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, name, slug, input.title, input.body, input.description ?? null, collection, tags, variables, is_template, source]);
447
+ db.run(`INSERT INTO prompts (id, name, slug, title, body, description, collection, tags, variables, is_template, source, project_id)
448
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, name, slug, input.title, input.body, input.description ?? null, collection, tags, variables, is_template, source, project_id]);
408
449
  db.run(`INSERT INTO prompt_versions (id, prompt_id, body, version, changed_by)
409
450
  VALUES (?, ?, ?, 1, ?)`, [generateId("VER"), id, input.body, input.changed_by ?? null]);
410
451
  return getPrompt(id);
@@ -448,10 +489,16 @@ function listPrompts(filter = {}) {
448
489
  params.push(`%"${tag}"%`);
449
490
  }
450
491
  }
492
+ let orderBy = "pinned DESC, use_count DESC, updated_at DESC";
493
+ if (filter.project_id !== undefined && filter.project_id !== null) {
494
+ conditions.push("(project_id = ? OR project_id IS NULL)");
495
+ params.push(filter.project_id);
496
+ orderBy = `(CASE WHEN project_id = '${filter.project_id}' THEN 0 ELSE 1 END), pinned DESC, use_count DESC, updated_at DESC`;
497
+ }
451
498
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
452
499
  const limit = filter.limit ?? 100;
453
500
  const offset = filter.offset ?? 0;
454
- const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY pinned DESC, use_count DESC, updated_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
501
+ const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
455
502
  return rows.map(rowToPrompt);
456
503
  }
457
504
  function updatePrompt(idOrSlug, input) {
@@ -578,6 +625,52 @@ function restoreVersion(promptId, version, changedBy) {
578
625
  VALUES (?, ?, ?, ?, ?)`, [generateId("VER"), promptId, ver.body, newVersion, changedBy ?? null]);
579
626
  }
580
627
 
628
+ // src/db/projects.ts
629
+ function rowToProject(row, promptCount) {
630
+ return {
631
+ id: row["id"],
632
+ name: row["name"],
633
+ slug: row["slug"],
634
+ description: row["description"] ?? null,
635
+ path: row["path"] ?? null,
636
+ prompt_count: promptCount,
637
+ created_at: row["created_at"]
638
+ };
639
+ }
640
+ function createProject(input) {
641
+ const db = getDatabase();
642
+ const id = generateId("proj");
643
+ const slug = generateSlug(input.name);
644
+ db.run(`INSERT INTO projects (id, name, slug, description, path) VALUES (?, ?, ?, ?, ?)`, [id, input.name, slug, input.description ?? null, input.path ?? null]);
645
+ return getProject(id);
646
+ }
647
+ function getProject(idOrSlug) {
648
+ const db = getDatabase();
649
+ const id = resolveProject(db, idOrSlug);
650
+ if (!id)
651
+ return null;
652
+ const row = db.query("SELECT * FROM projects WHERE id = ?").get(id);
653
+ if (!row)
654
+ return null;
655
+ const countRow = db.query("SELECT COUNT(*) as n FROM prompts WHERE project_id = ?").get(id);
656
+ return rowToProject(row, countRow.n);
657
+ }
658
+ function listProjects() {
659
+ const db = getDatabase();
660
+ const rows = db.query("SELECT * FROM projects ORDER BY name ASC").all();
661
+ return rows.map((row) => {
662
+ const countRow = db.query("SELECT COUNT(*) as n FROM prompts WHERE project_id = ?").get(row["id"]);
663
+ return rowToProject(row, countRow.n);
664
+ });
665
+ }
666
+ function deleteProject(idOrSlug) {
667
+ const db = getDatabase();
668
+ const id = resolveProject(db, idOrSlug);
669
+ if (!id)
670
+ throw new ProjectNotFoundError(idOrSlug);
671
+ db.run("DELETE FROM projects WHERE id = ?", [id]);
672
+ }
673
+
581
674
  // src/lib/search.ts
582
675
  function rowToSearchResult(row, snippet) {
583
676
  return {
@@ -592,6 +685,7 @@ function rowToSearchResult(row, snippet) {
592
685
  tags: JSON.parse(row["tags"] || "[]"),
593
686
  variables: JSON.parse(row["variables"] || "[]"),
594
687
  pinned: Boolean(row["pinned"]),
688
+ project_id: row["project_id"] ?? null,
595
689
  is_template: Boolean(row["is_template"]),
596
690
  source: row["source"],
597
691
  version: row["version"],
@@ -635,6 +729,10 @@ function searchPrompts(query, filter = {}) {
635
729
  for (const tag of filter.tags)
636
730
  params.push(`%"${tag}"%`);
637
731
  }
732
+ if (filter.project_id !== undefined && filter.project_id !== null) {
733
+ conditions.push("(p.project_id = ? OR p.project_id IS NULL)");
734
+ params.push(filter.project_id);
735
+ }
638
736
  const where = conditions.length > 0 ? `AND ${conditions.join(" AND ")}` : "";
639
737
  const limit = filter.limit ?? 50;
640
738
  const offset = filter.offset ?? 0;
@@ -755,7 +853,15 @@ var server_default = {
755
853
  const source = url.searchParams.get("source") ?? undefined;
756
854
  const limit = parseInt(url.searchParams.get("limit") ?? "100");
757
855
  const offset = parseInt(url.searchParams.get("offset") ?? "0");
758
- return json(listPrompts({ collection, tags, is_template, source, limit, offset }));
856
+ const projectParam = url.searchParams.get("project") ?? undefined;
857
+ let project_id;
858
+ if (projectParam) {
859
+ const pid = resolveProject(getDatabase(), projectParam);
860
+ if (!pid)
861
+ return notFound(`Project not found: ${projectParam}`);
862
+ project_id = pid;
863
+ }
864
+ return json(listPrompts({ collection, tags, is_template, source, limit, offset, project_id }));
759
865
  }
760
866
  if (path === "/api/prompts" && method === "POST") {
761
867
  const body = await parseBody(req);
@@ -864,6 +970,43 @@ var server_default = {
864
970
  const collection = url.searchParams.get("collection") ?? undefined;
865
971
  return json(exportToJson(collection));
866
972
  }
973
+ if (path === "/api/projects" && method === "GET") {
974
+ return json(listProjects());
975
+ }
976
+ if (path === "/api/projects" && method === "POST") {
977
+ const { name, description, path: projPath } = await parseBody(req);
978
+ if (!name)
979
+ return badRequest("name is required");
980
+ return json(createProject({ name, description, path: projPath }), 201);
981
+ }
982
+ const projectMatch = path.match(/^\/api\/projects\/([^/]+)$/);
983
+ if (projectMatch) {
984
+ const projId = projectMatch[1];
985
+ if (method === "GET") {
986
+ const project = getProject(projId);
987
+ if (!project)
988
+ return notFound(`Project not found: ${projId}`);
989
+ return json(project);
990
+ }
991
+ if (method === "DELETE") {
992
+ try {
993
+ deleteProject(projId);
994
+ return json({ deleted: true, id: projId });
995
+ } catch (e) {
996
+ return notFound(e instanceof Error ? e.message : String(e));
997
+ }
998
+ }
999
+ }
1000
+ const projectPromptsMatch = path.match(/^\/api\/projects\/([^/]+)\/prompts$/);
1001
+ if (projectPromptsMatch && method === "GET") {
1002
+ const projId = projectPromptsMatch[1];
1003
+ const project = getProject(projId);
1004
+ if (!project)
1005
+ return notFound(`Project not found: ${projId}`);
1006
+ const limit = parseInt(url.searchParams.get("limit") ?? "100");
1007
+ const offset = parseInt(url.searchParams.get("offset") ?? "0");
1008
+ return json(listPrompts({ project_id: project.id, limit, offset }));
1009
+ }
867
1010
  if (path === "/health") {
868
1011
  return json({ status: "ok", port: PORT });
869
1012
  }
@@ -11,12 +11,22 @@ export interface Prompt {
11
11
  is_template: boolean;
12
12
  source: PromptSource;
13
13
  pinned: boolean;
14
+ project_id: string | null;
14
15
  version: number;
15
16
  use_count: number;
16
17
  last_used_at: string | null;
17
18
  created_at: string;
18
19
  updated_at: string;
19
20
  }
21
+ export interface Project {
22
+ id: string;
23
+ name: string;
24
+ slug: string;
25
+ description: string | null;
26
+ path: string | null;
27
+ prompt_count: number;
28
+ created_at: string;
29
+ }
20
30
  export interface TemplateVariable {
21
31
  name: string;
22
32
  description?: string;
@@ -56,6 +66,7 @@ export interface CreatePromptInput {
56
66
  tags?: string[];
57
67
  source?: PromptSource;
58
68
  changed_by?: string;
69
+ project_id?: string | null;
59
70
  }
60
71
  export interface UpdatePromptInput {
61
72
  title?: string;
@@ -73,6 +84,7 @@ export interface ListPromptsFilter {
73
84
  q?: string;
74
85
  limit?: number;
75
86
  offset?: number;
87
+ project_id?: string | null;
76
88
  }
77
89
  export interface SearchResult {
78
90
  prompt: Prompt;
@@ -123,4 +135,7 @@ export declare class DuplicateSlugError extends Error {
123
135
  export declare class TemplateRenderError extends Error {
124
136
  constructor(message: string);
125
137
  }
138
+ export declare class ProjectNotFoundError extends Error {
139
+ constructor(id: string);
140
+ }
126
141
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,SAAS,EAAE,gBAAgB,EAAE,CAAA;IAC7B,WAAW,EAAE,OAAO,CAAA;IACpB,MAAM,EAAE,YAAY,CAAA;IACpB,MAAM,EAAE,OAAO,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,YAAY,GAAG,UAAU,CAAA;AAE/D,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,CAAC,CAAC,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,aAAa,EAAE,MAAM,EAAE,CAAA;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAA;IACrB,eAAe,EAAE,MAAM,CAAA;IACvB,iBAAiB,EAAE,MAAM,CAAA;IACzB,SAAS,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC9F,aAAa,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACrG,aAAa,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC3D,SAAS,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACpD;AAED,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,EAAE,EAAE,MAAM;CAIvB;AAED,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,EAAE,EAAE,MAAM;CAIvB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,IAAI,EAAE,MAAM;CAIzB;AAED,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/prompts",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Reusable prompt library for AI agents — CLI + MCP server + REST API + web dashboard",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",