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.
- package/README.md +157 -14
- package/dist/analyzer/config.d.ts.map +1 -1
- package/dist/analyzer/config.js +33 -1
- package/dist/analyzer/config.js.map +1 -1
- package/dist/analyzer/engine.d.ts +1 -0
- package/dist/analyzer/engine.d.ts.map +1 -1
- package/dist/analyzer/engine.js +27 -8
- package/dist/analyzer/engine.js.map +1 -1
- package/dist/analyzer/review.d.ts +23 -0
- package/dist/analyzer/review.d.ts.map +1 -0
- package/dist/analyzer/review.js +143 -0
- package/dist/analyzer/review.js.map +1 -0
- package/dist/analyzer/testplan.d.ts +59 -0
- package/dist/analyzer/testplan.d.ts.map +1 -0
- package/dist/analyzer/testplan.js +218 -0
- package/dist/analyzer/testplan.js.map +1 -0
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/gateway/analyzer.d.ts +6 -0
- package/dist/gateway/analyzer.d.ts.map +1 -0
- package/dist/gateway/analyzer.js +218 -0
- package/dist/gateway/analyzer.js.map +1 -0
- package/dist/gateway/cache.d.ts +35 -0
- package/dist/gateway/cache.d.ts.map +1 -0
- package/dist/gateway/cache.js +175 -0
- package/dist/gateway/cache.js.map +1 -0
- package/dist/gateway/config.d.ts +10 -0
- package/dist/gateway/config.d.ts.map +1 -0
- package/dist/gateway/config.js +167 -0
- package/dist/gateway/config.js.map +1 -0
- package/dist/gateway/context-memory.d.ts +68 -0
- package/dist/gateway/context-memory.d.ts.map +1 -0
- package/dist/gateway/context-memory.js +157 -0
- package/dist/gateway/context-memory.js.map +1 -0
- package/dist/gateway/observability.d.ts +83 -0
- package/dist/gateway/observability.d.ts.map +1 -0
- package/dist/gateway/observability.js +152 -0
- package/dist/gateway/observability.js.map +1 -0
- package/dist/gateway/privacy.d.ts +27 -0
- package/dist/gateway/privacy.d.ts.map +1 -0
- package/dist/gateway/privacy.js +139 -0
- package/dist/gateway/privacy.js.map +1 -0
- package/dist/gateway/providers.d.ts +66 -0
- package/dist/gateway/providers.d.ts.map +1 -0
- package/dist/gateway/providers.js +377 -0
- package/dist/gateway/providers.js.map +1 -0
- package/dist/gateway/router.d.ts +18 -0
- package/dist/gateway/router.d.ts.map +1 -0
- package/dist/gateway/router.js +102 -0
- package/dist/gateway/router.js.map +1 -0
- package/dist/gateway/server.d.ts +20 -0
- package/dist/gateway/server.d.ts.map +1 -0
- package/dist/gateway/server.js +387 -0
- package/dist/gateway/server.js.map +1 -0
- package/dist/gateway/translator.d.ts +19 -0
- package/dist/gateway/translator.d.ts.map +1 -0
- package/dist/gateway/translator.js +340 -0
- package/dist/gateway/translator.js.map +1 -0
- package/dist/gateway/types.d.ts +215 -0
- package/dist/gateway/types.d.ts.map +1 -0
- package/dist/gateway/types.js +3 -0
- package/dist/gateway/types.js.map +1 -0
- package/dist/parser/extract.d.ts +1 -0
- package/dist/parser/extract.d.ts.map +1 -1
- package/dist/parser/extract.js +8 -0
- package/dist/parser/extract.js.map +1 -1
- package/dist/parser/lang-go.d.ts.map +1 -1
- package/dist/parser/lang-go.js +41 -5
- package/dist/parser/lang-go.js.map +1 -1
- package/dist/parser/lang-java.d.ts.map +1 -1
- package/dist/parser/lang-java.js +1 -0
- package/dist/parser/lang-java.js.map +1 -1
- package/dist/parser/lang-js.d.ts.map +1 -1
- package/dist/parser/lang-js.js +23 -1
- package/dist/parser/lang-js.js.map +1 -1
- package/dist/parser/lang-py.d.ts.map +1 -1
- package/dist/parser/lang-py.js +22 -0
- package/dist/parser/lang-py.js.map +1 -1
- package/dist/parser/lang-ruby.d.ts.map +1 -1
- package/dist/parser/lang-ruby.js +1 -0
- package/dist/parser/lang-ruby.js.map +1 -1
- package/dist/parser/lang-ts.d.ts.map +1 -1
- package/dist/parser/lang-ts.js +62 -1
- package/dist/parser/lang-ts.js.map +1 -1
- package/dist/server/mcp.d.ts.map +1 -1
- package/dist/server/mcp.js +615 -106
- package/dist/server/mcp.js.map +1 -1
- package/dist/skills.js +32 -0
- package/dist/skills.js.map +1 -1
- package/dist/store/db.d.ts +44 -0
- package/dist/store/db.d.ts.map +1 -1
- package/dist/store/db.js +145 -25
- package/dist/store/db.js.map +1 -1
- package/dist/store/gateway-schema.sql +53 -0
- package/dist/store/schema.sql +33 -0
- package/dist/store/vectors.d.ts +65 -0
- package/dist/store/vectors.d.ts.map +1 -0
- package/dist/store/vectors.js +212 -0
- package/dist/store/vectors.js.map +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +9 -0
- package/dist/utils.js.map +1 -0
- package/docs/diagram2.svg +1 -1
- 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
|