@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.
- package/.github/FUNDING.yml +1 -0
- package/.github/workflows/ci.yml +27 -0
- package/README.md +28 -13
- package/dist/cli/commands/dashboard.js +1 -1
- package/dist/config.js +3 -3
- package/dist/ipc/__tests__/protocol.test.d.ts +1 -0
- package/dist/ipc/__tests__/protocol.test.js +129 -0
- package/dist/ipc/__tests__/protocol.test.js.map +1 -0
- package/dist/utils/__tests__/hash.test.d.ts +1 -0
- package/dist/utils/__tests__/hash.test.js +30 -0
- package/dist/utils/__tests__/hash.test.js.map +1 -0
- package/dist/utils/__tests__/paths.test.d.ts +1 -0
- package/dist/utils/__tests__/paths.test.js +63 -0
- package/dist/utils/__tests__/paths.test.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/commands/dashboard.ts +1 -1
- package/src/config.ts +3 -3
- package/src/ipc/__tests__/protocol.test.ts +153 -0
- package/src/utils/__tests__/hash.test.ts +39 -0
- package/src/utils/__tests__/paths.test.ts +70 -0
|
@@ -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
|
+
[](https://www.npmjs.com/package/@timmeck/marketing-brain)
|
|
4
|
+
[](https://www.npmjs.com/package/@timmeck/marketing-brain)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](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
|
|
197
|
+
Marketing Brain includes a REST API on port 7781 (default).
|
|
193
198
|
|
|
194
199
|
```bash
|
|
195
200
|
# Health check
|
|
196
|
-
curl http://localhost:
|
|
201
|
+
curl http://localhost:7781/api/v1/health
|
|
197
202
|
|
|
198
203
|
# List all available methods
|
|
199
|
-
curl http://localhost:
|
|
204
|
+
curl http://localhost:7781/api/v1/methods
|
|
200
205
|
|
|
201
206
|
# Call any method via RPC
|
|
202
|
-
curl -X POST http://localhost:
|
|
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
|
|
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:
|
|
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
|
|
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` | `
|
|
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 |
|
|
324
|
-
| MCP HTTP |
|
|
325
|
-
| Dashboard |
|
|
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
|
-
##
|
|
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
|
-
|
|
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', '
|
|
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:
|
|
12
|
+
port: 7781,
|
|
13
13
|
enabled: true,
|
|
14
14
|
},
|
|
15
15
|
mcpHttp: {
|
|
16
|
-
port:
|
|
16
|
+
port: 7782,
|
|
17
17
|
enabled: true,
|
|
18
18
|
},
|
|
19
19
|
dashboard: {
|
|
20
|
-
port:
|
|
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":"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"}
|
|
@@ -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":"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"}
|
package/package.json
CHANGED
|
@@ -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', '
|
|
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:
|
|
14
|
+
port: 7781,
|
|
15
15
|
enabled: true,
|
|
16
16
|
},
|
|
17
17
|
mcpHttp: {
|
|
18
|
-
port:
|
|
18
|
+
port: 7782,
|
|
19
19
|
enabled: true,
|
|
20
20
|
},
|
|
21
21
|
dashboard: {
|
|
22
|
-
port:
|
|
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
|
+
});
|