@timmeck/brain-core 1.1.0 → 1.5.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 (112) hide show
  1. package/README.md +66 -14
  2. package/brain.log +6 -0
  3. package/dist/config/__tests__/loader.test.d.ts +1 -0
  4. package/dist/config/__tests__/loader.test.js +85 -0
  5. package/dist/config/__tests__/loader.test.js.map +1 -0
  6. package/dist/config/loader.d.ts +15 -0
  7. package/dist/config/loader.js +39 -0
  8. package/dist/config/loader.js.map +1 -0
  9. package/dist/cross-brain/__tests__/client.test.d.ts +1 -0
  10. package/dist/cross-brain/__tests__/client.test.js +31 -0
  11. package/dist/cross-brain/__tests__/client.test.js.map +1 -0
  12. package/dist/cross-brain/__tests__/notifications.test.d.ts +1 -0
  13. package/dist/cross-brain/__tests__/notifications.test.js +52 -0
  14. package/dist/cross-brain/__tests__/notifications.test.js.map +1 -0
  15. package/dist/cross-brain/notifications.d.ts +25 -0
  16. package/dist/cross-brain/notifications.js +51 -0
  17. package/dist/cross-brain/notifications.js.map +1 -0
  18. package/dist/index.d.ts +16 -0
  19. package/dist/index.js +14 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/ipc/__tests__/protocol.test.d.ts +1 -0
  22. package/dist/ipc/__tests__/protocol.test.js +69 -0
  23. package/dist/ipc/__tests__/protocol.test.js.map +1 -0
  24. package/dist/learning/__tests__/base-engine.test.d.ts +1 -0
  25. package/dist/learning/__tests__/base-engine.test.js +49 -0
  26. package/dist/learning/__tests__/base-engine.test.js.map +1 -0
  27. package/dist/learning/base-engine.d.ts +16 -0
  28. package/dist/learning/base-engine.js +30 -0
  29. package/dist/learning/base-engine.js.map +1 -0
  30. package/dist/math/__tests__/time-decay.test.d.ts +1 -0
  31. package/dist/math/__tests__/time-decay.test.js +37 -0
  32. package/dist/math/__tests__/time-decay.test.js.map +1 -0
  33. package/dist/math/__tests__/wilson-score.test.d.ts +1 -0
  34. package/dist/math/__tests__/wilson-score.test.js +43 -0
  35. package/dist/math/__tests__/wilson-score.test.js.map +1 -0
  36. package/dist/math/time-decay.d.ts +10 -0
  37. package/dist/math/time-decay.js +16 -0
  38. package/dist/math/time-decay.js.map +1 -0
  39. package/dist/math/wilson-score.d.ts +10 -0
  40. package/dist/math/wilson-score.js +21 -0
  41. package/dist/math/wilson-score.js.map +1 -0
  42. package/dist/research/__tests__/base-engine.test.d.ts +1 -0
  43. package/dist/research/__tests__/base-engine.test.js +56 -0
  44. package/dist/research/__tests__/base-engine.test.js.map +1 -0
  45. package/dist/research/base-engine.d.ts +20 -0
  46. package/dist/research/base-engine.js +46 -0
  47. package/dist/research/base-engine.js.map +1 -0
  48. package/dist/synapses/__tests__/activation.test.d.ts +1 -0
  49. package/dist/synapses/__tests__/activation.test.js +87 -0
  50. package/dist/synapses/__tests__/activation.test.js.map +1 -0
  51. package/dist/synapses/__tests__/decay.test.d.ts +1 -0
  52. package/dist/synapses/__tests__/decay.test.js +73 -0
  53. package/dist/synapses/__tests__/decay.test.js.map +1 -0
  54. package/dist/synapses/__tests__/hebbian.test.d.ts +1 -0
  55. package/dist/synapses/__tests__/hebbian.test.js +95 -0
  56. package/dist/synapses/__tests__/hebbian.test.js.map +1 -0
  57. package/dist/synapses/__tests__/pathfinder.test.d.ts +1 -0
  58. package/dist/synapses/__tests__/pathfinder.test.js +74 -0
  59. package/dist/synapses/__tests__/pathfinder.test.js.map +1 -0
  60. package/dist/synapses/__tests__/synapse-manager.test.d.ts +1 -0
  61. package/dist/synapses/__tests__/synapse-manager.test.js +94 -0
  62. package/dist/synapses/__tests__/synapse-manager.test.js.map +1 -0
  63. package/dist/synapses/activation.d.ts +6 -0
  64. package/dist/synapses/activation.js +54 -0
  65. package/dist/synapses/activation.js.map +1 -0
  66. package/dist/synapses/decay.d.ts +9 -0
  67. package/dist/synapses/decay.js +26 -0
  68. package/dist/synapses/decay.js.map +1 -0
  69. package/dist/synapses/hebbian.d.ts +12 -0
  70. package/dist/synapses/hebbian.js +45 -0
  71. package/dist/synapses/hebbian.js.map +1 -0
  72. package/dist/synapses/pathfinder.d.ts +6 -0
  73. package/dist/synapses/pathfinder.js +54 -0
  74. package/dist/synapses/pathfinder.js.map +1 -0
  75. package/dist/synapses/synapse-manager.d.ts +35 -0
  76. package/dist/synapses/synapse-manager.js +72 -0
  77. package/dist/synapses/synapse-manager.js.map +1 -0
  78. package/dist/synapses/types.d.ts +85 -0
  79. package/dist/synapses/types.js +7 -0
  80. package/dist/synapses/types.js.map +1 -0
  81. package/dist/utils/__tests__/events.test.d.ts +1 -0
  82. package/dist/utils/__tests__/events.test.js +38 -0
  83. package/dist/utils/__tests__/events.test.js.map +1 -0
  84. package/dist/utils/__tests__/hash.test.d.ts +1 -0
  85. package/dist/utils/__tests__/hash.test.js +25 -0
  86. package/dist/utils/__tests__/hash.test.js.map +1 -0
  87. package/dist/utils/__tests__/logger.test.d.ts +1 -0
  88. package/dist/utils/__tests__/logger.test.js +29 -0
  89. package/dist/utils/__tests__/logger.test.js.map +1 -0
  90. package/dist/utils/__tests__/paths.test.d.ts +1 -0
  91. package/dist/utils/__tests__/paths.test.js +50 -0
  92. package/dist/utils/__tests__/paths.test.js.map +1 -0
  93. package/eslint.config.js +14 -0
  94. package/package.json +17 -2
  95. package/.github/FUNDING.yml +0 -1
  96. package/.github/workflows/ci.yml +0 -21
  97. package/src/api/server.ts +0 -210
  98. package/src/cli/colors.ts +0 -105
  99. package/src/cross-brain/client.ts +0 -94
  100. package/src/db/connection.ts +0 -22
  101. package/src/index.ts +0 -35
  102. package/src/ipc/client.ts +0 -117
  103. package/src/ipc/protocol.ts +0 -35
  104. package/src/ipc/server.ts +0 -170
  105. package/src/mcp/http-server.ts +0 -148
  106. package/src/mcp/server.ts +0 -84
  107. package/src/types/ipc.types.ts +0 -8
  108. package/src/utils/events.ts +0 -30
  109. package/src/utils/hash.ts +0 -5
  110. package/src/utils/logger.ts +0 -67
  111. package/src/utils/paths.ts +0 -24
  112. package/tsconfig.json +0 -18
@@ -0,0 +1,72 @@
1
+ import { strengthen, weaken } from './hebbian.js';
2
+ import { decayAll } from './decay.js';
3
+ import { spreadingActivation } from './activation.js';
4
+ import { findPath } from './pathfinder.js';
5
+ import { getLogger } from '../utils/logger.js';
6
+ /**
7
+ * Base synapse manager shared across all brains.
8
+ * Each brain extends this with domain-specific context methods
9
+ * (e.g. getErrorContext, getPostContext, getTradeContext).
10
+ */
11
+ export class BaseSynapseManager {
12
+ repo;
13
+ config;
14
+ logger = getLogger();
15
+ constructor(repo, config) {
16
+ this.repo = repo;
17
+ this.config = config;
18
+ }
19
+ strengthen(source, target, synapseType, context) {
20
+ this.logger.debug(`Strengthening synapse ${source.type}:${source.id} --${synapseType}--> ${target.type}:${target.id}`);
21
+ return strengthen(this.repo, source, target, synapseType, this.hebbianConfig(), context);
22
+ }
23
+ weaken(synapseId, factor = 0.5) {
24
+ this.logger.debug(`Weakening synapse ${synapseId} by factor ${factor}`);
25
+ weaken(this.repo, synapseId, this.hebbianConfig(), factor);
26
+ }
27
+ find(source, target, synapseType) {
28
+ return this.repo.findBySourceTarget(source.type, source.id, target.type, target.id, synapseType);
29
+ }
30
+ activate(startNode, maxDepth, minWeight) {
31
+ return spreadingActivation(this.repo, startNode, maxDepth ?? this.config.maxDepth, minWeight ?? this.config.minActivationWeight);
32
+ }
33
+ findPath(from, to, maxDepth) {
34
+ return findPath(this.repo, from, to, maxDepth ?? this.config.maxDepth + 2);
35
+ }
36
+ runDecay() {
37
+ this.logger.info('Running synapse decay cycle');
38
+ const result = decayAll(this.repo, this.decayConfig());
39
+ this.logger.info(`Decay complete: ${result.decayed} decayed, ${result.pruned} pruned`);
40
+ return result;
41
+ }
42
+ getStrongestSynapses(limit = 20) {
43
+ return this.repo.topByWeight(limit);
44
+ }
45
+ getDiverseSynapses(perType = 25) {
46
+ return this.repo.topDiverse(perType);
47
+ }
48
+ getNetworkStats() {
49
+ return {
50
+ totalNodes: this.repo.countNodes(),
51
+ totalSynapses: this.repo.totalCount(),
52
+ avgWeight: this.repo.avgWeight(),
53
+ nodesByType: {},
54
+ synapsesByType: this.repo.countByType(),
55
+ };
56
+ }
57
+ hebbianConfig() {
58
+ return {
59
+ initialWeight: this.config.initialWeight,
60
+ learningRate: this.config.learningRate,
61
+ pruneThreshold: this.config.pruneThreshold,
62
+ };
63
+ }
64
+ decayConfig() {
65
+ return {
66
+ decayHalfLifeDays: this.config.decayHalfLifeDays,
67
+ decayAfterDays: this.config.decayAfterDays,
68
+ pruneThreshold: this.config.pruneThreshold,
69
+ };
70
+ }
71
+ }
72
+ //# sourceMappingURL=synapse-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"synapse-manager.js","sourceRoot":"","sources":["../../src/synapses/synapse-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Generic synapse network types for the Brain ecosystem.
3
+ * Each brain provides its own NodeType/SynapseType string unions,
4
+ * but the record shape and algorithms are shared.
5
+ */
6
+ export interface NodeRef {
7
+ type: string;
8
+ id: number;
9
+ }
10
+ export interface SynapseRecord {
11
+ id: number;
12
+ source_type: string;
13
+ source_id: number;
14
+ target_type: string;
15
+ target_id: number;
16
+ synapse_type: string;
17
+ weight: number;
18
+ activation_count: number;
19
+ last_activated_at: string;
20
+ metadata: string | null;
21
+ created_at: string;
22
+ updated_at: string;
23
+ }
24
+ export interface ActivationResult {
25
+ node: NodeRef;
26
+ activation: number;
27
+ depth: number;
28
+ path: string[];
29
+ }
30
+ export interface PathNode {
31
+ type: string;
32
+ id: number;
33
+ }
34
+ export interface SynapsePath {
35
+ from: PathNode;
36
+ to: PathNode;
37
+ synapses: SynapseRecord[];
38
+ totalWeight: number;
39
+ hops: number;
40
+ }
41
+ export interface NetworkStats {
42
+ totalNodes: number;
43
+ totalSynapses: number;
44
+ avgWeight: number;
45
+ nodesByType: Record<string, number>;
46
+ synapsesByType: Record<string, number>;
47
+ }
48
+ export interface HebbianConfig {
49
+ initialWeight: number;
50
+ learningRate: number;
51
+ pruneThreshold: number;
52
+ }
53
+ export interface DecayConfig {
54
+ decayHalfLifeDays: number;
55
+ decayAfterDays: number;
56
+ pruneThreshold: number;
57
+ }
58
+ /**
59
+ * Interface that synapse repositories must implement
60
+ * for the shared algorithms to work.
61
+ */
62
+ export interface SynapseRepoInterface {
63
+ findBySourceTarget(sourceType: string, sourceId: number, targetType: string, targetId: number, synapseType: string): SynapseRecord | undefined;
64
+ create(data: {
65
+ source_type: string;
66
+ source_id: number;
67
+ target_type: string;
68
+ target_id: number;
69
+ synapse_type: string;
70
+ weight: number;
71
+ metadata: string | null;
72
+ }): number;
73
+ getById(id: number): SynapseRecord | undefined;
74
+ update(id: number, data: Partial<SynapseRecord>): void;
75
+ delete(id: number): void;
76
+ getOutgoing(nodeType: string, nodeId: number): SynapseRecord[];
77
+ getIncoming(nodeType: string, nodeId: number): SynapseRecord[];
78
+ findInactiveSince(cutoffIso: string): SynapseRecord[];
79
+ topByWeight(limit: number): SynapseRecord[];
80
+ topDiverse(perType: number): SynapseRecord[];
81
+ countNodes(): number;
82
+ totalCount(): number;
83
+ avgWeight(): number;
84
+ countByType(): Record<string, number>;
85
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Generic synapse network types for the Brain ecosystem.
3
+ * Each brain provides its own NodeType/SynapseType string unions,
4
+ * but the record shape and algorithms are shared.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/synapses/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
@@ -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"}
@@ -0,0 +1,14 @@
1
+ import tseslint from 'typescript-eslint';
2
+
3
+ export default tseslint.config(
4
+ ...tseslint.configs.recommended,
5
+ {
6
+ ignores: ['dist/', 'node_modules/', '**/*.js', '**/*.d.ts'],
7
+ },
8
+ {
9
+ rules: {
10
+ '@typescript-eslint/no-explicit-any': 'warn',
11
+ '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
12
+ },
13
+ },
14
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmeck/brain-core",
3
- "version": "1.1.0",
3
+ "version": "1.5.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",
@@ -20,12 +20,24 @@
20
20
  "./mcp/http-server": "./dist/mcp/http-server.js",
21
21
  "./cli/colors": "./dist/cli/colors.js",
22
22
  "./api/server": "./dist/api/server.js",
23
+ "./math/wilson-score": "./dist/math/wilson-score.js",
24
+ "./math/time-decay": "./dist/math/time-decay.js",
25
+ "./config/loader": "./dist/config/loader.js",
26
+ "./synapses/types": "./dist/synapses/types.js",
27
+ "./synapses/hebbian": "./dist/synapses/hebbian.js",
28
+ "./synapses/decay": "./dist/synapses/decay.js",
29
+ "./synapses/activation": "./dist/synapses/activation.js",
30
+ "./synapses/pathfinder": "./dist/synapses/pathfinder.js",
31
+ "./synapses/synapse-manager": "./dist/synapses/synapse-manager.js",
23
32
  "./cross-brain": "./dist/cross-brain/client.js"
24
33
  },
25
34
  "scripts": {
26
35
  "build": "tsc",
27
36
  "dev": "tsx src/index.ts",
28
- "test": "vitest"
37
+ "test": "vitest",
38
+ "lint": "eslint src/",
39
+ "lint:fix": "eslint src/ --fix",
40
+ "test:coverage": "vitest --coverage"
29
41
  },
30
42
  "keywords": [
31
43
  "brain",
@@ -53,8 +65,11 @@
53
65
  "devDependencies": {
54
66
  "@types/better-sqlite3": "^7.6.12",
55
67
  "@types/node": "^22.0.0",
68
+ "@vitest/coverage-v8": "^3.2.4",
69
+ "eslint": "^9.39.3",
56
70
  "tsx": "^4.19.0",
57
71
  "typescript": "^5.7.0",
72
+ "typescript-eslint": "^8.56.1",
58
73
  "vitest": "^3.0.0"
59
74
  }
60
75
  }
@@ -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
- }