@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
package/src/snapshots.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Session snapshots — capture terminal state for agent context handoff
|
|
2
|
+
|
|
3
|
+
import { loadHistory } from "./history.js";
|
|
4
|
+
import { bgStatus } from "./supervisor.js";
|
|
5
|
+
import { getEconomyStats, formatTokens } from "./economy.js";
|
|
6
|
+
import { listRecipes } from "./recipes/storage.js";
|
|
7
|
+
|
|
8
|
+
export interface SessionSnapshot {
|
|
9
|
+
cwd: string;
|
|
10
|
+
env: Record<string, string>;
|
|
11
|
+
runningProcesses: { pid: number; command: string; port?: number; uptime: number }[];
|
|
12
|
+
recentCommands: { cmd: string; exitCode?: boolean; summary?: string }[];
|
|
13
|
+
recipes: { name: string; command: string }[];
|
|
14
|
+
economy: { tokensSaved: string; tokensUsed: string };
|
|
15
|
+
timestamp: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Capture a compact snapshot of the current terminal state */
|
|
19
|
+
export function captureSnapshot(): SessionSnapshot {
|
|
20
|
+
// Filtered env — only relevant vars, no secrets
|
|
21
|
+
const safeEnvKeys = [
|
|
22
|
+
"PATH", "HOME", "USER", "SHELL", "NODE_ENV", "PWD", "LANG",
|
|
23
|
+
"TERM", "EDITOR", "VISUAL",
|
|
24
|
+
];
|
|
25
|
+
const env: Record<string, string> = {};
|
|
26
|
+
for (const key of safeEnvKeys) {
|
|
27
|
+
if (process.env[key]) env[key] = process.env[key]!;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Running processes
|
|
31
|
+
const processes = bgStatus().map(p => ({
|
|
32
|
+
pid: p.pid,
|
|
33
|
+
command: p.command,
|
|
34
|
+
port: p.port,
|
|
35
|
+
uptime: Date.now() - p.startedAt,
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// Recent commands (last 10, compressed)
|
|
39
|
+
const history = loadHistory().slice(-10);
|
|
40
|
+
const recentCommands = history.map(h => ({
|
|
41
|
+
cmd: h.cmd,
|
|
42
|
+
exitCode: h.error,
|
|
43
|
+
summary: h.nl !== h.cmd ? h.nl : undefined,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// Project recipes
|
|
47
|
+
const recipes = listRecipes(process.cwd()).slice(0, 10).map(r => ({
|
|
48
|
+
name: r.name,
|
|
49
|
+
command: r.command,
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// Economy
|
|
53
|
+
const econ = getEconomyStats();
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
cwd: process.cwd(),
|
|
57
|
+
env,
|
|
58
|
+
runningProcesses: processes,
|
|
59
|
+
recentCommands,
|
|
60
|
+
recipes,
|
|
61
|
+
economy: {
|
|
62
|
+
tokensSaved: formatTokens(econ.totalTokensSaved),
|
|
63
|
+
tokensUsed: formatTokens(econ.totalTokensUsed),
|
|
64
|
+
},
|
|
65
|
+
timestamp: Date.now(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Process supervisor — manages background processes for agents and humans
|
|
2
|
+
|
|
3
|
+
import { spawn, type ChildProcess } from "child_process";
|
|
4
|
+
import { createConnection } from "net";
|
|
5
|
+
|
|
6
|
+
export interface ManagedProcess {
|
|
7
|
+
pid: number;
|
|
8
|
+
command: string;
|
|
9
|
+
cwd: string;
|
|
10
|
+
port?: number;
|
|
11
|
+
startedAt: number;
|
|
12
|
+
lastOutput: string[];
|
|
13
|
+
exitCode?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const processes = new Map<number, { proc: ChildProcess; meta: ManagedProcess }>();
|
|
17
|
+
|
|
18
|
+
/** Auto-detect port from common commands */
|
|
19
|
+
function detectPort(command: string): number | undefined {
|
|
20
|
+
// "next dev -p 3001", "vite --port 4000", etc.
|
|
21
|
+
const portMatch = command.match(/-p\s+(\d+)|--port\s+(\d+)|PORT=(\d+)/);
|
|
22
|
+
if (portMatch) return parseInt(portMatch[1] ?? portMatch[2] ?? portMatch[3]);
|
|
23
|
+
|
|
24
|
+
// Common defaults
|
|
25
|
+
if (/\bnext\s+dev\b/.test(command)) return 3000;
|
|
26
|
+
if (/\bvite\b/.test(command)) return 5173;
|
|
27
|
+
if (/\bnuxt\s+dev\b/.test(command)) return 3000;
|
|
28
|
+
if (/\bremix\s+dev\b/.test(command)) return 5173;
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Start a background process */
|
|
33
|
+
export function bgStart(command: string, cwd?: string): ManagedProcess {
|
|
34
|
+
const workDir = cwd ?? process.cwd();
|
|
35
|
+
const proc = spawn("/bin/zsh", ["-c", command], {
|
|
36
|
+
cwd: workDir,
|
|
37
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
38
|
+
detached: false,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const meta: ManagedProcess = {
|
|
42
|
+
pid: proc.pid!,
|
|
43
|
+
command,
|
|
44
|
+
cwd: workDir,
|
|
45
|
+
port: detectPort(command),
|
|
46
|
+
startedAt: Date.now(),
|
|
47
|
+
lastOutput: [],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const pushOutput = (d: Buffer) => {
|
|
51
|
+
const lines = d.toString().split("\n").filter(l => l.trim());
|
|
52
|
+
meta.lastOutput.push(...lines);
|
|
53
|
+
// Keep last 50 lines
|
|
54
|
+
if (meta.lastOutput.length > 50) {
|
|
55
|
+
meta.lastOutput = meta.lastOutput.slice(-50);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
proc.stdout?.on("data", pushOutput);
|
|
60
|
+
proc.stderr?.on("data", pushOutput);
|
|
61
|
+
proc.on("close", (code) => {
|
|
62
|
+
meta.exitCode = code ?? 0;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
processes.set(proc.pid!, { proc, meta });
|
|
66
|
+
return meta;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** List all managed processes */
|
|
70
|
+
export function bgStatus(): ManagedProcess[] {
|
|
71
|
+
const result: ManagedProcess[] = [];
|
|
72
|
+
for (const [pid, { proc, meta }] of processes) {
|
|
73
|
+
// Check if still alive
|
|
74
|
+
try {
|
|
75
|
+
process.kill(pid, 0);
|
|
76
|
+
result.push({ ...meta, lastOutput: meta.lastOutput.slice(-5) });
|
|
77
|
+
} catch {
|
|
78
|
+
// Process is dead
|
|
79
|
+
result.push({ ...meta, exitCode: meta.exitCode ?? -1, lastOutput: meta.lastOutput.slice(-5) });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Stop a background process */
|
|
86
|
+
export function bgStop(pid: number): boolean {
|
|
87
|
+
const entry = processes.get(pid);
|
|
88
|
+
if (!entry) return false;
|
|
89
|
+
try {
|
|
90
|
+
entry.proc.kill("SIGTERM");
|
|
91
|
+
processes.delete(pid);
|
|
92
|
+
return true;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Get logs for a background process */
|
|
99
|
+
export function bgLogs(pid: number, tail: number = 20): string[] {
|
|
100
|
+
const entry = processes.get(pid);
|
|
101
|
+
if (!entry) return [];
|
|
102
|
+
return entry.meta.lastOutput.slice(-tail);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Wait for a port to be ready */
|
|
106
|
+
export function bgWaitPort(port: number, timeoutMs: number = 30000): Promise<boolean> {
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
const start = Date.now();
|
|
109
|
+
|
|
110
|
+
const check = () => {
|
|
111
|
+
if (Date.now() - start > timeoutMs) {
|
|
112
|
+
resolve(false);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const sock = createConnection({ port, host: "127.0.0.1" });
|
|
117
|
+
sock.on("connect", () => {
|
|
118
|
+
sock.destroy();
|
|
119
|
+
resolve(true);
|
|
120
|
+
});
|
|
121
|
+
sock.on("error", () => {
|
|
122
|
+
sock.destroy();
|
|
123
|
+
setTimeout(check, 500);
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
check();
|
|
128
|
+
});
|
|
129
|
+
}
|
package/src/tree.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Tree compression — convert flat file paths to compact tree representation
|
|
2
|
+
|
|
3
|
+
import { readdirSync, statSync } from "fs";
|
|
4
|
+
import { join, basename } from "path";
|
|
5
|
+
import { DEFAULT_EXCLUDE_DIRS } from "./search/filters.js";
|
|
6
|
+
|
|
7
|
+
export interface TreeNode {
|
|
8
|
+
name: string;
|
|
9
|
+
type: "file" | "dir";
|
|
10
|
+
size?: number;
|
|
11
|
+
children?: TreeNode[];
|
|
12
|
+
fileCount?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Build a tree from a directory */
|
|
16
|
+
export function buildTree(
|
|
17
|
+
dirPath: string,
|
|
18
|
+
options: { maxDepth?: number; includeHidden?: boolean; depth?: number } = {}
|
|
19
|
+
): TreeNode {
|
|
20
|
+
const { maxDepth = 2, includeHidden = false, depth = 0 } = options;
|
|
21
|
+
const name = basename(dirPath) || dirPath;
|
|
22
|
+
|
|
23
|
+
const node: TreeNode = { name, type: "dir", children: [], fileCount: 0 };
|
|
24
|
+
|
|
25
|
+
if (depth >= maxDepth) {
|
|
26
|
+
// Count files without listing them
|
|
27
|
+
try {
|
|
28
|
+
const entries = readdirSync(dirPath);
|
|
29
|
+
node.fileCount = entries.length;
|
|
30
|
+
node.children = undefined; // don't expand
|
|
31
|
+
} catch { node.fileCount = 0; }
|
|
32
|
+
return node;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const entries = readdirSync(dirPath);
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
if (!includeHidden && entry.startsWith(".")) continue;
|
|
39
|
+
if (DEFAULT_EXCLUDE_DIRS.includes(entry)) {
|
|
40
|
+
// Show as collapsed with count
|
|
41
|
+
try {
|
|
42
|
+
const subPath = join(dirPath, entry);
|
|
43
|
+
const subStat = statSync(subPath);
|
|
44
|
+
if (subStat.isDirectory()) {
|
|
45
|
+
node.children!.push({ name: entry, type: "dir", fileCount: -1 }); // -1 = hidden
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
} catch { continue; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const fullPath = join(dirPath, entry);
|
|
52
|
+
try {
|
|
53
|
+
const stat = statSync(fullPath);
|
|
54
|
+
if (stat.isDirectory()) {
|
|
55
|
+
node.children!.push(buildTree(fullPath, { maxDepth, includeHidden, depth: depth + 1 }));
|
|
56
|
+
} else {
|
|
57
|
+
node.children!.push({ name: entry, type: "file", size: stat.size });
|
|
58
|
+
node.fileCount!++;
|
|
59
|
+
}
|
|
60
|
+
} catch { continue; }
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
|
|
64
|
+
return node;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Render tree as compact string (for agents — minimum tokens) */
|
|
68
|
+
export function compactTree(node: TreeNode, indent: number = 0): string {
|
|
69
|
+
const pad = " ".repeat(indent);
|
|
70
|
+
|
|
71
|
+
if (node.type === "file") return `${pad}${node.name}`;
|
|
72
|
+
|
|
73
|
+
if (node.fileCount === -1) return `${pad}${node.name}/ (hidden)`;
|
|
74
|
+
if (!node.children || node.children.length === 0) return `${pad}${node.name}/ (empty)`;
|
|
75
|
+
if (!node.children.some(c => c.children)) {
|
|
76
|
+
// Leaf directory — compact single line
|
|
77
|
+
const files = node.children.filter(c => c.type === "file").map(c => c.name);
|
|
78
|
+
const dirs = node.children.filter(c => c.type === "dir");
|
|
79
|
+
const parts: string[] = [];
|
|
80
|
+
if (files.length <= 5) {
|
|
81
|
+
parts.push(...files);
|
|
82
|
+
} else {
|
|
83
|
+
parts.push(`${files.length} files`);
|
|
84
|
+
}
|
|
85
|
+
for (const d of dirs) {
|
|
86
|
+
parts.push(`${d.name}/${d.fileCount != null ? ` (${d.fileCount === -1 ? "hidden" : d.fileCount + " files"})` : ""}`);
|
|
87
|
+
}
|
|
88
|
+
return `${pad}${node.name}/ [${parts.join(", ")}]`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const lines = [`${pad}${node.name}/`];
|
|
92
|
+
for (const child of node.children) {
|
|
93
|
+
lines.push(compactTree(child, indent + 1));
|
|
94
|
+
}
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Render tree as JSON (for MCP) */
|
|
99
|
+
export function treeToJson(node: TreeNode): object {
|
|
100
|
+
return node;
|
|
101
|
+
}
|