@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,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.1.4",
4
- "description": "Natural language terminal — speak plain English, get shell commands",
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",