@monotykamary/localterm-server 2.62.3 → 2.63.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.
- package/dist/cdp/cdp-client.d.ts +10 -0
- package/dist/cdp/cdp-client.d.ts.map +1 -1
- package/dist/cdp/cdp-client.js +42 -0
- package/dist/cdp/cdp-client.js.map +1 -1
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +33 -15
- package/dist/constants.js.map +1 -1
- package/dist/daemon-config-store.d.ts +2 -0
- package/dist/daemon-config-store.d.ts.map +1 -1
- package/dist/daemon-config-store.js +15 -0
- package/dist/daemon-config-store.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +140 -5
- package/dist/index.js.map +1 -1
- package/dist/schemas.d.ts +28 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +25 -2
- package/dist/schemas.js.map +1 -1
- package/dist/session-manager.d.ts +9 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +76 -10
- package/dist/session-manager.js.map +1 -1
- package/dist/utils/create-synchronized-output-end-detector.d.ts +5 -0
- package/dist/utils/create-synchronized-output-end-detector.d.ts.map +1 -0
- package/dist/utils/create-synchronized-output-end-detector.js +18 -0
- package/dist/utils/create-synchronized-output-end-detector.js.map +1 -0
- package/dist/workspace-store.d.ts +17 -0
- package/dist/workspace-store.d.ts.map +1 -0
- package/dist/workspace-store.js +54 -0
- package/dist/workspace-store.js.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import { detectWithExplicitPort } from "./cdp/discover-explicit-endpoint.js";
|
|
|
21
21
|
import { DaemonConfigStore } from "./daemon-config-store.js";
|
|
22
22
|
import { formatCandidates, resolveCandidates, resolveCompletionContext, } from "./completion/index.js";
|
|
23
23
|
import { z } from "zod";
|
|
24
|
-
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, 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_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_READY_STATE_OPEN, AUTH_SECRET_FILENAME, AUTH_COOKIE_NAME, UPDATE_CHECK_FILENAME, } from "./constants.js";
|
|
24
|
+
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, 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_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_READY_STATE_OPEN, AUTH_SECRET_FILENAME, AUTH_COOKIE_NAME, UPDATE_CHECK_FILENAME, } from "./constants.js";
|
|
25
25
|
import { getDefaultShell, listKnownShells, resolveShellOverride } from "./default-shell.js";
|
|
26
26
|
import { resolveWindowId } from "./utils/resolve-window-id.js";
|
|
27
27
|
import { shellPathForUserShell } from "./utils/shell-path.js";
|
|
@@ -35,6 +35,7 @@ import { SessionEventManager } from "./session-event-manager.js";
|
|
|
35
35
|
import { WebhookTriggerManager } from "./webhook-trigger-manager.js";
|
|
36
36
|
import { getGitBranchInfo, getGitBranchPr, getGitDiff, getGitDiffFilePatch, getGitDiffFiles, getGitDiffSummary, } from "./git-diff.js";
|
|
37
37
|
import { HeartbeatStore } from "./heartbeat-store.js";
|
|
38
|
+
import { WorkspaceStore } from "./workspace-store.js";
|
|
38
39
|
import { createDefaultSecretBackend } from "./secret-backend.js";
|
|
39
40
|
import { SecretStore } from "./secret-store.js";
|
|
40
41
|
import { ProcessStore } from "./process-store.js";
|
|
@@ -187,7 +188,7 @@ const normalizeMouseAction = (input) => {
|
|
|
187
188
|
};
|
|
188
189
|
const buildApiRoutes = (ctx) => {
|
|
189
190
|
const api = new Hono();
|
|
190
|
-
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;
|
|
191
|
+
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;
|
|
191
192
|
// Headless SGR-1006 fallback for `mouse` when no CDP tab is reachable:
|
|
192
193
|
// encode the gesture as SGR bytes and write them straight to the PTY. Closes
|
|
193
194
|
// over `registry` so the automation layer stays CDP-agnostic. Coords arrive
|
|
@@ -1550,6 +1551,7 @@ const buildApiRoutes = (ctx) => {
|
|
|
1550
1551
|
api.get("/config", (context) => context.json({
|
|
1551
1552
|
cdpPort: getCdpPort(),
|
|
1552
1553
|
graceSeconds: getGraceSeconds(),
|
|
1554
|
+
workspaceRestore: getWorkspaceRestore(),
|
|
1553
1555
|
defaultShell: getDefaultShell(),
|
|
1554
1556
|
shells: listKnownShells(),
|
|
1555
1557
|
}));
|
|
@@ -1562,6 +1564,9 @@ const buildApiRoutes = (ctx) => {
|
|
|
1562
1564
|
graceSeconds: parsed.data.graceSeconds === undefined
|
|
1563
1565
|
? getGraceSeconds()
|
|
1564
1566
|
: applyGraceSeconds(parsed.data.graceSeconds),
|
|
1567
|
+
workspaceRestore: parsed.data.workspaceRestore === undefined
|
|
1568
|
+
? getWorkspaceRestore()
|
|
1569
|
+
: applyWorkspaceRestore(parsed.data.workspaceRestore),
|
|
1565
1570
|
defaultShell: getDefaultShell(),
|
|
1566
1571
|
shells: listKnownShells(),
|
|
1567
1572
|
});
|
|
@@ -1632,8 +1637,17 @@ export const createServer = async (options = {}) => {
|
|
|
1632
1637
|
sendControl: safeSend,
|
|
1633
1638
|
hooks: {
|
|
1634
1639
|
onOutputActivity: () => caffeinateManager.noteOutputActivity(),
|
|
1635
|
-
onSessionActivity: () =>
|
|
1636
|
-
|
|
1640
|
+
onSessionActivity: () => {
|
|
1641
|
+
caffeinateManager.pokeAuto();
|
|
1642
|
+
scheduleWorkspaceSnapshot();
|
|
1643
|
+
},
|
|
1644
|
+
onSessionEvent: (event, cwd) => {
|
|
1645
|
+
sessionEventManager.onSessionEvent(event, cwd);
|
|
1646
|
+
// A cwd change reshapes the workspace manifest (the tab's respawn cwd
|
|
1647
|
+
// moves), so re-snapshot alongside the attach/detach-driven snapshot.
|
|
1648
|
+
if (event === "cwd")
|
|
1649
|
+
scheduleWorkspaceSnapshot();
|
|
1650
|
+
},
|
|
1637
1651
|
onAutomationExit: (automationId, runId, exitCode, log) => {
|
|
1638
1652
|
automationStore.updateRun(automationId, runId, {
|
|
1639
1653
|
status: exitCode === 0 ? "completed" : "failed",
|
|
@@ -1715,6 +1729,7 @@ export const createServer = async (options = {}) => {
|
|
|
1715
1729
|
const caffeinateController = options.caffeinateController ?? new CaffeinateController();
|
|
1716
1730
|
const caffeinatePreferencesStore = new CaffeinatePreferencesStore(path.join(stateDirectory, "caffeinate.json"));
|
|
1717
1731
|
const worktreeConfigStore = new WorktreeConfigStore(stateDirectory);
|
|
1732
|
+
const workspaceStore = new WorkspaceStore(path.join(stateDirectory, WORKSPACE_FILENAME));
|
|
1718
1733
|
// Per-process secret injection: a backend (macOS Keychain on darwin) holds
|
|
1719
1734
|
// secret values; a secret is an identity + the env var it exports
|
|
1720
1735
|
// (~/.localterm/secrets.json, names + env var only — never values), and a
|
|
@@ -2217,6 +2232,11 @@ export const createServer = async (options = {}) => {
|
|
|
2217
2232
|
registry.rearmGrace();
|
|
2218
2233
|
return next;
|
|
2219
2234
|
};
|
|
2235
|
+
// Persist + read the workspace-restore toggle. Read at restore time, so a
|
|
2236
|
+
// `PUT /api/config` change takes effect on the next start (the restore runs
|
|
2237
|
+
// once at startup, not live-reactively).
|
|
2238
|
+
const getWorkspaceRestore = () => daemonConfigStore.getWorkspaceRestore();
|
|
2239
|
+
const applyWorkspaceRestore = (enabled) => daemonConfigStore.setWorkspaceRestore(enabled);
|
|
2220
2240
|
// Explicit "Connect now" (Settings → Automation browser → Connect): drop any
|
|
2221
2241
|
// live socket and await a fresh connect so the caller learns the outcome —
|
|
2222
2242
|
// connected + which browser, or the error that explains a failure (e.g. a
|
|
@@ -2241,6 +2261,110 @@ export const createServer = async (options = {}) => {
|
|
|
2241
2261
|
};
|
|
2242
2262
|
}
|
|
2243
2263
|
};
|
|
2264
|
+
// Workspace restore: reopen the user's last tabs via CDP on start (a
|
|
2265
|
+
// tmux-resurrect/herdr-style restore of the *layout*; the shells themselves
|
|
2266
|
+
// don't survive a stop). The manifest is snapshotted to disk — debounced on
|
|
2267
|
+
// attach/detach churn (scheduleWorkspaceSnapshot, hooked into onSessionActivity)
|
|
2268
|
+
// and flushed on graceful stop — so a restart, graceful or a crash, can reopen
|
|
2269
|
+
// the same tabs. Triggered once per (owner, windowId) after the first desktop
|
|
2270
|
+
// tab pairs with CDP, on a quiet window so surviving tabs (a daemon restart
|
|
2271
|
+
// with the browser left open) reattach and are counted before the deficit is
|
|
2272
|
+
// opened. Excludes automation-run tabs and dormant shells (filtered in
|
|
2273
|
+
// SessionManager.workspaceEntries), so only tabs that were actively open
|
|
2274
|
+
// come back. Opt out via Settings → Sessions (config.json workspaceRestore).
|
|
2275
|
+
let workspaceSnapshotTimer = null;
|
|
2276
|
+
const scheduleWorkspaceSnapshot = () => {
|
|
2277
|
+
if (workspaceSnapshotTimer !== null)
|
|
2278
|
+
clearTimeout(workspaceSnapshotTimer);
|
|
2279
|
+
const timer = setTimeout(() => {
|
|
2280
|
+
workspaceSnapshotTimer = null;
|
|
2281
|
+
workspaceStore.write(registry.workspaceEntries());
|
|
2282
|
+
}, WORKSPACE_SNAPSHOT_DEBOUNCE_MS);
|
|
2283
|
+
timer.unref?.();
|
|
2284
|
+
workspaceSnapshotTimer = timer;
|
|
2285
|
+
};
|
|
2286
|
+
const flushWorkspaceSnapshot = () => {
|
|
2287
|
+
if (workspaceSnapshotTimer !== null) {
|
|
2288
|
+
clearTimeout(workspaceSnapshotTimer);
|
|
2289
|
+
workspaceSnapshotTimer = null;
|
|
2290
|
+
}
|
|
2291
|
+
workspaceStore.write(registry.workspaceEntries());
|
|
2292
|
+
};
|
|
2293
|
+
const buildSpawnTabUrl = (cwd, shell) => {
|
|
2294
|
+
const url = new URL(localOrigin ?? publicOrigin ?? `http://${FRIENDLY_HOSTNAME}:${actualPort}`);
|
|
2295
|
+
if (cwd)
|
|
2296
|
+
url.searchParams.set("cwd", cwd);
|
|
2297
|
+
if (shell)
|
|
2298
|
+
url.searchParams.set("shell", shell);
|
|
2299
|
+
return url.toString();
|
|
2300
|
+
};
|
|
2301
|
+
const restoredWorkspaceKeys = new Set();
|
|
2302
|
+
const workspaceRestoreTimers = new Map();
|
|
2303
|
+
const scheduleWorkspaceRestore = (owner, windowId) => {
|
|
2304
|
+
if (!windowId)
|
|
2305
|
+
return;
|
|
2306
|
+
const key = `${owner ?? ""}\u0000${windowId}`;
|
|
2307
|
+
if (restoredWorkspaceKeys.has(key))
|
|
2308
|
+
return;
|
|
2309
|
+
const existing = workspaceRestoreTimers.get(key);
|
|
2310
|
+
if (existing)
|
|
2311
|
+
clearTimeout(existing);
|
|
2312
|
+
const timer = setTimeout(() => {
|
|
2313
|
+
workspaceRestoreTimers.delete(key);
|
|
2314
|
+
if (restoredWorkspaceKeys.has(key))
|
|
2315
|
+
return;
|
|
2316
|
+
restoredWorkspaceKeys.add(key);
|
|
2317
|
+
void restoreWorkspace(owner, windowId);
|
|
2318
|
+
}, WORKSPACE_RESTORE_SETTLE_MS);
|
|
2319
|
+
timer.unref?.();
|
|
2320
|
+
workspaceRestoreTimers.set(key, timer);
|
|
2321
|
+
};
|
|
2322
|
+
const restoreWorkspace = async (owner, windowId) => {
|
|
2323
|
+
if (!daemonConfigStore.getWorkspaceRestore())
|
|
2324
|
+
return;
|
|
2325
|
+
if (!cdpClient || !cdpClient.isConnected())
|
|
2326
|
+
return;
|
|
2327
|
+
const entry = workspaceStore
|
|
2328
|
+
.read()
|
|
2329
|
+
.find((candidate) => candidate.owner === owner && candidate.windowId === windowId);
|
|
2330
|
+
if (!entry || entry.tabs.length === 0)
|
|
2331
|
+
return;
|
|
2332
|
+
const manifest = entry.tabs;
|
|
2333
|
+
const openCount = registry.attachedClientCount(owner, windowId);
|
|
2334
|
+
if (openCount >= manifest.length)
|
|
2335
|
+
return;
|
|
2336
|
+
if (openCount === 0) {
|
|
2337
|
+
for (const tab of manifest) {
|
|
2338
|
+
await cdpClient.openBackgroundTab(buildSpawnTabUrl(tab.cwd, tab.shell));
|
|
2339
|
+
}
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
if (openCount === 1) {
|
|
2343
|
+
// Browser was fully closed; the lone bootstrap tab is repointed to the
|
|
2344
|
+
// first restored shell (navigate-the-bootstrap) so the reopen lands
|
|
2345
|
+
// exactly N tabs in the manifest's cwds instead of N−1 + one stray in
|
|
2346
|
+
// the default directory.
|
|
2347
|
+
const firstTargetId = [...wsToTargetId.entries()].find(([ws]) => {
|
|
2348
|
+
const profile = registry.clientProfile(ws);
|
|
2349
|
+
return profile !== null && profile.owner === owner && profile.windowId === windowId;
|
|
2350
|
+
})?.[1];
|
|
2351
|
+
if (firstTargetId) {
|
|
2352
|
+
await cdpClient.navigateTab(firstTargetId, buildSpawnTabUrl(manifest[0].cwd, manifest[0].shell));
|
|
2353
|
+
}
|
|
2354
|
+
for (let index = 1; index < manifest.length; index++) {
|
|
2355
|
+
await cdpClient.openBackgroundTab(buildSpawnTabUrl(manifest[index].cwd, manifest[index].shell));
|
|
2356
|
+
}
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
// Partial survival (a daemon restart with some tabs left open): surviving
|
|
2360
|
+
// tabs self-healed to their live cwds, so only the deficit is opened. Which
|
|
2361
|
+
// manifest cwds the survivors cover can't be matched (their URLs carry the
|
|
2362
|
+
// spawn cwd, stale after a `cd`), so the tail is opened as a best-effort
|
|
2363
|
+
// placement — the count is exact, the cwd placement is approximate.
|
|
2364
|
+
for (let index = openCount; index < manifest.length; index++) {
|
|
2365
|
+
await cdpClient.openBackgroundTab(buildSpawnTabUrl(manifest[index].cwd, manifest[index].shell));
|
|
2366
|
+
}
|
|
2367
|
+
};
|
|
2244
2368
|
const updateCheckStore = new UpdateCheckStore({
|
|
2245
2369
|
filePath: path.join(stateDirectory, UPDATE_CHECK_FILENAME),
|
|
2246
2370
|
currentVersion: options.currentVersion ?? readServerVersion(),
|
|
@@ -2276,6 +2400,8 @@ export const createServer = async (options = {}) => {
|
|
|
2276
2400
|
applyCdpPort,
|
|
2277
2401
|
getGraceSeconds,
|
|
2278
2402
|
applyGraceSeconds,
|
|
2403
|
+
getWorkspaceRestore,
|
|
2404
|
+
applyWorkspaceRestore,
|
|
2279
2405
|
broadcastThemes,
|
|
2280
2406
|
broadcastFonts,
|
|
2281
2407
|
connectCdpNow,
|
|
@@ -2557,8 +2683,16 @@ export const createServer = async (options = {}) => {
|
|
|
2557
2683
|
const token = parsed.data.token;
|
|
2558
2684
|
if (token !== null) {
|
|
2559
2685
|
const targetId = cdpClient?.findTargetIdForToken(token);
|
|
2560
|
-
if (targetId)
|
|
2686
|
+
if (targetId) {
|
|
2561
2687
|
wsToTargetId.set(ws, targetId);
|
|
2688
|
+
// A desktop tab just paired with CDP — arm a one-shot restore
|
|
2689
|
+
// for its (owner, windowId) so missing workspace tabs reopen
|
|
2690
|
+
// once the reconnection burst settles. Phone PWA tabs never pair
|
|
2691
|
+
// (no debug port), so restore stays scoped to the desktop.
|
|
2692
|
+
const profile = registry.clientProfile(ws);
|
|
2693
|
+
if (profile)
|
|
2694
|
+
scheduleWorkspaceRestore(profile.owner, profile.windowId);
|
|
2695
|
+
}
|
|
2562
2696
|
}
|
|
2563
2697
|
safeSend(ws, {
|
|
2564
2698
|
type: "cdp-controlled",
|
|
@@ -2693,6 +2827,7 @@ export const createServer = async (options = {}) => {
|
|
|
2693
2827
|
});
|
|
2694
2828
|
}
|
|
2695
2829
|
const stop = async () => {
|
|
2830
|
+
flushWorkspaceSnapshot();
|
|
2696
2831
|
automationScheduler.dispose();
|
|
2697
2832
|
folderWatchManager.dispose();
|
|
2698
2833
|
automationGitWatcher.dispose();
|