@hasna/terminal 4.2.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 +75 -6
- package/dist/output-processor.js +7 -3
- package/package.json +2 -1
- package/src/mcp/server.ts +80 -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 {
|
|
@@ -1214,9 +1227,10 @@ Be specific, not generic. Only flag real problems.`,
|
|
|
1214
1227
|
// ── batch: multiple operations in one round trip ───────────────────────────
|
|
1215
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).", {
|
|
1216
1229
|
ops: z.array(z.object({
|
|
1217
|
-
type: z.enum(["execute", "read", "search", "symbols"]).describe("Operation type"),
|
|
1230
|
+
type: z.enum(["execute", "read", "write", "search", "symbols"]).describe("Operation type"),
|
|
1218
1231
|
command: z.string().optional().describe("Shell command (for execute)"),
|
|
1219
|
-
path: z.string().optional().describe("File path (for read/symbols)"),
|
|
1232
|
+
path: z.string().optional().describe("File path (for read/write/symbols)"),
|
|
1233
|
+
content: z.string().optional().describe("File content (for write)"),
|
|
1220
1234
|
pattern: z.string().optional().describe("Search pattern (for search)"),
|
|
1221
1235
|
summarize: z.boolean().optional().describe("AI summarize (for read)"),
|
|
1222
1236
|
format: z.enum(["raw", "summary"]).optional().describe("Output format (for execute)"),
|
|
@@ -1258,8 +1272,26 @@ Be specific, not generic. Only flag real problems.`,
|
|
|
1258
1272
|
results.push({ op: i, type: "read", path: op.path, content: result.content, lines: result.content.split("\n").length });
|
|
1259
1273
|
}
|
|
1260
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
|
+
}
|
|
1261
1285
|
else if (op.type === "search" && op.pattern) {
|
|
1262
|
-
|
|
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, {});
|
|
1263
1295
|
results.push({ op: i, type: "search", pattern: op.pattern, totalMatches: result.totalMatches, files: result.files.slice(0, 10) });
|
|
1264
1296
|
}
|
|
1265
1297
|
else if (op.type === "symbols" && op.path) {
|
|
@@ -1295,6 +1327,43 @@ Be specific, not generic. Only flag real problems.`,
|
|
|
1295
1327
|
logCall("batch", { command: `${ops.length} ops`, durationMs: Date.now() - start, aiProcessed: true });
|
|
1296
1328
|
return { content: [{ type: "text", text: JSON.stringify({ results, total: results.length, durationMs: Date.now() - start }) }] };
|
|
1297
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
|
+
});
|
|
1298
1367
|
return server;
|
|
1299
1368
|
}
|
|
1300
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 });
|
|
@@ -1580,9 +1592,10 @@ Be specific, not generic. Only flag real problems.`,
|
|
|
1580
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).",
|
|
1581
1593
|
{
|
|
1582
1594
|
ops: z.array(z.object({
|
|
1583
|
-
type: z.enum(["execute", "read", "search", "symbols"]).describe("Operation type"),
|
|
1595
|
+
type: z.enum(["execute", "read", "write", "search", "symbols"]).describe("Operation type"),
|
|
1584
1596
|
command: z.string().optional().describe("Shell command (for execute)"),
|
|
1585
|
-
path: z.string().optional().describe("File path (for read/symbols)"),
|
|
1597
|
+
path: z.string().optional().describe("File path (for read/write/symbols)"),
|
|
1598
|
+
content: z.string().optional().describe("File content (for write)"),
|
|
1586
1599
|
pattern: z.string().optional().describe("Search pattern (for search)"),
|
|
1587
1600
|
summarize: z.boolean().optional().describe("AI summarize (for read)"),
|
|
1588
1601
|
format: z.enum(["raw", "summary"]).optional().describe("Output format (for execute)"),
|
|
@@ -1622,8 +1635,22 @@ Be specific, not generic. Only flag real problems.`,
|
|
|
1622
1635
|
} else {
|
|
1623
1636
|
results.push({ op: i, type: "read", path: op.path, content: result.content, lines: result.content.split("\n").length });
|
|
1624
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 });
|
|
1625
1646
|
} else if (op.type === "search" && op.pattern) {
|
|
1626
|
-
|
|
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, {});
|
|
1627
1654
|
results.push({ op: i, type: "search", pattern: op.pattern, totalMatches: result.totalMatches, files: result.files.slice(0, 10) });
|
|
1628
1655
|
} else if (op.type === "symbols" && op.path) {
|
|
1629
1656
|
const filePath = resolvePath(op.path, workDir);
|
|
@@ -1654,6 +1681,53 @@ Be specific, not generic. Only flag real problems.`,
|
|
|
1654
1681
|
}
|
|
1655
1682
|
);
|
|
1656
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
|
+
|
|
1657
1731
|
return server;
|
|
1658
1732
|
}
|
|
1659
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
|
);
|