@platless/axom-cli 0.1.0 → 0.5.9
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 +1 -1
- package/dist/authScopes.js +19 -0
- package/dist/axom-patch/cli/README.md +132 -0
- package/dist/axom-patch/cli/account-limits/README.md +27 -0
- package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
- package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
- package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
- package/dist/axom-patch/cli/account-limits/generate.py +120 -0
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
- package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
- package/dist/axom-patch/cli/claude-account-limits.json +37 -0
- package/dist/axom-patch/cli/codex-account-limits.json +37 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/nightly-task.md +152 -0
- package/dist/axom-patch/cli/pi-steering.json +37 -0
- package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
- package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
- package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
- package/dist/axom-patch/cli/qoder-foreground-output.md +42 -0
- package/dist/axom-patch/cli/qoder-steering.json +52 -0
- package/dist/axom-patch/cli/qoder-steering.md +30 -0
- package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
- package/dist/axom-patch/cli/runtime.json +63 -0
- package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
- package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
- package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
- package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
- package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
- package/dist/axom-patch/cli/zcode-models.json +38 -0
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
- package/dist/cli.js +36 -1
- package/dist/commands/auth.js +4 -13
- package/dist/commands/execute.js +4 -4
- package/dist/commands/record.js +6 -6
- package/dist/commands/struct.js +4 -4
- package/dist/commands/workstation.js +2 -2
- package/dist/credentials.js +2 -2
- package/dist/install/claude/commands.js +13 -13
- package/dist/keepers/acp/keeper.mjs +3083 -0
- package/dist/keepers/acp/manifest.json +9 -0
- package/dist/keepers/terminal/keeper.mjs +8469 -0
- package/dist/keepers/terminal/manifest.json +9 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
- package/dist/mcp.js +32 -14
- package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
- package/dist/runtime-deps/qoder-sdk/package.json +8 -0
- package/dist/test/keeperKit.js +67 -0
- package/dist/workstation/acp/acpAgents.js +2514 -681
- package/dist/workstation/acp/acpClient.js +95 -318
- package/dist/workstation/acp/activityBusy.js +43 -0
- package/dist/workstation/acp/activityReporting.js +54 -0
- package/dist/workstation/acp/adoption.js +28 -356
- package/dist/workstation/acp/authUrlSniffer.js +36 -0
- package/dist/workstation/acp/binaryInstall.js +100 -27
- package/dist/workstation/acp/builds/gc.js +245 -0
- package/dist/workstation/acp/builds/install/binary.js +28 -0
- package/dist/workstation/acp/builds/install/npm.js +235 -0
- package/dist/workstation/acp/builds/install/uv.js +254 -0
- package/dist/workstation/acp/builds/migrate.js +195 -0
- package/dist/workstation/acp/builds/model.js +128 -0
- package/dist/workstation/acp/builds/nodeGate.js +100 -0
- package/dist/workstation/acp/builds/store.js +352 -0
- package/dist/workstation/acp/commandTableProbe.js +68 -0
- package/dist/workstation/acp/configOptions.js +103 -1
- package/dist/workstation/acp/delegationMcp.js +24 -0
- package/dist/workstation/acp/envSecrets.js +27 -0
- package/dist/workstation/acp/initializeResponse.js +285 -0
- package/dist/workstation/acp/installErrors.js +12 -2
- package/dist/workstation/acp/installProbe.js +1 -1
- package/dist/workstation/acp/managedPatches.js +144 -0
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +80 -0
- package/dist/workstation/acp/permission.js +335 -37
- package/dist/workstation/acp/processor/checkpoint.js +87 -0
- package/dist/workstation/acp/processor/commands.js +35 -0
- package/dist/workstation/acp/processor/control.js +355 -0
- package/dist/workstation/acp/processor/emit.js +26 -0
- package/dist/workstation/acp/processor/jsonrpc.js +172 -0
- package/dist/workstation/acp/processor/memoryLog.js +49 -0
- package/dist/workstation/acp/processor/origins.js +48 -0
- package/dist/workstation/acp/processor/output.js +67 -0
- package/dist/workstation/acp/processor/projection.js +42 -0
- package/dist/workstation/acp/processor/record.js +128 -0
- package/dist/workstation/acp/processor/runtime.js +181 -0
- package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
- package/dist/workstation/acp/processor/session.js +525 -0
- package/dist/workstation/acp/processor/sideChannel.js +80 -0
- package/dist/workstation/acp/processor/state.js +74 -0
- package/dist/workstation/acp/processor/step.js +459 -0
- package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
- package/dist/workstation/acp/processor/test/harness.js +120 -0
- package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
- package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
- package/dist/workstation/acp/processor/transport/generations.js +76 -0
- package/dist/workstation/acp/processor/transport/keeper.js +130 -0
- package/dist/workstation/acp/processor/transport/local.js +269 -0
- package/dist/workstation/acp/processor/transport/types.js +78 -0
- package/dist/workstation/acp/processor/updates.js +364 -0
- package/dist/workstation/acp/processorSession.js +1512 -0
- package/dist/workstation/acp/qoderStartup.js +45 -0
- package/dist/workstation/acp/registry/catalog.js +385 -4
- package/dist/workstation/acp/registry/detect.js +55 -0
- package/dist/workstation/acp/registry/launch.js +61 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +114 -0
- package/dist/workstation/acp/terminalLogin.js +171 -0
- package/dist/workstation/acp/terminalLoginSpec.js +41 -0
- package/dist/workstation/acp/test/adapterFixtures.js +256 -0
- package/dist/workstation/acp/test/daemonLife.js +106 -0
- package/dist/workstation/acp/test/fakeAgent.js +185 -9
- package/dist/workstation/acp/test/keeperWorld.js +267 -0
- package/dist/workstation/acp/test/memoryOutbox.js +55 -0
- package/dist/workstation/acp/test/raw/fixtures.js +196 -0
- package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
- package/dist/workstation/acp/test/raw/sanitize.js +344 -0
- package/dist/workstation/acp/test/raw/sources.js +249 -0
- package/dist/workstation/acp/test/realAdapter.js +377 -0
- package/dist/workstation/acp/test/scriptAgent.js +206 -0
- package/dist/workstation/acp/test/sessionFrames.js +42 -0
- package/dist/workstation/acp/timedCommand.js +4 -2
- package/dist/workstation/acp/uvxInstall.js +72 -122
- package/dist/workstation/acp/validate.js +33 -5
- package/dist/workstation/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1686 -348
- package/dist/workstation/channels/agent.js +14 -6
- package/dist/workstation/channels/computer.js +203 -7
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +86 -6
- package/dist/workstation/config.js +3 -1
- package/dist/workstation/connection/encryptedChannel.js +88 -21
- package/dist/workstation/daemon.js +309 -6
- package/dist/workstation/daemonctl.js +28 -2
- package/dist/workstation/desktop/binding.js +36 -0
- package/dist/workstation/desktop/bridge.js +212 -0
- package/dist/workstation/desktop/flow-control.js +52 -0
- package/dist/workstation/desktop/focused-text.js +130 -0
- package/dist/workstation/desktop/protocol.js +35 -0
- package/dist/workstation/exec/attach.js +126 -0
- package/dist/workstation/exec/command.js +40 -0
- package/dist/workstation/exec/durable.js +517 -0
- package/dist/workstation/exec/fallback.js +106 -0
- package/dist/workstation/exec/policy.js +837 -0
- package/dist/workstation/exec/shaping.js +211 -0
- package/dist/workstation/exec.js +3 -112
- package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
- package/dist/workstation/files/adapters/toolRequests.js +121 -0
- package/dist/workstation/files/client.js +455 -0
- package/dist/workstation/files/errors.js +28 -0
- package/dist/workstation/files/fence.js +350 -0
- package/dist/workstation/files/find/fileIndex.js +153 -0
- package/dist/workstation/files/find/fuzzy.js +155 -0
- package/dist/workstation/files/host.js +122 -0
- package/dist/workstation/files/hostProtocol.js +2 -0
- package/dist/workstation/files/media/cache.js +620 -0
- package/dist/workstation/files/media/encoding.js +308 -0
- package/dist/workstation/files/media/estimator.js +100 -0
- package/dist/workstation/files/media/media.js +325 -0
- package/dist/workstation/files/media/packedAudio.js +23 -0
- package/dist/workstation/files/media/scheduler.js +551 -0
- package/dist/workstation/files/media/types.js +5 -0
- package/dist/workstation/files/scope.js +45 -0
- package/dist/workstation/files/structured.js +284 -0
- package/dist/workstation/files/text.js +356 -0
- package/dist/workstation/files/uploads.js +351 -0
- package/dist/workstation/journal.js +65 -236
- package/dist/workstation/keepers/acp/client.js +101 -0
- package/dist/workstation/keepers/acp/envelope.js +62 -0
- package/dist/workstation/keepers/acp/host.js +419 -0
- package/dist/workstation/keepers/acp/main.js +18 -0
- package/dist/workstation/keepers/acp/memoryStream.js +121 -0
- package/dist/workstation/keepers/acp/params.js +137 -0
- package/dist/workstation/keepers/acp/protocol.js +66 -0
- package/dist/workstation/keepers/acp/recover.js +120 -0
- package/dist/workstation/keepers/acp/session.js +584 -0
- package/dist/workstation/keepers/acp/stderr.js +154 -0
- package/dist/workstation/keepers/kit/activity.js +103 -0
- package/dist/workstation/keepers/kit/bootstrap.js +164 -0
- package/dist/workstation/keepers/kit/buildVersion.js +51 -0
- package/dist/workstation/keepers/kit/bundle.js +120 -0
- package/dist/workstation/keepers/kit/client.js +432 -0
- package/dist/workstation/keepers/kit/identity.js +21 -0
- package/dist/workstation/keepers/kit/ipc.js +112 -0
- package/dist/workstation/keepers/kit/launchConfig.js +8 -0
- package/dist/workstation/keepers/kit/launcher.js +249 -0
- package/dist/workstation/keepers/kit/lock.js +123 -0
- package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
- package/dist/workstation/keepers/kit/paths.js +75 -0
- package/dist/workstation/keepers/kit/protocol.js +43 -0
- package/dist/workstation/keepers/kit/server.js +518 -0
- package/dist/workstation/keepers/kit/streamLog.js +697 -0
- package/dist/workstation/keepers/legacy.js +296 -0
- package/dist/workstation/keepers/policy.js +576 -0
- package/dist/workstation/keepers/stop.js +34 -0
- package/dist/workstation/keepers/terminal/client.js +213 -0
- package/dist/workstation/keepers/terminal/exec.js +981 -0
- package/dist/workstation/keepers/terminal/generations.js +129 -0
- package/dist/workstation/keepers/terminal/keeper.js +62 -0
- package/dist/workstation/keepers/terminal/main.js +43 -0
- package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
- package/dist/workstation/keepers/terminal/params.js +27 -0
- package/dist/workstation/keepers/terminal/protocol.js +56 -0
- package/dist/workstation/keepers/terminal/pty.js +330 -0
- package/dist/workstation/oauth/deviceAuthorization.js +21 -12
- package/dist/workstation/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +201 -8
- package/dist/workstation/protocol/common.js +36 -0
- package/dist/workstation/protocol/computer.js +107 -7
- package/dist/workstation/protocol/management.js +50 -2
- package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
- package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
- package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
- package/dist/workstation/qoderAdapter/contract.js +78 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
- package/dist/workstation/qoderAdapter/history.js +85 -0
- package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
- package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
- package/dist/workstation/qoderAdapter/main.js +95 -0
- package/dist/workstation/qoderAdapter/mapper.js +117 -0
- package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
- package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
- package/dist/workstation/qoderAdapter/permissions.js +61 -0
- package/dist/workstation/qoderAdapter/profile.js +93 -0
- package/dist/workstation/qoderAdapter/sdkDriver.js +48 -0
- package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
- package/dist/workstation/qoderAdapter/session.js +319 -0
- package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
- package/dist/workstation/qoderAdapter/subwork.js +85 -0
- package/dist/workstation/qoderAdapter/taskControl.js +38 -0
- package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
- package/dist/workstation/qoderAdapter/types.js +1 -0
- package/dist/workstation/qoderAdapter/usage.js +37 -0
- package/dist/workstation/register.js +4 -0
- package/dist/workstation/systemMetrics.js +110 -1
- package/dist/workstation/tools/browser.js +394 -0
- package/dist/workstation/tools/client.js +277 -0
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +124 -0
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +86 -0
- package/dist/workstation/update/coordinator.js +32 -0
- package/dist/workstation/update/npmInstaller.js +2 -0
- package/dist/workstation/update/ptyPreflight.js +45 -0
- package/dist/workstation/update/taskRegistry.js +141 -23
- package/dist/workstation/update/watchdog.js +66 -0
- package/package.json +20 -7
- package/skills/axom/SKILL.md +55 -17
- package/skills/axom-definition/SKILL.md +32 -32
- package/skills/axom-platform/SKILL.md +39 -39
- package/dist/workstation/acp/acpSession.js +0 -1047
- package/dist/workstation/acp/contract.js +0 -494
- package/dist/workstation/acp/draftInterceptor.js +0 -154
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { keys, string, integer, validateRequest } from '../../tools/validation.js';
|
|
7
|
+
import { MediaScheduler, MediaConsumerExpiredError } from './scheduler.js';
|
|
8
|
+
import { decoderCommand } from './encoding.js';
|
|
9
|
+
/** The successful-result marker consumers match (`FileMediaStreamingGateway` maps it to FileMediaResourceExpired). */
|
|
10
|
+
export const MEDIA_CONSUMER_EXPIRED = 'media-consumer-expired';
|
|
11
|
+
const RECIPE = 'preview-v2-720p-h264-aac128-png2048-mp3128';
|
|
12
|
+
const STREAM_RECIPE = 'preview-v4-720p30-closedgop120-aac128-mp3128-noreservoir-globalpts';
|
|
13
|
+
const MAX_OUTPUT = 512 * 1024 * 1024;
|
|
14
|
+
const CACHE_LIMIT = 2 * 1024 * 1024 * 1024;
|
|
15
|
+
const TTL = 24 * 60 * 60_000;
|
|
16
|
+
const formats = 'mov,matroska,webm,mp3,wav,flac,ogg,aac,avi,aiff,png_pipe,jpeg_pipe,webp_pipe,bmp_pipe,tiff_pipe,gif';
|
|
17
|
+
const extensions = new Set('mp4 m4v mov mkv webm avi mp3 wav flac ogg opus aac m4a aif aiff png jpg jpeg webp bmp tif tiff gif heic heif avif'.split(' '));
|
|
18
|
+
async function snapshot(handle) {
|
|
19
|
+
const s = await handle.stat({ bigint: true });
|
|
20
|
+
if (!s.isFile() || s.size > 32n * 1024n * 1024n * 1024n)
|
|
21
|
+
throw new Error('Not a supported regular media file');
|
|
22
|
+
return { dev: String(s.dev), ino: String(s.ino), size: String(s.size), mtime: String(s.mtimeNs), ctime: String(s.ctimeNs) };
|
|
23
|
+
}
|
|
24
|
+
const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
25
|
+
function tool(command, args, input, signal) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
// Restrict decoding to local self-contained containers; never accept playlist/concat/network inputs.
|
|
28
|
+
// An explicit cwd: the files host changes its own cwd inside pinned() sections, which a child would otherwise inherit.
|
|
29
|
+
const child = spawn(command, args, { cwd: '/', stdio: ['ignore', 'pipe', 'pipe', input.fd], env: process.env });
|
|
30
|
+
let output = '';
|
|
31
|
+
let failure;
|
|
32
|
+
const stop = () => { failure ??= new Error('Media preparation cancelled or timed out'); child.kill('SIGTERM'); };
|
|
33
|
+
const timeout = setTimeout(() => cancel(), 5 * 60_000);
|
|
34
|
+
timeout.unref();
|
|
35
|
+
let kill;
|
|
36
|
+
const cancel = () => { stop(); kill = setTimeout(() => child.kill('SIGKILL'), 2000); kill.unref(); };
|
|
37
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
38
|
+
if (signal.aborted)
|
|
39
|
+
cancel();
|
|
40
|
+
child.stdout.on('data', chunk => { output += chunk.toString(); if (output.length > 1024 * 1024) {
|
|
41
|
+
failure = new Error('Media metadata exceeds limit');
|
|
42
|
+
cancel();
|
|
43
|
+
} });
|
|
44
|
+
child.stderr.resume(); // Diagnostics may contain local paths; keep public errors bounded and generic.
|
|
45
|
+
child.on('error', e => { failure = new Error(e.code === 'ENOENT' ? 'Install FFmpeg and ffprobe on this workstation (or set AXOM_FFMPEG / AXOM_FFPROBE).' : 'Unable to start workstation media decoder'); });
|
|
46
|
+
child.on('close', code => {
|
|
47
|
+
clearTimeout(timeout);
|
|
48
|
+
if (kill)
|
|
49
|
+
clearTimeout(kill);
|
|
50
|
+
signal.removeEventListener('abort', cancel);
|
|
51
|
+
if (failure)
|
|
52
|
+
reject(failure);
|
|
53
|
+
else if (code !== 0)
|
|
54
|
+
reject(new Error('Unable to prepare this media file. It may be damaged or unsupported.'));
|
|
55
|
+
else
|
|
56
|
+
resolve(output);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** Separate finite jobs: tools queue never waits for FFmpeg. Cache paths are derived, not client supplied. */
|
|
61
|
+
export class WorkstationMedia {
|
|
62
|
+
files;
|
|
63
|
+
cache;
|
|
64
|
+
jobs = new Map();
|
|
65
|
+
queue = Promise.resolve();
|
|
66
|
+
closed = false;
|
|
67
|
+
initialized;
|
|
68
|
+
scheduler;
|
|
69
|
+
constructor(files, cache) {
|
|
70
|
+
this.files = files;
|
|
71
|
+
this.cache = cache;
|
|
72
|
+
}
|
|
73
|
+
init() {
|
|
74
|
+
return this.initialized ??= (async () => {
|
|
75
|
+
await fs.mkdir(this.cache, { recursive: true, mode: 0o700 });
|
|
76
|
+
const stat = await fs.lstat(this.cache);
|
|
77
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid()))
|
|
78
|
+
throw new Error('Unsafe media cache');
|
|
79
|
+
await fs.chmod(this.cache, 0o700);
|
|
80
|
+
this.scheduler = await MediaScheduler.create(this.cache);
|
|
81
|
+
await this.trim();
|
|
82
|
+
})();
|
|
83
|
+
}
|
|
84
|
+
async trim() { await this.scheduler.trimCache(); }
|
|
85
|
+
async dispatch(method, p) {
|
|
86
|
+
if (this.closed)
|
|
87
|
+
throw new Error('Workstation media stopped');
|
|
88
|
+
if (!['files.media.prepare', 'files.media.read', 'files.media.cancel'].includes(method))
|
|
89
|
+
return this.streaming(method, p);
|
|
90
|
+
keys(p, [...(method === 'files.media.prepare' ? ['path'] : method === 'files.media.cancel' ? ['path', 'version'] : ['path', 'version', 'offset', 'length']), 'readOnly']);
|
|
91
|
+
if (p.readOnly !== undefined && typeof p.readOnly !== 'boolean')
|
|
92
|
+
throw new Error('Invalid readOnly');
|
|
93
|
+
const source = string(p, 'path');
|
|
94
|
+
if (!extensions.has(path.extname(source).slice(1).toLowerCase()))
|
|
95
|
+
throw new Error('Unsupported media format');
|
|
96
|
+
const input = await this.files.openMediaSource(source, p.readOnly === true);
|
|
97
|
+
let retained = false;
|
|
98
|
+
try {
|
|
99
|
+
const stamp = await snapshot(input);
|
|
100
|
+
const version = createHash('sha256').update(JSON.stringify([source, stamp, RECIPE])).digest('hex');
|
|
101
|
+
if (method !== 'files.media.prepare' && string(p, 'version', 64) !== version)
|
|
102
|
+
throw new Error('Media source changed; reopen its preview');
|
|
103
|
+
await this.init();
|
|
104
|
+
// The files host runs requests concurrently (the legacy keeper serialized them): one version's check-then-create
|
|
105
|
+
// of its job must not interleave with another request for the same version, or two conversions would start.
|
|
106
|
+
return await this.exclusive(version, async () => {
|
|
107
|
+
await this.scheduler.pinLegacy(version);
|
|
108
|
+
let job = this.jobs.get(version);
|
|
109
|
+
if (method === 'files.media.cancel') {
|
|
110
|
+
job?.abort.abort();
|
|
111
|
+
return { ok: true };
|
|
112
|
+
}
|
|
113
|
+
if (method === 'files.media.read') {
|
|
114
|
+
const offset = integer(p, 'offset', 0, Number.MAX_SAFE_INTEGER);
|
|
115
|
+
const length = integer(p, 'length', 1, 256 * 1024);
|
|
116
|
+
const media = job?.media ?? await this.cached(version);
|
|
117
|
+
if (!media)
|
|
118
|
+
throw new Error('Media preview is not ready');
|
|
119
|
+
const handle = await fs.open(path.join(this.cache, `${version}.${media.extension}`), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
120
|
+
try {
|
|
121
|
+
const stat = await handle.stat();
|
|
122
|
+
if (!stat.isFile() || stat.size !== media.size)
|
|
123
|
+
throw new Error('Media preview cache changed');
|
|
124
|
+
const bytes = Buffer.alloc(Math.min(length, Math.max(0, stat.size - offset)));
|
|
125
|
+
const { bytesRead } = await handle.read(bytes, 0, bytes.length, offset);
|
|
126
|
+
return { data: bytes.subarray(0, bytesRead).toString('base64'), size: stat.size, eof: offset + bytesRead >= stat.size };
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
await handle.close();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (job?.state === 'ready' && !await this.cached(version)) {
|
|
133
|
+
this.jobs.delete(version);
|
|
134
|
+
job = undefined;
|
|
135
|
+
}
|
|
136
|
+
if (!job) {
|
|
137
|
+
const cached = await this.cached(version);
|
|
138
|
+
if (cached)
|
|
139
|
+
return cached;
|
|
140
|
+
for (const [key, item] of this.jobs)
|
|
141
|
+
if (item.state !== 'preparing')
|
|
142
|
+
this.jobs.delete(key);
|
|
143
|
+
if (this.jobs.size >= 8)
|
|
144
|
+
throw new Error('Workstation media queue is full; try again shortly');
|
|
145
|
+
job = { version, snapshot: stamp, state: 'preparing', abort: new AbortController(), done: Promise.resolve() };
|
|
146
|
+
this.jobs.set(version, job);
|
|
147
|
+
retained = true;
|
|
148
|
+
const current = job;
|
|
149
|
+
current.done = this.scheduler.runFinite(signal => this.convert(version, input, stamp, signal), current.abort.signal)
|
|
150
|
+
.then(media => { current.media = media; current.state = 'ready'; }, error => { current.state = 'failed'; current.error = error.message; })
|
|
151
|
+
.finally(() => input.close());
|
|
152
|
+
this.queue = Promise.all([this.queue, current.done]).then(() => { });
|
|
153
|
+
}
|
|
154
|
+
// Report the failed attempt once, then allow a fresh user retry (e.g. after installing FFmpeg).
|
|
155
|
+
if (job.state === 'failed')
|
|
156
|
+
this.jobs.delete(version);
|
|
157
|
+
return job.media ?? { state: job.state, version, ...(job.error ? { error: job.error } : {}) };
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
if (!retained)
|
|
162
|
+
await input.close();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async streaming(method, p) {
|
|
166
|
+
validateRequest(method, p);
|
|
167
|
+
const source = string(p, 'path');
|
|
168
|
+
if (!extensions.has(path.extname(source).slice(1).toLowerCase()))
|
|
169
|
+
throw new Error('Unsupported media format');
|
|
170
|
+
if (method === 'files.media.release') {
|
|
171
|
+
await this.init();
|
|
172
|
+
await this.scheduler.releaseIdentity(string(p, 'version', 64), string(p, 'consumerId', 128));
|
|
173
|
+
return { ok: true };
|
|
174
|
+
}
|
|
175
|
+
const input = await this.files.openMediaSource(source, p.readOnly === true);
|
|
176
|
+
let transferred = false;
|
|
177
|
+
try {
|
|
178
|
+
const stamp = await snapshot(input);
|
|
179
|
+
const version = createHash('sha256').update(JSON.stringify([source, stamp, STREAM_RECIPE])).digest('hex');
|
|
180
|
+
const legacyVersion = createHash('sha256').update(JSON.stringify([source, stamp, RECIPE])).digest('hex');
|
|
181
|
+
if (method !== 'files.media.open' && string(p, 'version', 64) !== version)
|
|
182
|
+
throw new Error('Media source changed; reopen its preview');
|
|
183
|
+
await this.init();
|
|
184
|
+
const scheduler = this.scheduler;
|
|
185
|
+
const key = { version, sourcePath: source, snapshot: JSON.stringify(stamp), recipe: STREAM_RECIPE, legacyVersion };
|
|
186
|
+
const consumerId = string(p, 'consumerId', 128);
|
|
187
|
+
if (method === 'files.media.open') {
|
|
188
|
+
transferred = true;
|
|
189
|
+
return { ...await scheduler.open(input, key, consumerId, p.hls === true) };
|
|
190
|
+
}
|
|
191
|
+
if (method === 'files.media.status')
|
|
192
|
+
return { ...await scheduler.status(key, consumerId) };
|
|
193
|
+
if (method === 'files.media.seek')
|
|
194
|
+
return { ...await scheduler.seek(key, consumerId, integer(p, 'positionMs', 0, 21600000), integer(p, 'revision', 0, Number.MAX_SAFE_INTEGER)) };
|
|
195
|
+
if (method === 'files.media.interest') {
|
|
196
|
+
await scheduler.interest(key, consumerId, integer(p, 'positionMs', 0, 21600000), p.playing === true);
|
|
197
|
+
return { ok: true };
|
|
198
|
+
}
|
|
199
|
+
if (method === 'files.media.object')
|
|
200
|
+
return { ...await scheduler.object(key, consumerId, string(p, 'objectId', 128), integer(p, 'offset', 0, Number.MAX_SAFE_INTEGER), integer(p, 'length', 1, 256 * 1024)) };
|
|
201
|
+
if (method === 'files.media.release') {
|
|
202
|
+
await scheduler.release(key, consumerId);
|
|
203
|
+
return { ok: true };
|
|
204
|
+
}
|
|
205
|
+
throw new Error('Unsupported workstation tool');
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (error instanceof MediaConsumerExpiredError)
|
|
209
|
+
return { errorCode: MEDIA_CONSUMER_EXPIRED };
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
if (!transferred)
|
|
214
|
+
await input.close();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Media encodes running or queued (sampled by the daemon as activity). */
|
|
218
|
+
get activeEncodes() { return this.scheduler?.runningEncodes ?? 0; }
|
|
219
|
+
versionLocks = new Map();
|
|
220
|
+
/** Runs `action` after every earlier action for the same version has settled. */
|
|
221
|
+
async exclusive(version, action) {
|
|
222
|
+
const previous = this.versionLocks.get(version) ?? Promise.resolve();
|
|
223
|
+
const mine = previous.catch(() => { }).then(action);
|
|
224
|
+
const tail = mine.catch(() => { });
|
|
225
|
+
this.versionLocks.set(version, tail);
|
|
226
|
+
try {
|
|
227
|
+
return await mine;
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
if (this.versionLocks.get(version) === tail)
|
|
231
|
+
this.versionLocks.delete(version);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async cached(version) {
|
|
235
|
+
await this.scheduler.pinLegacy(version);
|
|
236
|
+
try {
|
|
237
|
+
const meta = path.join(this.cache, `${version}.json`);
|
|
238
|
+
const handle = await fs.open(meta, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
239
|
+
let media;
|
|
240
|
+
try {
|
|
241
|
+
if ((await handle.stat()).size > 4096)
|
|
242
|
+
return;
|
|
243
|
+
media = JSON.parse(await handle.readFile('utf8'));
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
await handle.close();
|
|
247
|
+
}
|
|
248
|
+
if (media.version !== version || media.state !== 'ready' || !['mp3', 'mp4', 'png'].includes(media.extension) || !Number.isSafeInteger(media.size) || media.size <= 0 || media.size >= MAX_OUTPUT)
|
|
249
|
+
return;
|
|
250
|
+
const stat = await fs.lstat(path.join(this.cache, `${version}.${media.extension}`));
|
|
251
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size !== media.size)
|
|
252
|
+
return;
|
|
253
|
+
return media;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async convert(version, input, stamp, signal) {
|
|
260
|
+
if (process.platform === 'win32')
|
|
261
|
+
throw new Error('Workstation media preparation currently requires macOS or Linux FFmpeg');
|
|
262
|
+
await this.trim();
|
|
263
|
+
const free = await fs.statfs(this.cache);
|
|
264
|
+
if (free.bavail * free.bsize < MAX_OUTPUT)
|
|
265
|
+
throw new Error('Not enough free disk space for a media preview');
|
|
266
|
+
const base = ['-v', 'error', '-max_alloc', '134217728', '-protocol_whitelist', 'file', '-format_whitelist', formats];
|
|
267
|
+
const raw = await tool(decoderCommand('AXOM_FFPROBE', 'ffprobe'), [...base, '-show_format', '-show_streams', '-of', 'json', '/dev/fd/3'], input, signal);
|
|
268
|
+
const probe = JSON.parse(raw);
|
|
269
|
+
const video = probe.streams.find(s => s.codec_type === 'video' && !s.disposition?.attached_pic);
|
|
270
|
+
const audio = probe.streams.find(s => s.codec_type === 'audio');
|
|
271
|
+
const duration = Number(probe.format.duration ?? video?.duration ?? audio?.duration ?? 0);
|
|
272
|
+
const image = !!video && !audio && duration === 0;
|
|
273
|
+
const kind = image ? 'image' : video ? 'video' : audio ? 'audio' : undefined;
|
|
274
|
+
if (!kind || !Number.isFinite(duration) || duration < 0 || duration > 6 * 60 * 60)
|
|
275
|
+
throw new Error('Unsupported media or duration exceeds six hours');
|
|
276
|
+
if (video && ((!video.width || !video.height) || video.width * video.height > 40_000_000))
|
|
277
|
+
throw new Error('Image/video dimensions exceed preview limits');
|
|
278
|
+
const extension = kind === 'video' ? 'mp4' : kind === 'audio' ? 'mp3' : 'png';
|
|
279
|
+
const output = path.join(this.cache, `.part-${randomUUID()}.${extension}`);
|
|
280
|
+
const destination = path.join(this.cache, `${version}.${extension}`);
|
|
281
|
+
let args = ['-nostdin', '-v', 'error', '-max_alloc', '134217728', '-threads', '2', '-protocol_whitelist', 'file', '-format_whitelist', formats, '-i', '/dev/fd/3', '-map_metadata', '-1'];
|
|
282
|
+
if (kind === 'image')
|
|
283
|
+
args.push('-map', '0:v:0', '-frames:v', '1', '-vf', "scale=w='min(2048,iw)':h='min(2048,ih)':force_original_aspect_ratio=decrease", '-c:v', 'png', '-threads', '1');
|
|
284
|
+
else if (kind === 'audio')
|
|
285
|
+
args.push('-map', '0:a:0', '-vn', '-c:a', 'libmp3lame', '-b:a', '128k', '-ac', '2', '-ar', '44100');
|
|
286
|
+
else {
|
|
287
|
+
args.push('-map', '0:v:0', '-map', '0:a:0?');
|
|
288
|
+
if (video.codec_name === 'h264' && video.pix_fmt === 'yuv420p' && video.width <= 1280 && video.height <= 720 && Number(probe.format.bit_rate) <= 2_000_000)
|
|
289
|
+
args.push('-c:v', 'copy');
|
|
290
|
+
else
|
|
291
|
+
args.push('-vf', "scale=w='min(1280,iw)':h='min(720,ih)':force_original_aspect_ratio=decrease:force_divisible_by=2", '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '26', '-maxrate', '1800k', '-bufsize', '3600k', '-pix_fmt', 'yuv420p', '-threads', '2');
|
|
292
|
+
args.push('-c:a', 'aac', '-b:a', '128k', '-ac', '2', '-movflags', '+faststart');
|
|
293
|
+
}
|
|
294
|
+
args.push('-fs', String(MAX_OUTPUT), '-y', output);
|
|
295
|
+
await this.scheduler.pinLegacy(version);
|
|
296
|
+
const releaseReservation = await this.scheduler.reserveLegacy(output, MAX_OUTPUT + 4096);
|
|
297
|
+
try {
|
|
298
|
+
await tool(decoderCommand('AXOM_FFMPEG', 'ffmpeg'), args, input, signal);
|
|
299
|
+
if (signal.aborted || !equal(stamp, await snapshot(input)))
|
|
300
|
+
throw new Error('Media source changed or preparation cancelled');
|
|
301
|
+
const stat = await fs.stat(output);
|
|
302
|
+
if (!stat.size || stat.size >= MAX_OUTPUT)
|
|
303
|
+
throw new Error('Media preview exceeds 512 MiB');
|
|
304
|
+
const factor = video ? Math.min(1, (image ? 2048 : 1280) / video.width, (image ? 2048 : 720) / video.height) : 0;
|
|
305
|
+
const media = { state: 'ready', version, kind, mimeType: kind === 'video' ? 'video/mp4' : kind === 'audio' ? 'audio/mpeg' : 'image/png', extension, size: stat.size, durationSeconds: duration, width: video ? Math.floor(video.width * factor) : 0, height: video ? Math.floor(video.height * factor) : 0 };
|
|
306
|
+
await this.scheduler.pinLegacy(version);
|
|
307
|
+
await fs.rename(output, destination);
|
|
308
|
+
const metaTemp = path.join(this.cache, `.part-${randomUUID()}.json`);
|
|
309
|
+
try {
|
|
310
|
+
await fs.writeFile(metaTemp, JSON.stringify(media), { mode: 0o600, flag: 'wx' });
|
|
311
|
+
await fs.rename(metaTemp, path.join(this.cache, `${version}.json`));
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
await fs.unlink(metaTemp).catch(() => { });
|
|
315
|
+
}
|
|
316
|
+
return media;
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
await fs.unlink(output).catch(() => { });
|
|
320
|
+
await releaseReservation();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async dispose() { this.closed = true; for (const job of this.jobs.values())
|
|
324
|
+
job.abort.abort(); await this.scheduler?.dispose(); await this.queue; this.jobs.clear(); }
|
|
325
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** RFC 8216 §3.4: one PRIV frame with an unsigned 33-bit MPEG timestamp. */
|
|
2
|
+
export function stampPackedAudio(mp3, transportTimestamp90k) {
|
|
3
|
+
if (transportTimestamp90k < 0n || transportTimestamp90k >= 1n << 33n)
|
|
4
|
+
throw new Error('Invalid packed audio timestamp');
|
|
5
|
+
const owner = Buffer.from('com.apple.streaming.transportStreamTimestamp\0');
|
|
6
|
+
const payload = Buffer.alloc(owner.length + 8);
|
|
7
|
+
owner.copy(payload);
|
|
8
|
+
payload.writeBigUInt64BE(transportTimestamp90k, owner.length);
|
|
9
|
+
const frame = Buffer.alloc(10);
|
|
10
|
+
frame.write('PRIV');
|
|
11
|
+
writeSynchsafe(frame, 4, payload.length);
|
|
12
|
+
const tag = Buffer.alloc(10);
|
|
13
|
+
tag.write('ID3');
|
|
14
|
+
tag[3] = 4;
|
|
15
|
+
writeSynchsafe(tag, 6, frame.length + payload.length);
|
|
16
|
+
return Buffer.concat([tag, frame, payload, mp3]);
|
|
17
|
+
}
|
|
18
|
+
function writeSynchsafe(buffer, offset, size) {
|
|
19
|
+
for (let i = 3; i >= 0; i--) {
|
|
20
|
+
buffer[offset + i] = size & 127;
|
|
21
|
+
size >>>= 7;
|
|
22
|
+
}
|
|
23
|
+
}
|