@hasna/prompts 0.2.0 → 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/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
  }
@@ -4182,25 +4215,24 @@ function uniqueSlug(baseSlug) {
4182
4215
  }
4183
4216
  return slug;
4184
4217
  }
4218
+ var CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
4219
+ function nanoid(len) {
4220
+ let id = "";
4221
+ for (let i = 0;i < len; i++) {
4222
+ id += CHARS[Math.floor(Math.random() * CHARS.length)];
4223
+ }
4224
+ return id;
4225
+ }
4185
4226
  function generatePromptId() {
4186
4227
  const db = getDatabase();
4187
- const row = db.query("SELECT id FROM prompts ORDER BY rowid DESC LIMIT 1").get();
4188
- let next = 1;
4189
- if (row) {
4190
- const match = row.id.match(/PRMT-(\d+)/);
4191
- if (match && match[1]) {
4192
- next = parseInt(match[1], 10) + 1;
4193
- }
4194
- }
4195
- return `PRMT-${String(next).padStart(5, "0")}`;
4228
+ let id;
4229
+ do {
4230
+ id = `prmt-${nanoid(8)}`;
4231
+ } while (db.query("SELECT 1 FROM prompts WHERE id = ?").get(id));
4232
+ return id;
4196
4233
  }
4197
4234
  function generateId(prefix) {
4198
- const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
4199
- let id = prefix + "-";
4200
- for (let i = 0;i < 8; i++) {
4201
- id += chars[Math.floor(Math.random() * chars.length)];
4202
- }
4203
- return id;
4235
+ return `${prefix}-${nanoid(8)}`;
4204
4236
  }
4205
4237
 
4206
4238
  // src/db/collections.ts
@@ -4361,6 +4393,12 @@ class DuplicateSlugError extends Error {
4361
4393
  this.name = "DuplicateSlugError";
4362
4394
  }
4363
4395
  }
4396
+ class ProjectNotFoundError extends Error {
4397
+ constructor(id) {
4398
+ super(`Project not found: ${id}`);
4399
+ this.name = "ProjectNotFoundError";
4400
+ }
4401
+ }
4364
4402
 
4365
4403
  // src/db/prompts.ts
4366
4404
  function rowToPrompt(row) {
@@ -4375,6 +4413,7 @@ function rowToPrompt(row) {
4375
4413
  tags: JSON.parse(row["tags"] || "[]"),
4376
4414
  variables: JSON.parse(row["variables"] || "[]"),
4377
4415
  pinned: Boolean(row["pinned"]),
4416
+ project_id: row["project_id"] ?? null,
4378
4417
  is_template: Boolean(row["is_template"]),
4379
4418
  source: row["source"],
4380
4419
  version: row["version"],
@@ -4398,11 +4437,12 @@ function createPrompt(input) {
4398
4437
  ensureCollection(collection);
4399
4438
  const tags = JSON.stringify(input.tags || []);
4400
4439
  const source = input.source || "manual";
4440
+ const project_id = input.project_id ?? null;
4401
4441
  const vars = extractVariables(input.body);
4402
4442
  const variables = JSON.stringify(vars.map((v) => ({ name: v, required: true })));
4403
4443
  const is_template = vars.length > 0 ? 1 : 0;
4404
- db.run(`INSERT INTO prompts (id, name, slug, title, body, description, collection, tags, variables, is_template, source)
4405
- 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]);
4406
4446
  db.run(`INSERT INTO prompt_versions (id, prompt_id, body, version, changed_by)
4407
4447
  VALUES (?, ?, ?, 1, ?)`, [generateId("VER"), id, input.body, input.changed_by ?? null]);
4408
4448
  return getPrompt(id);
@@ -4446,10 +4486,16 @@ function listPrompts(filter = {}) {
4446
4486
  params.push(`%"${tag}"%`);
4447
4487
  }
4448
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
+ }
4449
4495
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4450
4496
  const limit = filter.limit ?? 100;
4451
4497
  const offset = filter.offset ?? 0;
4452
- 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);
4453
4499
  return rows.map(rowToPrompt);
4454
4500
  }
4455
4501
  function updatePrompt(idOrSlug, input) {
@@ -4607,6 +4653,52 @@ function registerAgent(name, description) {
4607
4653
  return rowToAgent(db.query("SELECT * FROM agents WHERE id = ?").get(id));
4608
4654
  }
4609
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
+
4610
4702
  // src/lib/search.ts
4611
4703
  function rowToSearchResult(row, snippet) {
4612
4704
  return {
@@ -4621,6 +4713,7 @@ function rowToSearchResult(row, snippet) {
4621
4713
  tags: JSON.parse(row["tags"] || "[]"),
4622
4714
  variables: JSON.parse(row["variables"] || "[]"),
4623
4715
  pinned: Boolean(row["pinned"]),
4716
+ project_id: row["project_id"] ?? null,
4624
4717
  is_template: Boolean(row["is_template"]),
4625
4718
  source: row["source"],
4626
4719
  version: row["version"],
@@ -4664,6 +4757,10 @@ function searchPrompts(query, filter = {}) {
4664
4757
  for (const tag of filter.tags)
4665
4758
  params.push(`%"${tag}"%`);
4666
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
+ }
4667
4764
  const where = conditions.length > 0 ? `AND ${conditions.join(" AND ")}` : "";
4668
4765
  const limit = filter.limit ?? 50;
4669
4766
  const offset = filter.offset ?? 0;
@@ -4817,11 +4914,19 @@ server.registerTool("prompts_save", {
4817
4914
  tags: exports_external.array(exports_external.string()).optional().describe("Tags for filtering and search"),
4818
4915
  source: exports_external.enum(["manual", "ai-session", "imported"]).optional().describe("Where this prompt came from"),
4819
4916
  changed_by: exports_external.string().optional().describe("Agent name making this change"),
4820
- 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")
4821
4919
  }
4822
4920
  }, async (args) => {
4823
4921
  try {
4824
- 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
+ }
4825
4930
  const { prompt, created, duplicate_warning } = upsertPrompt(input, force ?? false);
4826
4931
  return ok({ ...prompt, _created: created, _duplicate_warning: duplicate_warning ?? null });
4827
4932
  } catch (e) {
@@ -4845,9 +4950,19 @@ server.registerTool("prompts_list", {
4845
4950
  is_template: exports_external.boolean().optional(),
4846
4951
  source: exports_external.enum(["manual", "ai-session", "imported"]).optional(),
4847
4952
  limit: exports_external.number().optional().default(50),
4848
- offset: exports_external.number().optional().default(0)
4849
- }
4850
- }, 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
+ });
4851
4966
  server.registerTool("prompts_delete", {
4852
4967
  description: "Delete a prompt by ID or slug.",
4853
4968
  inputSchema: { id: exports_external.string() }
@@ -4917,9 +5032,19 @@ server.registerTool("prompts_search", {
4917
5032
  tags: exports_external.array(exports_external.string()).optional(),
4918
5033
  is_template: exports_external.boolean().optional(),
4919
5034
  source: exports_external.enum(["manual", "ai-session", "imported"]).optional(),
4920
- limit: exports_external.number().optional().default(20)
4921
- }
4922
- }, 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
+ });
4923
5048
  server.registerTool("prompts_similar", {
4924
5049
  description: "Find prompts similar to a given prompt (by tag overlap and collection).",
4925
5050
  inputSchema: {
@@ -5057,10 +5182,19 @@ server.registerTool("prompts_save_from_session", {
5057
5182
  tags: exports_external.array(exports_external.string()).optional().describe("Relevant tags extracted from the prompt context"),
5058
5183
  collection: exports_external.string().optional().describe("Collection to save into (default: 'sessions')"),
5059
5184
  description: exports_external.string().optional().describe("One-line description of what this prompt does"),
5060
- 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")
5061
5187
  }
5062
- }, async ({ title, body, slug, tags, collection, description, agent }) => {
5188
+ }, async ({ title, body, slug, tags, collection, description, agent, project }) => {
5063
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
+ }
5064
5198
  const { prompt, created } = upsertPrompt({
5065
5199
  title,
5066
5200
  body,
@@ -5069,7 +5203,8 @@ server.registerTool("prompts_save_from_session", {
5069
5203
  collection: collection ?? "sessions",
5070
5204
  description,
5071
5205
  source: "ai-session",
5072
- changed_by: agent
5206
+ changed_by: agent,
5207
+ project_id
5073
5208
  });
5074
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}".` });
5075
5210
  } catch (e) {
@@ -5132,5 +5267,43 @@ server.registerTool("prompts_stats", {
5132
5267
  description: "Get usage statistics: most used prompts, recently used, counts by collection and source.",
5133
5268
  inputSchema: {}
5134
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
+ });
5135
5308
  var transport = new StdioServerTransport;
5136
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
  }
@@ -194,25 +227,24 @@ function uniqueSlug(baseSlug) {
194
227
  }
195
228
  return slug;
196
229
  }
230
+ var CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
231
+ function nanoid(len) {
232
+ let id = "";
233
+ for (let i = 0;i < len; i++) {
234
+ id += CHARS[Math.floor(Math.random() * CHARS.length)];
235
+ }
236
+ return id;
237
+ }
197
238
  function generatePromptId() {
198
239
  const db = getDatabase();
199
- const row = db.query("SELECT id FROM prompts ORDER BY rowid DESC LIMIT 1").get();
200
- let next = 1;
201
- if (row) {
202
- const match = row.id.match(/PRMT-(\d+)/);
203
- if (match && match[1]) {
204
- next = parseInt(match[1], 10) + 1;
205
- }
206
- }
207
- return `PRMT-${String(next).padStart(5, "0")}`;
240
+ let id;
241
+ do {
242
+ id = `prmt-${nanoid(8)}`;
243
+ } while (db.query("SELECT 1 FROM prompts WHERE id = ?").get(id));
244
+ return id;
208
245
  }
209
246
  function generateId(prefix) {
210
- const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
211
- let id = prefix + "-";
212
- for (let i = 0;i < 8; i++) {
213
- id += chars[Math.floor(Math.random() * chars.length)];
214
- }
215
- return id;
247
+ return `${prefix}-${nanoid(8)}`;
216
248
  }
217
249
 
218
250
  // src/db/collections.ts
@@ -364,6 +396,12 @@ class DuplicateSlugError extends Error {
364
396
  this.name = "DuplicateSlugError";
365
397
  }
366
398
  }
399
+ class ProjectNotFoundError extends Error {
400
+ constructor(id) {
401
+ super(`Project not found: ${id}`);
402
+ this.name = "ProjectNotFoundError";
403
+ }
404
+ }
367
405
 
368
406
  // src/db/prompts.ts
369
407
  function rowToPrompt(row) {
@@ -378,6 +416,7 @@ function rowToPrompt(row) {
378
416
  tags: JSON.parse(row["tags"] || "[]"),
379
417
  variables: JSON.parse(row["variables"] || "[]"),
380
418
  pinned: Boolean(row["pinned"]),
419
+ project_id: row["project_id"] ?? null,
381
420
  is_template: Boolean(row["is_template"]),
382
421
  source: row["source"],
383
422
  version: row["version"],
@@ -401,11 +440,12 @@ function createPrompt(input) {
401
440
  ensureCollection(collection);
402
441
  const tags = JSON.stringify(input.tags || []);
403
442
  const source = input.source || "manual";
443
+ const project_id = input.project_id ?? null;
404
444
  const vars = extractVariables(input.body);
405
445
  const variables = JSON.stringify(vars.map((v) => ({ name: v, required: true })));
406
446
  const is_template = vars.length > 0 ? 1 : 0;
407
- db.run(`INSERT INTO prompts (id, name, slug, title, body, description, collection, tags, variables, is_template, source)
408
- 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]);
409
449
  db.run(`INSERT INTO prompt_versions (id, prompt_id, body, version, changed_by)
410
450
  VALUES (?, ?, ?, 1, ?)`, [generateId("VER"), id, input.body, input.changed_by ?? null]);
411
451
  return getPrompt(id);
@@ -449,10 +489,16 @@ function listPrompts(filter = {}) {
449
489
  params.push(`%"${tag}"%`);
450
490
  }
451
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
+ }
452
498
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
453
499
  const limit = filter.limit ?? 100;
454
500
  const offset = filter.offset ?? 0;
455
- const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY pinned DESC, use_count DESC, updated_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
501
+ const rows = db.query(`SELECT * FROM prompts ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
456
502
  return rows.map(rowToPrompt);
457
503
  }
458
504
  function updatePrompt(idOrSlug, input) {
@@ -579,6 +625,52 @@ function restoreVersion(promptId, version, changedBy) {
579
625
  VALUES (?, ?, ?, ?, ?)`, [generateId("VER"), promptId, ver.body, newVersion, changedBy ?? null]);
580
626
  }
581
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
+
582
674
  // src/lib/search.ts
583
675
  function rowToSearchResult(row, snippet) {
584
676
  return {
@@ -593,6 +685,7 @@ function rowToSearchResult(row, snippet) {
593
685
  tags: JSON.parse(row["tags"] || "[]"),
594
686
  variables: JSON.parse(row["variables"] || "[]"),
595
687
  pinned: Boolean(row["pinned"]),
688
+ project_id: row["project_id"] ?? null,
596
689
  is_template: Boolean(row["is_template"]),
597
690
  source: row["source"],
598
691
  version: row["version"],
@@ -636,6 +729,10 @@ function searchPrompts(query, filter = {}) {
636
729
  for (const tag of filter.tags)
637
730
  params.push(`%"${tag}"%`);
638
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
+ }
639
736
  const where = conditions.length > 0 ? `AND ${conditions.join(" AND ")}` : "";
640
737
  const limit = filter.limit ?? 50;
641
738
  const offset = filter.offset ?? 0;
@@ -756,7 +853,15 @@ var server_default = {
756
853
  const source = url.searchParams.get("source") ?? undefined;
757
854
  const limit = parseInt(url.searchParams.get("limit") ?? "100");
758
855
  const offset = parseInt(url.searchParams.get("offset") ?? "0");
759
- 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 }));
760
865
  }
761
866
  if (path === "/api/prompts" && method === "POST") {
762
867
  const body = await parseBody(req);
@@ -865,6 +970,43 @@ var server_default = {
865
970
  const collection = url.searchParams.get("collection") ?? undefined;
866
971
  return json(exportToJson(collection));
867
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
+ }
868
1010
  if (path === "/health") {
869
1011
  return json({ status: "ok", port: PORT });
870
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