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,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderDashboard = renderDashboard;
|
|
4
|
+
function renderDashboard(metrics, paused) {
|
|
5
|
+
const stats = metrics.getStats();
|
|
6
|
+
const recent = metrics.getRecent(20);
|
|
7
|
+
const uptime = metrics.getUptime();
|
|
8
|
+
const uptimeStr = formatDuration(uptime);
|
|
9
|
+
const statusBadge = paused
|
|
10
|
+
? '<span class="badge paused">PAUSED</span>'
|
|
11
|
+
: '<span class="badge running">RUNNING</span>';
|
|
12
|
+
const savingsAmount = estimateCostSaved(stats.totalOriginalTokens - stats.totalOptimizedTokens);
|
|
13
|
+
return `<!DOCTYPE html>
|
|
14
|
+
<html lang="en">
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="utf-8">
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
18
|
+
<title>SmartContext Proxy</title>
|
|
19
|
+
<style>
|
|
20
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
21
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: #0f1117; color: #e1e4e8; }
|
|
22
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
23
|
+
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; }
|
|
25
|
+
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; }
|
|
27
|
+
.badge.running { background: #238636; color: #fff; }
|
|
28
|
+
.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; }
|
|
32
|
+
.btn.primary { background: #238636; border-color: #238636; }
|
|
33
|
+
.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; }
|
|
35
|
+
.card { background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 20px; }
|
|
36
|
+
.card .value { font-size: 32px; font-weight: 700; color: #58a6ff; }
|
|
37
|
+
.card .label { font-size: 13px; color: #8b949e; margin-top: 4px; }
|
|
38
|
+
.card.savings .value { color: #3fb950; }
|
|
39
|
+
table { width: 100%; border-collapse: collapse; }
|
|
40
|
+
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #21262d; font-size: 13px; }
|
|
41
|
+
th { color: #8b949e; font-weight: 600; }
|
|
42
|
+
.mono { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; }
|
|
43
|
+
.savings-pct { color: #3fb950; font-weight: 600; }
|
|
44
|
+
.pass { color: #8b949e; }
|
|
45
|
+
.tab-bar { display: flex; gap: 0; border-bottom: 1px solid #21262d; margin-bottom: 16px; }
|
|
46
|
+
.tab { padding: 8px 16px; cursor: pointer; border-bottom: 2px solid transparent; color: #8b949e; font-size: 14px; }
|
|
47
|
+
.tab.active { color: #e1e4e8; border-bottom-color: #58a6ff; }
|
|
48
|
+
.tab-content { display: none; }
|
|
49
|
+
.tab-content.active { display: block; }
|
|
50
|
+
.refresh-note { font-size: 11px; color: #484f58; text-align: right; margin-top: 8px; }
|
|
51
|
+
</style>
|
|
52
|
+
</head>
|
|
53
|
+
<body>
|
|
54
|
+
<div class="container">
|
|
55
|
+
<header>
|
|
56
|
+
<h1>SmartContext Proxy ${statusBadge}</h1>
|
|
57
|
+
<div class="controls">
|
|
58
|
+
${paused
|
|
59
|
+
? '<button class="btn primary" onclick="api(\'/_sc/resume\')">Resume</button>'
|
|
60
|
+
: '<button class="btn warn" onclick="api(\'/_sc/pause\')">Pause</button>'}
|
|
61
|
+
</div>
|
|
62
|
+
</header>
|
|
63
|
+
|
|
64
|
+
<div class="grid">
|
|
65
|
+
<div class="card savings">
|
|
66
|
+
<div class="value">$${savingsAmount}</div>
|
|
67
|
+
<div class="label">Estimated Saved</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="card">
|
|
70
|
+
<div class="value">${stats.totalRequests}</div>
|
|
71
|
+
<div class="label">Total Requests</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="card">
|
|
74
|
+
<div class="value">${stats.totalSavingsPercent}%</div>
|
|
75
|
+
<div class="label">Avg Token Savings</div>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="card">
|
|
78
|
+
<div class="value">${stats.avgLatencyOverheadMs}ms</div>
|
|
79
|
+
<div class="label">Avg Latency Overhead</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<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>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div id="tab-feed" class="tab-content active">
|
|
90
|
+
<h2>Recent Requests</h2>
|
|
91
|
+
<table>
|
|
92
|
+
<thead><tr><th>ID</th><th>Time</th><th>Provider/Model</th><th>Original</th><th>Optimized</th><th>Savings</th><th>Chunks</th><th>Latency</th></tr></thead>
|
|
93
|
+
<tbody>
|
|
94
|
+
${recent.reverse().map((r) => `
|
|
95
|
+
<tr>
|
|
96
|
+
<td class="mono">#${r.id}</td>
|
|
97
|
+
<td class="mono">${new Date(r.timestamp).toLocaleTimeString()}</td>
|
|
98
|
+
<td>${r.provider}/${r.model}</td>
|
|
99
|
+
<td class="mono">${formatTokens(r.originalTokens)}</td>
|
|
100
|
+
<td class="mono">${formatTokens(r.optimizedTokens)}</td>
|
|
101
|
+
<td class="${r.passThrough ? 'pass' : 'savings-pct'}">${r.passThrough ? 'pass' : `-${r.savingsPercent}%`}</td>
|
|
102
|
+
<td class="mono">${r.chunksRetrieved}</td>
|
|
103
|
+
<td class="mono">${r.latencyOverheadMs}ms</td>
|
|
104
|
+
</tr>
|
|
105
|
+
`).join('')}
|
|
106
|
+
</tbody>
|
|
107
|
+
</table>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div id="tab-providers" class="tab-content">
|
|
111
|
+
<h2>Savings by Provider</h2>
|
|
112
|
+
<table>
|
|
113
|
+
<thead><tr><th>Provider</th><th>Requests</th><th>Tokens Saved</th><th>Savings %</th></tr></thead>
|
|
114
|
+
<tbody>
|
|
115
|
+
${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>
|
|
122
|
+
`).join('')}
|
|
123
|
+
</tbody>
|
|
124
|
+
</table>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div id="tab-models" class="tab-content">
|
|
128
|
+
<h2>Savings by Model</h2>
|
|
129
|
+
<table>
|
|
130
|
+
<thead><tr><th>Model</th><th>Requests</th><th>Tokens Saved</th><th>Savings %</th></tr></thead>
|
|
131
|
+
<tbody>
|
|
132
|
+
${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>
|
|
139
|
+
`).join('')}
|
|
140
|
+
</tbody>
|
|
141
|
+
</table>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div class="refresh-note">Uptime: ${uptimeStr} | Auto-refresh: 10s</div>
|
|
145
|
+
</div>
|
|
146
|
+
<script>
|
|
147
|
+
function switchTab(name) {
|
|
148
|
+
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
|
149
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
150
|
+
document.getElementById('tab-' + name).classList.add('active');
|
|
151
|
+
event.target.classList.add('active');
|
|
152
|
+
}
|
|
153
|
+
async function api(path) {
|
|
154
|
+
await fetch(path, { method: 'POST' });
|
|
155
|
+
location.reload();
|
|
156
|
+
}
|
|
157
|
+
setTimeout(() => location.reload(), 10000);
|
|
158
|
+
</script>
|
|
159
|
+
</body>
|
|
160
|
+
</html>`;
|
|
161
|
+
}
|
|
162
|
+
function formatTokens(n) {
|
|
163
|
+
if (n >= 1000000)
|
|
164
|
+
return `${(n / 1000000).toFixed(1)}M`;
|
|
165
|
+
if (n >= 1000)
|
|
166
|
+
return `${(n / 1000).toFixed(1)}K`;
|
|
167
|
+
return String(n);
|
|
168
|
+
}
|
|
169
|
+
function formatDuration(ms) {
|
|
170
|
+
const s = Math.floor(ms / 1000);
|
|
171
|
+
if (s < 60)
|
|
172
|
+
return `${s}s`;
|
|
173
|
+
if (s < 3600)
|
|
174
|
+
return `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
175
|
+
return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
|
|
176
|
+
}
|
|
177
|
+
/** Rough cost estimate based on Anthropic/OpenAI pricing */
|
|
178
|
+
function estimateCostSaved(tokensSaved) {
|
|
179
|
+
// Assume avg $15/1M input tokens (Opus pricing)
|
|
180
|
+
const cost = (tokensSaved / 1000000) * 15;
|
|
181
|
+
return cost.toFixed(2);
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=dashboard.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { StorageAdapter, Chunk, ScoredChunk, SearchOptions, Exchange } from './types.js';
|
|
2
|
+
export declare class LanceDBAdapter implements StorageAdapter {
|
|
3
|
+
private basePath;
|
|
4
|
+
name: string;
|
|
5
|
+
private db;
|
|
6
|
+
private chunksTable;
|
|
7
|
+
private logsDir;
|
|
8
|
+
private dbPath;
|
|
9
|
+
constructor(basePath?: string);
|
|
10
|
+
initialize(): Promise<void>;
|
|
11
|
+
upsertChunks(chunks: Chunk[]): Promise<void>;
|
|
12
|
+
search(embedding: number[], options: SearchOptions): Promise<ScoredChunk[]>;
|
|
13
|
+
appendLog(sessionId: string, exchange: Exchange): Promise<void>;
|
|
14
|
+
getSessionLog(sessionId: string): Promise<Exchange[]>;
|
|
15
|
+
getStats(): Promise<{
|
|
16
|
+
chunks: number;
|
|
17
|
+
sessions: number;
|
|
18
|
+
diskBytes: number;
|
|
19
|
+
}>;
|
|
20
|
+
close(): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
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.LanceDBAdapter = void 0;
|
|
7
|
+
const lancedb_1 = require("@lancedb/lancedb");
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
class LanceDBAdapter {
|
|
11
|
+
basePath;
|
|
12
|
+
name = 'lancedb';
|
|
13
|
+
db;
|
|
14
|
+
chunksTable;
|
|
15
|
+
logsDir;
|
|
16
|
+
dbPath;
|
|
17
|
+
constructor(basePath = node_path_1.default.join(process.env['HOME'] || '.', '.smartcontext', 'data')) {
|
|
18
|
+
this.basePath = basePath;
|
|
19
|
+
this.dbPath = node_path_1.default.join(basePath, 'vectors');
|
|
20
|
+
this.logsDir = node_path_1.default.join(basePath, 'logs');
|
|
21
|
+
}
|
|
22
|
+
async initialize() {
|
|
23
|
+
node_fs_1.default.mkdirSync(this.dbPath, { recursive: true });
|
|
24
|
+
node_fs_1.default.mkdirSync(this.logsDir, { recursive: true });
|
|
25
|
+
this.db = await (0, lancedb_1.connect)(this.dbPath);
|
|
26
|
+
// Create or open chunks table
|
|
27
|
+
const tableNames = await this.db.tableNames();
|
|
28
|
+
if (tableNames.includes('chunks')) {
|
|
29
|
+
this.chunksTable = await this.db.openTable('chunks');
|
|
30
|
+
}
|
|
31
|
+
// Table will be created lazily on first upsert with actual data
|
|
32
|
+
}
|
|
33
|
+
async upsertChunks(chunks) {
|
|
34
|
+
if (chunks.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
const records = chunks.map((c) => ({
|
|
37
|
+
id: c.id,
|
|
38
|
+
text: c.text,
|
|
39
|
+
vector: c.embedding,
|
|
40
|
+
sessionId: c.sessionId,
|
|
41
|
+
timestamp: c.timestamp,
|
|
42
|
+
summary: c.metadata.summary,
|
|
43
|
+
tokenCount: c.metadata.tokenCount,
|
|
44
|
+
exchangeIndex: c.metadata.exchangeIndex,
|
|
45
|
+
files: JSON.stringify(c.metadata.files || []),
|
|
46
|
+
tools: JSON.stringify(c.metadata.tools || []),
|
|
47
|
+
}));
|
|
48
|
+
const tableNames = await this.db.tableNames();
|
|
49
|
+
if (!tableNames.includes('chunks')) {
|
|
50
|
+
this.chunksTable = await this.db.createTable('chunks', records);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
if (!this.chunksTable) {
|
|
54
|
+
this.chunksTable = await this.db.openTable('chunks');
|
|
55
|
+
}
|
|
56
|
+
await this.chunksTable.add(records);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async search(embedding, options) {
|
|
60
|
+
const tableNames = await this.db.tableNames();
|
|
61
|
+
if (!tableNames.includes('chunks'))
|
|
62
|
+
return [];
|
|
63
|
+
if (!this.chunksTable) {
|
|
64
|
+
this.chunksTable = await this.db.openTable('chunks');
|
|
65
|
+
}
|
|
66
|
+
const results = await this.chunksTable
|
|
67
|
+
.vectorSearch(embedding)
|
|
68
|
+
.limit(options.topK * 2) // Fetch extra for filtering
|
|
69
|
+
.toArray();
|
|
70
|
+
const scored = results.map((r) => {
|
|
71
|
+
// LanceDB returns _distance (L2), convert to similarity score
|
|
72
|
+
const distance = r['_distance'];
|
|
73
|
+
let score = 1 / (1 + distance); // Convert L2 distance to similarity
|
|
74
|
+
const boosts = {};
|
|
75
|
+
// Session recency boost
|
|
76
|
+
if (options.sessionBoost && r.sessionId === options.sessionBoost.sessionId) {
|
|
77
|
+
boosts.session = options.sessionBoost.boost;
|
|
78
|
+
score += options.sessionBoost.boost;
|
|
79
|
+
}
|
|
80
|
+
// File-path boost
|
|
81
|
+
if (options.fileBoost && options.fileBoost.patterns.length > 0) {
|
|
82
|
+
const files = JSON.parse(r.files || '[]');
|
|
83
|
+
const hasMatch = options.fileBoost.patterns.some((p) => files.some((f) => f.includes(p)));
|
|
84
|
+
if (hasMatch) {
|
|
85
|
+
boosts.filepath = options.fileBoost.boost;
|
|
86
|
+
score += options.fileBoost.boost;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
id: r.id,
|
|
91
|
+
text: r.text,
|
|
92
|
+
embedding: [], // Don't return embedding in search results
|
|
93
|
+
sessionId: r.sessionId,
|
|
94
|
+
timestamp: r.timestamp,
|
|
95
|
+
metadata: {
|
|
96
|
+
summary: r.summary,
|
|
97
|
+
tokenCount: r.tokenCount,
|
|
98
|
+
exchangeIndex: r.exchangeIndex,
|
|
99
|
+
files: JSON.parse(r.files || '[]'),
|
|
100
|
+
tools: JSON.parse(r.tools || '[]'),
|
|
101
|
+
},
|
|
102
|
+
score,
|
|
103
|
+
boosts,
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
// Filter by min score and limit
|
|
107
|
+
return scored
|
|
108
|
+
.filter((c) => c.score >= options.minScore)
|
|
109
|
+
.sort((a, b) => b.score - a.score)
|
|
110
|
+
.slice(0, options.topK);
|
|
111
|
+
}
|
|
112
|
+
async appendLog(sessionId, exchange) {
|
|
113
|
+
const logFile = node_path_1.default.join(this.logsDir, `${sessionId}.jsonl`);
|
|
114
|
+
const line = JSON.stringify(exchange) + '\n';
|
|
115
|
+
node_fs_1.default.appendFileSync(logFile, line);
|
|
116
|
+
}
|
|
117
|
+
async getSessionLog(sessionId) {
|
|
118
|
+
const logFile = node_path_1.default.join(this.logsDir, `${sessionId}.jsonl`);
|
|
119
|
+
if (!node_fs_1.default.existsSync(logFile))
|
|
120
|
+
return [];
|
|
121
|
+
return node_fs_1.default.readFileSync(logFile, 'utf-8')
|
|
122
|
+
.trim()
|
|
123
|
+
.split('\n')
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
.map((line) => JSON.parse(line));
|
|
126
|
+
}
|
|
127
|
+
async getStats() {
|
|
128
|
+
let chunks = 0;
|
|
129
|
+
const tableNames = await this.db.tableNames();
|
|
130
|
+
if (tableNames.includes('chunks')) {
|
|
131
|
+
if (!this.chunksTable) {
|
|
132
|
+
this.chunksTable = await this.db.openTable('chunks');
|
|
133
|
+
}
|
|
134
|
+
chunks = await this.chunksTable.countRows();
|
|
135
|
+
}
|
|
136
|
+
const sessions = node_fs_1.default.readdirSync(this.logsDir).filter((f) => f.endsWith('.jsonl')).length;
|
|
137
|
+
// Rough disk usage
|
|
138
|
+
let diskBytes = 0;
|
|
139
|
+
const walk = (dir) => {
|
|
140
|
+
if (!node_fs_1.default.existsSync(dir))
|
|
141
|
+
return;
|
|
142
|
+
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
143
|
+
const full = node_path_1.default.join(dir, entry.name);
|
|
144
|
+
if (entry.isDirectory())
|
|
145
|
+
walk(full);
|
|
146
|
+
else
|
|
147
|
+
diskBytes += node_fs_1.default.statSync(full).size;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
walk(this.basePath);
|
|
151
|
+
return { chunks, sessions, diskBytes };
|
|
152
|
+
}
|
|
153
|
+
async close() {
|
|
154
|
+
// LanceDB doesn't require explicit close
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
exports.LanceDBAdapter = LanceDBAdapter;
|
|
158
|
+
//# sourceMappingURL=lancedb.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface Chunk {
|
|
2
|
+
id: string;
|
|
3
|
+
text: string;
|
|
4
|
+
embedding: number[];
|
|
5
|
+
sessionId: string;
|
|
6
|
+
timestamp: number;
|
|
7
|
+
metadata: ChunkMetadata;
|
|
8
|
+
}
|
|
9
|
+
export interface ChunkMetadata {
|
|
10
|
+
files?: string[];
|
|
11
|
+
tools?: string[];
|
|
12
|
+
summary: string;
|
|
13
|
+
tokenCount: number;
|
|
14
|
+
exchangeIndex: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ScoredChunk extends Chunk {
|
|
17
|
+
score: number;
|
|
18
|
+
boosts?: Record<string, number>;
|
|
19
|
+
}
|
|
20
|
+
export interface SearchOptions {
|
|
21
|
+
topK: number;
|
|
22
|
+
minScore: number;
|
|
23
|
+
sessionBoost?: {
|
|
24
|
+
sessionId: string;
|
|
25
|
+
boost: number;
|
|
26
|
+
};
|
|
27
|
+
fileBoost?: {
|
|
28
|
+
patterns: string[];
|
|
29
|
+
boost: number;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export interface Exchange {
|
|
33
|
+
index: number;
|
|
34
|
+
userMessage: string;
|
|
35
|
+
assistantMessage: string;
|
|
36
|
+
timestamp: number;
|
|
37
|
+
metadata?: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
export interface StorageAdapter {
|
|
40
|
+
name: string;
|
|
41
|
+
initialize(): Promise<void>;
|
|
42
|
+
upsertChunks(chunks: Chunk[]): Promise<void>;
|
|
43
|
+
search(embedding: number[], options: SearchOptions): Promise<ScoredChunk[]>;
|
|
44
|
+
appendLog(sessionId: string, exchange: Exchange): Promise<void>;
|
|
45
|
+
getSessionLog(sessionId: string): Promise<Exchange[]>;
|
|
46
|
+
getStats(): Promise<{
|
|
47
|
+
chunks: number;
|
|
48
|
+
sessions: number;
|
|
49
|
+
diskBytes: number;
|
|
50
|
+
}>;
|
|
51
|
+
close(): Promise<void>;
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
const node_test_1 = require("node:test");
|
|
7
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
8
|
+
const chunker_js_1 = require("../context/chunker.js");
|
|
9
|
+
const budget_js_1 = require("../context/budget.js");
|
|
10
|
+
(0, node_test_1.describe)('Chunker', () => {
|
|
11
|
+
(0, node_test_1.it)('estimates tokens roughly correctly', () => {
|
|
12
|
+
const text = 'Hello world, this is a test message.';
|
|
13
|
+
const tokens = (0, chunker_js_1.estimateTokens)(text);
|
|
14
|
+
node_assert_1.default.ok(tokens > 5 && tokens < 20, `Expected 5-20 tokens, got ${tokens}`);
|
|
15
|
+
});
|
|
16
|
+
(0, node_test_1.it)('chunks a simple conversation into exchange pairs', () => {
|
|
17
|
+
const messages = [
|
|
18
|
+
{ role: 'user', content: 'What is TypeScript?' },
|
|
19
|
+
{ role: 'assistant', content: 'TypeScript is a superset of JavaScript that adds static types.' },
|
|
20
|
+
{ role: 'user', content: 'How do I install it?' },
|
|
21
|
+
{ role: 'assistant', content: 'Run npm install -g typescript' },
|
|
22
|
+
];
|
|
23
|
+
const chunks = (0, chunker_js_1.chunkConversation)(messages, 'test-session');
|
|
24
|
+
node_assert_1.default.strictEqual(chunks.length, 2);
|
|
25
|
+
node_assert_1.default.ok(chunks[0].text.includes('What is TypeScript'));
|
|
26
|
+
node_assert_1.default.ok(chunks[0].text.includes('superset of JavaScript'));
|
|
27
|
+
node_assert_1.default.ok(chunks[1].text.includes('How do I install'));
|
|
28
|
+
});
|
|
29
|
+
(0, node_test_1.it)('splits long exchanges at paragraph boundaries', () => {
|
|
30
|
+
const longResponse = Array(100).fill('This is a paragraph with some detailed content about various programming concepts, design patterns, and software architecture principles that spans multiple lines.\n\n').join('');
|
|
31
|
+
const messages = [
|
|
32
|
+
{ role: 'user', content: 'Tell me everything about programming.' },
|
|
33
|
+
{ role: 'assistant', content: longResponse },
|
|
34
|
+
];
|
|
35
|
+
const chunks = (0, chunker_js_1.chunkConversation)(messages, 'test-session');
|
|
36
|
+
node_assert_1.default.ok(chunks.length > 1, `Expected >1 chunks for long response, got ${chunks.length}`);
|
|
37
|
+
for (const chunk of chunks) {
|
|
38
|
+
node_assert_1.default.ok(chunk.metadata.tokenCount <= 2500, `Chunk too large: ${chunk.metadata.tokenCount} tokens`);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
(0, node_test_1.it)('keeps code blocks atomic', () => {
|
|
42
|
+
const response = 'Here is the code:\n\n```typescript\nfunction hello() {\n console.log("hello");\n console.log("world");\n}\n```\n\nThat is the function.';
|
|
43
|
+
const messages = [
|
|
44
|
+
{ role: 'user', content: 'Show me code' },
|
|
45
|
+
{ role: 'assistant', content: response },
|
|
46
|
+
];
|
|
47
|
+
const chunks = (0, chunker_js_1.chunkConversation)(messages, 'test-session');
|
|
48
|
+
// Code block should be in one chunk
|
|
49
|
+
const codeChunk = chunks.find((c) => c.text.includes('```typescript'));
|
|
50
|
+
node_assert_1.default.ok(codeChunk, 'Should have a chunk with the code block');
|
|
51
|
+
node_assert_1.default.ok(codeChunk.text.includes('console.log("world")'), 'Code block should be complete');
|
|
52
|
+
});
|
|
53
|
+
(0, node_test_1.it)('extracts file paths from text', () => {
|
|
54
|
+
const messages = [
|
|
55
|
+
{ role: 'user', content: 'Fix the bug in `src/proxy/server.ts`' },
|
|
56
|
+
{ role: 'assistant', content: 'I found the issue in /Users/vt/projects/test/index.ts' },
|
|
57
|
+
];
|
|
58
|
+
const chunks = (0, chunker_js_1.chunkConversation)(messages, 'test-session');
|
|
59
|
+
node_assert_1.default.ok(chunks[0].metadata.files.length > 0, 'Should extract file paths');
|
|
60
|
+
});
|
|
61
|
+
(0, node_test_1.it)('assigns unique IDs to chunks', () => {
|
|
62
|
+
const messages = [
|
|
63
|
+
{ role: 'user', content: 'Hello' },
|
|
64
|
+
{ role: 'assistant', content: 'Hi' },
|
|
65
|
+
{ role: 'user', content: 'Bye' },
|
|
66
|
+
{ role: 'assistant', content: 'Goodbye' },
|
|
67
|
+
];
|
|
68
|
+
const chunks = (0, chunker_js_1.chunkConversation)(messages, 'test-session');
|
|
69
|
+
const ids = new Set(chunks.map((c) => c.id));
|
|
70
|
+
node_assert_1.default.strictEqual(ids.size, chunks.length, 'All chunk IDs should be unique');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
(0, node_test_1.describe)('Budget', () => {
|
|
74
|
+
(0, node_test_1.it)('knows context limits for common models', () => {
|
|
75
|
+
node_assert_1.default.strictEqual((0, budget_js_1.getModelContextLimit)('claude-opus-4-6'), 200000);
|
|
76
|
+
node_assert_1.default.strictEqual((0, budget_js_1.getModelContextLimit)('gpt-4o'), 128000);
|
|
77
|
+
node_assert_1.default.strictEqual((0, budget_js_1.getModelContextLimit)('unknown-model'), 128000);
|
|
78
|
+
});
|
|
79
|
+
(0, node_test_1.it)('packs context within budget', () => {
|
|
80
|
+
const messages = Array(20).fill(null).map((_, i) => ({
|
|
81
|
+
role: i % 2 === 0 ? 'user' : 'assistant',
|
|
82
|
+
content: `Message ${i}: `.padEnd(200, 'x'),
|
|
83
|
+
}));
|
|
84
|
+
const mockChunks = [
|
|
85
|
+
{
|
|
86
|
+
id: '1', text: 'chunk 1 text', embedding: [], sessionId: 's1',
|
|
87
|
+
timestamp: Date.now(), score: 0.9, metadata: {
|
|
88
|
+
summary: 'chunk 1', tokenCount: 100, exchangeIndex: 0,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: '2', text: 'chunk 2 text', embedding: [], sessionId: 's1',
|
|
93
|
+
timestamp: Date.now(), score: 0.8, metadata: {
|
|
94
|
+
summary: 'chunk 2', tokenCount: 150, exchangeIndex: 1,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
const packed = (0, budget_js_1.packContext)('You are a helpful assistant.', messages, mockChunks, 'claude-opus-4-6', 3, // tier1_exchanges
|
|
99
|
+
500, // tier3_reserve
|
|
100
|
+
8192);
|
|
101
|
+
node_assert_1.default.ok(packed.tier1Messages.length > 0, 'Should have tier 1 messages');
|
|
102
|
+
node_assert_1.default.ok(packed.tier2Chunks.length > 0, 'Should have tier 2 chunks');
|
|
103
|
+
node_assert_1.default.ok(packed.optimizedTokens < packed.originalTokens, 'Should save tokens');
|
|
104
|
+
node_assert_1.default.ok(packed.savingsPercent > 0, 'Should have positive savings');
|
|
105
|
+
});
|
|
106
|
+
(0, node_test_1.it)('handles empty retrieval', () => {
|
|
107
|
+
const messages = [
|
|
108
|
+
{ role: 'user', content: 'Hello' },
|
|
109
|
+
{ role: 'assistant', content: 'Hi there' },
|
|
110
|
+
];
|
|
111
|
+
const packed = (0, budget_js_1.packContext)('System prompt', messages, [], 'gpt-4o', 3, 500, 8192);
|
|
112
|
+
node_assert_1.default.strictEqual(packed.tier2Chunks.length, 0);
|
|
113
|
+
node_assert_1.default.strictEqual(packed.savingsPercent, 0);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
(0, node_test_1.describe)('Metrics', () => {
|
|
117
|
+
(0, node_test_1.it)('records and aggregates correctly', async () => {
|
|
118
|
+
const { MetricsCollector } = await import('../metrics/collector.js');
|
|
119
|
+
const metrics = new MetricsCollector();
|
|
120
|
+
metrics.record({
|
|
121
|
+
id: 1, timestamp: Date.now(), provider: 'anthropic', model: 'claude-opus-4-6',
|
|
122
|
+
streaming: false, originalTokens: 1000, optimizedTokens: 300,
|
|
123
|
+
savingsPercent: 70, latencyOverheadMs: 12, chunksRetrieved: 5,
|
|
124
|
+
topScore: 0.89, passThrough: false,
|
|
125
|
+
});
|
|
126
|
+
metrics.record({
|
|
127
|
+
id: 2, timestamp: Date.now(), provider: 'openai', model: 'gpt-4o',
|
|
128
|
+
streaming: true, originalTokens: 500, optimizedTokens: 200,
|
|
129
|
+
savingsPercent: 60, latencyOverheadMs: 8, chunksRetrieved: 3,
|
|
130
|
+
topScore: 0.82, passThrough: false,
|
|
131
|
+
});
|
|
132
|
+
const stats = metrics.getStats();
|
|
133
|
+
node_assert_1.default.strictEqual(stats.totalRequests, 2);
|
|
134
|
+
node_assert_1.default.strictEqual(stats.totalOriginalTokens, 1500);
|
|
135
|
+
node_assert_1.default.strictEqual(stats.totalOptimizedTokens, 500);
|
|
136
|
+
node_assert_1.default.ok(stats.totalSavingsPercent > 50);
|
|
137
|
+
node_assert_1.default.ok(stats.byProvider['anthropic']);
|
|
138
|
+
node_assert_1.default.ok(stats.byModel['gpt-4o']);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
//# sourceMappingURL=context.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
const node_test_1 = require("node:test");
|
|
7
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
8
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
9
|
+
const server_js_1 = require("../proxy/server.js");
|
|
10
|
+
const auto_detect_js_1 = require("../config/auto-detect.js");
|
|
11
|
+
function httpRequest(url, options, body) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const req = node_http_1.default.request(url, options, (res) => {
|
|
14
|
+
let data = '';
|
|
15
|
+
res.on('data', (chunk) => (data += chunk));
|
|
16
|
+
res.on('end', () => resolve({ status: res.statusCode || 0, headers: res.headers, body: data }));
|
|
17
|
+
});
|
|
18
|
+
req.on('error', reject);
|
|
19
|
+
if (body)
|
|
20
|
+
req.write(body);
|
|
21
|
+
req.end();
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
(0, node_test_1.describe)('Dashboard & API', () => {
|
|
25
|
+
let proxy;
|
|
26
|
+
const PORT = 14810;
|
|
27
|
+
(0, node_test_1.before)(async () => {
|
|
28
|
+
const config = (0, auto_detect_js_1.buildConfig)({ proxy: { port: PORT, host: '127.0.0.1' } });
|
|
29
|
+
config.providers['anthropic'] = { apiKey: 'test', baseUrl: 'http://127.0.0.1:1' };
|
|
30
|
+
config.logging.level = 'error';
|
|
31
|
+
proxy = new server_js_1.ProxyServer(config);
|
|
32
|
+
await proxy.start();
|
|
33
|
+
});
|
|
34
|
+
(0, node_test_1.after)(async () => {
|
|
35
|
+
await proxy.stop();
|
|
36
|
+
});
|
|
37
|
+
(0, node_test_1.it)('serves dashboard at root path', async () => {
|
|
38
|
+
const res = await httpRequest(`http://127.0.0.1:${PORT}/`, { method: 'GET' });
|
|
39
|
+
node_assert_1.default.strictEqual(res.status, 200);
|
|
40
|
+
node_assert_1.default.ok(res.headers['content-type']?.includes('text/html'));
|
|
41
|
+
node_assert_1.default.ok(res.body.includes('SmartContext Proxy'));
|
|
42
|
+
node_assert_1.default.ok(res.body.includes('Total Requests'));
|
|
43
|
+
});
|
|
44
|
+
(0, node_test_1.it)('returns status via /_sc/status', async () => {
|
|
45
|
+
const res = await httpRequest(`http://127.0.0.1:${PORT}/_sc/status`, { method: 'GET' });
|
|
46
|
+
node_assert_1.default.strictEqual(res.status, 200);
|
|
47
|
+
const data = JSON.parse(res.body);
|
|
48
|
+
node_assert_1.default.strictEqual(data.state, 'running');
|
|
49
|
+
node_assert_1.default.ok(data.uptime >= 0);
|
|
50
|
+
});
|
|
51
|
+
(0, node_test_1.it)('returns stats via /_sc/stats', async () => {
|
|
52
|
+
const res = await httpRequest(`http://127.0.0.1:${PORT}/_sc/stats`, { method: 'GET' });
|
|
53
|
+
node_assert_1.default.strictEqual(res.status, 200);
|
|
54
|
+
const data = JSON.parse(res.body);
|
|
55
|
+
node_assert_1.default.strictEqual(data.totalRequests, 0);
|
|
56
|
+
node_assert_1.default.ok('byProvider' in data);
|
|
57
|
+
node_assert_1.default.ok('byModel' in data);
|
|
58
|
+
});
|
|
59
|
+
(0, node_test_1.it)('pause/resume works', async () => {
|
|
60
|
+
// Pause
|
|
61
|
+
let res = await httpRequest(`http://127.0.0.1:${PORT}/_sc/pause`, { method: 'POST' });
|
|
62
|
+
node_assert_1.default.strictEqual(res.status, 200);
|
|
63
|
+
let data = JSON.parse(res.body);
|
|
64
|
+
node_assert_1.default.strictEqual(data.state, 'paused');
|
|
65
|
+
// Check status
|
|
66
|
+
res = await httpRequest(`http://127.0.0.1:${PORT}/_sc/status`, { method: 'GET' });
|
|
67
|
+
data = JSON.parse(res.body);
|
|
68
|
+
node_assert_1.default.strictEqual(data.state, 'paused');
|
|
69
|
+
// Resume
|
|
70
|
+
res = await httpRequest(`http://127.0.0.1:${PORT}/_sc/resume`, { method: 'POST' });
|
|
71
|
+
data = JSON.parse(res.body);
|
|
72
|
+
node_assert_1.default.strictEqual(data.state, 'running');
|
|
73
|
+
});
|
|
74
|
+
(0, node_test_1.it)('returns feed via /_sc/feed', async () => {
|
|
75
|
+
const res = await httpRequest(`http://127.0.0.1:${PORT}/_sc/feed`, { method: 'GET' });
|
|
76
|
+
node_assert_1.default.strictEqual(res.status, 200);
|
|
77
|
+
const data = JSON.parse(res.body);
|
|
78
|
+
node_assert_1.default.ok(Array.isArray(data));
|
|
79
|
+
});
|
|
80
|
+
(0, node_test_1.it)('returns 404 for unknown API path', async () => {
|
|
81
|
+
const res = await httpRequest(`http://127.0.0.1:${PORT}/_sc/nonexistent`, { method: 'GET' });
|
|
82
|
+
node_assert_1.default.strictEqual(res.status, 404);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
//# sourceMappingURL=dashboard.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|