@hasna/terminal 4.3.2 → 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.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * PostgreSQL migrations for open-terminal cloud sync.
3
+ *
4
+ * Equivalent to the SQLite schema in sessions-db.ts, translated for PostgreSQL.
5
+ */
6
+ export const PG_MIGRATIONS = [
7
+ // Migration 1: sessions table
8
+ `CREATE TABLE IF NOT EXISTS sessions (
9
+ id TEXT PRIMARY KEY,
10
+ started_at BIGINT NOT NULL,
11
+ ended_at BIGINT,
12
+ cwd TEXT NOT NULL,
13
+ provider TEXT,
14
+ model TEXT
15
+ )`,
16
+ // Migration 2: interactions table
17
+ `CREATE TABLE IF NOT EXISTS interactions (
18
+ id SERIAL PRIMARY KEY,
19
+ session_id TEXT NOT NULL REFERENCES sessions(id),
20
+ nl TEXT NOT NULL,
21
+ command TEXT,
22
+ output TEXT,
23
+ exit_code INTEGER,
24
+ tokens_used INTEGER DEFAULT 0,
25
+ tokens_saved INTEGER DEFAULT 0,
26
+ duration_ms INTEGER,
27
+ model TEXT,
28
+ cached BOOLEAN DEFAULT FALSE,
29
+ created_at BIGINT NOT NULL
30
+ )`,
31
+ // Migration 3: indexes on interactions and sessions
32
+ `CREATE INDEX IF NOT EXISTS idx_interactions_session ON interactions(session_id)`,
33
+ `CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
34
+ // Migration 4: corrections table
35
+ `CREATE TABLE IF NOT EXISTS corrections (
36
+ id SERIAL PRIMARY KEY,
37
+ prompt TEXT NOT NULL,
38
+ failed_command TEXT NOT NULL,
39
+ error_output TEXT,
40
+ corrected_command TEXT NOT NULL,
41
+ worked BOOLEAN DEFAULT TRUE,
42
+ error_type TEXT,
43
+ created_at BIGINT NOT NULL
44
+ )`,
45
+ // Migration 5: outputs table
46
+ `CREATE TABLE IF NOT EXISTS outputs (
47
+ id SERIAL PRIMARY KEY,
48
+ session_id TEXT,
49
+ command TEXT NOT NULL,
50
+ raw_output_path TEXT,
51
+ compressed_summary TEXT,
52
+ tokens_raw INTEGER DEFAULT 0,
53
+ tokens_compressed INTEGER DEFAULT 0,
54
+ provider TEXT,
55
+ model TEXT,
56
+ created_at BIGINT NOT NULL
57
+ )`,
58
+ // Migration 6: index on corrections
59
+ `CREATE INDEX IF NOT EXISTS idx_corrections_prompt ON corrections(prompt)`,
60
+ // Migration 7: feedback table
61
+ `CREATE TABLE IF NOT EXISTS feedback (
62
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
63
+ message TEXT NOT NULL,
64
+ email TEXT,
65
+ category TEXT DEFAULT 'general',
66
+ version TEXT,
67
+ machine_id TEXT,
68
+ created_at TEXT NOT NULL DEFAULT NOW()::text
69
+ )`,
70
+ ];
@@ -1,6 +1,7 @@
1
1
  // MCP Server for terminal — exposes terminal capabilities to AI agents
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
4
5
  import { createSession } from "../sessions-db.js";
5
6
  import { createHelpers } from "./tools/helpers.js";
6
7
  // Tool registration modules
@@ -45,6 +46,39 @@ export function createServer() {
45
46
  registerMemoryTools(server, h);
46
47
  registerMetaTools(server, h);
47
48
  registerCloudTools(server, "terminal");
49
+ // ── Agent Tools ──────────────────────────────────────────────────────────
50
+ const _agentReg = new Map();
51
+ server.tool("register_agent", "Register an agent session (idempotent). Auto-updates last_seen_at on re-register.", { name: z.string(), session_id: z.string().optional() }, async (a) => {
52
+ const existing = [..._agentReg.values()].find(x => x.name === a.name);
53
+ if (existing) {
54
+ existing.last_seen_at = new Date().toISOString();
55
+ return { content: [{ type: "text", text: JSON.stringify(existing) }] };
56
+ }
57
+ const id = Math.random().toString(36).slice(2, 10);
58
+ const ag = { id, name: a.name, last_seen_at: new Date().toISOString() };
59
+ _agentReg.set(id, ag);
60
+ return { content: [{ type: "text", text: JSON.stringify(ag) }] };
61
+ });
62
+ server.tool("heartbeat", "Update last_seen_at to signal agent is active.", { agent_id: z.string() }, async (a) => {
63
+ const ag = _agentReg.get(a.agent_id);
64
+ if (!ag)
65
+ return { content: [{ type: "text", text: `Agent not found: ${a.agent_id}` }], isError: true };
66
+ ag.last_seen_at = new Date().toISOString();
67
+ return { content: [{ type: "text", text: JSON.stringify({ id: ag.id, name: ag.name, last_seen_at: ag.last_seen_at }) }] };
68
+ });
69
+ server.tool("set_focus", "Set active project context for this agent session.", { agent_id: z.string(), project_id: z.string().nullable().optional() }, async (a) => {
70
+ const ag = _agentReg.get(a.agent_id);
71
+ if (!ag)
72
+ return { content: [{ type: "text", text: `Agent not found: ${a.agent_id}` }], isError: true };
73
+ ag.project_id = a.project_id ?? undefined;
74
+ return { content: [{ type: "text", text: a.project_id ? `Focus: ${a.project_id}` : "Focus cleared" }] };
75
+ });
76
+ server.tool("list_agents", "List all registered agents.", {}, async () => {
77
+ const agents = [..._agentReg.values()];
78
+ if (agents.length === 0)
79
+ return { content: [{ type: "text", text: "No agents registered." }] };
80
+ return { content: [{ type: "text", text: JSON.stringify(agents, null, 2) }] };
81
+ });
48
82
  return server;
49
83
  }
50
84
  // ── main: start MCP server via stdio ─────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "4.3.2",
3
+ "version": "4.3.3",
4
4
  "description": "Smart terminal wrapper for AI agents and humans — structured output, token compression, MCP server, natural language",
5
5
  "type": "module",
6
6
  "files": [
@@ -0,0 +1,77 @@
1
+ /**
2
+ * PostgreSQL migrations for open-terminal cloud sync.
3
+ *
4
+ * Equivalent to the SQLite schema in sessions-db.ts, translated for PostgreSQL.
5
+ */
6
+
7
+ export const PG_MIGRATIONS: string[] = [
8
+ // Migration 1: sessions table
9
+ `CREATE TABLE IF NOT EXISTS sessions (
10
+ id TEXT PRIMARY KEY,
11
+ started_at BIGINT NOT NULL,
12
+ ended_at BIGINT,
13
+ cwd TEXT NOT NULL,
14
+ provider TEXT,
15
+ model TEXT
16
+ )`,
17
+
18
+ // Migration 2: interactions table
19
+ `CREATE TABLE IF NOT EXISTS interactions (
20
+ id SERIAL PRIMARY KEY,
21
+ session_id TEXT NOT NULL REFERENCES sessions(id),
22
+ nl TEXT NOT NULL,
23
+ command TEXT,
24
+ output TEXT,
25
+ exit_code INTEGER,
26
+ tokens_used INTEGER DEFAULT 0,
27
+ tokens_saved INTEGER DEFAULT 0,
28
+ duration_ms INTEGER,
29
+ model TEXT,
30
+ cached BOOLEAN DEFAULT FALSE,
31
+ created_at BIGINT NOT NULL
32
+ )`,
33
+
34
+ // Migration 3: indexes on interactions and sessions
35
+ `CREATE INDEX IF NOT EXISTS idx_interactions_session ON interactions(session_id)`,
36
+ `CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
37
+
38
+ // Migration 4: corrections table
39
+ `CREATE TABLE IF NOT EXISTS corrections (
40
+ id SERIAL PRIMARY KEY,
41
+ prompt TEXT NOT NULL,
42
+ failed_command TEXT NOT NULL,
43
+ error_output TEXT,
44
+ corrected_command TEXT NOT NULL,
45
+ worked BOOLEAN DEFAULT TRUE,
46
+ error_type TEXT,
47
+ created_at BIGINT NOT NULL
48
+ )`,
49
+
50
+ // Migration 5: outputs table
51
+ `CREATE TABLE IF NOT EXISTS outputs (
52
+ id SERIAL PRIMARY KEY,
53
+ session_id TEXT,
54
+ command TEXT NOT NULL,
55
+ raw_output_path TEXT,
56
+ compressed_summary TEXT,
57
+ tokens_raw INTEGER DEFAULT 0,
58
+ tokens_compressed INTEGER DEFAULT 0,
59
+ provider TEXT,
60
+ model TEXT,
61
+ created_at BIGINT NOT NULL
62
+ )`,
63
+
64
+ // Migration 6: index on corrections
65
+ `CREATE INDEX IF NOT EXISTS idx_corrections_prompt ON corrections(prompt)`,
66
+
67
+ // Migration 7: feedback table
68
+ `CREATE TABLE IF NOT EXISTS feedback (
69
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
70
+ message TEXT NOT NULL,
71
+ email TEXT,
72
+ category TEXT DEFAULT 'general',
73
+ version TEXT,
74
+ machine_id TEXT,
75
+ created_at TEXT NOT NULL DEFAULT NOW()::text
76
+ )`,
77
+ ];
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
 
@@ -52,6 +53,58 @@ export function createServer(): McpServer {
52
53
  registerMetaTools(server, h);
53
54
  registerCloudTools(server, "terminal");
54
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
+ );
107
+
55
108
  return server;
56
109
  }
57
110