@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.
- package/README.md +38 -38
- package/dist/src/factory.d.ts +13 -1
- package/dist/src/ws/index.d.ts +1 -1
- package/package.json +54 -54
- package/src/adapters/database.ts +21 -28
- package/src/adapters/minio.ts +69 -69
- package/src/adapters/mssql.ts +171 -195
- package/src/adapters/storage.ts +4 -4
- package/src/ai/fetch.ts +31 -31
- package/src/ai/helpers.ts +18 -22
- package/src/ai/index.ts +101 -113
- package/src/ai/interceptors/resolve-captured-files.ts +42 -49
- package/src/ai/prompt.ts +80 -83
- package/src/ai/tools/call-endpoint.tool.ts +75 -82
- package/src/ai/tools/capture-files.tool.ts +15 -17
- package/src/ai/tools/execute-code.tool.ts +73 -80
- package/src/ai/tools/query-graph.tool.ts +17 -17
- package/src/auth/middleware.ts +51 -51
- package/src/cli/extract-endpoints.ts +436 -474
- package/src/config.ts +124 -134
- package/src/db/migrate.ts +13 -13
- package/src/db/migrations/20260309012148_cloudy_maria_hill/snapshot.json +303 -303
- package/src/db/schema.ts +46 -58
- package/src/factory.ts +136 -139
- package/src/graph/generate-cypher.ts +97 -97
- package/src/graph/helpers.ts +37 -37
- package/src/graph/index.ts +20 -20
- package/src/graph/neo4j.ts +82 -89
- package/src/graph/resolver.ts +201 -211
- package/src/graph/seed.ts +101 -114
- package/src/graph/types.ts +88 -88
- package/src/graph/validate.ts +55 -57
- package/src/index.ts +5 -5
- package/src/routes/chat.ts +23 -23
- package/src/routes/files.ts +75 -80
- package/src/routes/threads.ts +52 -54
- package/src/routes/ws.ts +22 -22
- package/src/types.ts +30 -30
- package/src/ws/connections.ts +11 -11
- package/src/ws/events.ts +2 -2
- package/src/ws/index.ts +1 -5
- 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
|
-
|
|
8
|
-
|
|
7
|
+
backendFetch: NonNullable<ResolvedCortexAgentConfig["backendFetch"]>,
|
|
8
|
+
token: string,
|
|
9
9
|
) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
const options: RequestInit = { method };
|
|
52
|
+
if (body && (method === "POST" || method === "PUT")) {
|
|
53
|
+
options.body = body;
|
|
54
|
+
}
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
fullPath,
|
|
60
|
-
backendFetch,
|
|
61
|
-
token,
|
|
62
|
-
options,
|
|
63
|
-
);
|
|
56
|
+
const response = await fetchBackend(fullPath, backendFetch, token, options);
|
|
64
57
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
if (response.status === 204) {
|
|
82
|
+
return JSON.stringify({ success: true });
|
|
83
|
+
}
|
|
91
84
|
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
8
|
-
path: string,
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
29
|
-
return qs ? `?${qs}` : "";
|
|
25
|
+
const qs = searchParams.toString();
|
|
26
|
+
return qs ? `?${qs}` : "";
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
function createApiHelper(
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
backendFetch: NonNullable<ResolvedCortexAgentConfig["backendFetch"]>,
|
|
31
|
+
token: string,
|
|
35
32
|
) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
39
|
+
const response = await fetchBackend(path, backendFetch, token, options);
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
if (response.status === 204) return { success: true };
|
|
53
|
+
return response.json();
|
|
54
|
+
}
|
|
60
55
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
68
|
+
...args: [...paramNames: string[], body: string]
|
|
76
69
|
) => (...args: unknown[]) => Promise<unknown> = Object.getPrototypeOf(
|
|
77
|
-
|
|
70
|
+
async function () {},
|
|
78
71
|
).constructor;
|
|
79
72
|
|
|
80
73
|
export function createExecuteCodeTool(
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
backendFetch: NonNullable<ResolvedCortexAgentConfig["backendFetch"]>,
|
|
75
|
+
token: string,
|
|
83
76
|
) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
}
|
package/src/auth/middleware.ts
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
});
|