smartcontext-proxy 0.1.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 (166) hide show
  1. package/PLAN.md +406 -0
  2. package/PROGRESS.md +60 -0
  3. package/README.md +99 -0
  4. package/SPEC.md +915 -0
  5. package/adapters/openclaw/embedding.d.ts +8 -0
  6. package/adapters/openclaw/embedding.js +16 -0
  7. package/adapters/openclaw/embedding.ts +15 -0
  8. package/adapters/openclaw/index.d.ts +18 -0
  9. package/adapters/openclaw/index.js +42 -0
  10. package/adapters/openclaw/index.ts +43 -0
  11. package/adapters/openclaw/session-importer.d.ts +22 -0
  12. package/adapters/openclaw/session-importer.js +99 -0
  13. package/adapters/openclaw/session-importer.ts +105 -0
  14. package/adapters/openclaw/storage.d.ts +26 -0
  15. package/adapters/openclaw/storage.js +177 -0
  16. package/adapters/openclaw/storage.ts +183 -0
  17. package/dist/adapters/openclaw/embedding.d.ts +8 -0
  18. package/dist/adapters/openclaw/embedding.js +16 -0
  19. package/dist/adapters/openclaw/index.d.ts +18 -0
  20. package/dist/adapters/openclaw/index.js +42 -0
  21. package/dist/adapters/openclaw/session-importer.d.ts +22 -0
  22. package/dist/adapters/openclaw/session-importer.js +99 -0
  23. package/dist/adapters/openclaw/storage.d.ts +26 -0
  24. package/dist/adapters/openclaw/storage.js +177 -0
  25. package/dist/config/auto-detect.d.ts +3 -0
  26. package/dist/config/auto-detect.js +48 -0
  27. package/dist/config/defaults.d.ts +2 -0
  28. package/dist/config/defaults.js +28 -0
  29. package/dist/config/schema.d.ts +30 -0
  30. package/dist/config/schema.js +3 -0
  31. package/dist/context/budget.d.ts +25 -0
  32. package/dist/context/budget.js +85 -0
  33. package/dist/context/canonical.d.ts +39 -0
  34. package/dist/context/canonical.js +12 -0
  35. package/dist/context/chunker.d.ts +9 -0
  36. package/dist/context/chunker.js +148 -0
  37. package/dist/context/optimizer.d.ts +31 -0
  38. package/dist/context/optimizer.js +163 -0
  39. package/dist/context/retriever.d.ts +29 -0
  40. package/dist/context/retriever.js +103 -0
  41. package/dist/daemon/process.d.ts +6 -0
  42. package/dist/daemon/process.js +76 -0
  43. package/dist/daemon/service.d.ts +2 -0
  44. package/dist/daemon/service.js +99 -0
  45. package/dist/embedding/ollama.d.ts +11 -0
  46. package/dist/embedding/ollama.js +72 -0
  47. package/dist/embedding/types.d.ts +6 -0
  48. package/dist/embedding/types.js +3 -0
  49. package/dist/index.d.ts +2 -0
  50. package/dist/index.js +190 -0
  51. package/dist/metrics/collector.d.ts +43 -0
  52. package/dist/metrics/collector.js +72 -0
  53. package/dist/providers/anthropic.d.ts +15 -0
  54. package/dist/providers/anthropic.js +109 -0
  55. package/dist/providers/google.d.ts +13 -0
  56. package/dist/providers/google.js +40 -0
  57. package/dist/providers/ollama.d.ts +13 -0
  58. package/dist/providers/ollama.js +82 -0
  59. package/dist/providers/openai.d.ts +15 -0
  60. package/dist/providers/openai.js +115 -0
  61. package/dist/providers/types.d.ts +18 -0
  62. package/dist/providers/types.js +3 -0
  63. package/dist/proxy/router.d.ts +12 -0
  64. package/dist/proxy/router.js +46 -0
  65. package/dist/proxy/server.d.ts +25 -0
  66. package/dist/proxy/server.js +265 -0
  67. package/dist/proxy/stream.d.ts +8 -0
  68. package/dist/proxy/stream.js +32 -0
  69. package/dist/src/config/auto-detect.d.ts +3 -0
  70. package/dist/src/config/auto-detect.js +48 -0
  71. package/dist/src/config/defaults.d.ts +2 -0
  72. package/dist/src/config/defaults.js +28 -0
  73. package/dist/src/config/schema.d.ts +30 -0
  74. package/dist/src/config/schema.js +3 -0
  75. package/dist/src/context/budget.d.ts +25 -0
  76. package/dist/src/context/budget.js +85 -0
  77. package/dist/src/context/canonical.d.ts +39 -0
  78. package/dist/src/context/canonical.js +12 -0
  79. package/dist/src/context/chunker.d.ts +9 -0
  80. package/dist/src/context/chunker.js +148 -0
  81. package/dist/src/context/optimizer.d.ts +31 -0
  82. package/dist/src/context/optimizer.js +163 -0
  83. package/dist/src/context/retriever.d.ts +29 -0
  84. package/dist/src/context/retriever.js +103 -0
  85. package/dist/src/daemon/process.d.ts +6 -0
  86. package/dist/src/daemon/process.js +76 -0
  87. package/dist/src/daemon/service.d.ts +2 -0
  88. package/dist/src/daemon/service.js +99 -0
  89. package/dist/src/embedding/ollama.d.ts +11 -0
  90. package/dist/src/embedding/ollama.js +72 -0
  91. package/dist/src/embedding/types.d.ts +6 -0
  92. package/dist/src/embedding/types.js +3 -0
  93. package/dist/src/index.d.ts +2 -0
  94. package/dist/src/index.js +190 -0
  95. package/dist/src/metrics/collector.d.ts +43 -0
  96. package/dist/src/metrics/collector.js +72 -0
  97. package/dist/src/providers/anthropic.d.ts +15 -0
  98. package/dist/src/providers/anthropic.js +109 -0
  99. package/dist/src/providers/google.d.ts +13 -0
  100. package/dist/src/providers/google.js +40 -0
  101. package/dist/src/providers/ollama.d.ts +13 -0
  102. package/dist/src/providers/ollama.js +82 -0
  103. package/dist/src/providers/openai.d.ts +15 -0
  104. package/dist/src/providers/openai.js +115 -0
  105. package/dist/src/providers/types.d.ts +18 -0
  106. package/dist/src/providers/types.js +3 -0
  107. package/dist/src/proxy/router.d.ts +12 -0
  108. package/dist/src/proxy/router.js +46 -0
  109. package/dist/src/proxy/server.d.ts +25 -0
  110. package/dist/src/proxy/server.js +265 -0
  111. package/dist/src/proxy/stream.d.ts +8 -0
  112. package/dist/src/proxy/stream.js +32 -0
  113. package/dist/src/storage/lancedb.d.ts +21 -0
  114. package/dist/src/storage/lancedb.js +158 -0
  115. package/dist/src/storage/types.d.ts +52 -0
  116. package/dist/src/storage/types.js +3 -0
  117. package/dist/src/test/context.test.d.ts +1 -0
  118. package/dist/src/test/context.test.js +141 -0
  119. package/dist/src/test/dashboard.test.d.ts +1 -0
  120. package/dist/src/test/dashboard.test.js +85 -0
  121. package/dist/src/test/proxy.test.d.ts +1 -0
  122. package/dist/src/test/proxy.test.js +188 -0
  123. package/dist/src/ui/dashboard.d.ts +2 -0
  124. package/dist/src/ui/dashboard.js +183 -0
  125. package/dist/storage/lancedb.d.ts +21 -0
  126. package/dist/storage/lancedb.js +158 -0
  127. package/dist/storage/types.d.ts +52 -0
  128. package/dist/storage/types.js +3 -0
  129. package/dist/test/context.test.d.ts +1 -0
  130. package/dist/test/context.test.js +141 -0
  131. package/dist/test/dashboard.test.d.ts +1 -0
  132. package/dist/test/dashboard.test.js +85 -0
  133. package/dist/test/proxy.test.d.ts +1 -0
  134. package/dist/test/proxy.test.js +188 -0
  135. package/dist/ui/dashboard.d.ts +2 -0
  136. package/dist/ui/dashboard.js +183 -0
  137. package/package.json +38 -0
  138. package/src/config/auto-detect.ts +51 -0
  139. package/src/config/defaults.ts +26 -0
  140. package/src/config/schema.ts +33 -0
  141. package/src/context/budget.ts +126 -0
  142. package/src/context/canonical.ts +50 -0
  143. package/src/context/chunker.ts +165 -0
  144. package/src/context/optimizer.ts +201 -0
  145. package/src/context/retriever.ts +123 -0
  146. package/src/daemon/process.ts +70 -0
  147. package/src/daemon/service.ts +103 -0
  148. package/src/embedding/ollama.ts +68 -0
  149. package/src/embedding/types.ts +6 -0
  150. package/src/index.ts +176 -0
  151. package/src/metrics/collector.ts +114 -0
  152. package/src/providers/anthropic.ts +117 -0
  153. package/src/providers/google.ts +42 -0
  154. package/src/providers/ollama.ts +87 -0
  155. package/src/providers/openai.ts +127 -0
  156. package/src/providers/types.ts +20 -0
  157. package/src/proxy/router.ts +48 -0
  158. package/src/proxy/server.ts +315 -0
  159. package/src/proxy/stream.ts +39 -0
  160. package/src/storage/lancedb.ts +169 -0
  161. package/src/storage/types.ts +47 -0
  162. package/src/test/context.test.ts +165 -0
  163. package/src/test/dashboard.test.ts +94 -0
  164. package/src/test/proxy.test.ts +218 -0
  165. package/src/ui/dashboard.ts +184 -0
  166. package/tsconfig.json +18 -0
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Retriever = void 0;
4
+ /**
5
+ * Retrieval pipeline:
6
+ * 1. Embed query
7
+ * 2. Vector search (top-K candidates)
8
+ * 3. Apply boosts (recency, file-path)
9
+ * 4. Dedup near-duplicates
10
+ * 5. Confidence gate
11
+ */
12
+ class Retriever {
13
+ embedding;
14
+ storage;
15
+ config;
16
+ constructor(embedding, storage, config) {
17
+ this.embedding = embedding;
18
+ this.storage = storage;
19
+ this.config = config;
20
+ }
21
+ async retrieve(queryText, currentSessionId, mentionedFiles) {
22
+ // 1. Embed query
23
+ const embedStart = Date.now();
24
+ const [queryEmbedding] = await this.embedding.embed([queryText]);
25
+ const queryEmbeddingMs = Date.now() - embedStart;
26
+ // 2. Vector search with boosts
27
+ const searchStart = Date.now();
28
+ const candidates = await this.storage.search(queryEmbedding, {
29
+ topK: this.config.tier2_max_chunks * 2,
30
+ minScore: this.config.tier2_min_score * 0.8, // Lower threshold, filter later
31
+ sessionBoost: {
32
+ sessionId: currentSessionId,
33
+ boost: this.config.recency_boost,
34
+ },
35
+ fileBoost: mentionedFiles.length > 0
36
+ ? { patterns: mentionedFiles, boost: this.config.filepath_boost }
37
+ : undefined,
38
+ });
39
+ const searchMs = Date.now() - searchStart;
40
+ // 3. Confidence gate — if best score too low, skip retrieval
41
+ if (candidates.length === 0 || candidates[0].score < this.config.confidence_gate) {
42
+ return {
43
+ chunks: [],
44
+ queryEmbeddingMs,
45
+ searchMs,
46
+ candidates: candidates.length,
47
+ aboveThreshold: 0,
48
+ afterDedup: 0,
49
+ topScore: candidates[0]?.score || 0,
50
+ };
51
+ }
52
+ // 4. Filter by threshold
53
+ const aboveThreshold = candidates.filter((c) => c.score >= this.config.tier2_min_score);
54
+ // 5. Dedup near-duplicates (by text similarity)
55
+ const deduped = this.dedup(aboveThreshold);
56
+ // 6. Ensure minimum chunks
57
+ const result = deduped.slice(0, this.config.tier2_max_chunks);
58
+ const minChunks = Math.min(3, aboveThreshold.length);
59
+ while (result.length < minChunks && aboveThreshold.length > result.length) {
60
+ const next = aboveThreshold[result.length];
61
+ if (!result.some((r) => r.id === next.id)) {
62
+ result.push(next);
63
+ }
64
+ }
65
+ return {
66
+ chunks: result,
67
+ queryEmbeddingMs,
68
+ searchMs,
69
+ candidates: candidates.length,
70
+ aboveThreshold: aboveThreshold.length,
71
+ afterDedup: deduped.length,
72
+ topScore: result[0]?.score || 0,
73
+ };
74
+ }
75
+ /** Remove near-duplicate chunks (same content, different IDs) */
76
+ dedup(chunks) {
77
+ const kept = [];
78
+ for (const chunk of chunks) {
79
+ const isDup = kept.some((k) => {
80
+ // Simple text similarity check
81
+ const shorter = Math.min(k.text.length, chunk.text.length);
82
+ const longer = Math.max(k.text.length, chunk.text.length);
83
+ if (shorter / longer < 0.8)
84
+ return false;
85
+ // Compare first 200 chars
86
+ const a = k.text.slice(0, 200).toLowerCase();
87
+ const b = chunk.text.slice(0, 200).toLowerCase();
88
+ let matches = 0;
89
+ for (let i = 0; i < Math.min(a.length, b.length); i++) {
90
+ if (a[i] === b[i])
91
+ matches++;
92
+ }
93
+ return matches / Math.max(a.length, b.length) > this.config.dedup_threshold;
94
+ });
95
+ if (!isDup) {
96
+ kept.push(chunk);
97
+ }
98
+ }
99
+ return kept;
100
+ }
101
+ }
102
+ exports.Retriever = Retriever;
103
+ //# sourceMappingURL=retriever.js.map
@@ -0,0 +1,6 @@
1
+ export declare function getPid(): number | null;
2
+ export declare function writePid(): void;
3
+ export declare function removePid(): void;
4
+ export declare function startDaemon(args: string[]): number;
5
+ export declare function stopDaemon(): boolean;
6
+ export declare function isDaemonChild(): boolean;
@@ -0,0 +1,76 @@
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.getPid = getPid;
7
+ exports.writePid = writePid;
8
+ exports.removePid = removePid;
9
+ exports.startDaemon = startDaemon;
10
+ exports.stopDaemon = stopDaemon;
11
+ exports.isDaemonChild = isDaemonChild;
12
+ const node_fs_1 = __importDefault(require("node:fs"));
13
+ const node_path_1 = __importDefault(require("node:path"));
14
+ const node_child_process_1 = require("node:child_process");
15
+ const PID_FILE = node_path_1.default.join(process.env['HOME'] || '.', '.smartcontext', 'smartcontext.pid');
16
+ const LOG_FILE = node_path_1.default.join(process.env['HOME'] || '.', '.smartcontext', 'logs', 'proxy.log');
17
+ function getPid() {
18
+ try {
19
+ const pid = parseInt(node_fs_1.default.readFileSync(PID_FILE, 'utf-8').trim(), 10);
20
+ // Check if process is alive
21
+ process.kill(pid, 0);
22
+ return pid;
23
+ }
24
+ catch {
25
+ // Clean up stale PID file
26
+ try {
27
+ node_fs_1.default.unlinkSync(PID_FILE);
28
+ }
29
+ catch { }
30
+ return null;
31
+ }
32
+ }
33
+ function writePid() {
34
+ const dir = node_path_1.default.dirname(PID_FILE);
35
+ node_fs_1.default.mkdirSync(dir, { recursive: true });
36
+ node_fs_1.default.writeFileSync(PID_FILE, String(process.pid));
37
+ }
38
+ function removePid() {
39
+ try {
40
+ node_fs_1.default.unlinkSync(PID_FILE);
41
+ }
42
+ catch { }
43
+ }
44
+ function startDaemon(args) {
45
+ const existingPid = getPid();
46
+ if (existingPid) {
47
+ console.log(`Already running (PID ${existingPid})`);
48
+ return existingPid;
49
+ }
50
+ const logDir = node_path_1.default.dirname(LOG_FILE);
51
+ node_fs_1.default.mkdirSync(logDir, { recursive: true });
52
+ const out = node_fs_1.default.openSync(LOG_FILE, 'a');
53
+ const err = node_fs_1.default.openSync(LOG_FILE, 'a');
54
+ const child = (0, node_child_process_1.spawn)(process.execPath, [__filename, ...args, '--daemon-child'], {
55
+ detached: true,
56
+ stdio: ['ignore', out, err],
57
+ });
58
+ child.unref();
59
+ console.log(`Started (PID ${child.pid})`);
60
+ return child.pid;
61
+ }
62
+ function stopDaemon() {
63
+ const pid = getPid();
64
+ if (!pid) {
65
+ console.log('Not running');
66
+ return false;
67
+ }
68
+ process.kill(pid, 'SIGTERM');
69
+ removePid();
70
+ console.log(`Stopped (PID ${pid})`);
71
+ return true;
72
+ }
73
+ function isDaemonChild() {
74
+ return process.argv.includes('--daemon-child');
75
+ }
76
+ //# sourceMappingURL=process.js.map
@@ -0,0 +1,2 @@
1
+ export declare function installService(port: number): string;
2
+ export declare function uninstallService(): string;
@@ -0,0 +1,99 @@
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.installService = installService;
7
+ exports.uninstallService = uninstallService;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_os_1 = __importDefault(require("node:os"));
11
+ function installService(port) {
12
+ if (process.platform === 'darwin') {
13
+ return installLaunchAgent(port);
14
+ }
15
+ return installSystemd(port);
16
+ }
17
+ function uninstallService() {
18
+ if (process.platform === 'darwin') {
19
+ return uninstallLaunchAgent();
20
+ }
21
+ return uninstallSystemd();
22
+ }
23
+ function installLaunchAgent(port) {
24
+ const plistDir = node_path_1.default.join(node_os_1.default.homedir(), 'Library', 'LaunchAgents');
25
+ const plistPath = node_path_1.default.join(plistDir, 'com.smartcontext.proxy.plist');
26
+ const logDir = node_path_1.default.join(node_os_1.default.homedir(), '.smartcontext', 'logs');
27
+ node_fs_1.default.mkdirSync(plistDir, { recursive: true });
28
+ node_fs_1.default.mkdirSync(logDir, { recursive: true });
29
+ const nodePath = process.execPath;
30
+ const scriptPath = node_path_1.default.resolve(node_path_1.default.join(__dirname, '..', 'index.js'));
31
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
32
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
+ <plist version="1.0">
34
+ <dict>
35
+ <key>Label</key>
36
+ <string>com.smartcontext.proxy</string>
37
+ <key>ProgramArguments</key>
38
+ <array>
39
+ <string>${nodePath}</string>
40
+ <string>${scriptPath}</string>
41
+ <string>--port</string>
42
+ <string>${port}</string>
43
+ </array>
44
+ <key>RunAtLoad</key>
45
+ <true/>
46
+ <key>KeepAlive</key>
47
+ <true/>
48
+ <key>StandardOutPath</key>
49
+ <string>${logDir}/proxy.log</string>
50
+ <key>StandardErrorPath</key>
51
+ <string>${logDir}/proxy.err.log</string>
52
+ </dict>
53
+ </plist>`;
54
+ node_fs_1.default.writeFileSync(plistPath, plist);
55
+ return plistPath;
56
+ }
57
+ function uninstallLaunchAgent() {
58
+ const plistPath = node_path_1.default.join(node_os_1.default.homedir(), 'Library', 'LaunchAgents', 'com.smartcontext.proxy.plist');
59
+ try {
60
+ node_fs_1.default.unlinkSync(plistPath);
61
+ return `Removed ${plistPath}`;
62
+ }
63
+ catch {
64
+ return 'No service file found';
65
+ }
66
+ }
67
+ function installSystemd(port) {
68
+ const serviceDir = node_path_1.default.join(node_os_1.default.homedir(), '.config', 'systemd', 'user');
69
+ const servicePath = node_path_1.default.join(serviceDir, 'smartcontext-proxy.service');
70
+ node_fs_1.default.mkdirSync(serviceDir, { recursive: true });
71
+ const nodePath = process.execPath;
72
+ const scriptPath = node_path_1.default.resolve(node_path_1.default.join(__dirname, '..', 'index.js'));
73
+ const service = `[Unit]
74
+ Description=SmartContext Proxy
75
+ After=network.target
76
+
77
+ [Service]
78
+ Type=simple
79
+ ExecStart=${nodePath} ${scriptPath} --port ${port}
80
+ Restart=always
81
+ RestartSec=5
82
+
83
+ [Install]
84
+ WantedBy=default.target
85
+ `;
86
+ node_fs_1.default.writeFileSync(servicePath, service);
87
+ return servicePath;
88
+ }
89
+ function uninstallSystemd() {
90
+ const servicePath = node_path_1.default.join(node_os_1.default.homedir(), '.config', 'systemd', 'user', 'smartcontext-proxy.service');
91
+ try {
92
+ node_fs_1.default.unlinkSync(servicePath);
93
+ return `Removed ${servicePath}`;
94
+ }
95
+ catch {
96
+ return 'No service file found';
97
+ }
98
+ }
99
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1,11 @@
1
+ import type { EmbeddingAdapter } from './types.js';
2
+ export declare class OllamaEmbeddingAdapter implements EmbeddingAdapter {
3
+ private url;
4
+ private model;
5
+ name: string;
6
+ dimensions: number;
7
+ constructor(url?: string, model?: string);
8
+ initialize(): Promise<void>;
9
+ embed(texts: string[]): Promise<number[][]>;
10
+ private embedSingle;
11
+ }
@@ -0,0 +1,72 @@
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.OllamaEmbeddingAdapter = void 0;
7
+ const node_http_1 = __importDefault(require("node:http"));
8
+ const node_https_1 = __importDefault(require("node:https"));
9
+ const node_url_1 = require("node:url");
10
+ class OllamaEmbeddingAdapter {
11
+ url;
12
+ model;
13
+ name = 'ollama';
14
+ dimensions = 768; // nomic-embed-text default
15
+ constructor(url = 'http://localhost:11434', model = 'nomic-embed-text') {
16
+ this.url = url;
17
+ this.model = model;
18
+ }
19
+ async initialize() {
20
+ // Verify Ollama is reachable and model exists
21
+ try {
22
+ await this.embed(['test']);
23
+ }
24
+ catch (err) {
25
+ throw new Error(`Ollama embedding unavailable at ${this.url}: ${err}`);
26
+ }
27
+ }
28
+ async embed(texts) {
29
+ const results = [];
30
+ for (const text of texts) {
31
+ const embedding = await this.embedSingle(text);
32
+ results.push(embedding);
33
+ if (embedding.length !== this.dimensions) {
34
+ this.dimensions = embedding.length; // auto-detect dimensions
35
+ }
36
+ }
37
+ return results;
38
+ }
39
+ async embedSingle(text) {
40
+ const parsed = new node_url_1.URL(`${this.url}/api/embed`);
41
+ const transport = parsed.protocol === 'https:' ? node_https_1.default : node_http_1.default;
42
+ const body = JSON.stringify({ model: this.model, input: text });
43
+ return new Promise((resolve, reject) => {
44
+ const req = transport.request(parsed, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, (res) => {
45
+ let data = '';
46
+ res.on('data', (chunk) => (data += chunk));
47
+ res.on('end', () => {
48
+ try {
49
+ const parsed = JSON.parse(data);
50
+ if (parsed.embeddings?.[0]) {
51
+ resolve(parsed.embeddings[0]);
52
+ }
53
+ else if (parsed.embedding) {
54
+ resolve(parsed.embedding);
55
+ }
56
+ else {
57
+ reject(new Error(`Unexpected Ollama response: ${data.slice(0, 200)}`));
58
+ }
59
+ }
60
+ catch (e) {
61
+ reject(new Error(`Failed to parse Ollama response: ${e}`));
62
+ }
63
+ });
64
+ });
65
+ req.on('error', reject);
66
+ req.write(body);
67
+ req.end();
68
+ });
69
+ }
70
+ }
71
+ exports.OllamaEmbeddingAdapter = OllamaEmbeddingAdapter;
72
+ //# sourceMappingURL=ollama.js.map
@@ -0,0 +1,6 @@
1
+ export interface EmbeddingAdapter {
2
+ name: string;
3
+ dimensions: number;
4
+ embed(texts: string[]): Promise<number[][]>;
5
+ initialize(): Promise<void>;
6
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const auto_detect_js_1 = require("./config/auto-detect.js");
8
+ const server_js_1 = require("./proxy/server.js");
9
+ const ollama_js_1 = require("./embedding/ollama.js");
10
+ const lancedb_js_1 = require("./storage/lancedb.js");
11
+ const process_js_1 = require("./daemon/process.js");
12
+ const service_js_1 = require("./daemon/service.js");
13
+ const node_http_1 = __importDefault(require("node:http"));
14
+ const VERSION = '0.1.0';
15
+ function parseArgs(args) {
16
+ const result = {};
17
+ for (let i = 0; i < args.length; i++) {
18
+ const arg = args[i];
19
+ if (arg === '--port' || arg === '-p')
20
+ result.port = args[++i];
21
+ else if (arg === '--config' || arg === '-c')
22
+ result.config = args[++i];
23
+ else if (arg === '--help' || arg === '-h')
24
+ result.help = true;
25
+ else if (arg === '--version' || arg === '-v')
26
+ result.version = true;
27
+ else if (arg === '--no-optimize')
28
+ result.noOptimize = true;
29
+ else if (arg === '--embedding-url')
30
+ result.embeddingUrl = args[++i];
31
+ else if (arg === '--embedding-model')
32
+ result.embeddingModel = args[++i];
33
+ else if (arg === '--data-dir')
34
+ result.dataDir = args[++i];
35
+ else if (!arg.startsWith('-'))
36
+ result.command = arg;
37
+ }
38
+ return result;
39
+ }
40
+ function printHelp() {
41
+ console.log(`
42
+ SmartContext Proxy v${VERSION}
43
+ Intelligent context window optimization for LLM APIs
44
+
45
+ Usage:
46
+ smartcontext-proxy [options]
47
+ smartcontext-proxy status Show proxy status
48
+
49
+ Options:
50
+ --port, -p <port> Proxy port (default: 4800)
51
+ --config, -c <file> Config file path
52
+ --no-optimize Run in transparent proxy mode (no context optimization)
53
+ --embedding-url <url> Ollama URL for embeddings (default: http://localhost:11434)
54
+ --embedding-model <model> Embedding model (default: nomic-embed-text)
55
+ --data-dir <path> Data directory (default: ~/.smartcontext/data)
56
+ --help, -h Show help
57
+ --version, -v Show version
58
+
59
+ Client Integration:
60
+ ANTHROPIC_API_URL=http://localhost:4800/v1/anthropic
61
+ OPENAI_BASE_URL=http://localhost:4800/v1/openai
62
+ OLLAMA_HOST=http://localhost:4800/v1/ollama
63
+
64
+ API:
65
+ GET /health Health check
66
+ GET /_sc/status Proxy status
67
+ GET /_sc/stats Aggregate metrics
68
+ GET /_sc/feed Recent requests
69
+ POST /_sc/pause Pause optimization
70
+ POST /_sc/resume Resume optimization
71
+ `);
72
+ }
73
+ async function showStatus(port) {
74
+ return new Promise((resolve) => {
75
+ node_http_1.default.get(`http://127.0.0.1:${port}/_sc/status`, (res) => {
76
+ let data = '';
77
+ res.on('data', (chunk) => (data += chunk));
78
+ res.on('end', () => {
79
+ try {
80
+ const status = JSON.parse(data);
81
+ console.log(`SmartContext Proxy: ${status.state}`);
82
+ console.log(` Uptime: ${Math.round(status.uptime / 1000)}s`);
83
+ console.log(` Requests: ${status.requests}`);
84
+ console.log(` Mode: ${status.mode}`);
85
+ }
86
+ catch {
87
+ console.log('Could not parse status response');
88
+ }
89
+ resolve();
90
+ });
91
+ }).on('error', () => {
92
+ console.log(`SmartContext Proxy: not running on port ${port}`);
93
+ resolve();
94
+ });
95
+ });
96
+ }
97
+ async function main() {
98
+ const args = parseArgs(process.argv.slice(2));
99
+ if (args.version) {
100
+ console.log(VERSION);
101
+ return;
102
+ }
103
+ if (args.help) {
104
+ printHelp();
105
+ return;
106
+ }
107
+ const port = args.port ? parseInt(args.port, 10) : 4800;
108
+ if (args.command === 'status') {
109
+ await showStatus(port);
110
+ return;
111
+ }
112
+ if (args.command === 'stop') {
113
+ (0, process_js_1.stopDaemon)();
114
+ return;
115
+ }
116
+ if (args.command === 'start') {
117
+ (0, process_js_1.startDaemon)(process.argv.slice(3));
118
+ return;
119
+ }
120
+ if (args.command === 'restart') {
121
+ (0, process_js_1.stopDaemon)();
122
+ await new Promise(r => setTimeout(r, 1000));
123
+ (0, process_js_1.startDaemon)(process.argv.slice(3));
124
+ return;
125
+ }
126
+ if (args.command === 'install-service') {
127
+ const path = (0, service_js_1.installService)(port);
128
+ console.log(`Service installed: ${path}`);
129
+ return;
130
+ }
131
+ if (args.command === 'uninstall-service') {
132
+ console.log((0, service_js_1.uninstallService)());
133
+ return;
134
+ }
135
+ const config = (0, auto_detect_js_1.buildConfig)({
136
+ proxy: { port, host: '127.0.0.1' },
137
+ });
138
+ // Initialize embedding and storage (unless --no-optimize)
139
+ let embedding;
140
+ let storage;
141
+ if (!args.noOptimize) {
142
+ try {
143
+ const embeddingUrl = args.embeddingUrl || process.env['OLLAMA_HOST'] || 'http://localhost:11434';
144
+ const embeddingModel = args.embeddingModel || 'nomic-embed-text';
145
+ const dataDir = args.dataDir;
146
+ embedding = new ollama_js_1.OllamaEmbeddingAdapter(embeddingUrl, embeddingModel);
147
+ await embedding.initialize();
148
+ storage = new lancedb_js_1.LanceDBAdapter(dataDir);
149
+ await storage.initialize();
150
+ console.log(` Embedding: ${embeddingModel} @ ${embeddingUrl}`);
151
+ console.log(` Storage: LanceDB`);
152
+ }
153
+ catch (err) {
154
+ console.log(` Optimization unavailable: ${err}`);
155
+ console.log(` Running in transparent proxy mode`);
156
+ embedding = undefined;
157
+ storage = undefined;
158
+ }
159
+ }
160
+ const server = new server_js_1.ProxyServer(config, embedding, storage);
161
+ const providers = server.getProviderNames();
162
+ const mode = embedding && storage ? 'optimizing' : 'transparent';
163
+ await server.start();
164
+ console.log(`
165
+ ┌─────────────────────────────────────────────┐
166
+ │ SmartContext Proxy v${VERSION} │
167
+ │ http://${config.proxy.host}:${config.proxy.port} │
168
+ │ │
169
+ │ Providers: ${providers.join(', ').padEnd(31)}│
170
+ │ Mode: ${mode.padEnd(36)}│
171
+ └─────────────────────────────────────────────┘
172
+ `);
173
+ // Write PID file
174
+ (0, process_js_1.writePid)();
175
+ const shutdown = async () => {
176
+ console.log('\nShutting down...');
177
+ (0, process_js_1.removePid)();
178
+ await server.stop();
179
+ if (storage)
180
+ await storage.close();
181
+ process.exit(0);
182
+ };
183
+ process.on('SIGINT', shutdown);
184
+ process.on('SIGTERM', shutdown);
185
+ }
186
+ main().catch((err) => {
187
+ console.error('Fatal:', err);
188
+ process.exit(1);
189
+ });
190
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,43 @@
1
+ export interface RequestMetric {
2
+ id: number;
3
+ timestamp: number;
4
+ provider: string;
5
+ model: string;
6
+ streaming: boolean;
7
+ originalTokens: number;
8
+ optimizedTokens: number;
9
+ savingsPercent: number;
10
+ latencyOverheadMs: number;
11
+ chunksRetrieved: number;
12
+ topScore: number;
13
+ passThrough: boolean;
14
+ reason?: string;
15
+ }
16
+ export interface AggregateStats {
17
+ totalRequests: number;
18
+ totalOriginalTokens: number;
19
+ totalOptimizedTokens: number;
20
+ totalSavingsPercent: number;
21
+ avgLatencyOverheadMs: number;
22
+ avgChunksRetrieved: number;
23
+ byProvider: Record<string, ProviderStats>;
24
+ byModel: Record<string, ModelStats>;
25
+ }
26
+ export interface ProviderStats {
27
+ requests: number;
28
+ tokensSaved: number;
29
+ savingsPercent: number;
30
+ }
31
+ export interface ModelStats {
32
+ requests: number;
33
+ tokensSaved: number;
34
+ savingsPercent: number;
35
+ }
36
+ export declare class MetricsCollector {
37
+ private metrics;
38
+ private startTime;
39
+ record(metric: RequestMetric): void;
40
+ getRecent(limit?: number): RequestMetric[];
41
+ getStats(): AggregateStats;
42
+ getUptime(): number;
43
+ }