@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/README.md CHANGED
@@ -135,10 +135,12 @@ class MyRouter implements IpcRouter {
135
135
  |-------|---------|-------|
136
136
  | [Brain](https://github.com/timmeck/brain) | Error memory & code intelligence | 7777/7778 |
137
137
  | [Trading Brain](https://github.com/timmeck/trading-brain) | Adaptive trading intelligence | 7779/7780 |
138
- | [Marketing Brain](https://github.com/timmeck/marketing-brain) | Content strategy & social media | 7781/7782 |
138
+ | [Marketing Brain](https://github.com/timmeck/marketing-brain) | Content strategy & social media | 7781/7782/7783 |
139
139
 
140
140
  All three brains are standalone — brain-core is an **optional** shared dependency that eliminates code duplication.
141
141
 
142
+ Visit the [Brain Hub](https://timmeck.github.io/brain-hub/) for the full ecosystem overview.
143
+
142
144
  ## License
143
145
 
144
146
  [MIT](LICENSE)
package/brain.log ADDED
File without changes
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { CrossBrainClient } from '../client.js';
3
+ describe('CrossBrainClient', () => {
4
+ it('filters self from peers', () => {
5
+ const client = new CrossBrainClient('brain');
6
+ const names = client.getPeerNames();
7
+ expect(names).not.toContain('brain');
8
+ expect(names).toContain('trading-brain');
9
+ expect(names).toContain('marketing-brain');
10
+ });
11
+ it('returns empty for unavailable peers', async () => {
12
+ const client = new CrossBrainClient('brain');
13
+ const result = await client.query('nonexistent', 'status');
14
+ expect(result).toBeNull();
15
+ });
16
+ it('broadcast returns empty when no peers available', async () => {
17
+ const client = new CrossBrainClient('test', [
18
+ { name: 'fake', pipeName: '\\\\.\\pipe\\nonexistent-test-pipe' },
19
+ ]);
20
+ const results = await client.broadcast('status');
21
+ expect(results).toEqual([]);
22
+ });
23
+ it('getAvailablePeers returns empty when none running', async () => {
24
+ const client = new CrossBrainClient('test', [
25
+ { name: 'fake', pipeName: '\\\\.\\pipe\\nonexistent-test-pipe' },
26
+ ]);
27
+ const available = await client.getAvailablePeers();
28
+ expect(available).toEqual([]);
29
+ });
30
+ });
31
+ //# sourceMappingURL=client.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.test.js","sourceRoot":"","sources":["../../../src/cross-brain/__tests__/client.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEhD,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACjC,MAAM,MAAM,GAAG,IAAI,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAC7C,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,EAAE,CAAC;QACpC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QACrC,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;QACzC,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,KAAK,IAAI,EAAE;QACnD,MAAM,MAAM,GAAG,IAAI,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAC7C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;QAC3D,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QAC/D,MAAM,MAAM,GAAG,IAAI,gBAAgB,CAAC,MAAM,EAAE;YAC1C,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,oCAAoC,EAAE;SACjE,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QACjD,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;QACjE,MAAM,MAAM,GAAG,IAAI,gBAAgB,CAAC,MAAM,EAAE;YAC1C,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,oCAAoC,EAAE;SACjE,CAAC,CAAC;QACH,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,iBAAiB,EAAE,CAAC;QACnD,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { encodeMessage, MessageDecoder } from '../protocol.js';
3
+ describe('encodeMessage', () => {
4
+ it('encodes a request message', () => {
5
+ const msg = { id: 'test-1', type: 'request', method: 'status', params: {} };
6
+ const buffer = encodeMessage(msg);
7
+ expect(buffer).toBeInstanceOf(Buffer);
8
+ expect(buffer.length).toBeGreaterThan(4);
9
+ });
10
+ it('includes 4-byte length prefix', () => {
11
+ const msg = { id: 'test-1', type: 'request', method: 'test' };
12
+ const buffer = encodeMessage(msg);
13
+ const payloadLength = buffer.readUInt32BE(0);
14
+ expect(buffer.length).toBe(4 + payloadLength);
15
+ });
16
+ });
17
+ describe('MessageDecoder', () => {
18
+ it('decodes a single complete message', () => {
19
+ const msg = { id: 'x', type: 'response', result: { ok: true } };
20
+ const decoder = new MessageDecoder();
21
+ const messages = decoder.feed(encodeMessage(msg));
22
+ expect(messages).toHaveLength(1);
23
+ expect(messages[0].id).toBe('x');
24
+ expect(messages[0].result).toEqual({ ok: true });
25
+ });
26
+ it('handles multiple messages in one chunk', () => {
27
+ const msg1 = { id: '1', type: 'request', method: 'a' };
28
+ const msg2 = { id: '2', type: 'request', method: 'b' };
29
+ const decoder = new MessageDecoder();
30
+ const combined = Buffer.concat([encodeMessage(msg1), encodeMessage(msg2)]);
31
+ const messages = decoder.feed(combined);
32
+ expect(messages).toHaveLength(2);
33
+ });
34
+ it('handles partial messages across feeds', () => {
35
+ const msg = { id: 'partial', type: 'response', result: 'data' };
36
+ const buffer = encodeMessage(msg);
37
+ const decoder = new MessageDecoder();
38
+ const mid = Math.floor(buffer.length / 2);
39
+ const part1 = buffer.subarray(0, mid);
40
+ const part2 = buffer.subarray(mid);
41
+ expect(decoder.feed(part1)).toHaveLength(0);
42
+ const messages = decoder.feed(part2);
43
+ expect(messages).toHaveLength(1);
44
+ expect(messages[0].id).toBe('partial');
45
+ });
46
+ it('resets buffer state', () => {
47
+ const decoder = new MessageDecoder();
48
+ decoder.feed(Buffer.from([0, 0, 0, 10])); // incomplete
49
+ decoder.reset();
50
+ const msg = { id: 'after-reset', type: 'response', result: null };
51
+ const messages = decoder.feed(encodeMessage(msg));
52
+ expect(messages).toHaveLength(1);
53
+ expect(messages[0].id).toBe('after-reset');
54
+ });
55
+ it('handles byte-by-byte feeding', () => {
56
+ const msg = { id: 'byte', type: 'request', method: 'test' };
57
+ const buffer = encodeMessage(msg);
58
+ const decoder = new MessageDecoder();
59
+ let messages = [];
60
+ for (let i = 0; i < buffer.length; i++) {
61
+ messages = decoder.feed(buffer.subarray(i, i + 1));
62
+ if (messages.length > 0)
63
+ break;
64
+ }
65
+ expect(messages).toHaveLength(1);
66
+ expect(messages[0].id).toBe('byte');
67
+ });
68
+ });
69
+ //# sourceMappingURL=protocol.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.test.js","sourceRoot":"","sources":["../../../src/ipc/__tests__/protocol.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { TypedEventBus } from '../events.js';
3
+ describe('TypedEventBus', () => {
4
+ it('emits and receives events', () => {
5
+ const bus = new TypedEventBus();
6
+ const handler = vi.fn();
7
+ bus.on('test:fired', handler);
8
+ bus.emit('test:fired', { value: 42 });
9
+ expect(handler).toHaveBeenCalledWith({ value: 42 });
10
+ });
11
+ it('supports once listener', () => {
12
+ const bus = new TypedEventBus();
13
+ const handler = vi.fn();
14
+ bus.once('test:fired', handler);
15
+ bus.emit('test:fired', { value: 1 });
16
+ bus.emit('test:fired', { value: 2 });
17
+ expect(handler).toHaveBeenCalledTimes(1);
18
+ });
19
+ it('supports off to remove listener', () => {
20
+ const bus = new TypedEventBus();
21
+ const handler = vi.fn();
22
+ bus.on('test:fired', handler);
23
+ bus.off('test:fired', handler);
24
+ bus.emit('test:fired', { value: 1 });
25
+ expect(handler).not.toHaveBeenCalled();
26
+ });
27
+ it('handles multiple event types independently', () => {
28
+ const bus = new TypedEventBus();
29
+ const handler1 = vi.fn();
30
+ const handler2 = vi.fn();
31
+ bus.on('test:fired', handler1);
32
+ bus.on('test:other', handler2);
33
+ bus.emit('test:fired', { value: 10 });
34
+ expect(handler1).toHaveBeenCalledTimes(1);
35
+ expect(handler2).not.toHaveBeenCalled();
36
+ });
37
+ });
38
+ //# sourceMappingURL=events.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.test.js","sourceRoot":"","sources":["../../../src/utils/__tests__/events.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAO7C,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,GAAG,GAAG,IAAI,aAAa,EAAc,CAAC;QAC5C,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QACxB,GAAG,CAAC,EAAE,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAC9B,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QACtC,MAAM,CAAC,OAAO,CAAC,CAAC,oBAAoB,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,MAAM,GAAG,GAAG,IAAI,aAAa,EAAc,CAAC;QAC5C,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QACxB,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAChC,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;QACrC,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;QACrC,MAAM,CAAC,OAAO,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,GAAG,GAAG,IAAI,aAAa,EAAc,CAAC;QAC5C,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QACxB,GAAG,CAAC,EAAE,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAC9B,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAC/B,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;QACrC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,GAAG,GAAG,IAAI,aAAa,EAAc,CAAC;QAC5C,MAAM,QAAQ,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QACzB,GAAG,CAAC,EAAE,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;QAC/B,GAAG,CAAC,EAAE,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;QAC/B,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QACtC,MAAM,CAAC,QAAQ,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC1C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { sha256 } from '../hash.js';
3
+ describe('sha256', () => {
4
+ it('returns consistent hash for same input', () => {
5
+ expect(sha256('hello')).toBe(sha256('hello'));
6
+ });
7
+ it('returns different hashes for different inputs', () => {
8
+ expect(sha256('hello')).not.toBe(sha256('world'));
9
+ });
10
+ it('returns 64-char hex string', () => {
11
+ expect(sha256('test')).toMatch(/^[a-f0-9]{64}$/);
12
+ });
13
+ it('matches known SHA-256 value', () => {
14
+ expect(sha256('hello')).toBe('2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
15
+ });
16
+ it('handles empty string', () => {
17
+ expect(sha256('')).toBe('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
18
+ });
19
+ it('handles unicode', () => {
20
+ const hash = sha256('こんにちは');
21
+ expect(hash).toMatch(/^[a-f0-9]{64}$/);
22
+ expect(hash).toBe(sha256('こんにちは'));
23
+ });
24
+ });
25
+ //# sourceMappingURL=hash.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.test.js","sourceRoot":"","sources":["../../../src/utils/__tests__/hash.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEpC,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE;IACtB,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,kEAAkE,CAAC,CAAC;IACnG,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAC9B,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,kEAAkE,CAAC,CAAC;IAC9F,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,iBAAiB,EAAE,GAAG,EAAE;QACzB,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;QAC7B,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;QACvC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { createLogger, getLogger, resetLogger } from '../logger.js';
3
+ describe('logger', () => {
4
+ afterEach(() => {
5
+ resetLogger();
6
+ });
7
+ it('createLogger returns a logger instance', () => {
8
+ const logger = createLogger({ level: 'error' });
9
+ expect(logger).toBeDefined();
10
+ expect(typeof logger.info).toBe('function');
11
+ expect(typeof logger.error).toBe('function');
12
+ });
13
+ it('getLogger returns the same singleton', () => {
14
+ const a = createLogger({ level: 'error' });
15
+ const b = getLogger();
16
+ expect(a).toBe(b);
17
+ });
18
+ it('resetLogger allows creating a new logger', () => {
19
+ const a = createLogger({ level: 'error' });
20
+ resetLogger();
21
+ const b = createLogger({ level: 'warn' });
22
+ expect(a).not.toBe(b);
23
+ });
24
+ it('getLogger auto-creates if none exists', () => {
25
+ const logger = getLogger();
26
+ expect(logger).toBeDefined();
27
+ });
28
+ });
29
+ //# sourceMappingURL=logger.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.test.js","sourceRoot":"","sources":["../../../src/utils/__tests__/logger.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEpE,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE;IACtB,SAAS,CAAC,GAAG,EAAE;QACb,WAAW,EAAE,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QAChD,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;QAC7B,MAAM,CAAC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC5C,MAAM,CAAC,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,GAAG,YAAY,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QAC3C,MAAM,CAAC,GAAG,SAAS,EAAE,CAAC;QACtB,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,GAAG,YAAY,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QAC3C,WAAW,EAAE,CAAC;QACd,MAAM,CAAC,GAAG,YAAY,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAC1C,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;QAC3B,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;IAC/B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { normalizePath, getDataDir, getPipeName } from '../paths.js';
3
+ describe('normalizePath', () => {
4
+ it('converts backslashes to forward slashes', () => {
5
+ expect(normalizePath('C:\\Users\\test\\file.ts')).toBe('C:/Users/test/file.ts');
6
+ });
7
+ it('leaves forward slashes unchanged', () => {
8
+ expect(normalizePath('/home/user/file.ts')).toBe('/home/user/file.ts');
9
+ });
10
+ it('handles mixed slashes', () => {
11
+ expect(normalizePath('C:\\Users/test\\file.ts')).toBe('C:/Users/test/file.ts');
12
+ });
13
+ });
14
+ describe('getDataDir', () => {
15
+ afterEach(() => {
16
+ delete process.env['TEST_DATA_DIR'];
17
+ });
18
+ it('uses env var when set', () => {
19
+ process.env['TEST_DATA_DIR'] = '/custom/dir';
20
+ const result = getDataDir('TEST_DATA_DIR', '.brain');
21
+ // path.resolve normalises to platform-native form
22
+ expect(result).toContain('custom');
23
+ expect(result).not.toContain('.brain');
24
+ });
25
+ it('falls back to home dir', () => {
26
+ const result = getDataDir('NONEXISTENT_VAR_XYZ', '.test-brain');
27
+ expect(result).toContain('.test-brain');
28
+ });
29
+ });
30
+ describe('getPipeName', () => {
31
+ it('returns platform-specific path', () => {
32
+ const name = getPipeName('test-brain');
33
+ if (process.platform === 'win32') {
34
+ expect(name).toBe('\\\\.\\pipe\\test-brain');
35
+ }
36
+ else {
37
+ expect(name).toContain('test-brain.sock');
38
+ }
39
+ });
40
+ it('defaults to brain', () => {
41
+ const name = getPipeName();
42
+ if (process.platform === 'win32') {
43
+ expect(name).toBe('\\\\.\\pipe\\brain');
44
+ }
45
+ else {
46
+ expect(name).toContain('brain.sock');
47
+ }
48
+ });
49
+ });
50
+ //# sourceMappingURL=paths.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.test.js","sourceRoot":"","sources":["../../../src/utils/__tests__/paths.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAM,SAAS,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAErE,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,CAAC,aAAa,CAAC,0BAA0B,CAAC,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;IAClF,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;IACzE,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;QAC/B,MAAM,CAAC,aAAa,CAAC,yBAAyB,CAAC,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;IACjF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;QAC/B,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,GAAG,aAAa,CAAC;QAC7C,MAAM,MAAM,GAAG,UAAU,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAC;QACrD,kDAAkD;QAClD,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QACnC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,MAAM,MAAM,GAAG,UAAU,CAAC,qBAAqB,EAAE,aAAa,CAAC,CAAC;QAChE,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,MAAM,IAAI,GAAG,WAAW,CAAC,YAAY,CAAC,CAAC;QACvC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YACjC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;QAC/C,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,mBAAmB,EAAE,GAAG,EAAE;QAC3B,MAAM,IAAI,GAAG,WAAW,EAAE,CAAC;QAC3B,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YACjC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;QAC1C,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;QACvC,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmeck/brain-core",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Shared core infrastructure for the Brain ecosystem — IPC, MCP, CLI, DB connection, and utilities",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1 +0,0 @@
1
- github: timmeck
@@ -1,21 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [master, main]
6
- pull_request:
7
- branches: [master, main]
8
-
9
- jobs:
10
- build:
11
- runs-on: ubuntu-latest
12
- steps:
13
- - uses: actions/checkout@v4
14
- - uses: actions/setup-node@v4
15
- with:
16
- node-version: 20
17
- cache: npm
18
- - run: npm ci
19
- - run: npm run build
20
- - run: npm test
21
- if: ${{ hashFiles('vitest.config.*', 'src/**/*.test.ts', 'tests/**/*.test.ts') != '' }}
package/src/api/server.ts DELETED
@@ -1,210 +0,0 @@
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
- }