@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.
Files changed (82) hide show
  1. package/.claude/scheduled_tasks.lock +1 -1
  2. package/README.md +186 -0
  3. package/dist/App.js +217 -105
  4. package/dist/Browse.js +79 -0
  5. package/dist/FuzzyPicker.js +47 -0
  6. package/dist/StatusBar.js +20 -16
  7. package/dist/ai.js +45 -50
  8. package/dist/cli.js +138 -6
  9. package/dist/compression.js +107 -0
  10. package/dist/compression.test.js +42 -0
  11. package/dist/diff-cache.js +87 -0
  12. package/dist/diff-cache.test.js +27 -0
  13. package/dist/economy.js +79 -0
  14. package/dist/economy.test.js +13 -0
  15. package/dist/mcp/install.js +98 -0
  16. package/dist/mcp/server.js +333 -0
  17. package/dist/output-router.js +41 -0
  18. package/dist/parsers/base.js +2 -0
  19. package/dist/parsers/build.js +64 -0
  20. package/dist/parsers/errors.js +101 -0
  21. package/dist/parsers/files.js +78 -0
  22. package/dist/parsers/git.js +86 -0
  23. package/dist/parsers/index.js +48 -0
  24. package/dist/parsers/parsers.test.js +136 -0
  25. package/dist/parsers/tests.js +89 -0
  26. package/dist/providers/anthropic.js +39 -0
  27. package/dist/providers/base.js +4 -0
  28. package/dist/providers/cerebras.js +95 -0
  29. package/dist/providers/index.js +49 -0
  30. package/dist/providers/providers.test.js +14 -0
  31. package/dist/recipes/model.js +20 -0
  32. package/dist/recipes/recipes.test.js +36 -0
  33. package/dist/recipes/storage.js +118 -0
  34. package/dist/search/content-search.js +61 -0
  35. package/dist/search/file-search.js +61 -0
  36. package/dist/search/filters.js +34 -0
  37. package/dist/search/index.js +4 -0
  38. package/dist/search/search.test.js +22 -0
  39. package/dist/snapshots.js +51 -0
  40. package/dist/supervisor.js +112 -0
  41. package/dist/tree.js +94 -0
  42. package/package.json +7 -4
  43. package/src/App.tsx +371 -245
  44. package/src/Browse.tsx +103 -0
  45. package/src/FuzzyPicker.tsx +69 -0
  46. package/src/StatusBar.tsx +28 -34
  47. package/src/ai.ts +63 -51
  48. package/src/cli.tsx +132 -6
  49. package/src/compression.test.ts +50 -0
  50. package/src/compression.ts +140 -0
  51. package/src/diff-cache.test.ts +30 -0
  52. package/src/diff-cache.ts +125 -0
  53. package/src/economy.test.ts +16 -0
  54. package/src/economy.ts +99 -0
  55. package/src/mcp/install.ts +94 -0
  56. package/src/mcp/server.ts +476 -0
  57. package/src/output-router.ts +56 -0
  58. package/src/parsers/base.ts +72 -0
  59. package/src/parsers/build.ts +73 -0
  60. package/src/parsers/errors.ts +107 -0
  61. package/src/parsers/files.ts +91 -0
  62. package/src/parsers/git.ts +86 -0
  63. package/src/parsers/index.ts +66 -0
  64. package/src/parsers/parsers.test.ts +153 -0
  65. package/src/parsers/tests.ts +98 -0
  66. package/src/providers/anthropic.ts +44 -0
  67. package/src/providers/base.ts +34 -0
  68. package/src/providers/cerebras.ts +108 -0
  69. package/src/providers/index.ts +60 -0
  70. package/src/providers/providers.test.ts +16 -0
  71. package/src/recipes/model.ts +55 -0
  72. package/src/recipes/recipes.test.ts +44 -0
  73. package/src/recipes/storage.ts +142 -0
  74. package/src/search/content-search.ts +97 -0
  75. package/src/search/file-search.ts +86 -0
  76. package/src/search/filters.ts +36 -0
  77. package/src/search/index.ts +7 -0
  78. package/src/search/search.test.ts +25 -0
  79. package/src/snapshots.ts +67 -0
  80. package/src/supervisor.ts +129 -0
  81. package/src/tree.ts +101 -0
  82. package/tsconfig.json +2 -1
@@ -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
+ }
package/tsconfig.json CHANGED
@@ -10,5 +10,6 @@
10
10
  "esModuleInterop": true,
11
11
  "skipLibCheck": true
12
12
  },
13
- "include": ["src/**/*"]
13
+ "include": ["src/**/*"],
14
+ "exclude": ["src/**/*.test.ts"]
14
15
  }