context-lens 0.2.0 → 0.3.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 (119) hide show
  1. package/README.md +120 -25
  2. package/dist/cli-utils.d.ts +2 -1
  3. package/dist/cli-utils.d.ts.map +1 -1
  4. package/dist/cli-utils.js +40 -13
  5. package/dist/cli-utils.js.map +1 -1
  6. package/dist/cli.js +184 -68
  7. package/dist/cli.js.map +1 -1
  8. package/dist/core/conversation.d.ts +63 -0
  9. package/dist/core/conversation.d.ts.map +1 -0
  10. package/dist/core/conversation.js +305 -0
  11. package/dist/core/conversation.js.map +1 -0
  12. package/dist/core/health.d.ts +7 -0
  13. package/dist/core/health.d.ts.map +1 -0
  14. package/dist/core/health.js +311 -0
  15. package/dist/core/health.js.map +1 -0
  16. package/dist/core/models.d.ts +36 -0
  17. package/dist/core/models.d.ts.map +1 -0
  18. package/dist/core/models.js +111 -0
  19. package/dist/core/models.js.map +1 -0
  20. package/dist/core/parse.d.ts +17 -0
  21. package/dist/core/parse.d.ts.map +1 -0
  22. package/dist/core/parse.js +349 -0
  23. package/dist/core/parse.js.map +1 -0
  24. package/dist/core/routing.d.ts +47 -0
  25. package/dist/core/routing.d.ts.map +1 -0
  26. package/dist/core/routing.js +132 -0
  27. package/dist/core/routing.js.map +1 -0
  28. package/dist/core/security.d.ts +8 -0
  29. package/dist/core/security.d.ts.map +1 -0
  30. package/dist/core/security.js +222 -0
  31. package/dist/core/security.js.map +1 -0
  32. package/dist/core/source.d.ts +22 -0
  33. package/dist/core/source.d.ts.map +1 -0
  34. package/dist/core/source.js +56 -0
  35. package/dist/core/source.js.map +1 -0
  36. package/dist/core/tokens.d.ts +29 -0
  37. package/dist/core/tokens.d.ts.map +1 -0
  38. package/dist/core/tokens.js +163 -0
  39. package/dist/core/tokens.js.map +1 -0
  40. package/dist/core.d.ts +14 -22
  41. package/dist/core.d.ts.map +1 -1
  42. package/dist/core.js +14 -471
  43. package/dist/core.js.map +1 -1
  44. package/dist/http/headers.d.ts +25 -0
  45. package/dist/http/headers.d.ts.map +1 -0
  46. package/dist/http/headers.js +54 -0
  47. package/dist/http/headers.js.map +1 -0
  48. package/dist/lhar/composition.d.ts +12 -0
  49. package/dist/lhar/composition.d.ts.map +1 -0
  50. package/dist/lhar/composition.js +258 -0
  51. package/dist/lhar/composition.js.map +1 -0
  52. package/dist/lhar/export.d.ts +5 -0
  53. package/dist/lhar/export.d.ts.map +1 -0
  54. package/dist/lhar/export.js +59 -0
  55. package/dist/lhar/export.js.map +1 -0
  56. package/dist/lhar/record.d.ts +6 -0
  57. package/dist/lhar/record.d.ts.map +1 -0
  58. package/dist/lhar/record.js +216 -0
  59. package/dist/lhar/record.js.map +1 -0
  60. package/dist/lhar/response.d.ts +11 -0
  61. package/dist/lhar/response.d.ts.map +1 -0
  62. package/dist/lhar/response.js +132 -0
  63. package/dist/lhar/response.js.map +1 -0
  64. package/dist/lhar-types.generated.d.ts +24 -3
  65. package/dist/lhar-types.generated.d.ts.map +1 -1
  66. package/dist/lhar.d.ts +12 -19
  67. package/dist/lhar.d.ts.map +1 -1
  68. package/dist/lhar.js +16 -473
  69. package/dist/lhar.js.map +1 -1
  70. package/dist/server/api.d.ts +8 -0
  71. package/dist/server/api.d.ts.map +1 -0
  72. package/dist/server/api.js +292 -0
  73. package/dist/server/api.js.map +1 -0
  74. package/dist/server/config.d.ts +13 -0
  75. package/dist/server/config.d.ts.map +1 -0
  76. package/dist/server/config.js +36 -0
  77. package/dist/server/config.js.map +1 -0
  78. package/dist/server/projection.d.ts +9 -0
  79. package/dist/server/projection.d.ts.map +1 -0
  80. package/dist/server/projection.js +47 -0
  81. package/dist/server/projection.js.map +1 -0
  82. package/dist/server/proxy.d.ts +13 -0
  83. package/dist/server/proxy.d.ts.map +1 -0
  84. package/dist/server/proxy.js +218 -0
  85. package/dist/server/proxy.js.map +1 -0
  86. package/dist/server/static.d.ts +9 -0
  87. package/dist/server/static.d.ts.map +1 -0
  88. package/dist/server/static.js +78 -0
  89. package/dist/server/static.js.map +1 -0
  90. package/dist/server/store.d.ts +81 -0
  91. package/dist/server/store.d.ts.map +1 -0
  92. package/dist/server/store.js +632 -0
  93. package/dist/server/store.js.map +1 -0
  94. package/dist/server/webui.d.ts +5 -0
  95. package/dist/server/webui.d.ts.map +1 -0
  96. package/dist/server/webui.js +42 -0
  97. package/dist/server/webui.js.map +1 -0
  98. package/dist/server-utils.d.ts +2 -2
  99. package/dist/server-utils.d.ts.map +1 -1
  100. package/dist/server-utils.js +12 -21
  101. package/dist/server-utils.js.map +1 -1
  102. package/dist/server.js +31 -697
  103. package/dist/server.js.map +1 -1
  104. package/dist/types.d.ts +94 -10
  105. package/dist/types.d.ts.map +1 -1
  106. package/dist/version.generated.d.ts +2 -0
  107. package/dist/version.generated.d.ts.map +1 -0
  108. package/dist/version.generated.js +2 -0
  109. package/dist/version.generated.js.map +1 -0
  110. package/findings-screenshot.png +0 -0
  111. package/messages-screenshot.png +0 -0
  112. package/package.json +23 -10
  113. package/schema/lhar.schema.json +58 -4
  114. package/screenshot-overview.png +0 -0
  115. package/sessions-screenshot.png +0 -0
  116. package/timeline-screenshot.png +0 -0
  117. package/diff.png +0 -0
  118. package/overview-sidebar.png +0 -0
  119. package/public/index.html +0 -2804
@@ -0,0 +1,132 @@
1
+ /**
2
+ * URL path segments that represent API resources rather than "source tool" prefixes.
3
+ *
4
+ * Example: `/v1/messages` should not treat `v1` as a source tag.
5
+ */
6
+ export const API_PATH_SEGMENTS = new Set([
7
+ "v1",
8
+ "v1beta",
9
+ "v1alpha",
10
+ "v1internal",
11
+ "responses",
12
+ "chat",
13
+ "models",
14
+ "embeddings",
15
+ "backend-api",
16
+ "api",
17
+ ]);
18
+ /**
19
+ * Infer provider based on request path + headers.
20
+ *
21
+ * This is used for routing (choosing which upstream base URL to use) and parsing.
22
+ */
23
+ export function detectProvider(pathname, headers) {
24
+ return classifyRequest(pathname, headers).provider;
25
+ }
26
+ /**
27
+ * Infer the API "format" (schema family) from the request path.
28
+ *
29
+ * This is distinct from provider: e.g. OpenAI can be `responses` or `chat-completions`.
30
+ */
31
+ export function detectApiFormat(pathname) {
32
+ return classifyRequest(pathname, {}).apiFormat;
33
+ }
34
+ /**
35
+ * Classify an incoming request into `{ provider, apiFormat }`.
36
+ *
37
+ * Keep all path/format heuristics in one place to avoid drift between
38
+ * routing decisions and parsing decisions.
39
+ */
40
+ export function classifyRequest(pathname, headers) {
41
+ // ChatGPT backend traffic (Codex subscription)
42
+ if (pathname.match(/^\/(api|backend-api)\//))
43
+ return { provider: "chatgpt", apiFormat: "chatgpt-backend" };
44
+ // Anthropic Messages API
45
+ if (pathname.includes("/v1/messages"))
46
+ return { provider: "anthropic", apiFormat: "anthropic-messages" };
47
+ if (pathname.includes("/v1/complete"))
48
+ return { provider: "anthropic", apiFormat: "unknown" };
49
+ if (headers["anthropic-version"])
50
+ return { provider: "anthropic", apiFormat: "unknown" };
51
+ // Gemini: must come BEFORE openai catch-all (which matches /models/)
52
+ const isGeminiPath = pathname.includes(":generateContent") ||
53
+ pathname.includes(":streamGenerateContent") ||
54
+ pathname.match(/\/v1(beta|alpha)\/models\//) ||
55
+ pathname.includes("/v1internal:");
56
+ if (isGeminiPath || headers["x-goog-api-key"])
57
+ return { provider: "gemini", apiFormat: "gemini" };
58
+ // OpenAI
59
+ if (pathname.includes("/responses"))
60
+ return { provider: "openai", apiFormat: "responses" };
61
+ if (pathname.includes("/chat/completions"))
62
+ return { provider: "openai", apiFormat: "chat-completions" };
63
+ if (pathname.match(/\/(models|embeddings)/))
64
+ return { provider: "openai", apiFormat: "unknown" };
65
+ if (headers.authorization?.startsWith("Bearer sk-"))
66
+ return { provider: "openai", apiFormat: "unknown" };
67
+ return { provider: "unknown", apiFormat: "unknown" };
68
+ }
69
+ /**
70
+ * Extract a "source tool" tag from a request path.
71
+ *
72
+ * Example: `/claude/v1/messages` => `{ source: 'claude', cleanPath: '/v1/messages' }`.
73
+ *
74
+ * This tag is used for attribution in the UI/LHAR and for per-tool grouping.
75
+ */
76
+ export function extractSource(pathname) {
77
+ const match = pathname.match(/^\/([^/]+)(\/.*)?$/);
78
+ if (match?.[2] && !API_PATH_SEGMENTS.has(match[1])) {
79
+ // `decodeURIComponent` may introduce `/` via `%2f` (path traversal) or throw on bad encodings.
80
+ // Treat suspicious/invalid tags as "no source tag" and route the request normally.
81
+ let decoded = match[1];
82
+ try {
83
+ decoded = decodeURIComponent(match[1]);
84
+ }
85
+ catch {
86
+ decoded = match[1];
87
+ }
88
+ if (decoded.includes("/") ||
89
+ decoded.includes("\\") ||
90
+ decoded.includes("..")) {
91
+ return { source: null, cleanPath: pathname };
92
+ }
93
+ return { source: decoded, cleanPath: match[2] || "/" };
94
+ }
95
+ return { source: null, cleanPath: pathname };
96
+ }
97
+ /**
98
+ * Determine the final upstream target URL for a request.
99
+ *
100
+ * @param parsedUrl - Path + query extracted from the incoming request.
101
+ * @param headers - Headers used for detection and optional override.
102
+ * @param upstreams - Base URLs for each provider.
103
+ * @returns `{ targetUrl, provider }`.
104
+ */
105
+ export function resolveTargetUrl(parsedUrl, headers, upstreams) {
106
+ const provider = classifyRequest(parsedUrl.pathname, headers).provider;
107
+ const search = parsedUrl.search || "";
108
+ let targetUrl = headers["x-target-url"];
109
+ if (!targetUrl) {
110
+ if (provider === "chatgpt") {
111
+ targetUrl = upstreams.chatgpt + parsedUrl.pathname + search;
112
+ }
113
+ else if (provider === "anthropic") {
114
+ targetUrl = upstreams.anthropic + parsedUrl.pathname + search;
115
+ }
116
+ else if (provider === "gemini") {
117
+ const isCodeAssist = parsedUrl.pathname.includes("/v1internal");
118
+ targetUrl =
119
+ (isCodeAssist ? upstreams.geminiCodeAssist : upstreams.gemini) +
120
+ parsedUrl.pathname +
121
+ search;
122
+ }
123
+ else {
124
+ targetUrl = upstreams.openai + parsedUrl.pathname + search;
125
+ }
126
+ }
127
+ else if (!targetUrl.startsWith("http")) {
128
+ targetUrl = targetUrl + parsedUrl.pathname + search;
129
+ }
130
+ return { targetUrl, provider };
131
+ }
132
+ //# sourceMappingURL=routing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing.js","sourceRoot":"","sources":["../../src/core/routing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { ContextInfo, SecurityResult } from "../types.js";
2
+ /**
3
+ * Scan all messages in a context for prompt injection patterns.
4
+ *
5
+ * This is the main entry point, called from Store.storeRequest() before compaction.
6
+ */
7
+ export declare function scanSecurity(contextInfo: ContextInfo): SecurityResult;
8
+ //# sourceMappingURL=security.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../../src/core/security.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,WAAW,EAGX,cAAc,EAEf,MAAM,aAAa,CAAC;AA2PrB;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,WAAW,GAAG,cAAc,CAkBrE"}
@@ -0,0 +1,222 @@
1
+ const TIER1_PATTERNS = [
2
+ // Role hijacking
3
+ {
4
+ id: "role_hijack_ignore",
5
+ severity: "high",
6
+ pattern: /ignore\s+(?:all\s+)?(?:previous|prior|above|earlier|preceding)\s+instructions/i,
7
+ },
8
+ {
9
+ id: "role_hijack_disregard",
10
+ severity: "high",
11
+ pattern: /disregard\s+(?:all\s+)?(?:previous|prior|above|earlier|your)\s+(?:instructions|directives|rules|guidelines)/i,
12
+ },
13
+ {
14
+ id: "role_hijack_forget",
15
+ severity: "high",
16
+ pattern: /forget\s+(?:all\s+)?(?:previous|prior|above|earlier|your)\s+(?:instructions|directives|rules|context)/i,
17
+ },
18
+ {
19
+ id: "role_hijack_new_instructions",
20
+ severity: "high",
21
+ pattern: /(?:your\s+new\s+instructions\s+are|from\s+now\s+on\s+you\s+(?:are|will|must|should))/i,
22
+ },
23
+ {
24
+ id: "role_hijack_override",
25
+ severity: "high",
26
+ pattern: /system\s*prompt\s*override/i,
27
+ },
28
+ {
29
+ id: "role_hijack_act_as",
30
+ severity: "high",
31
+ pattern: /(?:you\s+are\s+now|act\s+as|pretend\s+(?:to\s+be|you\s+are))\s+(?:DAN|an?\s+unrestricted|an?\s+unfiltered|jailbroken|evil)/i,
32
+ },
33
+ // Known jailbreak templates
34
+ {
35
+ id: "jailbreak_dan",
36
+ severity: "high",
37
+ pattern: /\bDAN\s*(?:mode|prompt|jailbreak|\d+\.\d+)\b/i,
38
+ },
39
+ {
40
+ id: "jailbreak_developer_mode",
41
+ severity: "high",
42
+ pattern: /(?:developer|god)\s*mode\s*(?:enabled|activated|on)\b/i,
43
+ },
44
+ {
45
+ id: "jailbreak_do_anything_now",
46
+ severity: "high",
47
+ pattern: /do\s+anything\s+now/i,
48
+ },
49
+ // Chat template tokens in content (should never appear in user/tool messages)
50
+ {
51
+ id: "chat_template_inst",
52
+ severity: "high",
53
+ pattern: /\[INST\]|\[\/INST\]/,
54
+ },
55
+ {
56
+ id: "chat_template_im",
57
+ severity: "high",
58
+ pattern: /<\|im_start\|>|<\|im_end\|>/,
59
+ },
60
+ {
61
+ id: "chat_template_special",
62
+ severity: "high",
63
+ pattern: /<\|(?:system|user|assistant|endof(?:text|turn)|sep|pad)\|>/,
64
+ },
65
+ // Base64-encoded instruction blocks (heuristic: long base64 string > 100 chars)
66
+ {
67
+ id: "base64_block",
68
+ severity: "medium",
69
+ pattern: /(?:^|[\s:=])([A-Za-z0-9+/]{100,}={0,2})(?:$|[\s])/m,
70
+ },
71
+ // HTML/Markdown injection hiding content
72
+ {
73
+ id: "html_hidden_text",
74
+ severity: "medium",
75
+ pattern: /<!--[\s\S]*?(?:ignore|instruction|system|prompt|override)[\s\S]*?-->/i,
76
+ },
77
+ {
78
+ id: "html_invisible_style",
79
+ severity: "medium",
80
+ pattern: /style\s*=\s*["'][^"']*(?:font-size\s*:\s*0|display\s*:\s*none|visibility\s*:\s*hidden|color\s*:\s*(?:white|#fff(?:fff)?|rgba?\([^)]*,\s*0\)))[^"']*["']/i,
81
+ },
82
+ // Prompt leaking attempts
83
+ {
84
+ id: "prompt_leak_request",
85
+ severity: "medium",
86
+ pattern: /(?:reveal|show|display|output|print|repeat|echo)\s+(?:your\s+)?(?:system\s+prompt|instructions|initial\s+prompt|hidden\s+prompt|original\s+prompt)/i,
87
+ },
88
+ ];
89
+ // ---------------------------------------------------------------------------
90
+ // Tier 2: Heuristic analysis for structural anomalies
91
+ // ---------------------------------------------------------------------------
92
+ /**
93
+ * Detect role confusion: imperative AI instructions appearing in tool results.
94
+ * Only scans tool_result content blocks.
95
+ */
96
+ const ROLE_CONFUSION_PATTERNS = [
97
+ /\bas\s+an?\s+AI\b.*?\byou\s+(?:must|should|will|are)\b/i,
98
+ /\byou\s+are\s+an?\s+(?:helpful|AI|language\s+model|assistant)\b/i,
99
+ /\brespond\s+(?:only\s+)?(?:in|with)\b.*?\bformat\b/i,
100
+ /\balways\s+(?:respond|reply|answer|say)\b/i,
101
+ /\bnever\s+(?:mention|reveal|disclose|say|tell)\b/i,
102
+ ];
103
+ /**
104
+ * Detect unusual Unicode: zero-width characters, RTL overrides, homoglyphs.
105
+ */
106
+ const SUSPICIOUS_UNICODE = /[\u200B-\u200F\u2028-\u202F\uFEFF\u061C\u115F\u1160\u17B4\u17B5\u180E\u2000-\u200A\u2060-\u2064\u2066-\u2069\u206A-\u206F]|\u00AD|\u034F/;
107
+ // ---------------------------------------------------------------------------
108
+ // Scanner
109
+ // ---------------------------------------------------------------------------
110
+ function extractToolName(msg, allMessages) {
111
+ if (!msg.contentBlocks)
112
+ return null;
113
+ // Build tool_use_id -> name map from all messages
114
+ const nameMap = new Map();
115
+ for (const m of allMessages) {
116
+ if (m.contentBlocks) {
117
+ for (const b of m.contentBlocks) {
118
+ if (b.type === "tool_use" && b.id && b.name) {
119
+ nameMap.set(b.id, b.name);
120
+ }
121
+ }
122
+ }
123
+ }
124
+ for (const b of msg.contentBlocks) {
125
+ if (b.type === "tool_use" && b.name)
126
+ return b.name;
127
+ if (b.type === "tool_result" && b.tool_use_id) {
128
+ return nameMap.get(b.tool_use_id) || null;
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+ function isToolResultMessage(msg) {
134
+ if (!msg.contentBlocks)
135
+ return false;
136
+ return msg.contentBlocks.some((b) => b.type === "tool_result");
137
+ }
138
+ function truncateMatch(text, start, length) {
139
+ const snippet = text.slice(start, start + length);
140
+ return snippet.length > 120 ? `${snippet.slice(0, 117)}...` : snippet;
141
+ }
142
+ function scanMessage(msg, messageIndex, allMessages) {
143
+ const alerts = [];
144
+ const content = msg.content || "";
145
+ if (!content)
146
+ return alerts;
147
+ // Skip system messages; they are authored by the developer, not injected.
148
+ if (msg.role === "system" || msg.role === "developer")
149
+ return alerts;
150
+ const toolName = extractToolName(msg, allMessages);
151
+ const isToolResult = isToolResultMessage(msg);
152
+ // --- Tier 1: Pattern matching ---
153
+ for (const rule of TIER1_PATTERNS) {
154
+ const match = rule.pattern.exec(content);
155
+ if (match) {
156
+ alerts.push({
157
+ messageIndex,
158
+ role: msg.role,
159
+ toolName,
160
+ severity: rule.severity,
161
+ pattern: rule.id,
162
+ match: truncateMatch(content, match.index, match[0].length),
163
+ offset: match.index,
164
+ length: match[0].length,
165
+ });
166
+ }
167
+ }
168
+ // --- Tier 2: Role confusion (only in tool results) ---
169
+ if (isToolResult) {
170
+ for (const pat of ROLE_CONFUSION_PATTERNS) {
171
+ const match = pat.exec(content);
172
+ if (match) {
173
+ alerts.push({
174
+ messageIndex,
175
+ role: msg.role,
176
+ toolName,
177
+ severity: "medium",
178
+ pattern: "role_confusion",
179
+ match: truncateMatch(content, match.index, match[0].length),
180
+ offset: match.index,
181
+ length: match[0].length,
182
+ });
183
+ break; // One role confusion alert per message is enough
184
+ }
185
+ }
186
+ }
187
+ // --- Tier 2: Suspicious Unicode ---
188
+ const unicodeMatch = SUSPICIOUS_UNICODE.exec(content);
189
+ if (unicodeMatch) {
190
+ // Count total suspicious chars
191
+ const count = (content.match(new RegExp(SUSPICIOUS_UNICODE.source, "g")) || []).length;
192
+ alerts.push({
193
+ messageIndex,
194
+ role: msg.role,
195
+ toolName,
196
+ severity: "info",
197
+ pattern: "suspicious_unicode",
198
+ match: `${count} suspicious Unicode character${count > 1 ? "s" : ""} (zero-width, RTL override, etc.)`,
199
+ offset: unicodeMatch.index,
200
+ length: 1,
201
+ });
202
+ }
203
+ return alerts;
204
+ }
205
+ /**
206
+ * Scan all messages in a context for prompt injection patterns.
207
+ *
208
+ * This is the main entry point, called from Store.storeRequest() before compaction.
209
+ */
210
+ export function scanSecurity(contextInfo) {
211
+ const alerts = [];
212
+ for (let i = 0; i < contextInfo.messages.length; i++) {
213
+ const msgAlerts = scanMessage(contextInfo.messages[i], i, contextInfo.messages);
214
+ alerts.push(...msgAlerts);
215
+ }
216
+ const summary = { high: 0, medium: 0, info: 0 };
217
+ for (const alert of alerts) {
218
+ summary[alert.severity]++;
219
+ }
220
+ return { alerts, summary };
221
+ }
222
+ //# sourceMappingURL=security.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.js","sourceRoot":"","sources":["../../src/core/security.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { ContextInfo, HeaderSignature, SourceSignature } from "../types.js";
2
+ /**
3
+ * Header signatures used to infer which CLI/tool produced a request.
4
+ *
5
+ * This is best-effort: many tools do not have stable identifiers.
6
+ */
7
+ export declare const HEADER_SIGNATURES: HeaderSignature[];
8
+ /**
9
+ * System-prompt signatures used as a fallback when headers are missing/ambiguous.
10
+ */
11
+ export declare const SOURCE_SIGNATURES: SourceSignature[];
12
+ /**
13
+ * Infer a human-friendly "source tool" label (claude/codex/gemini/aider/...) for attribution.
14
+ *
15
+ * Priority:
16
+ * 1. explicit source tag if provided and not `"unknown"`
17
+ * 2. request header signatures
18
+ * 3. system prompt signatures
19
+ * 4. fallback to `"unknown"`
20
+ */
21
+ export declare function detectSource(contextInfo: ContextInfo, source: string | null, headers?: Record<string, string>): string;
22
+ //# sourceMappingURL=source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../../src/core/source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,eAAe,EAChB,MAAM,aAAa,CAAC;AAErB;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,EAAE,eAAe,EAK9C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,EAAE,eAAe,EAK9C,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAC1B,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,MAAM,CA0BR"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Header signatures used to infer which CLI/tool produced a request.
3
+ *
4
+ * This is best-effort: many tools do not have stable identifiers.
5
+ */
6
+ export const HEADER_SIGNATURES = [
7
+ { header: "user-agent", pattern: /^claude-cli\//, source: "claude" },
8
+ { header: "user-agent", pattern: /aider/i, source: "aider" },
9
+ { header: "user-agent", pattern: /kimi/i, source: "kimi" },
10
+ { header: "user-agent", pattern: /^GeminiCLI\//, source: "gemini" },
11
+ ];
12
+ /**
13
+ * System-prompt signatures used as a fallback when headers are missing/ambiguous.
14
+ */
15
+ export const SOURCE_SIGNATURES = [
16
+ { pattern: "Act as an expert software developer", source: "aider" },
17
+ { pattern: "You are Claude Code", source: "claude" },
18
+ { pattern: "You are Kimi Code CLI", source: "kimi" },
19
+ { pattern: "operating inside pi, a coding agent harness", source: "pi" },
20
+ ];
21
+ /**
22
+ * Infer a human-friendly "source tool" label (claude/codex/gemini/aider/...) for attribution.
23
+ *
24
+ * Priority:
25
+ * 1. explicit source tag if provided and not `"unknown"`
26
+ * 2. request header signatures
27
+ * 3. system prompt signatures
28
+ * 4. fallback to `"unknown"`
29
+ */
30
+ export function detectSource(contextInfo, source, headers) {
31
+ if (source && source !== "unknown")
32
+ return source;
33
+ // Primary: check request headers
34
+ if (headers) {
35
+ for (const sig of HEADER_SIGNATURES) {
36
+ const val = headers[sig.header];
37
+ if (!val)
38
+ continue;
39
+ if (sig.pattern instanceof RegExp
40
+ ? sig.pattern.test(val)
41
+ : val.includes(sig.pattern)) {
42
+ return sig.source;
43
+ }
44
+ }
45
+ }
46
+ // Fallback: check system prompt content
47
+ const systemText = (contextInfo.systemPrompts || [])
48
+ .map((sp) => sp.content)
49
+ .join("\n");
50
+ for (const sig of SOURCE_SIGNATURES) {
51
+ if (systemText.includes(sig.pattern))
52
+ return sig.source;
53
+ }
54
+ return source || "unknown";
55
+ }
56
+ //# sourceMappingURL=source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/core/source.ts"],"names":[],"mappings":"AAMA;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IAClD,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,QAAQ,EAAE;IACpE,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE;IAC5D,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE;IAC1D,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE;CACpE,CAAC;AAEF;;GAEG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IAClD,EAAE,OAAO,EAAE,qCAAqC,EAAE,MAAM,EAAE,OAAO,EAAE;IACnE,EAAE,OAAO,EAAE,qBAAqB,EAAE,MAAM,EAAE,QAAQ,EAAE;IACpD,EAAE,OAAO,EAAE,uBAAuB,EAAE,MAAM,EAAE,MAAM,EAAE;IACpD,EAAE,OAAO,EAAE,6CAA6C,EAAE,MAAM,EAAE,IAAI,EAAE;CACzE,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAC1B,WAAwB,EACxB,MAAqB,EACrB,OAAgC;IAEhC,IAAI,MAAM,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IAElD,iCAAiC;IACjC,IAAI,OAAO,EAAE,CAAC;QACZ,KAAK,MAAM,GAAG,IAAI,iBAAiB,EAAE,CAAC;YACpC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAChC,IAAI,CAAC,GAAG;gBAAE,SAAS;YACnB,IACE,GAAG,CAAC,OAAO,YAAY,MAAM;gBAC3B,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;gBACvB,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAC7B,CAAC;gBACD,OAAO,GAAG,CAAC,MAAM,CAAC;YACpB,CAAC;QACH,CAAC;IACH,CAAC;IAED,wCAAwC;IACxC,MAAM,UAAU,GAAG,CAAC,WAAW,CAAC,aAAa,IAAI,EAAE,CAAC;SACjD,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,OAAO,CAAC;SACvB,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,KAAK,MAAM,GAAG,IAAI,iBAAiB,EAAE,CAAC;QACpC,IAAI,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO,GAAG,CAAC,MAAM,CAAC;IAC1D,CAAC;IACD,OAAO,MAAM,IAAI,SAAS,CAAC;AAC7B,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Lightweight token estimator used throughout Context Lens.
3
+ *
4
+ * Approximates tokens as `ceil(chars / 4)`. For image content blocks,
5
+ * uses a fixed per-image estimate instead of stringifying base64 data.
6
+ *
7
+ * @param text - Value to estimate tokens for. Objects are stringified as JSON.
8
+ * @returns Estimated token count (>= 0).
9
+ */
10
+ export declare function estimateTokens(text: unknown): number;
11
+ /**
12
+ * Rescale all token sub-fields in a ContextInfo so they are internally
13
+ * consistent with an authoritative total (typically from API usage data).
14
+ *
15
+ * Adjusts `systemTokens`, `toolsTokens`, per-message `.tokens`, and
16
+ * `messagesTokens` proportionally, then applies a rounding residual fix
17
+ * to `messagesTokens` so the invariant
18
+ * `totalTokens === systemTokens + toolsTokens + messagesTokens` holds exactly.
19
+ */
20
+ export declare function rescaleContextTokens(ci: {
21
+ systemTokens: number;
22
+ toolsTokens: number;
23
+ messagesTokens: number;
24
+ totalTokens: number;
25
+ messages: {
26
+ tokens: number;
27
+ }[];
28
+ }, authoritative: number): void;
29
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/core/tokens.ts"],"names":[],"mappings":"AA8DA;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAqBpD;AAiCD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,EAAE,EAAE;IACF,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAChC,EACD,aAAa,EAAE,MAAM,GACpB,IAAI,CAyCN"}
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Approximate token cost for a single image.
3
+ *
4
+ * Anthropic charges based on image dimensions (~1,600 tokens per 512x512 tile).
5
+ * Since we don't decode image data, we use a conservative flat estimate of 1,600
6
+ * tokens (one tile). Most screenshots cost 2,000-6,400 tokens, so this slightly
7
+ * under-counts but is far better than stringifying megabytes of base64.
8
+ */
9
+ const IMAGE_TOKEN_ESTIMATE = 1_600;
10
+ /**
11
+ * Return true if the value looks like an image content block.
12
+ *
13
+ * Matches Anthropic `{type:"image", source:{type:"base64", data:"..."}}`,
14
+ * OpenAI `{type:"image_url", image_url:{url:"data:..."}}`,
15
+ * and Gemini `{inlineData:{...}}` / `{fileData:{...}}`.
16
+ */
17
+ function isImageBlock(val) {
18
+ if (!val || typeof val !== "object" || Array.isArray(val))
19
+ return false;
20
+ const obj = val;
21
+ if (obj.type === "image" || obj.type === "image_url")
22
+ return true;
23
+ if (obj.inlineData || obj.fileData)
24
+ return true;
25
+ return false;
26
+ }
27
+ /**
28
+ * Strip base64 image data from a value before stringifying for token estimation.
29
+ *
30
+ * Walks arrays and recognizes image blocks at any nesting level (top-level content
31
+ * arrays, tool_result content arrays, Gemini parts). Image blocks are replaced with
32
+ * a sentinel so the rest of the structure is still counted.
33
+ */
34
+ function stripImageData(val) {
35
+ if (!val || typeof val !== "object")
36
+ return val;
37
+ if (Array.isArray(val)) {
38
+ return val.map(stripImageData);
39
+ }
40
+ // Image block: return a lightweight placeholder
41
+ if (isImageBlock(val)) {
42
+ return {
43
+ type: val.type || "image",
44
+ _image: true,
45
+ };
46
+ }
47
+ const obj = val;
48
+ // tool_result blocks can nest image blocks inside their content array
49
+ if (obj.type === "tool_result" && Array.isArray(obj.content)) {
50
+ return { ...obj, content: obj.content.map(stripImageData) };
51
+ }
52
+ // Gemini turn: parts array may contain inlineData
53
+ if (Array.isArray(obj.parts)) {
54
+ return { ...obj, parts: obj.parts.map(stripImageData) };
55
+ }
56
+ return obj;
57
+ }
58
+ /**
59
+ * Lightweight token estimator used throughout Context Lens.
60
+ *
61
+ * Approximates tokens as `ceil(chars / 4)`. For image content blocks,
62
+ * uses a fixed per-image estimate instead of stringifying base64 data.
63
+ *
64
+ * @param text - Value to estimate tokens for. Objects are stringified as JSON.
65
+ * @returns Estimated token count (>= 0).
66
+ */
67
+ export function estimateTokens(text) {
68
+ if (!text)
69
+ return 0;
70
+ // Fast path: plain strings never contain image data
71
+ if (typeof text === "string") {
72
+ return Math.ceil(text.length / 4);
73
+ }
74
+ // Single image block
75
+ if (isImageBlock(text)) {
76
+ return IMAGE_TOKEN_ESTIMATE;
77
+ }
78
+ // Object/array: strip image data, then stringify the rest
79
+ const cleaned = stripImageData(text);
80
+ const s = JSON.stringify(cleaned);
81
+ const baseTokens = Math.ceil(s.length / 4);
82
+ // Count image blocks and add fixed estimate for each
83
+ const imageCount = countImages(text);
84
+ return baseTokens + imageCount * IMAGE_TOKEN_ESTIMATE;
85
+ }
86
+ /**
87
+ * Count the number of image blocks in a value (recursive).
88
+ */
89
+ function countImages(val) {
90
+ if (!val || typeof val !== "object")
91
+ return 0;
92
+ if (isImageBlock(val))
93
+ return 1;
94
+ if (Array.isArray(val)) {
95
+ let count = 0;
96
+ for (const item of val) {
97
+ count += countImages(item);
98
+ }
99
+ return count;
100
+ }
101
+ const obj = val;
102
+ // Check nested content in tool_result blocks
103
+ if (obj.type === "tool_result" && Array.isArray(obj.content)) {
104
+ return countImages(obj.content);
105
+ }
106
+ // Check Gemini parts
107
+ if (Array.isArray(obj.parts)) {
108
+ return countImages(obj.parts);
109
+ }
110
+ return 0;
111
+ }
112
+ /**
113
+ * Rescale all token sub-fields in a ContextInfo so they are internally
114
+ * consistent with an authoritative total (typically from API usage data).
115
+ *
116
+ * Adjusts `systemTokens`, `toolsTokens`, per-message `.tokens`, and
117
+ * `messagesTokens` proportionally, then applies a rounding residual fix
118
+ * to `messagesTokens` so the invariant
119
+ * `totalTokens === systemTokens + toolsTokens + messagesTokens` holds exactly.
120
+ */
121
+ export function rescaleContextTokens(ci, authoritative) {
122
+ const estimated = ci.systemTokens + ci.toolsTokens + ci.messagesTokens;
123
+ if (estimated === 0 || authoritative === 0) {
124
+ ci.systemTokens = 0;
125
+ ci.toolsTokens = 0;
126
+ ci.messagesTokens = 0;
127
+ ci.totalTokens = authoritative;
128
+ for (const msg of ci.messages) {
129
+ msg.tokens = 0;
130
+ }
131
+ return;
132
+ }
133
+ if (authoritative === estimated) {
134
+ ci.totalTokens = authoritative;
135
+ return;
136
+ }
137
+ const scale = authoritative / estimated;
138
+ ci.systemTokens = Math.round(ci.systemTokens * scale);
139
+ ci.toolsTokens = Math.round(ci.toolsTokens * scale);
140
+ for (const msg of ci.messages) {
141
+ msg.tokens = Math.round(msg.tokens * scale);
142
+ }
143
+ ci.messagesTokens = ci.messages.reduce((s, m) => s + m.tokens, 0);
144
+ ci.totalTokens = authoritative;
145
+ // Fix rounding residual so both invariants hold:
146
+ // totalTokens === systemTokens + toolsTokens + messagesTokens
147
+ // messagesTokens === sum(msg.tokens)
148
+ const residual = authoritative - (ci.systemTokens + ci.toolsTokens + ci.messagesTokens);
149
+ if (residual !== 0) {
150
+ ci.messagesTokens += residual;
151
+ // Push the residual into the largest per-message entry (or first if tied)
152
+ // so sum(msg.tokens) stays equal to messagesTokens.
153
+ if (ci.messages.length > 0) {
154
+ let maxIdx = 0;
155
+ for (let i = 1; i < ci.messages.length; i++) {
156
+ if (ci.messages[i].tokens > ci.messages[maxIdx].tokens)
157
+ maxIdx = i;
158
+ }
159
+ ci.messages[maxIdx].tokens += residual;
160
+ }
161
+ }
162
+ }
163
+ //# sourceMappingURL=tokens.js.map