@platless/axom-cli 0.5.9 → 0.6.52
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/README.md +25 -6
- package/dist/axom-patch/cli/README.md +129 -12
- package/dist/axom-patch/cli/account-limits/generate.py +15 -12
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
- package/dist/axom-patch/cli/claude-account-limits.json +9 -9
- package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
- package/dist/axom-patch/cli/codex-account-limits.json +11 -11
- package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
- package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
- package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
- package/dist/axom-patch/cli/nightly-task.md +55 -13
- package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
- package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
- package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
- package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1687 -0
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +277 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
- package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
- package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
- package/dist/axom-patch/cli/qoder-steering.md +20 -25
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
- package/dist/axom-patch/cli/runtime.json +19 -19
- package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
- package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
- package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
- package/dist/axom-patch/cli/zcode-env-account.json +58 -0
- package/dist/axom-patch/cli/zcode-models.json +9 -10
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -11
- package/dist/cli.js +11 -45
- package/dist/commands/auth.js +4 -1
- package/dist/commands/knowledge.js +47 -0
- package/dist/commands/update.js +126 -3
- package/dist/commands/workstation.js +101 -164
- package/dist/commands/workstationRegistrations.js +116 -0
- package/dist/credentialBroker.js +13 -2
- package/dist/credentials.js +16 -0
- package/dist/install/claude/commands.js +15 -15
- package/dist/keepers/terminal/keeper.mjs +1089 -673
- package/dist/keepers/terminal/manifest.json +4 -4
- package/dist/mcp.js +1 -1
- package/dist/native/darwin-x64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +113 -102
- package/dist/workstation/acp/acpClient.js +26 -0
- package/dist/workstation/acp/builds/install/npm.js +99 -10
- package/dist/workstation/acp/initializeResponse.js +6 -1
- package/dist/workstation/acp/managedPatches.js +189 -25
- package/dist/workstation/acp/managedRuntime.js +314 -0
- package/dist/workstation/acp/outbox.js +12 -5
- package/dist/workstation/acp/patchAssets.js +40 -0
- package/dist/workstation/acp/permission.js +2 -2
- package/dist/workstation/acp/processor/checkpoint.js +26 -34
- package/dist/workstation/acp/processor/control.js +7 -80
- package/dist/workstation/acp/processor/jsonrpc.js +8 -0
- package/dist/workstation/acp/processor/runtime.js +5 -81
- package/dist/workstation/acp/processor/session.js +54 -82
- package/dist/workstation/acp/processor/state.js +7 -13
- package/dist/workstation/acp/processor/step.js +3 -20
- package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
- package/dist/workstation/acp/processor/transport/local.js +19 -3
- package/dist/workstation/acp/processor/updates.js +156 -105
- package/dist/workstation/acp/processorSession.js +20 -173
- package/dist/workstation/acp/registry/catalog.js +65 -39
- package/dist/workstation/acp/registry/launch.js +16 -1
- package/dist/workstation/acp/switching.js +0 -3
- package/dist/workstation/acp/test/fakeAgent.js +13 -0
- package/dist/workstation/acp/test/keeperWorld.js +32 -3
- package/dist/workstation/acp/test/raw/sources.js +16 -130
- package/dist/workstation/acp/test/realAdapter.js +1 -47
- package/dist/workstation/acp/test/scriptAgent.js +11 -3
- package/dist/workstation/agentFlow.js +1 -1
- package/dist/workstation/agentSessions.js +114 -166
- package/dist/workstation/channels/agent.js +9 -1
- package/dist/workstation/channels/computer.js +27 -4
- package/dist/workstation/channels/management.js +130 -15
- package/dist/workstation/channels/url.js +1 -1
- package/dist/workstation/config.js +33 -9
- package/dist/workstation/connection/encryptedChannel.js +4 -1
- package/dist/workstation/control/bindingStore.js +204 -0
- package/dist/workstation/control/commands.js +125 -0
- package/dist/workstation/control/daemonIdentity.js +143 -0
- package/dist/workstation/control/deviceCommands.js +191 -0
- package/dist/workstation/control/deviceProtocol.js +68 -0
- package/dist/workstation/control/enrollment.js +106 -0
- package/dist/workstation/control/inspect.js +71 -0
- package/dist/workstation/control/locks.js +110 -0
- package/dist/workstation/control/protocol.js +40 -0
- package/dist/workstation/control/restartGuard.js +46 -0
- package/dist/workstation/control/rootFormat.js +16 -0
- package/dist/workstation/control/runtimeClient.js +152 -0
- package/dist/workstation/control/runtimeHost.js +235 -0
- package/dist/workstation/control/serverIdentity.js +35 -0
- package/dist/workstation/control/updateTransition.js +91 -0
- package/dist/workstation/daemon.js +266 -264
- package/dist/workstation/daemonctl.js +226 -25
- package/dist/workstation/desktopControl/audio.js +99 -0
- package/dist/workstation/desktopControl/automationDevice.js +219 -0
- package/dist/workstation/desktopControl/automationJson.js +74 -0
- package/dist/workstation/desktopControl/automationLocalState.js +23 -0
- package/dist/workstation/desktopControl/automationProtocol.js +253 -0
- package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
- package/dist/workstation/desktopControl/automationSession.js +81 -0
- package/dist/workstation/desktopControl/automationTransport.js +138 -0
- package/dist/workstation/desktopControl/capabilities.js +93 -0
- package/dist/workstation/desktopControl/captureModes.js +9 -0
- package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
- package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
- package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
- package/dist/workstation/desktopControl/deviceSockets.js +246 -0
- package/dist/workstation/desktopControl/helperProcess.js +602 -0
- package/dist/workstation/desktopControl/hostGrant.js +65 -0
- package/dist/workstation/desktopControl/lobby.js +131 -0
- package/dist/workstation/desktopControl/macProvider.js +156 -0
- package/dist/workstation/desktopControl/media.js +331 -0
- package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
- package/dist/workstation/desktopControl/nativeProvider.js +1044 -0
- package/dist/workstation/desktopControl/networkEvidence.js +43 -0
- package/dist/workstation/desktopControl/overview.js +58 -0
- package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
- package/dist/workstation/desktopControl/protocol.js +311 -0
- package/dist/workstation/desktopControl/provider.js +1 -0
- package/dist/workstation/desktopControl/quicMedia.js +240 -0
- package/dist/workstation/desktopControl/rateController.js +226 -0
- package/dist/workstation/desktopControl/supervisor.js +1774 -0
- package/dist/workstation/desktopControl/transport.js +27 -0
- package/dist/workstation/desktopControl/wallpaper.js +98 -0
- package/dist/workstation/exec/policy.js +5 -0
- package/dist/workstation/files/client.js +10 -1
- package/dist/workstation/files/credentials.js +142 -0
- package/dist/workstation/files/host.js +9 -2
- package/dist/workstation/files/text.js +42 -8
- package/dist/workstation/journal.js +5 -2
- package/dist/workstation/keepers/acp/client.js +38 -2
- package/dist/workstation/keepers/kit/buildVersion.js +35 -11
- package/dist/workstation/keepers/kit/bundle.js +21 -17
- package/dist/workstation/keepers/kit/client.js +2 -2
- package/dist/workstation/keepers/kit/launcher.js +123 -7
- package/dist/workstation/keepers/kit/paths.js +2 -1
- package/dist/workstation/keepers/legacy.js +16 -2
- package/dist/workstation/keepers/policy.js +5 -34
- package/dist/workstation/keepers/terminal/client.js +115 -23
- package/dist/workstation/keepers/terminal/exec.js +6 -3
- package/dist/workstation/keepers/terminal/frames.js +73 -0
- package/dist/workstation/keepers/terminal/generations.js +5 -8
- package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
- package/dist/workstation/keepers/terminal/input.js +146 -0
- package/dist/workstation/keepers/terminal/keeper.js +22 -8
- package/dist/workstation/keepers/terminal/main.js +1 -1
- package/dist/workstation/keepers/terminal/metadata.js +138 -0
- package/dist/workstation/keepers/terminal/protocol.js +4 -28
- package/dist/workstation/keepers/terminal/pty.js +1 -330
- package/dist/workstation/keepers/terminal/screen.js +246 -0
- package/dist/workstation/keepers/terminal/sessions.js +526 -0
- package/dist/workstation/managedInstall.js +57 -0
- package/dist/workstation/oauth/deviceAuthorization.js +31 -17
- package/dist/workstation/protocol/agent.js +2 -15
- package/dist/workstation/protocol/management.js +113 -5
- package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
- package/dist/workstation/rd/artifact.js +120 -0
- package/dist/workstation/rd/control.js +117 -0
- package/dist/workstation/rd/display.js +29 -0
- package/dist/workstation/rd/host.js +213 -0
- package/dist/workstation/rd/migrationReadiness.js +46 -0
- package/dist/workstation/rd/provider.js +256 -0
- package/dist/workstation/rd/sessions.js +391 -0
- package/dist/workstation/rd/state.js +57 -0
- package/dist/workstation/rd/update.js +168 -0
- package/dist/workstation/rd/upkeep.js +80 -0
- package/dist/workstation/register.js +27 -37
- package/dist/workstation/registrations/enrollment.js +250 -0
- package/dist/workstation/registrations/enrollmentPending.js +361 -0
- package/dist/workstation/registrations/migration.js +356 -0
- package/dist/workstation/registrations/model.js +34 -0
- package/dist/workstation/registrations/removal.js +222 -0
- package/dist/workstation/registrations/store.js +737 -0
- package/dist/workstation/runtime/desktopBinding.js +20 -0
- package/dist/workstation/runtime/deviceDesktop.js +193 -0
- package/dist/workstation/runtime/deviceRuntime.js +568 -0
- package/dist/workstation/runtime/deviceServices.js +117 -0
- package/dist/workstation/runtime/events.js +20 -0
- package/dist/workstation/runtime/ownership.js +100 -0
- package/dist/workstation/runtime/registrationRuntime.js +575 -0
- package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
- package/dist/workstation/runtime/removalActivity.js +184 -0
- package/dist/workstation/service/installer.js +197 -14
- package/dist/workstation/service/launchd.js +2 -0
- package/dist/workstation/service/systemd.js +1 -0
- package/dist/workstation/storageMetrics.js +118 -0
- package/dist/workstation/systemMetrics.js +47 -92
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- package/dist/workstation/tools/client.js +8 -27
- package/dist/workstation/tools/info.js +6 -13
- package/dist/workstation/tools/terminalRouter.js +17 -172
- package/dist/workstation/tools/validation.js +55 -1
- package/dist/workstation/update/archiveEvidence.js +206 -0
- package/dist/workstation/update/artifact.js +2 -1
- package/dist/workstation/update/coordinator.js +34 -13
- package/dist/workstation/update/deviceCompatibility.js +119 -0
- package/dist/workstation/update/deviceCoordinator.js +369 -0
- package/dist/workstation/update/deviceHealth.js +104 -0
- package/dist/workstation/update/deviceHealthProtocol.js +26 -0
- package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
- package/dist/workstation/update/deviceRollback.js +78 -0
- package/dist/workstation/update/globalInstall.js +186 -0
- package/dist/workstation/update/legacyBridge.js +414 -0
- package/dist/workstation/update/legacyOwner.js +39 -0
- package/dist/workstation/update/npmInstaller.js +51 -25
- package/dist/workstation/update/privateDurability.js +61 -0
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/recoveryPlan.js +124 -0
- package/dist/workstation/update/runUpdate.js +10 -3
- package/dist/workstation/update/taskRegistry.js +161 -3
- package/dist/workstation/update/watchdog.js +29806 -333
- package/dist/workstation/update/watchdogProcess.js +72 -0
- package/package.json +11 -7
- package/skills/axom/SKILL.md +11 -11
- package/skills/axom-definition/SKILL.md +3 -3
- package/skills/axom-platform/SKILL.md +3 -3
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
- package/dist/axom-patch/cli/qoder-steering.json +0 -52
- package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
- package/dist/commands/guide.js +0 -38
- package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
- package/dist/runtime-deps/qoder-sdk/package.json +0 -8
- package/dist/workstation/acp/delegationMcp.js +0 -24
- package/dist/workstation/acp/processor/sideChannel.js +0 -80
- package/dist/workstation/acp/qoderStartup.js +0 -45
- package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
- package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
- package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
- package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
- package/dist/workstation/qoderAdapter/contract.js +0 -78
- package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
- package/dist/workstation/qoderAdapter/history.js +0 -85
- package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
- package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
- package/dist/workstation/qoderAdapter/main.js +0 -95
- package/dist/workstation/qoderAdapter/mapper.js +0 -117
- package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
- package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
- package/dist/workstation/qoderAdapter/permissions.js +0 -61
- package/dist/workstation/qoderAdapter/profile.js +0 -93
- package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
- package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
- package/dist/workstation/qoderAdapter/session.js +0 -319
- package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
- package/dist/workstation/qoderAdapter/subwork.js +0 -85
- package/dist/workstation/qoderAdapter/taskControl.js +0 -38
- package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
- package/dist/workstation/qoderAdapter/usage.js +0 -37
- /package/dist/workstation/{qoderAdapter/types.js → desktopControl/automationPreparation.js} +0 -0
|
@@ -44,7 +44,7 @@ const MAX_READ_RECORDS = 1024;
|
|
|
44
44
|
const MAX_READ_BYTES = 1536 * 1024;
|
|
45
45
|
const MAX_GRACE_MS = 10 * 60 * 1000;
|
|
46
46
|
const EXEC_ID = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
47
|
-
const START_KEYS = ['execId', 'argv', 'cwd', 'env', 'deadlineEpochMs', 'killGraceMs', 'retainHeadBytes', 'maxUncommittedBytes', 'onKeeperLoss', 'label', 'jobId'];
|
|
47
|
+
const START_KEYS = ['execId', 'argv', 'cwd', 'env', 'deadlineEpochMs', 'killGraceMs', 'retainHeadBytes', 'maxUncommittedBytes', 'onKeeperLoss', 'label', 'jobId', 'registrationId'];
|
|
48
48
|
/**
|
|
49
49
|
* Whether a kept record stays in keep.jsonl once the daemon committed through `committed`. The head reserve (kept `out`/`err`
|
|
50
50
|
* records: the first `retainHeadBytes` of each stream) and the spawn record survive every non-final commit: the daemon shapes the
|
|
@@ -305,11 +305,14 @@ export class ExecHost {
|
|
|
305
305
|
bad('Invalid jobId');
|
|
306
306
|
if (this.runningCount() >= (this.options.runningLimit ?? EXEC_RUNNING_LIMIT))
|
|
307
307
|
throw new KeeperMethodError(ExecErrorCode.limit, 'Too many running commands in keeper-terminal');
|
|
308
|
+
const registrationId = params.registrationId;
|
|
309
|
+
if (registrationId !== undefined && (typeof registrationId !== 'string' || !registrationId || registrationId.length > 128 || /[\x00-\x1f]/.test(registrationId)))
|
|
310
|
+
bad('Invalid registrationId');
|
|
308
311
|
const meta = {
|
|
309
312
|
v: 1, execId, label: params.label.slice(0, EXEC_LABEL_CHARS), ...(jobId ? { jobId: jobId } : {}),
|
|
310
313
|
argv: argv, cwd, envKeys: Object.keys(env).sort(),
|
|
311
314
|
deadlineEpochMs: typeof deadline === 'number' ? deadline : null, killGraceMs, retainHeadBytes, maxUncommittedBytes, onKeeperLoss: onKeeperLoss,
|
|
312
|
-
createdAtMs: this.now(), owner: this.options.owner, spawn: null, exit: null, logReclaimed: null,
|
|
315
|
+
createdAtMs: this.now(), owner: this.options.owner, ...(typeof registrationId === 'string' ? { registrationId } : {}), spawn: null, exit: null, logReclaimed: null,
|
|
313
316
|
};
|
|
314
317
|
// Synchronously up to the spawn: a concurrent start of the same id finds the entry and waits for it.
|
|
315
318
|
let entry;
|
|
@@ -811,7 +814,7 @@ export class ExecHost {
|
|
|
811
814
|
describe(entry) {
|
|
812
815
|
const { meta } = entry;
|
|
813
816
|
return {
|
|
814
|
-
execId: meta.execId, label: meta.label, ...(meta.jobId ? { jobId: meta.jobId } : {}), state: this.stateOf(entry),
|
|
817
|
+
execId: meta.execId, label: meta.label, ...(meta.registrationId ? { registrationId: meta.registrationId } : {}), ...(meta.jobId ? { jobId: meta.jobId } : {}), state: this.stateOf(entry),
|
|
815
818
|
pid: meta.spawn?.pid ?? null, pgid: meta.spawn?.pgid ?? null, startTime: meta.spawn?.startTime ?? null,
|
|
816
819
|
createdAtMs: meta.createdAtMs, deadlineEpochMs: meta.deadlineEpochMs, onKeeperLoss: meta.onKeeperLoss, exit: meta.exit,
|
|
817
820
|
head: this.headOf(entry), committed: entry.committed, dropped: entry.dropped,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { KeeperMethodError } from '../kit/protocol.js';
|
|
2
|
+
import { TERMINAL_GRID_LIMITS as limits } from './gridProtocol.js';
|
|
3
|
+
/** Complete immutable snapshots, with deltas computed against any retained base. */
|
|
4
|
+
export class TerminalFrames {
|
|
5
|
+
cache = [];
|
|
6
|
+
bytes = 0;
|
|
7
|
+
/** A failed/oversized capture leaves both the revision and recovery cache untouched. */
|
|
8
|
+
publish(snapshot, session, geometry) {
|
|
9
|
+
validate(snapshot, session);
|
|
10
|
+
const previous = this.cache.at(-1)?.frame;
|
|
11
|
+
const sameSession = previous?.incarnation === session.incarnation && previous.id === session.id;
|
|
12
|
+
const nextGeometry = geometry ?? (sameSession ? previous.geometry + Number(previous.cols !== snapshot.cols || previous.rows !== snapshot.rows) : 1);
|
|
13
|
+
if (!Number.isSafeInteger(nextGeometry) || nextGeometry < 1 || (sameSession && nextGeometry < previous.geometry))
|
|
14
|
+
throw new Error('Invalid terminal geometry');
|
|
15
|
+
const next = structuredClone({
|
|
16
|
+
...snapshot, schema: 1, id: session.id, incarnation: session.incarnation,
|
|
17
|
+
revision: sameSession ? previous.revision + 1 : 1, baseRevision: 0,
|
|
18
|
+
geometry: nextGeometry, full: true, session,
|
|
19
|
+
});
|
|
20
|
+
if (sameSession && equalContent(previous, next))
|
|
21
|
+
return;
|
|
22
|
+
const bytes = resultBytes(next);
|
|
23
|
+
if (!sameSession) {
|
|
24
|
+
this.cache = [];
|
|
25
|
+
this.bytes = 0;
|
|
26
|
+
}
|
|
27
|
+
this.cache.push({ frame: next, bytes });
|
|
28
|
+
this.bytes += bytes;
|
|
29
|
+
while (this.cache.length > limits.maxCachedFrames || this.bytes > limits.maxFrameCacheBytes)
|
|
30
|
+
this.bytes -= this.cache.shift().bytes;
|
|
31
|
+
}
|
|
32
|
+
poll(after) {
|
|
33
|
+
const current = this.cache.at(-1)?.frame;
|
|
34
|
+
if (!current)
|
|
35
|
+
throw new Error('Terminal frame has not been captured');
|
|
36
|
+
if (after === current.revision)
|
|
37
|
+
return structuredClone({ ...current, full: false, baseRevision: after, styles: [], changedRows: [] });
|
|
38
|
+
const base = after === 0 ? undefined : this.cache.find(entry => entry.frame.revision === after)?.frame;
|
|
39
|
+
if (!base || base.geometry !== current.geometry || base.buffer !== current.buffer || base.incarnation !== current.incarnation)
|
|
40
|
+
return structuredClone(current);
|
|
41
|
+
const changedRows = current.changedRows.filter((row, index) => semanticRow(row, current) !== semanticRow(base.changedRows[index], base));
|
|
42
|
+
const delta = { ...current, full: false, baseRevision: after, changedRows };
|
|
43
|
+
resultBytes(delta);
|
|
44
|
+
return structuredClone(delta);
|
|
45
|
+
}
|
|
46
|
+
cacheStats() { return { frames: this.cache.length, bytes: this.bytes }; }
|
|
47
|
+
}
|
|
48
|
+
function semanticRow(row, frame) {
|
|
49
|
+
return JSON.stringify([row.wrapped ?? false, row.runs.flatMap(run => run.cells.map(cell => [cell.text, cell.width, frame.styles[run.style]]))]);
|
|
50
|
+
}
|
|
51
|
+
function equalContent(a, b) {
|
|
52
|
+
const header = (frame) => JSON.stringify([
|
|
53
|
+
frame.geometry, frame.cols, frame.rows, frame.buffer, frame.viewport, frame.cursor, frame.modes, frame.session,
|
|
54
|
+
]);
|
|
55
|
+
return header(a) === header(b) && a.changedRows.every((row, index) => semanticRow(row, a) === semanticRow(b.changedRows[index], b));
|
|
56
|
+
}
|
|
57
|
+
function resultBytes(frame) {
|
|
58
|
+
const bytes = Buffer.byteLength(JSON.stringify(frame));
|
|
59
|
+
// Leave room for the keeper response envelope and the tool response wrapper.
|
|
60
|
+
if (bytes > limits.maxResultBytes - 1024)
|
|
61
|
+
throw new KeeperMethodError('pty_snapshot_too_large', 'Terminal frame exceeds result limit; resize to a smaller viewport');
|
|
62
|
+
return bytes;
|
|
63
|
+
}
|
|
64
|
+
function validate(snapshot, session) {
|
|
65
|
+
if (snapshot.cols < limits.minCols || snapshot.cols > limits.maxCols || snapshot.rows < limits.minRows || snapshot.rows > limits.maxRows
|
|
66
|
+
|| snapshot.cols !== session.cols || snapshot.rows !== session.rows || snapshot.changedRows.length !== snapshot.rows)
|
|
67
|
+
throw new Error('Terminal snapshot must contain a complete matching grid');
|
|
68
|
+
for (const [index, row] of snapshot.changedRows.entries()) {
|
|
69
|
+
if (row.index !== index || row.runs.reduce((count, run) => count + run.cells.length, 0) !== snapshot.cols
|
|
70
|
+
|| row.runs.some(run => !snapshot.styles[run.style] || run.cells.some(cell => typeof cell.text !== 'string' || ![0, 1, 2].includes(cell.width))))
|
|
71
|
+
throw new Error('Terminal snapshot must contain complete rows and styles');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -2,11 +2,8 @@
|
|
|
2
2
|
// CLI upgrade an OLDER generation can still host PTYs and execs while a newer one takes new work. Nothing here starts an older
|
|
3
3
|
// generation: only the current build's client launches keeper-terminal. Exempt from the keeper-side import guard (daemon module).
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// it — spec §8.3: an available update never does anything by itself).
|
|
8
|
-
// - Everything else is routed by OWNER: a PTY id or an exec id is served by the generation that runs it, through a client bound to
|
|
9
|
-
// that generation (tools/terminalRouter.ts, exec/policy.ts).
|
|
5
|
+
// Durable exec work uses the serving picker and retains owner routing across generations.
|
|
6
|
+
// Native terminal tools bypass this selection and always use `primary` (tools/terminalRouter.ts).
|
|
10
7
|
import path from 'node:path';
|
|
11
8
|
import { readKeeperManifest } from '../kit/bundle.js';
|
|
12
9
|
import { TerminalKeeperClient } from './client.js';
|
|
@@ -93,7 +90,7 @@ export class TerminalGenerations {
|
|
|
93
90
|
}
|
|
94
91
|
return older !== undefined && older !== current ? { gen: older, client: await this.client(older) } : { gen: current, client: this.primary };
|
|
95
92
|
}
|
|
96
|
-
/**
|
|
93
|
+
/** Durable exec creation target. Native terminal creation uses primary directly. */
|
|
97
94
|
async serving() { return (await this.servingGeneration()).client; }
|
|
98
95
|
/** Every live generation with its client (the current build's only when it runs). Starts nothing. */
|
|
99
96
|
async running() {
|
|
@@ -109,9 +106,9 @@ export class TerminalGenerations {
|
|
|
109
106
|
const serving = await this.serving();
|
|
110
107
|
return (await this.running()).map((generation) => generation.client).filter((client) => client !== serving);
|
|
111
108
|
}
|
|
112
|
-
/**
|
|
109
|
+
/** Native terminal status belongs only to the current build. Starts nothing. */
|
|
113
110
|
async terminalStatus(fallback) {
|
|
114
|
-
return
|
|
111
|
+
return this.primary.terminalStatus(fallback);
|
|
115
112
|
}
|
|
116
113
|
/** Drops the connection to `gen` (it was stopped). */
|
|
117
114
|
forget(gen) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Native terminal wire contract. No runtime/platform dependencies. */
|
|
2
|
+
export const TERMINAL_GRID_SCHEMA = 1;
|
|
3
|
+
export const TERMINAL_KEEPER_PROTOCOL = 2;
|
|
4
|
+
export const TERMINAL_GRID_CAPABILITY = 'pty-grid-v1';
|
|
5
|
+
export const TERMINAL_GRID_LIMITS = {
|
|
6
|
+
minCols: 20, maxCols: 300, minRows: 5, maxRows: 100,
|
|
7
|
+
maxSessions: 8, maxAttachments: 16, leaseMs: 15_000, scrollbackLines: 10_000,
|
|
8
|
+
maxInputBytes: 64 * 1024, maxRequestBytes: 128 * 1024, maxResultBytes: 2 * 1024 * 1024,
|
|
9
|
+
maxCachedFrames: 8, maxFrameCacheBytes: 8 * 1024 * 1024,
|
|
10
|
+
};
|
|
11
|
+
export const TERMINAL_MODIFIERS = { shift: 1, alt: 2, ctrl: 4, meta: 8 };
|
|
12
|
+
export const TERMINAL_STYLE_FLAGS = {
|
|
13
|
+
bold: 1, dim: 2, italic: 4, underline: 8, blink: 16, inverse: 32, invisible: 64, strike: 128,
|
|
14
|
+
};
|
|
15
|
+
export const TERMINAL_DEFAULT_COLOR = -1;
|
|
16
|
+
export const TERMINAL_RGB_COLOR_BASE = 0x1000000;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { KeeperMethodError } from '../kit/protocol.js';
|
|
2
|
+
import { TERMINAL_GRID_LIMITS } from './gridProtocol.js';
|
|
3
|
+
const esc = '\x1b';
|
|
4
|
+
const modifiers = (mask) => 1 + (mask & 1) + (mask & 2) + (mask & 4) + (mask & 8);
|
|
5
|
+
const arrows = { 79: 'C', 80: 'D', 81: 'B', 82: 'A', 74: 'H', 77: 'F' };
|
|
6
|
+
const tilde = { 73: 2, 76: 3, 75: 5, 78: 6, 62: 15, 63: 17, 64: 18, 65: 19, 66: 20, 67: 21, 68: 23, 69: 24 };
|
|
7
|
+
const keypad = {
|
|
8
|
+
84: ['/', 'o'], 85: ['*', 'j'], 86: ['-', 'm'], 87: ['+', 'k'], 88: ['\r', 'M'],
|
|
9
|
+
89: ['1', 'q'], 90: ['2', 'r'], 91: ['3', 's'], 92: ['4', 't'], 93: ['5', 'u'], 94: ['6', 'v'],
|
|
10
|
+
95: ['7', 'w'], 96: ['8', 'x'], 97: ['9', 'y'], 98: ['0', 'p'], 99: ['.', 'n'], 103: ['=', 'X'],
|
|
11
|
+
};
|
|
12
|
+
/** One attachment's only held input is its mouse buttons; modifiers are explicit per event. */
|
|
13
|
+
export class TerminalInputEncoder {
|
|
14
|
+
buttons = new Set();
|
|
15
|
+
release() { this.buttons.clear(); }
|
|
16
|
+
encode(event, screen) {
|
|
17
|
+
switch (event.kind) {
|
|
18
|
+
case 'release':
|
|
19
|
+
this.release();
|
|
20
|
+
return '';
|
|
21
|
+
case 'text': return event.text;
|
|
22
|
+
case 'paste': return screen.modes.bracketedPaste ? `${esc}[200~${event.text}${esc}[201~` : event.text;
|
|
23
|
+
case 'key': return event.down ? encodeKey(event.usage, event.modifiers, screen.modes) : '';
|
|
24
|
+
case 'button': {
|
|
25
|
+
if (event.down)
|
|
26
|
+
this.buttons.add(event.button);
|
|
27
|
+
else
|
|
28
|
+
this.buttons.delete(event.button);
|
|
29
|
+
if (screen.modes.mouseTracking === 'none' || (!event.down && screen.modes.mouseTracking === 'x10'))
|
|
30
|
+
return '';
|
|
31
|
+
return mouse(event.button, event.x, event.y, event.modifiers, screen.modes, !event.down);
|
|
32
|
+
}
|
|
33
|
+
case 'pointer': {
|
|
34
|
+
const tracking = screen.modes.mouseTracking;
|
|
35
|
+
if (tracking !== 'any' && !(tracking === 'drag' && this.buttons.size))
|
|
36
|
+
return '';
|
|
37
|
+
return mouse(32 + (this.buttons.values().next().value ?? 3), event.x, event.y, event.modifiers, screen.modes);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function createTerminalInput(screen) {
|
|
43
|
+
const clients = new Map();
|
|
44
|
+
const encoder = (id) => { let value = clients.get(id); if (!value) {
|
|
45
|
+
value = new TerminalInputEncoder();
|
|
46
|
+
clients.set(id, value);
|
|
47
|
+
} return value; };
|
|
48
|
+
return {
|
|
49
|
+
input(event, clientId, context) { return encoder(clientId).encode(event, context); },
|
|
50
|
+
scroll(gesture, _clientId, context) {
|
|
51
|
+
if (!gesture.lines)
|
|
52
|
+
return '';
|
|
53
|
+
const state = screen.state();
|
|
54
|
+
if (context.modes.mouseTracking === 'x10')
|
|
55
|
+
return '';
|
|
56
|
+
if (context.modes.mouseTracking !== 'none')
|
|
57
|
+
return repeat(mouse(gesture.lines < 0 ? 64 : 65, gesture.x, gesture.y, gesture.modifiers, context.modes), Math.abs(gesture.lines));
|
|
58
|
+
if (state.buffer === 'normal')
|
|
59
|
+
return undefined;
|
|
60
|
+
if (!context.modes.alternateScroll)
|
|
61
|
+
return '';
|
|
62
|
+
return repeat(encodeKey(gesture.lines < 0 ? 82 : 81, gesture.modifiers, context.modes), Math.abs(gesture.lines));
|
|
63
|
+
},
|
|
64
|
+
release(clientId) { clients.get(clientId)?.release(); clients.delete(clientId); },
|
|
65
|
+
dispose() { clients.clear(); },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function repeat(bytes, count) {
|
|
69
|
+
if (!bytes)
|
|
70
|
+
return '';
|
|
71
|
+
if (count > Math.floor(TERMINAL_GRID_LIMITS.maxInputBytes / Buffer.byteLength(bytes)))
|
|
72
|
+
throw new KeeperMethodError('bad_request', 'Terminal input exceeds limit');
|
|
73
|
+
if (typeof bytes === 'string')
|
|
74
|
+
return bytes.repeat(count);
|
|
75
|
+
return Buffer.concat(Array.from({ length: count }, () => bytes));
|
|
76
|
+
}
|
|
77
|
+
function mouse(button, x, y, mask, modes, release = false) {
|
|
78
|
+
// The wire carries cells, so pixel encoding cannot be represented faithfully.
|
|
79
|
+
if (modes.mouseEncoding === 'sgr-pixels')
|
|
80
|
+
return '';
|
|
81
|
+
const mods = modes.mouseTracking === 'x10' ? 0 : (mask & 1 ? 4 : 0) + (mask & 2 ? 8 : 0) + (mask & 4 ? 16 : 0);
|
|
82
|
+
if (modes.mouseEncoding === 'sgr')
|
|
83
|
+
return `${esc}[<${button + mods};${x + 1};${y + 1}${release ? 'm' : 'M'}`;
|
|
84
|
+
const code = (release ? 3 : button) + mods + 32;
|
|
85
|
+
if (modes.mouseEncoding === 'urxvt')
|
|
86
|
+
return `${esc}[${code};${x + 1};${y + 1}M`;
|
|
87
|
+
if (modes.mouseEncoding === 'default' && (x > 222 || y > 222))
|
|
88
|
+
return '';
|
|
89
|
+
if (modes.mouseEncoding === 'default')
|
|
90
|
+
return Buffer.from([27, 91, 77, code, x + 33, y + 33]);
|
|
91
|
+
return `${esc}[M${String.fromCharCode(code, x + 33, y + 33)}`;
|
|
92
|
+
}
|
|
93
|
+
function encodeKey(usage, mask, modes) {
|
|
94
|
+
const mod = modifiers(mask);
|
|
95
|
+
if (arrows[usage])
|
|
96
|
+
return mod > 1 ? `${esc}[1;${mod}${arrows[usage]}` : `${esc}${modes.applicationCursor ? 'O' : '['}${arrows[usage]}`;
|
|
97
|
+
if (tilde[usage])
|
|
98
|
+
return `${esc}[${tilde[usage]}${mod > 1 ? `;${mod}` : ''}~`;
|
|
99
|
+
if (usage >= 58 && usage <= 61)
|
|
100
|
+
return mod > 1 ? `${esc}[1;${mod}${String.fromCharCode(80 + usage - 58)}` : `${esc}O${String.fromCharCode(80 + usage - 58)}`;
|
|
101
|
+
if (keypad[usage]) {
|
|
102
|
+
if (mask & 8 && usage !== 88)
|
|
103
|
+
return '';
|
|
104
|
+
return modes.applicationKeypad ? mod > 1 ? `${esc}[1;${mod}${keypad[usage][1]}` : `${esc}O${keypad[usage][1]}` : prefixed(keypad[usage][0], mask);
|
|
105
|
+
}
|
|
106
|
+
if (usage === 40)
|
|
107
|
+
return prefixed('\r', mask);
|
|
108
|
+
if (usage === 41)
|
|
109
|
+
return esc;
|
|
110
|
+
if (usage === 42)
|
|
111
|
+
return prefixed(mask & 4 ? '\b' : '\x7f', mask);
|
|
112
|
+
if (usage === 43)
|
|
113
|
+
return mask & 1 ? `${esc}[Z` : prefixed('\t', mask);
|
|
114
|
+
if (mask & 8)
|
|
115
|
+
return ''; // Native clients handle local Meta shortcuts.
|
|
116
|
+
let text = '';
|
|
117
|
+
if (usage >= 4 && usage <= 29)
|
|
118
|
+
text = String.fromCharCode((mask & 1 ? 65 : 97) + usage - 4);
|
|
119
|
+
else if (usage >= 30 && usage <= 39)
|
|
120
|
+
text = (mask & 1 ? '!@#$%^&*()' : '1234567890')[usage - 30];
|
|
121
|
+
else {
|
|
122
|
+
const punctuation = { 44: [' ', ' '], 45: ['-', '_'], 46: ['=', '+'], 47: ['[', '{'], 48: [']', '}'], 49: ['\\', '|'], 50: ['#', '~'], 51: [';', ':'], 52: ["'", '"'], 53: ['`', '~'], 54: [',', '<'], 55: ['.', '>'], 56: ['/', '?'] };
|
|
123
|
+
text = punctuation[usage]?.[mask & 1 ? 1 : 0] ?? '';
|
|
124
|
+
}
|
|
125
|
+
if (mask & 4) {
|
|
126
|
+
const upper = text.toUpperCase();
|
|
127
|
+
if (upper >= '@' && upper <= '_')
|
|
128
|
+
text = String.fromCharCode(upper.charCodeAt(0) - 64);
|
|
129
|
+
else if (text === ' ' || text === '2' || text === '`')
|
|
130
|
+
text = '\0';
|
|
131
|
+
else if (text === '3')
|
|
132
|
+
text = esc;
|
|
133
|
+
else if (text === '4')
|
|
134
|
+
text = '\x1c';
|
|
135
|
+
else if (text === '5')
|
|
136
|
+
text = '\x1d';
|
|
137
|
+
else if (text === '6')
|
|
138
|
+
text = '\x1e';
|
|
139
|
+
else if (text === '7' || text === '/')
|
|
140
|
+
text = '\x1f';
|
|
141
|
+
else if (text === '8' || text === '?')
|
|
142
|
+
text = '\x7f';
|
|
143
|
+
}
|
|
144
|
+
return prefixed(text, mask);
|
|
145
|
+
}
|
|
146
|
+
function prefixed(text, mask) { return text && mask & 2 ? esc + text : text; }
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
// keeper-terminal's methods on the kit server: `pty.*` over
|
|
1
|
+
// keeper-terminal's methods on the kit server: `pty.*` over NativePtySessions, `exec.*` over ExecHost, plus their activity samplers.
|
|
2
2
|
// Keeper-side module: node builtins and kit siblings only (the import guard enforces it).
|
|
3
3
|
import { constants, promises as fs } from 'node:fs';
|
|
4
4
|
import { absolutePath, badRequest as bad, environment } from './params.js';
|
|
5
|
-
import {
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { NativePtySessions, NATIVE_PTY_OPERATIONS } from './sessions.js';
|
|
6
7
|
import { KeeperMethodError } from '../kit/protocol.js';
|
|
7
8
|
import { EXEC_CAPABILITY, ExecErrorCode, PTY_CAPABILITY, execStream } from './protocol.js';
|
|
8
9
|
/**
|
|
@@ -21,18 +22,31 @@ async function launchOf(p) {
|
|
|
21
22
|
bad('Terminal shell is not an executable file');
|
|
22
23
|
return { cwd, shell, env };
|
|
23
24
|
}
|
|
24
|
-
export const KEEPER_CREATE_KEYS = [
|
|
25
|
+
export const KEEPER_CREATE_KEYS = ['id', 'cwd', 'shell', 'env', 'registrationId'];
|
|
25
26
|
export function createTerminalKeeper(options = {}) {
|
|
26
|
-
|
|
27
|
+
let available = false;
|
|
28
|
+
const sessions = new NativePtySessions({ ...options, loadPty: () => {
|
|
29
|
+
const module = (options.loadPty ?? (() => createRequire(import.meta.url)('node-pty')))();
|
|
30
|
+
available = true;
|
|
31
|
+
return module;
|
|
32
|
+
} });
|
|
27
33
|
const exec = options.exec;
|
|
28
34
|
return {
|
|
29
35
|
sessions,
|
|
30
36
|
exec,
|
|
31
|
-
capabilities: [...(
|
|
37
|
+
capabilities: [...(available ? [PTY_CAPABILITY] : []), ...(exec ? [EXEC_CAPABILITY] : [])],
|
|
32
38
|
install(server) {
|
|
33
|
-
server.register('pty.create', (params) =>
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
server.register('pty.create', (params) => {
|
|
40
|
+
if (Object.keys(params).some(key => !KEEPER_CREATE_KEYS.includes(key)))
|
|
41
|
+
bad('Unexpected tool parameter');
|
|
42
|
+
return sessions.create({ id: params.id, ...(params.cwd !== undefined ? { cwd: params.cwd } : {}), ...(params.registrationId !== undefined ? { registrationId: params.registrationId } : {}) }, () => launchOf(params));
|
|
43
|
+
}, { startsWork: true });
|
|
44
|
+
server.register('pty.list', (params) => {
|
|
45
|
+
if (Object.keys(params).length)
|
|
46
|
+
bad('Unexpected tool parameter');
|
|
47
|
+
return sessions.list();
|
|
48
|
+
});
|
|
49
|
+
for (const op of NATIVE_PTY_OPERATIONS)
|
|
36
50
|
server.register(`pty.${op}`, (params) => sessions.operate(op, params));
|
|
37
51
|
server.addActivitySampler(() => sessions.activity());
|
|
38
52
|
if (!exec)
|
|
@@ -8,7 +8,7 @@ import path, { sep } from 'node:path';
|
|
|
8
8
|
import { runKeeperMain } from '../kit/bootstrap.js';
|
|
9
9
|
import { ExecHost } from './exec.js';
|
|
10
10
|
import { createTerminalKeeper } from './keeper.js';
|
|
11
|
-
export const protocolVersion =
|
|
11
|
+
export const protocolVersion = 2;
|
|
12
12
|
/**
|
|
13
13
|
* node-pty from the bundle snapshot's own `node_modules` (copied next to keeper.mjs), never one found further up the
|
|
14
14
|
* directory tree: the snapshot is what the build hashed. Missing or foreign → PTYs unavailable (`pty` capability absent).
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readlink } from 'node:fs/promises';
|
|
3
|
+
import { hostname as localHostname } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
const clean = (value) => [...value.replace(/[\x00-\x1f\x7f-\x9f]/g, '').trim()].slice(0, 256).join('');
|
|
6
|
+
const validCwd = (value) => value.startsWith('/') && !/[\x00-\x1f\x7f-\x9f]/.test(value);
|
|
7
|
+
/** Resolved display metadata; native xterm hooks are the only OSC parser. */
|
|
8
|
+
export class TerminalMetadata {
|
|
9
|
+
options;
|
|
10
|
+
cwd;
|
|
11
|
+
cwdSource = 'launch';
|
|
12
|
+
explicitTitle = '';
|
|
13
|
+
listeners;
|
|
14
|
+
now;
|
|
15
|
+
hostname;
|
|
16
|
+
lastLookup = -Infinity;
|
|
17
|
+
oscRevision = 0;
|
|
18
|
+
closed = false;
|
|
19
|
+
pending;
|
|
20
|
+
cancelLookup;
|
|
21
|
+
timer;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.options = options;
|
|
24
|
+
this.cwd = options.cwd;
|
|
25
|
+
this.now = options.now ?? Date.now;
|
|
26
|
+
this.hostname = (options.hostname ?? localHostname()).toLowerCase();
|
|
27
|
+
this.listeners = [options.screen.onTitleChange(title => {
|
|
28
|
+
if (this.closed)
|
|
29
|
+
return;
|
|
30
|
+
const previous = this.title;
|
|
31
|
+
this.explicitTitle = clean(title);
|
|
32
|
+
if (this.title !== previous)
|
|
33
|
+
options.changed();
|
|
34
|
+
}), options.screen.onCwdChange(uri => this.osc7(uri))];
|
|
35
|
+
}
|
|
36
|
+
get title() { return this.explicitTitle || clean(path.posix.basename(this.cwd) || this.cwd) || 'Terminal'; }
|
|
37
|
+
read() { return { title: this.title, cwd: this.cwd, cwdSource: this.cwdSource }; }
|
|
38
|
+
/** Called once by the host factory, independently of output and attachments. */
|
|
39
|
+
start() {
|
|
40
|
+
if (this.closed || this.timer)
|
|
41
|
+
return;
|
|
42
|
+
void this.refresh();
|
|
43
|
+
this.timer = setInterval(() => { void this.refresh(); }, 2000);
|
|
44
|
+
this.timer.unref();
|
|
45
|
+
}
|
|
46
|
+
refresh() {
|
|
47
|
+
if (this.closed || this.options.live?.() === false)
|
|
48
|
+
return Promise.resolve();
|
|
49
|
+
if (this.pending)
|
|
50
|
+
return this.pending;
|
|
51
|
+
if (this.now() - this.lastLookup < 2000)
|
|
52
|
+
return Promise.resolve();
|
|
53
|
+
this.lastLookup = this.now();
|
|
54
|
+
const revision = this.oscRevision;
|
|
55
|
+
let timeout;
|
|
56
|
+
const bounded = new Promise(resolve => {
|
|
57
|
+
timeout = setTimeout(() => resolve(null), 1000);
|
|
58
|
+
this.cancelLookup = () => resolve(null);
|
|
59
|
+
// Normalize synchronous failures too; no failed lookup invalidates the last cwd.
|
|
60
|
+
try {
|
|
61
|
+
void (this.options.lookup ?? lookupProcessCwd)(this.options.pid).then(resolve, () => resolve(null));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
resolve(null);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
this.pending = bounded.then(cwd => {
|
|
68
|
+
if (!this.closed && this.options.live?.() !== false && revision === this.oscRevision && cwd && validCwd(cwd))
|
|
69
|
+
this.updateCwd(cwd, 'process');
|
|
70
|
+
}).finally(() => { clearTimeout(timeout); this.cancelLookup = undefined; this.pending = undefined; });
|
|
71
|
+
return this.pending;
|
|
72
|
+
}
|
|
73
|
+
dispose() {
|
|
74
|
+
if (this.closed)
|
|
75
|
+
return;
|
|
76
|
+
this.closed = true;
|
|
77
|
+
clearInterval(this.timer);
|
|
78
|
+
this.timer = undefined;
|
|
79
|
+
this.cancelLookup?.();
|
|
80
|
+
for (const listener of this.listeners)
|
|
81
|
+
listener.dispose();
|
|
82
|
+
}
|
|
83
|
+
osc7(uri) {
|
|
84
|
+
if (this.closed)
|
|
85
|
+
return;
|
|
86
|
+
try {
|
|
87
|
+
if (/[\x00-\x1f\x7f-\x9f]/.test(uri))
|
|
88
|
+
return;
|
|
89
|
+
const url = new URL(uri);
|
|
90
|
+
if (url.protocol !== 'file:' || url.username || url.password || url.port || uri.includes('?') || uri.includes('#'))
|
|
91
|
+
return;
|
|
92
|
+
const host = url.hostname.toLowerCase();
|
|
93
|
+
if (host && host !== 'localhost' && host !== this.hostname)
|
|
94
|
+
return;
|
|
95
|
+
const cwd = decodeURIComponent(url.pathname);
|
|
96
|
+
if (!validCwd(cwd))
|
|
97
|
+
return;
|
|
98
|
+
this.oscRevision++;
|
|
99
|
+
this.updateCwd(cwd, 'osc7');
|
|
100
|
+
}
|
|
101
|
+
catch { /* malformed/untrusted OSC payload is data, never executable */ }
|
|
102
|
+
}
|
|
103
|
+
updateCwd(cwd, source) {
|
|
104
|
+
if (cwd === this.cwd && source === this.cwdSource)
|
|
105
|
+
return;
|
|
106
|
+
this.cwd = cwd;
|
|
107
|
+
this.cwdSource = source;
|
|
108
|
+
this.options.changed();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** No shell interpolation: lsof receives the shell pid only as an argv element. */
|
|
112
|
+
export async function lookupProcessCwd(pid, platform = process.platform) {
|
|
113
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
114
|
+
return null;
|
|
115
|
+
if (platform === 'linux') {
|
|
116
|
+
try {
|
|
117
|
+
return await readlink(`/proc/${pid}/cwd`);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (platform !== 'darwin')
|
|
124
|
+
return null;
|
|
125
|
+
return new Promise(resolve => {
|
|
126
|
+
execFile('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], { timeout: 1000, maxBuffer: 64 * 1024 }, (error, stdout) => {
|
|
127
|
+
resolve(error ? null : stdout.split('\n').find(line => line.startsWith('n/'))?.slice(1) ?? null);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
export function createTerminalMetadata(context) {
|
|
132
|
+
// Capture initial values before installing metadata; read() must never call session().
|
|
133
|
+
const initial = context.session();
|
|
134
|
+
const metadata = new TerminalMetadata({ screen: context.screen, cwd: initial.cwd, pid: context.pty.pid,
|
|
135
|
+
changed: context.changed, live: () => !context.session().exited });
|
|
136
|
+
metadata.start();
|
|
137
|
+
return metadata;
|
|
138
|
+
}
|
|
@@ -1,32 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export const TERMINAL_PROTOCOL_VERSION = 1;
|
|
5
|
-
/** `keeper.hello` capability: node-pty loaded from the bundle snapshot, so `pty.*` works. Absent = PTYs unavailable. */
|
|
6
|
-
export const PTY_CAPABILITY = 'pty';
|
|
7
|
-
/** The legacy keeper's wording (tools/terminal.ts at the merge-base) when node-pty does not load; every `pty.*` call but `pty.list` answers it. */
|
|
1
|
+
// Terminal transport is a direct native-grid protocol replacement. Durable exec vocabulary follows.
|
|
2
|
+
export const TERMINAL_PROTOCOL_VERSION = 2;
|
|
3
|
+
export const PTY_CAPABILITY = 'pty-grid-v1';
|
|
8
4
|
export const PTY_UNAVAILABLE_ERROR = 'Terminal native dependency unavailable; reinstall CLI with optional node-pty support';
|
|
9
|
-
|
|
10
|
-
export const PTY_HISTORY_BYTES = 1024 * 1024;
|
|
11
|
-
export const PTY_MAX_POLL_BYTES = 64 * 1024;
|
|
12
|
-
export const PTY_SCROLLBACK = 1000;
|
|
13
|
-
/** A snapshot must fit one tool result (2 MiB, the same as keeper-terminal's outbound frame cap). */
|
|
14
|
-
export const PTY_RESULT_BYTES = 2 * 1024 * 1024;
|
|
15
|
-
export const PTY_SESSION_LIMIT = 8;
|
|
16
|
-
export const PTY_INPUT_BYTES = 64 * 1024;
|
|
17
|
-
/** Longest activity label (the foreground process basename; never its arguments). */
|
|
18
|
-
export const PTY_LABEL_CHARS = 64;
|
|
19
|
-
export const PTY_METHODS = ['pty.create', 'pty.attach', 'pty.poll', 'pty.write', 'pty.resize', 'pty.takeover', 'pty.detach', 'pty.close', 'pty.list'];
|
|
20
|
-
/** Typed error codes of `pty.*` (the message keeps today's `terminal.*` wording). */
|
|
21
|
-
export const PtyErrorCode = {
|
|
22
|
-
unavailable: 'pty_unavailable',
|
|
23
|
-
badRequest: 'bad_request',
|
|
24
|
-
notFound: 'pty_not_found',
|
|
25
|
-
notController: 'pty_not_controller',
|
|
26
|
-
limit: 'pty_limit',
|
|
27
|
-
exited: 'pty_exited',
|
|
28
|
-
snapshotTooLarge: 'pty_snapshot_too_large',
|
|
29
|
-
};
|
|
5
|
+
export const PTY_METHODS = ['pty.list', 'pty.create', 'pty.attach', 'pty.poll', 'pty.resize', 'pty.input', 'pty.scroll', 'pty.detach', 'pty.close'];
|
|
30
6
|
// ---- exec (spec §4.2): non-interactive commands with durable per-exec output logs ----------------------------------
|
|
31
7
|
/** `keeper.hello` capability: `exec.*` is served. */
|
|
32
8
|
export const EXEC_CAPABILITY = 'exec';
|