@hasna/terminal 3.2.1 → 3.3.0
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 +169 -51
- package/package.json +1 -1
- package/src/cli.tsx +26 -5
- package/src/mcp/install.ts +152 -54
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,216 @@
|
|
|
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 open-terminal -- ${
|
|
18
|
-
|
|
27
|
+
execSync(`claude mcp add --transport stdio --scope user open-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 open-terminal -s user`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
35
|
+
execSync(`claude mcp add --transport stdio --scope user open-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
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
const bin = which(TERMINAL_BIN) ?? "npx @hasna/terminal";
|
|
57
|
+
// Remove old entry if exists
|
|
58
|
+
content = content.replace(/\n?\[mcp_servers\.open-terminal\][^\[]*/g, "");
|
|
35
59
|
content += `\n[mcp_servers.open-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["open-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 open-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("open-terminal"))
|
|
119
|
+
return false;
|
|
120
|
+
content = content.replace(/\n?\[mcp_servers\.open-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?.["open-terminal"])
|
|
136
|
+
return false;
|
|
137
|
+
delete config.mcpServers["open-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 open-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(`
|
|
174
|
+
┌─────────────────────────────────────┐
|
|
175
|
+
│ open-terminal │
|
|
176
|
+
│ Smart terminal for AI agents │
|
|
177
|
+
└─────────────────────────────────────┘
|
|
178
|
+
|
|
179
|
+
Setting up MCP server for all agents...
|
|
180
|
+
`);
|
|
181
|
+
let count = 0;
|
|
182
|
+
if (installClaude(bin))
|
|
183
|
+
count++;
|
|
184
|
+
if (installCodex(bin))
|
|
185
|
+
count++;
|
|
186
|
+
if (installGemini(bin))
|
|
187
|
+
count++;
|
|
188
|
+
if (count === 0) {
|
|
189
|
+
console.log(`
|
|
190
|
+
No AI agents found. Install one first:
|
|
191
|
+
|
|
192
|
+
npm i -g @anthropic-ai/claude-code # Claude Code
|
|
193
|
+
npm i -g @openai/codex # Codex
|
|
194
|
+
npm i -g @anthropic-ai/gemini-cli # Gemini CLI
|
|
195
|
+
|
|
196
|
+
Then run: terminal install
|
|
197
|
+
`);
|
|
84
198
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
199
|
+
else {
|
|
200
|
+
console.log(`
|
|
201
|
+
Done. ${count} agent${count > 1 ? "s" : ""} configured.
|
|
202
|
+
Restart your agent to start using open-terminal.
|
|
203
|
+
|
|
204
|
+
Your AI agent now has these tools:
|
|
205
|
+
execute_smart Run any command, get AI-summarized output
|
|
206
|
+
execute_diff Run command, see only what changed
|
|
207
|
+
search_content Smart grep with file grouping
|
|
208
|
+
search_files Find files by pattern
|
|
209
|
+
read_symbol Read a function by name (not whole file)
|
|
210
|
+
boot Full project context in one call
|
|
211
|
+
repo_state Git status + log + diff in one call
|
|
212
|
+
`);
|
|
88
213
|
}
|
|
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
214
|
}
|
|
215
|
+
// Re-export individual installers for programmatic use
|
|
216
|
+
export { installClaude, installCodex, installGemini, uninstallClaude, uninstallCodex, uninstallGemini };
|
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,192 @@
|
|
|
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 open-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 open-terminal -s user`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
33
|
+
execSync(`claude mcp add --transport stdio --scope user open-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
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
const bin = which(TERMINAL_BIN) ?? "npx @hasna/terminal";
|
|
52
|
+
// Remove old entry if exists
|
|
53
|
+
content = content.replace(/\n?\[mcp_servers\.open-terminal\][^\[]*/g, "");
|
|
41
54
|
content += `\n[mcp_servers.open-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["open-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 open-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("open-terminal")) return false;
|
|
100
|
+
content = content.replace(/\n?\[mcp_servers\.open-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?.["open-terminal"]) return false;
|
|
113
|
+
delete config.mcpServers["open-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 open-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(`
|
|
151
|
+
┌─────────────────────────────────────┐
|
|
152
|
+
│ open-terminal │
|
|
153
|
+
│ Smart terminal for AI agents │
|
|
154
|
+
└─────────────────────────────────────┘
|
|
155
|
+
|
|
156
|
+
Setting up MCP server for all agents...
|
|
157
|
+
`);
|
|
158
|
+
|
|
159
|
+
let count = 0;
|
|
160
|
+
if (installClaude(bin)) count++;
|
|
161
|
+
if (installCodex(bin)) count++;
|
|
162
|
+
if (installGemini(bin)) count++;
|
|
163
|
+
|
|
164
|
+
if (count === 0) {
|
|
165
|
+
console.log(`
|
|
166
|
+
No AI agents found. Install one first:
|
|
167
|
+
|
|
168
|
+
npm i -g @anthropic-ai/claude-code # Claude Code
|
|
169
|
+
npm i -g @openai/codex # Codex
|
|
170
|
+
npm i -g @anthropic-ai/gemini-cli # Gemini CLI
|
|
171
|
+
|
|
172
|
+
Then run: terminal install
|
|
173
|
+
`);
|
|
174
|
+
} else {
|
|
175
|
+
console.log(`
|
|
176
|
+
Done. ${count} agent${count > 1 ? "s" : ""} configured.
|
|
177
|
+
Restart your agent to start using open-terminal.
|
|
178
|
+
|
|
179
|
+
Your AI agent now has these tools:
|
|
180
|
+
execute_smart Run any command, get AI-summarized output
|
|
181
|
+
execute_diff Run command, see only what changed
|
|
182
|
+
search_content Smart grep with file grouping
|
|
183
|
+
search_files Find files by pattern
|
|
184
|
+
read_symbol Read a function by name (not whole file)
|
|
185
|
+
boot Full project context in one call
|
|
186
|
+
repo_state Git status + log + diff in one call
|
|
187
|
+
`);
|
|
188
|
+
}
|
|
94
189
|
}
|
|
190
|
+
|
|
191
|
+
// Re-export individual installers for programmatic use
|
|
192
|
+
export { installClaude, installCodex, installGemini, uninstallClaude, uninstallCodex, uninstallGemini };
|