@rajat-rastogi/maestro 0.5.6 → 0.7.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.
@@ -503,6 +503,15 @@ function registerSessionsIpc(sessionManager2) {
503
503
  electron.ipcMain.handle("sessions:resize", async (_e, id, cols, rows) => {
504
504
  sessionManager2.resize(id, cols, rows);
505
505
  });
506
+ electron.ipcMain.handle("sessions:savedList", async () => {
507
+ return sessionManager2.listSaved();
508
+ });
509
+ electron.ipcMain.handle("sessions:savedResume", async (_e, ids, resumeAI) => {
510
+ return sessionManager2.resumeSaved(ids, resumeAI);
511
+ });
512
+ electron.ipcMain.handle("sessions:savedDismiss", async () => {
513
+ sessionManager2.dismissSaved();
514
+ });
506
515
  }
507
516
  const execAsync$2 = util.promisify(child_process.exec);
508
517
  class AgentConnectionPool {
@@ -580,6 +589,29 @@ class AgentConnectionPool {
580
589
  entry.ready = true;
581
590
  entry.queue.forEach((cb) => cb(ws));
582
591
  entry.queue = [];
592
+ entry.heartbeat = setInterval(() => {
593
+ if (ws.readyState !== WebSocket.OPEN) return;
594
+ let gotPong = false;
595
+ const handler = (data) => {
596
+ try {
597
+ const msg = JSON.parse(data.toString());
598
+ if (msg.type === "pong") {
599
+ gotPong = true;
600
+ ws.removeListener("message", handler);
601
+ }
602
+ } catch {
603
+ }
604
+ };
605
+ ws.on("message", handler);
606
+ ws.send(JSON.stringify({ type: "ping" }));
607
+ setTimeout(() => {
608
+ ws.removeListener("message", handler);
609
+ if (!gotPong && ws.readyState === WebSocket.OPEN) {
610
+ console.log(`[AgentPool] heartbeat failed for "${tunnelId}" — zombie connection, closing`);
611
+ ws.terminate();
612
+ }
613
+ }, 1e4);
614
+ }, 6e4);
583
615
  resolve(ws);
584
616
  });
585
617
  ws.on("error", (err) => {
@@ -685,6 +717,7 @@ class AgentConnectionPool {
685
717
  teardown(tunnelId) {
686
718
  const entry = this.pool.get(tunnelId);
687
719
  if (entry) {
720
+ if (entry.heartbeat) clearInterval(entry.heartbeat);
688
721
  try {
689
722
  entry.ws?.close();
690
723
  } catch {
@@ -692,6 +725,9 @@ class AgentConnectionPool {
692
725
  this.pool.delete(tunnelId);
693
726
  }
694
727
  }
728
+ releaseOne(tunnelId) {
729
+ this.teardown(tunnelId);
730
+ }
695
731
  releaseAll() {
696
732
  for (const id of Array.from(this.pool.keys())) {
697
733
  this.teardown(id);
@@ -1157,11 +1193,20 @@ class LocalPtyAdapter {
1157
1193
  } catch {
1158
1194
  }
1159
1195
  }
1160
- autoLaunch(provider) {
1196
+ autoLaunch(provider, skipPermissions, resumeAI) {
1161
1197
  if (provider === "claude") {
1162
- setTimeout(() => this.write("claude\r"), 800);
1198
+ const flags = [
1199
+ resumeAI ? "--continue" : "",
1200
+ skipPermissions ? "--dangerously-skip-permissions" : ""
1201
+ ].filter(Boolean).join(" ");
1202
+ setTimeout(() => this.write(`claude${flags ? " " + flags : ""}\r`), 800);
1163
1203
  } else if (provider === "copilot") {
1164
- setTimeout(() => this.write("copilot --output-format json\r"), 800);
1204
+ const flags = [
1205
+ "--output-format json",
1206
+ resumeAI ? "--continue" : "",
1207
+ skipPermissions ? "--yolo" : ""
1208
+ ].filter(Boolean).join(" ");
1209
+ setTimeout(() => this.write(`copilot ${flags}\r`), 800);
1165
1210
  }
1166
1211
  }
1167
1212
  }
@@ -1356,11 +1401,20 @@ class SshPtyAdapter {
1356
1401
  this.sshClient = null;
1357
1402
  this.devtunnelProc = null;
1358
1403
  }
1359
- autoLaunch(provider) {
1404
+ autoLaunch(provider, skipPermissions, resumeAI) {
1360
1405
  if (provider === "claude") {
1361
- setTimeout(() => this.write("claude\r"), 1200);
1406
+ const flags = [
1407
+ resumeAI ? "--continue" : "",
1408
+ skipPermissions ? "--dangerously-skip-permissions" : ""
1409
+ ].filter(Boolean).join(" ");
1410
+ setTimeout(() => this.write(`claude${flags ? " " + flags : ""}\r`), 1200);
1362
1411
  } else if (provider === "copilot") {
1363
- setTimeout(() => this.write("copilot --output-format json\r"), 1200);
1412
+ const flags = [
1413
+ "--output-format json",
1414
+ resumeAI ? "--continue" : "",
1415
+ skipPermissions ? "--yolo" : ""
1416
+ ].filter(Boolean).join(" ");
1417
+ setTimeout(() => this.write(`copilot ${flags}\r`), 1200);
1364
1418
  }
1365
1419
  }
1366
1420
  }
@@ -1373,7 +1427,11 @@ class AgentPtyAdapter {
1373
1427
  dataCallback = null;
1374
1428
  exitCallback = null;
1375
1429
  messageHandler = null;
1430
+ dead = false;
1376
1431
  async connect(cwd, cols, rows) {
1432
+ this.lastCwd = cwd;
1433
+ this.lastCols = cols;
1434
+ this.lastRows = rows;
1377
1435
  this.ws = await AgentConnectionPool.getInstance().getConnection(this.tunnelId);
1378
1436
  this.messageHandler = (raw) => {
1379
1437
  try {
@@ -1389,13 +1447,8 @@ class AgentPtyAdapter {
1389
1447
  };
1390
1448
  this.ws.on("message", this.messageHandler);
1391
1449
  this.ws.on("close", () => {
1392
- console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} WebSocket closed — connection lost`);
1393
- if (this.dataCallback) {
1394
- this.dataCallback("\r\n\x1B[1;31m[Maestro] Connection to remote agent lost. Close and recreate the terminal.\x1B[0m\r\n");
1395
- }
1396
- if (this.exitCallback) {
1397
- this.exitCallback(null);
1398
- }
1450
+ console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} WebSocket closed`);
1451
+ this.handleConnectionLost();
1399
1452
  });
1400
1453
  this.ws.on("error", (err) => {
1401
1454
  console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} WebSocket error: ${err.message}`);
@@ -1417,6 +1470,61 @@ class AgentPtyAdapter {
1417
1470
  this.ws.on("message", ackHandler);
1418
1471
  });
1419
1472
  }
1473
+ reconnectAttempts = 0;
1474
+ lastCwd = "";
1475
+ lastCols = 120;
1476
+ lastRows = 30;
1477
+ async handleConnectionLost() {
1478
+ if (this.dead) return;
1479
+ if (this.reconnectAttempts < 3) {
1480
+ this.reconnectAttempts++;
1481
+ console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} reconnecting (attempt ${this.reconnectAttempts}/3)...`);
1482
+ if (this.dataCallback) {
1483
+ this.dataCallback(`\r
1484
+ \x1B[1;33m[Maestro] Connection lost — reconnecting (${this.reconnectAttempts}/3)...\x1B[0m\r
1485
+ `);
1486
+ }
1487
+ AgentConnectionPool.getInstance().releaseOne(this.tunnelId);
1488
+ await new Promise((r) => setTimeout(r, 2e3 * this.reconnectAttempts));
1489
+ try {
1490
+ this.ws = await AgentConnectionPool.getInstance().getConnection(this.tunnelId);
1491
+ if (this.messageHandler) {
1492
+ this.ws.on("message", this.messageHandler);
1493
+ }
1494
+ this.ws.on("close", () => {
1495
+ console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} WebSocket closed`);
1496
+ this.handleConnectionLost();
1497
+ });
1498
+ this.ws.on("error", (err) => {
1499
+ console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} WebSocket error: ${err.message}`);
1500
+ });
1501
+ this.ws.send(JSON.stringify({
1502
+ type: "create",
1503
+ id: this.sessionId,
1504
+ shell: "powershell",
1505
+ cwd: this.lastCwd,
1506
+ cols: this.lastCols,
1507
+ rows: this.lastRows
1508
+ }));
1509
+ this.reconnectAttempts = 0;
1510
+ console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} reconnected successfully`);
1511
+ if (this.dataCallback) {
1512
+ this.dataCallback("\r\n\x1B[1;32m[Maestro] Reconnected to remote agent.\x1B[0m\r\n");
1513
+ }
1514
+ return;
1515
+ } catch (err) {
1516
+ console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} reconnect failed: ${err instanceof Error ? err.message : String(err)}`);
1517
+ }
1518
+ }
1519
+ this.dead = true;
1520
+ console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} all reconnect attempts failed`);
1521
+ if (this.dataCallback) {
1522
+ this.dataCallback("\r\n\x1B[1;31m[Maestro] Connection to remote agent lost. Close and recreate the terminal.\x1B[0m\r\n");
1523
+ }
1524
+ if (this.exitCallback) {
1525
+ this.exitCallback(null);
1526
+ }
1527
+ }
1420
1528
  onData(cb) {
1421
1529
  this.dataCallback = cb;
1422
1530
  }
@@ -1429,21 +1537,35 @@ class AgentPtyAdapter {
1429
1537
  }
1430
1538
  }
1431
1539
  resize(cols, rows) {
1540
+ this.lastCols = cols;
1541
+ this.lastRows = rows;
1432
1542
  if (this.ws?.readyState === WebSocket.OPEN) {
1433
1543
  this.ws.send(JSON.stringify({ type: "resize", id: this.sessionId, cols, rows }));
1434
1544
  }
1435
1545
  }
1436
1546
  kill() {
1547
+ this.dead = true;
1437
1548
  if (this.ws && this.messageHandler) {
1438
1549
  this.ws.removeListener("message", this.messageHandler);
1439
1550
  }
1440
- this.ws?.send(JSON.stringify({ type: "destroy", id: this.sessionId }));
1551
+ if (this.ws?.readyState === WebSocket.OPEN) {
1552
+ this.ws.send(JSON.stringify({ type: "destroy", id: this.sessionId }));
1553
+ }
1441
1554
  }
1442
- autoLaunch(provider) {
1555
+ autoLaunch(provider, skipPermissions, resumeAI) {
1443
1556
  if (provider === "claude") {
1444
- setTimeout(() => this.write("claude\r"), 600);
1557
+ const flags = [
1558
+ resumeAI ? "--continue" : "",
1559
+ skipPermissions ? "--dangerously-skip-permissions" : ""
1560
+ ].filter(Boolean).join(" ");
1561
+ setTimeout(() => this.write(`claude${flags ? " " + flags : ""}\r`), 600);
1445
1562
  } else if (provider === "copilot") {
1446
- setTimeout(() => this.write("copilot --output-format json\r"), 600);
1563
+ const flags = [
1564
+ "--output-format json",
1565
+ resumeAI ? "--continue" : "",
1566
+ skipPermissions ? "--yolo" : ""
1567
+ ].filter(Boolean).join(" ");
1568
+ setTimeout(() => this.write(`copilot ${flags}\r`), 600);
1447
1569
  }
1448
1570
  }
1449
1571
  }
@@ -1746,9 +1868,65 @@ class SessionManager {
1746
1868
  entries = /* @__PURE__ */ new Map();
1747
1869
  machineManager;
1748
1870
  push;
1871
+ savedPath;
1872
+ savedSessions = [];
1749
1873
  constructor(machineManager2, push) {
1750
1874
  this.machineManager = machineManager2;
1751
1875
  this.push = push;
1876
+ this.savedPath = path__namespace.join(electron.app.getPath("userData"), "saved-sessions.json");
1877
+ this.loadSaved();
1878
+ }
1879
+ loadSaved() {
1880
+ try {
1881
+ const data = fs__namespace.readFileSync(this.savedPath, "utf8");
1882
+ this.savedSessions = JSON.parse(data);
1883
+ console.log(`[SessionManager] Loaded ${this.savedSessions.length} saved sessions`);
1884
+ } catch {
1885
+ this.savedSessions = [];
1886
+ }
1887
+ }
1888
+ saveToDisk() {
1889
+ try {
1890
+ fs__namespace.writeFileSync(this.savedPath, JSON.stringify(this.savedSessions, null, 2), "utf8");
1891
+ } catch (err) {
1892
+ console.error("[SessionManager] Failed to save sessions:", err);
1893
+ }
1894
+ }
1895
+ listSaved() {
1896
+ return this.savedSessions;
1897
+ }
1898
+ dismissSaved() {
1899
+ this.savedSessions = [];
1900
+ this.saveToDisk();
1901
+ }
1902
+ async resumeSaved(ids, resumeAI) {
1903
+ const resumed = [];
1904
+ const skipped = [];
1905
+ for (const saved of this.savedSessions.filter((s) => ids.includes(s.id))) {
1906
+ const machine = this.machineManager.getById(saved.machineId);
1907
+ if (saved.machineId !== "local" && (!machine || machine.status === "offline")) {
1908
+ skipped.push(saved.name);
1909
+ continue;
1910
+ }
1911
+ try {
1912
+ const session = await this.create({
1913
+ name: saved.name,
1914
+ machineId: saved.machineId,
1915
+ provider: saved.provider,
1916
+ workingDirectory: saved.workingDirectory,
1917
+ tags: saved.tags,
1918
+ skipPermissions: saved.skipPermissions,
1919
+ resumeAI: resumeAI && saved.provider !== "none"
1920
+ });
1921
+ resumed.push(session);
1922
+ } catch (err) {
1923
+ console.error(`[SessionManager] Failed to resume "${saved.name}":`, err);
1924
+ skipped.push(saved.name);
1925
+ }
1926
+ }
1927
+ this.savedSessions = [];
1928
+ this.saveToDisk();
1929
+ return { resumed, skipped };
1752
1930
  }
1753
1931
  list() {
1754
1932
  return Array.from(this.entries.values()).map((e) => ({
@@ -1827,8 +2005,20 @@ class SessionManager {
1827
2005
  });
1828
2006
  monitor.start();
1829
2007
  if (opts.provider !== "none") {
1830
- adapter.autoLaunch(opts.provider);
1831
- }
2008
+ adapter.autoLaunch(opts.provider, opts.skipPermissions, opts.resumeAI);
2009
+ }
2010
+ this.savedSessions = this.savedSessions.filter((s) => s.id !== session.id);
2011
+ this.savedSessions.push({
2012
+ id: session.id,
2013
+ name: session.name,
2014
+ machineId: session.machineId,
2015
+ provider: session.provider,
2016
+ workingDirectory: session.workingDirectory,
2017
+ tags: session.tags,
2018
+ skipPermissions: opts.skipPermissions,
2019
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
2020
+ });
2021
+ this.saveToDisk();
1832
2022
  return { ...session };
1833
2023
  }
1834
2024
  destroy(id) {
@@ -1837,6 +2027,8 @@ class SessionManager {
1837
2027
  entry.monitor.stop();
1838
2028
  entry.adapter.kill();
1839
2029
  this.entries.delete(id);
2030
+ this.savedSessions = this.savedSessions.filter((s) => s.id !== id);
2031
+ this.saveToDisk();
1840
2032
  }
1841
2033
  rename(id, name) {
1842
2034
  const entry = this.entries.get(id);
@@ -1884,26 +2076,32 @@ class SessionManager {
1884
2076
  this.push("sessions:stateChanged", ev);
1885
2077
  }
1886
2078
  }
1887
- const logDir = electron.app.getPath("userData");
1888
- const logFile = path.join(logDir, "maestro-gui.log");
2079
+ let logStream = null;
1889
2080
  try {
1890
- if (fs__namespace.existsSync(logFile)) fs__namespace.renameSync(logFile, logFile + ".prev");
1891
- } catch {
2081
+ const logDir = electron.app.getPath("userData");
2082
+ fs__namespace.mkdirSync(logDir, { recursive: true });
2083
+ const logFile = path.join(logDir, "maestro-gui.log");
2084
+ try {
2085
+ if (fs__namespace.existsSync(logFile)) fs__namespace.renameSync(logFile, logFile + ".prev");
2086
+ } catch {
2087
+ }
2088
+ logStream = fs__namespace.createWriteStream(logFile, { flags: "a" });
2089
+ const origLog = console.log;
2090
+ const origErr = console.error;
2091
+ console.log = (...args) => {
2092
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${args.map(String).join(" ")}`;
2093
+ logStream?.write(line + "\n");
2094
+ origLog(...args);
2095
+ };
2096
+ console.error = (...args) => {
2097
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} ERROR ${args.map(String).join(" ")}`;
2098
+ logStream?.write(line + "\n");
2099
+ origErr(...args);
2100
+ };
2101
+ console.log(`[Maestro] Log file: ${logFile}`);
2102
+ } catch (err) {
2103
+ console.error("[Maestro] Failed to set up log file:", err);
1892
2104
  }
1893
- const logStream = fs__namespace.createWriteStream(logFile, { flags: "a" });
1894
- const origLog = console.log;
1895
- const origErr = console.error;
1896
- console.log = (...args) => {
1897
- const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${args.map(String).join(" ")}`;
1898
- logStream.write(line + "\n");
1899
- origLog(...args);
1900
- };
1901
- console.error = (...args) => {
1902
- const line = `${(/* @__PURE__ */ new Date()).toISOString()} ERROR ${args.map(String).join(" ")}`;
1903
- logStream.write(line + "\n");
1904
- origErr(...args);
1905
- };
1906
- console.log(`[Maestro] Log file: ${logFile}`);
1907
2105
  const startupArgs = process.argv.slice(2);
1908
2106
  let startupPlanFile = null;
1909
2107
  const pfIdx = startupArgs.indexOf("--plan-file");
@@ -1977,6 +2175,15 @@ electron.app.whenReady().then(() => {
1977
2175
  electron.ipcMain.handle("app:getStartupArgs", () => ({
1978
2176
  planFile: startupPlanFile
1979
2177
  }));
2178
+ electron.ipcMain.handle("app:getVersion", () => {
2179
+ try {
2180
+ const pkgPath = path.join(__dirname, "..", "..", "..", "package.json");
2181
+ const pkg = JSON.parse(fs__namespace.readFileSync(pkgPath, "utf8"));
2182
+ return pkg.version ?? "unknown";
2183
+ } catch {
2184
+ return electron.app.getVersion();
2185
+ }
2186
+ });
1980
2187
  createWindow();
1981
2188
  electron.app.on("activate", () => {
1982
2189
  if (electron.BrowserWindow.getAllWindows().length === 0) {
@@ -55,6 +55,7 @@ electron.contextBridge.exposeInMainWorld("maestro", {
55
55
  },
56
56
  // Read startup args passed from CLI
57
57
  getStartupArgs: () => electron.ipcRenderer.invoke("app:getStartupArgs"),
58
+ getVersion: () => electron.ipcRenderer.invoke("app:getVersion"),
58
59
  // Machine management
59
60
  machines: {
60
61
  list: () => electron.ipcRenderer.invoke("machines:list"),
@@ -84,6 +85,9 @@ electron.contextBridge.exposeInMainWorld("maestro", {
84
85
  acknowledge: (id) => electron.ipcRenderer.invoke("sessions:acknowledge", id),
85
86
  sendInput: (id, data) => electron.ipcRenderer.invoke("sessions:sendInput", id, data),
86
87
  resize: (id, cols, rows) => electron.ipcRenderer.invoke("sessions:resize", id, cols, rows),
88
+ savedList: () => electron.ipcRenderer.invoke("sessions:savedList"),
89
+ savedResume: (ids, resumeAI) => electron.ipcRenderer.invoke("sessions:savedResume", ids, resumeAI),
90
+ savedDismiss: () => electron.ipcRenderer.invoke("sessions:savedDismiss"),
87
91
  onData: (cb) => {
88
92
  const handler = (_, ev) => cb(ev);
89
93
  electron.ipcRenderer.on("sessions:data", handler);