@hasna/terminal 4.3.1 → 4.3.3

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 (81) 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/db/pg-migrations.js +70 -0
  15. package/dist/diff-cache.js +107 -0
  16. package/dist/discover.js +212 -0
  17. package/dist/economy.js +155 -0
  18. package/dist/expand-store.js +44 -0
  19. package/dist/file-cache.js +72 -0
  20. package/dist/file-index.js +62 -0
  21. package/dist/history.js +62 -0
  22. package/dist/lazy-executor.js +54 -0
  23. package/dist/line-dedup.js +59 -0
  24. package/dist/loop-detector.js +75 -0
  25. package/dist/mcp/install.js +189 -0
  26. package/dist/mcp/server.js +90 -0
  27. package/dist/mcp/tools/batch.js +111 -0
  28. package/dist/mcp/tools/execute.js +194 -0
  29. package/dist/mcp/tools/files.js +290 -0
  30. package/dist/mcp/tools/git.js +233 -0
  31. package/dist/mcp/tools/helpers.js +63 -0
  32. package/dist/mcp/tools/memory.js +151 -0
  33. package/dist/mcp/tools/meta.js +138 -0
  34. package/dist/mcp/tools/process.js +50 -0
  35. package/dist/mcp/tools/project.js +251 -0
  36. package/dist/mcp/tools/search.js +86 -0
  37. package/dist/noise-filter.js +94 -0
  38. package/dist/output-processor.js +233 -0
  39. package/dist/output-store.js +112 -0
  40. package/dist/paths.js +28 -0
  41. package/dist/providers/anthropic.js +43 -0
  42. package/dist/providers/base.js +4 -0
  43. package/dist/providers/cerebras.js +8 -0
  44. package/dist/providers/groq.js +8 -0
  45. package/dist/providers/index.js +142 -0
  46. package/dist/providers/openai-compat.js +93 -0
  47. package/dist/providers/xai.js +8 -0
  48. package/dist/recipes/model.js +20 -0
  49. package/dist/recipes/storage.js +153 -0
  50. package/dist/search/content-search.js +70 -0
  51. package/dist/search/file-search.js +61 -0
  52. package/dist/search/filters.js +34 -0
  53. package/dist/search/index.js +5 -0
  54. package/dist/search/semantic.js +346 -0
  55. package/dist/session-boot.js +59 -0
  56. package/dist/session-context.js +55 -0
  57. package/dist/sessions-db.js +240 -0
  58. package/dist/smart-display.js +286 -0
  59. package/dist/snapshots.js +51 -0
  60. package/dist/supervisor.js +112 -0
  61. package/dist/test-watchlist.js +131 -0
  62. package/dist/tokens.js +17 -0
  63. package/dist/tool-profiles.js +130 -0
  64. package/dist/tree.js +94 -0
  65. package/dist/usage-cache.js +65 -0
  66. package/package.json +2 -1
  67. package/src/Onboarding.tsx +1 -1
  68. package/src/ai.ts +5 -4
  69. package/src/cache.ts +2 -2
  70. package/src/db/pg-migrations.ts +77 -0
  71. package/src/economy.ts +3 -3
  72. package/src/history.ts +2 -2
  73. package/src/mcp/server.ts +55 -0
  74. package/src/mcp/tools/memory.ts +4 -2
  75. package/src/output-store.ts +2 -1
  76. package/src/paths.ts +32 -0
  77. package/src/recipes/storage.ts +3 -3
  78. package/src/session-context.ts +2 -2
  79. package/src/sessions-db.ts +15 -4
  80. package/src/tool-profiles.ts +4 -3
  81. package/src/usage-cache.ts +2 -2
package/src/mcp/server.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { z } from "zod";
5
6
  import { createSession } from "../sessions-db.js";
6
7
  import { createHelpers } from "./tools/helpers.js";
7
8
 
@@ -15,6 +16,7 @@ import { registerProcessTools } from "./tools/process.js";
15
16
  import { registerBatchTools } from "./tools/batch.js";
16
17
  import { registerMemoryTools } from "./tools/memory.js";
17
18
  import { registerMetaTools } from "./tools/meta.js";
19
+ import { registerCloudTools } from "@hasna/cloud";
18
20
 
19
21
  // ── server ───────────────────────────────────────────────────────────────────
20
22
 
@@ -49,6 +51,59 @@ export function createServer(): McpServer {
49
51
  registerBatchTools(server, h);
50
52
  registerMemoryTools(server, h);
51
53
  registerMetaTools(server, h);
54
+ registerCloudTools(server, "terminal");
55
+
56
+ // ── Agent Tools ──────────────────────────────────────────────────────────
57
+ const _agentReg = new Map<string, { id: string; name: string; last_seen_at: string; project_id?: string }>();
58
+
59
+ server.tool(
60
+ "register_agent",
61
+ "Register an agent session (idempotent). Auto-updates last_seen_at on re-register.",
62
+ { name: z.string(), session_id: z.string().optional() },
63
+ async (a) => {
64
+ const existing = [..._agentReg.values()].find(x => x.name === a.name);
65
+ if (existing) { existing.last_seen_at = new Date().toISOString(); return { content: [{ type: "text" as const, text: JSON.stringify(existing) }] }; }
66
+ const id = Math.random().toString(36).slice(2, 10);
67
+ const ag = { id, name: a.name, last_seen_at: new Date().toISOString() };
68
+ _agentReg.set(id, ag);
69
+ return { content: [{ type: "text" as const, text: JSON.stringify(ag) }] };
70
+ }
71
+ );
72
+
73
+ server.tool(
74
+ "heartbeat",
75
+ "Update last_seen_at to signal agent is active.",
76
+ { agent_id: z.string() },
77
+ async (a) => {
78
+ const ag = _agentReg.get(a.agent_id);
79
+ if (!ag) return { content: [{ type: "text" as const, text: `Agent not found: ${a.agent_id}` }], isError: true };
80
+ ag.last_seen_at = new Date().toISOString();
81
+ return { content: [{ type: "text" as const, text: JSON.stringify({ id: ag.id, name: ag.name, last_seen_at: ag.last_seen_at }) }] };
82
+ }
83
+ );
84
+
85
+ server.tool(
86
+ "set_focus",
87
+ "Set active project context for this agent session.",
88
+ { agent_id: z.string(), project_id: z.string().nullable().optional() },
89
+ async (a) => {
90
+ const ag = _agentReg.get(a.agent_id);
91
+ if (!ag) return { content: [{ type: "text" as const, text: `Agent not found: ${a.agent_id}` }], isError: true };
92
+ (ag as any).project_id = a.project_id ?? undefined;
93
+ return { content: [{ type: "text" as const, text: a.project_id ? `Focus: ${a.project_id}` : "Focus cleared" }] };
94
+ }
95
+ );
96
+
97
+ server.tool(
98
+ "list_agents",
99
+ "List all registered agents.",
100
+ {},
101
+ async () => {
102
+ const agents = [..._agentReg.values()];
103
+ if (agents.length === 0) return { content: [{ type: "text" as const, text: "No agents registered." }] };
104
+ return { content: [{ type: "text" as const, text: JSON.stringify(agents, null, 2) }] };
105
+ }
106
+ );
52
107
 
53
108
  return server;
54
109
  }
@@ -136,7 +136,8 @@ export function registerMemoryTools(server: McpServer, h: ToolHelpers): void {
136
136
  async ({ name, value }) => {
137
137
  const { existsSync, readFileSync, writeFileSync, chmodSync } = await import("fs");
138
138
  const { join } = await import("path");
139
- const secretsFile = join(process.env.HOME ?? "~", ".terminal", "secrets.json");
139
+ const { getTerminalDir } = await import("../../paths.js");
140
+ const secretsFile = join(getTerminalDir(), "secrets.json");
140
141
  let secrets: Record<string, string> = {};
141
142
  if (existsSync(secretsFile)) {
142
143
  try { secrets = JSON.parse(readFileSync(secretsFile, "utf8")); } catch {}
@@ -157,7 +158,8 @@ export function registerMemoryTools(server: McpServer, h: ToolHelpers): void {
157
158
  async () => {
158
159
  const { existsSync, readFileSync } = await import("fs");
159
160
  const { join } = await import("path");
160
- const secretsFile = join(process.env.HOME ?? "~", ".terminal", "secrets.json");
161
+ const { getTerminalDir } = await import("../../paths.js");
162
+ const secretsFile = join(getTerminalDir(), "secrets.json");
161
163
  let names: string[] = [];
162
164
  if (existsSync(secretsFile)) {
163
165
  try { names = Object.keys(JSON.parse(readFileSync(secretsFile, "utf8"))); } catch {}
@@ -4,8 +4,9 @@
4
4
  import { existsSync, mkdirSync, writeFileSync, readdirSync, statSync, unlinkSync } from "fs";
5
5
  import { join } from "path";
6
6
  import { createHash } from "crypto";
7
+ import { getTerminalDir } from "./paths.js";
7
8
 
8
- const OUTPUTS_DIR = join(process.env.HOME ?? "~", ".terminal", "outputs");
9
+ const OUTPUTS_DIR = join(getTerminalDir(), "outputs");
9
10
 
10
11
  /** Ensure outputs directory exists */
11
12
  function ensureDir() {
package/src/paths.ts ADDED
@@ -0,0 +1,32 @@
1
+ // Centralized path resolution for open-terminal global data directory.
2
+ // Migrated from ~/.terminal/ to ~/.hasna/terminal/ with backward compat.
3
+
4
+ import { existsSync, mkdirSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { join } from "path";
7
+
8
+ /**
9
+ * Get the global terminal data directory.
10
+ * New default: ~/.hasna/terminal/
11
+ * Legacy fallback: ~/.terminal/ (if it exists and new dir doesn't)
12
+ * Env override: HASNA_TERMINAL_DIR or TERMINAL_DIR
13
+ */
14
+ export function getTerminalDir(): string {
15
+ if (process.env.HASNA_TERMINAL_DIR) return process.env.HASNA_TERMINAL_DIR;
16
+ if (process.env.TERMINAL_DIR) return process.env.TERMINAL_DIR;
17
+
18
+ const home = homedir();
19
+ const newDir = join(home, ".hasna", "terminal");
20
+ const legacyDir = join(home, ".terminal");
21
+
22
+ // Use legacy dir if it exists and new one doesn't yet (backward compat)
23
+ if (!existsSync(newDir) && existsSync(legacyDir)) {
24
+ return legacyDir;
25
+ }
26
+
27
+ if (!existsSync(newDir)) {
28
+ mkdirSync(newDir, { recursive: true });
29
+ }
30
+
31
+ return newDir;
32
+ }
@@ -1,12 +1,12 @@
1
- // Recipes storage — global (~/.terminal/recipes.json) + per-project (.terminal/recipes.json)
1
+ // Recipes storage — global (~/.hasna/terminal/recipes.json) + per-project (.terminal/recipes.json)
2
2
 
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
- import { homedir } from "os";
5
4
  import { join } from "path";
6
5
  import type { Recipe, Collection, RecipeStore } from "./model.js";
7
6
  import { genId, extractVariables } from "./model.js";
7
+ import { getTerminalDir } from "../paths.js";
8
8
 
9
- const GLOBAL_DIR = join(homedir(), ".terminal");
9
+ const GLOBAL_DIR = getTerminalDir();
10
10
  const GLOBAL_FILE = join(GLOBAL_DIR, "recipes.json");
11
11
 
12
12
  function projectFile(projectPath: string): string {
@@ -2,10 +2,10 @@
2
2
  // Enables: terminal "show auth code" → terminal "explain that function"
3
3
 
4
4
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
5
- import { homedir } from "os";
6
5
  import { join } from "path";
6
+ import { getTerminalDir } from "./paths.js";
7
7
 
8
- const DIR = join(homedir(), ".terminal");
8
+ const DIR = getTerminalDir();
9
9
  const CTX_FILE = join(DIR, "session-context.json");
10
10
  const MAX_ENTRIES = 5;
11
11
 
@@ -2,20 +2,21 @@
2
2
 
3
3
  // @ts-ignore — bun:sqlite is a bun built-in
4
4
  import { Database } from "bun:sqlite";
5
+ import { SqliteAdapter } from "@hasna/cloud";
5
6
  import { existsSync, mkdirSync } from "fs";
6
- import { homedir } from "os";
7
7
  import { join } from "path";
8
8
  import { randomUUID } from "crypto";
9
+ import { getTerminalDir } from "./paths.js";
9
10
 
10
- const DIR = join(homedir(), ".terminal");
11
- const DB_PATH = join(DIR, "sessions.db");
11
+ const DIR = getTerminalDir();
12
+ const DB_PATH = process.env.HASNA_TERMINAL_DB_PATH ?? process.env.TERMINAL_DB_PATH ?? join(DIR, "sessions.db");
12
13
 
13
14
  let db: Database | null = null;
14
15
 
15
16
  function getDb(): Database {
16
17
  if (db) return db;
17
18
  if (!existsSync(DIR)) mkdirSync(DIR, { recursive: true });
18
- db = new Database(DB_PATH);
19
+ db = new SqliteAdapter(DB_PATH) as unknown as Database;
19
20
  db.exec("PRAGMA journal_mode = WAL");
20
21
 
21
22
  db.exec(`
@@ -71,6 +72,16 @@ function getDb(): Database {
71
72
  );
72
73
 
73
74
  CREATE INDEX IF NOT EXISTS idx_corrections_prompt ON corrections(prompt);
75
+
76
+ CREATE TABLE IF NOT EXISTS feedback (
77
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
78
+ message TEXT NOT NULL,
79
+ email TEXT,
80
+ category TEXT DEFAULT 'general',
81
+ version TEXT,
82
+ machine_id TEXT,
83
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
84
+ );
74
85
  `);
75
86
 
76
87
  return db;
@@ -1,9 +1,10 @@
1
1
  // Tool profiles — config-driven AI enhancement for specific command categories
2
- // Profiles are loaded from ~/.terminal/profiles/ (user-customizable)
2
+ // Profiles are loaded from ~/.hasna/terminal/profiles/ (user-customizable)
3
3
  // Each profile tells the AI how to handle a specific tool's output
4
4
 
5
5
  import { existsSync, readFileSync, readdirSync } from "fs";
6
6
  import { join } from "path";
7
+ import { getTerminalDir } from "./paths.js";
7
8
 
8
9
  export interface ToolProfile {
9
10
  name: string;
@@ -23,7 +24,7 @@ export interface ToolProfile {
23
24
  };
24
25
  }
25
26
 
26
- const PROFILES_DIR = join(process.env.HOME ?? "~", ".terminal", "profiles");
27
+ const PROFILES_DIR = join(getTerminalDir(), "profiles");
27
28
 
28
29
  /** Built-in profiles — sensible defaults, user can override */
29
30
  const BUILTIN_PROFILES: ToolProfile[] = [
@@ -90,7 +91,7 @@ const BUILTIN_PROFILES: ToolProfile[] = [
90
91
  },
91
92
  ];
92
93
 
93
- /** Load user profiles from ~/.terminal/profiles/ */
94
+ /** Load user profiles from ~/.hasna/terminal/profiles/ */
94
95
  function loadUserProfiles(): ToolProfile[] {
95
96
  if (!existsSync(PROFILES_DIR)) return [];
96
97
 
@@ -2,11 +2,11 @@
2
2
  // After 3 identical prompt→command mappings, cache locally
3
3
 
4
4
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
5
- import { homedir } from "os";
6
5
  import { join } from "path";
7
6
  import { createHash } from "crypto";
7
+ import { getTerminalDir } from "./paths.js";
8
8
 
9
- const DIR = join(homedir(), ".terminal");
9
+ const DIR = getTerminalDir();
10
10
  const CACHE_FILE = join(DIR, "learned.json");
11
11
 
12
12
  interface LearnedEntry {