gopeak 2.2.0 → 2.2.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.
@@ -0,0 +1,149 @@
1
+ /**
2
+ * GoPeak CLI Utilities
3
+ * Shared helpers for version checking, caching, and shell detection.
4
+ */
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { homedir } from 'os';
8
+ import { exec } from 'child_process';
9
+ import { promisify } from 'util';
10
+ import { fileURLToPath } from 'url';
11
+ import { dirname } from 'path';
12
+ import { get as httpsGet } from 'https';
13
+ const execAsync = promisify(exec);
14
+ /* ------------------------------------------------------------------ */
15
+ /* Paths */
16
+ /* ------------------------------------------------------------------ */
17
+ const GOPEAK_DIR = join(homedir(), '.gopeak');
18
+ const LAST_CHECK_FILE = join(GOPEAK_DIR, 'last-check');
19
+ const NOTIFY_FILE = join(GOPEAK_DIR, 'notify');
20
+ const ONBOARDING_SHOWN_FILE = join(GOPEAK_DIR, 'onboarding-shown');
21
+ const STAR_PROMPTED_FILE = join(GOPEAK_DIR, 'star-prompted');
22
+ export { GOPEAK_DIR, LAST_CHECK_FILE, NOTIFY_FILE, ONBOARDING_SHOWN_FILE, STAR_PROMPTED_FILE };
23
+ export function ensureGopeakDir() {
24
+ if (!existsSync(GOPEAK_DIR)) {
25
+ mkdirSync(GOPEAK_DIR, { recursive: true });
26
+ }
27
+ }
28
+ /* ------------------------------------------------------------------ */
29
+ /* Version */
30
+ /* ------------------------------------------------------------------ */
31
+ export function getLocalVersion() {
32
+ try {
33
+ const __filename = fileURLToPath(import.meta.url);
34
+ const __dirname = dirname(__filename);
35
+ // Walk up from build/cli/utils.js → project root
36
+ const pkgPath = join(__dirname, '..', '..', 'package.json');
37
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
38
+ return pkg.version ?? '0.0.0';
39
+ }
40
+ catch {
41
+ return '0.0.0';
42
+ }
43
+ }
44
+ /** Fetch latest version from npm registry (no dependencies). */
45
+ export function fetchLatestVersion() {
46
+ return new Promise((resolve) => {
47
+ const url = 'https://registry.npmjs.org/gopeak/latest';
48
+ const timeout = setTimeout(() => resolve(null), 5000);
49
+ httpsGet(url, (res) => {
50
+ let data = '';
51
+ res.on('data', (chunk) => { data += chunk; });
52
+ res.on('end', () => {
53
+ clearTimeout(timeout);
54
+ try {
55
+ const json = JSON.parse(data);
56
+ resolve(json.version ?? null);
57
+ }
58
+ catch {
59
+ resolve(null);
60
+ }
61
+ });
62
+ res.on('error', () => { clearTimeout(timeout); resolve(null); });
63
+ }).on('error', () => { clearTimeout(timeout); resolve(null); });
64
+ });
65
+ }
66
+ /** Compare two semver strings. Returns 1 if a > b, -1 if a < b, 0 if equal. */
67
+ export function compareSemver(a, b) {
68
+ const pa = a.replace(/^v/, '').split('.').map(Number);
69
+ const pb = b.replace(/^v/, '').split('.').map(Number);
70
+ for (let i = 0; i < 3; i++) {
71
+ const na = pa[i] ?? 0;
72
+ const nb = pb[i] ?? 0;
73
+ if (na > nb)
74
+ return 1;
75
+ if (na < nb)
76
+ return -1;
77
+ }
78
+ return 0;
79
+ }
80
+ /* ------------------------------------------------------------------ */
81
+ /* Cache */
82
+ /* ------------------------------------------------------------------ */
83
+ /** Check if the last update check was less than `maxAgeSeconds` ago. */
84
+ export function isCacheFresh(maxAgeSeconds = 86400) {
85
+ try {
86
+ if (!existsSync(LAST_CHECK_FILE))
87
+ return false;
88
+ const ts = parseInt(readFileSync(LAST_CHECK_FILE, 'utf-8').trim(), 10);
89
+ return (Date.now() / 1000 - ts) < maxAgeSeconds;
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
95
+ export function updateCacheTimestamp() {
96
+ ensureGopeakDir();
97
+ writeFileSync(LAST_CHECK_FILE, String(Math.floor(Date.now() / 1000)));
98
+ }
99
+ export function writeNotifyFile(message) {
100
+ ensureGopeakDir();
101
+ writeFileSync(NOTIFY_FILE, message);
102
+ }
103
+ export function clearNotifyFile() {
104
+ try {
105
+ if (existsSync(NOTIFY_FILE))
106
+ unlinkSync(NOTIFY_FILE);
107
+ }
108
+ catch { /* ignore */ }
109
+ }
110
+ /* ------------------------------------------------------------------ */
111
+ /* Shell detection */
112
+ /* ------------------------------------------------------------------ */
113
+ export function getShellRcFile() {
114
+ const shell = process.env.SHELL ?? '';
115
+ if (shell.includes('zsh'))
116
+ return join(homedir(), '.zshrc');
117
+ return join(homedir(), '.bashrc');
118
+ }
119
+ export function getShellName() {
120
+ const shell = process.env.SHELL ?? '';
121
+ if (shell.includes('zsh'))
122
+ return 'zsh';
123
+ return 'bash';
124
+ }
125
+ /* ------------------------------------------------------------------ */
126
+ /* Command helpers */
127
+ /* ------------------------------------------------------------------ */
128
+ export async function commandExists(cmd) {
129
+ try {
130
+ await execAsync(`command -v ${cmd}`);
131
+ return true;
132
+ }
133
+ catch {
134
+ return false;
135
+ }
136
+ }
137
+ export async function runCommand(cmd) {
138
+ try {
139
+ const { stdout, stderr } = await execAsync(cmd);
140
+ return { stdout: stdout.trim(), stderr: stderr.trim(), code: 0 };
141
+ }
142
+ catch (err) {
143
+ return {
144
+ stdout: (err.stdout ?? '').trim(),
145
+ stderr: (err.stderr ?? '').trim(),
146
+ code: err.code ?? 1,
147
+ };
148
+ }
149
+ }
package/build/cli.js ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * GoPeak CLI Entrypoint
4
+ *
5
+ * Routes subcommands or falls through to the MCP server.
6
+ *
7
+ * gopeak → Start MCP server (default, backward-compatible)
8
+ * gopeak setup → Install shell hooks
9
+ * gopeak check → Check for updates
10
+ * gopeak star → Star on GitHub
11
+ * gopeak uninstall → Remove shell hooks
12
+ * gopeak version → Print version
13
+ * gopeak help → Show help
14
+ */
15
+ import { getLocalVersion } from './cli/utils.js';
16
+ const args = process.argv.slice(2);
17
+ const command = args[0];
18
+ const CLI_COMMANDS = ['setup', 'check', 'star', 'notify', 'uninstall', 'version', 'help', '--version', '-v', '--help', '-h'];
19
+ async function main() {
20
+ // If no args or not a CLI command → start MCP server (original behavior)
21
+ if (!command || !CLI_COMMANDS.includes(command)) {
22
+ // Dynamic import to avoid loading MCP SDK for CLI-only commands
23
+ await import('./index.js');
24
+ return;
25
+ }
26
+ switch (command) {
27
+ case 'setup': {
28
+ const { setupShellHooks } = await import('./cli/setup.js');
29
+ await setupShellHooks();
30
+ break;
31
+ }
32
+ case 'check': {
33
+ const { checkForUpdates } = await import('./cli/check.js');
34
+ await checkForUpdates(args.slice(1));
35
+ break;
36
+ }
37
+ case 'star': {
38
+ const { starGoPeak } = await import('./cli/star.js');
39
+ await starGoPeak();
40
+ break;
41
+ }
42
+ case 'notify': {
43
+ const { showNotification } = await import('./cli/notify.js');
44
+ await showNotification();
45
+ break;
46
+ }
47
+ case 'uninstall': {
48
+ const { uninstallHooks } = await import('./cli/uninstall.js');
49
+ await uninstallHooks();
50
+ break;
51
+ }
52
+ case 'version':
53
+ case '--version':
54
+ case '-v': {
55
+ console.log(`gopeak v${getLocalVersion()}`);
56
+ break;
57
+ }
58
+ case 'help':
59
+ case '--help':
60
+ case '-h': {
61
+ printHelp();
62
+ break;
63
+ }
64
+ }
65
+ }
66
+ function printHelp() {
67
+ const version = getLocalVersion();
68
+ console.log(`
69
+ GoPeak v${version} — AI-Powered Godot Development via MCP
70
+
71
+ Usage:
72
+ gopeak Start MCP server (default)
73
+ gopeak setup Install shell hooks for update notifications
74
+ gopeak check Check for GoPeak updates
75
+ gopeak check --bg Background check (used by shell hooks)
76
+ gopeak check --quiet Print only if update available
77
+ gopeak star Star GoPeak on GitHub
78
+ gopeak uninstall Remove shell hooks
79
+ gopeak version Show current version
80
+ gopeak help Show this help
81
+
82
+ Shell hooks wrap these commands with update notifications:
83
+ claude, codex, gemini, opencode, omc, omx
84
+
85
+ More info: https://github.com/HaD0Yun/godot-mcp
86
+ `.trim());
87
+ }
88
+ main().catch((err) => {
89
+ console.error('gopeak:', err.message ?? err);
90
+ process.exit(1);
91
+ });
@@ -1,13 +1,54 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { EventEmitter } from 'node:events';
3
+ import { readFileSync } from 'node:fs';
3
4
  import http from 'node:http';
4
5
  import { WebSocket, WebSocketServer } from 'ws';
5
6
  const DEFAULT_PORT = 6505;
7
+ const DEFAULT_HOST = '127.0.0.1';
6
8
  const DEFAULT_TIMEOUT_MS = 30_000;
7
9
  const KEEPALIVE_INTERVAL_MS = 10_000;
8
10
  const SECOND_CONNECTION_CLOSE_CODE = 4000;
11
+ const BRIDGE_PORT_ENV_KEYS = ['GODOT_BRIDGE_PORT', 'MCP_BRIDGE_PORT', 'GOPEAK_BRIDGE_PORT'];
12
+ const BRIDGE_HOST_ENV_KEYS = ['GODOT_BRIDGE_HOST', 'MCP_BRIDGE_HOST', 'GOPEAK_BRIDGE_HOST'];
13
+ const BRIDGE_VERSION = (() => {
14
+ try {
15
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
16
+ return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
17
+ }
18
+ catch {
19
+ return '0.0.0';
20
+ }
21
+ })();
22
+ function resolveDefaultBridgePort() {
23
+ for (const key of BRIDGE_PORT_ENV_KEYS) {
24
+ const raw = process.env[key];
25
+ if (!raw || raw.trim().length === 0) {
26
+ continue;
27
+ }
28
+ const parsed = Number.parseInt(raw, 10);
29
+ if (Number.isInteger(parsed) && parsed >= 1 && parsed <= 65535) {
30
+ return parsed;
31
+ }
32
+ console.error(`[GodotBridge] Ignoring invalid ${key}="${raw}". Expected an integer between 1 and 65535.`);
33
+ }
34
+ return DEFAULT_PORT;
35
+ }
36
+ function resolveDefaultBridgeHost() {
37
+ for (const key of BRIDGE_HOST_ENV_KEYS) {
38
+ const raw = process.env[key];
39
+ if (!raw) {
40
+ continue;
41
+ }
42
+ const host = raw.trim();
43
+ if (host.length > 0) {
44
+ return host;
45
+ }
46
+ }
47
+ return DEFAULT_HOST;
48
+ }
9
49
  export class GodotBridge extends EventEmitter {
10
50
  port;
51
+ host;
11
52
  timeoutMs;
12
53
  httpServer = null;
13
54
  godotWss = null;
@@ -18,9 +59,10 @@ export class GodotBridge extends EventEmitter {
18
59
  pendingRequests = new Map();
19
60
  resourceQueues = new Map();
20
61
  visualizerHtml = this.getDefaultVisualizerHtml();
21
- constructor(port = DEFAULT_PORT, timeoutMs = DEFAULT_TIMEOUT_MS) {
62
+ constructor(port = DEFAULT_PORT, host = DEFAULT_HOST, timeoutMs = DEFAULT_TIMEOUT_MS) {
22
63
  super();
23
64
  this.port = port;
65
+ this.host = host;
24
66
  this.timeoutMs = timeoutMs;
25
67
  }
26
68
  start() {
@@ -49,7 +91,7 @@ export class GodotBridge extends EventEmitter {
49
91
  this.httpServer = server;
50
92
  this.godotWss = godotWss;
51
93
  this.vizWss = vizWss;
52
- this.log('info', `Unified HTTP+WS bridge listening on port ${this.port}`);
94
+ this.log('info', `Unified HTTP+WS bridge listening on ${this.host}:${this.port}`);
53
95
  resolve();
54
96
  });
55
97
  server.once('error', (error) => {
@@ -66,13 +108,14 @@ export class GodotBridge extends EventEmitter {
66
108
  vizWss.on('error', (error) => {
67
109
  this.log('error', `Visualizer WebSocket server error: ${error.message}`);
68
110
  });
69
- server.listen(this.port);
111
+ server.listen(this.port, this.host);
70
112
  });
71
113
  }
72
- stop() {
114
+ async stop() {
73
115
  this.stopKeepalive();
74
116
  this.rejectAllPending(new Error('GodotBridge stopped'));
75
117
  this.resourceQueues.clear();
118
+ const closeTasks = [];
76
119
  if (this.socket) {
77
120
  try {
78
121
  this.socket.close();
@@ -82,31 +125,35 @@ export class GodotBridge extends EventEmitter {
82
125
  this.socket = null;
83
126
  }
84
127
  if (this.godotWss) {
85
- for (const client of this.godotWss.clients) {
128
+ const godotWss = this.godotWss;
129
+ for (const client of godotWss.clients) {
86
130
  try {
87
131
  client.close();
88
132
  }
89
133
  catch {
90
134
  }
91
135
  }
92
- this.godotWss.close();
136
+ closeTasks.push(this.closeWebSocketServer(godotWss));
93
137
  this.godotWss = null;
94
138
  }
95
139
  if (this.vizWss) {
96
- for (const client of this.vizWss.clients) {
140
+ const vizWss = this.vizWss;
141
+ for (const client of vizWss.clients) {
97
142
  try {
98
143
  client.close();
99
144
  }
100
145
  catch {
101
146
  }
102
147
  }
103
- this.vizWss.close();
148
+ closeTasks.push(this.closeWebSocketServer(vizWss));
104
149
  this.vizWss = null;
105
150
  }
106
151
  if (this.httpServer) {
107
- this.httpServer.close();
152
+ const httpServer = this.httpServer;
153
+ closeTasks.push(this.closeHttpServer(httpServer));
108
154
  this.httpServer = null;
109
155
  }
156
+ await Promise.all(closeTasks);
110
157
  this.connectionInfo = null;
111
158
  this.visualizerHtml = this.getDefaultVisualizerHtml();
112
159
  this.log('info', 'WebSocket bridge stopped');
@@ -116,6 +163,7 @@ export class GodotBridge extends EventEmitter {
116
163
  }
117
164
  getStatus() {
118
165
  return {
166
+ host: this.host,
119
167
  port: this.port,
120
168
  connected: this.isConnected(),
121
169
  projectPath: this.connectionInfo?.projectPath,
@@ -174,7 +222,7 @@ export class GodotBridge extends EventEmitter {
174
222
  result: {
175
223
  protocolVersion: typeof parsed.params?.protocolVersion === 'string' ? parsed.params.protocolVersion : '2025-06-18',
176
224
  capabilities: {},
177
- serverInfo: { name: 'godot-mcp', version: '2.0.1' },
225
+ serverInfo: { name: 'gopeak', version: BRIDGE_VERSION },
178
226
  },
179
227
  }));
180
228
  return;
@@ -198,8 +246,8 @@ export class GodotBridge extends EventEmitter {
198
246
  if (pathname === '/health') {
199
247
  const payload = {
200
248
  status: 'ok',
201
- serverName: 'godot-mcp',
202
- version: '2.0.1',
249
+ serverName: 'gopeak',
250
+ version: BRIDGE_VERSION,
203
251
  bridge: this.getStatus(),
204
252
  uptime: process.uptime(),
205
253
  timestamp: new Date().toISOString(),
@@ -224,12 +272,50 @@ export class GodotBridge extends EventEmitter {
224
272
  }
225
273
  getRequestPathname(url) {
226
274
  try {
227
- return new URL(url ?? '/', `http://localhost:${this.port}`).pathname;
275
+ return new URL(url ?? '/', `http://${this.host}:${this.port}`).pathname;
228
276
  }
229
277
  catch {
230
278
  return '/';
231
279
  }
232
280
  }
281
+ closeWebSocketServer(server) {
282
+ return new Promise((resolve) => {
283
+ let settled = false;
284
+ const finish = () => {
285
+ if (!settled) {
286
+ settled = true;
287
+ resolve();
288
+ }
289
+ };
290
+ try {
291
+ server.close(() => {
292
+ finish();
293
+ });
294
+ }
295
+ catch {
296
+ finish();
297
+ }
298
+ });
299
+ }
300
+ closeHttpServer(server) {
301
+ return new Promise((resolve) => {
302
+ let settled = false;
303
+ const finish = () => {
304
+ if (!settled) {
305
+ settled = true;
306
+ resolve();
307
+ }
308
+ };
309
+ try {
310
+ server.close(() => {
311
+ finish();
312
+ });
313
+ }
314
+ catch {
315
+ finish();
316
+ }
317
+ });
318
+ }
233
319
  handleConnection(nextSocket) {
234
320
  if (this.socket && this.socket.readyState !== WebSocket.CLOSED) {
235
321
  this.log('warn', 'Rejecting second Godot connection');
@@ -461,10 +547,10 @@ export class GodotBridge extends EventEmitter {
461
547
  let defaultBridge = null;
462
548
  export function getDefaultBridge() {
463
549
  if (!defaultBridge) {
464
- defaultBridge = new GodotBridge();
550
+ defaultBridge = new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost());
465
551
  }
466
552
  return defaultBridge;
467
553
  }
468
- export function createBridge(port, timeoutMs) {
469
- return new GodotBridge(port, timeoutMs);
554
+ export function createBridge(port, timeoutMs, host) {
555
+ return new GodotBridge(port, host, timeoutMs);
470
556
  }