@ebowwa/terminal 0.3.7 → 0.3.8

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.
Binary file
package/dist/index.d.ts CHANGED
@@ -19,17 +19,4 @@ export { generateLocalSessionName, isLocalTmuxInstalled, listLocalSessions, hasL
19
19
  export { RESOURCE_COMMANDS } from "./resources.js";
20
20
  export type { ResourceCommand } from "./resources.js";
21
21
  export { detectNetworkError, formatNetworkError, type NetworkErrorDetails, } from "./network-error-detector.js";
22
-
23
- // SSH Config management
24
- export {
25
- addSSHConfigEntry,
26
- removeSSHConfigEntry,
27
- updateSSHConfigHost,
28
- listSSHConfigEntries,
29
- validateSSHConnection,
30
- ensureCorrectSSHKey,
31
- waitForSSHReady,
32
- syncNodesToSSHConfig,
33
- type SSHConfigEntry,
34
- type SyncResult,
35
- } from "./config.js";
22
+ export { addSSHConfigEntry, removeSSHConfigEntry, updateSSHConfigHost, listSSHConfigEntries, validateSSHConnection, ensureCorrectSSHKey, waitForSSHReady, syncNodesToSSHConfig, type SSHConfigEntry, type SyncResult, } from "./config.js";
package/dist/index.js CHANGED
@@ -15,7 +15,6 @@ var __toESM = (mod, isNodeMode, target) => {
15
15
  });
16
16
  return to;
17
17
  };
18
- var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
19
18
  var __export = (target, all) => {
20
19
  for (var name in all)
21
20
  __defProp(target, name, {
@@ -444,35 +443,34 @@ var init_error = __esm(() => {
444
443
  };
445
444
  });
446
445
 
447
- // node_modules/@ebowwa/codespaces-types/runtime/ssh.js
448
- var require_ssh = __commonJS((exports) => {
449
- Object.defineProperty(exports, "__esModule", { value: true });
450
- exports.FilePreviewOptionsSchema = exports.FilesListOptionsSchema = exports.SCPOptionsSchema = exports.SSHCommandSchema = exports.SSHOptionsSchema = undefined;
451
- var zod_1 = __require("zod");
452
- exports.SSHOptionsSchema = zod_1.z.object({
453
- host: zod_1.z.string().min(1, "Host is required"),
454
- user: zod_1.z.string().default("root"),
455
- timeout: zod_1.z.number().int().positive().default(5),
456
- port: zod_1.z.number().int().positive().max(65535).default(22),
457
- keyPath: zod_1.z.string().optional(),
458
- password: zod_1.z.string().optional()
446
+ // ../codespaces-types/runtime/ssh.js
447
+ import { z } from "zod";
448
+ var SSHOptionsSchema, SSHCommandSchema, SCPOptionsSchema, FilesListOptionsSchema, FilePreviewOptionsSchema;
449
+ var init_ssh = __esm(() => {
450
+ SSHOptionsSchema = z.object({
451
+ host: z.string().min(1, "Host is required"),
452
+ user: z.string().default("root"),
453
+ timeout: z.number().int().positive().default(5),
454
+ port: z.number().int().positive().max(65535).default(22),
455
+ keyPath: z.string().optional(),
456
+ password: z.string().optional()
459
457
  });
460
- exports.SSHCommandSchema = zod_1.z.string().min(1, "Command cannot be empty");
461
- exports.SCPOptionsSchema = exports.SSHOptionsSchema.extend({
462
- source: zod_1.z.string().min(1, "Source is required"),
463
- destination: zod_1.z.string().min(1, "Destination is required"),
464
- recursive: zod_1.z.boolean().default(false),
465
- preserve: zod_1.z.boolean().default(false)
458
+ SSHCommandSchema = z.string().min(1, "Command cannot be empty");
459
+ SCPOptionsSchema = SSHOptionsSchema.extend({
460
+ source: z.string().min(1, "Source is required"),
461
+ destination: z.string().min(1, "Destination is required"),
462
+ recursive: z.boolean().default(false),
463
+ preserve: z.boolean().default(false)
466
464
  });
467
- exports.FilesListOptionsSchema = zod_1.z.object({
468
- host: zod_1.z.string().min(1, "Host is required"),
469
- user: zod_1.z.string().default("root"),
470
- path: zod_1.z.string().default(".")
465
+ FilesListOptionsSchema = z.object({
466
+ host: z.string().min(1, "Host is required"),
467
+ user: z.string().default("root"),
468
+ path: z.string().default(".")
471
469
  });
472
- exports.FilePreviewOptionsSchema = zod_1.z.object({
473
- host: zod_1.z.string().min(1, "Host is required"),
474
- user: zod_1.z.string().default("root"),
475
- path: zod_1.z.string().min(1, "Path is required")
470
+ FilePreviewOptionsSchema = z.object({
471
+ host: z.string().min(1, "Host is required"),
472
+ user: z.string().default("root"),
473
+ path: z.string().min(1, "Path is required")
476
474
  });
477
475
  });
478
476
 
@@ -482,11 +480,11 @@ __export(exports_client, {
482
480
  execSSH: () => execSSH
483
481
  });
484
482
  async function execSSH(command, options) {
485
- const validatedCommand = import_ssh.SSHCommandSchema.safeParse(command);
483
+ const validatedCommand = SSHCommandSchema.safeParse(command);
486
484
  if (!validatedCommand.success) {
487
485
  throw new Error(`Invalid SSH command: ${validatedCommand.error.issues.map((i) => i.message).join(", ")}`);
488
486
  }
489
- const validatedOptions = import_ssh.SSHOptionsSchema.safeParse(options);
487
+ const validatedOptions = SSHOptionsSchema.safeParse(options);
490
488
  if (!validatedOptions.success) {
491
489
  throw new Error(`Invalid SSH options: ${validatedOptions.error.issues.map((i) => i.message).join(", ")}`);
492
490
  }
@@ -506,126 +504,16 @@ async function execSSH(command, options) {
506
504
  throw new SSHError(`SSH command failed: ${validatedCommand.data}`, error);
507
505
  }
508
506
  }
509
- var import_ssh;
510
507
  var init_client = __esm(() => {
511
508
  init_error();
509
+ init_ssh();
512
510
  init_pool();
513
- import_ssh = __toESM(require_ssh(), 1);
514
- });
515
-
516
- // node_modules/@ebowwa/codespaces-types/compile/terminal-websocket.js
517
- var require_terminal_websocket = __commonJS((exports) => {
518
- Object.defineProperty(exports, "__esModule", { value: true });
519
- exports.WebSocketCloseCode = undefined;
520
- exports.WebSocketCloseCode = {
521
- NORMAL_CLOSURE: 1000,
522
- ENDPOINT_GOING_AWAY: 1001,
523
- PROTOCOL_ERROR: 1002,
524
- UNSUPPORTED_DATA: 1003,
525
- NO_STATUS_RECEIVED: 1005,
526
- ABNORMAL_CLOSURE: 1006,
527
- INVALID_FRAME_PAYLOAD_DATA: 1007,
528
- POLICY_VIOLATION: 1008,
529
- MESSAGE_TOO_BIG: 1009,
530
- MISSING_MANDATORY_EXTENSION: 1010,
531
- INTERNAL_ERROR: 1011,
532
- SERVICE_RESTART: 1012,
533
- TRY_AGAIN_LATER: 1013,
534
- SESSION_NOT_FOUND: 4001,
535
- SESSION_ALREADY_CLOSED: 4002,
536
- INVALID_MESSAGE_FORMAT: 4003,
537
- AUTHENTICATION_FAILED: 4004,
538
- CONNECTION_TIMEOUT: 4005,
539
- SESSION_LIMIT_REACHED: 4006,
540
- INVALID_HOST: 4007,
541
- SSH_CONNECTION_FAILED: 4008,
542
- NETWORK_BLOCKED: 4009
543
- };
544
- });
545
-
546
- // node_modules/@ebowwa/codespaces-types/compile/index.js
547
- var require_compile = __commonJS((exports) => {
548
- var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
549
- if (k2 === undefined)
550
- k2 = k;
551
- var desc = Object.getOwnPropertyDescriptor(m, k);
552
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
553
- desc = { enumerable: true, get: function() {
554
- return m[k];
555
- } };
556
- }
557
- Object.defineProperty(o, k2, desc);
558
- } : function(o, m, k, k2) {
559
- if (k2 === undefined)
560
- k2 = k;
561
- o[k2] = m[k];
562
- });
563
- var __exportStar = exports && exports.__exportStar || function(m, exports2) {
564
- for (var p in m)
565
- if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p))
566
- __createBinding(exports2, m, p);
567
- };
568
- Object.defineProperty(exports, "__esModule", { value: true });
569
- exports.LoopStatus = exports.VolumeStatus = exports.ActionStatus = exports.EnvironmentStatus = undefined;
570
- exports.getEnvLocation = getEnvLocation;
571
- exports.getEnvRegionName = getEnvRegionName;
572
- exports.getEnvLocationLabel = getEnvLocationLabel;
573
- __exportStar(require_terminal_websocket(), exports);
574
- var EnvironmentStatus;
575
- (function(EnvironmentStatus2) {
576
- EnvironmentStatus2["Running"] = "running";
577
- EnvironmentStatus2["Stopped"] = "stopped";
578
- EnvironmentStatus2["Creating"] = "creating";
579
- EnvironmentStatus2["Deleting"] = "deleting";
580
- EnvironmentStatus2["Starting"] = "starting";
581
- EnvironmentStatus2["Stopping"] = "stopping";
582
- EnvironmentStatus2["Initializing"] = "initializing";
583
- })(EnvironmentStatus || (exports.EnvironmentStatus = EnvironmentStatus = {}));
584
- var ActionStatus;
585
- (function(ActionStatus2) {
586
- ActionStatus2["Running"] = "running";
587
- ActionStatus2["Success"] = "success";
588
- ActionStatus2["Error"] = "error";
589
- })(ActionStatus || (exports.ActionStatus = ActionStatus = {}));
590
- var VolumeStatus;
591
- (function(VolumeStatus2) {
592
- VolumeStatus2["Creating"] = "creating";
593
- VolumeStatus2["Available"] = "available";
594
- VolumeStatus2["Deleting"] = "deleting";
595
- })(VolumeStatus || (exports.VolumeStatus = VolumeStatus = {}));
596
- var LoopStatus;
597
- (function(LoopStatus2) {
598
- LoopStatus2["Running"] = "running";
599
- LoopStatus2["Stopped"] = "stopped";
600
- LoopStatus2["Error"] = "error";
601
- LoopStatus2["Completed"] = "completed";
602
- })(LoopStatus || (exports.LoopStatus = LoopStatus = {}));
603
- function getEnvLocation(env) {
604
- if (!env)
605
- return null;
606
- return env.location || null;
607
- }
608
- function getEnvRegionName(env) {
609
- var _a;
610
- if (!env)
611
- return "Unknown";
612
- return ((_a = env.location) === null || _a === undefined ? undefined : _a.name) || "Unknown";
613
- }
614
- function getEnvLocationLabel(env) {
615
- var _a, _b, _c;
616
- if (!env)
617
- return "Unknown";
618
- if (((_a = env.location) === null || _a === undefined ? undefined : _a.city) && ((_b = env.location) === null || _b === undefined ? undefined : _b.country)) {
619
- return "".concat(env.location.city, ", ").concat(env.location.country);
620
- }
621
- return ((_c = env.location) === null || _c === undefined ? undefined : _c.name) || "Unknown";
622
- }
623
511
  });
624
512
 
625
513
  // src/tmux.ts
626
514
  init_pool();
627
515
 
628
- // node_modules/@ebowwa/ssh/flags.js
516
+ // ../ssh/flags.js
629
517
  function sshConfig(key, value) {
630
518
  return { type: "config", key, value };
631
519
  }
@@ -1264,7 +1152,7 @@ class TmuxSessionManager {
1264
1152
  return { success: false, error: `Node ${nodeId} is not running` };
1265
1153
  }
1266
1154
  try {
1267
- const result = await createOrAttachTmuxSession(node.ip, node.user, node.keyPath, { sessionName });
1155
+ const result = await createOrAttachTmuxSession(node.ip, node.user, node.keyPath, {});
1268
1156
  return {
1269
1157
  success: true,
1270
1158
  sshArgs: result.sshArgs,
@@ -1768,10 +1656,10 @@ async function execViaTmuxParallel(commands, options, timeout = 10) {
1768
1656
  }
1769
1657
  // src/scp.ts
1770
1658
  init_error();
1659
+ init_ssh();
1771
1660
  init_pool();
1772
- var import_ssh2 = __toESM(require_ssh(), 1);
1773
1661
  async function scpUpload(options) {
1774
- const validated = import_ssh2.SCPOptionsSchema.safeParse(options);
1662
+ const validated = SCPOptionsSchema.safeParse(options);
1775
1663
  if (!validated.success) {
1776
1664
  throw new Error(`Invalid SCP options: ${validated.error.issues.map((i) => i.message).join(", ")}`);
1777
1665
  }
@@ -1798,7 +1686,7 @@ async function scpUpload(options) {
1798
1686
  }
1799
1687
  }
1800
1688
  async function scpDownload(options) {
1801
- const validated = import_ssh2.SCPOptionsSchema.safeParse(options);
1689
+ const validated = SCPOptionsSchema.safeParse(options);
1802
1690
  if (!validated.success) {
1803
1691
  throw new Error(`Invalid SCP options: ${validated.error.issues.map((i) => i.message).join(", ")}`);
1804
1692
  }
@@ -2449,7 +2337,63 @@ init_pool();
2449
2337
 
2450
2338
  // src/sessions.ts
2451
2339
  import path2 from "path";
2452
- var import_compile = __toESM(require_compile(), 1);
2340
+
2341
+ // ../codespaces-types/compile/index.js
2342
+ var WebSocketCloseCode = {
2343
+ NORMAL_CLOSURE: 1000,
2344
+ ENDPOINT_GOING_AWAY: 1001,
2345
+ PROTOCOL_ERROR: 1002,
2346
+ UNSUPPORTED_DATA: 1003,
2347
+ NO_STATUS_RECEIVED: 1005,
2348
+ ABNORMAL_CLOSURE: 1006,
2349
+ INVALID_FRAME_PAYLOAD_DATA: 1007,
2350
+ POLICY_VIOLATION: 1008,
2351
+ MESSAGE_TOO_BIG: 1009,
2352
+ MISSING_MANDATORY_EXTENSION: 1010,
2353
+ INTERNAL_ERROR: 1011,
2354
+ SERVICE_RESTART: 1012,
2355
+ TRY_AGAIN_LATER: 1013,
2356
+ SESSION_NOT_FOUND: 4001,
2357
+ SESSION_ALREADY_CLOSED: 4002,
2358
+ INVALID_MESSAGE_FORMAT: 4003,
2359
+ AUTHENTICATION_FAILED: 4004,
2360
+ CONNECTION_TIMEOUT: 4005,
2361
+ SESSION_LIMIT_REACHED: 4006,
2362
+ INVALID_HOST: 4007,
2363
+ SSH_CONNECTION_FAILED: 4008,
2364
+ NETWORK_BLOCKED: 4009
2365
+ };
2366
+ var EnvironmentStatus;
2367
+ ((EnvironmentStatus2) => {
2368
+ EnvironmentStatus2["Running"] = "running";
2369
+ EnvironmentStatus2["Stopped"] = "stopped";
2370
+ EnvironmentStatus2["Creating"] = "creating";
2371
+ EnvironmentStatus2["Deleting"] = "deleting";
2372
+ EnvironmentStatus2["Starting"] = "starting";
2373
+ EnvironmentStatus2["Stopping"] = "stopping";
2374
+ EnvironmentStatus2["Initializing"] = "initializing";
2375
+ })(EnvironmentStatus ||= {});
2376
+ var ActionStatus;
2377
+ ((ActionStatus2) => {
2378
+ ActionStatus2["Running"] = "running";
2379
+ ActionStatus2["Success"] = "success";
2380
+ ActionStatus2["Error"] = "error";
2381
+ })(ActionStatus ||= {});
2382
+ var VolumeStatus;
2383
+ ((VolumeStatus2) => {
2384
+ VolumeStatus2["Creating"] = "creating";
2385
+ VolumeStatus2["Available"] = "available";
2386
+ VolumeStatus2["Deleting"] = "deleting";
2387
+ })(VolumeStatus ||= {});
2388
+ var LoopStatus;
2389
+ ((LoopStatus2) => {
2390
+ LoopStatus2["Running"] = "running";
2391
+ LoopStatus2["Stopped"] = "stopped";
2392
+ LoopStatus2["Error"] = "error";
2393
+ LoopStatus2["Completed"] = "completed";
2394
+ })(LoopStatus ||= {});
2395
+
2396
+ // src/sessions.ts
2453
2397
  var metadataModule = null;
2454
2398
  function loadMetadata() {
2455
2399
  if (metadataModule === null) {
@@ -2502,7 +2446,7 @@ async function closeSession(sessionId) {
2502
2446
  } catch {}
2503
2447
  }
2504
2448
  try {
2505
- session.ws?.close(import_compile.WebSocketCloseCode.NORMAL_CLOSURE, "Session closed");
2449
+ session.ws?.close(WebSocketCloseCode.NORMAL_CLOSURE, "Session closed");
2506
2450
  } catch {}
2507
2451
  if (session.bootstrapLogStreamer) {
2508
2452
  try {
@@ -2822,14 +2766,14 @@ async function getOrCreateSession(host, user = "root", sessionId = null, keyPath
2822
2766
  console.log(`[Terminal] Process for ${newSessionId} exited with code ${exitCode}`);
2823
2767
  session.closed = true;
2824
2768
  try {
2825
- session.ws?.close(import_compile.WebSocketCloseCode.NORMAL_CLOSURE, `SSH process exited (code: ${exitCode})`);
2769
+ session.ws?.close(WebSocketCloseCode.NORMAL_CLOSURE, `SSH process exited (code: ${exitCode})`);
2826
2770
  } catch {}
2827
2771
  terminalSessions.delete(newSessionId);
2828
2772
  }).catch((err) => {
2829
2773
  console.log(`[Terminal] Process exit error for ${newSessionId}:`, err);
2830
2774
  session.closed = true;
2831
2775
  try {
2832
- session.ws?.close(import_compile.WebSocketCloseCode.INTERNAL_ERROR, "SSH process error");
2776
+ session.ws?.close(WebSocketCloseCode.INTERNAL_ERROR, "SSH process error");
2833
2777
  } catch {}
2834
2778
  });
2835
2779
  return session;
@@ -3379,14 +3323,392 @@ var RESOURCE_COMMANDS = {
3379
3323
  connections: `cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | wc -l`,
3380
3324
  ports: `cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | grep -v 'local_address' | awk '{print $2}' | cut -d: -f2 | sort -u | tr '\\n' ';' | sed 's/;$//'`
3381
3325
  };
3326
+ // src/network-error-detector.ts
3327
+ function detectNetworkError(error, host, serverStatus) {
3328
+ const errorMessage = typeof error === "string" ? error : error.message;
3329
+ const isConnectionRefused = errorMessage.includes("ECONNREFUSED") || errorMessage.includes("Connection refused") || errorMessage.includes("connect ECONNREFUSED");
3330
+ const isHetznerOrDatacenterIP = host.startsWith("195.") || host.startsWith("148.") || host.startsWith("116.") || host.startsWith("138.") || host.includes("hetzner") || host.includes("fsn1") || host.includes("nbg1") || host.includes("hel1");
3331
+ const serverIsRunning = serverStatus === "running";
3332
+ if (isConnectionRefused && serverIsRunning && isHetznerOrDatacenterIP) {
3333
+ return {
3334
+ type: "NETWORK_BLOCKED",
3335
+ message: "Network appears to be blocking this server",
3336
+ troubleshooting: [
3337
+ "\u2713 Server is running and responding to API",
3338
+ "\u2717 Your network is refusing connections to this IP",
3339
+ "",
3340
+ "\u26A0\uFE0F COMMON CAUSES:",
3341
+ "\u2022 Corporate/public WiFi blocks datacenter IPs",
3342
+ "\u2022 ISP firewall filtering VPS providers",
3343
+ "\u2022 Router content filtering",
3344
+ "",
3345
+ "\uD83D\uDD27 SOLUTIONS:",
3346
+ "1. Try a different network (hotspot, home WiFi, VPN)",
3347
+ "2. Check router firewall settings",
3348
+ "3. Contact ISP about blocking Hetzner IPs",
3349
+ "",
3350
+ "\uD83D\uDCA1 TEST: Works from other networks? = Network block confirmed"
3351
+ ],
3352
+ isLikelyNetworkBlock: true
3353
+ };
3354
+ }
3355
+ if (isConnectionRefused && !serverIsRunning) {
3356
+ return {
3357
+ type: "SERVER_NOT_READY",
3358
+ message: "Server is not ready yet",
3359
+ troubleshooting: [
3360
+ "Server is still starting up",
3361
+ "SSH daemon hasn't started yet",
3362
+ "",
3363
+ "\u23F3 Wait 1-2 minutes and try again"
3364
+ ],
3365
+ isLikelyNetworkBlock: false
3366
+ };
3367
+ }
3368
+ if (errorMessage.includes("All configured authentication") || errorMessage.includes("Authentication failed")) {
3369
+ return {
3370
+ type: "AUTH_FAILED",
3371
+ message: "SSH authentication failed",
3372
+ troubleshooting: [
3373
+ "SSH key issue or wrong credentials",
3374
+ "Check that the SSH key is configured correctly"
3375
+ ],
3376
+ isLikelyNetworkBlock: false
3377
+ };
3378
+ }
3379
+ return {
3380
+ type: "UNKNOWN",
3381
+ message: errorMessage,
3382
+ troubleshooting: [
3383
+ "Check if server is running",
3384
+ "Verify network connectivity",
3385
+ "Check SSH key configuration"
3386
+ ],
3387
+ isLikelyNetworkBlock: false
3388
+ };
3389
+ }
3390
+ function formatNetworkError(diagnosis) {
3391
+ if (diagnosis.isLikelyNetworkBlock) {
3392
+ return `\u26A0\uFE0F ${diagnosis.message}
3393
+
3394
+ ${diagnosis.troubleshooting.join(`
3395
+ `)}`;
3396
+ }
3397
+ return diagnosis.message;
3398
+ }
3399
+ // src/config.ts
3400
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, realpathSync } from "fs";
3401
+ import { join, dirname, isAbsolute, resolve } from "path";
3402
+ import { exec as exec2 } from "child_process";
3403
+ import { promisify as promisify3 } from "util";
3404
+ var execAsync3 = promisify3(exec2);
3405
+ var SSH_CONFIG_PATH = join(process.env.HOME || "~", ".ssh", "config");
3406
+ var BLOCK_START = "# >>> hetzner-codespaces managed";
3407
+ var BLOCK_END = "# <<< hetzner-codespaces managed";
3408
+ function resolveKeyPath2(keyPath) {
3409
+ if (isAbsolute(keyPath)) {
3410
+ return keyPath;
3411
+ }
3412
+ const resolved = resolve(process.cwd(), keyPath);
3413
+ if (existsSync(resolved)) {
3414
+ try {
3415
+ return realpathSync(resolved);
3416
+ } catch {
3417
+ return resolved;
3418
+ }
3419
+ }
3420
+ return keyPath;
3421
+ }
3422
+ function readSSHConfig() {
3423
+ if (!existsSync(SSH_CONFIG_PATH)) {
3424
+ return "";
3425
+ }
3426
+ return readFileSync(SSH_CONFIG_PATH, "utf-8");
3427
+ }
3428
+ function writeSSHConfig(content) {
3429
+ const sshDir = dirname(SSH_CONFIG_PATH);
3430
+ if (!existsSync(sshDir)) {
3431
+ mkdirSync(sshDir, { mode: 448, recursive: true });
3432
+ }
3433
+ writeFileSync(SSH_CONFIG_PATH, content, { mode: 384 });
3434
+ }
3435
+ function extractManagedBlock(config) {
3436
+ const startIdx = config.indexOf(BLOCK_START);
3437
+ const endIdx = config.indexOf(BLOCK_END);
3438
+ if (startIdx === -1 || endIdx === -1) {
3439
+ return { before: config, managed: "", after: "" };
3440
+ }
3441
+ return {
3442
+ before: config.substring(0, startIdx),
3443
+ managed: config.substring(startIdx, endIdx + BLOCK_END.length + 1),
3444
+ after: config.substring(endIdx + BLOCK_END.length + 1)
3445
+ };
3446
+ }
3447
+ function parseManagedEntries(managed) {
3448
+ const entries = new Map;
3449
+ const hostRegex = /# node-id: (\S+)\nHost ([^\n]+)\n([\s\S]*?)(?=# node-id:|$)/g;
3450
+ let match;
3451
+ while ((match = hostRegex.exec(managed)) !== null) {
3452
+ const id = match[1];
3453
+ const hosts = match[2].trim();
3454
+ const body = match[3];
3455
+ const hostMatch = body.match(/HostName\s+(\S+)/);
3456
+ const userMatch = body.match(/User\s+(\S+)/);
3457
+ const keyMatch = body.match(/IdentityFile\s+(\S+)/);
3458
+ const portMatch = body.match(/Port\s+(\d+)/);
3459
+ if (hostMatch && keyMatch) {
3460
+ entries.set(id, {
3461
+ id,
3462
+ name: hosts.split(/\s+/)[1] || hosts,
3463
+ host: hostMatch[1],
3464
+ user: userMatch?.[1] || "root",
3465
+ keyPath: keyMatch[1],
3466
+ port: portMatch ? parseInt(portMatch[1]) : 22
3467
+ });
3468
+ }
3469
+ }
3470
+ return entries;
3471
+ }
3472
+ function generateEntryBlock(entry) {
3473
+ const sanitizedName = entry.name.replace(/[^a-zA-Z0-9_-]/g, "-");
3474
+ const aliases = `node-${entry.id} ${sanitizedName}`;
3475
+ const absoluteKeyPath = resolveKeyPath2(entry.keyPath);
3476
+ return `# node-id: ${entry.id}
3477
+ Host ${aliases}
3478
+ HostName ${entry.host}
3479
+ User ${entry.user || "root"}
3480
+ IdentityFile "${absoluteKeyPath}"
3481
+ Port ${entry.port || 22}
3482
+ StrictHostKeyChecking no
3483
+ UserKnownHostsFile /dev/null
3484
+ LogLevel ERROR
3485
+ IdentitiesOnly yes
3486
+
3487
+ `;
3488
+ }
3489
+ function buildManagedBlock(entries) {
3490
+ if (entries.size === 0) {
3491
+ return "";
3492
+ }
3493
+ let block = `${BLOCK_START}
3494
+ `;
3495
+ block += `# Auto-generated SSH aliases for Hetzner nodes
3496
+ `;
3497
+ block += `# Do not edit manually - changes will be overwritten
3498
+
3499
+ `;
3500
+ for (const entry of entries.values()) {
3501
+ block += generateEntryBlock(entry);
3502
+ }
3503
+ block += `${BLOCK_END}
3504
+ `;
3505
+ return block;
3506
+ }
3507
+ function addSSHConfigEntry(entry) {
3508
+ const config = readSSHConfig();
3509
+ const { before, managed, after } = extractManagedBlock(config);
3510
+ const entries = parseManagedEntries(managed);
3511
+ entries.set(entry.id, entry);
3512
+ const newManaged = buildManagedBlock(entries);
3513
+ const newConfig = before.trimEnd() + `
3514
+
3515
+ ` + newManaged + after.trimStart();
3516
+ writeSSHConfig(newConfig);
3517
+ console.log(`[SSH Config] Added alias: ssh node-${entry.id} / ssh ${entry.name.replace(/[^a-zA-Z0-9_-]/g, "-")}`);
3518
+ }
3519
+ function removeSSHConfigEntry(id) {
3520
+ const config = readSSHConfig();
3521
+ const { before, managed, after } = extractManagedBlock(config);
3522
+ const entries = parseManagedEntries(managed);
3523
+ if (!entries.has(id)) {
3524
+ return;
3525
+ }
3526
+ entries.delete(id);
3527
+ const newManaged = buildManagedBlock(entries);
3528
+ const newConfig = before.trimEnd() + (newManaged ? `
3529
+
3530
+ ` + newManaged : "") + after.trimStart();
3531
+ writeSSHConfig(newConfig);
3532
+ console.log(`[SSH Config] Removed alias for node-${id}`);
3533
+ }
3534
+ function updateSSHConfigHost(id, newHost) {
3535
+ const config = readSSHConfig();
3536
+ const { before, managed, after } = extractManagedBlock(config);
3537
+ const entries = parseManagedEntries(managed);
3538
+ const entry = entries.get(id);
3539
+ if (!entry) {
3540
+ console.warn(`[SSH Config] No entry found for node-${id}`);
3541
+ return;
3542
+ }
3543
+ entry.host = newHost;
3544
+ entries.set(id, entry);
3545
+ const newManaged = buildManagedBlock(entries);
3546
+ const newConfig = before.trimEnd() + `
3547
+
3548
+ ` + newManaged + after.trimStart();
3549
+ writeSSHConfig(newConfig);
3550
+ console.log(`[SSH Config] Updated node-${id} host to ${newHost}`);
3551
+ }
3552
+ function listSSHConfigEntries() {
3553
+ const config = readSSHConfig();
3554
+ const { managed } = extractManagedBlock(config);
3555
+ const entries = parseManagedEntries(managed);
3556
+ return Array.from(entries.values());
3557
+ }
3558
+ async function validateSSHConnection(host, keyPath, user = "root", timeoutSeconds = 10) {
3559
+ try {
3560
+ const cmd = [
3561
+ "ssh",
3562
+ "-o",
3563
+ "StrictHostKeyChecking=no",
3564
+ "-o",
3565
+ "UserKnownHostsFile=/dev/null",
3566
+ "-o",
3567
+ `ConnectTimeout=${timeoutSeconds}`,
3568
+ "-o",
3569
+ "IdentitiesOnly=yes",
3570
+ "-o",
3571
+ "BatchMode=yes",
3572
+ "-i",
3573
+ keyPath,
3574
+ `${user}@${host}`,
3575
+ "echo CONNECTION_OK"
3576
+ ].join(" ");
3577
+ const { stdout } = await execAsync3(cmd, { timeout: (timeoutSeconds + 5) * 1000 });
3578
+ if (stdout.includes("CONNECTION_OK")) {
3579
+ return { success: true };
3580
+ }
3581
+ return {
3582
+ success: false,
3583
+ error: "Connection established but test command failed",
3584
+ diagnostics: stdout
3585
+ };
3586
+ } catch (error) {
3587
+ let diagnostics = "";
3588
+ if (!existsSync(keyPath)) {
3589
+ diagnostics += `Key file missing: ${keyPath}
3590
+ `;
3591
+ }
3592
+ try {
3593
+ const { stdout: agentKeys } = await execAsync3("ssh-add -l 2>&1");
3594
+ diagnostics += `ssh-agent keys:
3595
+ ${agentKeys}
3596
+ `;
3597
+ } catch {
3598
+ diagnostics += `ssh-agent: no keys loaded
3599
+ `;
3600
+ }
3601
+ return {
3602
+ success: false,
3603
+ error: error.message || String(error),
3604
+ diagnostics
3605
+ };
3606
+ }
3607
+ }
3608
+ async function ensureCorrectSSHKey(keyPath) {
3609
+ try {
3610
+ const { stdout: ourFingerprint } = await execAsync3(`ssh-keygen -lf "${keyPath}.pub"`);
3611
+ const ourFp = ourFingerprint.split(/\s+/)[1];
3612
+ const { stdout: agentList } = await execAsync3("ssh-add -l 2>&1").catch(() => ({ stdout: "" }));
3613
+ if (!agentList.includes(ourFp)) {
3614
+ await execAsync3("ssh-add -D 2>/dev/null").catch(() => {});
3615
+ await execAsync3(`ssh-add "${keyPath}"`);
3616
+ console.log(`[SSH] Added key to ssh-agent: ${keyPath}`);
3617
+ }
3618
+ } catch (error) {
3619
+ console.warn(`[SSH] Could not configure ssh-agent: ${error}`);
3620
+ }
3621
+ }
3622
+ async function waitForSSHReady(host, keyPath, options = {}) {
3623
+ const {
3624
+ user = "root",
3625
+ maxAttempts = 30,
3626
+ intervalMs = 5000,
3627
+ onAttempt
3628
+ } = options;
3629
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
3630
+ onAttempt?.(attempt, maxAttempts);
3631
+ const result = await validateSSHConnection(host, keyPath, user, 5);
3632
+ if (result.success) {
3633
+ return { success: true, attempts: attempt };
3634
+ }
3635
+ if (result.error?.includes("Permission denied")) {
3636
+ return {
3637
+ success: false,
3638
+ attempts: attempt,
3639
+ error: `SSH key rejected: ${result.error}
3640
+ ${result.diagnostics || ""}`
3641
+ };
3642
+ }
3643
+ if (attempt < maxAttempts) {
3644
+ await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
3645
+ }
3646
+ }
3647
+ return {
3648
+ success: false,
3649
+ attempts: maxAttempts,
3650
+ error: `SSH not ready after ${maxAttempts} attempts (${maxAttempts * intervalMs / 1000}s)`
3651
+ };
3652
+ }
3653
+ async function syncNodesToSSHConfig(nodes, options = {}) {
3654
+ const { validateSSH = false, onProgress } = options;
3655
+ const results = [];
3656
+ const existingEntries = listSSHConfigEntries();
3657
+ const existingIds = new Set(existingEntries.map((e) => e.id));
3658
+ for (const node of nodes) {
3659
+ const result = {
3660
+ id: node.id,
3661
+ name: node.name,
3662
+ ip: node.ip,
3663
+ status: "added"
3664
+ };
3665
+ try {
3666
+ if (existingIds.has(node.id)) {
3667
+ const existing = existingEntries.find((e) => e.id === node.id);
3668
+ if (existing?.host === node.ip) {
3669
+ result.status = "skipped";
3670
+ } else {
3671
+ updateSSHConfigHost(node.id, node.ip);
3672
+ result.status = "updated";
3673
+ }
3674
+ } else {
3675
+ addSSHConfigEntry({
3676
+ id: node.id,
3677
+ name: node.name,
3678
+ host: node.ip,
3679
+ user: "root",
3680
+ keyPath: node.keyPath
3681
+ });
3682
+ result.status = "added";
3683
+ }
3684
+ if (validateSSH && result.status !== "skipped") {
3685
+ const sshResult = await validateSSHConnection(node.ip, node.keyPath);
3686
+ result.sshReady = sshResult.success;
3687
+ if (!sshResult.success) {
3688
+ result.error = sshResult.error;
3689
+ }
3690
+ }
3691
+ } catch (error) {
3692
+ result.status = "error";
3693
+ result.error = String(error);
3694
+ }
3695
+ results.push(result);
3696
+ onProgress?.(result);
3697
+ }
3698
+ return results;
3699
+ }
3382
3700
  export {
3383
3701
  writeToSession,
3384
3702
  writeToPTY,
3385
3703
  waitForTextInPane,
3704
+ waitForSSHReady,
3386
3705
  validateSSHKeyMatch,
3387
3706
  validateSSHKeyForServer,
3707
+ validateSSHConnection,
3708
+ updateSSHConfigHost,
3388
3709
  testSSHKeyConnection,
3389
3710
  testSSHConnection,
3711
+ syncNodesToSSHConfig,
3390
3712
  switchWindow,
3391
3713
  switchPane,
3392
3714
  switchLocalWindow,
@@ -3403,12 +3725,14 @@ export {
3403
3725
  resetTmuxManager,
3404
3726
  renameWindow,
3405
3727
  renameLocalWindow,
3728
+ removeSSHConfigEntry,
3406
3729
  readFromPTY,
3407
3730
  previewFile,
3408
3731
  normalizeFingerprint,
3409
3732
  listWindowPanes,
3410
3733
  listTmuxSessions,
3411
3734
  listSessionWindows,
3735
+ listSSHConfigEntries,
3412
3736
  listLocalWindowPanes,
3413
3737
  listLocalSessions,
3414
3738
  listLocalSessionWindows,
@@ -3447,11 +3771,14 @@ export {
3447
3771
  getActivePTYSessions,
3448
3772
  generateSessionName,
3449
3773
  generateLocalSessionName,
3774
+ formatNetworkError,
3450
3775
  execViaTmuxParallel,
3451
3776
  execViaTmux,
3452
3777
  execSSHParallel,
3453
3778
  execSSH,
3454
3779
  ensureTmux,
3780
+ ensureCorrectSSHKey,
3781
+ detectNetworkError,
3455
3782
  detachWebSocket,
3456
3783
  createPTYSession,
3457
3784
  createOrAttachTmuxSession,
@@ -3466,6 +3793,7 @@ export {
3466
3793
  capturePane,
3467
3794
  captureLocalPane,
3468
3795
  attachWebSocket,
3796
+ addSSHConfigEntry,
3469
3797
  TmuxSessionManager,
3470
3798
  SSHKeyMismatchError,
3471
3799
  SSHError,
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Activity tracking for terminal operations
3
+ */
4
+ export interface Activity {
5
+ type: string;
6
+ message: string;
7
+ nodeId?: string;
8
+ sessionName?: string;
9
+ timestamp?: Date;
10
+ }
11
+ export declare function addActivity(activity: Activity): void;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Hetzner client stub for type checking
3
+ */
4
+ export interface HetznerClient {
5
+ }
6
+ export declare function createHetznerClient(): HetznerClient;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Hetzner types stub for type checking
3
+ */
4
+ export interface HetznerServer {
5
+ id: number;
6
+ name: string;
7
+ status: string;
8
+ }
9
+ export interface HetznerSSHKey {
10
+ id: number;
11
+ name: string;
12
+ fingerprint: string;
13
+ }
package/dist/manager.d.ts CHANGED
@@ -14,7 +14,27 @@
14
14
  * ENVIRONMENT VARIABLES:
15
15
  * - HETZNER_SSH_KEYS_DIR: Override default SSH keys directory
16
16
  */
17
- import type { HetznerClient } from "../lib/hetzner/client";
17
+ interface HetznerSSHKeysAPI {
18
+ getAll(): Promise<{
19
+ ssh_keys: HetznerSSHKey[];
20
+ }>;
21
+ list(): Promise<{
22
+ ssh_keys: HetznerSSHKey[];
23
+ }>;
24
+ create(params: {
25
+ name: string;
26
+ public_key: string;
27
+ }): Promise<HetznerSSHKey>;
28
+ findByName(name: string): Promise<HetznerSSHKey | undefined>;
29
+ }
30
+ interface HetznerClient {
31
+ ssh_keys: HetznerSSHKeysAPI;
32
+ }
33
+ interface HetznerSSHKey {
34
+ id: number;
35
+ name: string;
36
+ fingerprint: string;
37
+ }
18
38
  declare module "bun" {
19
39
  interface Env {
20
40
  HETZNER_SSH_KEYS_DIR?: string;
@@ -100,3 +120,4 @@ export declare class SSHKeyManager {
100
120
  * @returns SSH key information
101
121
  */
102
122
  export declare function ensureSSHKey(hetznerClient: HetznerClient, config?: SSHKeyManagerConfig): Promise<SSHKeyInfo>;
123
+ export {};
package/dist/mcp/index.js CHANGED
@@ -16,7 +16,6 @@ var __toESM = (mod, isNodeMode, target) => {
16
16
  });
17
17
  return to;
18
18
  };
19
- var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
20
19
  var __export = (target, all) => {
21
20
  for (var name in all)
22
21
  __defProp(target, name, {
@@ -445,35 +444,34 @@ var init_error = __esm(() => {
445
444
  };
446
445
  });
447
446
 
448
- // node_modules/@ebowwa/codespaces-types/runtime/ssh.js
449
- var require_ssh = __commonJS((exports) => {
450
- Object.defineProperty(exports, "__esModule", { value: true });
451
- exports.FilePreviewOptionsSchema = exports.FilesListOptionsSchema = exports.SCPOptionsSchema = exports.SSHCommandSchema = exports.SSHOptionsSchema = undefined;
452
- var zod_1 = __require("zod");
453
- exports.SSHOptionsSchema = zod_1.z.object({
454
- host: zod_1.z.string().min(1, "Host is required"),
455
- user: zod_1.z.string().default("root"),
456
- timeout: zod_1.z.number().int().positive().default(5),
457
- port: zod_1.z.number().int().positive().max(65535).default(22),
458
- keyPath: zod_1.z.string().optional(),
459
- password: zod_1.z.string().optional()
447
+ // ../codespaces-types/runtime/ssh.js
448
+ import { z } from "zod";
449
+ var SSHOptionsSchema, SSHCommandSchema, SCPOptionsSchema, FilesListOptionsSchema, FilePreviewOptionsSchema;
450
+ var init_ssh = __esm(() => {
451
+ SSHOptionsSchema = z.object({
452
+ host: z.string().min(1, "Host is required"),
453
+ user: z.string().default("root"),
454
+ timeout: z.number().int().positive().default(5),
455
+ port: z.number().int().positive().max(65535).default(22),
456
+ keyPath: z.string().optional(),
457
+ password: z.string().optional()
460
458
  });
461
- exports.SSHCommandSchema = zod_1.z.string().min(1, "Command cannot be empty");
462
- exports.SCPOptionsSchema = exports.SSHOptionsSchema.extend({
463
- source: zod_1.z.string().min(1, "Source is required"),
464
- destination: zod_1.z.string().min(1, "Destination is required"),
465
- recursive: zod_1.z.boolean().default(false),
466
- preserve: zod_1.z.boolean().default(false)
459
+ SSHCommandSchema = z.string().min(1, "Command cannot be empty");
460
+ SCPOptionsSchema = SSHOptionsSchema.extend({
461
+ source: z.string().min(1, "Source is required"),
462
+ destination: z.string().min(1, "Destination is required"),
463
+ recursive: z.boolean().default(false),
464
+ preserve: z.boolean().default(false)
467
465
  });
468
- exports.FilesListOptionsSchema = zod_1.z.object({
469
- host: zod_1.z.string().min(1, "Host is required"),
470
- user: zod_1.z.string().default("root"),
471
- path: zod_1.z.string().default(".")
466
+ FilesListOptionsSchema = z.object({
467
+ host: z.string().min(1, "Host is required"),
468
+ user: z.string().default("root"),
469
+ path: z.string().default(".")
472
470
  });
473
- exports.FilePreviewOptionsSchema = zod_1.z.object({
474
- host: zod_1.z.string().min(1, "Host is required"),
475
- user: zod_1.z.string().default("root"),
476
- path: zod_1.z.string().min(1, "Path is required")
471
+ FilePreviewOptionsSchema = z.object({
472
+ host: z.string().min(1, "Host is required"),
473
+ user: z.string().default("root"),
474
+ path: z.string().min(1, "Path is required")
477
475
  });
478
476
  });
479
477
 
@@ -483,11 +481,11 @@ __export(exports_client, {
483
481
  execSSH: () => execSSH
484
482
  });
485
483
  async function execSSH(command, options) {
486
- const validatedCommand = import_ssh.SSHCommandSchema.safeParse(command);
484
+ const validatedCommand = SSHCommandSchema.safeParse(command);
487
485
  if (!validatedCommand.success) {
488
486
  throw new Error(`Invalid SSH command: ${validatedCommand.error.issues.map((i) => i.message).join(", ")}`);
489
487
  }
490
- const validatedOptions = import_ssh.SSHOptionsSchema.safeParse(options);
488
+ const validatedOptions = SSHOptionsSchema.safeParse(options);
491
489
  if (!validatedOptions.success) {
492
490
  throw new Error(`Invalid SSH options: ${validatedOptions.error.issues.map((i) => i.message).join(", ")}`);
493
491
  }
@@ -507,126 +505,16 @@ async function execSSH(command, options) {
507
505
  throw new SSHError(`SSH command failed: ${validatedCommand.data}`, error);
508
506
  }
509
507
  }
510
- var import_ssh;
511
508
  var init_client = __esm(() => {
512
509
  init_error();
510
+ init_ssh();
513
511
  init_pool();
514
- import_ssh = __toESM(require_ssh(), 1);
515
- });
516
-
517
- // node_modules/@ebowwa/codespaces-types/compile/terminal-websocket.js
518
- var require_terminal_websocket = __commonJS((exports) => {
519
- Object.defineProperty(exports, "__esModule", { value: true });
520
- exports.WebSocketCloseCode = undefined;
521
- exports.WebSocketCloseCode = {
522
- NORMAL_CLOSURE: 1000,
523
- ENDPOINT_GOING_AWAY: 1001,
524
- PROTOCOL_ERROR: 1002,
525
- UNSUPPORTED_DATA: 1003,
526
- NO_STATUS_RECEIVED: 1005,
527
- ABNORMAL_CLOSURE: 1006,
528
- INVALID_FRAME_PAYLOAD_DATA: 1007,
529
- POLICY_VIOLATION: 1008,
530
- MESSAGE_TOO_BIG: 1009,
531
- MISSING_MANDATORY_EXTENSION: 1010,
532
- INTERNAL_ERROR: 1011,
533
- SERVICE_RESTART: 1012,
534
- TRY_AGAIN_LATER: 1013,
535
- SESSION_NOT_FOUND: 4001,
536
- SESSION_ALREADY_CLOSED: 4002,
537
- INVALID_MESSAGE_FORMAT: 4003,
538
- AUTHENTICATION_FAILED: 4004,
539
- CONNECTION_TIMEOUT: 4005,
540
- SESSION_LIMIT_REACHED: 4006,
541
- INVALID_HOST: 4007,
542
- SSH_CONNECTION_FAILED: 4008,
543
- NETWORK_BLOCKED: 4009
544
- };
545
- });
546
-
547
- // node_modules/@ebowwa/codespaces-types/compile/index.js
548
- var require_compile = __commonJS((exports) => {
549
- var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
550
- if (k2 === undefined)
551
- k2 = k;
552
- var desc = Object.getOwnPropertyDescriptor(m, k);
553
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
554
- desc = { enumerable: true, get: function() {
555
- return m[k];
556
- } };
557
- }
558
- Object.defineProperty(o, k2, desc);
559
- } : function(o, m, k, k2) {
560
- if (k2 === undefined)
561
- k2 = k;
562
- o[k2] = m[k];
563
- });
564
- var __exportStar = exports && exports.__exportStar || function(m, exports2) {
565
- for (var p in m)
566
- if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p))
567
- __createBinding(exports2, m, p);
568
- };
569
- Object.defineProperty(exports, "__esModule", { value: true });
570
- exports.LoopStatus = exports.VolumeStatus = exports.ActionStatus = exports.EnvironmentStatus = undefined;
571
- exports.getEnvLocation = getEnvLocation;
572
- exports.getEnvRegionName = getEnvRegionName;
573
- exports.getEnvLocationLabel = getEnvLocationLabel;
574
- __exportStar(require_terminal_websocket(), exports);
575
- var EnvironmentStatus;
576
- (function(EnvironmentStatus2) {
577
- EnvironmentStatus2["Running"] = "running";
578
- EnvironmentStatus2["Stopped"] = "stopped";
579
- EnvironmentStatus2["Creating"] = "creating";
580
- EnvironmentStatus2["Deleting"] = "deleting";
581
- EnvironmentStatus2["Starting"] = "starting";
582
- EnvironmentStatus2["Stopping"] = "stopping";
583
- EnvironmentStatus2["Initializing"] = "initializing";
584
- })(EnvironmentStatus || (exports.EnvironmentStatus = EnvironmentStatus = {}));
585
- var ActionStatus;
586
- (function(ActionStatus2) {
587
- ActionStatus2["Running"] = "running";
588
- ActionStatus2["Success"] = "success";
589
- ActionStatus2["Error"] = "error";
590
- })(ActionStatus || (exports.ActionStatus = ActionStatus = {}));
591
- var VolumeStatus;
592
- (function(VolumeStatus2) {
593
- VolumeStatus2["Creating"] = "creating";
594
- VolumeStatus2["Available"] = "available";
595
- VolumeStatus2["Deleting"] = "deleting";
596
- })(VolumeStatus || (exports.VolumeStatus = VolumeStatus = {}));
597
- var LoopStatus;
598
- (function(LoopStatus2) {
599
- LoopStatus2["Running"] = "running";
600
- LoopStatus2["Stopped"] = "stopped";
601
- LoopStatus2["Error"] = "error";
602
- LoopStatus2["Completed"] = "completed";
603
- })(LoopStatus || (exports.LoopStatus = LoopStatus = {}));
604
- function getEnvLocation(env) {
605
- if (!env)
606
- return null;
607
- return env.location || null;
608
- }
609
- function getEnvRegionName(env) {
610
- var _a;
611
- if (!env)
612
- return "Unknown";
613
- return ((_a = env.location) === null || _a === undefined ? undefined : _a.name) || "Unknown";
614
- }
615
- function getEnvLocationLabel(env) {
616
- var _a, _b, _c;
617
- if (!env)
618
- return "Unknown";
619
- if (((_a = env.location) === null || _a === undefined ? undefined : _a.city) && ((_b = env.location) === null || _b === undefined ? undefined : _b.country)) {
620
- return "".concat(env.location.city, ", ").concat(env.location.country);
621
- }
622
- return ((_c = env.location) === null || _c === undefined ? undefined : _c.name) || "Unknown";
623
- }
624
512
  });
625
513
 
626
514
  // src/tmux.ts
627
515
  init_pool();
628
516
 
629
- // node_modules/@ebowwa/ssh/flags.js
517
+ // ../ssh/flags.js
630
518
  function sshConfig(key, value) {
631
519
  return { type: "config", key, value };
632
520
  }
@@ -1265,7 +1153,7 @@ class TmuxSessionManager {
1265
1153
  return { success: false, error: `Node ${nodeId} is not running` };
1266
1154
  }
1267
1155
  try {
1268
- const result = await createOrAttachTmuxSession(node.ip, node.user, node.keyPath, { sessionName });
1156
+ const result = await createOrAttachTmuxSession(node.ip, node.user, node.keyPath, {});
1269
1157
  return {
1270
1158
  success: true,
1271
1159
  sshArgs: result.sshArgs,
@@ -1697,10 +1585,10 @@ async function testSSHConnection(options) {
1697
1585
  init_pool();
1698
1586
  // src/scp.ts
1699
1587
  init_error();
1588
+ init_ssh();
1700
1589
  init_pool();
1701
- var import_ssh2 = __toESM(require_ssh(), 1);
1702
1590
  async function scpUpload(options) {
1703
- const validated = import_ssh2.SCPOptionsSchema.safeParse(options);
1591
+ const validated = SCPOptionsSchema.safeParse(options);
1704
1592
  if (!validated.success) {
1705
1593
  throw new Error(`Invalid SCP options: ${validated.error.issues.map((i) => i.message).join(", ")}`);
1706
1594
  }
@@ -1727,7 +1615,7 @@ async function scpUpload(options) {
1727
1615
  }
1728
1616
  }
1729
1617
  async function scpDownload(options) {
1730
- const validated = import_ssh2.SCPOptionsSchema.safeParse(options);
1618
+ const validated = SCPOptionsSchema.safeParse(options);
1731
1619
  if (!validated.success) {
1732
1620
  throw new Error(`Invalid SCP options: ${validated.error.issues.map((i) => i.message).join(", ")}`);
1733
1621
  }
@@ -1989,7 +1877,63 @@ init_pool();
1989
1877
 
1990
1878
  // src/sessions.ts
1991
1879
  import path2 from "path";
1992
- var import_compile = __toESM(require_compile(), 1);
1880
+
1881
+ // ../codespaces-types/compile/index.js
1882
+ var WebSocketCloseCode = {
1883
+ NORMAL_CLOSURE: 1000,
1884
+ ENDPOINT_GOING_AWAY: 1001,
1885
+ PROTOCOL_ERROR: 1002,
1886
+ UNSUPPORTED_DATA: 1003,
1887
+ NO_STATUS_RECEIVED: 1005,
1888
+ ABNORMAL_CLOSURE: 1006,
1889
+ INVALID_FRAME_PAYLOAD_DATA: 1007,
1890
+ POLICY_VIOLATION: 1008,
1891
+ MESSAGE_TOO_BIG: 1009,
1892
+ MISSING_MANDATORY_EXTENSION: 1010,
1893
+ INTERNAL_ERROR: 1011,
1894
+ SERVICE_RESTART: 1012,
1895
+ TRY_AGAIN_LATER: 1013,
1896
+ SESSION_NOT_FOUND: 4001,
1897
+ SESSION_ALREADY_CLOSED: 4002,
1898
+ INVALID_MESSAGE_FORMAT: 4003,
1899
+ AUTHENTICATION_FAILED: 4004,
1900
+ CONNECTION_TIMEOUT: 4005,
1901
+ SESSION_LIMIT_REACHED: 4006,
1902
+ INVALID_HOST: 4007,
1903
+ SSH_CONNECTION_FAILED: 4008,
1904
+ NETWORK_BLOCKED: 4009
1905
+ };
1906
+ var EnvironmentStatus;
1907
+ ((EnvironmentStatus2) => {
1908
+ EnvironmentStatus2["Running"] = "running";
1909
+ EnvironmentStatus2["Stopped"] = "stopped";
1910
+ EnvironmentStatus2["Creating"] = "creating";
1911
+ EnvironmentStatus2["Deleting"] = "deleting";
1912
+ EnvironmentStatus2["Starting"] = "starting";
1913
+ EnvironmentStatus2["Stopping"] = "stopping";
1914
+ EnvironmentStatus2["Initializing"] = "initializing";
1915
+ })(EnvironmentStatus ||= {});
1916
+ var ActionStatus;
1917
+ ((ActionStatus2) => {
1918
+ ActionStatus2["Running"] = "running";
1919
+ ActionStatus2["Success"] = "success";
1920
+ ActionStatus2["Error"] = "error";
1921
+ })(ActionStatus ||= {});
1922
+ var VolumeStatus;
1923
+ ((VolumeStatus2) => {
1924
+ VolumeStatus2["Creating"] = "creating";
1925
+ VolumeStatus2["Available"] = "available";
1926
+ VolumeStatus2["Deleting"] = "deleting";
1927
+ })(VolumeStatus ||= {});
1928
+ var LoopStatus;
1929
+ ((LoopStatus2) => {
1930
+ LoopStatus2["Running"] = "running";
1931
+ LoopStatus2["Stopped"] = "stopped";
1932
+ LoopStatus2["Error"] = "error";
1933
+ LoopStatus2["Completed"] = "completed";
1934
+ })(LoopStatus ||= {});
1935
+
1936
+ // src/sessions.ts
1993
1937
  var metadataModule = null;
1994
1938
  function loadMetadata() {
1995
1939
  if (metadataModule === null) {
@@ -2030,7 +1974,7 @@ async function closeSession(sessionId) {
2030
1974
  } catch {}
2031
1975
  }
2032
1976
  try {
2033
- session.ws?.close(import_compile.WebSocketCloseCode.NORMAL_CLOSURE, "Session closed");
1977
+ session.ws?.close(WebSocketCloseCode.NORMAL_CLOSURE, "Session closed");
2034
1978
  } catch {}
2035
1979
  if (session.bootstrapLogStreamer) {
2036
1980
  try {
@@ -2350,14 +2294,14 @@ async function getOrCreateSession(host, user = "root", sessionId = null, keyPath
2350
2294
  console.log(`[Terminal] Process for ${newSessionId} exited with code ${exitCode}`);
2351
2295
  session.closed = true;
2352
2296
  try {
2353
- session.ws?.close(import_compile.WebSocketCloseCode.NORMAL_CLOSURE, `SSH process exited (code: ${exitCode})`);
2297
+ session.ws?.close(WebSocketCloseCode.NORMAL_CLOSURE, `SSH process exited (code: ${exitCode})`);
2354
2298
  } catch {}
2355
2299
  terminalSessions.delete(newSessionId);
2356
2300
  }).catch((err) => {
2357
2301
  console.log(`[Terminal] Process exit error for ${newSessionId}:`, err);
2358
2302
  session.closed = true;
2359
2303
  try {
2360
- session.ws?.close(import_compile.WebSocketCloseCode.INTERNAL_ERROR, "SSH process error");
2304
+ session.ws?.close(WebSocketCloseCode.INTERNAL_ERROR, "SSH process error");
2361
2305
  } catch {}
2362
2306
  });
2363
2307
  return session;
@@ -2439,6 +2383,12 @@ var DEFAULT_CONFIG3 = {
2439
2383
  historyLimit: 1e4,
2440
2384
  sessionAgeLimit: 30 * 24 * 60 * 60 * 1000
2441
2385
  };
2386
+ // src/config.ts
2387
+ import { join, dirname, isAbsolute, resolve } from "path";
2388
+ import { exec as exec2 } from "child_process";
2389
+ import { promisify as promisify3 } from "util";
2390
+ var execAsync3 = promisify3(exec2);
2391
+ var SSH_CONFIG_PATH = join(process.env.HOME || "~", ".ssh", "config");
2442
2392
  // src/mcp/index.ts
2443
2393
  async function listSessionsTool() {
2444
2394
  const sessions = getAllSessionInfo();
@@ -20,7 +20,9 @@ export interface TerminalSession {
20
20
  createdAt: number;
21
21
  lastUsed: number;
22
22
  reader: ReadableStreamDefaultReader<Uint8Array> | null;
23
- stderrReader: ReadableStreamDefaultReader<Uint8Array> | null;
23
+ stderrReader: (ReadableStreamDefaultReader<Uint8Array> & {
24
+ readMany?: () => Promise<any>;
25
+ }) | null;
24
26
  writer: WritableStreamDefaultWriter<Uint8Array> | null;
25
27
  closed: boolean;
26
28
  tmuxSessionName?: string;
Binary file
@@ -46,7 +46,7 @@ export interface TmuxSession {
46
46
  /**
47
47
  * Detailed session information with windows and panes
48
48
  */
49
- export interface DetailedTmuxSession extends TmuxSession {
49
+ export interface DetailedTmuxSession extends Omit<TmuxSession, 'windows'> {
50
50
  windows: Array<{
51
51
  index: string;
52
52
  name: string;
@@ -95,8 +95,8 @@ export interface CreateSessionOptions {
95
95
  * Options for batch command execution
96
96
  */
97
97
  export interface BatchCommandOptions {
98
- /** Command to execute */
99
- command: string;
98
+ /** Command to execute (optional - passed separately to sendCommandToNodes) */
99
+ command?: string;
100
100
  /** Target pane index (default: "0") */
101
101
  paneIndex?: string;
102
102
  /** Execute in parallel (default: true) */
@@ -293,6 +293,7 @@ export declare class TmuxSessionManager {
293
293
  success: boolean;
294
294
  cleaned?: number;
295
295
  errors?: string[];
296
+ error?: string;
296
297
  }>;
297
298
  /**
298
299
  * Get resource usage for sessions on a node
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ebowwa/terminal",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "Terminal session management with tmux integration, SSH client, WebSocket support, and MCP interface",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,10 +17,15 @@
17
17
  "./types": {
18
18
  "types": "./dist/types.d.ts",
19
19
  "default": "./dist/types.d.ts"
20
+ },
21
+ "./client": {
22
+ "types": "./dist/client.d.ts",
23
+ "default": "./dist/client.js"
20
24
  }
21
25
  },
22
26
  "scripts": {
23
- "build": "bun build src/index.ts --outdir dist --target bun --external 'node-ssh' --external 'hono' --external 'zod' --external 'node:*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external 'node-ssh' --external 'hono' --external 'zod' --external 'node:*' --external '@modelcontextprotocol/*'"
27
+ "build": "bun build src/index.ts --outdir dist --target bun --external 'node-ssh' --external 'hono' --external 'zod' --external 'node:*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external 'node-ssh' --external 'hono' --external 'zod' --external 'node:*' --external '@modelcontextprotocol/*'",
28
+ "prepublishOnly": "bun run build"
24
29
  },
25
30
  "files": [
26
31
  "dist"
@@ -61,5 +66,13 @@
61
66
  "@types/bun": "latest",
62
67
  "@types/node-ssh": "^11.0.0",
63
68
  "typescript": "^5.9.3"
69
+ },
70
+ "ownership": {
71
+ "domain": "terminal",
72
+ "responsibilities": [
73
+ "ssh",
74
+ "tmux",
75
+ "session-management"
76
+ ]
64
77
  }
65
78
  }
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Terminal MCP Server
4
- *
5
- * Exposes terminal session management via Model Context Protocol
6
- * Integrates with tmux, SSH, PTY, and file operations
7
- */
8
- export {};
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Terminal MCP Server (stdio protocol)
4
- *
5
- * Model Context Protocol server for terminal session management
6
- * Uses stdio transport for Claude Code integration
7
- */
8
- export {};