@rajat-rastogi/maestro 0.6.0 → 0.7.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.
@@ -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,8 +589,12 @@ class AgentConnectionPool {
580
589
  entry.ready = true;
581
590
  entry.queue.forEach((cb) => cb(ws));
582
591
  entry.queue = [];
592
+ console.log(`[AgentPool] heartbeat started for "${tunnelId}" (60s interval)`);
583
593
  entry.heartbeat = setInterval(() => {
584
- if (ws.readyState !== WebSocket.OPEN) return;
594
+ if (ws.readyState !== WebSocket.OPEN) {
595
+ console.log(`[AgentPool] heartbeat skip for "${tunnelId}" — ws not open (state: ${ws.readyState})`);
596
+ return;
597
+ }
585
598
  let gotPong = false;
586
599
  const handler = (data) => {
587
600
  try {
@@ -1184,11 +1197,20 @@ class LocalPtyAdapter {
1184
1197
  } catch {
1185
1198
  }
1186
1199
  }
1187
- autoLaunch(provider) {
1200
+ autoLaunch(provider, skipPermissions, resumeAI) {
1188
1201
  if (provider === "claude") {
1189
- setTimeout(() => this.write("claude\r"), 800);
1202
+ const flags = [
1203
+ resumeAI ? "--continue" : "",
1204
+ skipPermissions ? "--dangerously-skip-permissions" : ""
1205
+ ].filter(Boolean).join(" ");
1206
+ setTimeout(() => this.write(`claude${flags ? " " + flags : ""}\r`), 800);
1190
1207
  } else if (provider === "copilot") {
1191
- setTimeout(() => this.write("copilot --output-format json\r"), 800);
1208
+ const flags = [
1209
+ "--output-format json",
1210
+ resumeAI ? "--continue" : "",
1211
+ skipPermissions ? "--yolo" : ""
1212
+ ].filter(Boolean).join(" ");
1213
+ setTimeout(() => this.write(`copilot ${flags}\r`), 800);
1192
1214
  }
1193
1215
  }
1194
1216
  }
@@ -1383,11 +1405,20 @@ class SshPtyAdapter {
1383
1405
  this.sshClient = null;
1384
1406
  this.devtunnelProc = null;
1385
1407
  }
1386
- autoLaunch(provider) {
1408
+ autoLaunch(provider, skipPermissions, resumeAI) {
1387
1409
  if (provider === "claude") {
1388
- setTimeout(() => this.write("claude\r"), 1200);
1410
+ const flags = [
1411
+ resumeAI ? "--continue" : "",
1412
+ skipPermissions ? "--dangerously-skip-permissions" : ""
1413
+ ].filter(Boolean).join(" ");
1414
+ setTimeout(() => this.write(`claude${flags ? " " + flags : ""}\r`), 1200);
1389
1415
  } else if (provider === "copilot") {
1390
- setTimeout(() => this.write("copilot --output-format json\r"), 1200);
1416
+ const flags = [
1417
+ "--output-format json",
1418
+ resumeAI ? "--continue" : "",
1419
+ skipPermissions ? "--yolo" : ""
1420
+ ].filter(Boolean).join(" ");
1421
+ setTimeout(() => this.write(`copilot ${flags}\r`), 1200);
1391
1422
  }
1392
1423
  }
1393
1424
  }
@@ -1449,16 +1480,15 @@ class AgentPtyAdapter {
1449
1480
  lastRows = 30;
1450
1481
  async handleConnectionLost() {
1451
1482
  if (this.dead) return;
1452
- if (this.reconnectAttempts < 3) {
1453
- this.reconnectAttempts++;
1454
- console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} reconnecting (attempt ${this.reconnectAttempts}/3)...`);
1483
+ for (let attempt = 1; attempt <= 3; attempt++) {
1484
+ console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} reconnecting (attempt ${attempt}/3)...`);
1455
1485
  if (this.dataCallback) {
1456
1486
  this.dataCallback(`\r
1457
- \x1B[1;33m[Maestro] Connection lost — reconnecting (${this.reconnectAttempts}/3)...\x1B[0m\r
1487
+ \x1B[1;33m[Maestro] Connection lost — reconnecting (${attempt}/3)...\x1B[0m\r
1458
1488
  `);
1459
1489
  }
1460
1490
  AgentConnectionPool.getInstance().releaseOne(this.tunnelId);
1461
- await new Promise((r) => setTimeout(r, 2e3 * this.reconnectAttempts));
1491
+ await new Promise((r) => setTimeout(r, 2e3 * attempt));
1462
1492
  try {
1463
1493
  this.ws = await AgentConnectionPool.getInstance().getConnection(this.tunnelId);
1464
1494
  if (this.messageHandler) {
@@ -1479,14 +1509,13 @@ class AgentPtyAdapter {
1479
1509
  cols: this.lastCols,
1480
1510
  rows: this.lastRows
1481
1511
  }));
1482
- this.reconnectAttempts = 0;
1483
1512
  console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} reconnected successfully`);
1484
1513
  if (this.dataCallback) {
1485
1514
  this.dataCallback("\r\n\x1B[1;32m[Maestro] Reconnected to remote agent.\x1B[0m\r\n");
1486
1515
  }
1487
1516
  return;
1488
1517
  } catch (err) {
1489
- console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} reconnect failed: ${err instanceof Error ? err.message : String(err)}`);
1518
+ console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} reconnect attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`);
1490
1519
  }
1491
1520
  }
1492
1521
  this.dead = true;
@@ -1525,11 +1554,20 @@ class AgentPtyAdapter {
1525
1554
  this.ws.send(JSON.stringify({ type: "destroy", id: this.sessionId }));
1526
1555
  }
1527
1556
  }
1528
- autoLaunch(provider) {
1557
+ autoLaunch(provider, skipPermissions, resumeAI) {
1529
1558
  if (provider === "claude") {
1530
- setTimeout(() => this.write("claude\r"), 600);
1559
+ const flags = [
1560
+ resumeAI ? "--continue" : "",
1561
+ skipPermissions ? "--dangerously-skip-permissions" : ""
1562
+ ].filter(Boolean).join(" ");
1563
+ setTimeout(() => this.write(`claude${flags ? " " + flags : ""}\r`), 600);
1531
1564
  } else if (provider === "copilot") {
1532
- setTimeout(() => this.write("copilot --output-format json\r"), 600);
1565
+ const flags = [
1566
+ "--output-format json",
1567
+ resumeAI ? "--continue" : "",
1568
+ skipPermissions ? "--yolo" : ""
1569
+ ].filter(Boolean).join(" ");
1570
+ setTimeout(() => this.write(`copilot ${flags}\r`), 600);
1533
1571
  }
1534
1572
  }
1535
1573
  }
@@ -1584,7 +1622,7 @@ class StateMonitor {
1584
1622
  if (this.copilotReady && data.length >= 20) {
1585
1623
  if (data.includes("Enter to select")) {
1586
1624
  this.emitState("permission", "heuristic", '"Enter to select" — input required');
1587
- } else if (data.includes("Esc to cancel") || data.includes("Esc to stop")) {
1625
+ } else if ((data.includes("Esc to cancel") || data.includes("Esc to stop")) && !this.lastEmittedState?.startsWith("permission")) {
1588
1626
  this.emitState("working", "heuristic", '"Esc to cancel/stop" — copilot working');
1589
1627
  } else if (data.includes("for shortcuts") || data.includes("Describe a task") || data.includes("to get started")) {
1590
1628
  this.emitState("waiting", "heuristic", "copilot prompt detected — at prompt");
@@ -1832,9 +1870,65 @@ class SessionManager {
1832
1870
  entries = /* @__PURE__ */ new Map();
1833
1871
  machineManager;
1834
1872
  push;
1873
+ savedPath;
1874
+ savedSessions = [];
1835
1875
  constructor(machineManager2, push) {
1836
1876
  this.machineManager = machineManager2;
1837
1877
  this.push = push;
1878
+ this.savedPath = path__namespace.join(electron.app.getPath("userData"), "saved-sessions.json");
1879
+ this.loadSaved();
1880
+ }
1881
+ loadSaved() {
1882
+ try {
1883
+ const data = fs__namespace.readFileSync(this.savedPath, "utf8");
1884
+ this.savedSessions = JSON.parse(data);
1885
+ console.log(`[SessionManager] Loaded ${this.savedSessions.length} saved sessions`);
1886
+ } catch {
1887
+ this.savedSessions = [];
1888
+ }
1889
+ }
1890
+ saveToDisk() {
1891
+ try {
1892
+ fs__namespace.writeFileSync(this.savedPath, JSON.stringify(this.savedSessions, null, 2), "utf8");
1893
+ } catch (err) {
1894
+ console.error("[SessionManager] Failed to save sessions:", err);
1895
+ }
1896
+ }
1897
+ listSaved() {
1898
+ return this.savedSessions;
1899
+ }
1900
+ dismissSaved() {
1901
+ this.savedSessions = [];
1902
+ this.saveToDisk();
1903
+ }
1904
+ async resumeSaved(ids, resumeAI) {
1905
+ const resumed = [];
1906
+ const skipped = [];
1907
+ for (const saved of this.savedSessions.filter((s) => ids.includes(s.id))) {
1908
+ const machine = this.machineManager.getById(saved.machineId);
1909
+ if (saved.machineId !== "local" && (!machine || machine.status === "offline")) {
1910
+ skipped.push(saved.name);
1911
+ continue;
1912
+ }
1913
+ try {
1914
+ const session = await this.create({
1915
+ name: saved.name,
1916
+ machineId: saved.machineId,
1917
+ provider: saved.provider,
1918
+ workingDirectory: saved.workingDirectory,
1919
+ tags: saved.tags,
1920
+ skipPermissions: saved.skipPermissions,
1921
+ resumeAI: resumeAI && saved.provider !== "none"
1922
+ });
1923
+ resumed.push(session);
1924
+ } catch (err) {
1925
+ console.error(`[SessionManager] Failed to resume "${saved.name}":`, err);
1926
+ skipped.push(saved.name);
1927
+ }
1928
+ }
1929
+ this.savedSessions = [];
1930
+ this.saveToDisk();
1931
+ return { resumed, skipped };
1838
1932
  }
1839
1933
  list() {
1840
1934
  return Array.from(this.entries.values()).map((e) => ({
@@ -1913,8 +2007,20 @@ class SessionManager {
1913
2007
  });
1914
2008
  monitor.start();
1915
2009
  if (opts.provider !== "none") {
1916
- adapter.autoLaunch(opts.provider);
1917
- }
2010
+ adapter.autoLaunch(opts.provider, opts.skipPermissions, opts.resumeAI);
2011
+ }
2012
+ this.savedSessions = this.savedSessions.filter((s) => s.id !== session.id);
2013
+ this.savedSessions.push({
2014
+ id: session.id,
2015
+ name: session.name,
2016
+ machineId: session.machineId,
2017
+ provider: session.provider,
2018
+ workingDirectory: session.workingDirectory,
2019
+ tags: session.tags,
2020
+ skipPermissions: opts.skipPermissions,
2021
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
2022
+ });
2023
+ this.saveToDisk();
1918
2024
  return { ...session };
1919
2025
  }
1920
2026
  destroy(id) {
@@ -1923,6 +2029,8 @@ class SessionManager {
1923
2029
  entry.monitor.stop();
1924
2030
  entry.adapter.kill();
1925
2031
  this.entries.delete(id);
2032
+ this.savedSessions = this.savedSessions.filter((s) => s.id !== id);
2033
+ this.saveToDisk();
1926
2034
  }
1927
2035
  rename(id, name) {
1928
2036
  const entry = this.entries.get(id);
@@ -2069,6 +2177,15 @@ electron.app.whenReady().then(() => {
2069
2177
  electron.ipcMain.handle("app:getStartupArgs", () => ({
2070
2178
  planFile: startupPlanFile
2071
2179
  }));
2180
+ electron.ipcMain.handle("app:getVersion", () => {
2181
+ try {
2182
+ const pkgPath = path.join(__dirname, "..", "..", "..", "package.json");
2183
+ const pkg = JSON.parse(fs__namespace.readFileSync(pkgPath, "utf8"));
2184
+ return pkg.version ?? "unknown";
2185
+ } catch {
2186
+ return electron.app.getVersion();
2187
+ }
2188
+ });
2072
2189
  createWindow();
2073
2190
  electron.app.on("activate", () => {
2074
2191
  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);