@@ -56,6 +56,7 @@ import { z } from "zod";
5656import { eq } from "drizzle-orm" ;
5757
5858// server/db.ts
59+ import crypto from "node:crypto" ;
5960import os from "node:os" ;
6061import path from "node:path" ;
6162import Database from "better-sqlite3" ;
@@ -86,6 +87,24 @@ var usageHistory = sqliteTable("usage_history", {
8687 outputTokens : integer ( "output_tokens" ) . notNull ( ) ,
8788 costUsd : real ( "cost_usd" ) . notNull ( )
8889} ) ;
90+ var promptCategories = sqliteTable ( "prompt_categories" , {
91+ id : text ( "id" ) . primaryKey ( ) ,
92+ name : text ( "name" ) . notNull ( ) ,
93+ sortOrder : integer ( "sort_order" ) . notNull ( ) . default ( 0 ) ,
94+ createdAt : integer ( "created_at" ) . notNull ( )
95+ } ) ;
96+ var prompts = sqliteTable ( "prompts" , {
97+ id : text ( "id" ) . primaryKey ( ) ,
98+ categoryId : text ( "category_id" ) . notNull ( ) ,
99+ title : text ( "title" ) . notNull ( ) ,
100+ content : text ( "content" ) . notNull ( ) ,
101+ pinned : integer ( "pinned" , { mode : "boolean" } ) . notNull ( ) . default ( false ) ,
102+ sortOrder : integer ( "sort_order" ) . notNull ( ) . default ( 0 ) ,
103+ usageCount : integer ( "usage_count" ) . notNull ( ) . default ( 0 ) ,
104+ lastUsedAt : integer ( "last_used_at" ) ,
105+ createdAt : integer ( "created_at" ) . notNull ( ) ,
106+ updatedAt : integer ( "updated_at" ) . notNull ( )
107+ } ) ;
89108function initDb ( ) {
90109 const sqlite = new Database ( DB_PATH ) ;
91110 sqlite . pragma ( "journal_mode = WAL" ) ;
@@ -118,9 +137,42 @@ function initDb() {
118137 cost_usd REAL NOT NULL
119138 )
120139 ` ) ;
140+ sqlite . exec ( `
141+ CREATE TABLE IF NOT EXISTS prompt_categories (
142+ id TEXT PRIMARY KEY,
143+ name TEXT NOT NULL,
144+ sort_order INTEGER NOT NULL DEFAULT 0,
145+ created_at INTEGER NOT NULL
146+ )
147+ ` ) ;
148+ sqlite . exec ( `
149+ CREATE TABLE IF NOT EXISTS prompts (
150+ id TEXT PRIMARY KEY,
151+ category_id TEXT NOT NULL,
152+ title TEXT NOT NULL,
153+ content TEXT NOT NULL,
154+ pinned INTEGER NOT NULL DEFAULT 0,
155+ sort_order INTEGER NOT NULL DEFAULT 0,
156+ usage_count INTEGER NOT NULL DEFAULT 0,
157+ last_used_at INTEGER,
158+ created_at INTEGER NOT NULL,
159+ updated_at INTEGER NOT NULL
160+ )
161+ ` ) ;
162+ seedDefaultCategories ( sqlite ) ;
121163 return drizzle ( sqlite , { schema } ) ;
122164}
123- var schema = { preferences, tokenAlarms, usageHistory } ;
165+ function seedDefaultCategories ( sqlite ) {
166+ const count = sqlite . prepare ( "SELECT COUNT(*) as n FROM prompt_categories" ) . get ( ) ;
167+ if ( count . n > 0 ) return ;
168+ const now3 = Math . floor ( Date . now ( ) / 1e3 ) ;
169+ const defaults = [ "General" , "Development" , "Operations" , "Research" , "Writing" ] ;
170+ const stmt = sqlite . prepare ( "INSERT INTO prompt_categories (id, name, sort_order, created_at) VALUES (?, ?, ?, ?)" ) ;
171+ for ( let i = 0 ; i < defaults . length ; i ++ ) {
172+ stmt . run ( crypto . randomUUID ( ) , defaults [ i ] , i , now3 ) ;
173+ }
174+ }
175+ var schema = { preferences, tokenAlarms, usageHistory, promptCategories, prompts } ;
124176var db = initDb ( ) ;
125177
126178// server/lib/prefs.ts
@@ -158,6 +210,187 @@ async function handlePrefsPatch(c) {
158210 return c . json ( { ok : true } ) ;
159211}
160212
213+ // server/lib/prompts.ts
214+ import crypto2 from "node:crypto" ;
215+ import { eq as eq2 , sql } from "drizzle-orm" ;
216+ var now2 = ( ) => Math . floor ( Date . now ( ) / 1e3 ) ;
217+ function listCategories ( ) {
218+ return db . select ( ) . from ( promptCategories ) . orderBy ( promptCategories . sortOrder ) . all ( ) ;
219+ }
220+ function createCategory ( name ) {
221+ const maxOrder = db . select ( { max : sql `COALESCE(MAX(sort_order), -1)` } ) . from ( promptCategories ) . get ( ) ;
222+ const entry = {
223+ id : crypto2 . randomUUID ( ) ,
224+ name : name . trim ( ) ,
225+ sortOrder : ( maxOrder ?. max ?? - 1 ) + 1 ,
226+ createdAt : now2 ( )
227+ } ;
228+ db . insert ( promptCategories ) . values ( entry ) . run ( ) ;
229+ return entry ;
230+ }
231+ function updateCategory ( id , name ) {
232+ db . update ( promptCategories ) . set ( { name : name . trim ( ) } ) . where ( eq2 ( promptCategories . id , id ) ) . run ( ) ;
233+ }
234+ function deleteCategory ( id ) {
235+ db . delete ( prompts ) . where ( eq2 ( prompts . categoryId , id ) ) . run ( ) ;
236+ const result = db . delete ( promptCategories ) . where ( eq2 ( promptCategories . id , id ) ) . run ( ) ;
237+ return { deleted : result . changes } ;
238+ }
239+ function reorderCategories ( ids ) {
240+ for ( let i = 0 ; i < ids . length ; i ++ ) {
241+ db . update ( promptCategories ) . set ( { sortOrder : i } ) . where ( eq2 ( promptCategories . id , ids [ i ] ) ) . run ( ) ;
242+ }
243+ }
244+ function listPrompts ( ) {
245+ return db . select ( ) . from ( prompts ) . orderBy ( prompts . sortOrder ) . all ( ) ;
246+ }
247+ function createPrompt ( data ) {
248+ const maxOrder = db . select ( { max : sql `COALESCE(MAX(sort_order), -1)` } ) . from ( prompts ) . where ( eq2 ( prompts . categoryId , data . categoryId ) ) . get ( ) ;
249+ const entry = {
250+ id : crypto2 . randomUUID ( ) ,
251+ categoryId : data . categoryId ,
252+ title : data . title . trim ( ) ,
253+ content : data . content . trim ( ) ,
254+ pinned : false ,
255+ sortOrder : ( maxOrder ?. max ?? - 1 ) + 1 ,
256+ usageCount : 0 ,
257+ lastUsedAt : null ,
258+ createdAt : now2 ( ) ,
259+ updatedAt : now2 ( )
260+ } ;
261+ db . insert ( prompts ) . values ( entry ) . run ( ) ;
262+ return entry ;
263+ }
264+ function updatePrompt ( id , data ) {
265+ const set = { updatedAt : now2 ( ) } ;
266+ if ( data . title !== void 0 ) set . title = data . title . trim ( ) ;
267+ if ( data . content !== void 0 ) set . content = data . content . trim ( ) ;
268+ if ( data . categoryId !== void 0 ) set . categoryId = data . categoryId ;
269+ if ( data . pinned !== void 0 ) set . pinned = data . pinned ;
270+ return db . update ( prompts ) . set ( set ) . where ( eq2 ( prompts . id , id ) ) . run ( ) ;
271+ }
272+ function deletePrompt ( id ) {
273+ return db . delete ( prompts ) . where ( eq2 ( prompts . id , id ) ) . run ( ) ;
274+ }
275+ function recordUsage ( id ) {
276+ return db . update ( prompts ) . set ( { usageCount : sql `usage_count + 1` , lastUsedAt : now2 ( ) , updatedAt : now2 ( ) } ) . where ( eq2 ( prompts . id , id ) ) . run ( ) ;
277+ }
278+ function reorderPrompts ( ids ) {
279+ for ( let i = 0 ; i < ids . length ; i ++ ) {
280+ db . update ( prompts ) . set ( { sortOrder : i } ) . where ( eq2 ( prompts . id , ids [ i ] ) ) . run ( ) ;
281+ }
282+ }
283+ function exportAll ( ) {
284+ return {
285+ version : 1 ,
286+ categories : listCategories ( ) ,
287+ prompts : listPrompts ( )
288+ } ;
289+ }
290+ function importAll ( data ) {
291+ let catCount = 0 ;
292+ let promptCount = 0 ;
293+ const ts = now2 ( ) ;
294+ const categoryIdMap = /* @__PURE__ */ new Map ( ) ;
295+ for ( const cat of data . categories ) {
296+ const newId = crypto2 . randomUUID ( ) ;
297+ categoryIdMap . set ( cat . id , newId ) ;
298+ db . insert ( promptCategories ) . values ( { id : newId , name : cat . name , sortOrder : cat . sortOrder , createdAt : ts } ) . run ( ) ;
299+ catCount ++ ;
300+ }
301+ for ( const p of data . prompts ) {
302+ const mappedCategoryId = categoryIdMap . get ( p . categoryId ) ;
303+ if ( ! mappedCategoryId ) continue ;
304+ db . insert ( prompts ) . values ( {
305+ id : crypto2 . randomUUID ( ) ,
306+ categoryId : mappedCategoryId ,
307+ title : p . title ,
308+ content : p . content ,
309+ pinned : p . pinned ,
310+ sortOrder : p . sortOrder ,
311+ usageCount : 0 ,
312+ lastUsedAt : null ,
313+ createdAt : ts ,
314+ updatedAt : ts
315+ } ) . run ( ) ;
316+ promptCount ++ ;
317+ }
318+ return { categories : catCount , prompts : promptCount } ;
319+ }
320+
321+ // server/routes/prompts.ts
322+ function handlePromptsGet ( c ) {
323+ return c . json ( { categories : listCategories ( ) , prompts : listPrompts ( ) } ) ;
324+ }
325+ async function handlePromptsCreate ( c ) {
326+ const body = await c . req . json ( ) ;
327+ if ( ! body . categoryId || ! body . title ?. trim ( ) || ! body . content ?. trim ( ) ) {
328+ return c . json ( { ok : false , error : "categoryId, title, and content required" } , 400 ) ;
329+ }
330+ const prompt = createPrompt ( { categoryId : body . categoryId , title : body . title , content : body . content } ) ;
331+ return c . json ( { ok : true , prompt } ) ;
332+ }
333+ async function handlePromptsUpdate ( c ) {
334+ const id = c . req . param ( "id" ) ;
335+ const body = await c . req . json ( ) ;
336+ const result = updatePrompt ( id , body ) ;
337+ if ( result . changes === 0 ) return c . json ( { ok : false , error : "Prompt not found" } , 404 ) ;
338+ return c . json ( { ok : true } ) ;
339+ }
340+ async function handlePromptsDelete ( c ) {
341+ const result = deletePrompt ( c . req . param ( "id" ) ) ;
342+ if ( result . changes === 0 ) return c . json ( { ok : false , error : "Prompt not found" } , 404 ) ;
343+ return c . json ( { ok : true } ) ;
344+ }
345+ async function handlePromptsUse ( c ) {
346+ const result = recordUsage ( c . req . param ( "id" ) ) ;
347+ if ( result . changes === 0 ) return c . json ( { ok : false , error : "Prompt not found" } , 404 ) ;
348+ return c . json ( { ok : true } ) ;
349+ }
350+ async function handlePromptsReorder ( c ) {
351+ const body = await c . req . json ( ) ;
352+ if ( ! Array . isArray ( body . ids ) ) return c . json ( { ok : false , error : "ids array required" } , 400 ) ;
353+ reorderPrompts ( body . ids ) ;
354+ return c . json ( { ok : true } ) ;
355+ }
356+ function handleCategoriesGet ( c ) {
357+ return c . json ( { categories : listCategories ( ) } ) ;
358+ }
359+ async function handleCategoriesCreate ( c ) {
360+ const body = await c . req . json ( ) ;
361+ if ( ! body . name ?. trim ( ) ) return c . json ( { ok : false , error : "name required" } , 400 ) ;
362+ const category = createCategory ( body . name ) ;
363+ return c . json ( { ok : true , category } ) ;
364+ }
365+ async function handleCategoriesUpdate ( c ) {
366+ const id = c . req . param ( "id" ) ;
367+ const body = await c . req . json ( ) ;
368+ if ( ! body . name ?. trim ( ) ) return c . json ( { ok : false , error : "name required" } , 400 ) ;
369+ updateCategory ( id , body . name ) ;
370+ return c . json ( { ok : true } ) ;
371+ }
372+ async function handleCategoriesDelete ( c ) {
373+ const result = deleteCategory ( c . req . param ( "id" ) ) ;
374+ return c . json ( { ok : true , ...result } ) ;
375+ }
376+ async function handleCategoriesReorder ( c ) {
377+ const body = await c . req . json ( ) ;
378+ if ( ! Array . isArray ( body . ids ) ) return c . json ( { ok : false , error : "ids array required" } , 400 ) ;
379+ reorderCategories ( body . ids ) ;
380+ return c . json ( { ok : true } ) ;
381+ }
382+ function handlePromptsExport ( c ) {
383+ return c . json ( exportAll ( ) ) ;
384+ }
385+ async function handlePromptsImport ( c ) {
386+ const body = await c . req . json ( ) ;
387+ if ( body . version !== 1 || ! Array . isArray ( body . categories ) || ! Array . isArray ( body . prompts ) ) {
388+ return c . json ( { ok : false , error : "Invalid import format" } , 400 ) ;
389+ }
390+ const result = importAll ( body ) ;
391+ return c . json ( { ok : true , ...result } ) ;
392+ }
393+
161394// server/routes/version.ts
162395import { z as z2 } from "zod" ;
163396
@@ -299,6 +532,9 @@ var api = new Hono().basePath("/api");
299532api . get ( "/health" , handleHealth ) ;
300533api . get ( "/version" , handleVersionGet ) ;
301534api . get ( "/prefs" , handlePrefsGet ) ;
535+ api . get ( "/prompts" , handlePromptsGet ) ;
536+ api . get ( "/prompts/export" , handlePromptsExport ) ;
537+ api . get ( "/prompt-categories" , handleCategoriesGet ) ;
302538api . post ( "/version/dismiss" , ( c ) => {
303539 const denied = requireAuth ( c ) ;
304540 if ( denied ) return denied ;
@@ -319,6 +555,56 @@ api.patch("/prefs", (c) => {
319555 if ( denied ) return denied ;
320556 return handlePrefsPatch ( c ) ;
321557} ) ;
558+ api . post ( "/prompts" , ( c ) => {
559+ const denied = requireAuth ( c ) ;
560+ if ( denied ) return denied ;
561+ return handlePromptsCreate ( c ) ;
562+ } ) ;
563+ api . patch ( "/prompts/:id" , ( c ) => {
564+ const denied = requireAuth ( c ) ;
565+ if ( denied ) return denied ;
566+ return handlePromptsUpdate ( c ) ;
567+ } ) ;
568+ api . delete ( "/prompts/:id" , ( c ) => {
569+ const denied = requireAuth ( c ) ;
570+ if ( denied ) return denied ;
571+ return handlePromptsDelete ( c ) ;
572+ } ) ;
573+ api . post ( "/prompts/:id/use" , ( c ) => {
574+ const denied = requireAuth ( c ) ;
575+ if ( denied ) return denied ;
576+ return handlePromptsUse ( c ) ;
577+ } ) ;
578+ api . post ( "/prompts/reorder" , ( c ) => {
579+ const denied = requireAuth ( c ) ;
580+ if ( denied ) return denied ;
581+ return handlePromptsReorder ( c ) ;
582+ } ) ;
583+ api . post ( "/prompts/import" , ( c ) => {
584+ const denied = requireAuth ( c ) ;
585+ if ( denied ) return denied ;
586+ return handlePromptsImport ( c ) ;
587+ } ) ;
588+ api . post ( "/prompt-categories" , ( c ) => {
589+ const denied = requireAuth ( c ) ;
590+ if ( denied ) return denied ;
591+ return handleCategoriesCreate ( c ) ;
592+ } ) ;
593+ api . patch ( "/prompt-categories/:id" , ( c ) => {
594+ const denied = requireAuth ( c ) ;
595+ if ( denied ) return denied ;
596+ return handleCategoriesUpdate ( c ) ;
597+ } ) ;
598+ api . delete ( "/prompt-categories/:id" , ( c ) => {
599+ const denied = requireAuth ( c ) ;
600+ if ( denied ) return denied ;
601+ return handleCategoriesDelete ( c ) ;
602+ } ) ;
603+ api . post ( "/prompt-categories/reorder" , ( c ) => {
604+ const denied = requireAuth ( c ) ;
605+ if ( denied ) return denied ;
606+ return handleCategoriesReorder ( c ) ;
607+ } ) ;
322608app . route ( "/" , api ) ;
323609var VITE_DEV_PORT = 5173 ;
324610var spaResponse = ( ) => new Response ( injectedHtmlBuffer , {
0 commit comments