diff --git a/.gitignore b/.gitignore index 6a5294b..ea34b6f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ dist .DS_Store postgres_data/ -minio-data/ \ No newline at end of file +minio-data/ +package-lock.json diff --git a/drizzle/0007_illegal_lila_cheney.sql b/drizzle/0007_illegal_lila_cheney.sql new file mode 100644 index 0000000..a2a613f --- /dev/null +++ b/drizzle/0007_illegal_lila_cheney.sql @@ -0,0 +1,11 @@ +CREATE TABLE "scratchpads" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "content" text DEFAULT '' NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "scratchpads_userId_unique" UNIQUE("user_id") +); +--> statement-breakpoint +ALTER TABLE "scratchpads" ADD CONSTRAINT "scratchpads_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "scratchpads_user_id_idx" ON "scratchpads" USING btree ("user_id"); \ No newline at end of file diff --git a/drizzle/meta/0007_snapshot.json b/drizzle/meta/0007_snapshot.json new file mode 100644 index 0000000..114eed2 --- /dev/null +++ b/drizzle/meta/0007_snapshot.json @@ -0,0 +1,1038 @@ +{ + "id": "fd492a50-87a7-424d-8cc6-71e26d7aea55", + "prevId": "da7ab0e7-dae6-4cea-b033-cf300b46ffa0", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.aliases": { + "name": "aliases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alias_text": { + "name": "alias_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_node_id": { + "name": "canonical_node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "aliases_user_id_users_id_fk": { + "name": "aliases_user_id_users_id_fk", + "tableFrom": "aliases", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "aliases_canonical_node_id_nodes_id_fk": { + "name": "aliases_canonical_node_id_nodes_id_fk", + "tableFrom": "aliases", + "tableTo": "nodes", + "columnsFrom": [ + "canonical_node_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.edge_embeddings": { + "name": "edge_embeddings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "edge_id": { + "name": "edge_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1024)", + "primaryKey": false, + "notNull": true + }, + "model_name": { + "name": "model_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "edge_embeddings_embedding_idx": { + "name": "edge_embeddings_embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + }, + "edge_embeddings_edge_id_idx": { + "name": "edge_embeddings_edge_id_idx", + "columns": [ + { + "expression": "edge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "edge_embeddings_edge_id_edges_id_fk": { + "name": "edge_embeddings_edge_id_edges_id_fk", + "tableFrom": "edge_embeddings", + "tableTo": "edges", + "columnsFrom": [ + "edge_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.edges": { + "name": "edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_node_id": { + "name": "source_node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_node_id": { + "name": "target_node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "edge_type": { + "name": "edge_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "edges_user_id_source_node_id_idx": { + "name": "edges_user_id_source_node_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "edges_user_id_target_node_id_idx": { + "name": "edges_user_id_target_node_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "edges_user_id_edge_type_idx": { + "name": "edges_user_id_edge_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "edge_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "edges_user_id_users_id_fk": { + "name": "edges_user_id_users_id_fk", + "tableFrom": "edges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "edges_source_node_id_nodes_id_fk": { + "name": "edges_source_node_id_nodes_id_fk", + "tableFrom": "edges", + "tableTo": "nodes", + "columnsFrom": [ + "source_node_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "edges_target_node_id_nodes_id_fk": { + "name": "edges_target_node_id_nodes_id_fk", + "tableFrom": "edges", + "tableTo": "nodes", + "columnsFrom": [ + "target_node_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "edges_sourceNodeId_targetNodeId_edge_type_unique": { + "name": "edges_sourceNodeId_targetNodeId_edge_type_unique", + "nullsNotDistinct": false, + "columns": [ + "source_node_id", + "target_node_id", + "edge_type" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.node_embeddings": { + "name": "node_embeddings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1024)", + "primaryKey": false, + "notNull": true + }, + "model_name": { + "name": "model_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "node_embeddings_embedding_idx": { + "name": "node_embeddings_embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + }, + "node_embeddings_node_id_idx": { + "name": "node_embeddings_node_id_idx", + "columns": [ + { + "expression": "node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "node_embeddings_node_id_nodes_id_fk": { + "name": "node_embeddings_node_id_nodes_id_fk", + "tableFrom": "node_embeddings", + "tableTo": "nodes", + "columnsFrom": [ + "node_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.node_metadata": { + "name": "node_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "additional_data": { + "name": "additional_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "node_metadata_node_id_idx": { + "name": "node_metadata_node_id_idx", + "columns": [ + { + "expression": "node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "node_metadata_node_id_nodes_id_fk": { + "name": "node_metadata_node_id_nodes_id_fk", + "tableFrom": "node_metadata", + "tableTo": "nodes", + "columnsFrom": [ + "node_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "node_metadata_nodeId_unique": { + "name": "node_metadata_nodeId_unique", + "nullsNotDistinct": false, + "columns": [ + "node_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.nodes": { + "name": "nodes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_type": { + "name": "node_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "nodes_user_id_idx": { + "name": "nodes_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "nodes_user_id_node_type_idx": { + "name": "nodes_user_id_node_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "node_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "nodes_user_id_users_id_fk": { + "name": "nodes_user_id_users_id_fk", + "tableFrom": "nodes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scratchpads": { + "name": "scratchpads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scratchpads_user_id_idx": { + "name": "scratchpads_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scratchpads_user_id_users_id_fk": { + "name": "scratchpads_user_id_users_id_fk", + "tableFrom": "scratchpads", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scratchpads_userId_unique": { + "name": "scratchpads_userId_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.source_links": { + "name": "source_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "specific_location": { + "name": "specific_location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "source_links_source_id_idx": { + "name": "source_links_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "source_links_node_id_idx": { + "name": "source_links_node_id_idx", + "columns": [ + { + "expression": "node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "source_links_source_id_sources_id_fk": { + "name": "source_links_source_id_sources_id_fk", + "tableFrom": "source_links", + "tableTo": "sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "source_links_node_id_nodes_id_fk": { + "name": "source_links_node_id_nodes_id_fk", + "tableFrom": "source_links", + "tableTo": "nodes", + "columnsFrom": [ + "node_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "source_links_sourceId_nodeId_unique": { + "name": "source_links_sourceId_nodeId_unique", + "nullsNotDistinct": false, + "columns": [ + "source_id", + "node_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sources": { + "name": "sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_source": { + "name": "parent_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_ingested_at": { + "name": "last_ingested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sources_user_id_idx": { + "name": "sources_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sources_status_idx": { + "name": "sources_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sources_user_id_users_id_fk": { + "name": "sources_user_id_users_id_fk", + "tableFrom": "sources", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sources_userId_type_externalId_unique": { + "name": "sources_userId_type_externalId_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "type", + "external_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_profiles": { + "name": "user_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_updated_at": { + "name": "last_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_profiles_user_id_users_id_fk": { + "name": "user_profiles_user_id_users_id_fk", + "tableFrom": "user_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index d3f9ff7..080aa3f 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -50,6 +50,13 @@ "when": 1747562814401, "tag": "0006_faulty_ezekiel_stane", "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1772622104832, + "tag": "0007_illegal_lila_cheney", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts index 2857a98..fc9cc46 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -310,3 +310,27 @@ export const userProfilesRelations = relations(userProfiles, ({ one }) => ({ references: [users.id], }), })); + +export const scratchpads = pgTable( + "scratchpads", + { + id: typeId("scratchpad").primaryKey().notNull(), + userId: text() + .references(() => users.id) + .notNull(), + content: text().notNull().default(""), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + }, + (table) => [ + unique().on(table.userId), + index("scratchpads_user_id_idx").on(table.userId), + ], +); + +export const scratchpadsRelations = relations(scratchpads, ({ one }) => ({ + user: one(users, { + fields: [scratchpads.userId], + references: [users.id], + }), +})); diff --git a/src/lib/mcp/mcp-server.ts b/src/lib/mcp/mcp-server.ts index 82ae917..3604391 100644 --- a/src/lib/mcp/mcp-server.ts +++ b/src/lib/mcp/mcp-server.ts @@ -19,6 +19,16 @@ import { querySearchRequestSchema, type QuerySearchRequest, } from "~/lib/schemas/query-search"; +import { + scratchpadReadRequestSchema, + scratchpadWriteRequestSchema, + scratchpadEditRequestSchema, +} from "~/lib/schemas/scratchpad"; +import { + readScratchpad, + writeScratchpad, + editScratchpad, +} from "~/lib/scratchpad"; const transports: { [sessionId: string]: SSEServerTransport } = {}; @@ -93,6 +103,46 @@ server.tool( }, ); +// Read scratchpad +server.tool( + "read scratchpad", + scratchpadReadRequestSchema, + async ({ userId }) => { + const result = await readScratchpad({ userId }); + return { + content: [{ type: "text", text: result.content || "(empty scratchpad)" }], + }; + }, +); + +// Write scratchpad (overwrite or append) +server.tool( + "write scratchpad", + scratchpadWriteRequestSchema, + async (params) => { + const result = await writeScratchpad(params); + return { + content: [ + { type: "text", text: `Scratchpad updated.\n\n${result.content}` }, + ], + }; + }, +); + +// Edit scratchpad (replace text with safeguards) +server.tool("edit scratchpad", scratchpadEditRequestSchema, async (params) => { + const result = await editScratchpad(params); + if (!result.applied) { + return { + content: [{ type: "text", text: `Edit failed: ${result.message}` }], + isError: true, + }; + } + return { + content: [{ type: "text", text: `Edit applied.\n\n${result.content}` }], + }; +}); + export const addTransport = (transport: SSEServerTransport) => { transports[transport.sessionId] = transport; }; diff --git a/src/lib/schemas/scratchpad.ts b/src/lib/schemas/scratchpad.ts new file mode 100644 index 0000000..db16ec9 --- /dev/null +++ b/src/lib/schemas/scratchpad.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; + +export const scratchpadReadRequestSchema = z.object({ + userId: z.string(), +}); + +export const scratchpadWriteRequestSchema = z.object({ + userId: z.string(), + content: z.string(), + mode: z.enum(["overwrite", "append"]).default("overwrite"), +}); + +export const scratchpadEditRequestSchema = z.object({ + userId: z.string(), + oldText: z.string().min(1), + newText: z.string(), +}); + +export const scratchpadResponseSchema = z.object({ + content: z.string(), + updatedAt: z.coerce.date(), +}); + +export const scratchpadEditResponseSchema = scratchpadResponseSchema.extend({ + applied: z.boolean(), + message: z.string().optional(), +}); + +export type ScratchpadReadRequest = z.infer; +export type ScratchpadWriteRequest = z.infer< + typeof scratchpadWriteRequestSchema +>; +export type ScratchpadEditRequest = z.infer; +export type ScratchpadResponse = z.infer; +export type ScratchpadEditResponse = z.infer< + typeof scratchpadEditResponseSchema +>; diff --git a/src/lib/scratchpad.ts b/src/lib/scratchpad.ts new file mode 100644 index 0000000..c6ac5a4 --- /dev/null +++ b/src/lib/scratchpad.ts @@ -0,0 +1,86 @@ +import type { + ScratchpadReadRequest, + ScratchpadWriteRequest, + ScratchpadEditRequest, + ScratchpadResponse, + ScratchpadEditResponse, +} from "./schemas/scratchpad"; +import db from "~/db"; +import { scratchpads } from "~/db/schema"; + +export async function readScratchpad( + params: ScratchpadReadRequest, +): Promise { + const existing = await db.query.scratchpads.findFirst({ + where: (s, { eq }) => eq(s.userId, params.userId), + }); + + if (!existing) { + return { content: "", updatedAt: new Date() }; + } + + return { content: existing.content, updatedAt: existing.updatedAt }; +} + +export async function writeScratchpad( + params: ScratchpadWriteRequest, +): Promise { + const { userId, content, mode } = params; + + if (mode === "append") { + const existing = await readScratchpad({ userId }); + const newContent = existing.content + ? existing.content + "\n" + content + : content; + return upsertScratchpad(userId, newContent); + } + + return upsertScratchpad(userId, content); +} + +export async function editScratchpad( + params: ScratchpadEditRequest, +): Promise { + const { userId, oldText, newText } = params; + const existing = await readScratchpad({ userId }); + + if (!existing.content.includes(oldText)) { + return { + ...existing, + applied: false, + message: `The text to replace was not found in the scratchpad. The scratchpad content is:\n${existing.content}`, + }; + } + + const occurrences = existing.content.split(oldText).length - 1; + if (occurrences > 1) { + return { + ...existing, + applied: false, + message: `The text to replace appears ${occurrences} times in the scratchpad. Please provide a longer, more specific text snippet that appears exactly once.`, + }; + } + + const newContent = existing.content.replace(oldText, newText); + const result = await upsertScratchpad(userId, newContent); + + return { ...result, applied: true }; +} + +async function upsertScratchpad( + userId: string, + content: string, +): Promise { + const now = new Date(); + const rows = await db + .insert(scratchpads) + .values({ userId, content, updatedAt: now }) + .onConflictDoUpdate({ + target: [scratchpads.userId], + set: { content, updatedAt: now }, + }) + .returning(); + + const row = rows[0]!; + return { content: row.content, updatedAt: row.updatedAt }; +} diff --git a/src/routes/scratchpad/edit.post.ts b/src/routes/scratchpad/edit.post.ts new file mode 100644 index 0000000..c6fb8c8 --- /dev/null +++ b/src/routes/scratchpad/edit.post.ts @@ -0,0 +1,10 @@ +import { + scratchpadEditRequestSchema, + scratchpadEditResponseSchema, +} from "~/lib/schemas/scratchpad"; +import { editScratchpad } from "~/lib/scratchpad"; + +export default defineEventHandler(async (event) => { + const params = scratchpadEditRequestSchema.parse(await readBody(event)); + return scratchpadEditResponseSchema.parse(await editScratchpad(params)); +}); diff --git a/src/routes/scratchpad/read.post.ts b/src/routes/scratchpad/read.post.ts new file mode 100644 index 0000000..b27ff23 --- /dev/null +++ b/src/routes/scratchpad/read.post.ts @@ -0,0 +1,10 @@ +import { + scratchpadReadRequestSchema, + scratchpadResponseSchema, +} from "~/lib/schemas/scratchpad"; +import { readScratchpad } from "~/lib/scratchpad"; + +export default defineEventHandler(async (event) => { + const params = scratchpadReadRequestSchema.parse(await readBody(event)); + return scratchpadResponseSchema.parse(await readScratchpad(params)); +}); diff --git a/src/routes/scratchpad/write.post.ts b/src/routes/scratchpad/write.post.ts new file mode 100644 index 0000000..aba8efa --- /dev/null +++ b/src/routes/scratchpad/write.post.ts @@ -0,0 +1,10 @@ +import { + scratchpadWriteRequestSchema, + scratchpadResponseSchema, +} from "~/lib/schemas/scratchpad"; +import { writeScratchpad } from "~/lib/scratchpad"; + +export default defineEventHandler(async (event) => { + const params = scratchpadWriteRequestSchema.parse(await readBody(event)); + return scratchpadResponseSchema.parse(await writeScratchpad(params)); +}); diff --git a/src/types/typeid.ts b/src/types/typeid.ts index 30c1e06..9a8be7e 100644 --- a/src/types/typeid.ts +++ b/src/types/typeid.ts @@ -14,6 +14,7 @@ export const ID_TYPE_NAMES = [ "source_link", "user_profile", "message", + "scratchpad", ] as const; export const ID_TYPE_PREFIXES: Record<(typeof ID_TYPE_NAMES)[number], string> = @@ -28,6 +29,7 @@ export const ID_TYPE_PREFIXES: Record<(typeof ID_TYPE_NAMES)[number], string> = source_link: "sln", user_profile: "upf", message: "msg", + scratchpad: "spad", } as const; export type IdType = (typeof ID_TYPE_NAMES)[number];