@hasna/terminal 4.1.0 → 4.3.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/mcp/server.js +162 -6
- package/dist/output-processor.js +7 -3
- package/package.json +2 -1
- package/src/mcp/server.ts +164 -6
- package/src/output-processor.ts +7 -2
package/dist/mcp/server.js
CHANGED
|
@@ -67,10 +67,22 @@ function resolvePath(p, cwd) {
|
|
|
67
67
|
export function createServer() {
|
|
68
68
|
const server = new McpServer({
|
|
69
69
|
name: "terminal",
|
|
70
|
-
version: "
|
|
70
|
+
version: "4.2.0",
|
|
71
71
|
});
|
|
72
72
|
// Create a session for this MCP server instance
|
|
73
73
|
const sessionId = createSession(process.cwd(), "mcp");
|
|
74
|
+
// ── Mementos: cross-session project memory ────────────────────────────────
|
|
75
|
+
let mementosProjectId = null;
|
|
76
|
+
try {
|
|
77
|
+
const mementos = require("@hasna/mementos");
|
|
78
|
+
const projectName = process.cwd().split("/").pop() ?? "unknown";
|
|
79
|
+
const project = mementos.registerProject(projectName, process.cwd());
|
|
80
|
+
mementosProjectId = project?.id ?? null;
|
|
81
|
+
mementos.registerAgent("terminal-mcp");
|
|
82
|
+
if (mementosProjectId)
|
|
83
|
+
mementos.setFocus(mementosProjectId);
|
|
84
|
+
}
|
|
85
|
+
catch { } // mementos optional — works without it
|
|
74
86
|
/** Log a tool call to sessions.db for economy tracking */
|
|
75
87
|
function logCall(tool, data) {
|
|
76
88
|
try {
|
|
@@ -166,11 +178,12 @@ export function createServer() {
|
|
|
166
178
|
command: z.string().describe("Shell command to execute"),
|
|
167
179
|
cwd: z.string().optional().describe("Working directory"),
|
|
168
180
|
timeout: z.number().optional().describe("Timeout in ms (default: 30000)"),
|
|
169
|
-
|
|
181
|
+
verbosity: z.enum(["minimal", "normal", "detailed"]).optional().describe("Summary detail level (default: normal)"),
|
|
182
|
+
}, async ({ command, cwd, timeout, verbosity }) => {
|
|
170
183
|
const start = Date.now();
|
|
171
184
|
const result = await exec(command, cwd, timeout ?? 30000, true);
|
|
172
185
|
const output = (result.stdout + result.stderr).trim();
|
|
173
|
-
const processed = await processOutput(command, output);
|
|
186
|
+
const processed = await processOutput(command, output, undefined, verbosity);
|
|
174
187
|
const detailKey = output.split("\n").length > 15 ? storeOutput(command, output) : undefined;
|
|
175
188
|
logCall("execute_smart", { command, outputTokens: estimateTokens(output), tokensSaved: processed.tokensSaved, durationMs: Date.now() - start, exitCode: result.exitCode, aiProcessed: processed.aiProcessed });
|
|
176
189
|
return {
|
|
@@ -498,18 +511,21 @@ export function createServer() {
|
|
|
498
511
|
offset: z.number().optional().describe("Start line (0-indexed)"),
|
|
499
512
|
limit: z.number().optional().describe("Max lines to return"),
|
|
500
513
|
summarize: z.boolean().optional().describe("Return AI summary instead of full content (saves ~90% tokens)"),
|
|
501
|
-
|
|
514
|
+
focus: z.string().optional().describe("Focus hint for summary (e.g., 'public API', 'error handling', 'auth logic')"),
|
|
515
|
+
}, async ({ path: rawPath, offset, limit, summarize, focus }) => {
|
|
502
516
|
const start = Date.now();
|
|
503
517
|
const path = resolvePath(rawPath);
|
|
504
518
|
const result = cachedRead(path, { offset, limit });
|
|
505
519
|
if (summarize && result.content.length > 500) {
|
|
506
|
-
// AI-native file summary — ask directly what the file does
|
|
507
520
|
const provider = getOutputProvider();
|
|
508
521
|
const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
|
|
509
522
|
const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
|
|
523
|
+
const focusInstruction = focus
|
|
524
|
+
? `Focus specifically on: ${focus}. Describe only aspects related to "${focus}".`
|
|
525
|
+
: `Describe what this source file does in 2-4 lines. Include: main class/module name, key methods/functions, what it exports, and its purpose.`;
|
|
510
526
|
const summary = await provider.complete(`File: ${path}\n\n${content}`, {
|
|
511
527
|
model: outputModel,
|
|
512
|
-
system:
|
|
528
|
+
system: `${focusInstruction} Be specific — name the actual functions and what they do. Never just say "N lines of code."`,
|
|
513
529
|
maxTokens: 300,
|
|
514
530
|
temperature: 0.2,
|
|
515
531
|
});
|
|
@@ -1208,6 +1224,146 @@ Be specific, not generic. Only flag real problems.`,
|
|
|
1208
1224
|
});
|
|
1209
1225
|
return { content: [{ type: "text", text: recommendation }] };
|
|
1210
1226
|
});
|
|
1227
|
+
// ── batch: multiple operations in one round trip ───────────────────────────
|
|
1228
|
+
server.tool("batch", "Run multiple operations in ONE call. Saves N-1 round trips. Each op can be: execute (run command), read (file read/summarize), search (grep pattern), or symbols (file outline).", {
|
|
1229
|
+
ops: z.array(z.object({
|
|
1230
|
+
type: z.enum(["execute", "read", "write", "search", "symbols"]).describe("Operation type"),
|
|
1231
|
+
command: z.string().optional().describe("Shell command (for execute)"),
|
|
1232
|
+
path: z.string().optional().describe("File path (for read/write/symbols)"),
|
|
1233
|
+
content: z.string().optional().describe("File content (for write)"),
|
|
1234
|
+
pattern: z.string().optional().describe("Search pattern (for search)"),
|
|
1235
|
+
summarize: z.boolean().optional().describe("AI summarize (for read)"),
|
|
1236
|
+
format: z.enum(["raw", "summary"]).optional().describe("Output format (for execute)"),
|
|
1237
|
+
})).describe("Array of operations to run"),
|
|
1238
|
+
cwd: z.string().optional().describe("Working directory for all ops"),
|
|
1239
|
+
}, async ({ ops, cwd }) => {
|
|
1240
|
+
const start = Date.now();
|
|
1241
|
+
const workDir = cwd ?? process.cwd();
|
|
1242
|
+
const results = [];
|
|
1243
|
+
for (let i = 0; i < ops.slice(0, 10).length; i++) {
|
|
1244
|
+
const op = ops[i];
|
|
1245
|
+
try {
|
|
1246
|
+
if (op.type === "execute" && op.command) {
|
|
1247
|
+
const result = await exec(op.command, workDir, 30000);
|
|
1248
|
+
const output = (result.stdout + result.stderr).trim();
|
|
1249
|
+
if (op.format === "summary" && output.split("\n").length > 15) {
|
|
1250
|
+
const processed = await processOutput(op.command, output);
|
|
1251
|
+
results.push({ op: i, type: "execute", summary: processed.summary, exitCode: result.exitCode, tokensSaved: processed.tokensSaved });
|
|
1252
|
+
}
|
|
1253
|
+
else {
|
|
1254
|
+
results.push({ op: i, type: "execute", output: stripAnsi(output).slice(0, 2000), exitCode: result.exitCode });
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
else if (op.type === "read" && op.path) {
|
|
1258
|
+
const filePath = resolvePath(op.path, workDir);
|
|
1259
|
+
const result = cachedRead(filePath, {});
|
|
1260
|
+
if (op.summarize && result.content.length > 500) {
|
|
1261
|
+
const provider = getOutputProvider();
|
|
1262
|
+
const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
|
|
1263
|
+
const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
|
|
1264
|
+
const summary = await provider.complete(`File: ${filePath}\n\n${content}`, {
|
|
1265
|
+
model: outputModel,
|
|
1266
|
+
system: `Describe what this source file does in 2-4 lines. Include: main class/module name, key methods/functions, what it exports, and its purpose. Be specific.`,
|
|
1267
|
+
maxTokens: 300, temperature: 0.2,
|
|
1268
|
+
});
|
|
1269
|
+
results.push({ op: i, type: "read", path: op.path, summary, lines: result.content.split("\n").length });
|
|
1270
|
+
}
|
|
1271
|
+
else {
|
|
1272
|
+
results.push({ op: i, type: "read", path: op.path, content: result.content, lines: result.content.split("\n").length });
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
else if (op.type === "write" && op.path && op.content !== undefined) {
|
|
1276
|
+
const filePath = resolvePath(op.path, workDir);
|
|
1277
|
+
const { writeFileSync, mkdirSync, existsSync } = await import("fs");
|
|
1278
|
+
const { dirname } = await import("path");
|
|
1279
|
+
const dir = dirname(filePath);
|
|
1280
|
+
if (!existsSync(dir))
|
|
1281
|
+
mkdirSync(dir, { recursive: true });
|
|
1282
|
+
writeFileSync(filePath, op.content);
|
|
1283
|
+
results.push({ op: i, type: "write", path: op.path, ok: true, bytes: op.content.length });
|
|
1284
|
+
}
|
|
1285
|
+
else if (op.type === "search" && op.pattern) {
|
|
1286
|
+
// Search accepts both files and directories — resolve to parent dir if file
|
|
1287
|
+
let searchPath = op.path ? resolvePath(op.path, workDir) : workDir;
|
|
1288
|
+
try {
|
|
1289
|
+
const { statSync } = await import("fs");
|
|
1290
|
+
if (statSync(searchPath).isFile())
|
|
1291
|
+
searchPath = searchPath.replace(/\/[^/]+$/, "");
|
|
1292
|
+
}
|
|
1293
|
+
catch { }
|
|
1294
|
+
const result = await searchContent(op.pattern, searchPath, {});
|
|
1295
|
+
results.push({ op: i, type: "search", pattern: op.pattern, totalMatches: result.totalMatches, files: result.files.slice(0, 10) });
|
|
1296
|
+
}
|
|
1297
|
+
else if (op.type === "symbols" && op.path) {
|
|
1298
|
+
const filePath = resolvePath(op.path, workDir);
|
|
1299
|
+
const result = cachedRead(filePath, {});
|
|
1300
|
+
if (result.content && !result.content.startsWith("Error:")) {
|
|
1301
|
+
const provider = getOutputProvider();
|
|
1302
|
+
const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
|
|
1303
|
+
const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
|
|
1304
|
+
const summary = await provider.complete(`File: ${filePath}\n\n${content}`, {
|
|
1305
|
+
model: outputModel,
|
|
1306
|
+
system: `Extract all symbols. Return ONLY a JSON array. Each: {"name":"x","kind":"function|class|method|interface|type","line":N,"signature":"brief"}. For class methods use "Class.method". Exclude imports.`,
|
|
1307
|
+
maxTokens: 2000, temperature: 0,
|
|
1308
|
+
});
|
|
1309
|
+
let symbols = [];
|
|
1310
|
+
try {
|
|
1311
|
+
const m = summary.match(/\[[\s\S]*\]/);
|
|
1312
|
+
if (m)
|
|
1313
|
+
symbols = JSON.parse(m[0]);
|
|
1314
|
+
}
|
|
1315
|
+
catch { }
|
|
1316
|
+
results.push({ op: i, type: "symbols", path: op.path, symbols });
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
results.push({ op: i, type: "symbols", path: op.path, error: "Cannot read file" });
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
catch (err) {
|
|
1324
|
+
results.push({ op: i, type: op.type, error: err.message?.slice(0, 200) });
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
logCall("batch", { command: `${ops.length} ops`, durationMs: Date.now() - start, aiProcessed: true });
|
|
1328
|
+
return { content: [{ type: "text", text: JSON.stringify({ results, total: results.length, durationMs: Date.now() - start }) }] };
|
|
1329
|
+
});
|
|
1330
|
+
// ── Cross-session memory (mementos SDK) ────────────────────────────────────
|
|
1331
|
+
server.tool("remember", "Save a learning about this project for future sessions. Persists across restarts. Use for: project patterns, conventions, toolchain quirks, architectural decisions.", {
|
|
1332
|
+
key: z.string().describe("Short key (e.g., 'test-command', 'deploy-process', 'auth-pattern')"),
|
|
1333
|
+
value: z.string().describe("What to remember"),
|
|
1334
|
+
importance: z.number().optional().describe("1-10, default 7"),
|
|
1335
|
+
}, async ({ key, value, importance }) => {
|
|
1336
|
+
try {
|
|
1337
|
+
const mementos = require("@hasna/mementos");
|
|
1338
|
+
mementos.createMemory({ key, value, scope: "shared", category: "knowledge", importance: importance ?? 7 });
|
|
1339
|
+
logCall("remember", { command: `remember: ${key}` });
|
|
1340
|
+
return { content: [{ type: "text", text: JSON.stringify({ saved: key }) }] };
|
|
1341
|
+
}
|
|
1342
|
+
catch (e) {
|
|
1343
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: e.message?.slice(0, 200) }) }] };
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
server.tool("recall", "Recall project memories from previous sessions. Returns all saved learnings, patterns, and decisions for this project.", {
|
|
1347
|
+
search: z.string().optional().describe("Search query to filter memories"),
|
|
1348
|
+
limit: z.number().optional().describe("Max memories to return (default: 20)"),
|
|
1349
|
+
}, async ({ search, limit }) => {
|
|
1350
|
+
try {
|
|
1351
|
+
const mementos = require("@hasna/mementos");
|
|
1352
|
+
let memories;
|
|
1353
|
+
if (search) {
|
|
1354
|
+
memories = mementos.searchMemories(search, { limit: limit ?? 20 });
|
|
1355
|
+
}
|
|
1356
|
+
else {
|
|
1357
|
+
memories = mementos.listMemories({ scope: "shared", limit: limit ?? 20 });
|
|
1358
|
+
}
|
|
1359
|
+
const items = (memories ?? []).map((m) => ({ key: m.key, value: m.value, importance: m.importance }));
|
|
1360
|
+
logCall("recall", { command: `recall${search ? `: ${search}` : ""}` });
|
|
1361
|
+
return { content: [{ type: "text", text: JSON.stringify({ memories: items, total: items.length }) }] };
|
|
1362
|
+
}
|
|
1363
|
+
catch (e) {
|
|
1364
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: e.message?.slice(0, 200), memories: [] }) }] };
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1211
1367
|
return server;
|
|
1212
1368
|
}
|
|
1213
1369
|
// ── main: start MCP server via stdio ─────────────────────────────────────────
|
package/dist/output-processor.js
CHANGED
|
@@ -102,7 +102,7 @@ RULES:
|
|
|
102
102
|
* Process command output through AI summarization.
|
|
103
103
|
* Cheap AI call (~100 tokens) saves 1000+ tokens downstream.
|
|
104
104
|
*/
|
|
105
|
-
export async function processOutput(command, output, originalPrompt) {
|
|
105
|
+
export async function processOutput(command, output, originalPrompt, verbosity) {
|
|
106
106
|
const lines = output.split("\n");
|
|
107
107
|
// Fingerprint check — skip AI entirely for known patterns (0ms, $0)
|
|
108
108
|
const fp = fingerprint(command, output);
|
|
@@ -157,10 +157,14 @@ export async function processOutput(command, output, originalPrompt) {
|
|
|
157
157
|
// Falls back to main provider if Groq unavailable
|
|
158
158
|
const provider = getOutputProvider();
|
|
159
159
|
const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
|
|
160
|
+
const verbosityHint = verbosity === "minimal" ? "\nBe ULTRA concise — 1-2 lines max. Status + key number only."
|
|
161
|
+
: verbosity === "detailed" ? "\nBe thorough — include all relevant details, up to 15 lines."
|
|
162
|
+
: ""; // normal = default 8 lines from SUMMARIZE_PROMPT
|
|
163
|
+
const maxTok = verbosity === "minimal" ? 100 : verbosity === "detailed" ? 500 : 300;
|
|
160
164
|
const summary = await provider.complete(`${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}${hintsBlock}${profileHints}`, {
|
|
161
165
|
model: outputModel,
|
|
162
|
-
system: SUMMARIZE_PROMPT,
|
|
163
|
-
maxTokens:
|
|
166
|
+
system: SUMMARIZE_PROMPT + verbosityHint,
|
|
167
|
+
maxTokens: maxTok,
|
|
164
168
|
temperature: 0.2,
|
|
165
169
|
});
|
|
166
170
|
const originalTokens = estimateTokens(output);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/terminal",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "Smart terminal wrapper for AI agents and humans — structured output, token compression, MCP server, natural language",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
25
|
+
"@hasna/mementos": "^0.10.0",
|
|
25
26
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
26
27
|
"@typescript/vfs": "^1.6.4",
|
|
27
28
|
"ink": "^5.0.1",
|
package/src/mcp/server.ts
CHANGED
|
@@ -71,12 +71,23 @@ function resolvePath(p: string, cwd?: string): string {
|
|
|
71
71
|
export function createServer(): McpServer {
|
|
72
72
|
const server = new McpServer({
|
|
73
73
|
name: "terminal",
|
|
74
|
-
version: "
|
|
74
|
+
version: "4.2.0",
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
// Create a session for this MCP server instance
|
|
78
78
|
const sessionId = createSession(process.cwd(), "mcp");
|
|
79
79
|
|
|
80
|
+
// ── Mementos: cross-session project memory ────────────────────────────────
|
|
81
|
+
let mementosProjectId: string | null = null;
|
|
82
|
+
try {
|
|
83
|
+
const mementos = require("@hasna/mementos");
|
|
84
|
+
const projectName = process.cwd().split("/").pop() ?? "unknown";
|
|
85
|
+
const project = mementos.registerProject(projectName, process.cwd());
|
|
86
|
+
mementosProjectId = project?.id ?? null;
|
|
87
|
+
mementos.registerAgent("terminal-mcp");
|
|
88
|
+
if (mementosProjectId) mementos.setFocus(mementosProjectId);
|
|
89
|
+
} catch {} // mementos optional — works without it
|
|
90
|
+
|
|
80
91
|
/** Log a tool call to sessions.db for economy tracking */
|
|
81
92
|
function logCall(tool: string, data: {
|
|
82
93
|
command?: string;
|
|
@@ -194,12 +205,13 @@ export function createServer(): McpServer {
|
|
|
194
205
|
command: z.string().describe("Shell command to execute"),
|
|
195
206
|
cwd: z.string().optional().describe("Working directory"),
|
|
196
207
|
timeout: z.number().optional().describe("Timeout in ms (default: 30000)"),
|
|
208
|
+
verbosity: z.enum(["minimal", "normal", "detailed"]).optional().describe("Summary detail level (default: normal)"),
|
|
197
209
|
},
|
|
198
|
-
async ({ command, cwd, timeout }) => {
|
|
210
|
+
async ({ command, cwd, timeout, verbosity }) => {
|
|
199
211
|
const start = Date.now();
|
|
200
212
|
const result = await exec(command, cwd, timeout ?? 30000, true);
|
|
201
213
|
const output = (result.stdout + result.stderr).trim();
|
|
202
|
-
const processed = await processOutput(command, output);
|
|
214
|
+
const processed = await processOutput(command, output, undefined, verbosity);
|
|
203
215
|
|
|
204
216
|
const detailKey = output.split("\n").length > 15 ? storeOutput(command, output) : undefined;
|
|
205
217
|
logCall("execute_smart", { command, outputTokens: estimateTokens(output), tokensSaved: processed.tokensSaved, durationMs: Date.now() - start, exitCode: result.exitCode, aiProcessed: processed.aiProcessed });
|
|
@@ -698,22 +710,25 @@ export function createServer(): McpServer {
|
|
|
698
710
|
offset: z.number().optional().describe("Start line (0-indexed)"),
|
|
699
711
|
limit: z.number().optional().describe("Max lines to return"),
|
|
700
712
|
summarize: z.boolean().optional().describe("Return AI summary instead of full content (saves ~90% tokens)"),
|
|
713
|
+
focus: z.string().optional().describe("Focus hint for summary (e.g., 'public API', 'error handling', 'auth logic')"),
|
|
701
714
|
},
|
|
702
|
-
async ({ path: rawPath, offset, limit, summarize }) => {
|
|
715
|
+
async ({ path: rawPath, offset, limit, summarize, focus }) => {
|
|
703
716
|
const start = Date.now();
|
|
704
717
|
const path = resolvePath(rawPath);
|
|
705
718
|
const result = cachedRead(path, { offset, limit });
|
|
706
719
|
|
|
707
720
|
if (summarize && result.content.length > 500) {
|
|
708
|
-
// AI-native file summary — ask directly what the file does
|
|
709
721
|
const provider = getOutputProvider();
|
|
710
722
|
const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
|
|
711
723
|
const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
|
|
724
|
+
const focusInstruction = focus
|
|
725
|
+
? `Focus specifically on: ${focus}. Describe only aspects related to "${focus}".`
|
|
726
|
+
: `Describe what this source file does in 2-4 lines. Include: main class/module name, key methods/functions, what it exports, and its purpose.`;
|
|
712
727
|
const summary = await provider.complete(
|
|
713
728
|
`File: ${path}\n\n${content}`,
|
|
714
729
|
{
|
|
715
730
|
model: outputModel,
|
|
716
|
-
system:
|
|
731
|
+
system: `${focusInstruction} Be specific — name the actual functions and what they do. Never just say "N lines of code."`,
|
|
717
732
|
maxTokens: 300,
|
|
718
733
|
temperature: 0.2,
|
|
719
734
|
}
|
|
@@ -1570,6 +1585,149 @@ Be specific, not generic. Only flag real problems.`,
|
|
|
1570
1585
|
}
|
|
1571
1586
|
);
|
|
1572
1587
|
|
|
1588
|
+
// ── batch: multiple operations in one round trip ───────────────────────────
|
|
1589
|
+
|
|
1590
|
+
server.tool(
|
|
1591
|
+
"batch",
|
|
1592
|
+
"Run multiple operations in ONE call. Saves N-1 round trips. Each op can be: execute (run command), read (file read/summarize), search (grep pattern), or symbols (file outline).",
|
|
1593
|
+
{
|
|
1594
|
+
ops: z.array(z.object({
|
|
1595
|
+
type: z.enum(["execute", "read", "write", "search", "symbols"]).describe("Operation type"),
|
|
1596
|
+
command: z.string().optional().describe("Shell command (for execute)"),
|
|
1597
|
+
path: z.string().optional().describe("File path (for read/write/symbols)"),
|
|
1598
|
+
content: z.string().optional().describe("File content (for write)"),
|
|
1599
|
+
pattern: z.string().optional().describe("Search pattern (for search)"),
|
|
1600
|
+
summarize: z.boolean().optional().describe("AI summarize (for read)"),
|
|
1601
|
+
format: z.enum(["raw", "summary"]).optional().describe("Output format (for execute)"),
|
|
1602
|
+
})).describe("Array of operations to run"),
|
|
1603
|
+
cwd: z.string().optional().describe("Working directory for all ops"),
|
|
1604
|
+
},
|
|
1605
|
+
async ({ ops, cwd }) => {
|
|
1606
|
+
const start = Date.now();
|
|
1607
|
+
const workDir = cwd ?? process.cwd();
|
|
1608
|
+
const results: Record<string, any>[] = [];
|
|
1609
|
+
|
|
1610
|
+
for (let i = 0; i < ops.slice(0, 10).length; i++) {
|
|
1611
|
+
const op = ops[i];
|
|
1612
|
+
try {
|
|
1613
|
+
if (op.type === "execute" && op.command) {
|
|
1614
|
+
const result = await exec(op.command, workDir, 30000);
|
|
1615
|
+
const output = (result.stdout + result.stderr).trim();
|
|
1616
|
+
if (op.format === "summary" && output.split("\n").length > 15) {
|
|
1617
|
+
const processed = await processOutput(op.command, output);
|
|
1618
|
+
results.push({ op: i, type: "execute", summary: processed.summary, exitCode: result.exitCode, tokensSaved: processed.tokensSaved });
|
|
1619
|
+
} else {
|
|
1620
|
+
results.push({ op: i, type: "execute", output: stripAnsi(output).slice(0, 2000), exitCode: result.exitCode });
|
|
1621
|
+
}
|
|
1622
|
+
} else if (op.type === "read" && op.path) {
|
|
1623
|
+
const filePath = resolvePath(op.path, workDir);
|
|
1624
|
+
const result = cachedRead(filePath, {});
|
|
1625
|
+
if (op.summarize && result.content.length > 500) {
|
|
1626
|
+
const provider = getOutputProvider();
|
|
1627
|
+
const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
|
|
1628
|
+
const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
|
|
1629
|
+
const summary = await provider.complete(`File: ${filePath}\n\n${content}`, {
|
|
1630
|
+
model: outputModel,
|
|
1631
|
+
system: `Describe what this source file does in 2-4 lines. Include: main class/module name, key methods/functions, what it exports, and its purpose. Be specific.`,
|
|
1632
|
+
maxTokens: 300, temperature: 0.2,
|
|
1633
|
+
});
|
|
1634
|
+
results.push({ op: i, type: "read", path: op.path, summary, lines: result.content.split("\n").length });
|
|
1635
|
+
} else {
|
|
1636
|
+
results.push({ op: i, type: "read", path: op.path, content: result.content, lines: result.content.split("\n").length });
|
|
1637
|
+
}
|
|
1638
|
+
} else if (op.type === "write" && op.path && op.content !== undefined) {
|
|
1639
|
+
const filePath = resolvePath(op.path, workDir);
|
|
1640
|
+
const { writeFileSync, mkdirSync, existsSync } = await import("fs");
|
|
1641
|
+
const { dirname } = await import("path");
|
|
1642
|
+
const dir = dirname(filePath);
|
|
1643
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1644
|
+
writeFileSync(filePath, op.content);
|
|
1645
|
+
results.push({ op: i, type: "write", path: op.path, ok: true, bytes: op.content.length });
|
|
1646
|
+
} else if (op.type === "search" && op.pattern) {
|
|
1647
|
+
// Search accepts both files and directories — resolve to parent dir if file
|
|
1648
|
+
let searchPath = op.path ? resolvePath(op.path, workDir) : workDir;
|
|
1649
|
+
try {
|
|
1650
|
+
const { statSync } = await import("fs");
|
|
1651
|
+
if (statSync(searchPath).isFile()) searchPath = searchPath.replace(/\/[^/]+$/, "");
|
|
1652
|
+
} catch {}
|
|
1653
|
+
const result = await searchContent(op.pattern, searchPath, {});
|
|
1654
|
+
results.push({ op: i, type: "search", pattern: op.pattern, totalMatches: result.totalMatches, files: result.files.slice(0, 10) });
|
|
1655
|
+
} else if (op.type === "symbols" && op.path) {
|
|
1656
|
+
const filePath = resolvePath(op.path, workDir);
|
|
1657
|
+
const result = cachedRead(filePath, {});
|
|
1658
|
+
if (result.content && !result.content.startsWith("Error:")) {
|
|
1659
|
+
const provider = getOutputProvider();
|
|
1660
|
+
const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
|
|
1661
|
+
const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
|
|
1662
|
+
const summary = await provider.complete(`File: ${filePath}\n\n${content}`, {
|
|
1663
|
+
model: outputModel,
|
|
1664
|
+
system: `Extract all symbols. Return ONLY a JSON array. Each: {"name":"x","kind":"function|class|method|interface|type","line":N,"signature":"brief"}. For class methods use "Class.method". Exclude imports.`,
|
|
1665
|
+
maxTokens: 2000, temperature: 0,
|
|
1666
|
+
});
|
|
1667
|
+
let symbols: any[] = [];
|
|
1668
|
+
try { const m = summary.match(/\[[\s\S]*\]/); if (m) symbols = JSON.parse(m[0]); } catch {}
|
|
1669
|
+
results.push({ op: i, type: "symbols", path: op.path, symbols });
|
|
1670
|
+
} else {
|
|
1671
|
+
results.push({ op: i, type: "symbols", path: op.path, error: "Cannot read file" });
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
} catch (err: any) {
|
|
1675
|
+
results.push({ op: i, type: op.type, error: err.message?.slice(0, 200) });
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
logCall("batch", { command: `${ops.length} ops`, durationMs: Date.now() - start, aiProcessed: true });
|
|
1680
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ results, total: results.length, durationMs: Date.now() - start }) }] };
|
|
1681
|
+
}
|
|
1682
|
+
);
|
|
1683
|
+
|
|
1684
|
+
// ── Cross-session memory (mementos SDK) ────────────────────────────────────
|
|
1685
|
+
|
|
1686
|
+
server.tool(
|
|
1687
|
+
"remember",
|
|
1688
|
+
"Save a learning about this project for future sessions. Persists across restarts. Use for: project patterns, conventions, toolchain quirks, architectural decisions.",
|
|
1689
|
+
{
|
|
1690
|
+
key: z.string().describe("Short key (e.g., 'test-command', 'deploy-process', 'auth-pattern')"),
|
|
1691
|
+
value: z.string().describe("What to remember"),
|
|
1692
|
+
importance: z.number().optional().describe("1-10, default 7"),
|
|
1693
|
+
},
|
|
1694
|
+
async ({ key, value, importance }) => {
|
|
1695
|
+
try {
|
|
1696
|
+
const mementos = require("@hasna/mementos");
|
|
1697
|
+
mementos.createMemory({ key, value, scope: "shared", category: "knowledge", importance: importance ?? 7 });
|
|
1698
|
+
logCall("remember", { command: `remember: ${key}` });
|
|
1699
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ saved: key }) }] };
|
|
1700
|
+
} catch (e: any) {
|
|
1701
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ error: e.message?.slice(0, 200) }) }] };
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
);
|
|
1705
|
+
|
|
1706
|
+
server.tool(
|
|
1707
|
+
"recall",
|
|
1708
|
+
"Recall project memories from previous sessions. Returns all saved learnings, patterns, and decisions for this project.",
|
|
1709
|
+
{
|
|
1710
|
+
search: z.string().optional().describe("Search query to filter memories"),
|
|
1711
|
+
limit: z.number().optional().describe("Max memories to return (default: 20)"),
|
|
1712
|
+
},
|
|
1713
|
+
async ({ search, limit }) => {
|
|
1714
|
+
try {
|
|
1715
|
+
const mementos = require("@hasna/mementos");
|
|
1716
|
+
let memories;
|
|
1717
|
+
if (search) {
|
|
1718
|
+
memories = mementos.searchMemories(search, { limit: limit ?? 20 });
|
|
1719
|
+
} else {
|
|
1720
|
+
memories = mementos.listMemories({ scope: "shared", limit: limit ?? 20 });
|
|
1721
|
+
}
|
|
1722
|
+
const items = (memories ?? []).map((m: any) => ({ key: m.key, value: m.value, importance: m.importance }));
|
|
1723
|
+
logCall("recall", { command: `recall${search ? `: ${search}` : ""}` });
|
|
1724
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ memories: items, total: items.length }) }] };
|
|
1725
|
+
} catch (e: any) {
|
|
1726
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ error: e.message?.slice(0, 200), memories: [] }) }] };
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
);
|
|
1730
|
+
|
|
1573
1731
|
return server;
|
|
1574
1732
|
}
|
|
1575
1733
|
|
package/src/output-processor.ts
CHANGED
|
@@ -140,6 +140,7 @@ export async function processOutput(
|
|
|
140
140
|
command: string,
|
|
141
141
|
output: string,
|
|
142
142
|
originalPrompt?: string,
|
|
143
|
+
verbosity?: "minimal" | "normal" | "detailed",
|
|
143
144
|
): Promise<ProcessedOutput> {
|
|
144
145
|
const lines = output.split("\n");
|
|
145
146
|
|
|
@@ -201,12 +202,16 @@ export async function processOutput(
|
|
|
201
202
|
// Falls back to main provider if Groq unavailable
|
|
202
203
|
const provider = getOutputProvider();
|
|
203
204
|
const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
|
|
205
|
+
const verbosityHint = verbosity === "minimal" ? "\nBe ULTRA concise — 1-2 lines max. Status + key number only."
|
|
206
|
+
: verbosity === "detailed" ? "\nBe thorough — include all relevant details, up to 15 lines."
|
|
207
|
+
: ""; // normal = default 8 lines from SUMMARIZE_PROMPT
|
|
208
|
+
const maxTok = verbosity === "minimal" ? 100 : verbosity === "detailed" ? 500 : 300;
|
|
204
209
|
const summary = await provider.complete(
|
|
205
210
|
`${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}${hintsBlock}${profileHints}`,
|
|
206
211
|
{
|
|
207
212
|
model: outputModel,
|
|
208
|
-
system: SUMMARIZE_PROMPT,
|
|
209
|
-
maxTokens:
|
|
213
|
+
system: SUMMARIZE_PROMPT + verbosityHint,
|
|
214
|
+
maxTokens: maxTok,
|
|
210
215
|
temperature: 0.2,
|
|
211
216
|
}
|
|
212
217
|
);
|