@hasna/terminal 0.1.4 → 0.2.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/.claude/scheduled_tasks.lock +1 -1
- package/README.md +186 -0
- package/dist/App.js +217 -105
- package/dist/Browse.js +79 -0
- package/dist/FuzzyPicker.js +47 -0
- package/dist/StatusBar.js +20 -16
- package/dist/ai.js +45 -50
- package/dist/cli.js +138 -6
- package/dist/compression.js +107 -0
- package/dist/compression.test.js +42 -0
- package/dist/diff-cache.js +87 -0
- package/dist/diff-cache.test.js +27 -0
- package/dist/economy.js +79 -0
- package/dist/economy.test.js +13 -0
- package/dist/mcp/install.js +98 -0
- package/dist/mcp/server.js +333 -0
- package/dist/output-router.js +41 -0
- package/dist/parsers/base.js +2 -0
- package/dist/parsers/build.js +64 -0
- package/dist/parsers/errors.js +101 -0
- package/dist/parsers/files.js +78 -0
- package/dist/parsers/git.js +86 -0
- package/dist/parsers/index.js +48 -0
- package/dist/parsers/parsers.test.js +136 -0
- package/dist/parsers/tests.js +89 -0
- package/dist/providers/anthropic.js +39 -0
- package/dist/providers/base.js +4 -0
- package/dist/providers/cerebras.js +95 -0
- package/dist/providers/index.js +49 -0
- package/dist/providers/providers.test.js +14 -0
- package/dist/recipes/model.js +20 -0
- package/dist/recipes/recipes.test.js +36 -0
- package/dist/recipes/storage.js +118 -0
- package/dist/search/content-search.js +61 -0
- package/dist/search/file-search.js +61 -0
- package/dist/search/filters.js +34 -0
- package/dist/search/index.js +4 -0
- package/dist/search/search.test.js +22 -0
- package/dist/snapshots.js +51 -0
- package/dist/supervisor.js +112 -0
- package/dist/tree.js +94 -0
- package/package.json +7 -4
- package/src/App.tsx +371 -245
- package/src/Browse.tsx +103 -0
- package/src/FuzzyPicker.tsx +69 -0
- package/src/StatusBar.tsx +28 -34
- package/src/ai.ts +63 -51
- package/src/cli.tsx +132 -6
- package/src/compression.test.ts +50 -0
- package/src/compression.ts +140 -0
- package/src/diff-cache.test.ts +30 -0
- package/src/diff-cache.ts +125 -0
- package/src/economy.test.ts +16 -0
- package/src/economy.ts +99 -0
- package/src/mcp/install.ts +94 -0
- package/src/mcp/server.ts +476 -0
- package/src/output-router.ts +56 -0
- package/src/parsers/base.ts +72 -0
- package/src/parsers/build.ts +73 -0
- package/src/parsers/errors.ts +107 -0
- package/src/parsers/files.ts +91 -0
- package/src/parsers/git.ts +86 -0
- package/src/parsers/index.ts +66 -0
- package/src/parsers/parsers.test.ts +153 -0
- package/src/parsers/tests.ts +98 -0
- package/src/providers/anthropic.ts +44 -0
- package/src/providers/base.ts +34 -0
- package/src/providers/cerebras.ts +108 -0
- package/src/providers/index.ts +60 -0
- package/src/providers/providers.test.ts +16 -0
- package/src/recipes/model.ts +55 -0
- package/src/recipes/recipes.test.ts +44 -0
- package/src/recipes/storage.ts +142 -0
- package/src/search/content-search.ts +97 -0
- package/src/search/file-search.ts +86 -0
- package/src/search/filters.ts +36 -0
- package/src/search/index.ts +7 -0
- package/src/search/search.test.ts +25 -0
- package/src/snapshots.ts +67 -0
- package/src/supervisor.ts +129 -0
- package/src/tree.ts +101 -0
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Process supervisor — manages background processes for agents and humans
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { createConnection } from "net";
|
|
4
|
+
const processes = new Map();
|
|
5
|
+
/** Auto-detect port from common commands */
|
|
6
|
+
function detectPort(command) {
|
|
7
|
+
// "next dev -p 3001", "vite --port 4000", etc.
|
|
8
|
+
const portMatch = command.match(/-p\s+(\d+)|--port\s+(\d+)|PORT=(\d+)/);
|
|
9
|
+
if (portMatch)
|
|
10
|
+
return parseInt(portMatch[1] ?? portMatch[2] ?? portMatch[3]);
|
|
11
|
+
// Common defaults
|
|
12
|
+
if (/\bnext\s+dev\b/.test(command))
|
|
13
|
+
return 3000;
|
|
14
|
+
if (/\bvite\b/.test(command))
|
|
15
|
+
return 5173;
|
|
16
|
+
if (/\bnuxt\s+dev\b/.test(command))
|
|
17
|
+
return 3000;
|
|
18
|
+
if (/\bremix\s+dev\b/.test(command))
|
|
19
|
+
return 5173;
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
/** Start a background process */
|
|
23
|
+
export function bgStart(command, cwd) {
|
|
24
|
+
const workDir = cwd ?? process.cwd();
|
|
25
|
+
const proc = spawn("/bin/zsh", ["-c", command], {
|
|
26
|
+
cwd: workDir,
|
|
27
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
28
|
+
detached: false,
|
|
29
|
+
});
|
|
30
|
+
const meta = {
|
|
31
|
+
pid: proc.pid,
|
|
32
|
+
command,
|
|
33
|
+
cwd: workDir,
|
|
34
|
+
port: detectPort(command),
|
|
35
|
+
startedAt: Date.now(),
|
|
36
|
+
lastOutput: [],
|
|
37
|
+
};
|
|
38
|
+
const pushOutput = (d) => {
|
|
39
|
+
const lines = d.toString().split("\n").filter(l => l.trim());
|
|
40
|
+
meta.lastOutput.push(...lines);
|
|
41
|
+
// Keep last 50 lines
|
|
42
|
+
if (meta.lastOutput.length > 50) {
|
|
43
|
+
meta.lastOutput = meta.lastOutput.slice(-50);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
proc.stdout?.on("data", pushOutput);
|
|
47
|
+
proc.stderr?.on("data", pushOutput);
|
|
48
|
+
proc.on("close", (code) => {
|
|
49
|
+
meta.exitCode = code ?? 0;
|
|
50
|
+
});
|
|
51
|
+
processes.set(proc.pid, { proc, meta });
|
|
52
|
+
return meta;
|
|
53
|
+
}
|
|
54
|
+
/** List all managed processes */
|
|
55
|
+
export function bgStatus() {
|
|
56
|
+
const result = [];
|
|
57
|
+
for (const [pid, { proc, meta }] of processes) {
|
|
58
|
+
// Check if still alive
|
|
59
|
+
try {
|
|
60
|
+
process.kill(pid, 0);
|
|
61
|
+
result.push({ ...meta, lastOutput: meta.lastOutput.slice(-5) });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Process is dead
|
|
65
|
+
result.push({ ...meta, exitCode: meta.exitCode ?? -1, lastOutput: meta.lastOutput.slice(-5) });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
/** Stop a background process */
|
|
71
|
+
export function bgStop(pid) {
|
|
72
|
+
const entry = processes.get(pid);
|
|
73
|
+
if (!entry)
|
|
74
|
+
return false;
|
|
75
|
+
try {
|
|
76
|
+
entry.proc.kill("SIGTERM");
|
|
77
|
+
processes.delete(pid);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Get logs for a background process */
|
|
85
|
+
export function bgLogs(pid, tail = 20) {
|
|
86
|
+
const entry = processes.get(pid);
|
|
87
|
+
if (!entry)
|
|
88
|
+
return [];
|
|
89
|
+
return entry.meta.lastOutput.slice(-tail);
|
|
90
|
+
}
|
|
91
|
+
/** Wait for a port to be ready */
|
|
92
|
+
export function bgWaitPort(port, timeoutMs = 30000) {
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
const start = Date.now();
|
|
95
|
+
const check = () => {
|
|
96
|
+
if (Date.now() - start > timeoutMs) {
|
|
97
|
+
resolve(false);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const sock = createConnection({ port, host: "127.0.0.1" });
|
|
101
|
+
sock.on("connect", () => {
|
|
102
|
+
sock.destroy();
|
|
103
|
+
resolve(true);
|
|
104
|
+
});
|
|
105
|
+
sock.on("error", () => {
|
|
106
|
+
sock.destroy();
|
|
107
|
+
setTimeout(check, 500);
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
check();
|
|
111
|
+
});
|
|
112
|
+
}
|
package/dist/tree.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Tree compression — convert flat file paths to compact tree representation
|
|
2
|
+
import { readdirSync, statSync } from "fs";
|
|
3
|
+
import { join, basename } from "path";
|
|
4
|
+
import { DEFAULT_EXCLUDE_DIRS } from "./search/filters.js";
|
|
5
|
+
/** Build a tree from a directory */
|
|
6
|
+
export function buildTree(dirPath, options = {}) {
|
|
7
|
+
const { maxDepth = 2, includeHidden = false, depth = 0 } = options;
|
|
8
|
+
const name = basename(dirPath) || dirPath;
|
|
9
|
+
const node = { name, type: "dir", children: [], fileCount: 0 };
|
|
10
|
+
if (depth >= maxDepth) {
|
|
11
|
+
// Count files without listing them
|
|
12
|
+
try {
|
|
13
|
+
const entries = readdirSync(dirPath);
|
|
14
|
+
node.fileCount = entries.length;
|
|
15
|
+
node.children = undefined; // don't expand
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
node.fileCount = 0;
|
|
19
|
+
}
|
|
20
|
+
return node;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const entries = readdirSync(dirPath);
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (!includeHidden && entry.startsWith("."))
|
|
26
|
+
continue;
|
|
27
|
+
if (DEFAULT_EXCLUDE_DIRS.includes(entry)) {
|
|
28
|
+
// Show as collapsed with count
|
|
29
|
+
try {
|
|
30
|
+
const subPath = join(dirPath, entry);
|
|
31
|
+
const subStat = statSync(subPath);
|
|
32
|
+
if (subStat.isDirectory()) {
|
|
33
|
+
node.children.push({ name: entry, type: "dir", fileCount: -1 }); // -1 = hidden
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const fullPath = join(dirPath, entry);
|
|
42
|
+
try {
|
|
43
|
+
const stat = statSync(fullPath);
|
|
44
|
+
if (stat.isDirectory()) {
|
|
45
|
+
node.children.push(buildTree(fullPath, { maxDepth, includeHidden, depth: depth + 1 }));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
node.children.push({ name: entry, type: "file", size: stat.size });
|
|
49
|
+
node.fileCount++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch { }
|
|
58
|
+
return node;
|
|
59
|
+
}
|
|
60
|
+
/** Render tree as compact string (for agents — minimum tokens) */
|
|
61
|
+
export function compactTree(node, indent = 0) {
|
|
62
|
+
const pad = " ".repeat(indent);
|
|
63
|
+
if (node.type === "file")
|
|
64
|
+
return `${pad}${node.name}`;
|
|
65
|
+
if (node.fileCount === -1)
|
|
66
|
+
return `${pad}${node.name}/ (hidden)`;
|
|
67
|
+
if (!node.children || node.children.length === 0)
|
|
68
|
+
return `${pad}${node.name}/ (empty)`;
|
|
69
|
+
if (!node.children.some(c => c.children)) {
|
|
70
|
+
// Leaf directory — compact single line
|
|
71
|
+
const files = node.children.filter(c => c.type === "file").map(c => c.name);
|
|
72
|
+
const dirs = node.children.filter(c => c.type === "dir");
|
|
73
|
+
const parts = [];
|
|
74
|
+
if (files.length <= 5) {
|
|
75
|
+
parts.push(...files);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
parts.push(`${files.length} files`);
|
|
79
|
+
}
|
|
80
|
+
for (const d of dirs) {
|
|
81
|
+
parts.push(`${d.name}/${d.fileCount != null ? ` (${d.fileCount === -1 ? "hidden" : d.fileCount + " files"})` : ""}`);
|
|
82
|
+
}
|
|
83
|
+
return `${pad}${node.name}/ [${parts.join(", ")}]`;
|
|
84
|
+
}
|
|
85
|
+
const lines = [`${pad}${node.name}/`];
|
|
86
|
+
for (const child of node.children) {
|
|
87
|
+
lines.push(compactTree(child, indent + 1));
|
|
88
|
+
}
|
|
89
|
+
return lines.join("\n");
|
|
90
|
+
}
|
|
91
|
+
/** Render tree as JSON (for MCP) */
|
|
92
|
+
export function treeToJson(node) {
|
|
93
|
+
return node;
|
|
94
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/terminal",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Smart terminal wrapper for AI agents and humans — structured output, token compression, MCP server, natural language",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"t": "dist/cli.js",
|
|
@@ -10,12 +10,15 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"dev": "tsx src/cli.tsx",
|
|
13
|
-
"start": "node dist/cli.js"
|
|
13
|
+
"start": "node dist/cli.js",
|
|
14
|
+
"test": "bun test"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
16
17
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
17
19
|
"ink": "^5.0.1",
|
|
18
|
-
"react": "^18.2.0"
|
|
20
|
+
"react": "^18.2.0",
|
|
21
|
+
"zod": "^4.3.6"
|
|
19
22
|
},
|
|
20
23
|
"publishConfig": {
|
|
21
24
|
"access": "public",
|