@hasna/terminal 4.3.1 → 4.3.2

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 (79) hide show
  1. package/dist/App.js +404 -0
  2. package/dist/Browse.js +79 -0
  3. package/dist/FuzzyPicker.js +47 -0
  4. package/dist/Onboarding.js +51 -0
  5. package/dist/Spinner.js +12 -0
  6. package/dist/StatusBar.js +49 -0
  7. package/dist/ai.js +316 -0
  8. package/dist/cache.js +42 -0
  9. package/dist/cli.js +778 -0
  10. package/dist/command-rewriter.js +64 -0
  11. package/dist/command-validator.js +86 -0
  12. package/dist/compression.js +91 -0
  13. package/dist/context-hints.js +285 -0
  14. package/dist/diff-cache.js +107 -0
  15. package/dist/discover.js +212 -0
  16. package/dist/economy.js +155 -0
  17. package/dist/expand-store.js +44 -0
  18. package/dist/file-cache.js +72 -0
  19. package/dist/file-index.js +62 -0
  20. package/dist/history.js +62 -0
  21. package/dist/lazy-executor.js +54 -0
  22. package/dist/line-dedup.js +59 -0
  23. package/dist/loop-detector.js +75 -0
  24. package/dist/mcp/install.js +189 -0
  25. package/dist/mcp/server.js +56 -0
  26. package/dist/mcp/tools/batch.js +111 -0
  27. package/dist/mcp/tools/execute.js +194 -0
  28. package/dist/mcp/tools/files.js +290 -0
  29. package/dist/mcp/tools/git.js +233 -0
  30. package/dist/mcp/tools/helpers.js +63 -0
  31. package/dist/mcp/tools/memory.js +151 -0
  32. package/dist/mcp/tools/meta.js +138 -0
  33. package/dist/mcp/tools/process.js +50 -0
  34. package/dist/mcp/tools/project.js +251 -0
  35. package/dist/mcp/tools/search.js +86 -0
  36. package/dist/noise-filter.js +94 -0
  37. package/dist/output-processor.js +233 -0
  38. package/dist/output-store.js +112 -0
  39. package/dist/paths.js +28 -0
  40. package/dist/providers/anthropic.js +43 -0
  41. package/dist/providers/base.js +4 -0
  42. package/dist/providers/cerebras.js +8 -0
  43. package/dist/providers/groq.js +8 -0
  44. package/dist/providers/index.js +142 -0
  45. package/dist/providers/openai-compat.js +93 -0
  46. package/dist/providers/xai.js +8 -0
  47. package/dist/recipes/model.js +20 -0
  48. package/dist/recipes/storage.js +153 -0
  49. package/dist/search/content-search.js +70 -0
  50. package/dist/search/file-search.js +61 -0
  51. package/dist/search/filters.js +34 -0
  52. package/dist/search/index.js +5 -0
  53. package/dist/search/semantic.js +346 -0
  54. package/dist/session-boot.js +59 -0
  55. package/dist/session-context.js +55 -0
  56. package/dist/sessions-db.js +240 -0
  57. package/dist/smart-display.js +286 -0
  58. package/dist/snapshots.js +51 -0
  59. package/dist/supervisor.js +112 -0
  60. package/dist/test-watchlist.js +131 -0
  61. package/dist/tokens.js +17 -0
  62. package/dist/tool-profiles.js +130 -0
  63. package/dist/tree.js +94 -0
  64. package/dist/usage-cache.js +65 -0
  65. package/package.json +2 -1
  66. package/src/Onboarding.tsx +1 -1
  67. package/src/ai.ts +5 -4
  68. package/src/cache.ts +2 -2
  69. package/src/economy.ts +3 -3
  70. package/src/history.ts +2 -2
  71. package/src/mcp/server.ts +2 -0
  72. package/src/mcp/tools/memory.ts +4 -2
  73. package/src/output-store.ts +2 -1
  74. package/src/paths.ts +32 -0
  75. package/src/recipes/storage.ts +3 -3
  76. package/src/session-context.ts +2 -2
  77. package/src/sessions-db.ts +15 -4
  78. package/src/tool-profiles.ts +4 -3
  79. package/src/usage-cache.ts +2 -2
@@ -0,0 +1,8 @@
1
+ // xAI/Grok provider — code-optimized models
2
+ import { OpenAICompatibleProvider } from "./openai-compat.js";
3
+ export class XaiProvider extends OpenAICompatibleProvider {
4
+ name = "xai";
5
+ baseUrl = "https://api.x.ai/v1";
6
+ defaultModel = "grok-code-fast-1";
7
+ apiKeyEnvVar = "XAI_API_KEY";
8
+ }
@@ -0,0 +1,20 @@
1
+ // Recipes data model — reusable command templates with collections and projects
2
+ /** Generate a short random ID */
3
+ export function genId() {
4
+ return Math.random().toString(36).slice(2, 10);
5
+ }
6
+ /** Substitute variables in a command template */
7
+ export function substituteVariables(command, vars) {
8
+ let result = command;
9
+ for (const [name, value] of Object.entries(vars)) {
10
+ result = result.replace(new RegExp(`\\{${name}\\}`, "g"), value);
11
+ }
12
+ return result;
13
+ }
14
+ /** Extract variable placeholders from a command */
15
+ export function extractVariables(command) {
16
+ const matches = command.match(/\{(\w+)\}/g);
17
+ if (!matches)
18
+ return [];
19
+ return [...new Set(matches.map(m => m.slice(1, -1)))];
20
+ }
@@ -0,0 +1,153 @@
1
+ // Recipes storage — global (~/.hasna/terminal/recipes.json) + per-project (.terminal/recipes.json)
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { genId, extractVariables } from "./model.js";
5
+ import { getTerminalDir } from "../paths.js";
6
+ const GLOBAL_DIR = getTerminalDir();
7
+ const GLOBAL_FILE = join(GLOBAL_DIR, "recipes.json");
8
+ function projectFile(projectPath) {
9
+ return join(projectPath, ".terminal", "recipes.json");
10
+ }
11
+ function loadStore(filePath) {
12
+ if (!existsSync(filePath))
13
+ return { recipes: [], collections: [] };
14
+ try {
15
+ return JSON.parse(readFileSync(filePath, "utf8"));
16
+ }
17
+ catch {
18
+ return { recipes: [], collections: [] };
19
+ }
20
+ }
21
+ function saveStore(filePath, store) {
22
+ const dir = filePath.replace(/\/[^/]+$/, "");
23
+ if (!existsSync(dir))
24
+ mkdirSync(dir, { recursive: true });
25
+ writeFileSync(filePath, JSON.stringify(store, null, 2));
26
+ }
27
+ // ── Built-in system recipes (always available, zero config) ─────────────────
28
+ const SYSTEM_RECIPES = [
29
+ // Git workflows
30
+ { id: "sys-commit-push", name: "commit-push", description: "Stage all, commit, push to origin", command: "git add -A && git commit -m \"{message}\" && git push", tags: ["git"], collection: "git", variables: [{ name: "message", required: true }], createdAt: 0, updatedAt: 0 },
31
+ { id: "sys-pr", name: "create-pr", description: "Create GitHub PR from current branch", command: "gh pr create --title \"{title}\" --body \"{body}\"", tags: ["git", "github"], collection: "git", variables: [{ name: "title", required: true }, { name: "body", default: "" }], createdAt: 0, updatedAt: 0 },
32
+ { id: "sys-stash", name: "stash-switch", description: "Stash changes, switch branch, pop", command: "git stash && git checkout {branch} && git stash pop", tags: ["git"], collection: "git", variables: [{ name: "branch", required: true }], createdAt: 0, updatedAt: 0 },
33
+ // Quality checks
34
+ { id: "sys-todos", name: "find-todos", description: "Find all TODO/FIXME/HACK in source code", command: "grep -rn 'TODO\\|FIXME\\|HACK\\|XXX' {path} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.py' --include='*.go' --include='*.rs' --include='*.java' --include='*.rb'", tags: ["quality"], collection: "quality", variables: [{ name: "path", default: "src/" }], createdAt: 0, updatedAt: 0 },
35
+ { id: "sys-deadcode", name: "find-unused-exports", description: "Find exported symbols that may be unused", command: "grep -rn 'export ' {path} --include='*.ts' | sed 's/.*export //' | sed 's/[(<:].*//' | sort -u", tags: ["quality"], collection: "quality", variables: [{ name: "path", default: "src/" }], createdAt: 0, updatedAt: 0 },
36
+ { id: "sys-security", name: "security-scan", description: "Scan for common security anti-patterns", command: "grep -rn 'eval\\|exec\\|spawn\\|innerHTML\\|dangerouslySetInnerHTML\\|password.*=.*[\"'\\']' {path} --include='*.ts' --include='*.js' --include='*.py'", tags: ["security"], collection: "quality", variables: [{ name: "path", default: "src/" }], createdAt: 0, updatedAt: 0 },
37
+ // Project info
38
+ { id: "sys-deps", name: "list-deps", description: "Show project dependencies", command: "cat package.json 2>/dev/null | grep -A 100 '\"dependencies\"' | head -30 || cat requirements.txt 2>/dev/null || cat Cargo.toml 2>/dev/null | grep -A 50 'dependencies'", tags: ["deps"], collection: "project", variables: [], createdAt: 0, updatedAt: 0 },
39
+ { id: "sys-size", name: "project-size", description: "Count lines of code by file type", command: "find {path} -not -path '*/node_modules/*' -not -path '*/dist/*' -not -path '*/.git/*' -type f | xargs wc -l 2>/dev/null | sort -rn | head -20", tags: ["stats"], collection: "project", variables: [{ name: "path", default: "src/" }], createdAt: 0, updatedAt: 0 },
40
+ // Process management
41
+ { id: "sys-port", name: "kill-port", description: "Kill whatever is running on a port", command: "lsof -ti :{port} | xargs kill -9 2>/dev/null || echo 'Port {port} is free'", tags: ["process"], collection: "system", variables: [{ name: "port", required: true }], createdAt: 0, updatedAt: 0 },
42
+ { id: "sys-disk", name: "disk-usage", description: "Show disk usage of current directory", command: "du -sh {path}/* 2>/dev/null | sort -rh | head -15", tags: ["system"], collection: "system", variables: [{ name: "path", default: "." }], createdAt: 0, updatedAt: 0 },
43
+ ];
44
+ // ── CRUD operations ──────────────────────────────────────────────────────────
45
+ /** Get all recipes (merged: system + global + project-scoped) */
46
+ export function listRecipes(projectPath) {
47
+ const global = loadStore(GLOBAL_FILE).recipes;
48
+ if (!projectPath)
49
+ return [...global, ...SYSTEM_RECIPES];
50
+ const project = loadStore(projectFile(projectPath)).recipes;
51
+ return [...project, ...global, ...SYSTEM_RECIPES]; // project > global > system priority
52
+ }
53
+ /** Get recipes filtered by collection */
54
+ export function listByCollection(collection, projectPath) {
55
+ return listRecipes(projectPath).filter(r => r.collection === collection);
56
+ }
57
+ /** Get a recipe by name */
58
+ export function getRecipe(name, projectPath) {
59
+ return listRecipes(projectPath).find(r => r.name === name) ?? null;
60
+ }
61
+ /** Create a recipe */
62
+ export function createRecipe(opts) {
63
+ const filePath = opts.project ? projectFile(opts.project) : GLOBAL_FILE;
64
+ const store = loadStore(filePath);
65
+ // Prevent duplicates — update existing if same name
66
+ const existingIdx = store.recipes.findIndex(r => r.name === opts.name);
67
+ if (existingIdx >= 0) {
68
+ store.recipes[existingIdx].command = opts.command;
69
+ store.recipes[existingIdx].updatedAt = Date.now();
70
+ if (opts.description)
71
+ store.recipes[existingIdx].description = opts.description;
72
+ if (opts.tags)
73
+ store.recipes[existingIdx].tags = opts.tags;
74
+ if (opts.collection)
75
+ store.recipes[existingIdx].collection = opts.collection;
76
+ saveStore(filePath, store);
77
+ return store.recipes[existingIdx];
78
+ }
79
+ // Auto-detect variables from command if not explicitly provided
80
+ const detectedVars = extractVariables(opts.command);
81
+ const variables = opts.variables ?? detectedVars.map(name => ({ name, required: true }));
82
+ const recipe = {
83
+ id: genId(),
84
+ name: opts.name,
85
+ description: opts.description,
86
+ command: opts.command,
87
+ tags: opts.tags ?? [],
88
+ collection: opts.collection,
89
+ project: opts.project,
90
+ variables,
91
+ createdAt: Date.now(),
92
+ updatedAt: Date.now(),
93
+ };
94
+ store.recipes.push(recipe);
95
+ saveStore(filePath, store);
96
+ return recipe;
97
+ }
98
+ /** Delete a recipe by name — tries project first, then global */
99
+ export function deleteRecipe(name, projectPath) {
100
+ // Try project-scoped first
101
+ if (projectPath) {
102
+ const pFile = projectFile(projectPath);
103
+ const pStore = loadStore(pFile);
104
+ const before = pStore.recipes.length;
105
+ pStore.recipes = pStore.recipes.filter(r => r.name !== name);
106
+ if (pStore.recipes.length < before) {
107
+ saveStore(pFile, pStore);
108
+ return true;
109
+ }
110
+ }
111
+ // Fall back to global
112
+ const store = loadStore(GLOBAL_FILE);
113
+ const before = store.recipes.length;
114
+ store.recipes = store.recipes.filter(r => r.name !== name);
115
+ if (store.recipes.length < before) {
116
+ saveStore(GLOBAL_FILE, store);
117
+ return true;
118
+ }
119
+ return false;
120
+ }
121
+ // ── Collections ──────────────────────────────────────────────────────────────
122
+ export function listCollections(projectPath) {
123
+ const global = loadStore(GLOBAL_FILE).collections;
124
+ if (!projectPath)
125
+ return global;
126
+ const project = loadStore(projectFile(projectPath)).collections;
127
+ return [...project, ...global];
128
+ }
129
+ export function createCollection(opts) {
130
+ const filePath = opts.project ? projectFile(opts.project) : GLOBAL_FILE;
131
+ const store = loadStore(filePath);
132
+ // Prevent duplicates — return existing if same name
133
+ const existing = store.collections.find(c => c.name === opts.name);
134
+ if (existing)
135
+ return existing;
136
+ const collection = {
137
+ id: genId(),
138
+ name: opts.name,
139
+ description: opts.description,
140
+ project: opts.project,
141
+ createdAt: Date.now(),
142
+ };
143
+ store.collections.push(collection);
144
+ saveStore(filePath, store);
145
+ return collection;
146
+ }
147
+ /** Initialize project-scoped recipes file */
148
+ export function initProject(projectPath) {
149
+ const file = projectFile(projectPath);
150
+ if (!existsSync(file)) {
151
+ saveStore(file, { recipes: [], collections: [] });
152
+ }
153
+ }
@@ -0,0 +1,70 @@
1
+ // Smart content search — structured grep/ripgrep with grouping and dedup
2
+ import { spawn } from "child_process";
3
+ import { DEFAULT_EXCLUDE_DIRS, relevanceScore } from "./filters.js";
4
+ function exec(command, cwd) {
5
+ return new Promise((resolve) => {
6
+ const proc = spawn("/bin/zsh", ["-c", command], { cwd, stdio: ["ignore", "pipe", "pipe"] });
7
+ let out = "";
8
+ proc.stdout?.on("data", (d) => { out += d.toString(); });
9
+ proc.on("close", () => resolve(out));
10
+ });
11
+ }
12
+ export async function searchContent(pattern, cwd, options = {}) {
13
+ const { fileType, maxResults = 30, contextLines = 0 } = options;
14
+ // Prefer ripgrep, fall back to grep
15
+ const excludeArgs = DEFAULT_EXCLUDE_DIRS.map(d => `--glob '!${d}'`).join(" ");
16
+ const typeArg = fileType ? `--type ${fileType}` : "";
17
+ const contextArg = contextLines > 0 ? `-C ${contextLines}` : "";
18
+ // Try rg first, fall back to grep
19
+ const rgCmd = `rg --line-number --no-heading ${contextArg} ${typeArg} ${excludeArgs} '${pattern.replace(/'/g, "'\\''")}' 2>/dev/null | head -500`;
20
+ const grepCmd = `grep -rn ${contextArg} '${pattern.replace(/'/g, "'\\''")}' . --include='*.ts' --include='*.js' --include='*.py' --include='*.go' --include='*.rs' --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=dist 2>/dev/null | head -500`;
21
+ let raw = await exec(rgCmd, cwd);
22
+ if (!raw.trim()) {
23
+ raw = await exec(grepCmd, cwd);
24
+ }
25
+ const lines = raw.split("\n").filter(l => l.trim());
26
+ const fileMap = new Map();
27
+ let filteredCount = 0;
28
+ for (const line of lines) {
29
+ // Format: path:line:content
30
+ const match = line.match(/^([^:]+):(\d+):(.*)$/);
31
+ if (!match)
32
+ continue;
33
+ const [, path, lineNum, content] = match;
34
+ if (DEFAULT_EXCLUDE_DIRS.some(d => path.includes(`/${d}/`))) {
35
+ filteredCount++;
36
+ continue;
37
+ }
38
+ if (!fileMap.has(path))
39
+ fileMap.set(path, []);
40
+ fileMap.get(path).push({
41
+ line: parseInt(lineNum),
42
+ content: content.trim(),
43
+ });
44
+ }
45
+ // Sort files by relevance
46
+ const files = [...fileMap.entries()]
47
+ .map(([path, matches]) => ({
48
+ path,
49
+ matches: matches.slice(0, 5), // max 5 matches per file
50
+ relevance: relevanceScore(path),
51
+ }))
52
+ .sort((a, b) => b.relevance - a.relevance)
53
+ .slice(0, maxResults);
54
+ const totalMatches = [...fileMap.values()].reduce((sum, m) => sum + m.length, 0);
55
+ const filtered = filteredCount > 0 ? [{ count: filteredCount, reason: "excluded directories" }] : [];
56
+ const rawTokens = Math.ceil(raw.length / 4);
57
+ const truncated = totalMatches > files.reduce((s, f) => s + f.matches.length, 0);
58
+ const result = { query: pattern, totalMatches, files, filtered };
59
+ const resultTokens = Math.ceil(JSON.stringify(result).length / 4);
60
+ result.tokensSaved = Math.max(0, rawTokens - resultTokens);
61
+ result.truncated = truncated;
62
+ // Overflow guard — warn when results are truncated
63
+ if (totalMatches > maxResults * 3) {
64
+ result.overflow = {
65
+ warning: `${totalMatches} total matches across ${fileMap.size} files — showing top ${files.length}`,
66
+ suggestion: "Try a more specific pattern, add fileType filter, or use -l to list files only",
67
+ };
68
+ }
69
+ return result;
70
+ }
@@ -0,0 +1,61 @@
1
+ // Smart file search — structured, filtered, token-efficient results
2
+ import { spawn } from "child_process";
3
+ import { DEFAULT_EXCLUDE_DIRS, isSourceFile, isExcludedDir, relevanceScore } from "./filters.js";
4
+ function exec(command, cwd) {
5
+ return new Promise((resolve) => {
6
+ const proc = spawn("/bin/zsh", ["-c", command], { cwd, stdio: ["ignore", "pipe", "pipe"] });
7
+ let out = "";
8
+ proc.stdout?.on("data", (d) => { out += d.toString(); });
9
+ proc.stderr?.on("data", (d) => { out += d.toString(); });
10
+ proc.on("close", () => resolve(out));
11
+ });
12
+ }
13
+ export async function searchFiles(pattern, cwd, options = {}) {
14
+ const { includeNodeModules = false, maxResults = 50 } = options;
15
+ // Build find command
16
+ const excludes = includeNodeModules
17
+ ? DEFAULT_EXCLUDE_DIRS.filter(d => d !== "node_modules")
18
+ : DEFAULT_EXCLUDE_DIRS;
19
+ const excludeArgs = excludes.map(d => `-not -path '*/${d}/*'`).join(" ");
20
+ const command = `find . -name '${pattern}' -type f ${excludeArgs} 2>/dev/null | head -${maxResults * 3}`;
21
+ const raw = await exec(command, cwd);
22
+ const allPaths = raw.split("\n").filter(l => l.trim());
23
+ // Categorize
24
+ const source = [];
25
+ const config = [];
26
+ const other = [];
27
+ const filteredCounts = {};
28
+ for (const path of allPaths) {
29
+ if (isExcludedDir(path)) {
30
+ const dir = DEFAULT_EXCLUDE_DIRS.find(d => path.includes(`/${d}/`)) ?? "other";
31
+ filteredCounts[dir] = (filteredCounts[dir] ?? 0) + 1;
32
+ continue;
33
+ }
34
+ if (isSourceFile(path)) {
35
+ source.push(path);
36
+ }
37
+ else if (path.match(/\.(json|yaml|yml|toml|ini|env)/)) {
38
+ config.push(path);
39
+ }
40
+ else {
41
+ other.push(path);
42
+ }
43
+ }
44
+ // Sort by relevance
45
+ source.sort((a, b) => relevanceScore(b) - relevanceScore(a));
46
+ // Limit results
47
+ const filtered = Object.entries(filteredCounts).map(([reason, count]) => ({ reason, count }));
48
+ // Estimate token savings
49
+ const rawTokens = Math.ceil(raw.length / 4);
50
+ const result = {
51
+ query: pattern,
52
+ total: allPaths.length,
53
+ source: source.slice(0, maxResults),
54
+ config: config.slice(0, 10),
55
+ other: other.slice(0, 10),
56
+ filtered,
57
+ };
58
+ const resultTokens = Math.ceil(JSON.stringify(result).length / 4);
59
+ result.tokensSaved = Math.max(0, rawTokens - resultTokens);
60
+ return result;
61
+ }
@@ -0,0 +1,34 @@
1
+ // Smart filters for search results — auto-hide noise, prioritize source files
2
+ export const DEFAULT_EXCLUDE_DIRS = [
3
+ "node_modules", ".git", "dist", "build", ".next", "__pycache__",
4
+ "coverage", ".turbo", ".cache", ".output", "vendor", "target",
5
+ ];
6
+ export const SOURCE_EXTENSIONS = new Set([
7
+ ".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java", ".rb",
8
+ ".sh", ".c", ".cpp", ".h", ".css", ".scss", ".html", ".vue", ".svelte",
9
+ ".md", ".json", ".yaml", ".yml", ".toml", ".sql", ".graphql",
10
+ ]);
11
+ export const CONFIG_EXTENSIONS = new Set([
12
+ ".json", ".yaml", ".yml", ".toml", ".ini", ".env", ".config.js",
13
+ ".config.ts", ".config.mjs",
14
+ ]);
15
+ export function isSourceFile(path) {
16
+ const ext = path.match(/\.\w+$/)?.[0] ?? "";
17
+ return SOURCE_EXTENSIONS.has(ext);
18
+ }
19
+ export function isExcludedDir(path) {
20
+ return DEFAULT_EXCLUDE_DIRS.some(d => path.includes(`/${d}/`) || path.includes(`/${d}`));
21
+ }
22
+ /** Relevance score: higher = more relevant */
23
+ export function relevanceScore(path) {
24
+ if (isExcludedDir(path))
25
+ return 0;
26
+ const ext = path.match(/\.\w+$/)?.[0] ?? "";
27
+ if (SOURCE_EXTENSIONS.has(ext))
28
+ return 10;
29
+ if (CONFIG_EXTENSIONS.has(ext))
30
+ return 5;
31
+ if (path.includes("/test") || path.includes(".test.") || path.includes(".spec."))
32
+ return 7;
33
+ return 3;
34
+ }
@@ -0,0 +1,5 @@
1
+ // Smart search — unified entry point for file + content search
2
+ export { searchFiles } from "./file-search.js";
3
+ export { searchContent } from "./content-search.js";
4
+ export { DEFAULT_EXCLUDE_DIRS, SOURCE_EXTENSIONS, isSourceFile, isExcludedDir, relevanceScore } from "./filters.js";
5
+ export { semanticSearch, findExports, findComponents, findHooks } from "./semantic.js";