@m6d/cortex-cli 0.12.1 → 0.13.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 (74) hide show
  1. package/README.md +135 -130
  2. package/package.json +16 -5
  3. package/src/cli.ts +17 -12
  4. package/src/commands/apply.ts +169 -0
  5. package/src/commands/{cc.ts → management.ts} +150 -100
  6. package/src/commands/new.ts +50 -74
  7. package/src/contracts/charts/chart-spec.ts +1 -1
  8. package/src/contracts/client-tools/index.ts +3 -3
  9. package/src/contracts/{runtime → datasets}/datasets.ts +7 -42
  10. package/src/contracts/datasets/index.ts +3 -0
  11. package/src/contracts/egress-policy/index.ts +16 -0
  12. package/src/contracts/{egress → egress-policy}/scrub.ts +34 -5
  13. package/src/contracts/{egress → egress-policy}/url.ts +22 -9
  14. package/src/contracts/errors/index.ts +38 -0
  15. package/src/contracts/json/index.ts +5 -0
  16. package/src/contracts/manifest/index.ts +932 -0
  17. package/src/contracts/manifest-file/index.ts +16 -0
  18. package/src/contracts/markdown/index.ts +1 -1
  19. package/src/contracts/pattern/index.ts +276 -0
  20. package/src/contracts/server/index.ts +247 -0
  21. package/src/contracts/server/reserved-names.fixtures.json +18 -0
  22. package/src/contracts/triggers/index.ts +112 -0
  23. package/src/contracts/wire/index.ts +61 -32
  24. package/src/{cc → management}/client.ts +19 -7
  25. package/src/{cc → management}/flags.ts +2 -1
  26. package/src/{cc → management}/profiles.ts +5 -5
  27. package/src/scaffold/files.ts +362 -237
  28. package/src/scaffold/ports.ts +13 -24
  29. package/src/scaffold/templates/datasets.ts.txt +52 -0
  30. package/src/scaffold/templates/dockerignore.txt +7 -0
  31. package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
  32. package/src/scaffold/templates/gitignore.txt +2 -1
  33. package/src/scaffold/templates/server.ts.txt +40 -0
  34. package/src/scaffold/templates/setup.ts.txt +52 -0
  35. package/src/ui/prompts.ts +4 -31
  36. package/src/ui/report.ts +5 -15
  37. package/tsconfig.json +2 -3
  38. package/src/commands/graph.ts +0 -228
  39. package/src/commands/swagger.ts +0 -247
  40. package/src/config/load.ts +0 -122
  41. package/src/config/validate.ts +0 -46
  42. package/src/contracts/credentials/index.ts +0 -183
  43. package/src/contracts/egress/index.ts +0 -120
  44. package/src/contracts/graph/clients/embed.ts +0 -62
  45. package/src/contracts/graph/clients/neo4j.ts +0 -85
  46. package/src/contracts/graph/helpers.ts +0 -55
  47. package/src/contracts/graph/index.ts +0 -30
  48. package/src/contracts/graph/schema.ts +0 -65
  49. package/src/contracts/graph/types.ts +0 -125
  50. package/src/contracts/rich-text/index.ts +0 -220
  51. package/src/contracts/runtime/api.ts +0 -345
  52. package/src/contracts/runtime/auth.ts +0 -41
  53. package/src/contracts/runtime/grammar.ts +0 -169
  54. package/src/contracts/runtime/index.ts +0 -15
  55. package/src/contracts/runtime/models.ts +0 -50
  56. package/src/contracts/runtime/signature.ts +0 -255
  57. package/src/contracts/runtime/sync.ts +0 -196
  58. package/src/contracts/runtime/triggers.ts +0 -267
  59. package/src/graph/expand-domains.ts +0 -286
  60. package/src/graph/generate-cypher.ts +0 -201
  61. package/src/graph/seed.ts +0 -199
  62. package/src/graph/validate.ts +0 -78
  63. package/src/scaffold/features.ts +0 -190
  64. package/src/scaffold/templates/domains-README.md.txt +0 -16
  65. package/src/scaffold/templates/index.ts.txt +0 -18
  66. package/src/swagger/extract-endpoints.ts +0 -269
  67. package/src/swagger/generated-block.ts +0 -74
  68. package/src/swagger/openapi-schema.ts +0 -168
  69. package/src/swagger/parse-response.ts +0 -88
  70. package/src/swagger/types.ts +0 -25
  71. /package/src/contracts/{runtime → datasets}/dataset-sql-guide.ts +0 -0
  72. /package/src/contracts/{runtime → datasets}/dataset-sql.ts +0 -0
  73. /package/src/contracts/{runtime → datasets}/datasets.fixtures.json +0 -0
  74. /package/src/{cc → management}/output.ts +0 -0
@@ -1,201 +0,0 @@
1
- /**
2
- * Transforms declarative DomainDef into idempotent MERGE-based Cypher.
3
- *
4
- * Returns two phases:
5
- * - `nodes` -- MERGE statements that create/update Domain, Concept, Endpoint,
6
- * Service, and Rule nodes.
7
- * - `edges` -- MATCH+MERGE statements that wire up relationships between nodes.
8
- *
9
- * The caller MUST run all `nodes` from every domain before running any `edges`,
10
- * because edges may reference nodes defined in a different domain (e.g. a Rule
11
- * in the leaves domain that GOVERNS a Concept created by the servicing domain).
12
- *
13
- * Every label, relationship type and property name comes from the `GraphSchema`
14
- * the caller passes in — the project's own copy, so what this writes is by
15
- * construction what that project's resolver reads back.
16
- */
17
-
18
- import type {
19
- ConceptDef,
20
- DomainDef,
21
- EndpointDef,
22
- GraphSchema,
23
- RuleDef,
24
- ServiceDef,
25
- } from "@cortex/contracts/graph";
26
-
27
- type GeneratedCypher = {
28
- nodes: string[];
29
- edges: string[];
30
- };
31
-
32
- export function generateCypher(data: DomainDef, s: GraphSchema) {
33
- const nodes: string[] = [domainNode(data, s)];
34
- const edges: string[] = [];
35
-
36
- for (const concept of data.concepts ?? []) {
37
- nodes.push(conceptNode(concept, s));
38
- edges.push(...conceptEdges(concept, data.name, s));
39
- }
40
-
41
- for (const endpoint of data.endpoints ?? []) {
42
- nodes.push(endpointNode(endpoint, s));
43
- edges.push(...endpointEdges(endpoint, s));
44
- }
45
-
46
- for (const service of data.services ?? []) {
47
- nodes.push(serviceNode(service, s));
48
- edges.push(...serviceEdges(service, s));
49
- }
50
-
51
- for (const rule of data.rules ?? []) {
52
- nodes.push(ruleNode(rule, s));
53
- }
54
-
55
- return { nodes, edges } satisfies GeneratedCypher;
56
- }
57
-
58
- function domainNode(data: DomainDef, s: GraphSchema) {
59
- return `MERGE (d:${s.label.domain} {${s.prop.name}: '${esc(data.name)}'})
60
- SET d.${s.prop.description} = '${esc(data.description)}'${metadataClause("d", data.metadata, s)}`;
61
- }
62
-
63
- function conceptNode(concept: ConceptDef, s: GraphSchema) {
64
- const aliases =
65
- concept.aliases && concept.aliases.length > 0
66
- ? `, c.${s.prop.aliases} = [${concept.aliases.map((alias) => `'${esc(alias)}'`).join(", ")}]`
67
- : "";
68
-
69
- return `MERGE (c:${s.label.concept} {${s.prop.name}: '${esc(concept.name)}'})
70
- SET c.${s.prop.description} = '${esc(concept.description)}'${aliases}${metadataClause("c", concept.metadata, s)}`;
71
- }
72
-
73
- function conceptEdges(concept: ConceptDef, domainName: string, s: GraphSchema) {
74
- const edges = [
75
- `MATCH (c:${conceptRef(concept, s)})
76
- MATCH (d:${s.label.domain} {${s.prop.name}: '${esc(domainName)}'})
77
- MERGE (c)-[:${s.rel.belongsTo}]->(d)`,
78
- ];
79
-
80
- if (concept.parentConcept) {
81
- edges.push(
82
- `MATCH (child:${conceptRef(concept, s)})
83
- MATCH (parent:${conceptRef(concept.parentConcept, s)})
84
- MERGE (child)-[:${s.rel.specializes}]->(parent)`,
85
- );
86
- }
87
-
88
- edges.push(...governedByEdges(concept.governedBy, conceptRef(concept, s), s));
89
-
90
- return edges;
91
- }
92
-
93
- function endpointNode(endpoint: EndpointDef, s: GraphSchema) {
94
- return `MERGE (e:${endpointRef(endpoint, s)})
95
- SET e.${s.prop.name} = '${esc(endpoint.name)}',
96
- e.${s.prop.description} = '${esc(endpoint.description)}',
97
- e.${s.prop.params} = '${jsonStr(endpoint.params)}',
98
- e.${s.prop.body} = '${jsonStr(endpoint.body)}',
99
- e.${s.prop.response} = '${jsonStr(endpoint.response)}',
100
- e.${s.prop.propertiesDescriptions} = '${jsonStr(endpoint.propertiesDescriptions)}',
101
- e.${s.prop.successStatus} = ${endpoint.successStatus},
102
- e.${s.prop.errorStatuses} = [${endpoint.errorStatuses.join(", ")}]${metadataClause("e", endpoint.metadata, s)}`;
103
- }
104
-
105
- function endpointEdges(endpoint: EndpointDef, s: GraphSchema) {
106
- const edges: string[] = [];
107
- const self = endpointRef(endpoint, s);
108
-
109
- for (const [concepts, relationship] of [
110
- [endpoint.queries, s.rel.queriedVia],
111
- [endpoint.mutates, s.rel.mutatedVia],
112
- ] as const) {
113
- for (const concept of concepts ?? []) {
114
- edges.push(
115
- `MATCH (c:${conceptRef(concept, s)})
116
- MATCH (e:${self})
117
- MERGE (c)-[:${relationship}]->(e)`,
118
- );
119
- }
120
- }
121
-
122
- for (const returned of endpoint.returns ?? []) {
123
- // The field says which part of the response carries the concept; without
124
- // one the whole body is the concept.
125
- const field = returned.field ? ` SET r.${s.prop.field} = '${esc(returned.field)}'` : "";
126
- edges.push(
127
- `MATCH (e:${self})
128
- MATCH (c:${conceptRef(returned.concept, s)})
129
- MERGE (e)-[r:${s.rel.returns}]->(c)${field}`,
130
- );
131
- }
132
-
133
- edges.push(...governedByEdges(endpoint.governedBy, self, s));
134
-
135
- for (const dependency of endpoint.dependsOn ?? []) {
136
- edges.push(
137
- `MATCH (e:${self})
138
- MATCH (dep:${endpointRef(dependency.endpoint, s)})
139
- MERGE (e)-[d:${s.rel.dependsOn}]->(dep)
140
- SET d.${s.prop.paramName} = '${esc(dependency.paramName)}', d.${s.prop.fromField} = '${esc(dependency.fromField)}'`,
141
- );
142
- }
143
-
144
- return edges;
145
- }
146
-
147
- function serviceNode(service: ServiceDef, s: GraphSchema) {
148
- return `MERGE (s:${serviceRef(service, s)})
149
- SET s.${s.prop.name} = '${esc(service.name)}',
150
- s.${s.prop.description} = '${esc(service.description)}'${metadataClause("s", service.metadata, s)}`;
151
- }
152
-
153
- function serviceEdges(service: ServiceDef, s: GraphSchema) {
154
- return [
155
- `MATCH (c:${conceptRef(service.belongsTo, s)})
156
- MATCH (s:${serviceRef(service, s)})
157
- MERGE (c)-[:${s.rel.requestedVia}]->(s)`,
158
- ...governedByEdges(service.governedBy, serviceRef(service, s), s),
159
- ];
160
- }
161
-
162
- function ruleNode(rule: RuleDef, s: GraphSchema) {
163
- return `MERGE (r:${s.label.rule} {${s.prop.name}: '${esc(rule.name)}'})
164
- SET r.${s.prop.description} = '${esc(rule.description)}'${metadataClause("r", rule.metadata, s)}`;
165
- }
166
-
167
- /** Rules govern concepts, endpoints and services identically; only the target differs. */
168
- function governedByEdges(rules: RuleDef[] | undefined, targetRef: string, s: GraphSchema) {
169
- return (rules ?? []).map(
170
- (rule) => `MATCH (r:${s.label.rule} {${s.prop.name}: '${esc(rule.name)}'})
171
- MATCH (t:${targetRef})
172
- MERGE (r)-[:${s.rel.governs}]->(t)`,
173
- );
174
- }
175
-
176
- /** Endpoints are keyed on path plus method, so both must appear in every match. */
177
- function endpointRef(endpoint: Pick<EndpointDef, "path" | "method">, s: GraphSchema) {
178
- return `${s.label.endpoint} {${s.prop.path}: '${esc(endpoint.path)}', ${s.prop.method}: '${endpoint.method}'}`;
179
- }
180
-
181
- function conceptRef(concept: Pick<ConceptDef, "name">, s: GraphSchema) {
182
- return `${s.label.concept} {${s.prop.name}: '${esc(concept.name)}'}`;
183
- }
184
-
185
- function serviceRef(service: Pick<ServiceDef, "builtInId">, s: GraphSchema) {
186
- return `${s.label.service} {${s.prop.builtInId}: '${esc(service.builtInId)}'}`;
187
- }
188
-
189
- /** Omitted entirely when absent — an empty assignment is not valid Cypher. */
190
- function metadataClause(alias: string, metadata: unknown, s: GraphSchema) {
191
- return metadata ? `, ${alias}.${s.prop.metadata} = '${jsonStr(metadata)}'` : "";
192
- }
193
-
194
- /** Backslashes first, so escaping a quote cannot itself be escaped away. */
195
- function esc(value: string) {
196
- return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n");
197
- }
198
-
199
- function jsonStr(value: unknown) {
200
- return esc(JSON.stringify(value));
201
- }
package/src/graph/seed.ts DELETED
@@ -1,199 +0,0 @@
1
- /**
2
- * Seed orchestrator for the knowledge graph: validates domains, generates
3
- * Cypher, executes it against Neo4j, and embeds every concept.
4
- */
5
-
6
- import {
7
- Neo4jQueryError,
8
- type DomainDef,
9
- type EmbedFn,
10
- type GraphSchema,
11
- type Neo4jClient,
12
- } from "@cortex/contracts/graph";
13
-
14
- import { expandDomains } from "./expand-domains";
15
- import { generateCypher } from "./generate-cypher";
16
- import { validateDomain } from "./validate";
17
-
18
- type SeedEmbedding = {
19
- embed: EmbedFn;
20
- dimension: number;
21
- };
22
-
23
- type SeedGraphConfig = {
24
- neo4j: Neo4jClient;
25
- embedding: SeedEmbedding;
26
- /** `GRAPH_SCHEMA`, passed rather than imported so tests can drive a fake one. */
27
- schema: GraphSchema;
28
- /** Stamped onto the graph, so the server can refuse a shape it cannot read. */
29
- schemaVersion: number;
30
- };
31
-
32
- type StatementFailure = {
33
- /** The Neo4j error code where there was one, its message otherwise. */
34
- error: string;
35
- /** The statement as generated, untruncated — the caller decides how much to show. */
36
- statement: string;
37
- };
38
-
39
- type DomainSeedResult = {
40
- nodes: number;
41
- edges: number;
42
- failed: number;
43
- failures: StatementFailure[];
44
- };
45
-
46
- type SeedGraphResult = {
47
- nodes: number;
48
- edges: number;
49
- failed: number;
50
- /** Keyed by the same names the config declares under `knowledge.domains`. */
51
- domains: Record<string, DomainSeedResult>;
52
- embeddings: { concepts: number; dimension: number };
53
- };
54
-
55
- // Nodes for every domain run before any edge does, so a statement carries the
56
- // domain it came from rather than being attributed by the loop that runs it.
57
- type TaggedStatement = { domain: string; statement: string };
58
-
59
- /** Runs one statement; the failure string is Neo4j's error code when it has one. */
60
- async function runStatement(client: Neo4jClient, statement: string) {
61
- try {
62
- await client.query(statement);
63
- return undefined;
64
- } catch (error) {
65
- if (error instanceof Neo4jQueryError)
66
- return /^\d+$/.test(error.code) ? error.message : error.code;
67
- return error instanceof Error ? error.message : String(error);
68
- }
69
- }
70
-
71
- // Maps, not objects: a domain is named by the user, and `constructor` is a legal
72
- // domain name that on a plain record reads back `Object.prototype.constructor` —
73
- // a function, so `?? 0` never fires and `.push` throws.
74
- async function runPhase(client: Neo4jClient, statements: TaggedStatement[]) {
75
- const succeeded = new Map<string, number>();
76
- const failures = new Map<string, StatementFailure[]>();
77
-
78
- for (const { domain, statement } of statements) {
79
- const error = await runStatement(client, statement);
80
- if (error === undefined) {
81
- succeeded.set(domain, (succeeded.get(domain) ?? 0) + 1);
82
- continue;
83
- }
84
-
85
- const collected = failures.get(domain) ?? [];
86
- collected.push({ error, statement });
87
- failures.set(domain, collected);
88
- }
89
-
90
- return { succeeded, failures };
91
- }
92
-
93
- async function generateEmbeddings(
94
- client: Neo4jClient,
95
- embeddingConfig: SeedEmbedding,
96
- s: GraphSchema,
97
- ) {
98
- await client.query(
99
- `CREATE VECTOR INDEX ${s.conceptEmbeddingsIndex} IF NOT EXISTS
100
- FOR (c:${s.label.concept}) ON (c.${s.prop.embedding})
101
- OPTIONS {indexConfig: {
102
- \`vector.dimensions\`: ${embeddingConfig.dimension},
103
- \`vector.similarity_function\`: 'cosine'
104
- }}`,
105
- );
106
-
107
- const concepts = (await client.query(
108
- `MATCH (c:${s.label.concept}) RETURN c.${s.prop.name} AS name, c.${s.prop.description} AS description`,
109
- )) as { name: string; description: string }[];
110
-
111
- if (concepts.length === 0) {
112
- return { concepts: 0, dimension: embeddingConfig.dimension };
113
- }
114
-
115
- const embeddings = await embeddingConfig.embed(concepts.map((c) => c.description));
116
-
117
- await Promise.all(
118
- concepts.map((concept, idx) =>
119
- client.query(
120
- `MATCH (c:${s.label.concept} {${s.prop.name}: $name})
121
- SET c.${s.prop.embedding} = $embedding`,
122
- { name: concept.name, embedding: embeddings[idx]! },
123
- ),
124
- ),
125
- );
126
-
127
- return { concepts: concepts.length, dimension: embeddingConfig.dimension };
128
- }
129
-
130
- export async function seedGraph(config: SeedGraphConfig, domains: Record<string, DomainDef>) {
131
- const expandedDomains = expandDomains(domains);
132
-
133
- const validationErrors = Object.entries(expandedDomains).flatMap(([name, domain]) =>
134
- validateDomain(domain).map((err) => `[${name}] ${err}`),
135
- );
136
-
137
- if (validationErrors.length) {
138
- throw new Error(
139
- `Validation failed with ${validationErrors.length} error(s):\n${validationErrors.join("\n")}`,
140
- );
141
- }
142
-
143
- const client = config.neo4j;
144
- const targets = Object.entries(expandedDomains);
145
-
146
- const allCypher = targets.map(
147
- ([name, data]) => [name, generateCypher(data, config.schema)] as const,
148
- );
149
-
150
- // Stamped before anything else is written, so an interrupted seed still
151
- // leaves the graph labelled with the schema it was written against.
152
- await client.query(
153
- `MERGE (g:${config.schema.label.marker})
154
- SET g.${config.schema.prop.version} = ${config.schemaVersion}`,
155
- );
156
-
157
- // Nodes across every domain first, so cross-domain references resolve.
158
- const allNodes = allCypher.flatMap(([domain, c]) => tag(domain, c.nodes));
159
- const nodes = await runPhase(client, allNodes);
160
-
161
- // Edges once every node exists.
162
- const allEdges = allCypher.flatMap(([domain, c]) => tag(domain, c.edges));
163
- const edges = await runPhase(client, allEdges);
164
-
165
- const embeddings = await generateEmbeddings(client, config.embedding, config.schema);
166
-
167
- const domainResults = Object.fromEntries(
168
- targets.map(([name]) => {
169
- const failures = [
170
- ...(nodes.failures.get(name) ?? []),
171
- ...(edges.failures.get(name) ?? []),
172
- ];
173
- return [
174
- name,
175
- {
176
- nodes: nodes.succeeded.get(name) ?? 0,
177
- edges: edges.succeeded.get(name) ?? 0,
178
- failed: failures.length,
179
- failures,
180
- } satisfies DomainSeedResult,
181
- ];
182
- }),
183
- );
184
-
185
- // The aggregates stay the headline number; per-domain is the breakdown of it.
186
- return Object.values(domainResults).reduce<SeedGraphResult>(
187
- (total, domain) => ({
188
- ...total,
189
- nodes: total.nodes + domain.nodes,
190
- edges: total.edges + domain.edges,
191
- failed: total.failed + domain.failed,
192
- }),
193
- { nodes: 0, edges: 0, failed: 0, domains: domainResults, embeddings },
194
- );
195
- }
196
-
197
- function tag(domain: string, statements: string[]) {
198
- return statements.map((statement) => ({ domain, statement }) satisfies TaggedStatement);
199
- }
@@ -1,78 +0,0 @@
1
- import type { ConceptDef, DomainDef, RuleDef } from "@cortex/contracts/graph";
2
-
3
- /**
4
- * Checks a domain for the two mistakes that survive the type system: a name used
5
- * twice, and an entity nothing points at.
6
- *
7
- * Cross-domain references are allowed — a concept, endpoint or service may be
8
- * defined in another module — so local membership is deliberately not enforced.
9
- */
10
- export function validateDomain(data: DomainDef) {
11
- const referenced = collectReferences(data);
12
-
13
- return [
14
- ...duplicateNames(data.concepts, "concept"),
15
- ...duplicateNames(data.endpoints, "endpoint"),
16
- ...duplicateNames(data.services, "service"),
17
- ...duplicateNames(data.rules, "rule"),
18
- ...unreferenced(
19
- data.concepts,
20
- referenced.concepts,
21
- (name) => `Concept "${name}" is never referenced by any endpoint or service`,
22
- ),
23
- ...unreferenced(
24
- data.rules,
25
- referenced.rules,
26
- (name) => `Rule "${name}" is never referenced by any endpoint, service, or concept`,
27
- ),
28
- ];
29
- }
30
-
31
- function duplicateNames(items: { name: string }[] | undefined, kind: string) {
32
- const seen = new Set<string>();
33
-
34
- return (items ?? []).flatMap((item) => {
35
- const duplicate = seen.has(item.name);
36
- seen.add(item.name);
37
-
38
- return duplicate ? [`Duplicate ${kind} name: "${item.name}"`] : [];
39
- });
40
- }
41
-
42
- function unreferenced(
43
- items: { name: string }[] | undefined,
44
- referenced: Set<string>,
45
- message: (name: string) => string,
46
- ) {
47
- return (items ?? [])
48
- .filter((item) => !referenced.has(item.name))
49
- .map((item) => message(item.name));
50
- }
51
-
52
- /** Gathers every name pointed at from anywhere in the domain. */
53
- function collectReferences(data: DomainDef) {
54
- const concepts = new Set<string>();
55
- const rules = new Set<string>();
56
- const addRules = (governedBy: RuleDef[] | undefined) =>
57
- governedBy?.forEach((rule) => rules.add(rule.name));
58
- const addConcept = (concept: ConceptDef | undefined) => concept && concepts.add(concept.name);
59
-
60
- for (const endpoint of data.endpoints ?? []) {
61
- endpoint.queries?.forEach(addConcept);
62
- endpoint.mutates?.forEach(addConcept);
63
- endpoint.returns?.forEach((returned) => addConcept(returned.concept));
64
- addRules(endpoint.governedBy);
65
- }
66
-
67
- for (const service of data.services ?? []) {
68
- addConcept(service.belongsTo);
69
- addRules(service.governedBy);
70
- }
71
-
72
- for (const concept of data.concepts ?? []) {
73
- addConcept(concept.parentConcept);
74
- addRules(concept.governedBy);
75
- }
76
-
77
- return { concepts, rules };
78
- }
@@ -1,190 +0,0 @@
1
- /**
2
- * The five features `cortex new` toggles. A feature is not a
3
- * config key: it is a bundle of a `cortex.config.ts` fragment, a block of env
4
- * keys, and sometimes a compose service. Fragments concatenate — there is no
5
- * merge engine — so every field here is plain text that gets joined.
6
- *
7
- * The grouping is what the server code already enforces. `vision` is inert
8
- * without `storage`, so attachments writes both; `seedGraph` throws unless it
9
- * has all of `neo4j`, `embedding` and `knowledge.domains`, so knowledge writes
10
- * all three plus the `src/domains/` tree the domainsDir convention assumes.
11
- */
12
-
13
- export type Feature = "attachments" | "redis" | "auth" | "knowledge" | "controlCenter";
14
-
15
- type Definition = {
16
- /** One line, shown in `--help` and next to the option in the multi-select. */
17
- hint: string;
18
- /** Sits above the fragment in the generated config, commented either way. */
19
- note: string;
20
- /**
21
- * The `cortex.config.ts` fragment, written at zero indentation: the assembler
22
- * indents it into the object literal, or comments it out for a feature that
23
- * was not selected. Conditional spread throughout, so an unfilled env var
24
- * leaves the section *absent* rather than present-and-undefined.
25
- */
26
- config: string;
27
- /** The `.env` block, identical in `.env` and `.env.example`. */
28
- env: string;
29
- /** The `docker-compose.yml` service, indented under `services:`. */
30
- compose?: string;
31
- /** The named volume that service mounts. */
32
- volume?: string;
33
- /** What `cd`-then-edit has to fill in before the project boots. */
34
- secrets?: string;
35
- };
36
-
37
- export const FEATURES: Record<Feature, Definition> = {
38
- attachments: {
39
- hint: "MinIO storage for uploads, described by a vision model",
40
- note: "Attachments: an S3-compatible bucket for uploads, plus an image-capable\nmodel that describes them. Vision does nothing without storage, which is\nwhy one toggle writes both.",
41
- config: `...(process.env["CORTEX_MINIO_ENDPOINT"]
42
- ? {
43
- storage: {
44
- endPoint: process.env["CORTEX_MINIO_ENDPOINT"],
45
- port: Number(process.env["CORTEX_MINIO_PORT"] ?? {{port.minio}}),
46
- useSSL: process.env["CORTEX_MINIO_USE_SSL"] === "true",
47
- accessKey: process.env["CORTEX_MINIO_ACCESS_KEY"]!,
48
- secretKey: process.env["CORTEX_MINIO_SECRET_KEY"]!,
49
- },
50
- vision: {
51
- baseURL: process.env["CORTEX_VISION_URL"]!,
52
- apiKey: process.env["CORTEX_VISION_KEY"]!,
53
- modelName: process.env["CORTEX_VISION_MODEL"]!,
54
- },
55
- }
56
- : {}),`,
57
- env: `# --- Attachments -------------------------------------------------------------
58
- # S3-compatible object storage. docker-compose.yml runs MinIO on these values.
59
- CORTEX_MINIO_ENDPOINT=localhost
60
- CORTEX_MINIO_PORT={{port.minio}}
61
- CORTEX_MINIO_USE_SSL=false
62
- CORTEX_MINIO_ACCESS_KEY=minio
63
- CORTEX_MINIO_SECRET_KEY=minio-secret-key
64
-
65
- # An image-capable, OpenAI-compatible model. It describes uploaded images so the
66
- # agent can read them.
67
- CORTEX_VISION_URL=https://api.openai.com/v1
68
- CORTEX_VISION_KEY=your-vision-api-key
69
- CORTEX_VISION_MODEL=your-vision-model`,
70
- compose: ` minio:
71
- image: minio/minio:RELEASE.2025-04-22T22-12-26Z
72
- command: server /data --console-address ":9001"
73
- environment:
74
- MINIO_ROOT_USER: minio
75
- MINIO_ROOT_PASSWORD: minio-secret-key
76
- ports:
77
- - "{{port.minio}}:9000"
78
- - "{{port.minioConsole}}:9001"
79
- volumes:
80
- - minio-data:/data`,
81
- volume: "minio-data",
82
- secrets: "CORTEX_VISION_*",
83
- },
84
-
85
- redis: {
86
- hint: "resumable streams across restarts",
87
- note: "Redis makes live runs survive a restart and reach every instance. Without\nit, an in-memory fallback covers this single process.",
88
- config: `...(process.env["CORTEX_REDIS_URL"] ? { redis: { url: process.env["CORTEX_REDIS_URL"] } } : {}),`,
89
- env: `# --- Redis -------------------------------------------------------------------
90
- # Resumable streams. docker-compose.yml runs one on this URL.
91
- CORTEX_REDIS_URL=redis://localhost:{{port.redis}}`,
92
- compose: ` redis:
93
- image: redis:7-alpine
94
- ports:
95
- - "{{port.redis}}:6379"`,
96
- },
97
-
98
- auth: {
99
- hint: "JWT verification via JWKS",
100
- note: "Bearer tokens verified against an identity provider's JWKS endpoint. The\nprovider is external, so there is no service for it.",
101
- config: `...(process.env["CORTEX_AUTH_JWKS_URI"]
102
- ? {
103
- auth: {
104
- kind: "jwks" as const,
105
- jwksUri: process.env["CORTEX_AUTH_JWKS_URI"],
106
- issuer: process.env["CORTEX_AUTH_ISSUER"]!,
107
- },
108
- }
109
- : {}),`,
110
- env: `# --- Auth --------------------------------------------------------------------
111
- # Your identity provider's JWKS endpoint, and the issuer it stamps on tokens.
112
- CORTEX_AUTH_JWKS_URI=https://your-idp.example.com/.well-known/jwks.json
113
- CORTEX_AUTH_ISSUER=https://your-idp.example.com/`,
114
- secrets: "CORTEX_AUTH_*",
115
- },
116
-
117
- knowledge: {
118
- hint: "Swagger + domains + Neo4j",
119
- note: "The knowledge graph, which is three keys rather than one: Neo4j stores it,\nthe embedding endpoint makes it searchable, and the domain definitions under\nsrc/domains/ are what `cortex graph seed` loads into it.",
120
- config: `...(process.env["CORTEX_NEO4J_URL"]
121
- ? {
122
- neo4j: {
123
- url: process.env["CORTEX_NEO4J_URL"],
124
- user: process.env["CORTEX_NEO4J_USER"]!,
125
- password: process.env["CORTEX_NEO4J_PASSWORD"]!,
126
- },
127
- embedding: {
128
- baseURL: process.env["CORTEX_EMBEDDING_URL"]!,
129
- apiKey: process.env["CORTEX_EMBEDDING_KEY"]!,
130
- modelName: process.env["CORTEX_EMBEDDING_MODEL"]!,
131
- dimension: Number(process.env["CORTEX_EMBEDDING_DIMENSION"] ?? 1024),
132
- },
133
- knowledge: {
134
- // swagger: { url: "https://api.example.com/swagger/v1/swagger.json" },
135
- // Import the domains you write under src/domains/ and list them here.
136
- domains: {},
137
- },
138
- }
139
- : {}),`,
140
- env: `# --- Knowledge graph ---------------------------------------------------------
141
- # Neo4j. docker-compose.yml runs one with exactly these credentials. The URL is
142
- # the HTTP endpoint, not the bolt one: the server talks to Neo4j over its HTTP
143
- # transaction API.
144
- CORTEX_NEO4J_URL=http://localhost:{{port.neo4j}}
145
- CORTEX_NEO4J_USER=neo4j
146
- CORTEX_NEO4J_PASSWORD=cortex-dev-password
147
-
148
- # An OpenAI-compatible embeddings endpoint — external, like the main model.
149
- # CORTEX_EMBEDDING_DIMENSION has to match what the model returns.
150
- CORTEX_EMBEDDING_URL=https://api.openai.com/v1
151
- CORTEX_EMBEDDING_KEY=your-embedding-api-key
152
- CORTEX_EMBEDDING_MODEL=your-embedding-model
153
- CORTEX_EMBEDDING_DIMENSION=1024`,
154
- compose: ` neo4j:
155
- image: neo4j:5-community
156
- environment:
157
- NEO4J_AUTH: neo4j/cortex-dev-password
158
- # Neo4j Browser, served on the HTTP port, opens bolt at whatever this
159
- # advertises. Without it the browser would offer 7687, which is the
160
- # container's port and not the one published below.
161
- NEO4J_server_bolt_advertised__address: ":{{port.neo4jBolt}}"
162
- ports:
163
- - "{{port.neo4j}}:7474"
164
- - "{{port.neo4jBolt}}:7687"
165
- volumes:
166
- - neo4j-data:/data`,
167
- volume: "neo4j-data",
168
- secrets: "CORTEX_EMBEDDING_*",
169
- },
170
-
171
- controlCenter: {
172
- hint: "serve agents published on a Control Center instance",
173
- note: "The Control Center you point this server at. External, so no service here.",
174
- config: `...(process.env["CORTEX_CONTROL_CENTER_URL"]
175
- ? {
176
- controlCenter: {
177
- url: process.env["CORTEX_CONTROL_CENTER_URL"],
178
- apiKey: process.env["CORTEX_CONTROL_CENTER_API_KEY"]!,
179
- },
180
- }
181
- : {}),`,
182
- env: `# --- Control Center ----------------------------------------------------------
183
- CORTEX_CONTROL_CENTER_URL=http://localhost:20000
184
- CORTEX_CONTROL_CENTER_API_KEY=cc_live_...`,
185
- secrets: "CORTEX_CONTROL_CENTER_API_KEY",
186
- },
187
- };
188
-
189
- /** Catalogue order, which is the order every generated file lists them in. */
190
- export const FEATURE_NAMES = Object.keys(FEATURES) as Feature[];
@@ -1,16 +0,0 @@
1
- # Domains
2
-
3
- Domain definitions live here, one directory each, and are what `cortex graph seed` loads into Neo4j.
4
-
5
- ```
6
- src/domains/
7
- leaves/
8
- index.ts defineDomain({ concepts, endpoints, rules, services })
9
- concepts/leave.concept.ts
10
- endpoints/listLeaves.endpoint.ts
11
- services/leaveRequests.service.ts
12
- ```
13
-
14
- Export each domain from its `index.ts`, then list it under `knowledge.domains` in
15
- `cortex.config.ts`. `cortex swagger sync` fills in the params, body, and response of every
16
- `.endpoint.ts` from your Swagger spec.