@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.
@@ -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: "3.4.0",
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
- }, async ({ command, cwd, timeout }) => {
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
- }, async ({ path: rawPath, offset, limit, summarize }) => {
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: `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 — name the actual functions and what they do. Never just say "N lines of code."`,
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 ─────────────────────────────────────────
@@ -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: 300,
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.1.0",
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: "3.4.0",
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: `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 — name the actual functions and what they do. Never just say "N lines of code."`,
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
 
@@ -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: 300,
213
+ system: SUMMARIZE_PROMPT + verbosityHint,
214
+ maxTokens: maxTok,
210
215
  temperature: 0.2,
211
216
  }
212
217
  );