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,121 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.installCA = installCA;
7
+ exports.uninstallCA = uninstallCA;
8
+ exports.isCAInstalled = isCAInstalled;
9
+ const node_child_process_1 = require("node:child_process");
10
+ const node_fs_1 = __importDefault(require("node:fs"));
11
+ const ca_manager_js_1 = require("./ca-manager.js");
12
+ const CA_NAME = 'SmartContext Proxy CA';
13
+ /** Install CA cert into system trust store */
14
+ function installCA() {
15
+ const certPath = (0, ca_manager_js_1.getCACertPath)();
16
+ if (!node_fs_1.default.existsSync(certPath)) {
17
+ return { success: false, message: 'CA cert not found. Run ensureCA() first.', requiresSudo: false };
18
+ }
19
+ if (process.platform === 'darwin') {
20
+ return installMacOS(certPath);
21
+ }
22
+ else if (process.platform === 'linux') {
23
+ return installLinux(certPath);
24
+ }
25
+ return { success: false, message: `Unsupported platform: ${process.platform}`, requiresSudo: false };
26
+ }
27
+ /** Remove CA cert from system trust store */
28
+ function uninstallCA() {
29
+ if (process.platform === 'darwin') {
30
+ return uninstallMacOS();
31
+ }
32
+ else if (process.platform === 'linux') {
33
+ return uninstallLinux();
34
+ }
35
+ return { success: false, message: `Unsupported platform: ${process.platform}`, requiresSudo: false };
36
+ }
37
+ /** Check if CA is installed in trust store */
38
+ function isCAInstalled() {
39
+ if (process.platform === 'darwin') {
40
+ try {
41
+ const result = (0, node_child_process_1.execFileSync)('security', [
42
+ 'find-certificate', '-c', CA_NAME, '-Z',
43
+ '/Library/Keychains/System.keychain',
44
+ ], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
45
+ return result.includes(CA_NAME);
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ else if (process.platform === 'linux') {
52
+ const dest = '/usr/local/share/ca-certificates/smartcontext-ca.crt';
53
+ return node_fs_1.default.existsSync(dest);
54
+ }
55
+ return false;
56
+ }
57
+ function installMacOS(certPath) {
58
+ try {
59
+ // Add to system keychain (requires admin)
60
+ (0, node_child_process_1.execFileSync)('sudo', [
61
+ 'security', 'add-trusted-cert',
62
+ '-d', '-r', 'trustRoot',
63
+ '-k', '/Library/Keychains/System.keychain',
64
+ certPath,
65
+ ], { stdio: 'pipe' });
66
+ return { success: true, message: 'CA installed in macOS System Keychain', requiresSudo: true };
67
+ }
68
+ catch (err) {
69
+ const msg = err instanceof Error ? err.message : String(err);
70
+ if (msg.includes('sudo') || msg.includes('password')) {
71
+ return { success: false, message: 'Requires sudo. Run: sudo smartcontext-proxy install', requiresSudo: true };
72
+ }
73
+ return { success: false, message: `Failed to install CA: ${msg}`, requiresSudo: true };
74
+ }
75
+ }
76
+ function uninstallMacOS() {
77
+ try {
78
+ (0, node_child_process_1.execFileSync)('sudo', [
79
+ 'security', 'remove-trusted-cert',
80
+ '-d', (0, ca_manager_js_1.getCACertPath)(),
81
+ ], { stdio: 'pipe' });
82
+ return { success: true, message: 'CA removed from macOS System Keychain', requiresSudo: true };
83
+ }
84
+ catch (err) {
85
+ // Try alternative: delete by name
86
+ try {
87
+ (0, node_child_process_1.execFileSync)('sudo', [
88
+ 'security', 'delete-certificate',
89
+ '-c', CA_NAME,
90
+ '/Library/Keychains/System.keychain',
91
+ ], { stdio: 'pipe' });
92
+ return { success: true, message: 'CA removed from macOS System Keychain', requiresSudo: true };
93
+ }
94
+ catch {
95
+ return { success: false, message: `Failed to remove CA: ${err}`, requiresSudo: true };
96
+ }
97
+ }
98
+ }
99
+ function installLinux(certPath) {
100
+ const dest = '/usr/local/share/ca-certificates/smartcontext-ca.crt';
101
+ try {
102
+ (0, node_child_process_1.execFileSync)('sudo', ['cp', certPath, dest], { stdio: 'pipe' });
103
+ (0, node_child_process_1.execFileSync)('sudo', ['update-ca-certificates'], { stdio: 'pipe' });
104
+ return { success: true, message: 'CA installed in Linux trust store', requiresSudo: true };
105
+ }
106
+ catch (err) {
107
+ return { success: false, message: `Failed: ${err}`, requiresSudo: true };
108
+ }
109
+ }
110
+ function uninstallLinux() {
111
+ const dest = '/usr/local/share/ca-certificates/smartcontext-ca.crt';
112
+ try {
113
+ (0, node_child_process_1.execFileSync)('sudo', ['rm', '-f', dest], { stdio: 'pipe' });
114
+ (0, node_child_process_1.execFileSync)('sudo', ['update-ca-certificates', '--fresh'], { stdio: 'pipe' });
115
+ return { success: true, message: 'CA removed from Linux trust store', requiresSudo: true };
116
+ }
117
+ catch (err) {
118
+ return { success: false, message: `Failed: ${err}`, requiresSudo: true };
119
+ }
120
+ }
121
+ //# sourceMappingURL=trust-store.js.map
@@ -0,0 +1,8 @@
1
+ /** Build the native tray app if not already built */
2
+ export declare function buildTray(): boolean;
3
+ /** Launch the tray app */
4
+ export declare function startTray(): boolean;
5
+ /** Stop the tray app */
6
+ export declare function stopTray(): void;
7
+ /** Check if tray is running */
8
+ export declare function isTrayRunning(): boolean;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildTray = buildTray;
7
+ exports.startTray = startTray;
8
+ exports.stopTray = stopTray;
9
+ exports.isTrayRunning = isTrayRunning;
10
+ const node_child_process_1 = require("node:child_process");
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const node_fs_1 = __importDefault(require("node:fs"));
13
+ let trayProcess = null;
14
+ const TRAY_BINARY_DIR = node_path_1.default.join(__dirname, '..', '..', 'native', 'macos', 'SmartContextTray');
15
+ const TRAY_BUILD_DIR = node_path_1.default.join(TRAY_BINARY_DIR, '.build', 'release');
16
+ const TRAY_BINARY = node_path_1.default.join(TRAY_BUILD_DIR, 'SmartContextTray');
17
+ /** Build the native tray app if not already built */
18
+ function buildTray() {
19
+ if (process.platform !== 'darwin')
20
+ return false;
21
+ if (node_fs_1.default.existsSync(TRAY_BINARY))
22
+ return true;
23
+ try {
24
+ const { execFileSync } = require('node:child_process');
25
+ execFileSync('swift', ['build', '-c', 'release'], {
26
+ cwd: TRAY_BINARY_DIR,
27
+ stdio: 'pipe',
28
+ timeout: 60000,
29
+ });
30
+ return node_fs_1.default.existsSync(TRAY_BINARY);
31
+ }
32
+ catch (err) {
33
+ console.log(`Tray build failed: ${err}`);
34
+ return false;
35
+ }
36
+ }
37
+ /** Launch the tray app */
38
+ function startTray() {
39
+ if (process.platform !== 'darwin')
40
+ return false;
41
+ if (trayProcess)
42
+ return true;
43
+ if (!buildTray()) {
44
+ console.log('Tray app not available (build failed or not macOS)');
45
+ return false;
46
+ }
47
+ trayProcess = (0, node_child_process_1.execFile)(TRAY_BINARY, [], {}, (err) => {
48
+ if (err)
49
+ console.log(`Tray exited: ${err.message}`);
50
+ trayProcess = null;
51
+ });
52
+ trayProcess.unref();
53
+ return true;
54
+ }
55
+ /** Stop the tray app */
56
+ function stopTray() {
57
+ if (trayProcess) {
58
+ trayProcess.kill('SIGTERM');
59
+ trayProcess = null;
60
+ }
61
+ }
62
+ /** Check if tray is running */
63
+ function isTrayRunning() {
64
+ return trayProcess !== null && !trayProcess.killed;
65
+ }
66
+ //# sourceMappingURL=bridge.js.map
@@ -1,2 +1,11 @@
1
1
  import type { MetricsCollector } from '../metrics/collector.js';
2
- export declare function renderDashboard(metrics: MetricsCollector, paused: boolean): string;
2
+ export interface DashboardState {
3
+ paused: boolean;
4
+ mode: 'transparent' | 'optimizing';
5
+ proxyType: 'connect' | 'legacy';
6
+ abTestEnabled: boolean;
7
+ debugHeaders: boolean;
8
+ caInstalled: boolean;
9
+ systemProxyActive: boolean;
10
+ }
11
+ export declare function renderDashboard(metrics: MetricsCollector, state: DashboardState): string;
@@ -1,15 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderDashboard = renderDashboard;
4
- function renderDashboard(metrics, paused) {
4
+ function renderDashboard(metrics, state) {
5
5
  const stats = metrics.getStats();
6
6
  const recent = metrics.getRecent(20);
7
- const uptime = metrics.getUptime();
8
- const uptimeStr = formatDuration(uptime);
9
- const statusBadge = paused
7
+ const uptimeStr = formatDuration(metrics.getUptime());
8
+ const savingsAmount = estimateCostSaved(stats.totalOriginalTokens - stats.totalOptimizedTokens);
9
+ const stateBadge = state.paused
10
10
  ? '<span class="badge paused">PAUSED</span>'
11
11
  : '<span class="badge running">RUNNING</span>';
12
- const savingsAmount = estimateCostSaved(stats.totalOriginalTokens - stats.totalOptimizedTokens);
12
+ const modeBadge = state.mode === 'optimizing'
13
+ ? '<span class="badge opt">OPTIMIZING</span>'
14
+ : '<span class="badge transparent">TRANSPARENT</span>';
13
15
  return `<!DOCTYPE html>
14
16
  <html lang="en">
15
17
  <head>
@@ -21,17 +23,23 @@ function renderDashboard(metrics, paused) {
21
23
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: #0f1117; color: #e1e4e8; }
22
24
  .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
23
25
  header { display: flex; justify-content: space-between; align-items: center; padding: 16px 0; border-bottom: 1px solid #21262d; margin-bottom: 24px; }
24
- h1 { font-size: 20px; font-weight: 600; }
26
+ h1 { font-size: 20px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
25
27
  h2 { font-size: 16px; font-weight: 600; margin-bottom: 12px; color: #8b949e; }
26
- .badge { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; text-transform: uppercase; }
28
+ .badge { padding: 4px 12px; border-radius: 12px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
27
29
  .badge.running { background: #238636; color: #fff; }
28
30
  .badge.paused { background: #d29922; color: #000; }
29
- .controls { display: flex; gap: 8px; }
30
- .btn { padding: 6px 16px; border-radius: 6px; border: 1px solid #30363d; background: #21262d; color: #e1e4e8; cursor: pointer; font-size: 13px; }
31
- .btn:hover { background: #30363d; }
31
+ .badge.opt { background: #1f6feb; color: #fff; }
32
+ .badge.transparent { background: #30363d; color: #8b949e; }
33
+ .badge.on { background: #238636; color: #fff; }
34
+ .badge.off { background: #30363d; color: #8b949e; }
35
+ .controls { display: flex; gap: 8px; align-items: center; }
36
+ .btn { padding: 6px 16px; border-radius: 6px; border: 1px solid #30363d; background: #21262d; color: #e1e4e8; cursor: pointer; font-size: 13px; transition: all 0.15s; }
37
+ .btn:hover { background: #30363d; border-color: #484f58; }
32
38
  .btn.primary { background: #238636; border-color: #238636; }
39
+ .btn.primary:hover { background: #2ea043; }
33
40
  .btn.warn { background: #d29922; border-color: #d29922; color: #000; }
34
- .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 24px; }
41
+ .btn.danger { background: #da3633; border-color: #da3633; }
42
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-bottom: 24px; }
35
43
  .card { background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 20px; }
36
44
  .card .value { font-size: 32px; font-weight: 700; color: #58a6ff; }
37
45
  .card .label { font-size: 13px; color: #8b949e; margin-top: 4px; }
@@ -47,20 +55,46 @@ function renderDashboard(metrics, paused) {
47
55
  .tab.active { color: #e1e4e8; border-bottom-color: #58a6ff; }
48
56
  .tab-content { display: none; }
49
57
  .tab-content.active { display: block; }
58
+ .settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
59
+ .setting { background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 16px; display: flex; justify-content: space-between; align-items: center; }
60
+ .setting-info { flex: 1; }
61
+ .setting-name { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
62
+ .setting-desc { font-size: 12px; color: #8b949e; }
63
+ .toggle { position: relative; width: 44px; height: 24px; cursor: pointer; }
64
+ .toggle input { display: none; }
65
+ .toggle .slider { position: absolute; inset: 0; background: #30363d; border-radius: 12px; transition: 0.2s; }
66
+ .toggle .slider:before { content: ''; position: absolute; width: 18px; height: 18px; left: 3px; top: 3px; background: #8b949e; border-radius: 50%; transition: 0.2s; }
67
+ .toggle input:checked + .slider { background: #238636; }
68
+ .toggle input:checked + .slider:before { transform: translateX(20px); background: #fff; }
69
+ .status-row { display: flex; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
70
+ .status-pill { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #8b949e; }
71
+ .dot { width: 8px; height: 8px; border-radius: 50%; }
72
+ .dot.green { background: #3fb950; }
73
+ .dot.yellow { background: #d29922; }
74
+ .dot.red { background: #f85149; }
75
+ .dot.gray { background: #484f58; }
50
76
  .refresh-note { font-size: 11px; color: #484f58; text-align: right; margin-top: 8px; }
51
77
  </style>
52
78
  </head>
53
79
  <body>
54
80
  <div class="container">
55
81
  <header>
56
- <h1>SmartContext Proxy ${statusBadge}</h1>
82
+ <h1>SmartContext Proxy ${stateBadge} ${modeBadge}</h1>
57
83
  <div class="controls">
58
- ${paused
84
+ ${state.paused
59
85
  ? '<button class="btn primary" onclick="api(\'/_sc/resume\')">Resume</button>'
60
86
  : '<button class="btn warn" onclick="api(\'/_sc/pause\')">Pause</button>'}
61
87
  </div>
62
88
  </header>
63
89
 
90
+ <div class="status-row">
91
+ <div class="status-pill"><div class="dot ${state.caInstalled ? 'green' : 'red'}"></div>CA Certificate</div>
92
+ <div class="status-pill"><div class="dot ${state.systemProxyActive ? 'green' : 'gray'}"></div>System Proxy</div>
93
+ <div class="status-pill"><div class="dot ${state.mode === 'optimizing' ? 'green' : 'gray'}"></div>Context Optimization</div>
94
+ <div class="status-pill"><div class="dot ${state.abTestEnabled ? 'yellow' : 'gray'}"></div>A/B Test</div>
95
+ <div class="status-pill"><div class="dot ${state.proxyType === 'connect' ? 'green' : 'gray'}"></div>${state.proxyType === 'connect' ? 'Transparent' : 'Legacy'} Mode</div>
96
+ </div>
97
+
64
98
  <div class="grid">
65
99
  <div class="card savings">
66
100
  <div class="value">$${savingsAmount}</div>
@@ -81,9 +115,10 @@ function renderDashboard(metrics, paused) {
81
115
  </div>
82
116
 
83
117
  <div class="tab-bar">
84
- <div class="tab active" onclick="switchTab('feed')">Live Feed</div>
85
- <div class="tab" onclick="switchTab('providers')">By Provider</div>
86
- <div class="tab" onclick="switchTab('models')">By Model</div>
118
+ <div class="tab active" onclick="switchTab('feed',this)">Live Feed</div>
119
+ <div class="tab" onclick="switchTab('providers',this)">By Provider</div>
120
+ <div class="tab" onclick="switchTab('models',this)">By Model</div>
121
+ <div class="tab" onclick="switchTab('settings',this)">Settings</div>
87
122
  </div>
88
123
 
89
124
  <div id="tab-feed" class="tab-content active">
@@ -113,12 +148,7 @@ function renderDashboard(metrics, paused) {
113
148
  <thead><tr><th>Provider</th><th>Requests</th><th>Tokens Saved</th><th>Savings %</th></tr></thead>
114
149
  <tbody>
115
150
  ${Object.entries(stats.byProvider).map(([name, s]) => `
116
- <tr>
117
- <td>${name}</td>
118
- <td class="mono">${s.requests}</td>
119
- <td class="mono">${formatTokens(s.tokensSaved)}</td>
120
- <td class="savings-pct">${s.savingsPercent}%</td>
121
- </tr>
151
+ <tr><td>${name}</td><td class="mono">${s.requests}</td><td class="mono">${formatTokens(s.tokensSaved)}</td><td class="savings-pct">${s.savingsPercent}%</td></tr>
122
152
  `).join('')}
123
153
  </tbody>
124
154
  </table>
@@ -130,31 +160,88 @@ function renderDashboard(metrics, paused) {
130
160
  <thead><tr><th>Model</th><th>Requests</th><th>Tokens Saved</th><th>Savings %</th></tr></thead>
131
161
  <tbody>
132
162
  ${Object.entries(stats.byModel).map(([name, s]) => `
133
- <tr>
134
- <td>${name}</td>
135
- <td class="mono">${s.requests}</td>
136
- <td class="mono">${formatTokens(s.tokensSaved)}</td>
137
- <td class="savings-pct">${s.savingsPercent}%</td>
138
- </tr>
163
+ <tr><td>${name}</td><td class="mono">${s.requests}</td><td class="mono">${formatTokens(s.tokensSaved)}</td><td class="savings-pct">${s.savingsPercent}%</td></tr>
139
164
  `).join('')}
140
165
  </tbody>
141
166
  </table>
142
167
  </div>
143
168
 
144
- <div class="refresh-note">Uptime: ${uptimeStr} | Auto-refresh: 10s</div>
169
+ <div id="tab-settings" class="tab-content">
170
+ <h2>Controls</h2>
171
+ <div class="settings-grid">
172
+ <div class="setting">
173
+ <div class="setting-info">
174
+ <div class="setting-name">Context Optimization</div>
175
+ <div class="setting-desc">Optimize LLM context windows to reduce token usage</div>
176
+ </div>
177
+ <label class="toggle">
178
+ <input type="checkbox" ${!state.paused ? 'checked' : ''} onchange="api(this.checked ? '/_sc/resume' : '/_sc/pause')">
179
+ <span class="slider"></span>
180
+ </label>
181
+ </div>
182
+
183
+ <div class="setting">
184
+ <div class="setting-info">
185
+ <div class="setting-name">A/B Test Mode</div>
186
+ <div class="setting-desc">Send each request twice to compare quality</div>
187
+ </div>
188
+ <label class="toggle">
189
+ <input type="checkbox" ${state.abTestEnabled ? 'checked' : ''} onchange="api(this.checked ? '/_sc/ab-test/enable' : '/_sc/ab-test/disable')">
190
+ <span class="slider"></span>
191
+ </label>
192
+ </div>
193
+
194
+ <div class="setting">
195
+ <div class="setting-info">
196
+ <div class="setting-name">Debug Headers</div>
197
+ <div class="setting-desc">Add X-SmartContext-* headers to responses</div>
198
+ </div>
199
+ <label class="toggle">
200
+ <input type="checkbox" ${state.debugHeaders ? 'checked' : ''} onchange="api(this.checked ? '/_sc/debug-headers/enable' : '/_sc/debug-headers/disable')">
201
+ <span class="slider"></span>
202
+ </label>
203
+ </div>
204
+
205
+ <div class="setting">
206
+ <div class="setting-info">
207
+ <div class="setting-name">System Proxy</div>
208
+ <div class="setting-desc">Auto-intercept all LLM traffic system-wide</div>
209
+ </div>
210
+ <label class="toggle">
211
+ <input type="checkbox" ${state.systemProxyActive ? 'checked' : ''} onchange="api(this.checked ? '/_sc/system-proxy/enable' : '/_sc/system-proxy/disable')">
212
+ <span class="slider"></span>
213
+ </label>
214
+ </div>
215
+ </div>
216
+ </div>
217
+
218
+ <div class="refresh-note">v0.2.0 | Uptime: ${uptimeStr} | Auto-refresh: 5s</div>
145
219
  </div>
146
220
  <script>
147
- function switchTab(name) {
221
+ function switchTab(name, el) {
148
222
  document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
149
223
  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
150
224
  document.getElementById('tab-' + name).classList.add('active');
151
- event.target.classList.add('active');
225
+ if (el) el.classList.add('active');
226
+ location.hash = name;
152
227
  }
153
228
  async function api(path) {
154
229
  await fetch(path, { method: 'POST' });
155
230
  location.reload();
156
231
  }
157
- setTimeout(() => location.reload(), 10000);
232
+ // Restore tab from URL hash
233
+ (function() {
234
+ var hash = location.hash.replace('#', '');
235
+ if (hash && document.getElementById('tab-' + hash)) {
236
+ document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
237
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
238
+ document.getElementById('tab-' + hash).classList.add('active');
239
+ document.querySelectorAll('.tab').forEach(t => {
240
+ if (t.getAttribute('onclick') && t.getAttribute('onclick').indexOf("'" + hash + "'") !== -1) t.classList.add('active');
241
+ });
242
+ }
243
+ })();
244
+ setTimeout(() => { var h = location.hash; location.reload(); if (h) location.hash = h; }, 5000);
158
245
  </script>
159
246
  </body>
160
247
  </html>`;
@@ -174,9 +261,7 @@ function formatDuration(ms) {
174
261
  return `${Math.floor(s / 60)}m ${s % 60}s`;
175
262
  return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
176
263
  }
177
- /** Rough cost estimate based on Anthropic/OpenAI pricing */
178
264
  function estimateCostSaved(tokensSaved) {
179
- // Assume avg $15/1M input tokens (Opus pricing)
180
265
  const cost = (tokensSaved / 1000000) * 15;
181
266
  return cost.toFixed(2);
182
267
  }
@@ -0,0 +1,8 @@
1
+ import type { Server } from 'node:http';
2
+ import type { RequestMetric } from '../metrics/collector.js';
3
+ /** Attach WebSocket server to existing HTTP server */
4
+ export declare function attachWebSocketFeed(server: Server): void;
5
+ /** Broadcast a new request metric to all connected clients */
6
+ export declare function broadcastMetric(metric: RequestMetric): void;
7
+ /** Get count of connected WebSocket clients */
8
+ export declare function getConnectedClients(): number;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.attachWebSocketFeed = attachWebSocketFeed;
4
+ exports.broadcastMetric = broadcastMetric;
5
+ exports.getConnectedClients = getConnectedClients;
6
+ const ws_1 = require("ws");
7
+ let wss = null;
8
+ /** Attach WebSocket server to existing HTTP server */
9
+ function attachWebSocketFeed(server) {
10
+ wss = new ws_1.WebSocketServer({ server, path: '/_sc/ws' });
11
+ wss.on('connection', (ws) => {
12
+ ws.send(JSON.stringify({ type: 'connected', timestamp: Date.now() }));
13
+ });
14
+ }
15
+ /** Broadcast a new request metric to all connected clients */
16
+ function broadcastMetric(metric) {
17
+ if (!wss)
18
+ return;
19
+ const data = JSON.stringify({ type: 'request', data: metric });
20
+ for (const client of wss.clients) {
21
+ if (client.readyState === ws_1.WebSocket.OPEN) {
22
+ client.send(data);
23
+ }
24
+ }
25
+ }
26
+ /** Get count of connected WebSocket clients */
27
+ function getConnectedClients() {
28
+ return wss?.clients.size || 0;
29
+ }
30
+ //# sourceMappingURL=ws-feed.js.map
@@ -0,0 +1,13 @@
1
+ // swift-tools-version:5.9
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "SmartContextTray",
6
+ platforms: [.macOS(.v12)],
7
+ targets: [
8
+ .executableTarget(
9
+ name: "SmartContextTray",
10
+ path: "Sources"
11
+ ),
12
+ ]
13
+ )