@m6d/cortex-server 1.0.0

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.
Files changed (82) hide show
  1. package/README.md +64 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/src/adapters/database.d.ts +27 -0
  4. package/dist/src/adapters/minio.d.ts +10 -0
  5. package/dist/src/adapters/mssql.d.ts +3 -0
  6. package/dist/src/adapters/storage.d.ts +6 -0
  7. package/dist/src/ai/fetch.d.ts +2 -0
  8. package/dist/src/ai/helpers.d.ts +5 -0
  9. package/dist/src/ai/index.d.ts +4 -0
  10. package/dist/src/ai/interceptors/resolve-captured-files.d.ts +11 -0
  11. package/dist/src/ai/prompt.d.ts +4 -0
  12. package/dist/src/ai/tools/call-endpoint.tool.d.ts +7 -0
  13. package/dist/src/ai/tools/capture-files.tool.d.ts +6 -0
  14. package/dist/src/ai/tools/execute-code.tool.d.ts +4 -0
  15. package/dist/src/ai/tools/query-graph.tool.d.ts +5 -0
  16. package/dist/src/auth/middleware.d.ts +4 -0
  17. package/dist/src/cli/extract-endpoints.d.ts +6 -0
  18. package/dist/src/config.d.ts +145 -0
  19. package/dist/src/db/migrate.d.ts +1 -0
  20. package/dist/src/db/schema.d.ts +345 -0
  21. package/dist/src/factory.d.ts +17 -0
  22. package/dist/src/graph/generate-cypher.d.ts +22 -0
  23. package/dist/src/graph/helpers.d.ts +60 -0
  24. package/dist/src/graph/index.d.ts +11 -0
  25. package/dist/src/graph/neo4j.d.ts +18 -0
  26. package/dist/src/graph/resolver.d.ts +51 -0
  27. package/dist/src/graph/seed.d.ts +19 -0
  28. package/dist/src/graph/types.d.ts +104 -0
  29. package/dist/src/graph/validate.d.ts +2 -0
  30. package/dist/src/index.d.ts +10 -0
  31. package/dist/src/routes/chat.d.ts +3 -0
  32. package/dist/src/routes/files.d.ts +3 -0
  33. package/dist/src/routes/index.d.ts +4 -0
  34. package/dist/src/routes/threads.d.ts +3 -0
  35. package/dist/src/routes/ws.d.ts +3 -0
  36. package/dist/src/types.d.ts +56 -0
  37. package/dist/src/ws/connections.d.ts +4 -0
  38. package/dist/src/ws/events.d.ts +8 -0
  39. package/dist/src/ws/index.d.ts +3 -0
  40. package/dist/src/ws/notify.d.ts +2 -0
  41. package/index.ts +1 -0
  42. package/package.json +57 -0
  43. package/src/adapters/database.ts +33 -0
  44. package/src/adapters/minio.ts +89 -0
  45. package/src/adapters/mssql.ts +203 -0
  46. package/src/adapters/storage.ts +6 -0
  47. package/src/ai/fetch.ts +39 -0
  48. package/src/ai/helpers.ts +36 -0
  49. package/src/ai/index.ts +145 -0
  50. package/src/ai/interceptors/resolve-captured-files.ts +64 -0
  51. package/src/ai/prompt.ts +120 -0
  52. package/src/ai/tools/call-endpoint.tool.ts +96 -0
  53. package/src/ai/tools/capture-files.tool.ts +22 -0
  54. package/src/ai/tools/execute-code.tool.ts +108 -0
  55. package/src/ai/tools/query-graph.tool.ts +35 -0
  56. package/src/auth/middleware.ts +63 -0
  57. package/src/cli/extract-endpoints.ts +588 -0
  58. package/src/config.ts +155 -0
  59. package/src/db/migrate.ts +21 -0
  60. package/src/db/migrations/20260309012148_cloudy_maria_hill/migration.sql +36 -0
  61. package/src/db/migrations/20260309012148_cloudy_maria_hill/snapshot.json +305 -0
  62. package/src/db/schema.ts +77 -0
  63. package/src/factory.ts +159 -0
  64. package/src/graph/generate-cypher.ts +179 -0
  65. package/src/graph/helpers.ts +68 -0
  66. package/src/graph/index.ts +47 -0
  67. package/src/graph/neo4j.ts +117 -0
  68. package/src/graph/resolver.ts +357 -0
  69. package/src/graph/seed.ts +172 -0
  70. package/src/graph/types.ts +152 -0
  71. package/src/graph/validate.ts +80 -0
  72. package/src/index.ts +27 -0
  73. package/src/routes/chat.ts +38 -0
  74. package/src/routes/files.ts +105 -0
  75. package/src/routes/index.ts +4 -0
  76. package/src/routes/threads.ts +69 -0
  77. package/src/routes/ws.ts +33 -0
  78. package/src/types.ts +50 -0
  79. package/src/ws/connections.ts +23 -0
  80. package/src/ws/events.ts +6 -0
  81. package/src/ws/index.ts +7 -0
  82. package/src/ws/notify.ts +9 -0
@@ -0,0 +1,3 @@
1
+ import { Hono } from "hono";
2
+ import type { CortexAppEnv } from "../types.ts";
3
+ export declare function createThreadRoutes(): Hono<CortexAppEnv, import("hono/types").BlankSchema, "/">;
@@ -0,0 +1,3 @@
1
+ import { Hono } from "hono";
2
+ import type { AppEnv } from "../types.ts";
3
+ export declare function createWsRoute(): Hono<AppEnv, import("hono/types").BlankSchema, "/">;
@@ -0,0 +1,56 @@
1
+ import type { UIMessage } from "ai";
2
+ import type { ResolvedCortexAgentConfig } from "./config";
3
+ export type Thread = {
4
+ id: string;
5
+ userId: string;
6
+ agentId: string;
7
+ title: string | null;
8
+ session: Record<string, unknown> | null;
9
+ createdAt: Date;
10
+ updatedAt: Date;
11
+ };
12
+ export type StoredMessage = {
13
+ id: string;
14
+ threadId: string;
15
+ text: string | null;
16
+ content: UIMessage;
17
+ role: "system" | "user" | "assistant" | "tool";
18
+ createdAt: Date;
19
+ updatedAt: Date;
20
+ };
21
+ export type CapturedFileInput = {
22
+ id: string;
23
+ file: {
24
+ name: string;
25
+ bytes: string;
26
+ };
27
+ };
28
+ export type AppEnv = {
29
+ Bindings: {};
30
+ Variables: {
31
+ user: {
32
+ id: string;
33
+ token: string;
34
+ } | undefined;
35
+ };
36
+ };
37
+ export type AuthedAppEnv = {
38
+ Bindings: {};
39
+ Variables: {
40
+ user: {
41
+ id: string;
42
+ token: string;
43
+ };
44
+ };
45
+ };
46
+ export type CortexAppEnv = {
47
+ Bindings: {};
48
+ Variables: {
49
+ user: {
50
+ id: string;
51
+ token: string;
52
+ };
53
+ agentConfig: ResolvedCortexAgentConfig;
54
+ agentId: string;
55
+ };
56
+ };
@@ -0,0 +1,4 @@
1
+ import type { WSContext } from "hono/ws";
2
+ export declare function addConnection(userId: string, ws: WSContext): void;
3
+ export declare function removeConnection(userId: string, ws: WSContext): void;
4
+ export declare function getConnections(userId: string): WSContext<unknown>[];
@@ -0,0 +1,8 @@
1
+ export type ThreadTitleUpdatedEvent = {
2
+ type: "thread:title-updated";
3
+ payload: {
4
+ threadId: string;
5
+ title: string;
6
+ };
7
+ };
8
+ export type WsEvent = ThreadTitleUpdatedEvent;
@@ -0,0 +1,3 @@
1
+ export { addConnection, removeConnection, getConnections, } from "./connections.ts";
2
+ export { notify } from "./notify.ts";
3
+ export type { WsEvent, ThreadTitleUpdatedEvent } from "./events.ts";
@@ -0,0 +1,2 @@
1
+ import type { WsEvent } from "./events.ts";
2
+ export declare function notify(userId: string, event: WsEvent): void;
package/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/index";
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@m6d/cortex-server",
3
+ "version": "1.0.0",
4
+ "description": "Reusable AI agent chat server library for Hono + Bun",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "bun": "./index.ts",
11
+ "default": "./index.ts"
12
+ }
13
+ },
14
+ "main": "./index.ts",
15
+ "types": "./dist/index.d.ts",
16
+ "files": [
17
+ "dist",
18
+ "src",
19
+ "index.ts",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.build.json",
24
+ "db:generate": "drizzle-kit generate",
25
+ "db:migrate": "drizzle-kit migrate"
26
+ },
27
+ "dependencies": {
28
+ "@ai-sdk/provider": "^3.0.0",
29
+ "@hono/zod-validator": "^0.5.0",
30
+ "drizzle-orm": "^1.0.0-beta.15-859cf75",
31
+ "minio": "^8.0.7",
32
+ "mssql": "^12.2.0"
33
+ },
34
+ "devDependencies": {
35
+ "@ai-sdk/openai-compatible": "^2.0.0",
36
+ "@types/bun": "latest",
37
+ "@types/minio": "^7.1.1",
38
+ "@types/mssql": "^9.1.5",
39
+ "ai": "^6.0.104",
40
+ "drizzle-kit": "^1.0.0-beta.15-859cf75",
41
+ "hono": "^4.12.3",
42
+ "typescript": "^5"
43
+ },
44
+ "peerDependencies": {
45
+ "@ai-sdk/openai-compatible": "^2.0.0",
46
+ "ai": "^6.0.0",
47
+ "hono": "^4.12.3",
48
+ "jose": "^6.1.3",
49
+ "zod": "^3.24.0"
50
+ },
51
+ "engines": {
52
+ "bun": ">=1.0.0"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ }
57
+ }
@@ -0,0 +1,33 @@
1
+ import type { ToolUIPart, UIMessage } from "ai";
2
+ import type { Thread, StoredMessage, CapturedFileInput } from "../types";
3
+
4
+ export type DatabaseAdapter = {
5
+ threads: {
6
+ list(userId: string, agentId: string): Promise<Thread[]>;
7
+ getById(userId: string, threadId: string): Promise<Thread | null>;
8
+ create(userId: string, agentId: string): Promise<Thread>;
9
+ delete(userId: string, threadId: string): Promise<void>;
10
+ updateTitle(threadId: string, title: string): Promise<void>;
11
+ updateSession(
12
+ threadId: string,
13
+ session: Record<string, unknown>,
14
+ ): Promise<void>;
15
+ };
16
+ messages: {
17
+ list(
18
+ userId: string,
19
+ threadId: string,
20
+ opts?: { limit?: number },
21
+ ): Promise<StoredMessage[]>;
22
+ upsert(threadId: string, messages: UIMessage[]): Promise<void>;
23
+ };
24
+ capturedFiles: {
25
+ create(
26
+ userId: string,
27
+ messageId: string,
28
+ toolPart: ToolUIPart,
29
+ files: CapturedFileInput[],
30
+ ): Promise<{ id: string; uploadId: string }[]>;
31
+ getById(id: string, userId: string): Promise<{ name: string } | null>;
32
+ };
33
+ };
@@ -0,0 +1,89 @@
1
+ import * as Minio from "minio";
2
+ import { Readable } from "node:stream";
3
+ import type { StorageAdapter } from "./storage";
4
+
5
+ export type MinioConfig = {
6
+ endPoint: string;
7
+ port: number;
8
+ useSSL: boolean;
9
+ accessKey: string;
10
+ secretKey: string;
11
+ bucketName?: string;
12
+ };
13
+
14
+ export function createMinioAdapter(config: MinioConfig) {
15
+ if (!config.endPoint) {
16
+ throw new Error("MinIO adapter: endPoint is required");
17
+ }
18
+
19
+ const client = new Minio.Client({
20
+ endPoint: config.endPoint,
21
+ port: config.port,
22
+ useSSL: config.useSSL,
23
+ accessKey: config.accessKey,
24
+ secretKey: config.secretKey,
25
+ });
26
+
27
+ const bucketName = config.bucketName ?? "ai-storage";
28
+ let bucketReady = false;
29
+
30
+ async function ensureBucket() {
31
+ if (bucketReady) return;
32
+ if (!(await client.bucketExists(bucketName))) {
33
+ await client.makeBucket(bucketName);
34
+ }
35
+ bucketReady = true;
36
+ }
37
+
38
+ const adapter: StorageAdapter = {
39
+ async put(path: string, data: string) {
40
+ try {
41
+ await ensureBucket();
42
+ await client.putObject(bucketName, path, Buffer.from(data, "base64"));
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ },
48
+
49
+ async get(path: string) {
50
+ try {
51
+ const stream = await client.getObject(bucketName, path);
52
+ return await new Promise<string>((resolve, reject) => {
53
+ const chunks: Uint8Array[] = [];
54
+
55
+ stream.on("error", () => {
56
+ reject();
57
+ });
58
+
59
+ stream.on("data", (chunk) => chunks.push(chunk));
60
+ stream.on("end", () => {
61
+ const buffer = Buffer.concat(chunks);
62
+ resolve(buffer.toString("base64"));
63
+ });
64
+ });
65
+ } catch {
66
+ return false;
67
+ }
68
+ },
69
+
70
+ async delete(paths: string[]) {
71
+ try {
72
+ await client.removeObjects(
73
+ bucketName,
74
+ paths.map((x) => ({ name: x })),
75
+ );
76
+ return true;
77
+ } catch {
78
+ return false;
79
+ }
80
+ },
81
+
82
+ async stream(path: string) {
83
+ const nodeStream = await client.getObject(bucketName, path);
84
+ return Readable.toWeb(nodeStream) as unknown as ReadableStream;
85
+ },
86
+ };
87
+
88
+ return adapter;
89
+ }
@@ -0,0 +1,203 @@
1
+ import type { ToolUIPart, UIMessage } from "ai";
2
+ import {
3
+ and,
4
+ desc,
5
+ eq,
6
+ getColumns,
7
+ inArray,
8
+ type InferInsertModel,
9
+ } from "drizzle-orm";
10
+ import { drizzle } from "drizzle-orm/node-mssql";
11
+ import { threads, messages, capturedFiles } from "../db/schema";
12
+ import type { DatabaseAdapter } from "./database";
13
+ import type { StorageAdapter } from "./storage";
14
+ import type { Thread, CapturedFileInput } from "../types";
15
+
16
+ export function createMssqlAdapter(
17
+ connectionString: string,
18
+ storage: StorageAdapter,
19
+ ) {
20
+ const db = drizzle(connectionString, { casing: "snake_case" });
21
+
22
+ const adapter: DatabaseAdapter = {
23
+ threads: {
24
+ async list(userId: string, agentId: string) {
25
+ return (await db
26
+ .select()
27
+ .from(threads)
28
+ .where(and(eq(threads.userId, userId), eq(threads.agentId, agentId)))
29
+ .orderBy(desc(threads.createdAt))) as Thread[];
30
+ },
31
+
32
+ async getById(userId: string, threadId: string) {
33
+ const result = await db
34
+ .select()
35
+ .top(1)
36
+ .from(threads)
37
+ .where(and(eq(threads.id, threadId), eq(threads.userId, userId)))
38
+ .execute();
39
+ return result.length ? (result[0] as Thread) : null;
40
+ },
41
+
42
+ async create(userId: string, agentId: string) {
43
+ const result = await db
44
+ .insert(threads)
45
+ .output()
46
+ .values({ userId, agentId })
47
+ .execute();
48
+ return result[0] as Thread;
49
+ },
50
+
51
+ async delete(userId: string, threadId: string) {
52
+ const capturedFilePaths = await db
53
+ .select({ id: capturedFiles.id })
54
+ .from(capturedFiles)
55
+ .innerJoin(messages, eq(messages.id, capturedFiles.messageId))
56
+ .innerJoin(threads, eq(threads.id, messages.threadId))
57
+ .where(and(eq(threads.id, threadId), eq(threads.userId, userId)))
58
+ .execute()
59
+ .then((x) => x.map((y) => `captured_files/${y.id}`));
60
+
61
+ await storage.delete(capturedFilePaths);
62
+
63
+ await db
64
+ .delete(threads)
65
+ .where(and(eq(threads.id, threadId), eq(threads.userId, userId)))
66
+ .execute();
67
+ },
68
+
69
+ async updateTitle(threadId: string, title: string) {
70
+ await db
71
+ .update(threads)
72
+ .set({ title })
73
+ .where(eq(threads.id, threadId))
74
+ .execute();
75
+ },
76
+
77
+ async updateSession(threadId: string, session: Record<string, unknown>) {
78
+ await db
79
+ .update(threads)
80
+ .set({ session })
81
+ .where(eq(threads.id, threadId))
82
+ .execute();
83
+ },
84
+ },
85
+
86
+ messages: {
87
+ async list(userId: string, threadId: string, opts?: { limit?: number }) {
88
+ return await db
89
+ .select(getColumns(messages))
90
+ .from(messages)
91
+ .innerJoin(threads, eq(threads.id, messages.threadId))
92
+ .where(and(eq(threads.userId, userId), eq(threads.id, threadId)))
93
+ .orderBy(desc(messages.createdAt))
94
+ .offset(0)
95
+ .fetch(opts?.limit ?? 100)
96
+ .then((x) => x.reverse());
97
+ },
98
+
99
+ async upsert(threadId: string, messagesToInsert: UIMessage[]) {
100
+ const ids = messagesToInsert.map((x) => x.id);
101
+
102
+ const existingIds = await db
103
+ .select({ id: messages.id })
104
+ .from(messages)
105
+ .where(inArray(messages.id, ids))
106
+ .execute()
107
+ .then((x) => x.map((y) => y.id));
108
+
109
+ const newMessages = messagesToInsert.filter(
110
+ (x) => !existingIds.includes(x.id),
111
+ );
112
+
113
+ if (newMessages.length) {
114
+ await db
115
+ .insert(messages)
116
+ .values(
117
+ newMessages.map(
118
+ (x) =>
119
+ ({
120
+ id: x.id,
121
+ role: x.role,
122
+ threadId,
123
+ content: x,
124
+ text: x.parts.find((y) => y.type === "text")?.text,
125
+ }) satisfies InferInsertModel<typeof messages>,
126
+ ),
127
+ )
128
+ .execute();
129
+ }
130
+
131
+ const existingMessages = messagesToInsert.filter((x) =>
132
+ existingIds.includes(x.id),
133
+ );
134
+ if (existingMessages.length) {
135
+ await db.transaction(async (tx) => {
136
+ for (const message of existingMessages) {
137
+ await tx
138
+ .update(messages)
139
+ .set({
140
+ content: message,
141
+ text: message.parts.find((x) => x.type === "text")?.text,
142
+ })
143
+ .where(eq(messages.id, message.id))
144
+ .execute();
145
+ }
146
+ });
147
+ }
148
+ },
149
+ },
150
+
151
+ capturedFiles: {
152
+ async create(
153
+ userId: string,
154
+ messageId: string,
155
+ toolPart: ToolUIPart,
156
+ files: CapturedFileInput[],
157
+ ) {
158
+ const result = await db
159
+ .insert(capturedFiles)
160
+ .output({ id: capturedFiles.id })
161
+ .values(
162
+ files.map(
163
+ (file) =>
164
+ ({
165
+ userId,
166
+ messageId,
167
+ toolPart,
168
+ name: file.file.name,
169
+ agentGeneratedId: file.id,
170
+ }) satisfies InferInsertModel<typeof capturedFiles>,
171
+ ),
172
+ )
173
+ .execute()
174
+ .then((x) => x.map((y) => y.id));
175
+
176
+ await Promise.all(
177
+ result.map(
178
+ async (id, idx) =>
179
+ await storage.put(`captured_files/${id}`, files[idx]!.file.bytes),
180
+ ),
181
+ );
182
+
183
+ return result.map((r, i) => ({ id: files[i]!.id, uploadId: r }));
184
+ },
185
+
186
+ async getById(id: string, userId: string) {
187
+ const file = await db
188
+ .select()
189
+ .top(1)
190
+ .from(capturedFiles)
191
+ .where(
192
+ and(eq(capturedFiles.id, id), eq(capturedFiles.userId, userId)),
193
+ )
194
+ .execute()
195
+ .then((x) => (x.length ? x[0] : null));
196
+
197
+ return file ? { name: file.name } : null;
198
+ },
199
+ },
200
+ };
201
+
202
+ return adapter;
203
+ }
@@ -0,0 +1,6 @@
1
+ export type StorageAdapter = {
2
+ put(path: string, data: string): Promise<boolean>;
3
+ get(path: string): Promise<string | false>;
4
+ delete(paths: string[]): Promise<boolean>;
5
+ stream(path: string): Promise<ReadableStream>;
6
+ };
@@ -0,0 +1,39 @@
1
+ import type { ResolvedCortexAgentConfig } from "../config.ts";
2
+
3
+ export async function fetchBackend(
4
+ path: string,
5
+ backendFetch: NonNullable<ResolvedCortexAgentConfig["backendFetch"]>,
6
+ token: string,
7
+ options?: RequestInit,
8
+ ) {
9
+ let finalOptions = options;
10
+
11
+ // Apply body interceptor if configured and request has a body
12
+ if (
13
+ backendFetch.transformRequestBody &&
14
+ finalOptions?.body &&
15
+ typeof finalOptions.body === "string"
16
+ ) {
17
+ try {
18
+ const parsed = JSON.parse(finalOptions.body) as Record<string, unknown>;
19
+ const transformed = await backendFetch.transformRequestBody(parsed, {
20
+ token,
21
+ });
22
+ finalOptions = { ...finalOptions, body: JSON.stringify(transformed) };
23
+ } catch {
24
+ // If body isn't valid JSON, pass through as-is
25
+ }
26
+ }
27
+
28
+ return fetch(`${backendFetch.baseUrl}${path}`, {
29
+ ...finalOptions,
30
+ headers: {
31
+ "Content-Type": "application/json",
32
+ "X-Requested-With": "cortex-server",
33
+ "X-Service-Api-Key": backendFetch.apiKey,
34
+ "X-Service-Token": token,
35
+ ...backendFetch.headers,
36
+ ...finalOptions?.headers,
37
+ },
38
+ });
39
+ }
@@ -0,0 +1,36 @@
1
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
2
+ import type { ResolvedCortexAgentConfig } from "../config.ts";
3
+
4
+ export function createModel(config: ResolvedCortexAgentConfig["model"]) {
5
+ const provider = createOpenAICompatible({
6
+ name: config.providerName ?? "default",
7
+ baseURL: config.baseURL,
8
+ apiKey: config.apiKey,
9
+ });
10
+
11
+ return provider(config.modelName);
12
+ }
13
+
14
+ export function createEmbeddingModel(
15
+ config: ResolvedCortexAgentConfig["embedding"],
16
+ ) {
17
+ const provider = createOpenAICompatible({
18
+ name: "embedding",
19
+ baseURL: config.baseURL,
20
+ apiKey: config.apiKey,
21
+ });
22
+
23
+ return provider.textEmbeddingModel(config.modelName);
24
+ }
25
+
26
+ export function streamToBase64(stream: ReadableStream<Uint8Array>) {
27
+ return new Response(stream)
28
+ .arrayBuffer()
29
+ .then((buffer) => Buffer.from(buffer).toString("base64"));
30
+ }
31
+
32
+ export function tokenToUserId(token: string) {
33
+ return JSON.parse(Buffer.from(token.split(".")[1]!, "base64").toString())[
34
+ "sub"
35
+ ] as string;
36
+ }