@hasna/terminal 3.3.9 → 3.4.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/dist/cli.js +8 -0
- package/dist/mcp/server.js +39 -6
- package/dist/sessions-db.js +58 -0
- package/package.json +1 -1
- package/src/cli.tsx +10 -0
- package/src/mcp/server.ts +48 -6
- package/src/sessions-db.ts +73 -0
package/dist/cli.js
CHANGED
|
@@ -78,6 +78,14 @@ if (args[0] === "uninstall") {
|
|
|
78
78
|
handleInstall(["uninstall"]);
|
|
79
79
|
process.exit(0);
|
|
80
80
|
}
|
|
81
|
+
// ── Prune ────────────────────────────────────────────────────────────────────
|
|
82
|
+
if (args[0] === "prune") {
|
|
83
|
+
const days = parseInt(args.find(a => a.startsWith("--older-than="))?.split("=")[1] ?? "90");
|
|
84
|
+
const { pruneSessions } = await import("./sessions-db.js");
|
|
85
|
+
const result = pruneSessions(days);
|
|
86
|
+
console.log(` Pruned ${result.sessionsDeleted} sessions, ${result.interactionsDeleted} interactions older than ${days}d`);
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
81
89
|
// ── MCP commands ─────────────────────────────────────────────────────────────
|
|
82
90
|
if (args[0] === "mcp") {
|
|
83
91
|
if (args[1] === "serve" || args.length === 1) {
|
package/dist/mcp/server.js
CHANGED
|
@@ -12,7 +12,7 @@ import { listRecipes, listCollections, getRecipe, createRecipe } from "../recipe
|
|
|
12
12
|
import { substituteVariables } from "../recipes/model.js";
|
|
13
13
|
import { bgStart, bgStatus, bgStop, bgLogs, bgWaitPort } from "../supervisor.js";
|
|
14
14
|
import { diffOutput } from "../diff-cache.js";
|
|
15
|
-
import { listSessions, getSessionInteractions, getSessionStats } from "../sessions-db.js";
|
|
15
|
+
import { createSession, logInteraction, listSessions, getSessionInteractions, getSessionStats, getSessionEconomy } from "../sessions-db.js";
|
|
16
16
|
import { cachedRead } from "../file-cache.js";
|
|
17
17
|
import { getBootContext, invalidateBootCache } from "../session-boot.js";
|
|
18
18
|
import { storeOutput, expandOutput } from "../expand-store.js";
|
|
@@ -57,8 +57,26 @@ function exec(command, cwd, timeout, allowRewrite = false) {
|
|
|
57
57
|
export function createServer() {
|
|
58
58
|
const server = new McpServer({
|
|
59
59
|
name: "terminal",
|
|
60
|
-
version: "
|
|
60
|
+
version: "3.4.0",
|
|
61
61
|
});
|
|
62
|
+
// Create a session for this MCP server instance
|
|
63
|
+
const sessionId = createSession(process.cwd(), "mcp");
|
|
64
|
+
/** Log a tool call to sessions.db for economy tracking */
|
|
65
|
+
function logCall(tool, data) {
|
|
66
|
+
try {
|
|
67
|
+
logInteraction(sessionId, {
|
|
68
|
+
nl: `[mcp:${tool}]${data.command ? ` ${data.command.slice(0, 200)}` : ""}`,
|
|
69
|
+
command: data.command?.slice(0, 500),
|
|
70
|
+
exitCode: data.exitCode,
|
|
71
|
+
tokensUsed: data.aiProcessed ? (data.outputTokens ?? 0) : 0,
|
|
72
|
+
tokensSaved: data.tokensSaved ?? 0,
|
|
73
|
+
durationMs: data.durationMs,
|
|
74
|
+
model: data.model,
|
|
75
|
+
cached: false,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch { } // never let logging break tool execution
|
|
79
|
+
}
|
|
62
80
|
// ── execute: run a command, return structured result ──────────────────────
|
|
63
81
|
server.tool("execute", "Run a shell command. Format guide: no format/raw for git commit/push (<50 tokens). format=compressed for long build output (CPU-only, no AI). format=json or format=summary for AI-summarized output (234ms, saves 80% tokens). Prefer execute_smart for most tasks.", {
|
|
64
82
|
command: z.string().describe("Shell command to execute"),
|
|
@@ -67,15 +85,16 @@ export function createServer() {
|
|
|
67
85
|
format: z.enum(["raw", "json", "compressed", "summary"]).optional().describe("Output format"),
|
|
68
86
|
maxTokens: z.number().optional().describe("Token budget for compressed/summary format"),
|
|
69
87
|
}, async ({ command, cwd, timeout, format, maxTokens }) => {
|
|
88
|
+
const start = Date.now();
|
|
70
89
|
const result = await exec(command, cwd, timeout ?? 30000);
|
|
71
90
|
const output = (result.stdout + result.stderr).trim();
|
|
72
91
|
// Raw mode — with lazy execution for large results
|
|
73
92
|
if (!format || format === "raw") {
|
|
74
93
|
const clean = stripAnsi(output);
|
|
75
|
-
// Lazy mode: if >100 lines, return count + sample instead of full output
|
|
76
94
|
if (shouldBeLazy(clean, command)) {
|
|
77
95
|
const lazy = toLazy(clean, command);
|
|
78
96
|
const detailKey = storeOutput(command, clean);
|
|
97
|
+
logCall("execute", { command, outputTokens: estimateTokens(clean), tokensSaved: 0, durationMs: Date.now() - start, exitCode: result.exitCode });
|
|
79
98
|
return {
|
|
80
99
|
content: [{ type: "text", text: JSON.stringify({
|
|
81
100
|
exitCode: result.exitCode, ...lazy, detailKey, duration: result.duration,
|
|
@@ -83,6 +102,7 @@ export function createServer() {
|
|
|
83
102
|
}) }],
|
|
84
103
|
};
|
|
85
104
|
}
|
|
105
|
+
logCall("execute", { command, outputTokens: estimateTokens(clean), tokensSaved: 0, durationMs: Date.now() - start, exitCode: result.exitCode });
|
|
86
106
|
return {
|
|
87
107
|
content: [{ type: "text", text: JSON.stringify({
|
|
88
108
|
exitCode: result.exitCode, output: clean, duration: result.duration, tokens: estimateTokens(clean),
|
|
@@ -95,6 +115,7 @@ export function createServer() {
|
|
|
95
115
|
try {
|
|
96
116
|
const processed = await processOutput(command, output);
|
|
97
117
|
const detailKey = output.split("\n").length > 15 ? storeOutput(command, output) : undefined;
|
|
118
|
+
logCall("execute", { command, outputTokens: estimateTokens(output), tokensSaved: processed.tokensSaved, durationMs: Date.now() - start, exitCode: result.exitCode, aiProcessed: processed.aiProcessed });
|
|
98
119
|
return {
|
|
99
120
|
content: [{ type: "text", text: JSON.stringify({
|
|
100
121
|
exitCode: result.exitCode,
|
|
@@ -109,6 +130,7 @@ export function createServer() {
|
|
|
109
130
|
}
|
|
110
131
|
catch {
|
|
111
132
|
const compressed = compress(command, output, { maxTokens });
|
|
133
|
+
logCall("execute", { command, outputTokens: estimateTokens(output), tokensSaved: compressed.tokensSaved, durationMs: Date.now() - start, exitCode: result.exitCode });
|
|
112
134
|
return {
|
|
113
135
|
content: [{ type: "text", text: JSON.stringify({
|
|
114
136
|
exitCode: result.exitCode, output: compressed.content, duration: result.duration,
|
|
@@ -135,11 +157,12 @@ export function createServer() {
|
|
|
135
157
|
cwd: z.string().optional().describe("Working directory"),
|
|
136
158
|
timeout: z.number().optional().describe("Timeout in ms (default: 30000)"),
|
|
137
159
|
}, async ({ command, cwd, timeout }) => {
|
|
138
|
-
const
|
|
160
|
+
const start = Date.now();
|
|
161
|
+
const result = await exec(command, cwd, timeout ?? 30000, true);
|
|
139
162
|
const output = (result.stdout + result.stderr).trim();
|
|
140
163
|
const processed = await processOutput(command, output);
|
|
141
|
-
// Progressive disclosure: store full output, return summary + expand key
|
|
142
164
|
const detailKey = output.split("\n").length > 15 ? storeOutput(command, output) : undefined;
|
|
165
|
+
logCall("execute_smart", { command, outputTokens: estimateTokens(output), tokensSaved: processed.tokensSaved, durationMs: Date.now() - start, exitCode: result.exitCode, aiProcessed: processed.aiProcessed });
|
|
143
166
|
return {
|
|
144
167
|
content: [{ type: "text", text: JSON.stringify({
|
|
145
168
|
exitCode: result.exitCode,
|
|
@@ -217,7 +240,9 @@ export function createServer() {
|
|
|
217
240
|
includeNodeModules: z.boolean().optional().describe("Include node_modules (default: false)"),
|
|
218
241
|
maxResults: z.number().optional().describe("Max results per category (default: 50)"),
|
|
219
242
|
}, async ({ pattern, path, includeNodeModules, maxResults }) => {
|
|
243
|
+
const start = Date.now();
|
|
220
244
|
const result = await searchFiles(pattern, path ?? process.cwd(), { includeNodeModules, maxResults });
|
|
245
|
+
logCall("search_files", { command: `search_files ${pattern}`, tokensSaved: result.tokensSaved ?? 0, durationMs: Date.now() - start });
|
|
221
246
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
222
247
|
});
|
|
223
248
|
// ── search_content: smart grep with grouping ──────────────────────────────
|
|
@@ -228,7 +253,9 @@ export function createServer() {
|
|
|
228
253
|
maxResults: z.number().optional().describe("Max files to return (default: 30)"),
|
|
229
254
|
contextLines: z.number().optional().describe("Context lines around matches (default: 0)"),
|
|
230
255
|
}, async ({ pattern, path, fileType, maxResults, contextLines }) => {
|
|
256
|
+
const start = Date.now();
|
|
231
257
|
const result = await searchContent(pattern, path ?? process.cwd(), { fileType, maxResults, contextLines });
|
|
258
|
+
logCall("search_content", { command: `grep ${pattern}`, tokensSaved: result.tokensSaved ?? 0, durationMs: Date.now() - start });
|
|
232
259
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
233
260
|
});
|
|
234
261
|
// ── search_semantic: AST-powered code search ───────────────────────────────
|
|
@@ -340,6 +367,7 @@ export function createServer() {
|
|
|
340
367
|
cwd: z.string().optional().describe("Working directory"),
|
|
341
368
|
timeout: z.number().optional().describe("Timeout in ms"),
|
|
342
369
|
}, async ({ command, cwd, timeout }) => {
|
|
370
|
+
const start = Date.now();
|
|
343
371
|
const workDir = cwd ?? process.cwd();
|
|
344
372
|
const result = await exec(command, workDir, timeout ?? 30000);
|
|
345
373
|
const output = (result.stdout + result.stderr).trim();
|
|
@@ -347,6 +375,7 @@ export function createServer() {
|
|
|
347
375
|
if (diff.tokensSaved > 0) {
|
|
348
376
|
recordSaving("diff", diff.tokensSaved);
|
|
349
377
|
}
|
|
378
|
+
logCall("execute_diff", { command, outputTokens: estimateTokens(output), tokensSaved: diff.tokensSaved, durationMs: Date.now() - start, exitCode: result.exitCode });
|
|
350
379
|
if (diff.unchanged) {
|
|
351
380
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
352
381
|
exitCode: result.exitCode, unchanged: true, diffSummary: diff.diffSummary,
|
|
@@ -388,7 +417,8 @@ export function createServer() {
|
|
|
388
417
|
}
|
|
389
418
|
if (action === "detail" && sessionId) {
|
|
390
419
|
const interactions = getSessionInteractions(sessionId);
|
|
391
|
-
|
|
420
|
+
const economy = getSessionEconomy(sessionId);
|
|
421
|
+
return { content: [{ type: "text", text: JSON.stringify({ interactions, economy }) }] };
|
|
392
422
|
}
|
|
393
423
|
const sessions = listSessions(limit ?? 20);
|
|
394
424
|
return { content: [{ type: "text", text: JSON.stringify(sessions) }] };
|
|
@@ -453,9 +483,11 @@ export function createServer() {
|
|
|
453
483
|
limit: z.number().optional().describe("Max lines to return"),
|
|
454
484
|
summarize: z.boolean().optional().describe("Return AI summary instead of full content (saves ~90% tokens)"),
|
|
455
485
|
}, async ({ path, offset, limit, summarize }) => {
|
|
486
|
+
const start = Date.now();
|
|
456
487
|
const result = cachedRead(path, { offset, limit });
|
|
457
488
|
if (summarize && result.content.length > 500) {
|
|
458
489
|
const processed = await processOutput(`cat ${path}`, result.content);
|
|
490
|
+
logCall("read_file", { command: path, outputTokens: estimateTokens(result.content), tokensSaved: processed.tokensSaved, durationMs: Date.now() - start, aiProcessed: true });
|
|
459
491
|
return {
|
|
460
492
|
content: [{ type: "text", text: JSON.stringify({
|
|
461
493
|
summary: processed.summary,
|
|
@@ -465,6 +497,7 @@ export function createServer() {
|
|
|
465
497
|
}) }],
|
|
466
498
|
};
|
|
467
499
|
}
|
|
500
|
+
logCall("read_file", { command: path, outputTokens: estimateTokens(result.content), tokensSaved: 0, durationMs: Date.now() - start });
|
|
468
501
|
return {
|
|
469
502
|
content: [{ type: "text", text: JSON.stringify({
|
|
470
503
|
content: result.content,
|
package/dist/sessions-db.js
CHANGED
|
@@ -137,6 +137,45 @@ export function getSessionStats() {
|
|
|
137
137
|
errorRate: totalInteractions > 0 ? (errors.c ?? 0) / totalInteractions : 0,
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
|
+
/** Get economy stats for a specific session */
|
|
141
|
+
export function getSessionEconomy(sessionId) {
|
|
142
|
+
const d = getDb();
|
|
143
|
+
const rows = d.prepare("SELECT nl, tokens_saved, tokens_used, duration_ms FROM interactions WHERE session_id = ?").all(sessionId);
|
|
144
|
+
const tools = {};
|
|
145
|
+
let totalSaved = 0, totalUsed = 0, aiCalls = 0, totalLatency = 0, latencyCount = 0;
|
|
146
|
+
for (const r of rows) {
|
|
147
|
+
totalSaved += r.tokens_saved ?? 0;
|
|
148
|
+
totalUsed += r.tokens_used ?? 0;
|
|
149
|
+
if (r.tokens_used > 0)
|
|
150
|
+
aiCalls++;
|
|
151
|
+
if (r.duration_ms) {
|
|
152
|
+
totalLatency += r.duration_ms;
|
|
153
|
+
latencyCount++;
|
|
154
|
+
}
|
|
155
|
+
// Extract tool name from nl field: [mcp:toolname] command
|
|
156
|
+
const toolMatch = r.nl.match(/^\[mcp:(\w+)\]/);
|
|
157
|
+
const tool = toolMatch?.[1] ?? "cli";
|
|
158
|
+
if (!tools[tool])
|
|
159
|
+
tools[tool] = { calls: 0, tokensSaved: 0 };
|
|
160
|
+
tools[tool].calls++;
|
|
161
|
+
tools[tool].tokensSaved += r.tokens_saved ?? 0;
|
|
162
|
+
}
|
|
163
|
+
// Savings at consumer model rates (×5 turns before compaction)
|
|
164
|
+
const multiplied = totalSaved * 5;
|
|
165
|
+
return {
|
|
166
|
+
totalCalls: rows.length,
|
|
167
|
+
tokensSaved: totalSaved,
|
|
168
|
+
tokensUsed: totalUsed,
|
|
169
|
+
aiCalls,
|
|
170
|
+
avgLatencyMs: latencyCount > 0 ? Math.round(totalLatency / latencyCount) : 0,
|
|
171
|
+
savingsUsd: {
|
|
172
|
+
opus: (multiplied * 15) / 1_000_000,
|
|
173
|
+
sonnet: (multiplied * 3) / 1_000_000,
|
|
174
|
+
haiku: (multiplied * 0.8) / 1_000_000,
|
|
175
|
+
},
|
|
176
|
+
tools,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
140
179
|
// ── Corrections ─────────────────────────────────────────────────────────────
|
|
141
180
|
/** Record a correction: command failed, then AI retried with a better one */
|
|
142
181
|
export function recordCorrection(prompt, failedCommand, errorOutput, correctedCommand, worked, errorType) {
|
|
@@ -164,6 +203,25 @@ export function findSimilarCorrections(prompt, limit = 5) {
|
|
|
164
203
|
export function recordOutput(command, rawOutputPath, compressedSummary, tokensRaw, tokensCompressed, provider, model, sessionId) {
|
|
165
204
|
getDb().prepare("INSERT INTO outputs (session_id, command, raw_output_path, compressed_summary, tokens_raw, tokens_compressed, provider, model, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)").run(sessionId ?? null, command, rawOutputPath ?? null, compressedSummary?.slice(0, 5000) ?? "", tokensRaw, tokensCompressed, provider ?? null, model ?? null, Date.now());
|
|
166
205
|
}
|
|
206
|
+
/** Prune sessions and interactions older than N days */
|
|
207
|
+
export function pruneSessions(olderThanDays = 90) {
|
|
208
|
+
const d = getDb();
|
|
209
|
+
const cutoff = Date.now() - (olderThanDays * 24 * 60 * 60 * 1000);
|
|
210
|
+
const oldSessions = d.prepare("SELECT id FROM sessions WHERE started_at < ?").all(cutoff);
|
|
211
|
+
if (oldSessions.length === 0)
|
|
212
|
+
return { sessionsDeleted: 0, interactionsDeleted: 0 };
|
|
213
|
+
const ids = oldSessions.map(s => s.id);
|
|
214
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
215
|
+
const intResult = d.prepare(`DELETE FROM interactions WHERE session_id IN (${placeholders})`).run(...ids);
|
|
216
|
+
const sesResult = d.prepare(`DELETE FROM sessions WHERE id IN (${placeholders})`).run(...ids);
|
|
217
|
+
// Also prune old corrections and outputs
|
|
218
|
+
d.prepare("DELETE FROM corrections WHERE created_at < ?").run(cutoff);
|
|
219
|
+
d.prepare("DELETE FROM outputs WHERE created_at < ?").run(cutoff);
|
|
220
|
+
return {
|
|
221
|
+
sessionsDeleted: oldSessions.length,
|
|
222
|
+
interactionsDeleted: intResult.changes ?? 0,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
167
225
|
/** Close the database connection */
|
|
168
226
|
export function closeDb() {
|
|
169
227
|
if (db) {
|
package/package.json
CHANGED
package/src/cli.tsx
CHANGED
|
@@ -83,6 +83,16 @@ if (args[0] === "uninstall") {
|
|
|
83
83
|
process.exit(0);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// ── Prune ────────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
if (args[0] === "prune") {
|
|
89
|
+
const days = parseInt(args.find(a => a.startsWith("--older-than="))?.split("=")[1] ?? "90");
|
|
90
|
+
const { pruneSessions } = await import("./sessions-db.js");
|
|
91
|
+
const result = pruneSessions(days);
|
|
92
|
+
console.log(` Pruned ${result.sessionsDeleted} sessions, ${result.interactionsDeleted} interactions older than ${days}d`);
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
|
|
86
96
|
// ── MCP commands ─────────────────────────────────────────────────────────────
|
|
87
97
|
|
|
88
98
|
if (args[0] === "mcp") {
|
package/src/mcp/server.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { listRecipes, listCollections, getRecipe, createRecipe } from "../recipe
|
|
|
13
13
|
import { substituteVariables } from "../recipes/model.js";
|
|
14
14
|
import { bgStart, bgStatus, bgStop, bgLogs, bgWaitPort } from "../supervisor.js";
|
|
15
15
|
import { diffOutput } from "../diff-cache.js";
|
|
16
|
-
import { listSessions, getSessionInteractions, getSessionStats } from "../sessions-db.js";
|
|
16
|
+
import { createSession, logInteraction, listSessions, getSessionInteractions, getSessionStats, getSessionEconomy } from "../sessions-db.js";
|
|
17
17
|
import { cachedRead, cacheStats } from "../file-cache.js";
|
|
18
18
|
import { getBootContext, invalidateBootCache } from "../session-boot.js";
|
|
19
19
|
import { storeOutput, expandOutput } from "../expand-store.js";
|
|
@@ -62,9 +62,36 @@ function exec(command: string, cwd?: string, timeout?: number, allowRewrite: boo
|
|
|
62
62
|
export function createServer(): McpServer {
|
|
63
63
|
const server = new McpServer({
|
|
64
64
|
name: "terminal",
|
|
65
|
-
version: "
|
|
65
|
+
version: "3.4.0",
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
// Create a session for this MCP server instance
|
|
69
|
+
const sessionId = createSession(process.cwd(), "mcp");
|
|
70
|
+
|
|
71
|
+
/** Log a tool call to sessions.db for economy tracking */
|
|
72
|
+
function logCall(tool: string, data: {
|
|
73
|
+
command?: string;
|
|
74
|
+
outputTokens?: number;
|
|
75
|
+
tokensSaved?: number;
|
|
76
|
+
durationMs?: number;
|
|
77
|
+
exitCode?: number;
|
|
78
|
+
aiProcessed?: boolean;
|
|
79
|
+
model?: string;
|
|
80
|
+
}) {
|
|
81
|
+
try {
|
|
82
|
+
logInteraction(sessionId, {
|
|
83
|
+
nl: `[mcp:${tool}]${data.command ? ` ${data.command.slice(0, 200)}` : ""}`,
|
|
84
|
+
command: data.command?.slice(0, 500),
|
|
85
|
+
exitCode: data.exitCode,
|
|
86
|
+
tokensUsed: data.aiProcessed ? (data.outputTokens ?? 0) : 0,
|
|
87
|
+
tokensSaved: data.tokensSaved ?? 0,
|
|
88
|
+
durationMs: data.durationMs,
|
|
89
|
+
model: data.model,
|
|
90
|
+
cached: false,
|
|
91
|
+
});
|
|
92
|
+
} catch {} // never let logging break tool execution
|
|
93
|
+
}
|
|
94
|
+
|
|
68
95
|
// ── execute: run a command, return structured result ──────────────────────
|
|
69
96
|
|
|
70
97
|
server.tool(
|
|
@@ -78,16 +105,17 @@ export function createServer(): McpServer {
|
|
|
78
105
|
maxTokens: z.number().optional().describe("Token budget for compressed/summary format"),
|
|
79
106
|
},
|
|
80
107
|
async ({ command, cwd, timeout, format, maxTokens }) => {
|
|
108
|
+
const start = Date.now();
|
|
81
109
|
const result = await exec(command, cwd, timeout ?? 30000);
|
|
82
110
|
const output = (result.stdout + result.stderr).trim();
|
|
83
111
|
|
|
84
112
|
// Raw mode — with lazy execution for large results
|
|
85
113
|
if (!format || format === "raw") {
|
|
86
114
|
const clean = stripAnsi(output);
|
|
87
|
-
// Lazy mode: if >100 lines, return count + sample instead of full output
|
|
88
115
|
if (shouldBeLazy(clean, command)) {
|
|
89
116
|
const lazy = toLazy(clean, command);
|
|
90
117
|
const detailKey = storeOutput(command, clean);
|
|
118
|
+
logCall("execute", { command, outputTokens: estimateTokens(clean), tokensSaved: 0, durationMs: Date.now() - start, exitCode: result.exitCode });
|
|
91
119
|
return {
|
|
92
120
|
content: [{ type: "text" as const, text: JSON.stringify({
|
|
93
121
|
exitCode: result.exitCode, ...lazy, detailKey, duration: result.duration,
|
|
@@ -95,6 +123,7 @@ export function createServer(): McpServer {
|
|
|
95
123
|
}) }],
|
|
96
124
|
};
|
|
97
125
|
}
|
|
126
|
+
logCall("execute", { command, outputTokens: estimateTokens(clean), tokensSaved: 0, durationMs: Date.now() - start, exitCode: result.exitCode });
|
|
98
127
|
return {
|
|
99
128
|
content: [{ type: "text" as const, text: JSON.stringify({
|
|
100
129
|
exitCode: result.exitCode, output: clean, duration: result.duration, tokens: estimateTokens(clean),
|
|
@@ -108,6 +137,7 @@ export function createServer(): McpServer {
|
|
|
108
137
|
try {
|
|
109
138
|
const processed = await processOutput(command, output);
|
|
110
139
|
const detailKey = output.split("\n").length > 15 ? storeOutput(command, output) : undefined;
|
|
140
|
+
logCall("execute", { command, outputTokens: estimateTokens(output), tokensSaved: processed.tokensSaved, durationMs: Date.now() - start, exitCode: result.exitCode, aiProcessed: processed.aiProcessed });
|
|
111
141
|
return {
|
|
112
142
|
content: [{ type: "text" as const, text: JSON.stringify({
|
|
113
143
|
exitCode: result.exitCode,
|
|
@@ -121,6 +151,7 @@ export function createServer(): McpServer {
|
|
|
121
151
|
};
|
|
122
152
|
} catch {
|
|
123
153
|
const compressed = compress(command, output, { maxTokens });
|
|
154
|
+
logCall("execute", { command, outputTokens: estimateTokens(output), tokensSaved: compressed.tokensSaved, durationMs: Date.now() - start, exitCode: result.exitCode });
|
|
124
155
|
return {
|
|
125
156
|
content: [{ type: "text" as const, text: JSON.stringify({
|
|
126
157
|
exitCode: result.exitCode, output: compressed.content, duration: result.duration,
|
|
@@ -156,12 +187,13 @@ export function createServer(): McpServer {
|
|
|
156
187
|
timeout: z.number().optional().describe("Timeout in ms (default: 30000)"),
|
|
157
188
|
},
|
|
158
189
|
async ({ command, cwd, timeout }) => {
|
|
159
|
-
const
|
|
190
|
+
const start = Date.now();
|
|
191
|
+
const result = await exec(command, cwd, timeout ?? 30000, true);
|
|
160
192
|
const output = (result.stdout + result.stderr).trim();
|
|
161
193
|
const processed = await processOutput(command, output);
|
|
162
194
|
|
|
163
|
-
// Progressive disclosure: store full output, return summary + expand key
|
|
164
195
|
const detailKey = output.split("\n").length > 15 ? storeOutput(command, output) : undefined;
|
|
196
|
+
logCall("execute_smart", { command, outputTokens: estimateTokens(output), tokensSaved: processed.tokensSaved, durationMs: Date.now() - start, exitCode: result.exitCode, aiProcessed: processed.aiProcessed });
|
|
165
197
|
|
|
166
198
|
return {
|
|
167
199
|
content: [{ type: "text" as const, text: JSON.stringify({
|
|
@@ -274,7 +306,9 @@ export function createServer(): McpServer {
|
|
|
274
306
|
maxResults: z.number().optional().describe("Max results per category (default: 50)"),
|
|
275
307
|
},
|
|
276
308
|
async ({ pattern, path, includeNodeModules, maxResults }) => {
|
|
309
|
+
const start = Date.now();
|
|
277
310
|
const result = await searchFiles(pattern, path ?? process.cwd(), { includeNodeModules, maxResults });
|
|
311
|
+
logCall("search_files", { command: `search_files ${pattern}`, tokensSaved: (result as any).tokensSaved ?? 0, durationMs: Date.now() - start });
|
|
278
312
|
return { content: [{ type: "text" as const, text: JSON.stringify(result) }] };
|
|
279
313
|
}
|
|
280
314
|
);
|
|
@@ -292,7 +326,9 @@ export function createServer(): McpServer {
|
|
|
292
326
|
contextLines: z.number().optional().describe("Context lines around matches (default: 0)"),
|
|
293
327
|
},
|
|
294
328
|
async ({ pattern, path, fileType, maxResults, contextLines }) => {
|
|
329
|
+
const start = Date.now();
|
|
295
330
|
const result = await searchContent(pattern, path ?? process.cwd(), { fileType, maxResults, contextLines });
|
|
331
|
+
logCall("search_content", { command: `grep ${pattern}`, tokensSaved: result.tokensSaved ?? 0, durationMs: Date.now() - start });
|
|
296
332
|
return { content: [{ type: "text" as const, text: JSON.stringify(result) }] };
|
|
297
333
|
}
|
|
298
334
|
);
|
|
@@ -482,6 +518,7 @@ export function createServer(): McpServer {
|
|
|
482
518
|
timeout: z.number().optional().describe("Timeout in ms"),
|
|
483
519
|
},
|
|
484
520
|
async ({ command, cwd, timeout }) => {
|
|
521
|
+
const start = Date.now();
|
|
485
522
|
const workDir = cwd ?? process.cwd();
|
|
486
523
|
const result = await exec(command, workDir, timeout ?? 30000);
|
|
487
524
|
const output = (result.stdout + result.stderr).trim();
|
|
@@ -490,6 +527,7 @@ export function createServer(): McpServer {
|
|
|
490
527
|
if (diff.tokensSaved > 0) {
|
|
491
528
|
recordSaving("diff", diff.tokensSaved);
|
|
492
529
|
}
|
|
530
|
+
logCall("execute_diff", { command, outputTokens: estimateTokens(output), tokensSaved: diff.tokensSaved, durationMs: Date.now() - start, exitCode: result.exitCode });
|
|
493
531
|
|
|
494
532
|
if (diff.unchanged) {
|
|
495
533
|
return { content: [{ type: "text" as const, text: JSON.stringify({
|
|
@@ -553,7 +591,8 @@ export function createServer(): McpServer {
|
|
|
553
591
|
}
|
|
554
592
|
if (action === "detail" && sessionId) {
|
|
555
593
|
const interactions = getSessionInteractions(sessionId);
|
|
556
|
-
|
|
594
|
+
const economy = getSessionEconomy(sessionId);
|
|
595
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ interactions, economy }) }] };
|
|
557
596
|
}
|
|
558
597
|
const sessions = listSessions(limit ?? 20);
|
|
559
598
|
return { content: [{ type: "text" as const, text: JSON.stringify(sessions) }] };
|
|
@@ -646,10 +685,12 @@ export function createServer(): McpServer {
|
|
|
646
685
|
summarize: z.boolean().optional().describe("Return AI summary instead of full content (saves ~90% tokens)"),
|
|
647
686
|
},
|
|
648
687
|
async ({ path, offset, limit, summarize }) => {
|
|
688
|
+
const start = Date.now();
|
|
649
689
|
const result = cachedRead(path, { offset, limit });
|
|
650
690
|
|
|
651
691
|
if (summarize && result.content.length > 500) {
|
|
652
692
|
const processed = await processOutput(`cat ${path}`, result.content);
|
|
693
|
+
logCall("read_file", { command: path, outputTokens: estimateTokens(result.content), tokensSaved: processed.tokensSaved, durationMs: Date.now() - start, aiProcessed: true });
|
|
653
694
|
return {
|
|
654
695
|
content: [{ type: "text" as const, text: JSON.stringify({
|
|
655
696
|
summary: processed.summary,
|
|
@@ -660,6 +701,7 @@ export function createServer(): McpServer {
|
|
|
660
701
|
};
|
|
661
702
|
}
|
|
662
703
|
|
|
704
|
+
logCall("read_file", { command: path, outputTokens: estimateTokens(result.content), tokensSaved: 0, durationMs: Date.now() - start });
|
|
663
705
|
return {
|
|
664
706
|
content: [{ type: "text" as const, text: JSON.stringify({
|
|
665
707
|
content: result.content,
|
package/src/sessions-db.ts
CHANGED
|
@@ -212,6 +212,55 @@ export function getSessionStats(): SessionStats {
|
|
|
212
212
|
};
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
/** Get economy stats for a specific session */
|
|
216
|
+
export function getSessionEconomy(sessionId: string): {
|
|
217
|
+
totalCalls: number;
|
|
218
|
+
tokensSaved: number;
|
|
219
|
+
tokensUsed: number;
|
|
220
|
+
aiCalls: number;
|
|
221
|
+
avgLatencyMs: number;
|
|
222
|
+
savingsUsd: { opus: number; sonnet: number; haiku: number };
|
|
223
|
+
tools: Record<string, { calls: number; tokensSaved: number }>;
|
|
224
|
+
} {
|
|
225
|
+
const d = getDb();
|
|
226
|
+
const rows = d.prepare(
|
|
227
|
+
"SELECT nl, tokens_saved, tokens_used, duration_ms FROM interactions WHERE session_id = ?"
|
|
228
|
+
).all(sessionId) as { nl: string; tokens_saved: number; tokens_used: number; duration_ms: number | null }[];
|
|
229
|
+
|
|
230
|
+
const tools: Record<string, { calls: number; tokensSaved: number }> = {};
|
|
231
|
+
let totalSaved = 0, totalUsed = 0, aiCalls = 0, totalLatency = 0, latencyCount = 0;
|
|
232
|
+
|
|
233
|
+
for (const r of rows) {
|
|
234
|
+
totalSaved += r.tokens_saved ?? 0;
|
|
235
|
+
totalUsed += r.tokens_used ?? 0;
|
|
236
|
+
if (r.tokens_used > 0) aiCalls++;
|
|
237
|
+
if (r.duration_ms) { totalLatency += r.duration_ms; latencyCount++; }
|
|
238
|
+
|
|
239
|
+
// Extract tool name from nl field: [mcp:toolname] command
|
|
240
|
+
const toolMatch = r.nl.match(/^\[mcp:(\w+)\]/);
|
|
241
|
+
const tool = toolMatch?.[1] ?? "cli";
|
|
242
|
+
if (!tools[tool]) tools[tool] = { calls: 0, tokensSaved: 0 };
|
|
243
|
+
tools[tool].calls++;
|
|
244
|
+
tools[tool].tokensSaved += r.tokens_saved ?? 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Savings at consumer model rates (×5 turns before compaction)
|
|
248
|
+
const multiplied = totalSaved * 5;
|
|
249
|
+
return {
|
|
250
|
+
totalCalls: rows.length,
|
|
251
|
+
tokensSaved: totalSaved,
|
|
252
|
+
tokensUsed: totalUsed,
|
|
253
|
+
aiCalls,
|
|
254
|
+
avgLatencyMs: latencyCount > 0 ? Math.round(totalLatency / latencyCount) : 0,
|
|
255
|
+
savingsUsd: {
|
|
256
|
+
opus: (multiplied * 15) / 1_000_000,
|
|
257
|
+
sonnet: (multiplied * 3) / 1_000_000,
|
|
258
|
+
haiku: (multiplied * 0.8) / 1_000_000,
|
|
259
|
+
},
|
|
260
|
+
tools,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
215
264
|
// ── Corrections ─────────────────────────────────────────────────────────────
|
|
216
265
|
|
|
217
266
|
/** Record a correction: command failed, then AI retried with a better one */
|
|
@@ -267,6 +316,30 @@ export function recordOutput(
|
|
|
267
316
|
).run(sessionId ?? null, command, rawOutputPath ?? null, compressedSummary?.slice(0, 5000) ?? "", tokensRaw, tokensCompressed, provider ?? null, model ?? null, Date.now());
|
|
268
317
|
}
|
|
269
318
|
|
|
319
|
+
/** Prune sessions and interactions older than N days */
|
|
320
|
+
export function pruneSessions(olderThanDays: number = 90): { sessionsDeleted: number; interactionsDeleted: number } {
|
|
321
|
+
const d = getDb();
|
|
322
|
+
const cutoff = Date.now() - (olderThanDays * 24 * 60 * 60 * 1000);
|
|
323
|
+
const oldSessions = d.prepare("SELECT id FROM sessions WHERE started_at < ?").all(cutoff) as { id: string }[];
|
|
324
|
+
|
|
325
|
+
if (oldSessions.length === 0) return { sessionsDeleted: 0, interactionsDeleted: 0 };
|
|
326
|
+
|
|
327
|
+
const ids = oldSessions.map(s => s.id);
|
|
328
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
329
|
+
|
|
330
|
+
const intResult = d.prepare(`DELETE FROM interactions WHERE session_id IN (${placeholders})`).run(...ids);
|
|
331
|
+
const sesResult = d.prepare(`DELETE FROM sessions WHERE id IN (${placeholders})`).run(...ids);
|
|
332
|
+
|
|
333
|
+
// Also prune old corrections and outputs
|
|
334
|
+
d.prepare("DELETE FROM corrections WHERE created_at < ?").run(cutoff);
|
|
335
|
+
d.prepare("DELETE FROM outputs WHERE created_at < ?").run(cutoff);
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
sessionsDeleted: oldSessions.length,
|
|
339
|
+
interactionsDeleted: (intResult as any).changes ?? 0,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
270
343
|
/** Close the database connection */
|
|
271
344
|
export function closeDb(): void {
|
|
272
345
|
if (db) { db.close(); db = null; }
|