milens 0.6.1 → 0.6.3

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 (105) hide show
  1. package/README.md +157 -14
  2. package/dist/analyzer/config.d.ts.map +1 -1
  3. package/dist/analyzer/config.js +33 -1
  4. package/dist/analyzer/config.js.map +1 -1
  5. package/dist/analyzer/engine.d.ts +1 -0
  6. package/dist/analyzer/engine.d.ts.map +1 -1
  7. package/dist/analyzer/engine.js +27 -8
  8. package/dist/analyzer/engine.js.map +1 -1
  9. package/dist/analyzer/review.d.ts +23 -0
  10. package/dist/analyzer/review.d.ts.map +1 -0
  11. package/dist/analyzer/review.js +143 -0
  12. package/dist/analyzer/review.js.map +1 -0
  13. package/dist/analyzer/testplan.d.ts +59 -0
  14. package/dist/analyzer/testplan.d.ts.map +1 -0
  15. package/dist/analyzer/testplan.js +218 -0
  16. package/dist/analyzer/testplan.js.map +1 -0
  17. package/dist/cli.js +2 -0
  18. package/dist/cli.js.map +1 -1
  19. package/dist/gateway/analyzer.d.ts +6 -0
  20. package/dist/gateway/analyzer.d.ts.map +1 -0
  21. package/dist/gateway/analyzer.js +218 -0
  22. package/dist/gateway/analyzer.js.map +1 -0
  23. package/dist/gateway/cache.d.ts +35 -0
  24. package/dist/gateway/cache.d.ts.map +1 -0
  25. package/dist/gateway/cache.js +175 -0
  26. package/dist/gateway/cache.js.map +1 -0
  27. package/dist/gateway/config.d.ts +10 -0
  28. package/dist/gateway/config.d.ts.map +1 -0
  29. package/dist/gateway/config.js +167 -0
  30. package/dist/gateway/config.js.map +1 -0
  31. package/dist/gateway/context-memory.d.ts +68 -0
  32. package/dist/gateway/context-memory.d.ts.map +1 -0
  33. package/dist/gateway/context-memory.js +157 -0
  34. package/dist/gateway/context-memory.js.map +1 -0
  35. package/dist/gateway/observability.d.ts +83 -0
  36. package/dist/gateway/observability.d.ts.map +1 -0
  37. package/dist/gateway/observability.js +152 -0
  38. package/dist/gateway/observability.js.map +1 -0
  39. package/dist/gateway/privacy.d.ts +27 -0
  40. package/dist/gateway/privacy.d.ts.map +1 -0
  41. package/dist/gateway/privacy.js +139 -0
  42. package/dist/gateway/privacy.js.map +1 -0
  43. package/dist/gateway/providers.d.ts +66 -0
  44. package/dist/gateway/providers.d.ts.map +1 -0
  45. package/dist/gateway/providers.js +377 -0
  46. package/dist/gateway/providers.js.map +1 -0
  47. package/dist/gateway/router.d.ts +18 -0
  48. package/dist/gateway/router.d.ts.map +1 -0
  49. package/dist/gateway/router.js +102 -0
  50. package/dist/gateway/router.js.map +1 -0
  51. package/dist/gateway/server.d.ts +20 -0
  52. package/dist/gateway/server.d.ts.map +1 -0
  53. package/dist/gateway/server.js +387 -0
  54. package/dist/gateway/server.js.map +1 -0
  55. package/dist/gateway/translator.d.ts +19 -0
  56. package/dist/gateway/translator.d.ts.map +1 -0
  57. package/dist/gateway/translator.js +340 -0
  58. package/dist/gateway/translator.js.map +1 -0
  59. package/dist/gateway/types.d.ts +215 -0
  60. package/dist/gateway/types.d.ts.map +1 -0
  61. package/dist/gateway/types.js +3 -0
  62. package/dist/gateway/types.js.map +1 -0
  63. package/dist/parser/extract.d.ts +1 -0
  64. package/dist/parser/extract.d.ts.map +1 -1
  65. package/dist/parser/extract.js +8 -0
  66. package/dist/parser/extract.js.map +1 -1
  67. package/dist/parser/lang-go.d.ts.map +1 -1
  68. package/dist/parser/lang-go.js +41 -5
  69. package/dist/parser/lang-go.js.map +1 -1
  70. package/dist/parser/lang-java.d.ts.map +1 -1
  71. package/dist/parser/lang-java.js +1 -0
  72. package/dist/parser/lang-java.js.map +1 -1
  73. package/dist/parser/lang-js.d.ts.map +1 -1
  74. package/dist/parser/lang-js.js +23 -1
  75. package/dist/parser/lang-js.js.map +1 -1
  76. package/dist/parser/lang-py.d.ts.map +1 -1
  77. package/dist/parser/lang-py.js +22 -0
  78. package/dist/parser/lang-py.js.map +1 -1
  79. package/dist/parser/lang-ruby.d.ts.map +1 -1
  80. package/dist/parser/lang-ruby.js +1 -0
  81. package/dist/parser/lang-ruby.js.map +1 -1
  82. package/dist/parser/lang-ts.d.ts.map +1 -1
  83. package/dist/parser/lang-ts.js +62 -1
  84. package/dist/parser/lang-ts.js.map +1 -1
  85. package/dist/server/mcp.d.ts.map +1 -1
  86. package/dist/server/mcp.js +615 -106
  87. package/dist/server/mcp.js.map +1 -1
  88. package/dist/skills.js +32 -0
  89. package/dist/skills.js.map +1 -1
  90. package/dist/store/db.d.ts +44 -0
  91. package/dist/store/db.d.ts.map +1 -1
  92. package/dist/store/db.js +145 -25
  93. package/dist/store/db.js.map +1 -1
  94. package/dist/store/gateway-schema.sql +53 -0
  95. package/dist/store/schema.sql +33 -0
  96. package/dist/store/vectors.d.ts +65 -0
  97. package/dist/store/vectors.d.ts.map +1 -0
  98. package/dist/store/vectors.js +212 -0
  99. package/dist/store/vectors.js.map +1 -0
  100. package/dist/utils.d.ts +3 -0
  101. package/dist/utils.d.ts.map +1 -0
  102. package/dist/utils.js +9 -0
  103. package/dist/utils.js.map +1 -0
  104. package/docs/diagram2.svg +1 -1
  105. package/package.json +2 -1
@@ -0,0 +1,68 @@
1
+ /** A shared context entry */
2
+ interface ContextEntry {
3
+ key: string;
4
+ value: string;
5
+ source: string;
6
+ addedAt: number;
7
+ hash: string;
8
+ tags: string[];
9
+ }
10
+ /** Change notification */
11
+ interface ContextChange {
12
+ type: 'add' | 'update' | 'remove';
13
+ key: string;
14
+ value?: string;
15
+ source: string;
16
+ timestamp: number;
17
+ }
18
+ type ChangeListener = (change: ContextChange) => void;
19
+ export declare class ContextMemory {
20
+ private agents;
21
+ private entries;
22
+ private listeners;
23
+ private changeLog;
24
+ private maxEntries;
25
+ private maxChangeLog;
26
+ constructor(opts?: {
27
+ maxEntries?: number;
28
+ maxChangeLog?: number;
29
+ });
30
+ /** Register a new agent session */
31
+ registerAgent(name: string, role: string): string;
32
+ /** Remove an agent session */
33
+ removeAgent(agentId: string): void;
34
+ /** Add or update a context entry (dedup by content hash) */
35
+ set(agentId: string, key: string, value: string, tags?: string[]): boolean;
36
+ /** Get a context entry */
37
+ get(key: string): string | undefined;
38
+ /** Remove a context entry */
39
+ remove(agentId: string, key: string): boolean;
40
+ /** Get all context entries matching tags */
41
+ getByTags(tags: string[]): ContextEntry[];
42
+ /** Get all context contributed by a specific agent */
43
+ getByAgent(agentId: string): ContextEntry[];
44
+ /** Subscribe to context changes */
45
+ subscribe(agentId: string, listener: ChangeListener): void;
46
+ /** Get changes since a timestamp */
47
+ getChangesSince(since: number): ContextChange[];
48
+ /** Build a summary string of shared context (for injection into prompts) */
49
+ buildContextSummary(maxTokens?: number): string;
50
+ /** List active agents */
51
+ listAgents(): {
52
+ id: string;
53
+ name: string;
54
+ role: string;
55
+ contextCount: number;
56
+ lastActive: number;
57
+ }[];
58
+ /** Get stats */
59
+ getStats(): {
60
+ agents: number;
61
+ entries: number;
62
+ changeLogSize: number;
63
+ };
64
+ private recordChange;
65
+ private evictOldest;
66
+ }
67
+ export {};
68
+ //# sourceMappingURL=context-memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-memory.d.ts","sourceRoot":"","sources":["../../src/gateway/context-memory.ts"],"names":[],"mappings":"AAcA,6BAA6B;AAC7B,UAAU,YAAY;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,0BAA0B;AAC1B,UAAU,aAAa;IACrB,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,KAAK,cAAc,GAAG,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;AAEtD,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,OAAO,CAAmC;IAClD,OAAO,CAAC,SAAS,CAAqC;IACtD,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,YAAY,CAAS;gBAEjB,IAAI,GAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAO;IAKrE,mCAAmC;IACnC,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;IAWjD,8BAA8B;IAC9B,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAKlC,4DAA4D;IAC5D,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,EAAO,GAAG,OAAO;IAgC9E,0BAA0B;IAC1B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIpC,6BAA6B;IAC7B,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO;IAY7C,4CAA4C;IAC5C,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,YAAY,EAAE;IAOzC,sDAAsD;IACtD,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,EAAE;IAQ3C,mCAAmC;IACnC,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,GAAG,IAAI;IAI1D,oCAAoC;IACpC,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE;IAI/C,4EAA4E;IAC5E,mBAAmB,CAAC,SAAS,SAAO,GAAG,MAAM;IAoB7C,yBAAyB;IACzB,UAAU,IAAI;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,EAAE;IAUpG,gBAAgB;IAChB,QAAQ;;;;;IAQR,OAAO,CAAC,YAAY;IAcpB,OAAO,CAAC,WAAW;CASpB"}
@@ -0,0 +1,157 @@
1
+ // ── Context Memory — shared context across multi-agent sessions ──
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ export class ContextMemory {
4
+ agents = new Map();
5
+ entries = new Map();
6
+ listeners = new Map();
7
+ changeLog = [];
8
+ maxEntries;
9
+ maxChangeLog;
10
+ constructor(opts = {}) {
11
+ this.maxEntries = opts.maxEntries ?? 500;
12
+ this.maxChangeLog = opts.maxChangeLog ?? 1000;
13
+ }
14
+ /** Register a new agent session */
15
+ registerAgent(name, role) {
16
+ const id = randomUUID().slice(0, 8);
17
+ this.agents.set(id, {
18
+ id, name, role,
19
+ joinedAt: Date.now(),
20
+ lastActive: Date.now(),
21
+ contextKeys: new Set(),
22
+ });
23
+ return id;
24
+ }
25
+ /** Remove an agent session */
26
+ removeAgent(agentId) {
27
+ this.agents.delete(agentId);
28
+ this.listeners.delete(agentId);
29
+ }
30
+ /** Add or update a context entry (dedup by content hash) */
31
+ set(agentId, key, value, tags = []) {
32
+ const agent = this.agents.get(agentId);
33
+ if (!agent)
34
+ return false;
35
+ const hash = createHash('sha256').update(value).digest('hex').slice(0, 12);
36
+ // Check for duplicate content under a different key
37
+ const existing = this.entries.get(key);
38
+ if (existing && existing.hash === hash) {
39
+ return false; // No change
40
+ }
41
+ // Enforce capacity
42
+ if (!existing && this.entries.size >= this.maxEntries) {
43
+ this.evictOldest();
44
+ }
45
+ const changeType = existing ? 'update' : 'add';
46
+ this.entries.set(key, {
47
+ key, value,
48
+ source: agentId,
49
+ addedAt: Date.now(),
50
+ hash,
51
+ tags,
52
+ });
53
+ agent.contextKeys.add(key);
54
+ agent.lastActive = Date.now();
55
+ this.recordChange({ type: changeType, key, value, source: agentId, timestamp: Date.now() });
56
+ return true;
57
+ }
58
+ /** Get a context entry */
59
+ get(key) {
60
+ return this.entries.get(key)?.value;
61
+ }
62
+ /** Remove a context entry */
63
+ remove(agentId, key) {
64
+ const entry = this.entries.get(key);
65
+ if (!entry)
66
+ return false;
67
+ this.entries.delete(key);
68
+ const agent = this.agents.get(agentId);
69
+ agent?.contextKeys.delete(key);
70
+ this.recordChange({ type: 'remove', key, source: agentId, timestamp: Date.now() });
71
+ return true;
72
+ }
73
+ /** Get all context entries matching tags */
74
+ getByTags(tags) {
75
+ const tagSet = new Set(tags);
76
+ return [...this.entries.values()].filter(e => e.tags.some(t => tagSet.has(t)));
77
+ }
78
+ /** Get all context contributed by a specific agent */
79
+ getByAgent(agentId) {
80
+ const agent = this.agents.get(agentId);
81
+ if (!agent)
82
+ return [];
83
+ return [...agent.contextKeys]
84
+ .map(k => this.entries.get(k))
85
+ .filter((e) => e !== undefined);
86
+ }
87
+ /** Subscribe to context changes */
88
+ subscribe(agentId, listener) {
89
+ this.listeners.set(agentId, listener);
90
+ }
91
+ /** Get changes since a timestamp */
92
+ getChangesSince(since) {
93
+ return this.changeLog.filter(c => c.timestamp > since);
94
+ }
95
+ /** Build a summary string of shared context (for injection into prompts) */
96
+ buildContextSummary(maxTokens = 2000) {
97
+ const entries = [...this.entries.values()]
98
+ .sort((a, b) => b.addedAt - a.addedAt);
99
+ const lines = [];
100
+ let approxTokens = 0;
101
+ for (const entry of entries) {
102
+ const line = `[${entry.tags.join(',')}] ${entry.key}: ${entry.value}`;
103
+ const lineTokens = Math.ceil(line.length / 4);
104
+ if (approxTokens + lineTokens > maxTokens)
105
+ break;
106
+ lines.push(line);
107
+ approxTokens += lineTokens;
108
+ }
109
+ return lines.length
110
+ ? `--- Shared Context (${this.agents.size} agents) ---\n${lines.join('\n')}`
111
+ : '';
112
+ }
113
+ /** List active agents */
114
+ listAgents() {
115
+ return [...this.agents.values()].map(a => ({
116
+ id: a.id,
117
+ name: a.name,
118
+ role: a.role,
119
+ contextCount: a.contextKeys.size,
120
+ lastActive: a.lastActive,
121
+ }));
122
+ }
123
+ /** Get stats */
124
+ getStats() {
125
+ return {
126
+ agents: this.agents.size,
127
+ entries: this.entries.size,
128
+ changeLogSize: this.changeLog.length,
129
+ };
130
+ }
131
+ recordChange(change) {
132
+ this.changeLog.push(change);
133
+ if (this.changeLog.length > this.maxChangeLog) {
134
+ this.changeLog = this.changeLog.slice(-Math.floor(this.maxChangeLog * 0.8));
135
+ }
136
+ // Notify other agents
137
+ for (const [agentId, listener] of this.listeners) {
138
+ if (agentId !== change.source) {
139
+ try {
140
+ listener(change);
141
+ }
142
+ catch { /* ignore listener errors */ }
143
+ }
144
+ }
145
+ }
146
+ evictOldest() {
147
+ let oldest = null;
148
+ for (const entry of this.entries.values()) {
149
+ if (!oldest || entry.addedAt < oldest.addedAt)
150
+ oldest = entry;
151
+ }
152
+ if (oldest) {
153
+ this.entries.delete(oldest.key);
154
+ }
155
+ }
156
+ }
157
+ //# sourceMappingURL=context-memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-memory.js","sourceRoot":"","sources":["../../src/gateway/context-memory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ import type { CostRecord, Intent, Tier } from './types.js';
2
+ import type BetterSqlite3 from 'better-sqlite3';
3
+ export declare class CostTracker {
4
+ private rawDb;
5
+ private stmts;
6
+ constructor(rawDb: BetterSqlite3.Database);
7
+ private prepareStatements;
8
+ /** Record a completed request */
9
+ record(data: {
10
+ provider: string;
11
+ model: string;
12
+ tokensIn: number;
13
+ tokensOut: number;
14
+ costEstimate: number;
15
+ durationMs: number;
16
+ symbolIds: string[];
17
+ filePaths: string[];
18
+ intent: Intent;
19
+ complexityScore: number;
20
+ tier: Tier;
21
+ cacheHit: boolean;
22
+ }): string;
23
+ /** Cost breakdown by provider + model */
24
+ getCostByProvider(windowSql?: string): {
25
+ provider: string;
26
+ model: string;
27
+ request_count: number;
28
+ total_tokens_in: number;
29
+ total_tokens_out: number;
30
+ total_cost: number;
31
+ avg_duration: number;
32
+ }[];
33
+ /** Cost breakdown by file path */
34
+ getCostByFile(limit?: number, windowSql?: string): {
35
+ file_path: string;
36
+ request_count: number;
37
+ total_tokens: number;
38
+ total_cost: number;
39
+ }[];
40
+ /** Cost breakdown by intent */
41
+ getCostByIntent(windowSql?: string): {
42
+ intent: string;
43
+ request_count: number;
44
+ total_tokens: number;
45
+ total_cost: number;
46
+ avg_complexity: number;
47
+ }[];
48
+ /** Cost breakdown by tier */
49
+ getCostByTier(windowSql?: string): {
50
+ tier: string;
51
+ request_count: number;
52
+ total_cost: number;
53
+ avg_duration: number;
54
+ }[];
55
+ /** Cache hit/miss stats and savings */
56
+ getCacheSavings(windowSql?: string): {
57
+ cacheHits: any;
58
+ cacheMisses: any;
59
+ costSaved: any;
60
+ tokensSaved: any;
61
+ hitRate: number;
62
+ };
63
+ /** Most expensive symbols */
64
+ getTopExpensiveSymbols(limit?: number, windowSql?: string): {
65
+ symbol_id: string;
66
+ request_count: number;
67
+ total_cost: number;
68
+ total_tokens: number;
69
+ }[];
70
+ /** Overall summary */
71
+ getSummary(windowSql?: string): {
72
+ totalRequests: any;
73
+ totalTokensIn: any;
74
+ totalTokensOut: any;
75
+ totalCost: any;
76
+ avgDuration: any;
77
+ firstRequest: any;
78
+ lastRequest: any;
79
+ };
80
+ /** Get recent requests for debugging */
81
+ getRecentRequests(limit?: number): CostRecord[];
82
+ }
83
+ //# sourceMappingURL=observability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../../src/gateway/observability.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,aAAa,MAAM,gBAAgB,CAAC;AAEhD,qBAAa,WAAW;IAGV,OAAO,CAAC,KAAK;IAFzB,OAAO,CAAC,KAAK,CAA+C;gBAExC,KAAK,EAAE,aAAa,CAAC,QAAQ;IAIjD,OAAO,CAAC,iBAAiB;IAyFzB,iCAAiC;IACjC,MAAM,CAAC,IAAI,EAAE;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,eAAe,EAAE,MAAM,CAAC;QACxB,IAAI,EAAE,IAAI,CAAC;QACX,QAAQ,EAAE,OAAO,CAAC;KACnB,GAAG,MAAM;IAiBV,yCAAyC;IACzC,iBAAiB,CAAC,SAAS,SAAa,GACS;QAC7C,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAChC,aAAa,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAC;QACzE,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;KAC1C,EAAE;IAGL,kCAAkC;IAClC,aAAa,CAAC,KAAK,SAAK,EAAE,SAAS,SAAa,GACI;QAChD,SAAS,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;KACpF,EAAE;IAGL,+BAA+B;IAC/B,eAAe,CAAC,SAAS,SAAa,GACS;QAC3C,MAAM,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;KACzG,EAAE;IAGL,6BAA6B;IAC7B,aAAa,CAAC,SAAS,SAAa,GACS;QACzC,IAAI,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;KAC/E,EAAE;IAGL,uCAAuC;IACvC,eAAe,CAAC,SAAS,SAAa;;;;;;;IAatC,6BAA6B;IAC7B,sBAAsB,CAAC,KAAK,SAAK,EAAE,SAAS,SAAa,GACD;QACpD,SAAS,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;KACpF,EAAE;IAGL,sBAAsB;IACtB,UAAU,CAAC,SAAS,SAAa;;;;;;;;;IAajC,wCAAwC;IACxC,iBAAiB,CAAC,KAAK,SAAK,GAAG,UAAU,EAAE;CAkB5C"}
@@ -0,0 +1,152 @@
1
+ // ── Observability — per-function / per-file cost tracking ──
2
+ import { randomUUID } from 'node:crypto';
3
+ export class CostTracker {
4
+ rawDb;
5
+ stmts;
6
+ constructor(rawDb) {
7
+ this.rawDb = rawDb;
8
+ this.stmts = this.prepareStatements();
9
+ }
10
+ prepareStatements() {
11
+ return {
12
+ insert: this.rawDb.prepare(`INSERT INTO gateway_cost_log
13
+ (request_id, provider, model, tokens_in, tokens_out, cost_estimate, duration_ms,
14
+ symbol_ids, file_paths, intent, complexity_score, tier, cache_hit)
15
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
16
+ byProvider: this.rawDb.prepare(`SELECT provider, model,
17
+ COUNT(*) as request_count,
18
+ SUM(tokens_in) as total_tokens_in,
19
+ SUM(tokens_out) as total_tokens_out,
20
+ SUM(cost_estimate) as total_cost,
21
+ AVG(duration_ms) as avg_duration
22
+ FROM gateway_cost_log
23
+ WHERE timestamp >= datetime('now', ?)
24
+ GROUP BY provider, model
25
+ ORDER BY total_cost DESC`),
26
+ byFile: this.rawDb.prepare(`SELECT jf.value as file_path,
27
+ COUNT(*) as request_count,
28
+ SUM(cl.tokens_in + cl.tokens_out) as total_tokens,
29
+ SUM(cl.cost_estimate) as total_cost
30
+ FROM gateway_cost_log cl, json_each(cl.file_paths) jf
31
+ WHERE cl.timestamp >= datetime('now', ?)
32
+ GROUP BY jf.value
33
+ ORDER BY total_cost DESC
34
+ LIMIT ?`),
35
+ byIntent: this.rawDb.prepare(`SELECT intent,
36
+ COUNT(*) as request_count,
37
+ SUM(tokens_in + tokens_out) as total_tokens,
38
+ SUM(cost_estimate) as total_cost,
39
+ AVG(complexity_score) as avg_complexity
40
+ FROM gateway_cost_log
41
+ WHERE timestamp >= datetime('now', ?)
42
+ GROUP BY intent
43
+ ORDER BY total_cost DESC`),
44
+ byTier: this.rawDb.prepare(`SELECT tier,
45
+ COUNT(*) as request_count,
46
+ SUM(cost_estimate) as total_cost,
47
+ AVG(duration_ms) as avg_duration
48
+ FROM gateway_cost_log
49
+ WHERE timestamp >= datetime('now', ?)
50
+ GROUP BY tier`),
51
+ cacheSavings: this.rawDb.prepare(`SELECT
52
+ SUM(CASE WHEN cache_hit = 1 THEN 1 ELSE 0 END) as cache_hits,
53
+ SUM(CASE WHEN cache_hit = 0 THEN 1 ELSE 0 END) as cache_misses,
54
+ SUM(CASE WHEN cache_hit = 1 THEN cost_estimate ELSE 0 END) as cost_saved,
55
+ SUM(CASE WHEN cache_hit = 1 THEN tokens_in + tokens_out ELSE 0 END) as tokens_saved
56
+ FROM gateway_cost_log
57
+ WHERE timestamp >= datetime('now', ?)`),
58
+ topSymbols: this.rawDb.prepare(`SELECT js.value as symbol_id,
59
+ COUNT(*) as request_count,
60
+ SUM(cl.cost_estimate) as total_cost,
61
+ SUM(cl.tokens_in + cl.tokens_out) as total_tokens
62
+ FROM gateway_cost_log cl, json_each(cl.symbol_ids) js
63
+ WHERE cl.timestamp >= datetime('now', ?)
64
+ GROUP BY js.value
65
+ ORDER BY total_cost DESC
66
+ LIMIT ?`),
67
+ summary: this.rawDb.prepare(`SELECT
68
+ COUNT(*) as total_requests,
69
+ SUM(tokens_in) as total_tokens_in,
70
+ SUM(tokens_out) as total_tokens_out,
71
+ SUM(cost_estimate) as total_cost,
72
+ AVG(duration_ms) as avg_duration,
73
+ MIN(timestamp) as first_request,
74
+ MAX(timestamp) as last_request
75
+ FROM gateway_cost_log
76
+ WHERE timestamp >= datetime('now', ?)`),
77
+ recentRequests: this.rawDb.prepare(`SELECT * FROM gateway_cost_log ORDER BY timestamp DESC LIMIT ?`),
78
+ };
79
+ }
80
+ /** Record a completed request */
81
+ record(data) {
82
+ const requestId = randomUUID();
83
+ this.stmts.insert.run(requestId, data.provider, data.model, data.tokensIn, data.tokensOut, data.costEstimate, data.durationMs, JSON.stringify(data.symbolIds), JSON.stringify(data.filePaths), data.intent, data.complexityScore, data.tier, data.cacheHit ? 1 : 0);
84
+ return requestId;
85
+ }
86
+ /** Cost breakdown by provider + model */
87
+ getCostByProvider(windowSql = '-30 days') {
88
+ return this.stmts.byProvider.all(windowSql);
89
+ }
90
+ /** Cost breakdown by file path */
91
+ getCostByFile(limit = 20, windowSql = '-30 days') {
92
+ return this.stmts.byFile.all(windowSql, limit);
93
+ }
94
+ /** Cost breakdown by intent */
95
+ getCostByIntent(windowSql = '-30 days') {
96
+ return this.stmts.byIntent.all(windowSql);
97
+ }
98
+ /** Cost breakdown by tier */
99
+ getCostByTier(windowSql = '-30 days') {
100
+ return this.stmts.byTier.all(windowSql);
101
+ }
102
+ /** Cache hit/miss stats and savings */
103
+ getCacheSavings(windowSql = '-30 days') {
104
+ const row = this.stmts.cacheSavings.get(windowSql);
105
+ return {
106
+ cacheHits: row?.cache_hits ?? 0,
107
+ cacheMisses: row?.cache_misses ?? 0,
108
+ costSaved: row?.cost_saved ?? 0,
109
+ tokensSaved: row?.tokens_saved ?? 0,
110
+ hitRate: row?.cache_hits
111
+ ? row.cache_hits / (row.cache_hits + row.cache_misses)
112
+ : 0,
113
+ };
114
+ }
115
+ /** Most expensive symbols */
116
+ getTopExpensiveSymbols(limit = 10, windowSql = '-30 days') {
117
+ return this.stmts.topSymbols.all(windowSql, limit);
118
+ }
119
+ /** Overall summary */
120
+ getSummary(windowSql = '-30 days') {
121
+ const row = this.stmts.summary.get(windowSql);
122
+ return {
123
+ totalRequests: row?.total_requests ?? 0,
124
+ totalTokensIn: row?.total_tokens_in ?? 0,
125
+ totalTokensOut: row?.total_tokens_out ?? 0,
126
+ totalCost: row?.total_cost ?? 0,
127
+ avgDuration: row?.avg_duration ?? 0,
128
+ firstRequest: row?.first_request ?? null,
129
+ lastRequest: row?.last_request ?? null,
130
+ };
131
+ }
132
+ /** Get recent requests for debugging */
133
+ getRecentRequests(limit = 10) {
134
+ return this.stmts.recentRequests.all(limit).map(r => ({
135
+ requestId: r.request_id,
136
+ timestamp: r.timestamp,
137
+ provider: r.provider,
138
+ model: r.model,
139
+ tokensIn: r.tokens_in,
140
+ tokensOut: r.tokens_out,
141
+ costEstimate: r.cost_estimate,
142
+ durationMs: r.duration_ms,
143
+ symbolIds: JSON.parse(r.symbol_ids || '[]'),
144
+ filePaths: JSON.parse(r.file_paths || '[]'),
145
+ intent: r.intent,
146
+ complexityScore: r.complexity_score,
147
+ tier: r.tier,
148
+ cacheHit: !!r.cache_hit,
149
+ }));
150
+ }
151
+ }
152
+ //# sourceMappingURL=observability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observability.js","sourceRoot":"","sources":["../../src/gateway/observability.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import type { ChatMessage, PrivacyRule, PrivacyDecision, MessageAnalysis } from './types.js';
2
+ import type BetterSqlite3 from 'better-sqlite3';
3
+ export declare class PrivacyFilter {
4
+ private rawDb;
5
+ private defaultRules;
6
+ private rulesCache;
7
+ private stmts;
8
+ constructor(rawDb: BetterSqlite3.Database, defaultRules?: PrivacyRule[]);
9
+ private prepareStatements;
10
+ /** Evaluate privacy decision for a request based on referenced files */
11
+ evaluate(analysis: MessageAnalysis): PrivacyDecision;
12
+ /** Mask sensitive content in messages — returns masked messages + unmask map */
13
+ maskMessages(messages: ChatMessage[], rules: PrivacyRule[]): {
14
+ masked: ChatMessage[];
15
+ unmaskMap: Map<string, string>;
16
+ };
17
+ /** Restore original content from masked tokens */
18
+ unmask(text: string, unmaskMap: Map<string, string>): string;
19
+ /** Add a privacy rule (persisted to DB) */
20
+ addRule(rule: PrivacyRule): void;
21
+ /** Remove a privacy rule */
22
+ removeRule(pathPattern: string): boolean;
23
+ /** List all rules */
24
+ listRules(): PrivacyRule[];
25
+ private loadRules;
26
+ }
27
+ //# sourceMappingURL=privacy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"privacy.d.ts","sourceRoot":"","sources":["../../src/gateway/privacy.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAiB,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC5G,OAAO,KAAK,aAAa,MAAM,gBAAgB,CAAC;AAEhD,qBAAa,aAAa;IAKtB,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,YAAY;IALtB,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,KAAK,CAAiD;gBAGpD,KAAK,EAAE,aAAa,CAAC,QAAQ,EAC7B,YAAY,GAAE,WAAW,EAAO;IAK1C,OAAO,CAAC,iBAAiB;IAUzB,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,eAAe,GAAG,eAAe;IA6BpD,gFAAgF;IAChF,YAAY,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG;QAAE,MAAM,EAAE,WAAW,EAAE,CAAC;QAAC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE;IAsBtH,kDAAkD;IAClD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM;IAQ5D,2CAA2C;IAC3C,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAKhC,4BAA4B;IAC5B,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAMxC,qBAAqB;IACrB,SAAS,IAAI,WAAW,EAAE;IAI1B,OAAO,CAAC,SAAS;CAUlB"}
@@ -0,0 +1,139 @@
1
+ // ── Privacy filter — path-based sensitivity rules with masking ──
2
+ import { randomBytes } from 'node:crypto';
3
+ export class PrivacyFilter {
4
+ rawDb;
5
+ defaultRules;
6
+ rulesCache = null;
7
+ stmts;
8
+ constructor(rawDb, defaultRules = []) {
9
+ this.rawDb = rawDb;
10
+ this.defaultRules = defaultRules;
11
+ this.stmts = this.prepareStatements();
12
+ }
13
+ prepareStatements() {
14
+ return {
15
+ listRules: this.rawDb.prepare('SELECT * FROM gateway_privacy_rules ORDER BY path_pattern'),
16
+ addRule: this.rawDb.prepare(`INSERT OR REPLACE INTO gateway_privacy_rules (path_pattern, action, reason) VALUES (?, ?, ?)`),
17
+ removeRule: this.rawDb.prepare('DELETE FROM gateway_privacy_rules WHERE path_pattern = ?'),
18
+ };
19
+ }
20
+ /** Evaluate privacy decision for a request based on referenced files */
21
+ evaluate(analysis) {
22
+ const rules = this.loadRules();
23
+ const matched = [];
24
+ let highestAction = 'allow';
25
+ for (const file of analysis.files) {
26
+ for (const rule of rules) {
27
+ if (matchGlob(file, rule.pathPattern)) {
28
+ matched.push(rule);
29
+ highestAction = promoteAction(highestAction, rule.action);
30
+ }
31
+ }
32
+ }
33
+ // Also check symbol file paths
34
+ for (const sym of analysis.symbols) {
35
+ for (const rule of rules) {
36
+ if (matchGlob(sym.filePath, rule.pathPattern)) {
37
+ if (!matched.some(m => m.pathPattern === rule.pathPattern)) {
38
+ matched.push(rule);
39
+ highestAction = promoteAction(highestAction, rule.action);
40
+ }
41
+ }
42
+ }
43
+ }
44
+ return { action: highestAction, matchedRules: matched };
45
+ }
46
+ /** Mask sensitive content in messages — returns masked messages + unmask map */
47
+ maskMessages(messages, rules) {
48
+ const unmaskMap = new Map();
49
+ const masked = messages.map(msg => {
50
+ const content = typeof msg.content === 'string' ? msg.content : '';
51
+ if (!content)
52
+ return msg;
53
+ let result = content;
54
+ for (const rule of rules) {
55
+ if (rule.action !== 'mask')
56
+ continue;
57
+ result = maskPatternOccurrences(result, rule.pathPattern, unmaskMap);
58
+ }
59
+ // Mask inline @sensitive annotations
60
+ result = maskSensitiveAnnotations(result, unmaskMap);
61
+ return { ...msg, content: result };
62
+ });
63
+ return { masked, unmaskMap };
64
+ }
65
+ /** Restore original content from masked tokens */
66
+ unmask(text, unmaskMap) {
67
+ let result = text;
68
+ for (const [token, original] of unmaskMap) {
69
+ result = result.replaceAll(token, original);
70
+ }
71
+ return result;
72
+ }
73
+ /** Add a privacy rule (persisted to DB) */
74
+ addRule(rule) {
75
+ this.stmts.addRule.run(rule.pathPattern, rule.action, rule.reason ?? null);
76
+ this.rulesCache = null;
77
+ }
78
+ /** Remove a privacy rule */
79
+ removeRule(pathPattern) {
80
+ const r = this.stmts.removeRule.run(pathPattern);
81
+ this.rulesCache = null;
82
+ return r.changes > 0;
83
+ }
84
+ /** List all rules */
85
+ listRules() {
86
+ return this.loadRules();
87
+ }
88
+ loadRules() {
89
+ if (this.rulesCache)
90
+ return this.rulesCache;
91
+ const dbRules = this.stmts.listRules.all().map(r => ({
92
+ pathPattern: r.path_pattern,
93
+ action: r.action,
94
+ reason: r.reason ?? undefined,
95
+ }));
96
+ this.rulesCache = [...this.defaultRules, ...dbRules];
97
+ return this.rulesCache;
98
+ }
99
+ }
100
+ // ── Helpers ──
101
+ /** Simple glob matching: supports * and ** */
102
+ function matchGlob(filePath, pattern) {
103
+ const regex = pattern
104
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars
105
+ .replace(/\*\*/g, '{{GLOBSTAR}}') // Temp placeholder
106
+ .replace(/\*/g, '[^/]*') // Single glob
107
+ .replace(/{{GLOBSTAR}}/g, '.*'); // Double glob
108
+ return new RegExp(`^${regex}$`).test(filePath);
109
+ }
110
+ /** Promote to higher severity: block > local-only > mask > allow */
111
+ function promoteAction(current, incoming) {
112
+ const priority = { allow: 0, mask: 1, 'local-only': 2, block: 3 };
113
+ return (priority[incoming] ?? 0) > (priority[current] ?? 0) ? incoming : current;
114
+ }
115
+ /** Replace occurrences matching a glob pattern in text with masked tokens */
116
+ function maskPatternOccurrences(text, pathPattern, unmaskMap) {
117
+ // Extract a searchable pattern from the glob (e.g., "secrets/**" → "secrets/")
118
+ const literalPrefix = pathPattern.replace(/\*.*$/, '');
119
+ if (!literalPrefix || !text.includes(literalPrefix))
120
+ return text;
121
+ // Find all file-path-like substrings starting with the literal prefix
122
+ const filePathRe = new RegExp(literalPrefix.replace(/[.+^${}()|[\]\\]/g, '\\$&') + '[\\w/._-]*', 'g');
123
+ return text.replace(filePathRe, match => {
124
+ if (!matchGlob(match, pathPattern))
125
+ return match;
126
+ const token = `[REDACTED_${randomBytes(4).toString('hex')}]`;
127
+ unmaskMap.set(token, match);
128
+ return token;
129
+ });
130
+ }
131
+ /** Mask content wrapped in @sensitive{...} annotations */
132
+ function maskSensitiveAnnotations(text, unmaskMap) {
133
+ return text.replace(/@sensitive\{([^}]+)\}/g, (_match, inner) => {
134
+ const token = `[REDACTED_${randomBytes(4).toString('hex')}]`;
135
+ unmaskMap.set(token, inner);
136
+ return token;
137
+ });
138
+ }
139
+ //# sourceMappingURL=privacy.js.map