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.
- package/PLAN.md +406 -0
- package/PROGRESS.md +60 -0
- package/README.md +99 -0
- package/SPEC.md +915 -0
- package/adapters/openclaw/embedding.d.ts +8 -0
- package/adapters/openclaw/embedding.js +16 -0
- package/adapters/openclaw/embedding.ts +15 -0
- package/adapters/openclaw/index.d.ts +18 -0
- package/adapters/openclaw/index.js +42 -0
- package/adapters/openclaw/index.ts +43 -0
- package/adapters/openclaw/session-importer.d.ts +22 -0
- package/adapters/openclaw/session-importer.js +99 -0
- package/adapters/openclaw/session-importer.ts +105 -0
- package/adapters/openclaw/storage.d.ts +26 -0
- package/adapters/openclaw/storage.js +177 -0
- package/adapters/openclaw/storage.ts +183 -0
- package/dist/adapters/openclaw/embedding.d.ts +8 -0
- package/dist/adapters/openclaw/embedding.js +16 -0
- package/dist/adapters/openclaw/index.d.ts +18 -0
- package/dist/adapters/openclaw/index.js +42 -0
- package/dist/adapters/openclaw/session-importer.d.ts +22 -0
- package/dist/adapters/openclaw/session-importer.js +99 -0
- package/dist/adapters/openclaw/storage.d.ts +26 -0
- package/dist/adapters/openclaw/storage.js +177 -0
- package/dist/config/auto-detect.d.ts +3 -0
- package/dist/config/auto-detect.js +48 -0
- package/dist/config/defaults.d.ts +2 -0
- package/dist/config/defaults.js +28 -0
- package/dist/config/schema.d.ts +30 -0
- package/dist/config/schema.js +3 -0
- package/dist/context/budget.d.ts +25 -0
- package/dist/context/budget.js +85 -0
- package/dist/context/canonical.d.ts +39 -0
- package/dist/context/canonical.js +12 -0
- package/dist/context/chunker.d.ts +9 -0
- package/dist/context/chunker.js +148 -0
- package/dist/context/optimizer.d.ts +31 -0
- package/dist/context/optimizer.js +163 -0
- package/dist/context/retriever.d.ts +29 -0
- package/dist/context/retriever.js +103 -0
- package/dist/daemon/process.d.ts +6 -0
- package/dist/daemon/process.js +76 -0
- package/dist/daemon/service.d.ts +2 -0
- package/dist/daemon/service.js +99 -0
- package/dist/embedding/ollama.d.ts +11 -0
- package/dist/embedding/ollama.js +72 -0
- package/dist/embedding/types.d.ts +6 -0
- package/dist/embedding/types.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +190 -0
- package/dist/metrics/collector.d.ts +43 -0
- package/dist/metrics/collector.js +72 -0
- package/dist/providers/anthropic.d.ts +15 -0
- package/dist/providers/anthropic.js +109 -0
- package/dist/providers/google.d.ts +13 -0
- package/dist/providers/google.js +40 -0
- package/dist/providers/ollama.d.ts +13 -0
- package/dist/providers/ollama.js +82 -0
- package/dist/providers/openai.d.ts +15 -0
- package/dist/providers/openai.js +115 -0
- package/dist/providers/types.d.ts +18 -0
- package/dist/providers/types.js +3 -0
- package/dist/proxy/router.d.ts +12 -0
- package/dist/proxy/router.js +46 -0
- package/dist/proxy/server.d.ts +25 -0
- package/dist/proxy/server.js +265 -0
- package/dist/proxy/stream.d.ts +8 -0
- package/dist/proxy/stream.js +32 -0
- package/dist/src/config/auto-detect.d.ts +3 -0
- package/dist/src/config/auto-detect.js +48 -0
- package/dist/src/config/defaults.d.ts +2 -0
- package/dist/src/config/defaults.js +28 -0
- package/dist/src/config/schema.d.ts +30 -0
- package/dist/src/config/schema.js +3 -0
- package/dist/src/context/budget.d.ts +25 -0
- package/dist/src/context/budget.js +85 -0
- package/dist/src/context/canonical.d.ts +39 -0
- package/dist/src/context/canonical.js +12 -0
- package/dist/src/context/chunker.d.ts +9 -0
- package/dist/src/context/chunker.js +148 -0
- package/dist/src/context/optimizer.d.ts +31 -0
- package/dist/src/context/optimizer.js +163 -0
- package/dist/src/context/retriever.d.ts +29 -0
- package/dist/src/context/retriever.js +103 -0
- package/dist/src/daemon/process.d.ts +6 -0
- package/dist/src/daemon/process.js +76 -0
- package/dist/src/daemon/service.d.ts +2 -0
- package/dist/src/daemon/service.js +99 -0
- package/dist/src/embedding/ollama.d.ts +11 -0
- package/dist/src/embedding/ollama.js +72 -0
- package/dist/src/embedding/types.d.ts +6 -0
- package/dist/src/embedding/types.js +3 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +190 -0
- package/dist/src/metrics/collector.d.ts +43 -0
- package/dist/src/metrics/collector.js +72 -0
- package/dist/src/providers/anthropic.d.ts +15 -0
- package/dist/src/providers/anthropic.js +109 -0
- package/dist/src/providers/google.d.ts +13 -0
- package/dist/src/providers/google.js +40 -0
- package/dist/src/providers/ollama.d.ts +13 -0
- package/dist/src/providers/ollama.js +82 -0
- package/dist/src/providers/openai.d.ts +15 -0
- package/dist/src/providers/openai.js +115 -0
- package/dist/src/providers/types.d.ts +18 -0
- package/dist/src/providers/types.js +3 -0
- package/dist/src/proxy/router.d.ts +12 -0
- package/dist/src/proxy/router.js +46 -0
- package/dist/src/proxy/server.d.ts +25 -0
- package/dist/src/proxy/server.js +265 -0
- package/dist/src/proxy/stream.d.ts +8 -0
- package/dist/src/proxy/stream.js +32 -0
- package/dist/src/storage/lancedb.d.ts +21 -0
- package/dist/src/storage/lancedb.js +158 -0
- package/dist/src/storage/types.d.ts +52 -0
- package/dist/src/storage/types.js +3 -0
- package/dist/src/test/context.test.d.ts +1 -0
- package/dist/src/test/context.test.js +141 -0
- package/dist/src/test/dashboard.test.d.ts +1 -0
- package/dist/src/test/dashboard.test.js +85 -0
- package/dist/src/test/proxy.test.d.ts +1 -0
- package/dist/src/test/proxy.test.js +188 -0
- package/dist/src/ui/dashboard.d.ts +2 -0
- package/dist/src/ui/dashboard.js +183 -0
- package/dist/storage/lancedb.d.ts +21 -0
- package/dist/storage/lancedb.js +158 -0
- package/dist/storage/types.d.ts +52 -0
- package/dist/storage/types.js +3 -0
- package/dist/test/context.test.d.ts +1 -0
- package/dist/test/context.test.js +141 -0
- package/dist/test/dashboard.test.d.ts +1 -0
- package/dist/test/dashboard.test.js +85 -0
- package/dist/test/proxy.test.d.ts +1 -0
- package/dist/test/proxy.test.js +188 -0
- package/dist/ui/dashboard.d.ts +2 -0
- package/dist/ui/dashboard.js +183 -0
- package/package.json +38 -0
- package/src/config/auto-detect.ts +51 -0
- package/src/config/defaults.ts +26 -0
- package/src/config/schema.ts +33 -0
- package/src/context/budget.ts +126 -0
- package/src/context/canonical.ts +50 -0
- package/src/context/chunker.ts +165 -0
- package/src/context/optimizer.ts +201 -0
- package/src/context/retriever.ts +123 -0
- package/src/daemon/process.ts +70 -0
- package/src/daemon/service.ts +103 -0
- package/src/embedding/ollama.ts +68 -0
- package/src/embedding/types.ts +6 -0
- package/src/index.ts +176 -0
- package/src/metrics/collector.ts +114 -0
- package/src/providers/anthropic.ts +117 -0
- package/src/providers/google.ts +42 -0
- package/src/providers/ollama.ts +87 -0
- package/src/providers/openai.ts +127 -0
- package/src/providers/types.ts +20 -0
- package/src/proxy/router.ts +48 -0
- package/src/proxy/server.ts +315 -0
- package/src/proxy/stream.ts +39 -0
- package/src/storage/lancedb.ts +169 -0
- package/src/storage/types.ts +47 -0
- package/src/test/context.test.ts +165 -0
- package/src/test/dashboard.test.ts +94 -0
- package/src/test/proxy.test.ts +218 -0
- package/src/ui/dashboard.ts +184 -0
- 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,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
|
package/dist/index.d.ts
ADDED
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
|
+
}
|