@timmeck/marketing-brain 0.2.0 → 0.2.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.
@@ -0,0 +1 @@
1
+ github: timmeck
@@ -0,0 +1,27 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - uses: actions/setup-node@v4
17
+ with:
18
+ node-version: 20
19
+ cache: npm
20
+
21
+ - run: npm ci
22
+
23
+ - run: npm run build
24
+
25
+ - name: Run tests
26
+ run: npm test
27
+ if: ${{ hashFiles('vitest.config.*', 'src/**/*.test.ts', 'tests/**/*.test.ts') != '' }}
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Marketing Brain
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@timmeck/marketing-brain)](https://www.npmjs.com/package/@timmeck/marketing-brain)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@timmeck/marketing-brain)](https://www.npmjs.com/package/@timmeck/marketing-brain)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+ [![GitHub stars](https://img.shields.io/github/stars/timmeck/marketing-brain)](https://github.com/timmeck/marketing-brain)
7
+
3
8
  **Self-Learning Marketing Intelligence System for Claude Code**
4
9
 
5
10
  Marketing Brain is an MCP server that gives Claude Code a persistent marketing memory. It tracks every post you publish, learns what works across platforms, and builds a Hebbian synapse network connecting posts, campaigns, strategies, templates, and insights. Over time, it learns your best-performing patterns and proactively suggests what to post, when, and where.
@@ -189,31 +194,31 @@ These tools are available to Claude Code when Marketing Brain is configured:
189
194
 
190
195
  ## REST API
191
196
 
192
- Marketing Brain includes a REST API on port 7780 (default).
197
+ Marketing Brain includes a REST API on port 7781 (default).
193
198
 
194
199
  ```bash
195
200
  # Health check
196
- curl http://localhost:7780/api/v1/health
201
+ curl http://localhost:7781/api/v1/health
197
202
 
198
203
  # List all available methods
199
- curl http://localhost:7780/api/v1/methods
204
+ curl http://localhost:7781/api/v1/methods
200
205
 
201
206
  # Call any method via RPC
202
- curl -X POST http://localhost:7780/api/v1/rpc \
207
+ curl -X POST http://localhost:7781/api/v1/rpc \
203
208
  -H "Content-Type: application/json" \
204
209
  -d '{"method": "analytics.summary", "params": {}}'
205
210
  ```
206
211
 
207
212
  ## Dashboard Server
208
213
 
209
- The daemon starts a live dashboard server on port 7782 (default).
214
+ The daemon starts a live dashboard server on port 7783 (default).
210
215
 
211
216
  ```bash
212
217
  # Open the dashboard in your browser
213
218
  marketing dashboard
214
219
 
215
220
  # Or visit directly while the daemon is running
216
- open http://localhost:7782
221
+ open http://localhost:7783
217
222
  ```
218
223
 
219
224
  Features:
@@ -235,7 +240,7 @@ Features:
235
240
  v v v
236
241
  +--------+---------+ +--------+---------+ +--------+---------+
237
242
  | MCP Server | | REST API | | Dashboard Server |
238
- | (stdio) | | (port 7780) | | (port 7782) |
243
+ | (stdio) | | (port 7781) | | (port 7783) |
239
244
  +--------+---------+ +--------+---------+ +--------+---------+
240
245
  | | |
241
246
  +----------+-------------+----------+-------------+
@@ -312,7 +317,7 @@ marketing config delete learning.intervalMs
312
317
  |---|---|---|
313
318
  | `MARKETING_BRAIN_DATA_DIR` | `~/.marketing-brain` | Data directory |
314
319
  | `MARKETING_BRAIN_LOG_LEVEL` | `info` | Log level |
315
- | `MARKETING_BRAIN_API_PORT` | `7780` | REST API port |
320
+ | `MARKETING_BRAIN_API_PORT` | `7781` | REST API port |
316
321
  | `MARKETING_BRAIN_API_KEY` | — | API authentication key |
317
322
  | `MARKETING_BRAIN_DB_PATH` | `~/.marketing-brain/marketing-brain.db` | Database path |
318
323
 
@@ -320,9 +325,9 @@ marketing config delete learning.intervalMs
320
325
 
321
326
  | Service | Default Port | Description |
322
327
  |---|---|---|
323
- | REST API | 7780 | JSON-RPC endpoint for integrations |
324
- | MCP HTTP | 7781 | MCP HTTP transport (optional) |
325
- | Dashboard | 7782 | Live dashboard with SSE |
328
+ | REST API | 7781 | JSON-RPC endpoint for integrations |
329
+ | MCP HTTP | 7782 | MCP HTTP transport (optional) |
330
+ | Dashboard | 7783 | Live dashboard with SSE |
326
331
 
327
332
  ## Tech Stack
328
333
 
@@ -333,9 +338,19 @@ marketing config delete learning.intervalMs
333
338
  - **Chalk** — Colored terminal output
334
339
  - **Winston** — Structured logging
335
340
 
336
- ## Related
341
+ ## Brain Ecosystem
342
+
343
+ Marketing Brain is part of the **Brain Ecosystem** — a family of standalone MCP servers that give Claude Code persistent, self-learning memory.
344
+
345
+ | Brain | Purpose | Ports |
346
+ |-------|---------|-------|
347
+ | [Brain](https://github.com/timmeck/brain) | Error memory & code intelligence | 7777 / 7778 |
348
+ | [Trading Brain](https://github.com/timmeck/trading-brain) | Adaptive trading intelligence | 7779 / 7780 |
349
+ | **Marketing Brain** | Content strategy & engagement | **7781** / 7782 / 7783 |
350
+ | [Brain Core](https://github.com/timmeck/brain-core) | Shared infrastructure (optional) | — |
351
+ | [Brain Hub](https://timmeck.github.io/brain-hub/) | Ecosystem landing page | — |
337
352
 
338
- - [Brain](https://github.com/timmeck/brain) Adaptive error memory & code intelligence (same architecture, applied to debugging)
353
+ Each brain is **fully standalone** — [Brain Core](https://www.npmjs.com/package/@timmeck/brain-core) is an optional shared dependency that eliminates code duplication across brains.
339
354
 
340
355
  ## License
341
356
 
@@ -15,7 +15,7 @@ export function dashboardCommand() {
15
15
  .option('-o, --output <path>', 'Output HTML file path')
16
16
  .option('--no-open', 'Generate HTML but do not open in browser')
17
17
  .option('-l, --live', 'Enable live mode (SSE updates from daemon)')
18
- .option('-p, --port <n>', 'Dashboard server port for live mode', '7781')
18
+ .option('-p, --port <n>', 'Dashboard server port for live mode', '7783')
19
19
  .action(async (opts) => {
20
20
  await withIpc(async (client) => {
21
21
  console.log(`${icons.chart} ${c.info('Generating dashboard...')}`);
package/dist/config.js CHANGED
@@ -9,15 +9,15 @@ const defaults = {
9
9
  timeout: 5000,
10
10
  },
11
11
  api: {
12
- port: 7780,
12
+ port: 7781,
13
13
  enabled: true,
14
14
  },
15
15
  mcpHttp: {
16
- port: 7781,
16
+ port: 7782,
17
17
  enabled: true,
18
18
  },
19
19
  dashboard: {
20
- port: 7782,
20
+ port: 7783,
21
21
  enabled: true,
22
22
  },
23
23
  learning: {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,129 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Buffer } from 'node:buffer';
3
+ import { encodeMessage, MessageDecoder } from '../protocol.js';
4
+ const makeMessage = (overrides = {}) => ({
5
+ id: '1',
6
+ type: 'request',
7
+ method: 'ping',
8
+ ...overrides,
9
+ });
10
+ describe('encodeMessage', () => {
11
+ it('returns a Buffer', () => {
12
+ const buf = encodeMessage(makeMessage());
13
+ expect(Buffer.isBuffer(buf)).toBe(true);
14
+ });
15
+ it('prefixes payload with 4-byte big-endian length', () => {
16
+ const msg = makeMessage();
17
+ const buf = encodeMessage(msg);
18
+ const payloadLength = buf.readUInt32BE(0);
19
+ expect(buf.length).toBe(4 + payloadLength);
20
+ });
21
+ it('contains the JSON payload after the length prefix', () => {
22
+ const msg = makeMessage({ id: 'test-42', method: 'hello' });
23
+ const buf = encodeMessage(msg);
24
+ const payloadLength = buf.readUInt32BE(0);
25
+ const json = buf.subarray(4, 4 + payloadLength).toString('utf8');
26
+ const parsed = JSON.parse(json);
27
+ expect(parsed.id).toBe('test-42');
28
+ expect(parsed.method).toBe('hello');
29
+ expect(parsed.type).toBe('request');
30
+ });
31
+ });
32
+ describe('MessageDecoder', () => {
33
+ it('decodes a single complete message', () => {
34
+ const decoder = new MessageDecoder();
35
+ const msg = makeMessage({ id: 'a' });
36
+ const encoded = encodeMessage(msg);
37
+ const messages = decoder.feed(encoded);
38
+ expect(messages).toHaveLength(1);
39
+ expect(messages[0].id).toBe('a');
40
+ });
41
+ it('decodes multiple messages fed at once', () => {
42
+ const decoder = new MessageDecoder();
43
+ const buf = Buffer.concat([
44
+ encodeMessage(makeMessage({ id: '1' })),
45
+ encodeMessage(makeMessage({ id: '2' })),
46
+ encodeMessage(makeMessage({ id: '3' })),
47
+ ]);
48
+ const messages = decoder.feed(buf);
49
+ expect(messages).toHaveLength(3);
50
+ expect(messages.map((m) => m.id)).toEqual(['1', '2', '3']);
51
+ });
52
+ it('handles partial messages across multiple feeds', () => {
53
+ const decoder = new MessageDecoder();
54
+ const encoded = encodeMessage(makeMessage({ id: 'split' }));
55
+ // Split in the middle of the payload
56
+ const mid = Math.floor(encoded.length / 2);
57
+ const part1 = encoded.subarray(0, mid);
58
+ const part2 = encoded.subarray(mid);
59
+ const first = decoder.feed(part1);
60
+ expect(first).toHaveLength(0);
61
+ const second = decoder.feed(part2);
62
+ expect(second).toHaveLength(1);
63
+ expect(second[0].id).toBe('split');
64
+ });
65
+ it('handles chunk split inside the 4-byte length header', () => {
66
+ const decoder = new MessageDecoder();
67
+ const encoded = encodeMessage(makeMessage({ id: 'header-split' }));
68
+ // Only send 2 bytes of the 4-byte header
69
+ const first = decoder.feed(encoded.subarray(0, 2));
70
+ expect(first).toHaveLength(0);
71
+ const second = decoder.feed(encoded.subarray(2));
72
+ expect(second).toHaveLength(1);
73
+ expect(second[0].id).toBe('header-split');
74
+ });
75
+ it('returns empty array when buffer has incomplete data', () => {
76
+ const decoder = new MessageDecoder();
77
+ // Just 3 bytes -- not even enough for the length header
78
+ const partial = Buffer.from([0x00, 0x00, 0x00]);
79
+ expect(decoder.feed(partial)).toHaveLength(0);
80
+ });
81
+ it('reset clears internal buffer', () => {
82
+ const decoder = new MessageDecoder();
83
+ const encoded = encodeMessage(makeMessage({ id: 'before-reset' }));
84
+ // Feed partial data, then reset
85
+ decoder.feed(encoded.subarray(0, 5));
86
+ decoder.reset();
87
+ // Now feed a fresh complete message
88
+ const messages = decoder.feed(encodeMessage(makeMessage({ id: 'after-reset' })));
89
+ expect(messages).toHaveLength(1);
90
+ expect(messages[0].id).toBe('after-reset');
91
+ });
92
+ it('preserves leftover bytes for next feed', () => {
93
+ const decoder = new MessageDecoder();
94
+ const msg1 = encodeMessage(makeMessage({ id: 'first' }));
95
+ const msg2 = encodeMessage(makeMessage({ id: 'second' }));
96
+ // Feed first message + partial second message
97
+ const combined = Buffer.concat([msg1, msg2.subarray(0, 6)]);
98
+ const first = decoder.feed(combined);
99
+ expect(first).toHaveLength(1);
100
+ expect(first[0].id).toBe('first');
101
+ // Feed rest of second message
102
+ const second = decoder.feed(msg2.subarray(6));
103
+ expect(second).toHaveLength(1);
104
+ expect(second[0].id).toBe('second');
105
+ });
106
+ it('round-trips all IpcMessage fields', () => {
107
+ const decoder = new MessageDecoder();
108
+ const msg = {
109
+ id: 'rt-1',
110
+ type: 'response',
111
+ result: { status: 'ok', data: [1, 2, 3] },
112
+ };
113
+ const messages = decoder.feed(encodeMessage(msg));
114
+ expect(messages).toHaveLength(1);
115
+ expect(messages[0]).toEqual(msg);
116
+ });
117
+ it('round-trips error messages', () => {
118
+ const decoder = new MessageDecoder();
119
+ const msg = {
120
+ id: 'err-1',
121
+ type: 'response',
122
+ error: { code: -1, message: 'something went wrong' },
123
+ };
124
+ const messages = decoder.feed(encodeMessage(msg));
125
+ expect(messages).toHaveLength(1);
126
+ expect(messages[0]).toEqual(msg);
127
+ });
128
+ });
129
+ //# 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":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAG/D,MAAM,WAAW,GAAG,CAAC,YAAiC,EAAE,EAAc,EAAE,CAAC,CAAC;IACxE,EAAE,EAAE,GAAG;IACP,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,MAAM;IACd,GAAG,SAAS;CACb,CAAC,CAAC;AAEH,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,kBAAkB,EAAE,GAAG,EAAE;QAC1B,MAAM,GAAG,GAAG,aAAa,CAAC,WAAW,EAAE,CAAC,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,GAAG,GAAG,WAAW,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;QAC/B,MAAM,aAAa,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,MAAM,GAAG,GAAG,WAAW,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;QAC5D,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;QAC/B,MAAM,aAAa,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC1C,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,aAAa,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACjE,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAChC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAClC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,OAAO,GAAG,IAAI,cAAc,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,WAAW,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;QACrC,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;QAEnC,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACvC,MAAM,CAAC,QAAQ,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,OAAO,GAAG,IAAI,cAAc,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC;YACxB,aAAa,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;YACvC,aAAa,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;YACvC,aAAa,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;SACxC,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACnC,MAAM,CAAC,QAAQ,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,OAAO,GAAG,IAAI,cAAc,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,aAAa,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;QAE5D,qCAAqC;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC3C,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QACvC,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAEpC,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAClC,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAE9B,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnC,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,OAAO,GAAG,IAAI,cAAc,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,aAAa,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC;QAEnE,yCAAyC;QACzC,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACnD,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAE9B,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QACjD,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,OAAO,GAAG,IAAI,cAAc,EAAE,CAAC;QACrC,wDAAwD;QACxD,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QAChD,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,MAAM,OAAO,GAAG,IAAI,cAAc,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,aAAa,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC;QAEnE,gCAAgC;QAChC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACrC,OAAO,CAAC,KAAK,EAAE,CAAC;QAEhB,oCAAoC;QACpC,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,CAAC;QACjF,MAAM,CAAC,QAAQ,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,OAAO,GAAG,IAAI,cAAc,EAAE,CAAC;QACrC,MAAM,IAAI,GAAG,aAAa,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;QACzD,MAAM,IAAI,GAAG,aAAa,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;QAE1D,8CAA8C;QAC9C,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5D,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrC,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAElC,8BAA8B;QAC9B,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9C,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,OAAO,GAAG,IAAI,cAAc,EAAE,CAAC;QACrC,MAAM,GAAG,GAAe;YACtB,EAAE,EAAE,MAAM;YACV,IAAI,EAAE,UAAU;YAChB,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE;SAC1C,CAAC;QACF,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;QAClD,MAAM,CAAC,QAAQ,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,OAAO,GAAG,IAAI,cAAc,EAAE,CAAC;QACrC,MAAM,GAAG,GAAe;YACtB,EAAE,EAAE,OAAO;YACX,IAAI,EAAE,UAAU;YAChB,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,sBAAsB,EAAE;SACrD,CAAC;QACF,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;QAClD,MAAM,CAAC,QAAQ,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { sha256 } from '../hash.js';
3
+ describe('sha256', () => {
4
+ it('returns a 64-character hex string', () => {
5
+ const result = sha256('hello');
6
+ expect(result).toHaveLength(64);
7
+ expect(result).toMatch(/^[0-9a-f]{64}$/);
8
+ });
9
+ it('produces correct hash for known input', () => {
10
+ // SHA-256 of "hello" is well-known
11
+ expect(sha256('hello')).toBe('2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
12
+ });
13
+ it('produces different hashes for different inputs', () => {
14
+ expect(sha256('foo')).not.toBe(sha256('bar'));
15
+ });
16
+ it('produces the same hash for the same input', () => {
17
+ expect(sha256('deterministic')).toBe(sha256('deterministic'));
18
+ });
19
+ it('handles empty string', () => {
20
+ const result = sha256('');
21
+ expect(result).toHaveLength(64);
22
+ expect(result).toBe('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
23
+ });
24
+ it('handles unicode input', () => {
25
+ const result = sha256('Hallo Welt! 🚀');
26
+ expect(result).toHaveLength(64);
27
+ expect(result).toMatch(/^[0-9a-f]{64}$/);
28
+ });
29
+ });
30
+ //# 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,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QAChC,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,mCAAmC;QACnC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAC1B,kEAAkE,CACnE,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAC9B,MAAM,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC;QAC1B,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QAChC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CACjB,kEAAkE,CACnE,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;QAC/B,MAAM,MAAM,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;QACxC,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QAChC,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { normalizePath, getDataDir, getPipeName } from '../paths.js';
5
+ describe('normalizePath', () => {
6
+ it('converts backslashes to forward slashes', () => {
7
+ expect(normalizePath('C:\\Users\\test\\file.txt')).toBe('C:/Users/test/file.txt');
8
+ });
9
+ it('leaves forward slashes unchanged', () => {
10
+ expect(normalizePath('/home/user/file.txt')).toBe('/home/user/file.txt');
11
+ });
12
+ it('handles mixed separators', () => {
13
+ expect(normalizePath('src\\utils/hash.ts')).toBe('src/utils/hash.ts');
14
+ });
15
+ it('handles empty string', () => {
16
+ expect(normalizePath('')).toBe('');
17
+ });
18
+ it('handles path with no separators', () => {
19
+ expect(normalizePath('file.txt')).toBe('file.txt');
20
+ });
21
+ });
22
+ describe('getDataDir', () => {
23
+ const originalEnv = process.env['MARKETING_BRAIN_DATA_DIR'];
24
+ afterEach(() => {
25
+ if (originalEnv !== undefined) {
26
+ process.env['MARKETING_BRAIN_DATA_DIR'] = originalEnv;
27
+ }
28
+ else {
29
+ delete process.env['MARKETING_BRAIN_DATA_DIR'];
30
+ }
31
+ });
32
+ it('returns env-based directory when MARKETING_BRAIN_DATA_DIR is set', () => {
33
+ process.env['MARKETING_BRAIN_DATA_DIR'] = '/custom/data';
34
+ const result = getDataDir();
35
+ expect(result).toBe(path.resolve('/custom/data'));
36
+ });
37
+ it('returns homedir-based directory when env is not set', () => {
38
+ delete process.env['MARKETING_BRAIN_DATA_DIR'];
39
+ const result = getDataDir();
40
+ expect(result).toBe(path.join(os.homedir(), '.marketing-brain'));
41
+ });
42
+ });
43
+ describe('getPipeName', () => {
44
+ it('uses default name when no argument is given', () => {
45
+ const result = getPipeName();
46
+ if (process.platform === 'win32') {
47
+ expect(result).toBe('\\\\.\\pipe\\marketing-brain');
48
+ }
49
+ else {
50
+ expect(result).toBe(path.join(os.tmpdir(), 'marketing-brain.sock'));
51
+ }
52
+ });
53
+ it('uses custom name when provided', () => {
54
+ const result = getPipeName('my-app');
55
+ if (process.platform === 'win32') {
56
+ expect(result).toBe('\\\\.\\pipe\\my-app');
57
+ }
58
+ else {
59
+ expect(result).toBe(path.join(os.tmpdir(), 'my-app.sock'));
60
+ }
61
+ });
62
+ });
63
+ //# 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,EAAkB,SAAS,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,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,2BAA2B,CAAC,CAAC,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;IACpF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,CAAC,aAAa,CAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IAC3E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0BAA0B,EAAE,GAAG,EAAE;QAClC,MAAM,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAC9B,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC;IAE5D,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC9B,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,GAAG,WAAW,CAAC;QACxD,CAAC;aAAM,CAAC;YACN,OAAO,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC;QACjD,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC1E,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,GAAG,cAAc,CAAC;QACzD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,OAAO,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC;QAC/C,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,MAAM,GAAG,WAAW,EAAE,CAAC;QAC7B,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YACjC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;QACtD,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,sBAAsB,CAAC,CAAC,CAAC;QACtE,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,MAAM,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YACjC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;QAC7C,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,aAAa,CAAC,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmeck/marketing-brain",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Self-learning marketing intelligence system with Hebbian synapse network",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -18,7 +18,7 @@ export function dashboardCommand(): Command {
18
18
  .option('-o, --output <path>', 'Output HTML file path')
19
19
  .option('--no-open', 'Generate HTML but do not open in browser')
20
20
  .option('-l, --live', 'Enable live mode (SSE updates from daemon)')
21
- .option('-p, --port <n>', 'Dashboard server port for live mode', '7781')
21
+ .option('-p, --port <n>', 'Dashboard server port for live mode', '7783')
22
22
  .action(async (opts) => {
23
23
  await withIpc(async (client) => {
24
24
  console.log(`${icons.chart} ${c.info('Generating dashboard...')}`);
package/src/config.ts CHANGED
@@ -11,15 +11,15 @@ const defaults: MarketingBrainConfig = {
11
11
  timeout: 5000,
12
12
  },
13
13
  api: {
14
- port: 7780,
14
+ port: 7781,
15
15
  enabled: true,
16
16
  },
17
17
  mcpHttp: {
18
- port: 7781,
18
+ port: 7782,
19
19
  enabled: true,
20
20
  },
21
21
  dashboard: {
22
- port: 7782,
22
+ port: 7783,
23
23
  enabled: true,
24
24
  },
25
25
  learning: {
@@ -0,0 +1,153 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Buffer } from 'node:buffer';
3
+ import { encodeMessage, MessageDecoder } from '../protocol.js';
4
+ import type { IpcMessage } from '../../types/ipc.types.js';
5
+
6
+ const makeMessage = (overrides: Partial<IpcMessage> = {}): IpcMessage => ({
7
+ id: '1',
8
+ type: 'request',
9
+ method: 'ping',
10
+ ...overrides,
11
+ });
12
+
13
+ describe('encodeMessage', () => {
14
+ it('returns a Buffer', () => {
15
+ const buf = encodeMessage(makeMessage());
16
+ expect(Buffer.isBuffer(buf)).toBe(true);
17
+ });
18
+
19
+ it('prefixes payload with 4-byte big-endian length', () => {
20
+ const msg = makeMessage();
21
+ const buf = encodeMessage(msg);
22
+ const payloadLength = buf.readUInt32BE(0);
23
+ expect(buf.length).toBe(4 + payloadLength);
24
+ });
25
+
26
+ it('contains the JSON payload after the length prefix', () => {
27
+ const msg = makeMessage({ id: 'test-42', method: 'hello' });
28
+ const buf = encodeMessage(msg);
29
+ const payloadLength = buf.readUInt32BE(0);
30
+ const json = buf.subarray(4, 4 + payloadLength).toString('utf8');
31
+ const parsed = JSON.parse(json);
32
+ expect(parsed.id).toBe('test-42');
33
+ expect(parsed.method).toBe('hello');
34
+ expect(parsed.type).toBe('request');
35
+ });
36
+ });
37
+
38
+ describe('MessageDecoder', () => {
39
+ it('decodes a single complete message', () => {
40
+ const decoder = new MessageDecoder();
41
+ const msg = makeMessage({ id: 'a' });
42
+ const encoded = encodeMessage(msg);
43
+
44
+ const messages = decoder.feed(encoded);
45
+ expect(messages).toHaveLength(1);
46
+ expect(messages[0].id).toBe('a');
47
+ });
48
+
49
+ it('decodes multiple messages fed at once', () => {
50
+ const decoder = new MessageDecoder();
51
+ const buf = Buffer.concat([
52
+ encodeMessage(makeMessage({ id: '1' })),
53
+ encodeMessage(makeMessage({ id: '2' })),
54
+ encodeMessage(makeMessage({ id: '3' })),
55
+ ]);
56
+
57
+ const messages = decoder.feed(buf);
58
+ expect(messages).toHaveLength(3);
59
+ expect(messages.map((m) => m.id)).toEqual(['1', '2', '3']);
60
+ });
61
+
62
+ it('handles partial messages across multiple feeds', () => {
63
+ const decoder = new MessageDecoder();
64
+ const encoded = encodeMessage(makeMessage({ id: 'split' }));
65
+
66
+ // Split in the middle of the payload
67
+ const mid = Math.floor(encoded.length / 2);
68
+ const part1 = encoded.subarray(0, mid);
69
+ const part2 = encoded.subarray(mid);
70
+
71
+ const first = decoder.feed(part1);
72
+ expect(first).toHaveLength(0);
73
+
74
+ const second = decoder.feed(part2);
75
+ expect(second).toHaveLength(1);
76
+ expect(second[0].id).toBe('split');
77
+ });
78
+
79
+ it('handles chunk split inside the 4-byte length header', () => {
80
+ const decoder = new MessageDecoder();
81
+ const encoded = encodeMessage(makeMessage({ id: 'header-split' }));
82
+
83
+ // Only send 2 bytes of the 4-byte header
84
+ const first = decoder.feed(encoded.subarray(0, 2));
85
+ expect(first).toHaveLength(0);
86
+
87
+ const second = decoder.feed(encoded.subarray(2));
88
+ expect(second).toHaveLength(1);
89
+ expect(second[0].id).toBe('header-split');
90
+ });
91
+
92
+ it('returns empty array when buffer has incomplete data', () => {
93
+ const decoder = new MessageDecoder();
94
+ // Just 3 bytes -- not even enough for the length header
95
+ const partial = Buffer.from([0x00, 0x00, 0x00]);
96
+ expect(decoder.feed(partial)).toHaveLength(0);
97
+ });
98
+
99
+ it('reset clears internal buffer', () => {
100
+ const decoder = new MessageDecoder();
101
+ const encoded = encodeMessage(makeMessage({ id: 'before-reset' }));
102
+
103
+ // Feed partial data, then reset
104
+ decoder.feed(encoded.subarray(0, 5));
105
+ decoder.reset();
106
+
107
+ // Now feed a fresh complete message
108
+ const messages = decoder.feed(encodeMessage(makeMessage({ id: 'after-reset' })));
109
+ expect(messages).toHaveLength(1);
110
+ expect(messages[0].id).toBe('after-reset');
111
+ });
112
+
113
+ it('preserves leftover bytes for next feed', () => {
114
+ const decoder = new MessageDecoder();
115
+ const msg1 = encodeMessage(makeMessage({ id: 'first' }));
116
+ const msg2 = encodeMessage(makeMessage({ id: 'second' }));
117
+
118
+ // Feed first message + partial second message
119
+ const combined = Buffer.concat([msg1, msg2.subarray(0, 6)]);
120
+ const first = decoder.feed(combined);
121
+ expect(first).toHaveLength(1);
122
+ expect(first[0].id).toBe('first');
123
+
124
+ // Feed rest of second message
125
+ const second = decoder.feed(msg2.subarray(6));
126
+ expect(second).toHaveLength(1);
127
+ expect(second[0].id).toBe('second');
128
+ });
129
+
130
+ it('round-trips all IpcMessage fields', () => {
131
+ const decoder = new MessageDecoder();
132
+ const msg: IpcMessage = {
133
+ id: 'rt-1',
134
+ type: 'response',
135
+ result: { status: 'ok', data: [1, 2, 3] },
136
+ };
137
+ const messages = decoder.feed(encodeMessage(msg));
138
+ expect(messages).toHaveLength(1);
139
+ expect(messages[0]).toEqual(msg);
140
+ });
141
+
142
+ it('round-trips error messages', () => {
143
+ const decoder = new MessageDecoder();
144
+ const msg: IpcMessage = {
145
+ id: 'err-1',
146
+ type: 'response',
147
+ error: { code: -1, message: 'something went wrong' },
148
+ };
149
+ const messages = decoder.feed(encodeMessage(msg));
150
+ expect(messages).toHaveLength(1);
151
+ expect(messages[0]).toEqual(msg);
152
+ });
153
+ });
@@ -0,0 +1,39 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { sha256 } from '../hash.js';
3
+
4
+ describe('sha256', () => {
5
+ it('returns a 64-character hex string', () => {
6
+ const result = sha256('hello');
7
+ expect(result).toHaveLength(64);
8
+ expect(result).toMatch(/^[0-9a-f]{64}$/);
9
+ });
10
+
11
+ it('produces correct hash for known input', () => {
12
+ // SHA-256 of "hello" is well-known
13
+ expect(sha256('hello')).toBe(
14
+ '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
15
+ );
16
+ });
17
+
18
+ it('produces different hashes for different inputs', () => {
19
+ expect(sha256('foo')).not.toBe(sha256('bar'));
20
+ });
21
+
22
+ it('produces the same hash for the same input', () => {
23
+ expect(sha256('deterministic')).toBe(sha256('deterministic'));
24
+ });
25
+
26
+ it('handles empty string', () => {
27
+ const result = sha256('');
28
+ expect(result).toHaveLength(64);
29
+ expect(result).toBe(
30
+ 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
31
+ );
32
+ });
33
+
34
+ it('handles unicode input', () => {
35
+ const result = sha256('Hallo Welt! 🚀');
36
+ expect(result).toHaveLength(64);
37
+ expect(result).toMatch(/^[0-9a-f]{64}$/);
38
+ });
39
+ });
@@ -0,0 +1,70 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { normalizePath, getDataDir, getPipeName } from '../paths.js';
5
+
6
+ describe('normalizePath', () => {
7
+ it('converts backslashes to forward slashes', () => {
8
+ expect(normalizePath('C:\\Users\\test\\file.txt')).toBe('C:/Users/test/file.txt');
9
+ });
10
+
11
+ it('leaves forward slashes unchanged', () => {
12
+ expect(normalizePath('/home/user/file.txt')).toBe('/home/user/file.txt');
13
+ });
14
+
15
+ it('handles mixed separators', () => {
16
+ expect(normalizePath('src\\utils/hash.ts')).toBe('src/utils/hash.ts');
17
+ });
18
+
19
+ it('handles empty string', () => {
20
+ expect(normalizePath('')).toBe('');
21
+ });
22
+
23
+ it('handles path with no separators', () => {
24
+ expect(normalizePath('file.txt')).toBe('file.txt');
25
+ });
26
+ });
27
+
28
+ describe('getDataDir', () => {
29
+ const originalEnv = process.env['MARKETING_BRAIN_DATA_DIR'];
30
+
31
+ afterEach(() => {
32
+ if (originalEnv !== undefined) {
33
+ process.env['MARKETING_BRAIN_DATA_DIR'] = originalEnv;
34
+ } else {
35
+ delete process.env['MARKETING_BRAIN_DATA_DIR'];
36
+ }
37
+ });
38
+
39
+ it('returns env-based directory when MARKETING_BRAIN_DATA_DIR is set', () => {
40
+ process.env['MARKETING_BRAIN_DATA_DIR'] = '/custom/data';
41
+ const result = getDataDir();
42
+ expect(result).toBe(path.resolve('/custom/data'));
43
+ });
44
+
45
+ it('returns homedir-based directory when env is not set', () => {
46
+ delete process.env['MARKETING_BRAIN_DATA_DIR'];
47
+ const result = getDataDir();
48
+ expect(result).toBe(path.join(os.homedir(), '.marketing-brain'));
49
+ });
50
+ });
51
+
52
+ describe('getPipeName', () => {
53
+ it('uses default name when no argument is given', () => {
54
+ const result = getPipeName();
55
+ if (process.platform === 'win32') {
56
+ expect(result).toBe('\\\\.\\pipe\\marketing-brain');
57
+ } else {
58
+ expect(result).toBe(path.join(os.tmpdir(), 'marketing-brain.sock'));
59
+ }
60
+ });
61
+
62
+ it('uses custom name when provided', () => {
63
+ const result = getPipeName('my-app');
64
+ if (process.platform === 'win32') {
65
+ expect(result).toBe('\\\\.\\pipe\\my-app');
66
+ } else {
67
+ expect(result).toBe(path.join(os.tmpdir(), 'my-app.sock'));
68
+ }
69
+ });
70
+ });