@monotykamary/localterm-server 2.76.9 → 2.76.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@ import { detectWithExplicitPort } from "./cdp/discover-explicit-endpoint.js";
22
22
  import { DaemonConfigStore } from "./daemon-config-store.js";
23
23
  import { formatCandidates, resolveCandidates, resolveCompletionContext, } from "./completion/index.js";
24
24
  import { z } from "zod";
25
- import { ACTIVITY_DIRNAME, ACTIVITY_REFRESH_DEBOUNCE_MS, ACTIVITY_WATCHED_PROGRAMS, AUTOMATION_EVENT_DEBOUNCE_MS, AUTOMATION_RECONCILE_MIN_DOWNTIME_MS, AUTOMATION_RUN_QUERY_PARAM, AUTOMATION_WATCH_DEBOUNCE_MS, AUTOMATION_WATCH_POST_RUN_GRACE_MS, AUTOMATION_WEBHOOK_DEBOUNCE_MS, DEFAULT_HOST, DEFAULT_PORT, FRIENDLY_HOSTNAME, GIT_DIRTY_THROTTLE_MS, GIT_MAX_REF_LENGTH, HERDR_THEME_SYNC_DEBOUNCE_MS, HIBERNATE_FILENAME, HTTP_STATUS_ACCEPTED, HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_CONFLICT, HTTP_STATUS_CREATED, HTTP_STATUS_NOT_FOUND, HTTP_STATUS_PAYLOAD_TOO_LARGE, HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, MAX_AUTOMATIONS, MAX_IMAGE_UPLOAD_BYTES, MAX_IMAGE_UPLOAD_REQUEST_BYTES, MAX_PROCESSES, MAX_SECRETS, MS_PER_MINUTE, PROCESSES_FILENAME, SECRETS_FILENAME, THEMES_FILENAME, FONTS_FILENAME, SECRETS_SHIMS_DIRNAME, SECRET_EXPORT_SCRYPT_WORK_FACTOR, AUTOMATION_AGENT_SESSIONS_DIRNAME, SERVER_STOP_GRACE_MS, SESSION_ID_QUERY_PARAM, WORKSPACE_FILENAME, WORKSPACE_RESTORE_SETTLE_MS, WORKSPACE_SNAPSHOT_DEBOUNCE_MS, SESSION_ACTIVITY_WINDOW_MS, WINDOW_ID_QUERY_PARAM, WAIT_DEFAULT_TIMEOUT_MS, WS_BACKPRESSURE_THRESHOLD_BYTES, WS_CLOSE_BACKPRESSURE, WS_CLOSE_CAPACITY_REACHED, WS_CLOSE_POLICY_VIOLATION, WS_HEARTBEAT_GRACE_MS, WS_HEARTBEAT_INTERVAL_MS, WS_HEARTBEAT_TIMEOUT_MS, WS_MAX_INBOUND_PAYLOAD_BYTES, WS_READY_STATE_OPEN, AUTH_SECRET_FILENAME, AUTH_COOKIE_NAME, UPDATE_CHECK_FILENAME, } from "./constants.js";
25
+ import { ACTIVITY_DIRNAME, ACTIVITY_REFRESH_DEBOUNCE_MS, ACTIVITY_WATCHED_PROGRAMS, AUTOMATION_EVENT_DEBOUNCE_MS, AUTOMATION_RECONCILE_MIN_DOWNTIME_MS, AUTOMATION_RUN_QUERY_PARAM, AUTOMATION_WATCH_DEBOUNCE_MS, AUTOMATION_WATCH_POST_RUN_GRACE_MS, AUTOMATION_WEBHOOK_DEBOUNCE_MS, DEFAULT_HOST, DEFAULT_PORT, FRIENDLY_HOSTNAME, GIT_DIRTY_THROTTLE_MS, GIT_MAX_REF_LENGTH, HERDR_THEME_SYNC_DEBOUNCE_MS, HIBERNATE_FILENAME, HTTP_STATUS_ACCEPTED, HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_CONFLICT, HTTP_STATUS_CREATED, HTTP_STATUS_NOT_FOUND, HTTP_STATUS_PAYLOAD_TOO_LARGE, HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, MAX_AUTOMATIONS, MAX_IMAGE_UPLOAD_BYTES, MAX_IMAGE_UPLOAD_REQUEST_BYTES, MAX_PROCESSES, MAX_SECRETS, MS_PER_MINUTE, PROCESSES_FILENAME, SECRETS_FILENAME, THEMES_FILENAME, FONTS_FILENAME, SECRETS_SHIMS_DIRNAME, SECRET_EXPORT_SCRYPT_WORK_FACTOR, AUTOMATION_AGENT_SESSIONS_DIRNAME, SERVER_STOP_GRACE_MS, SESSION_ID_QUERY_PARAM, SESSION_ACTIVITY_WINDOW_MS, WINDOW_ID_QUERY_PARAM, WAIT_DEFAULT_TIMEOUT_MS, WS_BACKPRESSURE_THRESHOLD_BYTES, WS_CLOSE_BACKPRESSURE, WS_CLOSE_CAPACITY_REACHED, WS_CLOSE_POLICY_VIOLATION, WS_HEARTBEAT_GRACE_MS, WS_HEARTBEAT_INTERVAL_MS, WS_HEARTBEAT_TIMEOUT_MS, WS_MAX_INBOUND_PAYLOAD_BYTES, WS_READY_STATE_OPEN, AUTH_SECRET_FILENAME, AUTH_COOKIE_NAME, UPDATE_CHECK_FILENAME, } from "./constants.js";
26
26
  import { getDefaultShell, listKnownShells, resolveShellOverride } from "./default-shell.js";
27
27
  import { resolveWindowId } from "./utils/resolve-window-id.js";
28
28
  import { resolveTermBackspaceSequence } from "./utils/resolve-term-backspace-sequence.js";
@@ -40,7 +40,6 @@ import { WebhookTriggerManager } from "./webhook-trigger-manager.js";
40
40
  import { getGitBranchInfo, getGitBranchPr, getGitDiff, getGitDiffFileContents, getGitDiffFilePatch, getGitDiffFiles, getGitDiffSummary, } from "./git-diff.js";
41
41
  import { HeartbeatStore } from "./heartbeat-store.js";
42
42
  import { HibernateStore } from "./hibernate-store.js";
43
- import { WorkspaceStore } from "./workspace-store.js";
44
43
  import { createDefaultSecretBackend } from "./secret-backend.js";
45
44
  import { SecretStore } from "./secret-store.js";
46
45
  import { ProcessStore } from "./process-store.js";
@@ -194,7 +193,7 @@ const normalizeMouseAction = (input) => {
194
193
  };
195
194
  const buildApiRoutes = (ctx) => {
196
195
  const api = new Hono();
197
- const { registry, ownerFor, cdpClient, secretBackend, secretExportScryptWorkFactor, secretStore, shimsDir, processStore, themeStore, fontStore, syncSecretShims, automationStore, broadcastAutomations, syncFolderWatchers, syncSessionEventListeners, webhookTriggerManager, worktreeConfigStore, portsSnapshotProcesses, portsSnapshotListeners, toAutomationWithNextRun, listAutomationsWithNextRun, tryLaunch, getCdpPort, applyCdpPort, getGraceSeconds, applyGraceSeconds, getWorkspaceRestore, applyWorkspaceRestore, connectCdpNow, buildTabUrl, mintViewerCookie, stateDirectory, broadcastThemes, broadcastFonts, updateCheckStore, } = ctx;
196
+ const { registry, ownerFor, cdpClient, secretBackend, secretExportScryptWorkFactor, secretStore, shimsDir, processStore, themeStore, fontStore, syncSecretShims, automationStore, broadcastAutomations, syncFolderWatchers, syncSessionEventListeners, webhookTriggerManager, worktreeConfigStore, portsSnapshotProcesses, portsSnapshotListeners, toAutomationWithNextRun, listAutomationsWithNextRun, tryLaunch, getCdpPort, applyCdpPort, getGraceSeconds, applyGraceSeconds, connectCdpNow, buildTabUrl, mintViewerCookie, stateDirectory, broadcastThemes, broadcastFonts, updateCheckStore, } = ctx;
198
197
  // Headless SGR-1006 fallback for `mouse` when no CDP tab is reachable:
199
198
  // encode the gesture as SGR bytes and write them straight to the PTY. Closes
200
199
  // over `registry` so the automation layer stays CDP-agnostic. Coords arrive
@@ -1584,7 +1583,6 @@ const buildApiRoutes = (ctx) => {
1584
1583
  api.get("/config", (context) => context.json({
1585
1584
  cdpPort: getCdpPort(),
1586
1585
  graceSeconds: getGraceSeconds(),
1587
- workspaceRestore: getWorkspaceRestore(),
1588
1586
  defaultShell: getDefaultShell(),
1589
1587
  shells: listKnownShells(),
1590
1588
  }));
@@ -1597,9 +1595,6 @@ const buildApiRoutes = (ctx) => {
1597
1595
  graceSeconds: parsed.data.graceSeconds === undefined
1598
1596
  ? getGraceSeconds()
1599
1597
  : applyGraceSeconds(parsed.data.graceSeconds),
1600
- workspaceRestore: parsed.data.workspaceRestore === undefined
1601
- ? getWorkspaceRestore()
1602
- : applyWorkspaceRestore(parsed.data.workspaceRestore),
1603
1598
  defaultShell: getDefaultShell(),
1604
1599
  shells: listKnownShells(),
1605
1600
  });
@@ -1677,15 +1672,10 @@ export const createServer = async (options = {}) => {
1677
1672
  onOutputActivity: () => caffeinateManager.noteOutputActivity(),
1678
1673
  onSessionActivity: () => {
1679
1674
  caffeinateManager.pokeAuto();
1680
- scheduleWorkspaceSnapshot();
1681
1675
  refreshHerdrThemeSyncState();
1682
1676
  },
1683
1677
  onSessionEvent: (event, cwd) => {
1684
1678
  sessionEventManager.onSessionEvent(event, cwd);
1685
- // A cwd change reshapes the workspace manifest (the tab's respawn cwd
1686
- // moves), so re-snapshot alongside the attach/detach-driven snapshot.
1687
- if (event === "cwd")
1688
- scheduleWorkspaceSnapshot();
1689
1679
  },
1690
1680
  onAutomationExit: (automationId, runId, exitCode, log) => {
1691
1681
  automationStore.updateRun(automationId, runId, {
@@ -1770,10 +1760,9 @@ export const createServer = async (options = {}) => {
1770
1760
  const caffeinateController = options.caffeinateController ?? new CaffeinateController();
1771
1761
  const caffeinatePreferencesStore = new CaffeinatePreferencesStore(path.join(stateDirectory, "caffeinate.json"));
1772
1762
  const worktreeConfigStore = new WorktreeConfigStore(stateDirectory);
1773
- const workspaceStore = new WorkspaceStore(path.join(stateDirectory, WORKSPACE_FILENAME));
1774
1763
  const hibernateStore = new HibernateStore(path.join(stateDirectory, HIBERNATE_FILENAME));
1775
1764
  // Immutable shutdown snapshot for this daemon lifetime. Exact old session ids
1776
- // let surviving or CDP-restored browser tabs recover only their own text.
1765
+ // let surviving browser tabs recover only their own text.
1777
1766
  const startupHibernateEntries = hibernateStore.read();
1778
1767
  // Per-process secret injection: a backend (macOS Keychain on darwin) holds
1779
1768
  // secret values; a secret is an identity + the env var it exports
@@ -2302,11 +2291,6 @@ export const createServer = async (options = {}) => {
2302
2291
  registry.rearmGrace();
2303
2292
  return next;
2304
2293
  };
2305
- // Persist + read the workspace-restore toggle. Read at restore time, so a
2306
- // `PUT /api/config` change takes effect on the next start (the restore runs
2307
- // once at startup, not live-reactively).
2308
- const getWorkspaceRestore = () => daemonConfigStore.getWorkspaceRestore();
2309
- const applyWorkspaceRestore = (enabled) => daemonConfigStore.setWorkspaceRestore(enabled);
2310
2294
  // Explicit "Connect now" (Settings → Automation browser → Connect): drop any
2311
2295
  // live socket and await a fresh connect so the caller learns the outcome —
2312
2296
  // connected + which browser, or the error that explains a failure (e.g. a
@@ -2331,45 +2315,6 @@ export const createServer = async (options = {}) => {
2331
2315
  };
2332
2316
  }
2333
2317
  };
2334
- // Workspace restore: reopen the user's last tabs via CDP on start (a
2335
- // tmux-resurrect/herdr-style restore of the *layout*; the shells themselves
2336
- // don't survive a stop). The manifest is snapshotted to disk — debounced on
2337
- // attach/detach churn (scheduleWorkspaceSnapshot, hooked into onSessionActivity)
2338
- // and flushed on graceful stop — so a restart, graceful or a crash, can reopen
2339
- // the same tabs. Triggered once per (owner, windowId) after the first desktop
2340
- // tab pairs with CDP, on a quiet window so surviving tabs (a daemon restart
2341
- // with the browser left open) reattach and are counted before the deficit is
2342
- // opened. Excludes automation-run tabs and dormant shells (filtered in
2343
- // SessionManager.workspaceEntries), so only tabs that were actively open
2344
- // come back. Opt out via Settings → Sessions (config.json workspaceRestore).
2345
- let workspaceSnapshotTimer = null;
2346
- const scheduleWorkspaceSnapshot = () => {
2347
- if (workspaceSnapshotTimer !== null)
2348
- clearTimeout(workspaceSnapshotTimer);
2349
- const timer = setTimeout(() => {
2350
- workspaceSnapshotTimer = null;
2351
- workspaceStore.write(registry.workspaceEntries());
2352
- }, WORKSPACE_SNAPSHOT_DEBOUNCE_MS);
2353
- timer.unref?.();
2354
- workspaceSnapshotTimer = timer;
2355
- };
2356
- const flushWorkspaceSnapshot = () => {
2357
- if (workspaceSnapshotTimer !== null) {
2358
- clearTimeout(workspaceSnapshotTimer);
2359
- workspaceSnapshotTimer = null;
2360
- }
2361
- workspaceStore.write(registry.workspaceEntries());
2362
- };
2363
- const buildSpawnTabUrl = (cwd, shell, sessionId) => {
2364
- const url = new URL(localOrigin ?? publicOrigin ?? `http://${FRIENDLY_HOSTNAME}:${actualPort}`);
2365
- if (cwd)
2366
- url.searchParams.set("cwd", cwd);
2367
- if (shell)
2368
- url.searchParams.set("shell", shell);
2369
- if (sessionId)
2370
- url.searchParams.set(SESSION_ID_QUERY_PARAM, sessionId);
2371
- return url.toString();
2372
- };
2373
2318
  const hibernatedTabFor = (owner, windowId, sessionId) => {
2374
2319
  if (!windowId || !sessionId)
2375
2320
  return undefined;
@@ -2377,70 +2322,6 @@ export const createServer = async (options = {}) => {
2377
2322
  .find((entry) => entry.owner === owner && entry.windowId === windowId)
2378
2323
  ?.tabs.find((tab) => tab.sessionId === sessionId);
2379
2324
  };
2380
- const restoredWorkspaceKeys = new Set();
2381
- const workspaceRestoreTimers = new Map();
2382
- const scheduleWorkspaceRestore = (owner, windowId) => {
2383
- if (!windowId)
2384
- return;
2385
- const key = `${owner ?? ""}\u0000${windowId}`;
2386
- if (restoredWorkspaceKeys.has(key))
2387
- return;
2388
- const existing = workspaceRestoreTimers.get(key);
2389
- if (existing)
2390
- clearTimeout(existing);
2391
- const timer = setTimeout(() => {
2392
- workspaceRestoreTimers.delete(key);
2393
- if (restoredWorkspaceKeys.has(key))
2394
- return;
2395
- restoredWorkspaceKeys.add(key);
2396
- void restoreWorkspace(owner, windowId);
2397
- }, WORKSPACE_RESTORE_SETTLE_MS);
2398
- timer.unref?.();
2399
- workspaceRestoreTimers.set(key, timer);
2400
- };
2401
- const restoreWorkspace = async (owner, windowId) => {
2402
- if (!daemonConfigStore.getWorkspaceRestore())
2403
- return;
2404
- if (!cdpClient || !cdpClient.isConnected())
2405
- return;
2406
- const hibernatedEntry = startupHibernateEntries.find((candidate) => candidate.owner === owner && candidate.windowId === windowId);
2407
- const workspaceEntry = workspaceStore
2408
- .read()
2409
- .find((candidate) => candidate.owner === owner && candidate.windowId === windowId);
2410
- const manifest = hibernatedEntry?.tabs ?? workspaceEntry?.tabs ?? [];
2411
- if (manifest.length === 0)
2412
- return;
2413
- const urlFor = (index) => {
2414
- const tab = manifest[index];
2415
- const sessionId = "sessionId" in tab && typeof tab.sessionId === "string" ? tab.sessionId : undefined;
2416
- return buildSpawnTabUrl(tab.cwd, tab.shell, sessionId);
2417
- };
2418
- const openCount = registry.attachedClientCount(owner, windowId);
2419
- if (openCount >= manifest.length)
2420
- return;
2421
- if (openCount === 0) {
2422
- for (let index = 0; index < manifest.length; index += 1) {
2423
- await cdpClient.openBackgroundTab(urlFor(index));
2424
- }
2425
- return;
2426
- }
2427
- if (openCount === 1) {
2428
- // Browser was fully closed; repoint the lone bootstrap to the first tab.
2429
- const firstTargetId = [...wsToTargetId.entries()].find(([ws]) => {
2430
- const profile = registry.clientProfile(ws);
2431
- return profile !== null && profile.owner === owner && profile.windowId === windowId;
2432
- })?.[1];
2433
- if (firstTargetId)
2434
- await cdpClient.navigateTab(firstTargetId, urlFor(0));
2435
- for (let index = 1; index < manifest.length; index += 1) {
2436
- await cdpClient.openBackgroundTab(urlFor(index));
2437
- }
2438
- return;
2439
- }
2440
- for (let index = openCount; index < manifest.length; index += 1) {
2441
- await cdpClient.openBackgroundTab(urlFor(index));
2442
- }
2443
- };
2444
2325
  const updateCheckStore = new UpdateCheckStore({
2445
2326
  filePath: path.join(stateDirectory, UPDATE_CHECK_FILENAME),
2446
2327
  currentVersion: options.currentVersion ?? readServerVersion(),
@@ -2476,8 +2357,6 @@ export const createServer = async (options = {}) => {
2476
2357
  applyCdpPort,
2477
2358
  getGraceSeconds,
2478
2359
  applyGraceSeconds,
2479
- getWorkspaceRestore,
2480
- applyWorkspaceRestore,
2481
2360
  broadcastThemes,
2482
2361
  broadcastFonts,
2483
2362
  connectCdpNow,
@@ -2779,13 +2658,6 @@ export const createServer = async (options = {}) => {
2779
2658
  const targetId = cdpClient?.findTargetIdForToken(token);
2780
2659
  if (targetId) {
2781
2660
  wsToTargetId.set(ws, targetId);
2782
- // A desktop tab just paired with CDP — arm a one-shot restore
2783
- // for its (owner, windowId) so missing workspace tabs reopen
2784
- // once the reconnection burst settles. Phone PWA tabs never pair
2785
- // (no debug port), so restore stays scoped to the desktop.
2786
- const profile = registry.clientProfile(ws);
2787
- if (profile)
2788
- scheduleWorkspaceRestore(profile.owner, profile.windowId);
2789
2661
  }
2790
2662
  }
2791
2663
  safeSend(ws, {
@@ -2925,7 +2797,6 @@ export const createServer = async (options = {}) => {
2925
2797
  if (stopPromise)
2926
2798
  return stopPromise;
2927
2799
  stopPromise = (async () => {
2928
- flushWorkspaceSnapshot();
2929
2800
  hibernateStore.write(await registry.hibernateEntries());
2930
2801
  automationScheduler.dispose();
2931
2802
  folderWatchManager.dispose();