@cortexa/core 1.0.1 → 1.1.1

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 CHANGED
@@ -23,6 +23,7 @@
23
23
  - [Architecture](#architecture)
24
24
  - [Supported Databases](#supported-databases)
25
25
  - [CLI Reference](#cli-reference)
26
+ - [REST API](#rest-api)
26
27
  - [Configuration](#configuration)
27
28
  - [Requirements](#requirements)
28
29
  - [Contributing](#contributing)
@@ -307,6 +308,85 @@ npm install @powersync/mysql-zongji
307
308
  | `cortexa actions` | View and manage action recommendations |
308
309
  | `cortexa explain <type> <id>` | AI explanation of anomaly, insight, or event |
309
310
  | `cortexa ask "<question>"` | Ask a natural language question |
311
+ | `cortexa serve` | Start REST API server (`--port`, `--host`) |
312
+
313
+ ## REST API
314
+
315
+ Start the HTTP server to access Cortexa from any language (Python, Go, Ruby, etc.):
316
+
317
+ ```bash
318
+ npx cortexa serve
319
+ # Cortexa API running at http://127.0.0.1:3210
320
+ ```
321
+
322
+ Options: `--port <port>` (default: 3210), `--host <host>` (default: 127.0.0.1), `--no-cors`.
323
+
324
+ ### Endpoints
325
+
326
+ | Method | Endpoint | Description |
327
+ |--------|----------|-------------|
328
+ | GET | `/api/status` | Connection status |
329
+ | GET | `/api/entities` | List classified entities |
330
+ | GET | `/api/relationships` | List entity relationships |
331
+ | GET | `/api/events` | List change events (`?entity=`, `?last=`) |
332
+ | GET | `/api/baselines` | Learned rate baselines |
333
+ | GET | `/api/anomalies` | Detected anomalies (`?severity=`, `?entity=`) |
334
+ | GET | `/api/insights` | State analysis insights (`?entity=`, `?severity=`) |
335
+ | GET | `/api/transitions` | Transition statistics (`?entity=`) |
336
+ | GET | `/api/correlations` | Cross-entity correlations |
337
+ | GET | `/api/distributions` | Value distributions (`?entity=`) |
338
+ | GET | `/api/graph` | Knowledge graph summary |
339
+ | GET | `/api/graph/export` | Full graph as JSON |
340
+ | GET | `/api/graph/entity/:name` | Entity intelligence |
341
+ | GET | `/api/actions` | Recommendations (`?status=`, `?action=`) |
342
+ | POST | `/api/discover` | Trigger schema discovery |
343
+ | POST | `/api/explain` | AI explanation (`{ type, id }`) |
344
+ | POST | `/api/ask` | Natural language question (`{ question }`) |
345
+ | POST | `/api/watch` | Start watching (`{ interval, once }`) |
346
+ | POST | `/api/unwatch` | Stop watching |
347
+ | POST | `/api/actions/:id/approve` | Approve recommendation |
348
+ | POST | `/api/actions/:id/reject` | Reject recommendation |
349
+
350
+ All responses return `{ ok: boolean, data?: ..., error?: string }`.
351
+
352
+ ### Examples
353
+
354
+ ```bash
355
+ # Get anomalies
356
+ curl http://localhost:3210/api/anomalies?severity=high
357
+
358
+ # Ask a question
359
+ curl -X POST http://localhost:3210/api/ask \
360
+ -H "Content-Type: application/json" \
361
+ -d '{"question": "Why did order activity spike today?"}'
362
+
363
+ # Explain an anomaly
364
+ curl -X POST http://localhost:3210/api/explain \
365
+ -H "Content-Type: application/json" \
366
+ -d '{"type": "anomaly", "id": 1}'
367
+ ```
368
+
369
+ ```python
370
+ # Python example
371
+ import requests
372
+
373
+ r = requests.get("http://localhost:3210/api/anomalies", params={"severity": "high"})
374
+ print(r.json()["data"])
375
+
376
+ r = requests.post("http://localhost:3210/api/ask", json={"question": "Are orders healthy?"})
377
+ print(r.json()["data"]["answer"])
378
+ ```
379
+
380
+ ### Programmatic usage
381
+
382
+ ```ts
383
+ import { CortexaServer } from '@cortexa/core';
384
+
385
+ const server = new CortexaServer(config, { port: 3210, cors: true });
386
+ await server.start();
387
+ // ... later
388
+ await server.stop();
389
+ ```
310
390
 
311
391
  ## Configuration
312
392
 
package/dist/cli/index.js CHANGED
@@ -4742,6 +4742,283 @@ Return valid JSON in this exact format:
4742
4742
  }
4743
4743
  };
4744
4744
 
4745
+ // src/server/server.ts
4746
+ import { createServer } from "http";
4747
+
4748
+ // src/server/router.ts
4749
+ var Router = class {
4750
+ routes = [];
4751
+ get(path4, handler) {
4752
+ this.addRoute("GET", path4, handler);
4753
+ }
4754
+ post(path4, handler) {
4755
+ this.addRoute("POST", path4, handler);
4756
+ }
4757
+ addRoute(method, path4, handler) {
4758
+ const paramNames = [];
4759
+ const patternStr = path4.replace(/:(\w+)/g, (_match, name) => {
4760
+ paramNames.push(name);
4761
+ return "([^/]+)";
4762
+ });
4763
+ this.routes.push({
4764
+ method,
4765
+ pattern: new RegExp(`^${patternStr}$`),
4766
+ paramNames,
4767
+ handler
4768
+ });
4769
+ }
4770
+ async handle(req, res, cors) {
4771
+ if (cors) {
4772
+ res.setHeader("Access-Control-Allow-Origin", "*");
4773
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
4774
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
4775
+ }
4776
+ if (req.method === "OPTIONS") {
4777
+ res.writeHead(204);
4778
+ res.end();
4779
+ return;
4780
+ }
4781
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
4782
+ const path4 = url.pathname;
4783
+ const method = req.method ?? "GET";
4784
+ const query = {};
4785
+ url.searchParams.forEach((value, key) => {
4786
+ query[key] = value;
4787
+ });
4788
+ for (const route of this.routes) {
4789
+ if (route.method !== method) continue;
4790
+ const match = route.pattern.exec(path4);
4791
+ if (!match) continue;
4792
+ const pathParams = {};
4793
+ for (let i = 0; i < route.paramNames.length; i++) {
4794
+ pathParams[route.paramNames[i]] = match[i + 1];
4795
+ }
4796
+ let body = void 0;
4797
+ if (method === "POST") {
4798
+ body = await readBody(req);
4799
+ }
4800
+ const params = { path: path4, method, query, body, pathParams };
4801
+ try {
4802
+ const result = await route.handler(params);
4803
+ sendJson(res, result.ok ? 200 : 400, result);
4804
+ } catch (err) {
4805
+ const message = err instanceof Error ? err.message : "Internal server error";
4806
+ sendJson(res, 500, { ok: false, error: message });
4807
+ }
4808
+ return;
4809
+ }
4810
+ sendJson(res, 404, { ok: false, error: `Not found: ${method} ${path4}` });
4811
+ }
4812
+ };
4813
+ function sendJson(res, statusCode, data) {
4814
+ res.writeHead(statusCode, { "Content-Type": "application/json" });
4815
+ res.end(JSON.stringify(data));
4816
+ }
4817
+ function readBody(req) {
4818
+ return new Promise((resolve, reject) => {
4819
+ const chunks = [];
4820
+ req.on("data", (chunk) => chunks.push(chunk));
4821
+ req.on("end", () => {
4822
+ const raw = Buffer.concat(chunks).toString();
4823
+ if (!raw) {
4824
+ resolve(void 0);
4825
+ return;
4826
+ }
4827
+ try {
4828
+ resolve(JSON.parse(raw));
4829
+ } catch {
4830
+ reject(new Error("Invalid JSON body"));
4831
+ }
4832
+ });
4833
+ req.on("error", reject);
4834
+ });
4835
+ }
4836
+
4837
+ // src/server/handlers.ts
4838
+ function registerHandlers(router, cortexa) {
4839
+ router.get("/api/status", async () => {
4840
+ return { ok: true, data: cortexa.status };
4841
+ });
4842
+ router.get("/api/entities", async () => {
4843
+ const { entities } = cortexa.exportGraph();
4844
+ return { ok: true, data: entities };
4845
+ });
4846
+ router.get("/api/relationships", async () => {
4847
+ const { relationships } = cortexa.exportGraph();
4848
+ return { ok: true, data: relationships };
4849
+ });
4850
+ router.post("/api/discover", async (params) => {
4851
+ const body = params.body ?? {};
4852
+ const result = await cortexa.discover({
4853
+ includeSampleData: body.includeSampleData !== false,
4854
+ excludeTables: body.excludeTables,
4855
+ excludeColumns: body.excludeColumns
4856
+ });
4857
+ return {
4858
+ ok: true,
4859
+ data: {
4860
+ tables: result.raw.tables.length,
4861
+ entities: result.entities.length,
4862
+ relationships: result.relationships.length
4863
+ }
4864
+ };
4865
+ });
4866
+ router.get("/api/events", async (params) => {
4867
+ const filter = {};
4868
+ if (params.query.entity) filter.entity = params.query.entity;
4869
+ if (params.query.last) filter.last = parseInt(params.query.last, 10);
4870
+ if (params.query.operation) filter.operation = params.query.operation;
4871
+ return { ok: true, data: cortexa.getEvents(filter) };
4872
+ });
4873
+ router.get("/api/baselines", async () => {
4874
+ return { ok: true, data: cortexa.getBaselines() };
4875
+ });
4876
+ router.get("/api/anomalies", async (params) => {
4877
+ const filter = {};
4878
+ if (params.query.entity) filter.entity = params.query.entity;
4879
+ if (params.query.last) filter.last = parseInt(params.query.last, 10);
4880
+ if (params.query.severity) filter.severity = params.query.severity;
4881
+ return { ok: true, data: cortexa.getAnomalies(filter) };
4882
+ });
4883
+ router.get("/api/insights", async (params) => {
4884
+ const filter = {};
4885
+ if (params.query.entity) filter.entity = params.query.entity;
4886
+ if (params.query.last) filter.last = parseInt(params.query.last, 10);
4887
+ if (params.query.severity) filter.severity = params.query.severity;
4888
+ return { ok: true, data: cortexa.getInsights(filter) };
4889
+ });
4890
+ router.get("/api/transitions", async (params) => {
4891
+ return { ok: true, data: cortexa.getTransitions(params.query.entity) };
4892
+ });
4893
+ router.get("/api/correlations", async () => {
4894
+ return { ok: true, data: cortexa.getCorrelations() };
4895
+ });
4896
+ router.get("/api/distributions", async (params) => {
4897
+ return { ok: true, data: cortexa.getDistributions(params.query.entity) };
4898
+ });
4899
+ router.get("/api/graph", async () => {
4900
+ return { ok: true, data: cortexa.graph().getSummary() };
4901
+ });
4902
+ router.get("/api/graph/export", async () => {
4903
+ return { ok: true, data: cortexa.graph().export() };
4904
+ });
4905
+ router.get("/api/graph/entity/:name", async (params) => {
4906
+ const intel = cortexa.graph().entity(params.pathParams.name).intelligence();
4907
+ if (!intel) {
4908
+ return { ok: false, error: `Entity "${params.pathParams.name}" not found in knowledge graph` };
4909
+ }
4910
+ return { ok: true, data: intel };
4911
+ });
4912
+ router.get("/api/actions", async (params) => {
4913
+ const filter = {};
4914
+ if (params.query.status) filter.status = params.query.status;
4915
+ if (params.query.action) filter.action = params.query.action;
4916
+ return { ok: true, data: cortexa.getRecommendations(filter) };
4917
+ });
4918
+ router.post("/api/actions/:id/approve", async (params) => {
4919
+ const id = parseInt(params.pathParams.id, 10);
4920
+ if (isNaN(id)) return { ok: false, error: "Invalid id" };
4921
+ cortexa.approveRecommendation(id);
4922
+ return { ok: true, data: { id, status: "approved" } };
4923
+ });
4924
+ router.post("/api/actions/:id/reject", async (params) => {
4925
+ const id = parseInt(params.pathParams.id, 10);
4926
+ if (isNaN(id)) return { ok: false, error: "Invalid id" };
4927
+ cortexa.rejectRecommendation(id);
4928
+ return { ok: true, data: { id, status: "rejected" } };
4929
+ });
4930
+ router.post("/api/explain", async (params) => {
4931
+ const body = params.body;
4932
+ if (!body?.type || !body?.id) {
4933
+ return { ok: false, error: 'Required: { type: "anomaly"|"insight"|"event"|"entity", id: number }' };
4934
+ }
4935
+ const target = {
4936
+ type: body.type,
4937
+ id: body.id
4938
+ };
4939
+ const result = await cortexa.explain(target);
4940
+ return { ok: true, data: result };
4941
+ });
4942
+ router.post("/api/ask", async (params) => {
4943
+ const body = params.body;
4944
+ if (!body?.question || typeof body.question !== "string") {
4945
+ return { ok: false, error: "Required: { question: string }" };
4946
+ }
4947
+ const result = await cortexa.ask(body.question, {
4948
+ entityHint: body.entity
4949
+ });
4950
+ return { ok: true, data: result };
4951
+ });
4952
+ router.post("/api/watch", async (params) => {
4953
+ const body = params.body ?? {};
4954
+ await cortexa.watch({
4955
+ interval: body.interval,
4956
+ once: body.once
4957
+ });
4958
+ return { ok: true, data: { watching: true } };
4959
+ });
4960
+ router.post("/api/unwatch", async () => {
4961
+ cortexa.unwatch();
4962
+ return { ok: true, data: { watching: false } };
4963
+ });
4964
+ }
4965
+
4966
+ // src/server/server.ts
4967
+ var CortexaServer = class {
4968
+ cortexa;
4969
+ serverConfig;
4970
+ logger;
4971
+ httpServer = null;
4972
+ constructor(cortexaConfig, serverConfig) {
4973
+ this.cortexa = new Cortexa(cortexaConfig);
4974
+ this.serverConfig = {
4975
+ port: serverConfig?.port ?? 3210,
4976
+ host: serverConfig?.host ?? "127.0.0.1",
4977
+ cors: serverConfig?.cors ?? true
4978
+ };
4979
+ this.logger = createLogger();
4980
+ }
4981
+ async start() {
4982
+ await this.cortexa.connect();
4983
+ const router = new Router();
4984
+ registerHandlers(router, this.cortexa);
4985
+ const cors = this.serverConfig.cors ?? true;
4986
+ this.httpServer = createServer((req, res) => {
4987
+ router.handle(req, res, cors).catch((err) => {
4988
+ this.logger.error({ err }, "Unhandled request error");
4989
+ res.writeHead(500, { "Content-Type": "application/json" });
4990
+ res.end(JSON.stringify({ ok: false, error: "Internal server error" }));
4991
+ });
4992
+ });
4993
+ const { port, host } = this.serverConfig;
4994
+ await new Promise((resolve) => {
4995
+ this.httpServer.listen(port, host, () => {
4996
+ this.logger.info({ port, host }, "Cortexa REST API server started");
4997
+ resolve();
4998
+ });
4999
+ });
5000
+ }
5001
+ async stop() {
5002
+ if (this.httpServer) {
5003
+ await new Promise((resolve, reject) => {
5004
+ this.httpServer.close((err) => {
5005
+ if (err) reject(err);
5006
+ else resolve();
5007
+ });
5008
+ });
5009
+ this.httpServer = null;
5010
+ }
5011
+ await this.cortexa.disconnect();
5012
+ this.logger.info("Cortexa REST API server stopped");
5013
+ }
5014
+ get address() {
5015
+ return {
5016
+ port: this.serverConfig.port ?? 3210,
5017
+ host: this.serverConfig.host ?? "127.0.0.1"
5018
+ };
5019
+ }
5020
+ };
5021
+
4745
5022
  // src/index.ts
4746
5023
  var KnowledgeGraph = class {
4747
5024
  store;
@@ -5811,6 +6088,58 @@ Related Entities: ${result.relatedEntities.join(", ")}`);
5811
6088
  }
5812
6089
  }
5813
6090
 
6091
+ // src/cli/serve.ts
6092
+ async function runServe(options) {
6093
+ try {
6094
+ const config = await loadConfig();
6095
+ const port = options.port ? parseInt(options.port, 10) : 3210;
6096
+ const host = options.host ?? "127.0.0.1";
6097
+ const server = new CortexaServer(
6098
+ { ...config, projectRoot: process.cwd() },
6099
+ { port, host, cors: !options.noCors }
6100
+ );
6101
+ console.log("Starting Cortexa REST API server...");
6102
+ await server.start();
6103
+ console.log("");
6104
+ console.log(` Cortexa API running at http://${host}:${port}`);
6105
+ console.log("");
6106
+ console.log(" Endpoints:");
6107
+ console.log(" GET /api/status Connection status");
6108
+ console.log(" GET /api/entities List entities");
6109
+ console.log(" GET /api/relationships List relationships");
6110
+ console.log(" GET /api/events List events");
6111
+ console.log(" GET /api/baselines List baselines");
6112
+ console.log(" GET /api/anomalies List anomalies");
6113
+ console.log(" GET /api/insights List insights");
6114
+ console.log(" GET /api/transitions Transition stats");
6115
+ console.log(" GET /api/correlations Correlation status");
6116
+ console.log(" GET /api/distributions Distribution summaries");
6117
+ console.log(" GET /api/graph Knowledge graph summary");
6118
+ console.log(" GET /api/graph/export Export full graph");
6119
+ console.log(" GET /api/graph/entity/:n Entity intelligence");
6120
+ console.log(" GET /api/actions Recommendations");
6121
+ console.log(" POST /api/discover Trigger discovery");
6122
+ console.log(" POST /api/explain Explain anomaly/insight");
6123
+ console.log(" POST /api/ask Ask a question");
6124
+ console.log(" POST /api/watch Start watching");
6125
+ console.log(" POST /api/unwatch Stop watching");
6126
+ console.log(" POST /api/actions/:id/approve");
6127
+ console.log(" POST /api/actions/:id/reject");
6128
+ console.log("");
6129
+ console.log(" Press Ctrl+C to stop");
6130
+ const shutdown = async () => {
6131
+ console.log("\nShutting down...");
6132
+ await server.stop();
6133
+ process.exit(0);
6134
+ };
6135
+ process.on("SIGINT", () => void shutdown());
6136
+ process.on("SIGTERM", () => void shutdown());
6137
+ } catch (error) {
6138
+ console.error(`Error: ${error.message}`);
6139
+ process.exit(1);
6140
+ }
6141
+ }
6142
+
5814
6143
  // src/cli/banner.ts
5815
6144
  var RESET = "\x1B[0m";
5816
6145
  var BOLD = "\x1B[1m";
@@ -5917,5 +6246,8 @@ program.command("explain <type> <id>").description("Explain why an anomaly, insi
5917
6246
  program.command("ask <question>").description("Ask a natural language question about your database").option("--json", "Output raw JSON instead of formatted text").option("--entity <name>", "Focus context on a specific entity").action(async (question, options) => {
5918
6247
  await runAsk(question, options);
5919
6248
  });
6249
+ program.command("serve").description("Start REST API server for cross-language access").option("--port <port>", "Port to listen on (default: 3210)").option("--host <host>", "Host to bind to (default: 127.0.0.1)").option("--no-cors", "Disable CORS headers").action(async (options) => {
6250
+ await runServe(options);
6251
+ });
5920
6252
  program.parse();
5921
6253
  //# sourceMappingURL=index.js.map