smartcontext-proxy 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/PLAN-v2.md +390 -0
  2. package/dist/src/context/ab-test.d.ts +32 -0
  3. package/dist/src/context/ab-test.js +133 -0
  4. package/dist/src/index.js +99 -78
  5. package/dist/src/proxy/classifier.d.ts +14 -0
  6. package/dist/src/proxy/classifier.js +63 -0
  7. package/dist/src/proxy/connect-proxy.d.ts +37 -0
  8. package/dist/src/proxy/connect-proxy.js +234 -0
  9. package/dist/src/proxy/server.js +10 -1
  10. package/dist/src/proxy/tls-interceptor.d.ts +23 -0
  11. package/dist/src/proxy/tls-interceptor.js +211 -0
  12. package/dist/src/proxy/transparent-listener.d.ts +31 -0
  13. package/dist/src/proxy/transparent-listener.js +285 -0
  14. package/dist/src/proxy/tunnel.d.ts +7 -0
  15. package/dist/src/proxy/tunnel.js +33 -0
  16. package/dist/src/system/dns-redirect.d.ts +28 -0
  17. package/dist/src/system/dns-redirect.js +141 -0
  18. package/dist/src/system/installer.d.ts +25 -0
  19. package/dist/src/system/installer.js +180 -0
  20. package/dist/src/system/linux.d.ts +11 -0
  21. package/dist/src/system/linux.js +60 -0
  22. package/dist/src/system/macos.d.ts +24 -0
  23. package/dist/src/system/macos.js +98 -0
  24. package/dist/src/system/pf-redirect.d.ts +25 -0
  25. package/dist/src/system/pf-redirect.js +177 -0
  26. package/dist/src/system/watchdog.d.ts +7 -0
  27. package/dist/src/system/watchdog.js +115 -0
  28. package/dist/src/test/connect-proxy.test.d.ts +1 -0
  29. package/dist/src/test/connect-proxy.test.js +147 -0
  30. package/dist/src/test/dashboard.test.js +1 -0
  31. package/dist/src/tls/ca-manager.d.ts +9 -0
  32. package/dist/src/tls/ca-manager.js +117 -0
  33. package/dist/src/tls/trust-store.d.ts +11 -0
  34. package/dist/src/tls/trust-store.js +121 -0
  35. package/dist/src/tray/bridge.d.ts +8 -0
  36. package/dist/src/tray/bridge.js +66 -0
  37. package/dist/src/ui/dashboard.d.ts +10 -1
  38. package/dist/src/ui/dashboard.js +119 -34
  39. package/dist/src/ui/ws-feed.d.ts +8 -0
  40. package/dist/src/ui/ws-feed.js +30 -0
  41. package/native/macos/SmartContextTray/Package.swift +13 -0
  42. package/native/macos/SmartContextTray/Sources/main.swift +206 -0
  43. package/package.json +6 -2
  44. package/src/context/ab-test.ts +172 -0
  45. package/src/index.ts +104 -74
  46. package/src/proxy/classifier.ts +71 -0
  47. package/src/proxy/connect-proxy.ts +251 -0
  48. package/src/proxy/server.ts +11 -2
  49. package/src/proxy/tls-interceptor.ts +261 -0
  50. package/src/proxy/transparent-listener.ts +328 -0
  51. package/src/proxy/tunnel.ts +32 -0
  52. package/src/system/dns-redirect.ts +144 -0
  53. package/src/system/installer.ts +148 -0
  54. package/src/system/linux.ts +57 -0
  55. package/src/system/macos.ts +89 -0
  56. package/src/system/pf-redirect.ts +175 -0
  57. package/src/system/watchdog.ts +76 -0
  58. package/src/test/connect-proxy.test.ts +170 -0
  59. package/src/test/dashboard.test.ts +1 -0
  60. package/src/tls/ca-manager.ts +140 -0
  61. package/src/tls/trust-store.ts +123 -0
  62. package/src/tray/bridge.ts +61 -0
  63. package/src/ui/dashboard.ts +129 -35
  64. package/src/ui/ws-feed.ts +32 -0
@@ -0,0 +1,206 @@
1
+ import AppKit
2
+ import Foundation
3
+
4
+ // MARK: - Status Polling
5
+
6
+ struct ProxyStatus: Codable {
7
+ let state: String
8
+ let uptime: Int
9
+ let requests: Int
10
+ let mode: String
11
+ }
12
+
13
+ struct ProxyStats: Codable {
14
+ let totalRequests: Int
15
+ let totalOriginalTokens: Int
16
+ let totalOptimizedTokens: Int
17
+ let totalSavingsPercent: Int
18
+ let avgLatencyOverheadMs: Int
19
+ }
20
+
21
+ class StatusPoller {
22
+ var status: ProxyStatus?
23
+ var stats: ProxyStats?
24
+ let port: Int
25
+
26
+ init(port: Int = 4800) {
27
+ self.port = port
28
+ }
29
+
30
+ func poll() {
31
+ fetchJSON("http://127.0.0.1:\(port)/_sc/status") { (result: ProxyStatus?) in
32
+ self.status = result
33
+ }
34
+ fetchJSON("http://127.0.0.1:\(port)/_sc/stats") { (result: ProxyStats?) in
35
+ self.stats = result
36
+ }
37
+ }
38
+
39
+ private func fetchJSON<T: Codable>(_ urlString: String, completion: @escaping (T?) -> Void) {
40
+ guard let url = URL(string: urlString) else { completion(nil); return }
41
+ let task = URLSession.shared.dataTask(with: url) { data, _, _ in
42
+ guard let data = data else { completion(nil); return }
43
+ completion(try? JSONDecoder().decode(T.self, from: data))
44
+ }
45
+ task.resume()
46
+ }
47
+
48
+ func apiCall(_ path: String) {
49
+ guard let url = URL(string: "http://127.0.0.1:\(port)\(path)") else { return }
50
+ var request = URLRequest(url: url)
51
+ request.httpMethod = "POST"
52
+ URLSession.shared.dataTask(with: request).resume()
53
+ }
54
+ }
55
+
56
+ // MARK: - App Delegate
57
+
58
+ class TrayAppDelegate: NSObject, NSApplicationDelegate {
59
+ var statusItem: NSStatusItem!
60
+ var poller = StatusPoller()
61
+ var pollTimer: Timer?
62
+
63
+ func applicationDidFinishLaunching(_ notification: Notification) {
64
+ statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
65
+
66
+ updateIcon(state: "starting")
67
+ buildMenu()
68
+
69
+ // Poll every 3 seconds
70
+ poller.poll()
71
+ pollTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in
72
+ self?.poller.poll()
73
+ DispatchQueue.main.async { self?.updateMenu() }
74
+ }
75
+ }
76
+
77
+ func updateIcon(state: String) {
78
+ guard let button = statusItem.button else { return }
79
+
80
+ switch state {
81
+ case "running":
82
+ button.title = "◉"
83
+ button.contentTintColor = .systemGreen
84
+ case "paused":
85
+ button.title = "◉"
86
+ button.contentTintColor = .systemYellow
87
+ default:
88
+ button.title = "◎"
89
+ button.contentTintColor = .systemGray
90
+ }
91
+ }
92
+
93
+ func buildMenu() {
94
+ let menu = NSMenu()
95
+
96
+ // Header
97
+ let headerItem = NSMenuItem(title: "SmartContext Proxy", action: nil, keyEquivalent: "")
98
+ headerItem.isEnabled = false
99
+ menu.addItem(headerItem)
100
+
101
+ menu.addItem(NSMenuItem.separator())
102
+
103
+ // Stats (placeholder, updated by polling)
104
+ let statsItem = NSMenuItem(title: "Loading...", action: nil, keyEquivalent: "")
105
+ statsItem.tag = 100
106
+ statsItem.isEnabled = false
107
+ menu.addItem(statsItem)
108
+
109
+ let savingsItem = NSMenuItem(title: "", action: nil, keyEquivalent: "")
110
+ savingsItem.tag = 101
111
+ savingsItem.isEnabled = false
112
+ menu.addItem(savingsItem)
113
+
114
+ menu.addItem(NSMenuItem.separator())
115
+
116
+ // Dashboard
117
+ let dashboardItem = NSMenuItem(title: "Open Dashboard", action: #selector(openDashboard), keyEquivalent: "d")
118
+ dashboardItem.target = self
119
+ menu.addItem(dashboardItem)
120
+
121
+ menu.addItem(NSMenuItem.separator())
122
+
123
+ // Pause/Resume
124
+ let pauseItem = NSMenuItem(title: "Pause Optimization", action: #selector(togglePause), keyEquivalent: "p")
125
+ pauseItem.tag = 200
126
+ pauseItem.target = self
127
+ menu.addItem(pauseItem)
128
+
129
+ menu.addItem(NSMenuItem.separator())
130
+
131
+ // Quit
132
+ let quitItem = NSMenuItem(title: "Quit SmartContext", action: #selector(quit), keyEquivalent: "q")
133
+ quitItem.target = self
134
+ menu.addItem(quitItem)
135
+
136
+ statusItem.menu = menu
137
+ }
138
+
139
+ func updateMenu() {
140
+ guard let menu = statusItem.menu else { return }
141
+
142
+ let state = poller.status?.state ?? "offline"
143
+ updateIcon(state: state)
144
+
145
+ // Update stats
146
+ if let stats = poller.stats {
147
+ let savings = estimateCost(stats.totalOriginalTokens - stats.totalOptimizedTokens)
148
+ if let item = menu.item(withTag: 100) {
149
+ item.title = "Requests: \(stats.totalRequests) | Savings: \(stats.totalSavingsPercent)%"
150
+ }
151
+ if let item = menu.item(withTag: 101) {
152
+ item.title = "Estimated saved: $\(savings)"
153
+ }
154
+ } else {
155
+ if let item = menu.item(withTag: 100) {
156
+ item.title = "Proxy offline"
157
+ }
158
+ }
159
+
160
+ // Update pause/resume text
161
+ if let item = menu.item(withTag: 200) {
162
+ if state == "paused" {
163
+ item.title = "Resume Optimization"
164
+ } else {
165
+ item.title = "Pause Optimization"
166
+ }
167
+ }
168
+ }
169
+
170
+ func estimateCost(_ tokensSaved: Int) -> String {
171
+ let cost = Double(tokensSaved) / 1_000_000.0 * 15.0
172
+ return String(format: "%.2f", cost)
173
+ }
174
+
175
+ @objc func openDashboard() {
176
+ if let url = URL(string: "http://localhost:\(poller.port)/") {
177
+ NSWorkspace.shared.open(url)
178
+ }
179
+ }
180
+
181
+ @objc func togglePause() {
182
+ let state = poller.status?.state ?? "running"
183
+ if state == "paused" {
184
+ poller.apiCall("/_sc/resume")
185
+ } else {
186
+ poller.apiCall("/_sc/pause")
187
+ }
188
+ // Update after short delay
189
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
190
+ self?.poller.poll()
191
+ self?.updateMenu()
192
+ }
193
+ }
194
+
195
+ @objc func quit() {
196
+ NSApplication.shared.terminate(self)
197
+ }
198
+ }
199
+
200
+ // MARK: - Entry Point
201
+
202
+ let app = NSApplication.shared
203
+ app.setActivationPolicy(.accessory) // No dock icon
204
+ let delegate = TrayAppDelegate()
205
+ app.delegate = delegate
206
+ app.run()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smartcontext-proxy",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Intelligent context window optimization proxy for LLM APIs",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -30,9 +30,13 @@
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^22.0.0",
33
+ "@types/node-forge": "^1.3.14",
34
+ "@types/ws": "^8.18.1",
33
35
  "typescript": "^5.7.0"
34
36
  },
35
37
  "dependencies": {
36
- "@lancedb/lancedb": "^0.27.1"
38
+ "@lancedb/lancedb": "^0.27.1",
39
+ "node-forge": "^1.4.0",
40
+ "ws": "^8.20.0"
37
41
  }
38
42
  }
@@ -0,0 +1,172 @@
1
+ import type { CanonicalRequest } from './canonical.js';
2
+ import type { ProviderAdapter } from '../providers/types.js';
3
+ import http from 'node:http';
4
+ import https from 'node:https';
5
+ import { URL } from 'node:url';
6
+
7
+ export interface ABComparison {
8
+ requestId: number;
9
+ timestamp: number;
10
+ provider: string;
11
+ model: string;
12
+ optimizedTokens: number;
13
+ originalTokens: number;
14
+ savingsPercent: number;
15
+ semanticSimilarity: number;
16
+ qualityMatch: boolean; // >0.95 similarity
17
+ responseA: string; // optimized (returned to client)
18
+ responseB: string; // original (stored, not returned)
19
+ latencyA: number;
20
+ latencyB: number;
21
+ }
22
+
23
+ let abResults: ABComparison[] = [];
24
+ let abEnabled = false;
25
+ let abSampleRate = 100; // percent
26
+
27
+ export function enableABTest(sampleRate: number = 100): void {
28
+ abEnabled = true;
29
+ abSampleRate = Math.min(100, Math.max(1, sampleRate));
30
+ }
31
+
32
+ export function disableABTest(): void {
33
+ abEnabled = false;
34
+ }
35
+
36
+ export function isABEnabled(): boolean {
37
+ return abEnabled;
38
+ }
39
+
40
+ export function shouldABTest(): boolean {
41
+ if (!abEnabled) return false;
42
+ return Math.random() * 100 < abSampleRate;
43
+ }
44
+
45
+ export function getABResults(limit: number = 50): ABComparison[] {
46
+ return abResults.slice(-limit);
47
+ }
48
+
49
+ export function getABSummary(): {
50
+ total: number;
51
+ qualityMatch: number;
52
+ minorDiff: number;
53
+ significantDiff: number;
54
+ avgSavings: number;
55
+ } {
56
+ const total = abResults.length;
57
+ if (total === 0) return { total: 0, qualityMatch: 0, minorDiff: 0, significantDiff: 0, avgSavings: 0 };
58
+
59
+ let qualityMatch = 0;
60
+ let minorDiff = 0;
61
+ let significantDiff = 0;
62
+ let totalSavings = 0;
63
+
64
+ for (const r of abResults) {
65
+ if (r.semanticSimilarity >= 0.95) qualityMatch++;
66
+ else if (r.semanticSimilarity >= 0.85) minorDiff++;
67
+ else significantDiff++;
68
+ totalSavings += r.savingsPercent;
69
+ }
70
+
71
+ return {
72
+ total,
73
+ qualityMatch,
74
+ minorDiff,
75
+ significantDiff,
76
+ avgSavings: Math.round(totalSavings / total),
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Send the original (unoptimized) request to the provider for comparison.
82
+ * This is Path B — the result is stored but NOT returned to the client.
83
+ */
84
+ export async function sendOriginalForComparison(
85
+ originalBody: Buffer,
86
+ hostname: string,
87
+ port: number,
88
+ path: string,
89
+ headers: Record<string, string>,
90
+ requestId: number,
91
+ provider: string,
92
+ model: string,
93
+ optimizedResponse: string,
94
+ optimizedTokens: number,
95
+ originalTokens: number,
96
+ savingsPercent: number,
97
+ ): Promise<void> {
98
+ const startTime = Date.now();
99
+
100
+ try {
101
+ const useHTTPS = port === 443 || hostname.includes('.');
102
+ const transport = useHTTPS ? https : http;
103
+ const url = `${useHTTPS ? 'https' : 'http'}://${hostname}:${port}${path}`;
104
+
105
+ const forwardHeaders = { ...headers };
106
+ forwardHeaders['content-length'] = String(originalBody.length);
107
+
108
+ const responseB = await new Promise<string>((resolve, reject) => {
109
+ const req = transport.request(url, { method: 'POST', headers: forwardHeaders }, (res) => {
110
+ let data = '';
111
+ res.on('data', (chunk) => (data += chunk));
112
+ res.on('end', () => resolve(data));
113
+ });
114
+ req.on('error', reject);
115
+ req.write(originalBody);
116
+ req.end();
117
+ });
118
+
119
+ const latencyB = Date.now() - startTime;
120
+
121
+ // Simple text similarity (cosine of character trigrams)
122
+ const similarity = textSimilarity(optimizedResponse, responseB);
123
+
124
+ abResults.push({
125
+ requestId,
126
+ timestamp: Date.now(),
127
+ provider,
128
+ model,
129
+ optimizedTokens,
130
+ originalTokens,
131
+ savingsPercent,
132
+ semanticSimilarity: similarity,
133
+ qualityMatch: similarity >= 0.95,
134
+ responseA: optimizedResponse.slice(0, 500),
135
+ responseB: responseB.slice(0, 500),
136
+ latencyA: 0, // filled externally
137
+ latencyB,
138
+ });
139
+
140
+ // Keep max 1000 results
141
+ if (abResults.length > 1000) {
142
+ abResults = abResults.slice(-500);
143
+ }
144
+ } catch {
145
+ // A/B test failure is non-critical
146
+ }
147
+ }
148
+
149
+ /** Simple text similarity using character trigrams */
150
+ function textSimilarity(a: string, b: string): number {
151
+ if (a === b) return 1.0;
152
+ if (!a || !b) return 0;
153
+
154
+ const trigramsA = getTrigrams(a.toLowerCase());
155
+ const trigramsB = getTrigrams(b.toLowerCase());
156
+
157
+ let intersection = 0;
158
+ for (const t of trigramsA) {
159
+ if (trigramsB.has(t)) intersection++;
160
+ }
161
+
162
+ const union = trigramsA.size + trigramsB.size - intersection;
163
+ return union === 0 ? 0 : intersection / union;
164
+ }
165
+
166
+ function getTrigrams(s: string): Set<string> {
167
+ const set = new Set<string>();
168
+ for (let i = 0; i < s.length - 2; i++) {
169
+ set.add(s.substring(i, i + 3));
170
+ }
171
+ return set;
172
+ }
package/src/index.ts CHANGED
@@ -2,29 +2,38 @@
2
2
 
3
3
  import { buildConfig } from './config/auto-detect.js';
4
4
  import { ProxyServer } from './proxy/server.js';
5
+ import { ConnectProxy } from './proxy/connect-proxy.js';
5
6
  import { OllamaEmbeddingAdapter } from './embedding/ollama.js';
6
7
  import { LanceDBAdapter } from './storage/lancedb.js';
8
+ import { ContextOptimizer } from './context/optimizer.js';
7
9
  import type { EmbeddingAdapter } from './embedding/types.js';
8
10
  import type { StorageAdapter } from './storage/types.js';
9
- import { writePid, removePid, getPid, stopDaemon, startDaemon, isDaemonChild } from './daemon/process.js';
10
- import { installService, uninstallService } from './daemon/service.js';
11
+ import type { ProviderAdapter } from './providers/types.js';
12
+ import { AnthropicAdapter } from './providers/anthropic.js';
13
+ import { OpenAIAdapter } from './providers/openai.js';
14
+ import { OllamaAdapter } from './providers/ollama.js';
15
+ import { GoogleAdapter } from './providers/google.js';
16
+ import { ensureCA } from './tls/ca-manager.js';
17
+ import { writePid, removePid, getPid, stopDaemon } from './daemon/process.js';
18
+ import { install, uninstall, status as installStatus } from './system/installer.js';
11
19
  import http from 'node:http';
12
20
 
13
- const VERSION = '0.1.0';
21
+ const VERSION = '0.2.0';
14
22
 
15
23
  function parseArgs(args: string[]): Record<string, string | boolean> {
16
24
  const result: Record<string, string | boolean> = {};
17
25
  for (let i = 0; i < args.length; i++) {
18
26
  const arg = args[i];
19
27
  if (arg === '--port' || arg === '-p') result.port = args[++i];
20
- else if (arg === '--config' || arg === '-c') result.config = args[++i];
21
28
  else if (arg === '--help' || arg === '-h') result.help = true;
22
29
  else if (arg === '--version' || arg === '-v') result.version = true;
23
30
  else if (arg === '--no-optimize') result.noOptimize = true;
31
+ else if (arg === '--legacy') result.legacy = true;
32
+ else if (arg === '--purge') result.purge = true;
24
33
  else if (arg === '--embedding-url') result.embeddingUrl = args[++i];
25
34
  else if (arg === '--embedding-model') result.embeddingModel = args[++i];
26
35
  else if (arg === '--data-dir') result.dataDir = args[++i];
27
- else if (!arg.startsWith('-')) result.command = arg;
36
+ else if (!arg.startsWith('-')) result.command = result.command ? result.command : arg;
28
37
  }
29
38
  return result;
30
39
  }
@@ -32,56 +41,52 @@ function parseArgs(args: string[]): Record<string, string | boolean> {
32
41
  function printHelp(): void {
33
42
  console.log(`
34
43
  SmartContext Proxy v${VERSION}
35
- Intelligent context window optimization for LLM APIs
44
+ Transparent LLM context optimization proxy
36
45
 
37
- Usage:
38
- smartcontext-proxy [options]
39
- smartcontext-proxy status Show proxy status
46
+ Commands:
47
+ (default) Start proxy in foreground
48
+ install Install: CA cert + system proxy + auto-start service
49
+ uninstall Remove all: CA, proxy config, service (--purge for data)
50
+ status Show installation and proxy status
51
+ start Start as background daemon
52
+ stop Stop daemon
53
+ restart Restart daemon
40
54
 
41
55
  Options:
42
- --port, -p <port> Proxy port (default: 4800)
43
- --config, -c <file> Config file path
44
- --no-optimize Run in transparent proxy mode (no context optimization)
45
- --embedding-url <url> Ollama URL for embeddings (default: http://localhost:11434)
46
- --embedding-model <model> Embedding model (default: nomic-embed-text)
47
- --data-dir <path> Data directory (default: ~/.smartcontext/data)
48
- --help, -h Show help
49
- --version, -v Show version
50
-
51
- Client Integration:
52
- ANTHROPIC_API_URL=http://localhost:4800/v1/anthropic
53
- OPENAI_BASE_URL=http://localhost:4800/v1/openai
54
- OLLAMA_HOST=http://localhost:4800/v1/ollama
55
-
56
- API:
57
- GET /health Health check
58
- GET /_sc/status Proxy status
59
- GET /_sc/stats Aggregate metrics
60
- GET /_sc/feed Recent requests
61
- POST /_sc/pause Pause optimization
62
- POST /_sc/resume Resume optimization
56
+ --port, -p <port> Proxy port (default: 4800)
57
+ --no-optimize Disable context optimization (transparent proxy only)
58
+ --legacy Use legacy explicit-route proxy instead of CONNECT proxy
59
+ --purge With uninstall: also delete all data
60
+ --help, -h Show help
61
+ --version, -v Show version
62
+
63
+ After install, all LLM API traffic is automatically intercepted.
64
+ Dashboard: http://localhost:4800
63
65
  `);
64
66
  }
65
67
 
66
68
  async function showStatus(port: number): Promise<void> {
69
+ const inst = installStatus(port);
70
+ console.log('Installation:');
71
+ for (const [key, val] of Object.entries(inst)) {
72
+ console.log(` ${key}: ${val}`);
73
+ }
74
+
67
75
  return new Promise((resolve) => {
68
76
  http.get(`http://127.0.0.1:${port}/_sc/status`, (res) => {
69
77
  let data = '';
70
78
  res.on('data', (chunk) => (data += chunk));
71
79
  res.on('end', () => {
72
80
  try {
73
- const status = JSON.parse(data);
74
- console.log(`SmartContext Proxy: ${status.state}`);
75
- console.log(` Uptime: ${Math.round(status.uptime / 1000)}s`);
76
- console.log(` Requests: ${status.requests}`);
77
- console.log(` Mode: ${status.mode}`);
78
- } catch {
79
- console.log('Could not parse status response');
80
- }
81
+ const s = JSON.parse(data);
82
+ console.log(`\nProxy: ${s.state} (${s.mode})`);
83
+ console.log(` Uptime: ${Math.round(s.uptime / 1000)}s`);
84
+ console.log(` Requests: ${s.requests}`);
85
+ } catch { console.log('\nProxy: response parse error'); }
81
86
  resolve();
82
87
  });
83
88
  }).on('error', () => {
84
- console.log(`SmartContext Proxy: not running on port ${port}`);
89
+ console.log(`\nProxy: not running on port ${port}`);
85
90
  resolve();
86
91
  });
87
92
  });
@@ -95,73 +100,98 @@ async function main(): Promise<void> {
95
100
 
96
101
  const port = args.port ? parseInt(args.port as string, 10) : 4800;
97
102
 
103
+ // Commands
98
104
  if (args.command === 'status') { await showStatus(port); return; }
99
105
  if (args.command === 'stop') { stopDaemon(); return; }
100
- if (args.command === 'start') { startDaemon(process.argv.slice(3)); return; }
101
- if (args.command === 'restart') { stopDaemon(); await new Promise(r => setTimeout(r, 1000)); startDaemon(process.argv.slice(3)); return; }
102
- if (args.command === 'install-service') {
103
- const path = installService(port);
104
- console.log(`Service installed: ${path}`);
106
+
107
+ if (args.command === 'install') {
108
+ console.log('Installing SmartContext Proxy...\n');
109
+ ensureCA();
110
+ const result = install(port);
111
+ for (const step of result.steps) {
112
+ console.log(` ${step.success ? '✓' : '✗'} ${step.step}: ${step.message}`);
113
+ }
114
+ console.log(result.success ? '\nInstalled successfully.' : '\nInstallation failed (rolled back).');
105
115
  return;
106
116
  }
107
- if (args.command === 'uninstall-service') {
108
- console.log(uninstallService());
117
+
118
+ if (args.command === 'uninstall') {
119
+ console.log('Uninstalling SmartContext Proxy...\n');
120
+ const result = uninstall(!!args.purge);
121
+ for (const step of result.steps) {
122
+ console.log(` ${step.success ? '✓' : '✗'} ${step.step}: ${step.message}`);
123
+ }
124
+ console.log(result.success ? '\nUninstalled.' : '\nSome steps failed.');
109
125
  return;
110
126
  }
111
127
 
112
- const config = buildConfig({
113
- proxy: { port, host: '127.0.0.1' },
114
- });
128
+ // Build config
129
+ const config = buildConfig({ proxy: { port, host: '127.0.0.1' } });
115
130
 
116
- // Initialize embedding and storage (unless --no-optimize)
131
+ // Initialize embedding and storage
117
132
  let embedding: EmbeddingAdapter | undefined;
118
133
  let storage: StorageAdapter | undefined;
134
+ let optimizer: ContextOptimizer | undefined;
119
135
 
120
136
  if (!args.noOptimize) {
121
137
  try {
122
- const embeddingUrl = (args.embeddingUrl as string) || process.env['OLLAMA_HOST'] || 'http://localhost:11434';
123
- const embeddingModel = (args.embeddingModel as string) || 'nomic-embed-text';
124
- const dataDir = args.dataDir as string | undefined;
138
+ const embUrl = (args.embeddingUrl as string) || process.env['OLLAMA_HOST'] || 'http://localhost:11434';
139
+ const embModel = (args.embeddingModel as string) || 'nomic-embed-text';
125
140
 
126
- embedding = new OllamaEmbeddingAdapter(embeddingUrl, embeddingModel);
141
+ embedding = new OllamaEmbeddingAdapter(embUrl, embModel);
127
142
  await embedding.initialize();
128
143
 
129
- storage = new LanceDBAdapter(dataDir);
144
+ storage = new LanceDBAdapter(args.dataDir as string | undefined);
130
145
  await storage.initialize();
131
146
 
132
- console.log(` Embedding: ${embeddingModel} @ ${embeddingUrl}`);
133
- console.log(` Storage: LanceDB`);
147
+ optimizer = new ContextOptimizer(embedding, storage, config.context);
134
148
  } catch (err) {
135
149
  console.log(` Optimization unavailable: ${err}`);
136
150
  console.log(` Running in transparent proxy mode`);
137
- embedding = undefined;
138
- storage = undefined;
139
151
  }
140
152
  }
141
153
 
142
- const server = new ProxyServer(config, embedding, storage);
143
- const providers = server.getProviderNames();
144
- const mode = embedding && storage ? 'optimizing' : 'transparent';
154
+ // Build provider adapters map
155
+ const adapters = new Map<string, ProviderAdapter>();
156
+ adapters.set('anthropic', new AnthropicAdapter());
157
+ adapters.set('openai', new OpenAIAdapter());
158
+ adapters.set('ollama', new OllamaAdapter());
159
+ adapters.set('google', new GoogleAdapter());
160
+ adapters.set('openrouter', new OpenAIAdapter('https://openrouter.ai/api'));
161
+ adapters.set('groq', new OpenAIAdapter('https://api.groq.com'));
162
+ adapters.set('together', new OpenAIAdapter('https://api.together.xyz'));
163
+ adapters.set('deepseek', new OpenAIAdapter('https://api.deepseek.com'));
164
+
165
+ const mode = optimizer ? 'optimizing' : 'transparent';
166
+
167
+ if (args.legacy) {
168
+ // Legacy mode: explicit /v1/{provider}/* routing
169
+ const server = new ProxyServer(config, embedding, storage);
170
+ await server.start();
171
+ console.log(` Mode: legacy (explicit routing)`);
172
+ } else {
173
+ // Default: CONNECT proxy with transparent interception
174
+ ensureCA();
175
+ const proxy = new ConnectProxy(config, optimizer, adapters);
176
+ await proxy.start();
177
+ }
145
178
 
146
- await server.start();
179
+ writePid();
147
180
 
148
181
  console.log(`
149
- ┌─────────────────────────────────────────────┐
150
- │ SmartContext Proxy v${VERSION}
151
- │ http://${config.proxy.host}:${config.proxy.port}
152
-
153
- Providers: ${providers.join(', ').padEnd(31)}│
154
- Mode: ${mode.padEnd(36)}│
155
- └─────────────────────────────────────────────┘
182
+ ┌──────────────────────────────────────────────────┐
183
+ │ SmartContext Proxy v${VERSION}
184
+ │ http://127.0.0.1:${port}
185
+
186
+ Mode: ${(args.legacy ? 'legacy (explicit)' : 'transparent (CONNECT)').padEnd(40)}│
187
+ Optimization: ${mode.padEnd(33)}│
188
+ │ Dashboard: http://localhost:${port}/ │
189
+ └──────────────────────────────────────────────────┘
156
190
  `);
157
191
 
158
- // Write PID file
159
- writePid();
160
-
161
192
  const shutdown = async () => {
162
193
  console.log('\nShutting down...');
163
194
  removePid();
164
- await server.stop();
165
195
  if (storage) await storage.close();
166
196
  process.exit(0);
167
197
  };