@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
@@ -4,93 +4,86 @@ import type { ResolvedCortexAgentConfig } from "../../config.ts";
4
4
  import { fetchBackend } from "../fetch.ts";
5
5
 
6
6
  export function createCallEndpointTool(
7
- backendFetch: NonNullable<ResolvedCortexAgentConfig["backendFetch"]>,
8
- token: string,
7
+ backendFetch: NonNullable<ResolvedCortexAgentConfig["backendFetch"]>,
8
+ token: string,
9
9
  ) {
10
- return tool({
11
- title: "Call an API endpoint",
12
- description:
13
- "Call an API endpoint on the backend. Use queryGraph first to discover the correct endpoint, parameters, and business rules. For write operations (POST/PUT/DELETE), ALWAYS get explicit user confirmation before calling.",
14
- inputSchema: z.object({
15
- path: z
16
- .string()
17
- .describe(
18
- 'The API path including path parameters. Example: "/items/list" or "/resources/{id}"',
19
- ),
20
- method: z
21
- .enum(["GET", "POST", "PUT", "DELETE"])
22
- .describe("The HTTP method"),
23
- queryParams: z
24
- .string()
25
- .optional()
26
- .describe("Optional JSON-encoded string of query parameters."),
27
- body: z
28
- .string()
29
- .optional()
30
- .describe(
31
- "Optional JSON-encoded string of request body for POST/PUT. For parameters of type file, use `capturedFile#[uploadId]` as the value.",
32
- ),
33
- }),
34
- execute: async ({ path, method, queryParams, body }) => {
35
- let fullPath = path;
36
- if (queryParams) {
37
- const params = JSON.parse(queryParams) as Record<string, unknown>;
38
- const searchParams = new URLSearchParams();
39
- for (const [key, value] of Object.entries(params)) {
40
- if (value == null || value === "") continue;
41
- if (Array.isArray(value)) {
42
- for (const item of value) {
43
- searchParams.append(key, String(item));
10
+ return tool({
11
+ title: "Call an API endpoint",
12
+ description:
13
+ "Call an API endpoint on the backend. Use queryGraph first to discover the correct endpoint, parameters, and business rules. For write operations (POST/PUT/DELETE), ALWAYS get explicit user confirmation before calling.",
14
+ inputSchema: z.object({
15
+ path: z
16
+ .string()
17
+ .describe(
18
+ 'The API path including path parameters. Example: "/items/list" or "/resources/{id}"',
19
+ ),
20
+ method: z.enum(["GET", "POST", "PUT", "DELETE"]).describe("The HTTP method"),
21
+ queryParams: z
22
+ .string()
23
+ .optional()
24
+ .describe("Optional JSON-encoded string of query parameters."),
25
+ body: z
26
+ .string()
27
+ .optional()
28
+ .describe(
29
+ "Optional JSON-encoded string of request body for POST/PUT. For parameters of type file, use `capturedFile#[uploadId]` as the value.",
30
+ ),
31
+ }),
32
+ execute: async ({ path, method, queryParams, body }) => {
33
+ let fullPath = path;
34
+ if (queryParams) {
35
+ const params = JSON.parse(queryParams) as Record<string, unknown>;
36
+ const searchParams = new URLSearchParams();
37
+ for (const [key, value] of Object.entries(params)) {
38
+ if (value == null || value === "") continue;
39
+ if (Array.isArray(value)) {
40
+ for (const item of value) {
41
+ searchParams.append(key, String(item));
42
+ }
43
+ } else {
44
+ searchParams.set(key, String(value));
45
+ }
46
+ }
47
+ const qs = searchParams.toString();
48
+ if (qs) fullPath += `?${qs}`;
44
49
  }
45
- } else {
46
- searchParams.set(key, String(value));
47
- }
48
- }
49
- const qs = searchParams.toString();
50
- if (qs) fullPath += `?${qs}`;
51
- }
52
50
 
53
- const options: RequestInit = { method };
54
- if (body && (method === "POST" || method === "PUT")) {
55
- options.body = body;
56
- }
51
+ const options: RequestInit = { method };
52
+ if (body && (method === "POST" || method === "PUT")) {
53
+ options.body = body;
54
+ }
57
55
 
58
- const response = await fetchBackend(
59
- fullPath,
60
- backendFetch,
61
- token,
62
- options,
63
- );
56
+ const response = await fetchBackend(fullPath, backendFetch, token, options);
64
57
 
65
- if (!response.ok) {
66
- let message: string;
67
- let details: unknown = undefined;
68
- try {
69
- const errorBody = (await response.json()) as Record<string, unknown>;
70
- message =
71
- (errorBody.message as string) ||
72
- (errorBody.title as string) ||
73
- JSON.stringify(errorBody);
74
- if (errorBody.errors) {
75
- details = errorBody.errors;
76
- }
77
- } catch {
78
- message = `Request failed with status ${response.status}`;
79
- }
80
- return JSON.stringify({
81
- error: true,
82
- status: response.status,
83
- message,
84
- ...(details ? { details } : {}),
85
- });
86
- }
58
+ if (!response.ok) {
59
+ let message: string;
60
+ let details: unknown = undefined;
61
+ try {
62
+ const errorBody = (await response.json()) as Record<string, unknown>;
63
+ message =
64
+ (errorBody.message as string) ||
65
+ (errorBody.title as string) ||
66
+ JSON.stringify(errorBody);
67
+ if (errorBody.errors) {
68
+ details = errorBody.errors;
69
+ }
70
+ } catch {
71
+ message = `Request failed with status ${response.status}`;
72
+ }
73
+ return JSON.stringify({
74
+ error: true,
75
+ status: response.status,
76
+ message,
77
+ ...(details ? { details } : {}),
78
+ });
79
+ }
87
80
 
88
- if (response.status === 204) {
89
- return JSON.stringify({ success: true });
90
- }
81
+ if (response.status === 204) {
82
+ return JSON.stringify({ success: true });
83
+ }
91
84
 
92
- const data = await response.json();
93
- return JSON.stringify(data);
94
- },
95
- });
85
+ const data = await response.json();
86
+ return JSON.stringify(data);
87
+ },
88
+ });
96
89
  }
@@ -2,21 +2,19 @@ import { tool } from "ai";
2
2
  import z from "zod";
3
3
 
4
4
  export const captureFilesTool = tool({
5
- description: "Let the user upload a file",
6
- inputSchema: z.object({
7
- files: z.array(
8
- z.object({
9
- label: z
10
- .string()
11
- .describe(
12
- "A label to view to the user to know what file they need to upload",
13
- ),
14
- id: z
15
- .string()
16
- .describe(
17
- "An ID to the file for you to use it when calling apis or later processing",
18
- ),
19
- }),
20
- ),
21
- }),
5
+ description: "Let the user upload a file",
6
+ inputSchema: z.object({
7
+ files: z.array(
8
+ z.object({
9
+ label: z
10
+ .string()
11
+ .describe("A label to view to the user to know what file they need to upload"),
12
+ id: z
13
+ .string()
14
+ .describe(
15
+ "An ID to the file for you to use it when calling apis or later processing",
16
+ ),
17
+ }),
18
+ ),
19
+ }),
22
20
  });
@@ -4,105 +4,98 @@ import type { ResolvedCortexAgentConfig } from "../../config.ts";
4
4
  import { fetchBackend } from "../fetch.ts";
5
5
 
6
6
  type ApiHelper = {
7
- get: (
8
- path: string,
9
- queryParams?: Record<string, unknown>,
10
- ) => Promise<unknown>;
11
- post: (path: string, body?: unknown) => Promise<unknown>;
12
- put: (path: string, body?: unknown) => Promise<unknown>;
13
- del: (path: string) => Promise<unknown>;
7
+ get: (path: string, queryParams?: Record<string, unknown>) => Promise<unknown>;
8
+ post: (path: string, body?: unknown) => Promise<unknown>;
9
+ put: (path: string, body?: unknown) => Promise<unknown>;
10
+ del: (path: string) => Promise<unknown>;
14
11
  };
15
12
 
16
13
  function buildQueryString(params: Record<string, unknown>) {
17
- const searchParams = new URLSearchParams();
18
- for (const [key, value] of Object.entries(params)) {
19
- if (value == null || value === "") continue;
20
- if (Array.isArray(value)) {
21
- for (const item of value) {
22
- searchParams.append(key, String(item));
23
- }
24
- } else {
25
- searchParams.set(key, String(value));
14
+ const searchParams = new URLSearchParams();
15
+ for (const [key, value] of Object.entries(params)) {
16
+ if (value == null || value === "") continue;
17
+ if (Array.isArray(value)) {
18
+ for (const item of value) {
19
+ searchParams.append(key, String(item));
20
+ }
21
+ } else {
22
+ searchParams.set(key, String(value));
23
+ }
26
24
  }
27
- }
28
- const qs = searchParams.toString();
29
- return qs ? `?${qs}` : "";
25
+ const qs = searchParams.toString();
26
+ return qs ? `?${qs}` : "";
30
27
  }
31
28
 
32
29
  function createApiHelper(
33
- backendFetch: NonNullable<ResolvedCortexAgentConfig["backendFetch"]>,
34
- token: string,
30
+ backendFetch: NonNullable<ResolvedCortexAgentConfig["backendFetch"]>,
31
+ token: string,
35
32
  ) {
36
- async function request(method: string, path: string, body?: unknown) {
37
- const options: RequestInit = { method };
38
- if (body !== undefined && (method === "POST" || method === "PUT")) {
39
- options.body = JSON.stringify(body);
40
- }
33
+ async function request(method: string, path: string, body?: unknown) {
34
+ const options: RequestInit = { method };
35
+ if (body !== undefined && (method === "POST" || method === "PUT")) {
36
+ options.body = JSON.stringify(body);
37
+ }
41
38
 
42
- const response = await fetchBackend(path, backendFetch, token, options);
39
+ const response = await fetchBackend(path, backendFetch, token, options);
43
40
 
44
- if (!response.ok) {
45
- let message: string;
46
- try {
47
- const errorBody = await response.json();
48
- message = JSON.stringify(errorBody);
49
- } catch {
50
- message = `HTTP ${response.status}`;
51
- }
52
- throw new Error(
53
- `API ${method} ${path} failed (${response.status}): ${message}`,
54
- );
55
- }
41
+ if (!response.ok) {
42
+ let message: string;
43
+ try {
44
+ const errorBody = await response.json();
45
+ message = JSON.stringify(errorBody);
46
+ } catch {
47
+ message = `HTTP ${response.status}`;
48
+ }
49
+ throw new Error(`API ${method} ${path} failed (${response.status}): ${message}`);
50
+ }
56
51
 
57
- if (response.status === 204) return { success: true };
58
- return response.json();
59
- }
52
+ if (response.status === 204) return { success: true };
53
+ return response.json();
54
+ }
60
55
 
61
- return {
62
- get: (path, queryParams) => {
63
- const fullPath = queryParams
64
- ? path + buildQueryString(queryParams)
65
- : path;
66
- return request("GET", fullPath);
67
- },
68
- post: (path, body) => request("POST", path, body),
69
- put: (path, body) => request("PUT", path, body),
70
- del: (path) => request("DELETE", path),
71
- } satisfies ApiHelper;
56
+ return {
57
+ get: (path, queryParams) => {
58
+ const fullPath = queryParams ? path + buildQueryString(queryParams) : path;
59
+ return request("GET", fullPath);
60
+ },
61
+ post: (path, body) => request("POST", path, body),
62
+ put: (path, body) => request("PUT", path, body),
63
+ del: (path) => request("DELETE", path),
64
+ } satisfies ApiHelper;
72
65
  }
73
66
 
74
67
  const AsyncFunction: new (
75
- ...args: [...paramNames: string[], body: string]
68
+ ...args: [...paramNames: string[], body: string]
76
69
  ) => (...args: unknown[]) => Promise<unknown> = Object.getPrototypeOf(
77
- async function () {},
70
+ async function () {},
78
71
  ).constructor;
79
72
 
80
73
  export function createExecuteCodeTool(
81
- backendFetch: NonNullable<ResolvedCortexAgentConfig["backendFetch"]>,
82
- token: string,
74
+ backendFetch: NonNullable<ResolvedCortexAgentConfig["backendFetch"]>,
75
+ token: string,
83
76
  ) {
84
- return tool({
85
- title: "Executes JavaScript code",
86
- description:
87
- "Run a JavaScript script that calls APIs and returns only relevant data. The script has an `api` helper: api.get(path, params?), api.post(path, body?), api.put(path, body?), api.del(path). Each returns parsed JSON and throws on error. For parameters of type file, use `capturedFile#[uploadId]` as the value.",
88
- inputSchema: z.object({
89
- code: z
90
- .string()
91
- .describe(
92
- "Async JS function body. Use the `api` helper to call endpoints. Return only the data needed for your response.",
93
- ),
94
- }),
95
- execute: async ({ code }) => {
96
- const apiHelper = createApiHelper(backendFetch, token);
77
+ return tool({
78
+ title: "Executes JavaScript code",
79
+ description:
80
+ "Run a JavaScript script that calls APIs and returns only relevant data. The script has an `api` helper: api.get(path, params?), api.post(path, body?), api.put(path, body?), api.del(path). Each returns parsed JSON and throws on error. For parameters of type file, use `capturedFile#[uploadId]` as the value.",
81
+ inputSchema: z.object({
82
+ code: z
83
+ .string()
84
+ .describe(
85
+ "Async JS function body. Use the `api` helper to call endpoints. Return only the data needed for your response.",
86
+ ),
87
+ }),
88
+ execute: async ({ code }) => {
89
+ const apiHelper = createApiHelper(backendFetch, token);
97
90
 
98
- try {
99
- const fn = new AsyncFunction("api", code);
100
- const result = await fn(apiHelper);
101
- return JSON.stringify(result);
102
- } catch (e) {
103
- const message = e instanceof Error ? e.message : String(e);
104
- return JSON.stringify({ error: true, message });
105
- }
106
- },
107
- });
91
+ try {
92
+ const fn = new AsyncFunction("api", code);
93
+ const result = await fn(apiHelper);
94
+ return JSON.stringify(result);
95
+ } catch (e) {
96
+ const message = e instanceof Error ? e.message : String(e);
97
+ return JSON.stringify({ error: true, message });
98
+ }
99
+ },
100
+ });
108
101
  }
@@ -3,9 +3,9 @@ import z from "zod";
3
3
  import type { Neo4jClient } from "../../graph/neo4j.ts";
4
4
 
5
5
  export function createQueryGraphTool(neo4j: Neo4jClient) {
6
- return tool({
7
- title: "Query existing Knowledge Graph",
8
- description: `Run a Cypher query against the Neo4j knowledge graph to find relevant
6
+ return tool({
7
+ title: "Query existing Knowledge Graph",
8
+ description: `Run a Cypher query against the Neo4j knowledge graph to find relevant
9
9
  API endpoints, business rules, and system concepts.
10
10
 
11
11
  Concepts' descriptions are embedded (vectorized), always use this method to search unknowns; use regular keywords only if you received it before or you know it for a fact:
@@ -18,18 +18,18 @@ MATCH (ancestor)-[:QUERIED_VIA|MUTATED_VIA]->(e)
18
18
  RETURN node, e
19
19
  ORDER BY score DESC;
20
20
  \`\`\``,
21
- inputSchema: z.object({
22
- query: z.string().describe("The Cypher query to execute"),
23
- parameters: z
24
- .string()
25
- .optional()
26
- .describe(
27
- 'Optional JSON-encoded string of query parameters. Example: `{"name": "LeaveBalance"}` if you know the exact name; for parameters that need to be embedded first prepend the name with `#`, e.g., `{"#paramName": "Text to be embedded before passed to query"}`',
28
- ),
29
- }),
30
- execute: async ({ query, parameters }) => {
31
- const params = parameters ? JSON.parse(parameters) : undefined;
32
- return neo4j.query(query, params);
33
- },
34
- });
21
+ inputSchema: z.object({
22
+ query: z.string().describe("The Cypher query to execute"),
23
+ parameters: z
24
+ .string()
25
+ .optional()
26
+ .describe(
27
+ 'Optional JSON-encoded string of query parameters. Example: `{"name": "LeaveBalance"}` if you know the exact name; for parameters that need to be embedded first prepend the name with `#`, e.g., `{"#paramName": "Text to be embedded before passed to query"}`',
28
+ ),
29
+ }),
30
+ execute: async ({ query, parameters }) => {
31
+ const params = parameters ? JSON.parse(parameters) : undefined;
32
+ return neo4j.query(query, params);
33
+ },
34
+ });
35
35
  }
@@ -6,58 +6,58 @@ import type { AppEnv, AuthedAppEnv } from "../types";
6
6
  import type { CortexConfig } from "../config";
7
7
 
8
8
  export function createUserLoaderMiddleware(authConfig: CortexConfig["auth"]) {
9
- const jwks = createRemoteJWKSet(new URL(authConfig.jwksUri));
10
-
11
- return createMiddleware<AppEnv>(async (c, next) => {
12
- let token: string | null = null;
13
-
14
- if (authConfig.tokenExtractor) {
15
- token = authConfig.tokenExtractor(c.req.raw);
16
- } else {
17
- // 1. Authorization header
18
- const authHeader = c.req.header("Authorization");
19
- if (authHeader?.startsWith("Bearer ")) {
20
- token = authHeader.slice(7);
21
- }
22
-
23
- // 2. Query parameter
24
- if (!token) {
25
- const url = new URL(c.req.url);
26
- token = url.searchParams.get("token");
27
- }
28
-
29
- // 3. Cookie
30
- if (!token && authConfig.cookieName) {
31
- token = getCookie(c, authConfig.cookieName) ?? null;
32
- }
33
- }
34
-
35
- if (!token) {
36
- await next();
37
- return;
38
- }
39
-
40
- try {
41
- const { payload } = await jwtVerify(token, jwks, {
42
- issuer: authConfig.issuer,
43
- clockTolerance: Infinity,
44
- });
45
-
46
- if (payload.sub) {
47
- c.set("user", { id: payload.sub, token });
48
- }
49
- await next();
50
- } catch {
51
- await next();
52
- }
53
- });
9
+ const jwks = createRemoteJWKSet(new URL(authConfig.jwksUri));
10
+
11
+ return createMiddleware<AppEnv>(async (c, next) => {
12
+ let token: string | null = null;
13
+
14
+ if (authConfig.tokenExtractor) {
15
+ token = authConfig.tokenExtractor(c.req.raw);
16
+ } else {
17
+ // 1. Authorization header
18
+ const authHeader = c.req.header("Authorization");
19
+ if (authHeader?.startsWith("Bearer ")) {
20
+ token = authHeader.slice(7);
21
+ }
22
+
23
+ // 2. Query parameter
24
+ if (!token) {
25
+ const url = new URL(c.req.url);
26
+ token = url.searchParams.get("token");
27
+ }
28
+
29
+ // 3. Cookie
30
+ if (!token && authConfig.cookieName) {
31
+ token = getCookie(c, authConfig.cookieName) ?? null;
32
+ }
33
+ }
34
+
35
+ if (!token) {
36
+ await next();
37
+ return;
38
+ }
39
+
40
+ try {
41
+ const { payload } = await jwtVerify(token, jwks, {
42
+ issuer: authConfig.issuer,
43
+ clockTolerance: Infinity,
44
+ });
45
+
46
+ if (payload.sub) {
47
+ c.set("user", { id: payload.sub, token });
48
+ }
49
+ await next();
50
+ } catch {
51
+ await next();
52
+ }
53
+ });
54
54
  }
55
55
 
56
56
  export const requireAuth = createMiddleware<AuthedAppEnv>(async (c, next) => {
57
- const user = c.get("user");
58
- if (!user) {
59
- throw new HTTPException(401, { message: "Unauthorized" });
60
- }
61
- c.set("user", user);
62
- await next();
57
+ const user = c.get("user");
58
+ if (!user) {
59
+ throw new HTTPException(401, { message: "Unauthorized" });
60
+ }
61
+ c.set("user", user);
62
+ await next();
63
63
  });