@timmeck/brain-core 1.1.0 → 1.2.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 (39) hide show
  1. package/README.md +3 -1
  2. package/brain.log +0 -0
  3. package/dist/cross-brain/__tests__/client.test.d.ts +1 -0
  4. package/dist/cross-brain/__tests__/client.test.js +31 -0
  5. package/dist/cross-brain/__tests__/client.test.js.map +1 -0
  6. package/dist/ipc/__tests__/protocol.test.d.ts +1 -0
  7. package/dist/ipc/__tests__/protocol.test.js +69 -0
  8. package/dist/ipc/__tests__/protocol.test.js.map +1 -0
  9. package/dist/utils/__tests__/events.test.d.ts +1 -0
  10. package/dist/utils/__tests__/events.test.js +38 -0
  11. package/dist/utils/__tests__/events.test.js.map +1 -0
  12. package/dist/utils/__tests__/hash.test.d.ts +1 -0
  13. package/dist/utils/__tests__/hash.test.js +25 -0
  14. package/dist/utils/__tests__/hash.test.js.map +1 -0
  15. package/dist/utils/__tests__/logger.test.d.ts +1 -0
  16. package/dist/utils/__tests__/logger.test.js +29 -0
  17. package/dist/utils/__tests__/logger.test.js.map +1 -0
  18. package/dist/utils/__tests__/paths.test.d.ts +1 -0
  19. package/dist/utils/__tests__/paths.test.js +50 -0
  20. package/dist/utils/__tests__/paths.test.js.map +1 -0
  21. package/package.json +1 -1
  22. package/.github/FUNDING.yml +0 -1
  23. package/.github/workflows/ci.yml +0 -21
  24. package/src/api/server.ts +0 -210
  25. package/src/cli/colors.ts +0 -105
  26. package/src/cross-brain/client.ts +0 -94
  27. package/src/db/connection.ts +0 -22
  28. package/src/index.ts +0 -35
  29. package/src/ipc/client.ts +0 -117
  30. package/src/ipc/protocol.ts +0 -35
  31. package/src/ipc/server.ts +0 -170
  32. package/src/mcp/http-server.ts +0 -148
  33. package/src/mcp/server.ts +0 -84
  34. package/src/types/ipc.types.ts +0 -8
  35. package/src/utils/events.ts +0 -30
  36. package/src/utils/hash.ts +0 -5
  37. package/src/utils/logger.ts +0 -67
  38. package/src/utils/paths.ts +0 -24
  39. package/tsconfig.json +0 -18
package/src/cli/colors.ts DELETED
@@ -1,105 +0,0 @@
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
- }
@@ -1,94 +0,0 @@
1
- import { IpcClient } from '../ipc/client.js';
2
- import { getPipeName } from '../utils/paths.js';
3
-
4
- export interface BrainPeer {
5
- name: string;
6
- pipeName: string;
7
- }
8
-
9
- const DEFAULT_PEERS: BrainPeer[] = [
10
- { name: 'brain', pipeName: getPipeName('brain') },
11
- { name: 'trading-brain', pipeName: getPipeName('trading-brain') },
12
- { name: 'marketing-brain', pipeName: getPipeName('marketing-brain') },
13
- ];
14
-
15
- export class CrossBrainClient {
16
- private peers: BrainPeer[];
17
-
18
- constructor(
19
- private selfName: string,
20
- peers?: BrainPeer[],
21
- ) {
22
- this.peers = (peers ?? DEFAULT_PEERS).filter(p => p.name !== selfName);
23
- }
24
-
25
- /**
26
- * Query a specific peer brain by name.
27
- * Returns null if the peer is not available.
28
- */
29
- async query(peerName: string, method: string, params?: unknown): Promise<unknown | null> {
30
- const peer = this.peers.find(p => p.name === peerName);
31
- if (!peer) return null;
32
-
33
- const client = new IpcClient(peer.pipeName, 3000);
34
- try {
35
- await client.connect();
36
- const result = await client.request(method, params);
37
- return result;
38
- } catch {
39
- return null;
40
- } finally {
41
- client.disconnect();
42
- }
43
- }
44
-
45
- /**
46
- * Broadcast a query to all available peer brains.
47
- * Returns results from all peers that responded.
48
- */
49
- async broadcast(method: string, params?: unknown): Promise<{ name: string; result: unknown }[]> {
50
- const results: { name: string; result: unknown }[] = [];
51
-
52
- const promises = this.peers.map(async (peer) => {
53
- const client = new IpcClient(peer.pipeName, 3000);
54
- try {
55
- await client.connect();
56
- const result = await client.request(method, params);
57
- results.push({ name: peer.name, result });
58
- } catch {
59
- // Peer not available — skip
60
- } finally {
61
- client.disconnect();
62
- }
63
- });
64
-
65
- await Promise.all(promises);
66
- return results;
67
- }
68
-
69
- /**
70
- * Check which peer brains are currently running.
71
- */
72
- async getAvailablePeers(): Promise<string[]> {
73
- const available: string[] = [];
74
-
75
- const checks = this.peers.map(async (peer) => {
76
- const client = new IpcClient(peer.pipeName, 1000);
77
- try {
78
- await client.connect();
79
- available.push(peer.name);
80
- } catch {
81
- // Not available
82
- } finally {
83
- client.disconnect();
84
- }
85
- });
86
-
87
- await Promise.all(checks);
88
- return available;
89
- }
90
-
91
- getPeerNames(): string[] {
92
- return this.peers.map(p => p.name);
93
- }
94
- }
@@ -1,22 +0,0 @@
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 DELETED
@@ -1,35 +0,0 @@
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';
32
-
33
- // ── Cross-Brain ────────────────────────────────────────────
34
- export { CrossBrainClient } from './cross-brain/client.js';
35
- export type { BrainPeer } from './cross-brain/client.js';
package/src/ipc/client.ts DELETED
@@ -1,117 +0,0 @@
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
- }
@@ -1,35 +0,0 @@
1
- import { Buffer } from 'node:buffer';
2
- import type { IpcMessage } from '../types/ipc.types.js';
3
-
4
- export function encodeMessage(msg: IpcMessage): Buffer {
5
- const json = JSON.stringify(msg);
6
- const payload = Buffer.from(json, 'utf8');
7
- const frame = Buffer.alloc(4 + payload.length);
8
- frame.writeUInt32BE(payload.length, 0);
9
- payload.copy(frame, 4);
10
- return frame;
11
- }
12
-
13
- export class MessageDecoder {
14
- private buffer = Buffer.alloc(0);
15
-
16
- feed(chunk: Buffer): IpcMessage[] {
17
- this.buffer = Buffer.concat([this.buffer, chunk]);
18
- const messages: IpcMessage[] = [];
19
-
20
- while (this.buffer.length >= 4) {
21
- const length = this.buffer.readUInt32BE(0);
22
- if (this.buffer.length < 4 + length) break;
23
-
24
- const json = this.buffer.subarray(4, 4 + length).toString('utf8');
25
- this.buffer = this.buffer.subarray(4 + length);
26
- messages.push(JSON.parse(json) as IpcMessage);
27
- }
28
-
29
- return messages;
30
- }
31
-
32
- reset(): void {
33
- this.buffer = Buffer.alloc(0);
34
- }
35
- }
package/src/ipc/server.ts DELETED
@@ -1,170 +0,0 @@
1
- import net from 'node:net';
2
- import fs from 'node:fs';
3
- import { randomUUID } from 'node:crypto';
4
- import { getLogger } from '../utils/logger.js';
5
- import type { IpcMessage } from '../types/ipc.types.js';
6
- import { encodeMessage, MessageDecoder } from './protocol.js';
7
-
8
- export interface IpcRouter {
9
- handle(method: string, params: unknown): unknown;
10
- listMethods(): string[];
11
- }
12
-
13
- export class IpcServer {
14
- private server: net.Server | null = null;
15
- private clients = new Map<string, net.Socket>();
16
- private logger = getLogger();
17
-
18
- constructor(
19
- private router: IpcRouter,
20
- private pipeName: string,
21
- private daemonName: string = 'brain',
22
- ) {}
23
-
24
- start(): void {
25
- this.createServer();
26
- this.listen();
27
- }
28
-
29
- private createServer(): void {
30
- this.server = net.createServer((socket) => {
31
- const clientId = randomUUID();
32
- this.clients.set(clientId, socket);
33
- const decoder = new MessageDecoder();
34
-
35
- this.logger.info(`IPC client connected: ${clientId}`);
36
-
37
- socket.on('data', (chunk) => {
38
- const messages = decoder.feed(chunk);
39
- for (const msg of messages) {
40
- this.handleMessage(clientId, msg, socket);
41
- }
42
- });
43
-
44
- socket.on('close', () => {
45
- this.logger.info(`IPC client disconnected: ${clientId}`);
46
- this.clients.delete(clientId);
47
- });
48
-
49
- socket.on('error', (err) => {
50
- this.logger.error(`IPC client ${clientId} error:`, err);
51
- this.clients.delete(clientId);
52
- });
53
- });
54
- }
55
-
56
- private listen(retried = false): void {
57
- if (!this.server) return;
58
-
59
- this.server.on('error', (err: NodeJS.ErrnoException) => {
60
- if (err.code === 'EADDRINUSE' && !retried) {
61
- this.logger.warn(`IPC pipe in use, attempting to recover stale pipe: ${this.pipeName}`);
62
- this.recoverStalePipe();
63
- } else {
64
- this.logger.error('IPC server error:', err);
65
- }
66
- });
67
-
68
- this.server.listen(this.pipeName, () => {
69
- this.logger.info(`IPC server listening on ${this.pipeName}`);
70
- });
71
- }
72
-
73
- private recoverStalePipe(): void {
74
- const probe = net.createConnection(this.pipeName);
75
-
76
- probe.on('connect', () => {
77
- probe.destroy();
78
- this.logger.error(`IPC pipe is held by another running daemon. Stop it first with: ${this.daemonName} stop`);
79
- });
80
-
81
- probe.on('error', () => {
82
- probe.destroy();
83
- this.logger.info('Stale IPC pipe detected, reclaiming...');
84
-
85
- if (process.platform !== 'win32') {
86
- try { fs.unlinkSync(this.pipeName); } catch { /* ignore */ }
87
- }
88
-
89
- this.createServer();
90
- this.server!.on('error', (err) => {
91
- this.logger.error('IPC server error after recovery:', err);
92
- });
93
- this.server!.listen(this.pipeName, () => {
94
- this.logger.info(`IPC server recovered and listening on ${this.pipeName}`);
95
- });
96
- });
97
-
98
- probe.setTimeout(2000, () => {
99
- probe.destroy();
100
- this.logger.warn('IPC pipe probe timed out, treating as stale');
101
- if (process.platform !== 'win32') {
102
- try { fs.unlinkSync(this.pipeName); } catch { /* ignore */ }
103
- }
104
- this.createServer();
105
- this.server!.on('error', (err) => {
106
- this.logger.error('IPC server error after timeout recovery:', err);
107
- });
108
- this.server!.listen(this.pipeName, () => {
109
- this.logger.info(`IPC server recovered (timeout) and listening on ${this.pipeName}`);
110
- });
111
- });
112
- }
113
-
114
- private handleMessage(_clientId: string, msg: IpcMessage, socket: net.Socket): void {
115
- if (msg.type !== 'request' || !msg.method) return;
116
-
117
- try {
118
- const result = this.router.handle(msg.method, msg.params);
119
- const response: IpcMessage = {
120
- id: msg.id,
121
- type: 'response',
122
- result,
123
- };
124
- socket.write(encodeMessage(response));
125
- } catch (err) {
126
- const response: IpcMessage = {
127
- id: msg.id,
128
- type: 'response',
129
- error: { code: -1, message: err instanceof Error ? err.message : String(err) },
130
- };
131
- socket.write(encodeMessage(response));
132
- }
133
- }
134
-
135
- notify(clientId: string | null, notification: Omit<IpcMessage, 'id' | 'type'>): void {
136
- const msg: IpcMessage = {
137
- id: randomUUID(),
138
- type: 'notification',
139
- ...notification,
140
- };
141
- const encoded = encodeMessage(msg);
142
-
143
- if (clientId) {
144
- const socket = this.clients.get(clientId);
145
- if (socket && !socket.destroyed) {
146
- socket.write(encoded);
147
- }
148
- } else {
149
- for (const socket of this.clients.values()) {
150
- if (!socket.destroyed) {
151
- socket.write(encoded);
152
- }
153
- }
154
- }
155
- }
156
-
157
- getClientCount(): number {
158
- return this.clients.size;
159
- }
160
-
161
- stop(): void {
162
- for (const socket of this.clients.values()) {
163
- socket.destroy();
164
- }
165
- this.clients.clear();
166
- this.server?.close();
167
- this.server = null;
168
- this.logger.info('IPC server stopped');
169
- }
170
- }