@hasna/terminal 3.2.1 → 3.3.1
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/cli.js +23 -5
- package/dist/mcp/install.js +143 -52
- package/dist/mcp/server.js +5 -5
- package/package.json +1 -1
- package/src/cli.tsx +26 -5
- package/src/mcp/install.ts +126 -55
- package/src/mcp/server.ts +5 -5
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,13 @@ EXAMPLES:
|
|
|
18
18
|
terminal "kill port 3000"
|
|
19
19
|
terminal "how many lines of code"
|
|
20
20
|
|
|
21
|
+
SETUP:
|
|
22
|
+
install Set up MCP server for all AI agents (Claude, Codex, Gemini)
|
|
23
|
+
install --claude Set up for Claude Code only
|
|
24
|
+
install --codex Set up for Codex only
|
|
25
|
+
install --gemini Set up for Gemini CLI only
|
|
26
|
+
uninstall Remove from all agents
|
|
27
|
+
|
|
21
28
|
SUBCOMMANDS:
|
|
22
29
|
repo Git repo state (branch + status + log)
|
|
23
30
|
symbols <file> File outline (functions, classes, exports)
|
|
@@ -26,8 +33,7 @@ SUBCOMMANDS:
|
|
|
26
33
|
sessions [stats|<id>] Session history and analytics
|
|
27
34
|
recipe add|list|run|delete Reusable command recipes
|
|
28
35
|
collection create|list Recipe collections
|
|
29
|
-
mcp serve Start MCP server
|
|
30
|
-
mcp install --claude|--codex Install MCP server
|
|
36
|
+
mcp serve Start MCP server (called by agents, not you)
|
|
31
37
|
discover [--days=N] [--json] Scan Claude sessions, show token savings potential
|
|
32
38
|
snapshot Terminal state as JSON
|
|
33
39
|
--help Show this help
|
|
@@ -61,6 +67,17 @@ if (args[0] === "--version" || args[0] === "-v") {
|
|
|
61
67
|
}
|
|
62
68
|
process.exit(0);
|
|
63
69
|
}
|
|
70
|
+
// ── Install / Uninstall ──────────────────────────────────────────────────────
|
|
71
|
+
if (args[0] === "install") {
|
|
72
|
+
const { handleInstall } = await import("./mcp/install.js");
|
|
73
|
+
handleInstall(args.slice(1));
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
if (args[0] === "uninstall") {
|
|
77
|
+
const { handleInstall } = await import("./mcp/install.js");
|
|
78
|
+
handleInstall(["uninstall"]);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
64
81
|
// ── MCP commands ─────────────────────────────────────────────────────────────
|
|
65
82
|
if (args[0] === "mcp") {
|
|
66
83
|
if (args[1] === "serve" || args.length === 1) {
|
|
@@ -71,11 +88,12 @@ if (args[0] === "mcp") {
|
|
|
71
88
|
});
|
|
72
89
|
}
|
|
73
90
|
else if (args[1] === "install") {
|
|
74
|
-
|
|
75
|
-
|
|
91
|
+
// Legacy: `terminal mcp install` still works
|
|
92
|
+
const { handleInstall } = await import("./mcp/install.js");
|
|
93
|
+
handleInstall(args.slice(2));
|
|
76
94
|
}
|
|
77
95
|
else {
|
|
78
|
-
console.log("Usage:
|
|
96
|
+
console.log("Usage: terminal mcp serve");
|
|
79
97
|
}
|
|
80
98
|
}
|
|
81
99
|
// ── Hook commands ────────────────────────────────────────────────────────────
|
package/dist/mcp/install.js
CHANGED
|
@@ -1,98 +1,189 @@
|
|
|
1
|
-
// MCP installation
|
|
1
|
+
// MCP installation — one command to rule them all
|
|
2
|
+
// `npx @hasna/terminal install` → installs globally + configures all AI agents
|
|
2
3
|
import { execSync } from "child_process";
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
4
5
|
import { homedir } from "os";
|
|
5
6
|
import { join } from "path";
|
|
6
|
-
const TERMINAL_BIN = "terminal"; // the CLI binary name
|
|
7
7
|
function which(cmd) {
|
|
8
8
|
try {
|
|
9
|
-
return execSync(`which ${cmd}`, { encoding: "utf8" }).trim();
|
|
9
|
+
return execSync(`which ${cmd}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
10
10
|
}
|
|
11
11
|
catch {
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
function log(icon, msg) { console.log(` ${icon} ${msg}`); }
|
|
16
|
+
// ── Detect what's installed ──────────────────────────────────────────────────
|
|
17
|
+
function hasClaude() { return !!which("claude"); }
|
|
18
|
+
function hasCodex() { return !!which("codex"); }
|
|
19
|
+
function hasGemini() { return !!which("gemini"); }
|
|
20
|
+
// ── Install for Claude Code ─────────────────────────────────────────────────
|
|
21
|
+
function installClaude(bin) {
|
|
22
|
+
if (!hasClaude()) {
|
|
23
|
+
log("–", "Claude Code not found, skipping");
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
16
26
|
try {
|
|
17
|
-
execSync(`claude mcp add --transport stdio --scope user
|
|
18
|
-
|
|
27
|
+
execSync(`claude mcp add --transport stdio --scope user terminal -- ${bin} mcp serve`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
28
|
+
log("✓", "Claude Code");
|
|
19
29
|
return true;
|
|
20
30
|
}
|
|
21
|
-
catch
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
catch {
|
|
32
|
+
// May already exist
|
|
33
|
+
try {
|
|
34
|
+
execSync(`claude mcp remove terminal -s user`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
35
|
+
execSync(`claude mcp add --transport stdio --scope user terminal -- ${bin} mcp serve`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
36
|
+
log("✓", "Claude Code (updated)");
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
log("✗", `Claude Code — ${e}`);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
24
43
|
}
|
|
25
44
|
}
|
|
26
|
-
|
|
27
|
-
|
|
45
|
+
// ── Install for Codex ───────────────────────────────────────────────────────
|
|
46
|
+
function installCodex(bin) {
|
|
47
|
+
if (!hasCodex()) {
|
|
48
|
+
log("–", "Codex not found, skipping");
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const dir = join(homedir(), ".codex");
|
|
52
|
+
const configPath = join(dir, "config.toml");
|
|
28
53
|
try {
|
|
54
|
+
if (!existsSync(dir))
|
|
55
|
+
mkdirSync(dir, { recursive: true });
|
|
29
56
|
let content = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
30
|
-
if
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
const bin = which(TERMINAL_BIN) ?? "npx @hasna/terminal";
|
|
35
|
-
content += `\n[mcp_servers.open-terminal]\ncommand = "${bin}"\nargs = ["mcp", "serve"]\n`;
|
|
57
|
+
// Remove old entry if exists
|
|
58
|
+
content = content.replace(/\n?\[mcp_servers\.terminal\][^\[]*/g, "");
|
|
59
|
+
content += `\n[mcp_servers.terminal]\ncommand = "${bin}"\nargs = ["mcp", "serve"]\n`;
|
|
36
60
|
writeFileSync(configPath, content);
|
|
37
|
-
|
|
61
|
+
log("✓", "Codex");
|
|
38
62
|
return true;
|
|
39
63
|
}
|
|
40
64
|
catch (e) {
|
|
41
|
-
|
|
65
|
+
log("✗", `Codex — ${e}`);
|
|
42
66
|
return false;
|
|
43
67
|
}
|
|
44
68
|
}
|
|
45
|
-
|
|
46
|
-
|
|
69
|
+
// ── Install for Gemini ──────────────────────────────────────────────────────
|
|
70
|
+
function installGemini(bin) {
|
|
71
|
+
if (!hasGemini()) {
|
|
72
|
+
log("–", "Gemini CLI not found, skipping");
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const dir = join(homedir(), ".gemini");
|
|
76
|
+
const configPath = join(dir, "settings.json");
|
|
47
77
|
try {
|
|
78
|
+
if (!existsSync(dir))
|
|
79
|
+
mkdirSync(dir, { recursive: true });
|
|
48
80
|
let config = {};
|
|
49
81
|
if (existsSync(configPath)) {
|
|
50
|
-
|
|
82
|
+
try {
|
|
83
|
+
config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
84
|
+
}
|
|
85
|
+
catch { }
|
|
51
86
|
}
|
|
52
87
|
if (!config.mcpServers)
|
|
53
88
|
config.mcpServers = {};
|
|
54
|
-
|
|
55
|
-
const args = which(TERMINAL_BIN) ? ["mcp", "serve"] : ["@hasna/terminal", "mcp", "serve"];
|
|
56
|
-
config.mcpServers["open-terminal"] = { command: bin, args };
|
|
89
|
+
config.mcpServers["terminal"] = { command: bin, args: ["mcp", "serve"] };
|
|
57
90
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
58
|
-
|
|
91
|
+
log("✓", "Gemini CLI");
|
|
59
92
|
return true;
|
|
60
93
|
}
|
|
61
94
|
catch (e) {
|
|
62
|
-
|
|
95
|
+
log("✗", `Gemini — ${e}`);
|
|
63
96
|
return false;
|
|
64
97
|
}
|
|
65
98
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
99
|
+
// ── Uninstall ───────────────────────────────────────────────────────────────
|
|
100
|
+
function uninstallClaude() {
|
|
101
|
+
if (!hasClaude())
|
|
102
|
+
return false;
|
|
103
|
+
try {
|
|
104
|
+
execSync(`claude mcp remove terminal -s user`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
105
|
+
log("✓", "Removed from Claude Code");
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
70
111
|
}
|
|
71
|
-
|
|
112
|
+
function uninstallCodex() {
|
|
113
|
+
const configPath = join(homedir(), ".codex", "config.toml");
|
|
114
|
+
if (!existsSync(configPath))
|
|
115
|
+
return false;
|
|
116
|
+
try {
|
|
117
|
+
let content = readFileSync(configPath, "utf8");
|
|
118
|
+
if (!content.includes("terminal"))
|
|
119
|
+
return false;
|
|
120
|
+
content = content.replace(/\n?\[mcp_servers\.terminal\][^\[]*/g, "");
|
|
121
|
+
writeFileSync(configPath, content);
|
|
122
|
+
log("✓", "Removed from Codex");
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function uninstallGemini() {
|
|
130
|
+
const configPath = join(homedir(), ".gemini", "settings.json");
|
|
131
|
+
if (!existsSync(configPath))
|
|
132
|
+
return false;
|
|
133
|
+
try {
|
|
134
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
135
|
+
if (!config.mcpServers?.["terminal"])
|
|
136
|
+
return false;
|
|
137
|
+
delete config.mcpServers["terminal"];
|
|
138
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
139
|
+
log("✓", "Removed from Gemini CLI");
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// ── Main install handler ────────────────────────────────────────────────────
|
|
147
|
+
export function handleInstall(args) {
|
|
72
148
|
const flags = new Set(args);
|
|
73
|
-
|
|
74
|
-
|
|
149
|
+
// Uninstall
|
|
150
|
+
if (flags.has("uninstall") || flags.has("--uninstall")) {
|
|
151
|
+
console.log("\n Removing terminal MCP server...\n");
|
|
152
|
+
uninstallClaude();
|
|
153
|
+
uninstallCodex();
|
|
154
|
+
uninstallGemini();
|
|
155
|
+
console.log("\n Done. Restart your agents to apply.\n");
|
|
75
156
|
return;
|
|
76
157
|
}
|
|
77
|
-
|
|
78
|
-
|
|
158
|
+
// Targeted install
|
|
159
|
+
if (flags.has("--claude") || flags.has("--codex") || flags.has("--gemini")) {
|
|
160
|
+
const bin = which("terminal") ?? which("t") ?? "npx @hasna/terminal";
|
|
161
|
+
console.log("");
|
|
162
|
+
if (flags.has("--claude"))
|
|
163
|
+
installClaude(bin);
|
|
164
|
+
if (flags.has("--codex"))
|
|
165
|
+
installCodex(bin);
|
|
166
|
+
if (flags.has("--gemini"))
|
|
167
|
+
installGemini(bin);
|
|
168
|
+
console.log("");
|
|
79
169
|
return;
|
|
80
170
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
171
|
+
// ── Default: install everything ─────────────────────────────────────────
|
|
172
|
+
const bin = which("terminal") ?? which("t") ?? "npx @hasna/terminal";
|
|
173
|
+
console.log(`\n terminal — setting up MCP...\n`);
|
|
174
|
+
let count = 0;
|
|
175
|
+
if (installClaude(bin))
|
|
176
|
+
count++;
|
|
177
|
+
if (installCodex(bin))
|
|
178
|
+
count++;
|
|
179
|
+
if (installGemini(bin))
|
|
180
|
+
count++;
|
|
181
|
+
if (count === 0) {
|
|
182
|
+
console.log(`\n No agents found. Install Claude Code, Codex, or Gemini CLI first.\n`);
|
|
84
183
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return;
|
|
184
|
+
else {
|
|
185
|
+
console.log(`\n ${count} agent${count > 1 ? "s" : ""} ready. Restart to apply.\n`);
|
|
88
186
|
}
|
|
89
|
-
console.log("Usage: t mcp install [--claude|--codex|--gemini|--all]");
|
|
90
|
-
console.log("");
|
|
91
|
-
console.log("Install open-terminal as an MCP server for AI coding agents.");
|
|
92
|
-
console.log("");
|
|
93
|
-
console.log("Options:");
|
|
94
|
-
console.log(" --claude Install for Claude Code");
|
|
95
|
-
console.log(" --codex Install for OpenAI Codex");
|
|
96
|
-
console.log(" --gemini Install for Gemini CLI");
|
|
97
|
-
console.log(" --all Install for all agents");
|
|
98
187
|
}
|
|
188
|
+
// Re-export individual installers for programmatic use
|
|
189
|
+
export { installClaude, installCodex, installGemini, uninstallClaude, uninstallCodex, uninstallGemini };
|
package/dist/mcp/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// MCP Server for
|
|
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
4
|
import { z } from "zod";
|
|
@@ -56,7 +56,7 @@ function exec(command, cwd, timeout) {
|
|
|
56
56
|
// ── server ───────────────────────────────────────────────────────────────────
|
|
57
57
|
export function createServer() {
|
|
58
58
|
const server = new McpServer({
|
|
59
|
-
name: "
|
|
59
|
+
name: "terminal",
|
|
60
60
|
version: "0.2.0",
|
|
61
61
|
});
|
|
62
62
|
// ── execute: run a command, return structured result ──────────────────────
|
|
@@ -202,10 +202,10 @@ export function createServer() {
|
|
|
202
202
|
};
|
|
203
203
|
});
|
|
204
204
|
// ── status: show server info ──────────────────────────────────────────────
|
|
205
|
-
server.tool("status", "Get
|
|
205
|
+
server.tool("status", "Get terminal server status, capabilities, and available parsers.", async () => {
|
|
206
206
|
return {
|
|
207
207
|
content: [{ type: "text", text: JSON.stringify({
|
|
208
|
-
name: "
|
|
208
|
+
name: "terminal", version: "3.3.0", cwd: process.cwd(),
|
|
209
209
|
features: ["ai-output-processing", "token-compression", "noise-filtering", "diff-caching", "lazy-execution", "progressive-disclosure"],
|
|
210
210
|
}) }],
|
|
211
211
|
};
|
|
@@ -541,5 +541,5 @@ export async function startMcpServer() {
|
|
|
541
541
|
const server = createServer();
|
|
542
542
|
const transport = new StdioServerTransport();
|
|
543
543
|
await server.connect(transport);
|
|
544
|
-
console.error("
|
|
544
|
+
console.error("terminal MCP server running on stdio");
|
|
545
545
|
}
|
package/package.json
CHANGED
package/src/cli.tsx
CHANGED
|
@@ -21,6 +21,13 @@ EXAMPLES:
|
|
|
21
21
|
terminal "kill port 3000"
|
|
22
22
|
terminal "how many lines of code"
|
|
23
23
|
|
|
24
|
+
SETUP:
|
|
25
|
+
install Set up MCP server for all AI agents (Claude, Codex, Gemini)
|
|
26
|
+
install --claude Set up for Claude Code only
|
|
27
|
+
install --codex Set up for Codex only
|
|
28
|
+
install --gemini Set up for Gemini CLI only
|
|
29
|
+
uninstall Remove from all agents
|
|
30
|
+
|
|
24
31
|
SUBCOMMANDS:
|
|
25
32
|
repo Git repo state (branch + status + log)
|
|
26
33
|
symbols <file> File outline (functions, classes, exports)
|
|
@@ -29,8 +36,7 @@ SUBCOMMANDS:
|
|
|
29
36
|
sessions [stats|<id>] Session history and analytics
|
|
30
37
|
recipe add|list|run|delete Reusable command recipes
|
|
31
38
|
collection create|list Recipe collections
|
|
32
|
-
mcp serve Start MCP server
|
|
33
|
-
mcp install --claude|--codex Install MCP server
|
|
39
|
+
mcp serve Start MCP server (called by agents, not you)
|
|
34
40
|
discover [--days=N] [--json] Scan Claude sessions, show token savings potential
|
|
35
41
|
snapshot Terminal state as JSON
|
|
36
42
|
--help Show this help
|
|
@@ -63,6 +69,20 @@ if (args[0] === "--version" || args[0] === "-v") {
|
|
|
63
69
|
process.exit(0);
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
// ── Install / Uninstall ──────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
if (args[0] === "install") {
|
|
75
|
+
const { handleInstall } = await import("./mcp/install.js");
|
|
76
|
+
handleInstall(args.slice(1));
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (args[0] === "uninstall") {
|
|
81
|
+
const { handleInstall } = await import("./mcp/install.js");
|
|
82
|
+
handleInstall(["uninstall"]);
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
66
86
|
// ── MCP commands ─────────────────────────────────────────────────────────────
|
|
67
87
|
|
|
68
88
|
if (args[0] === "mcp") {
|
|
@@ -73,10 +93,11 @@ if (args[0] === "mcp") {
|
|
|
73
93
|
process.exit(1);
|
|
74
94
|
});
|
|
75
95
|
} else if (args[1] === "install") {
|
|
76
|
-
|
|
77
|
-
|
|
96
|
+
// Legacy: `terminal mcp install` still works
|
|
97
|
+
const { handleInstall } = await import("./mcp/install.js");
|
|
98
|
+
handleInstall(args.slice(2));
|
|
78
99
|
} else {
|
|
79
|
-
console.log("Usage:
|
|
100
|
+
console.log("Usage: terminal mcp serve");
|
|
80
101
|
}
|
|
81
102
|
}
|
|
82
103
|
|
package/src/mcp/install.ts
CHANGED
|
@@ -1,94 +1,165 @@
|
|
|
1
|
-
// MCP installation
|
|
1
|
+
// MCP installation — one command to rule them all
|
|
2
|
+
// `npx @hasna/terminal install` → installs globally + configures all AI agents
|
|
2
3
|
|
|
3
4
|
import { execSync } from "child_process";
|
|
4
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
5
6
|
import { homedir } from "os";
|
|
6
7
|
import { join } from "path";
|
|
7
8
|
|
|
8
|
-
const TERMINAL_BIN = "terminal"; // the CLI binary name
|
|
9
|
-
|
|
10
9
|
function which(cmd: string): string | null {
|
|
11
|
-
try {
|
|
12
|
-
return execSync(`which ${cmd}`, { encoding: "utf8" }).trim();
|
|
13
|
-
} catch {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
10
|
+
try { return execSync(`which ${cmd}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim(); } catch { return null; }
|
|
16
11
|
}
|
|
17
12
|
|
|
18
|
-
|
|
13
|
+
function log(icon: string, msg: string) { console.log(` ${icon} ${msg}`); }
|
|
14
|
+
|
|
15
|
+
// ── Detect what's installed ──────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function hasClaude(): boolean { return !!which("claude"); }
|
|
18
|
+
function hasCodex(): boolean { return !!which("codex"); }
|
|
19
|
+
function hasGemini(): boolean { return !!which("gemini"); }
|
|
20
|
+
|
|
21
|
+
// ── Install for Claude Code ─────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
function installClaude(bin: string): boolean {
|
|
24
|
+
if (!hasClaude()) { log("–", "Claude Code not found, skipping"); return false; }
|
|
19
25
|
try {
|
|
20
|
-
execSync(
|
|
21
|
-
|
|
22
|
-
{ stdio: "inherit" }
|
|
23
|
-
);
|
|
24
|
-
console.log("✓ Installed open-terminal MCP server for Claude Code");
|
|
26
|
+
execSync(`claude mcp add --transport stdio --scope user terminal -- ${bin} mcp serve`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
27
|
+
log("✓", "Claude Code");
|
|
25
28
|
return true;
|
|
26
|
-
} catch
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
} catch {
|
|
30
|
+
// May already exist
|
|
31
|
+
try {
|
|
32
|
+
execSync(`claude mcp remove terminal -s user`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
33
|
+
execSync(`claude mcp add --transport stdio --scope user terminal -- ${bin} mcp serve`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
34
|
+
log("✓", "Claude Code (updated)");
|
|
35
|
+
return true;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
log("✗", `Claude Code — ${e}`);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
29
40
|
}
|
|
30
41
|
}
|
|
31
42
|
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
// ── Install for Codex ───────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
function installCodex(bin: string): boolean {
|
|
46
|
+
if (!hasCodex()) { log("–", "Codex not found, skipping"); return false; }
|
|
47
|
+
const dir = join(homedir(), ".codex");
|
|
48
|
+
const configPath = join(dir, "config.toml");
|
|
34
49
|
try {
|
|
50
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
35
51
|
let content = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
36
|
-
if
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
const bin = which(TERMINAL_BIN) ?? "npx @hasna/terminal";
|
|
41
|
-
content += `\n[mcp_servers.open-terminal]\ncommand = "${bin}"\nargs = ["mcp", "serve"]\n`;
|
|
52
|
+
// Remove old entry if exists
|
|
53
|
+
content = content.replace(/\n?\[mcp_servers\.terminal\][^\[]*/g, "");
|
|
54
|
+
content += `\n[mcp_servers.terminal]\ncommand = "${bin}"\nargs = ["mcp", "serve"]\n`;
|
|
42
55
|
writeFileSync(configPath, content);
|
|
43
|
-
|
|
56
|
+
log("✓", "Codex");
|
|
44
57
|
return true;
|
|
45
58
|
} catch (e) {
|
|
46
|
-
|
|
59
|
+
log("✗", `Codex — ${e}`);
|
|
47
60
|
return false;
|
|
48
61
|
}
|
|
49
62
|
}
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
// ── Install for Gemini ──────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
function installGemini(bin: string): boolean {
|
|
67
|
+
if (!hasGemini()) { log("–", "Gemini CLI not found, skipping"); return false; }
|
|
68
|
+
const dir = join(homedir(), ".gemini");
|
|
69
|
+
const configPath = join(dir, "settings.json");
|
|
53
70
|
try {
|
|
71
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
54
72
|
let config: any = {};
|
|
55
73
|
if (existsSync(configPath)) {
|
|
56
|
-
config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
74
|
+
try { config = JSON.parse(readFileSync(configPath, "utf8")); } catch {}
|
|
57
75
|
}
|
|
58
76
|
if (!config.mcpServers) config.mcpServers = {};
|
|
59
|
-
|
|
60
|
-
const args = which(TERMINAL_BIN) ? ["mcp", "serve"] : ["@hasna/terminal", "mcp", "serve"];
|
|
61
|
-
config.mcpServers["open-terminal"] = { command: bin, args };
|
|
77
|
+
config.mcpServers["terminal"] = { command: bin, args: ["mcp", "serve"] };
|
|
62
78
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
63
|
-
|
|
79
|
+
log("✓", "Gemini CLI");
|
|
64
80
|
return true;
|
|
65
81
|
} catch (e) {
|
|
66
|
-
|
|
82
|
+
log("✗", `Gemini — ${e}`);
|
|
67
83
|
return false;
|
|
68
84
|
}
|
|
69
85
|
}
|
|
70
86
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
// ── Uninstall ───────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
function uninstallClaude(): boolean {
|
|
90
|
+
if (!hasClaude()) return false;
|
|
91
|
+
try { execSync(`claude mcp remove terminal -s user`, { stdio: ["pipe", "pipe", "pipe"] }); log("✓", "Removed from Claude Code"); return true; } catch { return false; }
|
|
75
92
|
}
|
|
76
93
|
|
|
77
|
-
|
|
94
|
+
function uninstallCodex(): boolean {
|
|
95
|
+
const configPath = join(homedir(), ".codex", "config.toml");
|
|
96
|
+
if (!existsSync(configPath)) return false;
|
|
97
|
+
try {
|
|
98
|
+
let content = readFileSync(configPath, "utf8");
|
|
99
|
+
if (!content.includes("terminal")) return false;
|
|
100
|
+
content = content.replace(/\n?\[mcp_servers\.terminal\][^\[]*/g, "");
|
|
101
|
+
writeFileSync(configPath, content);
|
|
102
|
+
log("✓", "Removed from Codex");
|
|
103
|
+
return true;
|
|
104
|
+
} catch { return false; }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function uninstallGemini(): boolean {
|
|
108
|
+
const configPath = join(homedir(), ".gemini", "settings.json");
|
|
109
|
+
if (!existsSync(configPath)) return false;
|
|
110
|
+
try {
|
|
111
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
112
|
+
if (!config.mcpServers?.["terminal"]) return false;
|
|
113
|
+
delete config.mcpServers["terminal"];
|
|
114
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
115
|
+
log("✓", "Removed from Gemini CLI");
|
|
116
|
+
return true;
|
|
117
|
+
} catch { return false; }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Main install handler ────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
export function handleInstall(args: string[]): void {
|
|
78
123
|
const flags = new Set(args);
|
|
79
124
|
|
|
80
|
-
|
|
81
|
-
if (flags.has("
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
125
|
+
// Uninstall
|
|
126
|
+
if (flags.has("uninstall") || flags.has("--uninstall")) {
|
|
127
|
+
console.log("\n Removing terminal MCP server...\n");
|
|
128
|
+
uninstallClaude();
|
|
129
|
+
uninstallCodex();
|
|
130
|
+
uninstallGemini();
|
|
131
|
+
console.log("\n Done. Restart your agents to apply.\n");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Targeted install
|
|
136
|
+
if (flags.has("--claude") || flags.has("--codex") || flags.has("--gemini")) {
|
|
137
|
+
const bin = which("terminal") ?? which("t") ?? "npx @hasna/terminal";
|
|
138
|
+
console.log("");
|
|
139
|
+
if (flags.has("--claude")) installClaude(bin);
|
|
140
|
+
if (flags.has("--codex")) installCodex(bin);
|
|
141
|
+
if (flags.has("--gemini")) installGemini(bin);
|
|
142
|
+
console.log("");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Default: install everything ─────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
const bin = which("terminal") ?? which("t") ?? "npx @hasna/terminal";
|
|
149
|
+
|
|
150
|
+
console.log(`\n terminal — setting up MCP...\n`);
|
|
151
|
+
|
|
152
|
+
let count = 0;
|
|
153
|
+
if (installClaude(bin)) count++;
|
|
154
|
+
if (installCodex(bin)) count++;
|
|
155
|
+
if (installGemini(bin)) count++;
|
|
156
|
+
|
|
157
|
+
if (count === 0) {
|
|
158
|
+
console.log(`\n No agents found. Install Claude Code, Codex, or Gemini CLI first.\n`);
|
|
159
|
+
} else {
|
|
160
|
+
console.log(`\n ${count} agent${count > 1 ? "s" : ""} ready. Restart to apply.\n`);
|
|
161
|
+
}
|
|
94
162
|
}
|
|
163
|
+
|
|
164
|
+
// Re-export individual installers for programmatic use
|
|
165
|
+
export { installClaude, installCodex, installGemini, uninstallClaude, uninstallCodex, uninstallGemini };
|
package/src/mcp/server.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// MCP Server for
|
|
1
|
+
// MCP Server for terminal — exposes terminal capabilities to AI agents
|
|
2
2
|
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -61,7 +61,7 @@ function exec(command: string, cwd?: string, timeout?: number): Promise<{ exitCo
|
|
|
61
61
|
|
|
62
62
|
export function createServer(): McpServer {
|
|
63
63
|
const server = new McpServer({
|
|
64
|
-
name: "
|
|
64
|
+
name: "terminal",
|
|
65
65
|
version: "0.2.0",
|
|
66
66
|
});
|
|
67
67
|
|
|
@@ -251,11 +251,11 @@ export function createServer(): McpServer {
|
|
|
251
251
|
|
|
252
252
|
server.tool(
|
|
253
253
|
"status",
|
|
254
|
-
"Get
|
|
254
|
+
"Get terminal server status, capabilities, and available parsers.",
|
|
255
255
|
async () => {
|
|
256
256
|
return {
|
|
257
257
|
content: [{ type: "text" as const, text: JSON.stringify({
|
|
258
|
-
name: "
|
|
258
|
+
name: "terminal", version: "3.3.0", cwd: process.cwd(),
|
|
259
259
|
features: ["ai-output-processing", "token-compression", "noise-filtering", "diff-caching", "lazy-execution", "progressive-disclosure"],
|
|
260
260
|
}) }],
|
|
261
261
|
};
|
|
@@ -762,5 +762,5 @@ export async function startMcpServer(): Promise<void> {
|
|
|
762
762
|
const server = createServer();
|
|
763
763
|
const transport = new StdioServerTransport();
|
|
764
764
|
await server.connect(transport);
|
|
765
|
-
console.error("
|
|
765
|
+
console.error("terminal MCP server running on stdio");
|
|
766
766
|
}
|