@timmeck/brain-core 1.0.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.
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/api/server.d.ts +29 -0
- package/dist/api/server.js +183 -0
- package/dist/api/server.js.map +1 -0
- package/dist/cli/colors.d.ts +47 -0
- package/dist/cli/colors.js +93 -0
- package/dist/cli/colors.js.map +1 -0
- package/dist/db/connection.d.ts +2 -0
- package/dist/db/connection.js +19 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc/client.d.ts +16 -0
- package/dist/ipc/client.js +100 -0
- package/dist/ipc/client.js.map +1 -0
- package/dist/ipc/protocol.d.ts +8 -0
- package/dist/ipc/protocol.js +29 -0
- package/dist/ipc/protocol.js.map +1 -0
- package/dist/ipc/server.d.ts +22 -0
- package/dist/ipc/server.js +156 -0
- package/dist/ipc/server.js.map +1 -0
- package/dist/mcp/http-server.d.ts +23 -0
- package/dist/mcp/http-server.js +126 -0
- package/dist/mcp/http-server.js.map +1 -0
- package/dist/mcp/server.d.ts +15 -0
- package/dist/mcp/server.js +66 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/types/ipc.types.d.ts +11 -0
- package/dist/types/ipc.types.js +2 -0
- package/dist/types/ipc.types.js.map +1 -0
- package/dist/utils/events.d.ts +18 -0
- package/dist/utils/events.js +27 -0
- package/dist/utils/events.js.map +1 -0
- package/dist/utils/hash.d.ts +1 -0
- package/dist/utils/hash.js +5 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +43 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/paths.d.ts +8 -0
- package/dist/utils/paths.js +23 -0
- package/dist/utils/paths.js.map +1 -0
- package/package.json +59 -0
- package/src/api/server.ts +210 -0
- package/src/cli/colors.ts +105 -0
- package/src/db/connection.ts +22 -0
- package/src/index.ts +31 -0
- package/src/ipc/client.ts +117 -0
- package/src/ipc/protocol.ts +35 -0
- package/src/ipc/server.ts +170 -0
- package/src/mcp/http-server.ts +148 -0
- package/src/mcp/server.ts +84 -0
- package/src/types/ipc.types.ts +8 -0
- package/src/utils/events.ts +30 -0
- package/src/utils/hash.ts +5 -0
- package/src/utils/logger.ts +67 -0
- package/src/utils/paths.ts +24 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/utils/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;;;;;;;;;GAUG;AACH,MAAM,OAAO,aAAiD,SAAQ,YAAY;IAChF,IAAI,CAA6B,KAAQ,EAAE,IAAU;QACnD,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACjC,CAAC;IAED,EAAE,CAA6B,KAAQ,EAAE,QAA8B;QACrE,OAAO,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IACnC,CAAC;IAED,IAAI,CAA6B,KAAQ,EAAE,QAA8B;QACvE,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IACrC,CAAC;IAED,GAAG,CAA6B,KAAQ,EAAE,QAA8B;QACtE,OAAO,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IACpC,CAAC;CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sha256(input: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.js","sourceRoot":"","sources":["../../src/utils/hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
export interface LoggerOptions {
|
|
3
|
+
level?: string;
|
|
4
|
+
file?: string;
|
|
5
|
+
maxSize?: number;
|
|
6
|
+
maxFiles?: number;
|
|
7
|
+
/** Environment variable name for log level override (e.g. 'BRAIN_LOG_LEVEL') */
|
|
8
|
+
envVar?: string;
|
|
9
|
+
/** Default log filename when no file is specified (e.g. 'brain.log') */
|
|
10
|
+
defaultFilename?: string;
|
|
11
|
+
/** Data directory to place the log file in */
|
|
12
|
+
dataDir?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function createLogger(opts?: LoggerOptions): winston.Logger;
|
|
15
|
+
export declare function getLogger(): winston.Logger;
|
|
16
|
+
export declare function resetLogger(): void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const { combine, timestamp, printf, colorize } = winston.format;
|
|
4
|
+
const logFormat = printf(({ level, message, timestamp, ...meta }) => {
|
|
5
|
+
const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';
|
|
6
|
+
return `${timestamp} [${level}]${metaStr} ${message}`;
|
|
7
|
+
});
|
|
8
|
+
let loggerInstance = null;
|
|
9
|
+
export function createLogger(opts) {
|
|
10
|
+
if (loggerInstance)
|
|
11
|
+
return loggerInstance;
|
|
12
|
+
const envVar = opts?.envVar ?? 'BRAIN_LOG_LEVEL';
|
|
13
|
+
const defaultFilename = opts?.defaultFilename ?? 'brain.log';
|
|
14
|
+
const level = opts?.level ?? process.env[envVar] ?? 'info';
|
|
15
|
+
const logFile = opts?.file ?? (opts?.dataDir ? path.join(opts.dataDir, defaultFilename) : defaultFilename);
|
|
16
|
+
const maxSize = opts?.maxSize ?? 10 * 1024 * 1024; // 10MB
|
|
17
|
+
const maxFiles = opts?.maxFiles ?? 3;
|
|
18
|
+
const transports = [
|
|
19
|
+
new winston.transports.File({
|
|
20
|
+
filename: logFile,
|
|
21
|
+
maxsize: maxSize,
|
|
22
|
+
maxFiles,
|
|
23
|
+
format: combine(timestamp(), logFormat),
|
|
24
|
+
}),
|
|
25
|
+
];
|
|
26
|
+
if (process.env['NODE_ENV'] !== 'production') {
|
|
27
|
+
transports.push(new winston.transports.Console({
|
|
28
|
+
format: combine(colorize(), timestamp(), logFormat),
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
loggerInstance = winston.createLogger({ level, transports });
|
|
32
|
+
return loggerInstance;
|
|
33
|
+
}
|
|
34
|
+
export function getLogger() {
|
|
35
|
+
if (!loggerInstance) {
|
|
36
|
+
return createLogger();
|
|
37
|
+
}
|
|
38
|
+
return loggerInstance;
|
|
39
|
+
}
|
|
40
|
+
export function resetLogger() {
|
|
41
|
+
loggerInstance = null;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;AAEhE,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE;IAClE,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3E,OAAO,GAAG,SAAS,KAAK,KAAK,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC;AACxD,CAAC,CAAC,CAAC;AAEH,IAAI,cAAc,GAA0B,IAAI,CAAC;AAejD,MAAM,UAAU,YAAY,CAAC,IAAoB;IAC/C,IAAI,cAAc;QAAE,OAAO,cAAc,CAAC;IAE1C,MAAM,MAAM,GAAG,IAAI,EAAE,MAAM,IAAI,iBAAiB,CAAC;IACjD,MAAM,eAAe,GAAG,IAAI,EAAE,eAAe,IAAI,WAAW,CAAC;IAE7D,MAAM,KAAK,GAAG,IAAI,EAAE,KAAK,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC;IAC3D,MAAM,OAAO,GAAG,IAAI,EAAE,IAAI,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;IAC3G,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,IAAI,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO;IAC1D,MAAM,QAAQ,GAAG,IAAI,EAAE,QAAQ,IAAI,CAAC,CAAC;IAErC,MAAM,UAAU,GAAwB;QACtC,IAAI,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC;YAC1B,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,OAAO;YAChB,QAAQ;YACR,MAAM,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,SAAS,CAAC;SACxC,CAAC;KACH,CAAC;IAEF,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,YAAY,EAAE,CAAC;QAC7C,UAAU,CAAC,IAAI,CACb,IAAI,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC;YAC7B,MAAM,EAAE,OAAO,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,EAAE,SAAS,CAAC;SACpD,CAAC,CACH,CAAC;IACJ,CAAC;IAED,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;IAC7D,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,OAAO,YAAY,EAAE,CAAC;IACxB,CAAC;IACD,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,cAAc,GAAG,IAAI,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function normalizePath(filePath: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* Get the data directory for a brain instance.
|
|
4
|
+
* @param envVar - Environment variable name (e.g. 'BRAIN_DATA_DIR')
|
|
5
|
+
* @param defaultDir - Default directory name in home (e.g. '.brain')
|
|
6
|
+
*/
|
|
7
|
+
export declare function getDataDir(envVar?: string, defaultDir?: string): string;
|
|
8
|
+
export declare function getPipeName(name?: string): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
export function normalizePath(filePath) {
|
|
4
|
+
return filePath.replace(/\\/g, '/');
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Get the data directory for a brain instance.
|
|
8
|
+
* @param envVar - Environment variable name (e.g. 'BRAIN_DATA_DIR')
|
|
9
|
+
* @param defaultDir - Default directory name in home (e.g. '.brain')
|
|
10
|
+
*/
|
|
11
|
+
export function getDataDir(envVar = 'BRAIN_DATA_DIR', defaultDir = '.brain') {
|
|
12
|
+
const envDirValue = process.env[envVar];
|
|
13
|
+
if (envDirValue)
|
|
14
|
+
return path.resolve(envDirValue);
|
|
15
|
+
return path.join(os.homedir(), defaultDir);
|
|
16
|
+
}
|
|
17
|
+
export function getPipeName(name = 'brain') {
|
|
18
|
+
if (process.platform === 'win32') {
|
|
19
|
+
return `\\\\.\\pipe\\${name}`;
|
|
20
|
+
}
|
|
21
|
+
return path.join(os.tmpdir(), `${name}.sock`);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,OAAO,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AACtC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,SAAiB,gBAAgB,EAAE,aAAqB,QAAQ;IACzF,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACxC,IAAI,WAAW;QAAE,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAClD,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAe,OAAO;IAChD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,OAAO,gBAAgB,IAAI,EAAE,CAAC;IAChC,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,GAAG,IAAI,OAAO,CAAC,CAAC;AAChD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@timmeck/brain-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared core infrastructure for the Brain ecosystem — IPC, MCP, CLI, DB connection, and utilities",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./dist/index.js",
|
|
10
|
+
"./types": "./dist/types/ipc.types.js",
|
|
11
|
+
"./utils/hash": "./dist/utils/hash.js",
|
|
12
|
+
"./utils/logger": "./dist/utils/logger.js",
|
|
13
|
+
"./utils/paths": "./dist/utils/paths.js",
|
|
14
|
+
"./utils/events": "./dist/utils/events.js",
|
|
15
|
+
"./db": "./dist/db/connection.js",
|
|
16
|
+
"./ipc/protocol": "./dist/ipc/protocol.js",
|
|
17
|
+
"./ipc/server": "./dist/ipc/server.js",
|
|
18
|
+
"./ipc/client": "./dist/ipc/client.js",
|
|
19
|
+
"./mcp/server": "./dist/mcp/server.js",
|
|
20
|
+
"./mcp/http-server": "./dist/mcp/http-server.js",
|
|
21
|
+
"./cli/colors": "./dist/cli/colors.js",
|
|
22
|
+
"./api/server": "./dist/api/server.js"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"dev": "tsx src/index.ts",
|
|
27
|
+
"test": "vitest"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"brain",
|
|
31
|
+
"brain-core",
|
|
32
|
+
"mcp",
|
|
33
|
+
"claude-code",
|
|
34
|
+
"ipc",
|
|
35
|
+
"named-pipes",
|
|
36
|
+
"synapse-network",
|
|
37
|
+
"developer-tools",
|
|
38
|
+
"shared-infrastructure"
|
|
39
|
+
],
|
|
40
|
+
"author": "Tim Mecklenburg",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/timmeck/brain-core"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
48
|
+
"better-sqlite3": "^11.7.0",
|
|
49
|
+
"chalk": "^5.6.2",
|
|
50
|
+
"winston": "^3.17.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/better-sqlite3": "^7.6.12",
|
|
54
|
+
"@types/node": "^22.0.0",
|
|
55
|
+
"tsx": "^4.19.0",
|
|
56
|
+
"typescript": "^5.7.0",
|
|
57
|
+
"vitest": "^3.0.0"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { getLogger } from '../utils/logger.js';
|
|
3
|
+
import type { IpcRouter } from '../ipc/server.js';
|
|
4
|
+
|
|
5
|
+
export interface ApiServerOptions {
|
|
6
|
+
port: number;
|
|
7
|
+
router: IpcRouter;
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RouteDefinition {
|
|
12
|
+
method: string;
|
|
13
|
+
pattern: RegExp;
|
|
14
|
+
ipcMethod: string;
|
|
15
|
+
extractParams: (match: RegExpMatchArray, query: URLSearchParams, body?: unknown) => unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class BaseApiServer {
|
|
19
|
+
private server: http.Server | null = null;
|
|
20
|
+
protected logger = getLogger();
|
|
21
|
+
private routes: RouteDefinition[];
|
|
22
|
+
protected sseClients: Set<http.ServerResponse> = new Set();
|
|
23
|
+
|
|
24
|
+
constructor(protected options: ApiServerOptions) {
|
|
25
|
+
this.routes = this.buildRoutes();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
start(): void {
|
|
29
|
+
const { port, apiKey } = this.options;
|
|
30
|
+
|
|
31
|
+
this.server = http.createServer((req, res) => {
|
|
32
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
33
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
34
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key');
|
|
35
|
+
|
|
36
|
+
if (req.method === 'OPTIONS') {
|
|
37
|
+
res.writeHead(204);
|
|
38
|
+
res.end();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (apiKey) {
|
|
43
|
+
const provided = (req.headers['x-api-key'] as string) ??
|
|
44
|
+
req.headers.authorization?.replace('Bearer ', '');
|
|
45
|
+
if (provided !== apiKey) {
|
|
46
|
+
this.json(res, 401, { error: 'Unauthorized', message: 'Invalid or missing API key' });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.handleRequest(req, res).catch((err) => {
|
|
52
|
+
this.logger.error('API error:', err);
|
|
53
|
+
this.json(res, 500, {
|
|
54
|
+
error: 'Internal Server Error',
|
|
55
|
+
message: err instanceof Error ? err.message : String(err),
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
this.server.listen(port, () => {
|
|
61
|
+
this.logger.info(`REST API server started on http://localhost:${port}`);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
stop(): void {
|
|
66
|
+
for (const client of this.sseClients) {
|
|
67
|
+
try { client.end(); } catch { /* ignore */ }
|
|
68
|
+
}
|
|
69
|
+
this.sseClients.clear();
|
|
70
|
+
this.server?.close();
|
|
71
|
+
this.server = null;
|
|
72
|
+
this.logger.info('REST API server stopped');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Override to add domain-specific RESTful routes */
|
|
76
|
+
protected buildRoutes(): RouteDefinition[] {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
81
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
82
|
+
const pathname = url.pathname;
|
|
83
|
+
const method = req.method ?? 'GET';
|
|
84
|
+
const query = url.searchParams;
|
|
85
|
+
|
|
86
|
+
// Health check
|
|
87
|
+
if (pathname === '/api/v1/health') {
|
|
88
|
+
this.json(res, 200, { status: 'ok', timestamp: new Date().toISOString() });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// SSE event stream
|
|
93
|
+
if (pathname === '/api/v1/events' && method === 'GET') {
|
|
94
|
+
res.writeHead(200, {
|
|
95
|
+
'Content-Type': 'text/event-stream',
|
|
96
|
+
'Cache-Control': 'no-cache',
|
|
97
|
+
'Connection': 'keep-alive',
|
|
98
|
+
});
|
|
99
|
+
res.write('data: {"type":"connected"}\n\n');
|
|
100
|
+
this.sseClients.add(res);
|
|
101
|
+
req.on('close', () => this.sseClients.delete(res));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// List all available methods
|
|
106
|
+
if (pathname === '/api/v1/methods' && method === 'GET') {
|
|
107
|
+
const methods = this.options.router.listMethods();
|
|
108
|
+
this.json(res, 200, {
|
|
109
|
+
methods,
|
|
110
|
+
rpcEndpoint: '/api/v1/rpc',
|
|
111
|
+
usage: 'POST /api/v1/rpc with body { "method": "<method>", "params": {...} }',
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Generic RPC endpoint
|
|
117
|
+
if (pathname === '/api/v1/rpc' && method === 'POST') {
|
|
118
|
+
const body = await this.readBody(req);
|
|
119
|
+
if (!body) {
|
|
120
|
+
this.json(res, 400, { error: 'Bad Request', message: 'Empty request body' });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const parsed = JSON.parse(body);
|
|
125
|
+
|
|
126
|
+
// Batch RPC support
|
|
127
|
+
if (Array.isArray(parsed)) {
|
|
128
|
+
const results = parsed.map((call: { method: string; params?: unknown; id?: string | number }) => {
|
|
129
|
+
try {
|
|
130
|
+
const result = this.options.router.handle(call.method, call.params ?? {});
|
|
131
|
+
return { id: call.id, result };
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return { id: call.id, error: err instanceof Error ? err.message : String(err) };
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
this.json(res, 200, results);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!parsed.method) {
|
|
141
|
+
this.json(res, 400, { error: 'Bad Request', message: 'Missing "method" field' });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const result = this.options.router.handle(parsed.method, parsed.params ?? {});
|
|
147
|
+
this.json(res, 200, { result });
|
|
148
|
+
} catch (err) {
|
|
149
|
+
this.json(res, 400, { error: err instanceof Error ? err.message : String(err) });
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// RESTful routes
|
|
155
|
+
let body: unknown = undefined;
|
|
156
|
+
if (method === 'POST' || method === 'PUT') {
|
|
157
|
+
try {
|
|
158
|
+
const raw = await this.readBody(req);
|
|
159
|
+
body = raw ? JSON.parse(raw) : {};
|
|
160
|
+
} catch {
|
|
161
|
+
this.json(res, 400, { error: 'Bad Request', message: 'Invalid JSON body' });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const route of this.routes) {
|
|
167
|
+
if (route.method !== method) continue;
|
|
168
|
+
const match = pathname.match(route.pattern);
|
|
169
|
+
if (!match) continue;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const params = route.extractParams(match, query, body);
|
|
173
|
+
const result = this.options.router.handle(route.ipcMethod, params);
|
|
174
|
+
this.json(res, method === 'POST' ? 201 : 200, { result });
|
|
175
|
+
} catch (err) {
|
|
176
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
177
|
+
const status = msg.startsWith('Unknown method') ? 404 : 400;
|
|
178
|
+
this.json(res, status, { error: msg });
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.json(res, 404, { error: 'Not Found', message: `No route for ${method} ${pathname}` });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
protected json(res: http.ServerResponse, status: number, data: unknown): void {
|
|
187
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
188
|
+
res.end(JSON.stringify(data));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
protected readBody(req: http.IncomingMessage): Promise<string> {
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
const chunks: Buffer[] = [];
|
|
194
|
+
req.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
195
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
196
|
+
req.on('error', reject);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
protected broadcastSSE(data: unknown): void {
|
|
201
|
+
const msg = `data: ${JSON.stringify(data)}\n\n`;
|
|
202
|
+
for (const client of this.sseClients) {
|
|
203
|
+
try {
|
|
204
|
+
client.write(msg);
|
|
205
|
+
} catch {
|
|
206
|
+
this.sseClients.delete(client);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
// Shared brand color palette — identical across all brains
|
|
4
|
+
export const c = {
|
|
5
|
+
// Primary palette
|
|
6
|
+
blue: chalk.hex('#5b9cff'),
|
|
7
|
+
purple: chalk.hex('#b47aff'),
|
|
8
|
+
cyan: chalk.hex('#47e5ff'),
|
|
9
|
+
green: chalk.hex('#3dffa0'),
|
|
10
|
+
red: chalk.hex('#ff5577'),
|
|
11
|
+
orange: chalk.hex('#ffb347'),
|
|
12
|
+
dim: chalk.hex('#8b8fb0'),
|
|
13
|
+
dimmer: chalk.hex('#4a4d6e'),
|
|
14
|
+
|
|
15
|
+
// Semantic
|
|
16
|
+
label: chalk.hex('#8b8fb0'),
|
|
17
|
+
value: chalk.white.bold,
|
|
18
|
+
heading: chalk.hex('#5b9cff').bold,
|
|
19
|
+
success: chalk.hex('#3dffa0').bold,
|
|
20
|
+
error: chalk.hex('#ff5577').bold,
|
|
21
|
+
warn: chalk.hex('#ffb347').bold,
|
|
22
|
+
info: chalk.hex('#47e5ff'),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Shared base icons — each brain extends with domain-specific icons
|
|
26
|
+
export const baseIcons = {
|
|
27
|
+
check: '\u2713',
|
|
28
|
+
cross: '\u2717',
|
|
29
|
+
arrow: '\u2192',
|
|
30
|
+
dot: '\u25CF',
|
|
31
|
+
circle: '\u25CB',
|
|
32
|
+
bar: '\u2588',
|
|
33
|
+
barLight: '\u2591',
|
|
34
|
+
dash: '\u2500',
|
|
35
|
+
pipe: '\u2502',
|
|
36
|
+
corner: '\u2514',
|
|
37
|
+
tee: '\u251C',
|
|
38
|
+
star: '\u2605',
|
|
39
|
+
bolt: '\u26A1',
|
|
40
|
+
gear: '\u2699',
|
|
41
|
+
chart: '\uD83D\uDCCA',
|
|
42
|
+
synapse: '\uD83D\uDD17',
|
|
43
|
+
insight: '\uD83D\uDCA1',
|
|
44
|
+
warn: '\u26A0',
|
|
45
|
+
error: '\u274C',
|
|
46
|
+
ok: '\u2705',
|
|
47
|
+
clock: '\u23F1',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function header(title: string, icon?: string): string {
|
|
51
|
+
const prefix = icon ? `${icon} ` : '';
|
|
52
|
+
const line = c.dimmer(baseIcons.dash.repeat(40));
|
|
53
|
+
return `\n${line}\n${prefix}${c.heading(title)}\n${line}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function keyValue(key: string, value: string | number, indent = 2): string {
|
|
57
|
+
const pad = ' '.repeat(indent);
|
|
58
|
+
return `${pad}${c.label(key + ':')} ${c.value(String(value))}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function statusBadge(status: string): string {
|
|
62
|
+
switch (status.toLowerCase()) {
|
|
63
|
+
case 'resolved':
|
|
64
|
+
case 'active':
|
|
65
|
+
case 'running':
|
|
66
|
+
return c.green(`[${status.toUpperCase()}]`);
|
|
67
|
+
case 'open':
|
|
68
|
+
case 'unresolved':
|
|
69
|
+
return c.red(`[${status.toUpperCase()}]`);
|
|
70
|
+
case 'warning':
|
|
71
|
+
return c.warn(`[${status.toUpperCase()}]`);
|
|
72
|
+
default:
|
|
73
|
+
return c.dim(`[${status.toUpperCase()}]`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function progressBar(current: number, total: number, width = 20): string {
|
|
78
|
+
const pct = Math.min(1, current / Math.max(1, total));
|
|
79
|
+
const filled = Math.round(pct * width);
|
|
80
|
+
const empty = width - filled;
|
|
81
|
+
return c.cyan(baseIcons.bar.repeat(filled)) + c.dimmer(baseIcons.barLight.repeat(empty));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function divider(width = 40): string {
|
|
85
|
+
return c.dimmer(baseIcons.dash.repeat(width));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function table(rows: string[][], colWidths?: number[]): string {
|
|
89
|
+
if (rows.length === 0) return '';
|
|
90
|
+
const widths = colWidths ?? rows[0].map((_, i) =>
|
|
91
|
+
Math.max(...rows.map(r => stripAnsi(r[i] ?? '').length))
|
|
92
|
+
);
|
|
93
|
+
return rows.map(row =>
|
|
94
|
+
row.map((cell, i) => {
|
|
95
|
+
const stripped = stripAnsi(cell);
|
|
96
|
+
const pad = Math.max(0, (widths[i] ?? stripped.length) - stripped.length);
|
|
97
|
+
return cell + ' '.repeat(pad);
|
|
98
|
+
}).join(' ')
|
|
99
|
+
).join('\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function stripAnsi(str: string): string {
|
|
103
|
+
// eslint-disable-next-line no-control-regex
|
|
104
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
105
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getLogger } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
export function createConnection(dbPath: string): Database.Database {
|
|
7
|
+
const logger = getLogger();
|
|
8
|
+
const dir = path.dirname(dbPath);
|
|
9
|
+
if (!fs.existsSync(dir)) {
|
|
10
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
logger.info(`Opening database at ${dbPath}`);
|
|
14
|
+
const db = new Database(dbPath);
|
|
15
|
+
|
|
16
|
+
db.pragma('journal_mode = WAL');
|
|
17
|
+
db.pragma('synchronous = NORMAL');
|
|
18
|
+
db.pragma('cache_size = 10000');
|
|
19
|
+
db.pragma('foreign_keys = ON');
|
|
20
|
+
|
|
21
|
+
return db;
|
|
22
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// ── Types ──────────────────────────────────────────────────
|
|
2
|
+
export type { IpcMessage } from './types/ipc.types.js';
|
|
3
|
+
|
|
4
|
+
// ── Utils ──────────────────────────────────────────────────
|
|
5
|
+
export { sha256 } from './utils/hash.js';
|
|
6
|
+
export { createLogger, getLogger, resetLogger } from './utils/logger.js';
|
|
7
|
+
export type { LoggerOptions } from './utils/logger.js';
|
|
8
|
+
export { normalizePath, getDataDir, getPipeName } from './utils/paths.js';
|
|
9
|
+
export { TypedEventBus } from './utils/events.js';
|
|
10
|
+
|
|
11
|
+
// ── DB ─────────────────────────────────────────────────────
|
|
12
|
+
export { createConnection } from './db/connection.js';
|
|
13
|
+
|
|
14
|
+
// ── IPC ────────────────────────────────────────────────────
|
|
15
|
+
export { encodeMessage, MessageDecoder } from './ipc/protocol.js';
|
|
16
|
+
export { IpcServer } from './ipc/server.js';
|
|
17
|
+
export type { IpcRouter } from './ipc/server.js';
|
|
18
|
+
export { IpcClient } from './ipc/client.js';
|
|
19
|
+
|
|
20
|
+
// ── MCP ────────────────────────────────────────────────────
|
|
21
|
+
export { startMcpServer } from './mcp/server.js';
|
|
22
|
+
export type { McpServerOptions } from './mcp/server.js';
|
|
23
|
+
export { McpHttpServer } from './mcp/http-server.js';
|
|
24
|
+
export type { McpHttpServerOptions } from './mcp/http-server.js';
|
|
25
|
+
|
|
26
|
+
// ── CLI ────────────────────────────────────────────────────
|
|
27
|
+
export { c, baseIcons, header, keyValue, statusBadge, progressBar, divider, table, stripAnsi } from './cli/colors.js';
|
|
28
|
+
|
|
29
|
+
// ── API ────────────────────────────────────────────────────
|
|
30
|
+
export { BaseApiServer } from './api/server.js';
|
|
31
|
+
export type { ApiServerOptions, RouteDefinition } from './api/server.js';
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import type { IpcMessage } from '../types/ipc.types.js';
|
|
4
|
+
import { encodeMessage, MessageDecoder } from './protocol.js';
|
|
5
|
+
import { getPipeName } from '../utils/paths.js';
|
|
6
|
+
|
|
7
|
+
interface PendingRequest {
|
|
8
|
+
resolve: (result: unknown) => void;
|
|
9
|
+
reject: (err: Error) => void;
|
|
10
|
+
timer: ReturnType<typeof setTimeout>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class IpcClient {
|
|
14
|
+
private socket: net.Socket | null = null;
|
|
15
|
+
private decoder = new MessageDecoder();
|
|
16
|
+
private pending = new Map<string, PendingRequest>();
|
|
17
|
+
private onNotification?: (msg: IpcMessage) => void;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
private pipeName: string = getPipeName(),
|
|
21
|
+
private timeout: number = 5000,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
connect(): Promise<void> {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
this.socket = net.createConnection(this.pipeName, () => {
|
|
27
|
+
resolve();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
this.socket.on('data', (chunk) => {
|
|
31
|
+
const messages = this.decoder.feed(chunk);
|
|
32
|
+
for (const msg of messages) {
|
|
33
|
+
this.handleMessage(msg);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
this.socket.on('error', (err) => {
|
|
38
|
+
reject(err);
|
|
39
|
+
for (const [id, req] of this.pending) {
|
|
40
|
+
clearTimeout(req.timer);
|
|
41
|
+
req.reject(new Error(`Connection error: ${err.message}`));
|
|
42
|
+
this.pending.delete(id);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
this.socket.on('close', () => {
|
|
47
|
+
for (const [id, req] of this.pending) {
|
|
48
|
+
clearTimeout(req.timer);
|
|
49
|
+
req.reject(new Error('Connection closed'));
|
|
50
|
+
this.pending.delete(id);
|
|
51
|
+
}
|
|
52
|
+
this.socket = null;
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
request(method: string, params?: unknown): Promise<unknown> {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
if (!this.socket || this.socket.destroyed) {
|
|
60
|
+
return reject(new Error('Not connected'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const id = randomUUID();
|
|
64
|
+
const timer = setTimeout(() => {
|
|
65
|
+
this.pending.delete(id);
|
|
66
|
+
reject(new Error(`Request timeout: ${method} (${this.timeout}ms)`));
|
|
67
|
+
}, this.timeout);
|
|
68
|
+
|
|
69
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
70
|
+
|
|
71
|
+
const msg: IpcMessage = {
|
|
72
|
+
id,
|
|
73
|
+
type: 'request',
|
|
74
|
+
method,
|
|
75
|
+
params,
|
|
76
|
+
};
|
|
77
|
+
this.socket.write(encodeMessage(msg));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setNotificationHandler(handler: (msg: IpcMessage) => void): void {
|
|
82
|
+
this.onNotification = handler;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
disconnect(): void {
|
|
86
|
+
for (const [id, req] of this.pending) {
|
|
87
|
+
clearTimeout(req.timer);
|
|
88
|
+
req.reject(new Error('Client disconnecting'));
|
|
89
|
+
this.pending.delete(id);
|
|
90
|
+
}
|
|
91
|
+
this.socket?.destroy();
|
|
92
|
+
this.socket = null;
|
|
93
|
+
this.decoder.reset();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get connected(): boolean {
|
|
97
|
+
return this.socket !== null && !this.socket.destroyed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private handleMessage(msg: IpcMessage): void {
|
|
101
|
+
if (msg.type === 'response') {
|
|
102
|
+
const req = this.pending.get(msg.id);
|
|
103
|
+
if (!req) return;
|
|
104
|
+
|
|
105
|
+
clearTimeout(req.timer);
|
|
106
|
+
this.pending.delete(msg.id);
|
|
107
|
+
|
|
108
|
+
if (msg.error) {
|
|
109
|
+
req.reject(new Error(msg.error.message));
|
|
110
|
+
} else {
|
|
111
|
+
req.resolve(msg.result);
|
|
112
|
+
}
|
|
113
|
+
} else if (msg.type === 'notification') {
|
|
114
|
+
this.onNotification?.(msg);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|