@m6d/cortex-server 1.1.1 → 1.1.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.
Files changed (42) hide show
  1. package/README.md +38 -38
  2. package/dist/src/factory.d.ts +13 -1
  3. package/dist/src/ws/index.d.ts +1 -1
  4. package/package.json +54 -54
  5. package/src/adapters/database.ts +21 -28
  6. package/src/adapters/minio.ts +69 -69
  7. package/src/adapters/mssql.ts +171 -195
  8. package/src/adapters/storage.ts +4 -4
  9. package/src/ai/fetch.ts +31 -31
  10. package/src/ai/helpers.ts +18 -22
  11. package/src/ai/index.ts +101 -113
  12. package/src/ai/interceptors/resolve-captured-files.ts +42 -49
  13. package/src/ai/prompt.ts +80 -83
  14. package/src/ai/tools/call-endpoint.tool.ts +75 -82
  15. package/src/ai/tools/capture-files.tool.ts +15 -17
  16. package/src/ai/tools/execute-code.tool.ts +73 -80
  17. package/src/ai/tools/query-graph.tool.ts +17 -17
  18. package/src/auth/middleware.ts +51 -51
  19. package/src/cli/extract-endpoints.ts +436 -474
  20. package/src/config.ts +124 -134
  21. package/src/db/migrate.ts +13 -13
  22. package/src/db/migrations/20260309012148_cloudy_maria_hill/snapshot.json +303 -303
  23. package/src/db/schema.ts +46 -58
  24. package/src/factory.ts +136 -139
  25. package/src/graph/generate-cypher.ts +97 -97
  26. package/src/graph/helpers.ts +37 -37
  27. package/src/graph/index.ts +20 -20
  28. package/src/graph/neo4j.ts +82 -89
  29. package/src/graph/resolver.ts +201 -211
  30. package/src/graph/seed.ts +101 -114
  31. package/src/graph/types.ts +88 -88
  32. package/src/graph/validate.ts +55 -57
  33. package/src/index.ts +5 -5
  34. package/src/routes/chat.ts +23 -23
  35. package/src/routes/files.ts +75 -80
  36. package/src/routes/threads.ts +52 -54
  37. package/src/routes/ws.ts +22 -22
  38. package/src/types.ts +30 -30
  39. package/src/ws/connections.ts +11 -11
  40. package/src/ws/events.ts +2 -2
  41. package/src/ws/index.ts +1 -5
  42. package/src/ws/notify.ts +4 -4
@@ -1,203 +1,179 @@
1
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";
2
+ import { and, desc, eq, getColumns, inArray, type InferInsertModel } from "drizzle-orm";
10
3
  import { drizzle } from "drizzle-orm/node-mssql";
11
4
  import { threads, messages, capturedFiles } from "../db/schema";
12
5
  import type { DatabaseAdapter } from "./database";
13
6
  import type { StorageAdapter } from "./storage";
14
7
  import type { Thread, CapturedFileInput } from "../types";
15
8
 
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;
9
+ export function createMssqlAdapter(connectionString: string, storage: StorageAdapter) {
10
+ const db = drizzle(connectionString, { casing: "snake_case" });
11
+
12
+ const adapter: DatabaseAdapter = {
13
+ threads: {
14
+ async list(userId: string, agentId: string) {
15
+ return (await db
16
+ .select()
17
+ .from(threads)
18
+ .where(and(eq(threads.userId, userId), eq(threads.agentId, agentId)))
19
+ .orderBy(desc(threads.createdAt))) as Thread[];
20
+ },
21
+
22
+ async getById(userId: string, threadId: string) {
23
+ const result = await db
24
+ .select()
25
+ .top(1)
26
+ .from(threads)
27
+ .where(and(eq(threads.id, threadId), eq(threads.userId, userId)))
28
+ .execute();
29
+ return result.length ? (result[0] as Thread) : null;
30
+ },
31
+
32
+ async create(userId: string, agentId: string) {
33
+ const result = await db
34
+ .insert(threads)
35
+ .output()
36
+ .values({ userId, agentId })
37
+ .execute();
38
+ return result[0] as Thread;
39
+ },
40
+
41
+ async delete(userId: string, threadId: string) {
42
+ const capturedFilePaths = await db
43
+ .select({ id: capturedFiles.id })
44
+ .from(capturedFiles)
45
+ .innerJoin(messages, eq(messages.id, capturedFiles.messageId))
46
+ .innerJoin(threads, eq(threads.id, messages.threadId))
47
+ .where(and(eq(threads.id, threadId), eq(threads.userId, userId)))
48
+ .execute()
49
+ .then((x) => x.map((y) => `captured_files/${y.id}`));
50
+
51
+ await storage.delete(capturedFilePaths);
52
+
53
+ await db
54
+ .delete(threads)
55
+ .where(and(eq(threads.id, threadId), eq(threads.userId, userId)))
56
+ .execute();
57
+ },
58
+
59
+ async updateTitle(threadId: string, title: string) {
60
+ await db.update(threads).set({ title }).where(eq(threads.id, threadId)).execute();
61
+ },
62
+
63
+ async updateSession(threadId: string, session: Record<string, unknown>) {
64
+ await db.update(threads).set({ session }).where(eq(threads.id, threadId)).execute();
65
+ },
66
+ },
67
+
68
+ messages: {
69
+ async list(userId: string, threadId: string, opts?: { limit?: number }) {
70
+ return await db
71
+ .select(getColumns(messages))
72
+ .from(messages)
73
+ .innerJoin(threads, eq(threads.id, messages.threadId))
74
+ .where(and(eq(threads.userId, userId), eq(threads.id, threadId)))
75
+ .orderBy(desc(messages.createdAt))
76
+ .offset(0)
77
+ .fetch(opts?.limit ?? 100)
78
+ .then((x) => x.reverse());
79
+ },
80
+
81
+ async upsert(threadId: string, messagesToInsert: UIMessage[]) {
82
+ const ids = messagesToInsert.map((x) => x.id);
83
+
84
+ const existingIds = await db
85
+ .select({ id: messages.id })
86
+ .from(messages)
87
+ .where(inArray(messages.id, ids))
88
+ .execute()
89
+ .then((x) => x.map((y) => y.id));
90
+
91
+ const newMessages = messagesToInsert.filter((x) => !existingIds.includes(x.id));
92
+
93
+ if (newMessages.length) {
94
+ await db
95
+ .insert(messages)
96
+ .values(
97
+ newMessages.map(
98
+ (x) =>
99
+ ({
100
+ id: x.id,
101
+ role: x.role,
102
+ threadId,
103
+ content: x,
104
+ text: x.parts.find((y) => y.type === "text")?.text,
105
+ }) satisfies InferInsertModel<typeof messages>,
106
+ ),
107
+ )
108
+ .execute();
109
+ }
110
+
111
+ const existingMessages = messagesToInsert.filter((x) => existingIds.includes(x.id));
112
+ if (existingMessages.length) {
113
+ await db.transaction(async (tx) => {
114
+ for (const message of existingMessages) {
115
+ await tx
116
+ .update(messages)
117
+ .set({
118
+ content: message,
119
+ text: message.parts.find((x) => x.type === "text")?.text,
120
+ })
121
+ .where(eq(messages.id, message.id))
122
+ .execute();
123
+ }
124
+ });
125
+ }
126
+ },
127
+ },
128
+
129
+ capturedFiles: {
130
+ async create(
131
+ userId: string,
132
+ messageId: string,
133
+ toolPart: ToolUIPart,
134
+ files: CapturedFileInput[],
135
+ ) {
136
+ const result = await db
137
+ .insert(capturedFiles)
138
+ .output({ id: capturedFiles.id })
139
+ .values(
140
+ files.map(
141
+ (file) =>
142
+ ({
143
+ userId,
144
+ messageId,
145
+ toolPart,
146
+ name: file.file.name,
147
+ agentGeneratedId: file.id,
148
+ }) satisfies InferInsertModel<typeof capturedFiles>,
149
+ ),
150
+ )
151
+ .execute()
152
+ .then((x) => x.map((y) => y.id));
153
+
154
+ await Promise.all(
155
+ result.map(
156
+ async (id, idx) =>
157
+ await storage.put(`captured_files/${id}`, files[idx]!.file.bytes),
158
+ ),
159
+ );
160
+
161
+ return result.map((r, i) => ({ id: files[i]!.id, uploadId: r }));
162
+ },
163
+
164
+ async getById(id: string, userId: string) {
165
+ const file = await db
166
+ .select()
167
+ .top(1)
168
+ .from(capturedFiles)
169
+ .where(and(eq(capturedFiles.id, id), eq(capturedFiles.userId, userId)))
170
+ .execute()
171
+ .then((x) => (x.length ? x[0] : null));
172
+
173
+ return file ? { name: file.name } : null;
174
+ },
175
+ },
176
+ };
177
+
178
+ return adapter;
203
179
  }
@@ -1,6 +1,6 @@
1
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>;
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
6
  };
package/src/ai/fetch.ts CHANGED
@@ -1,39 +1,39 @@
1
1
  import type { ResolvedCortexAgentConfig } from "../config.ts";
2
2
 
3
3
  export async function fetchBackend(
4
- path: string,
5
- backendFetch: NonNullable<ResolvedCortexAgentConfig["backendFetch"]>,
6
- token: string,
7
- options?: RequestInit,
4
+ path: string,
5
+ backendFetch: NonNullable<ResolvedCortexAgentConfig["backendFetch"]>,
6
+ token: string,
7
+ options?: RequestInit,
8
8
  ) {
9
- let finalOptions = options;
9
+ let finalOptions = options;
10
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
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
+ }
25
26
  }
26
- }
27
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
- });
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
39
  }
package/src/ai/helpers.ts CHANGED
@@ -2,35 +2,31 @@ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
2
2
  import type { ResolvedCortexAgentConfig } from "../config.ts";
3
3
 
4
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
- });
5
+ const provider = createOpenAICompatible({
6
+ name: config.providerName ?? "default",
7
+ baseURL: config.baseURL,
8
+ apiKey: config.apiKey,
9
+ });
10
10
 
11
- return provider(config.modelName);
11
+ return provider(config.modelName);
12
12
  }
13
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
- });
14
+ export function createEmbeddingModel(config: ResolvedCortexAgentConfig["embedding"]) {
15
+ const provider = createOpenAICompatible({
16
+ name: "embedding",
17
+ baseURL: config.baseURL,
18
+ apiKey: config.apiKey,
19
+ });
22
20
 
23
- return provider.textEmbeddingModel(config.modelName);
21
+ return provider.textEmbeddingModel(config.modelName);
24
22
  }
25
23
 
26
- export function streamToBase64(stream: ReadableStream<Uint8Array>) {
27
- return new Response(stream)
28
- .arrayBuffer()
29
- .then((buffer) => Buffer.from(buffer).toString("base64"));
24
+ export async function streamToBase64(stream: ReadableStream<Uint8Array>) {
25
+ return new Response(stream)
26
+ .arrayBuffer()
27
+ .then((buffer) => Buffer.from(buffer).toString("base64"));
30
28
  }
31
29
 
32
30
  export function tokenToUserId(token: string) {
33
- return JSON.parse(Buffer.from(token.split(".")[1]!, "base64").toString())[
34
- "sub"
35
- ] as string;
31
+ return JSON.parse(Buffer.from(token.split(".")[1]!, "base64").toString())["sub"] as string;
36
32
  }