@hasna/terminal 4.3.2 → 4.3.4
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.
- package/dist/db/pg-migrations.js +70 -0
- package/dist/mcp/server.js +34 -0
- package/package.json +5 -4
- package/src/db/pg-migrations.ts +77 -0
- package/src/mcp/server.ts +53 -0
|
@@ -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
|
+
];
|
package/dist/mcp/server.js
CHANGED
|
@@ -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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/terminal",
|
|
3
|
-
"version": "4.3.
|
|
4
|
-
"description": "Smart terminal wrapper for AI agents and humans
|
|
3
|
+
"version": "4.3.4",
|
|
4
|
+
"description": "Smart terminal wrapper for AI agents and humans \u2014 structured output, token compression, MCP server, natural language",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist/**",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"build": "tsc",
|
|
19
19
|
"dev": "tsx src/cli.tsx",
|
|
20
20
|
"start": "node dist/cli.js",
|
|
21
|
-
"test": "bun test"
|
|
21
|
+
"test": "bun test",
|
|
22
|
+
"postinstall": "mkdir -p $HOME/.hasna/terminal/profiles $HOME/.hasna/terminal/outputs 2>/dev/null || true"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
@@ -45,4 +46,4 @@
|
|
|
45
46
|
"tsx": "^4.0.0",
|
|
46
47
|
"typescript": "^5.9.3"
|
|
47
48
|
}
|
|
48
|
-
}
|
|
49
|
+
}
|
|
@@ -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
|
|