brainferno-mcp-bridge 0.1.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/CHANGELOG.md +19 -0
- package/LICENSE +202 -0
- package/NOTICE +27 -0
- package/README.md +410 -0
- package/THIRD_PARTY_NOTICES.md +52 -0
- package/dist/bridge/handshake.d.ts +34 -0
- package/dist/bridge/handshake.js +36 -0
- package/dist/bridge/handshake.js.map +1 -0
- package/dist/bridge/script-escape.d.ts +16 -0
- package/dist/bridge/script-escape.js +47 -0
- package/dist/bridge/script-escape.js.map +1 -0
- package/dist/bridge/socket.d.ts +79 -0
- package/dist/bridge/socket.js +424 -0
- package/dist/bridge/socket.js.map +1 -0
- package/dist/bridge/types.d.ts +49 -0
- package/dist/bridge/types.js +38 -0
- package/dist/bridge/types.js.map +1 -0
- package/dist/config.d.ts +87 -0
- package/dist/config.js +172 -0
- package/dist/config.js.map +1 -0
- package/dist/drivers/ame-webservice.d.ts +118 -0
- package/dist/drivers/ame-webservice.js +356 -0
- package/dist/drivers/ame-webservice.js.map +1 -0
- package/dist/drivers/illustrator-delegate.d.ts +71 -0
- package/dist/drivers/illustrator-delegate.js +120 -0
- package/dist/drivers/illustrator-delegate.js.map +1 -0
- package/dist/drivers/osscript.d.ts +60 -0
- package/dist/drivers/osscript.js +230 -0
- package/dist/drivers/osscript.js.map +1 -0
- package/dist/http.d.ts +26 -0
- package/dist/http.js +126 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/install/cli.d.ts +2 -0
- package/dist/install/cli.js +310 -0
- package/dist/install/cli.js.map +1 -0
- package/dist/install/lib.d.ts +76 -0
- package/dist/install/lib.js +194 -0
- package/dist/install/lib.js.map +1 -0
- package/dist/jobs.d.ts +117 -0
- package/dist/jobs.js +196 -0
- package/dist/jobs.js.map +1 -0
- package/dist/logging.d.ts +8 -0
- package/dist/logging.js +33 -0
- package/dist/logging.js.map +1 -0
- package/dist/server.d.ts +38 -0
- package/dist/server.js +90 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/after-effects.d.ts +100 -0
- package/dist/tools/after-effects.js +695 -0
- package/dist/tools/after-effects.js.map +1 -0
- package/dist/tools/audio.d.ts +118 -0
- package/dist/tools/audio.js +356 -0
- package/dist/tools/audio.js.map +1 -0
- package/dist/tools/audition.d.ts +15 -0
- package/dist/tools/audition.js +329 -0
- package/dist/tools/audition.js.map +1 -0
- package/dist/tools/diagnostics.d.ts +17 -0
- package/dist/tools/diagnostics.js +61 -0
- package/dist/tools/diagnostics.js.map +1 -0
- package/dist/tools/illustrator-delegate.d.ts +9 -0
- package/dist/tools/illustrator-delegate.js +61 -0
- package/dist/tools/illustrator-delegate.js.map +1 -0
- package/dist/tools/illustrator.d.ts +36 -0
- package/dist/tools/illustrator.js +299 -0
- package/dist/tools/illustrator.js.map +1 -0
- package/dist/tools/jobs.d.ts +39 -0
- package/dist/tools/jobs.js +83 -0
- package/dist/tools/jobs.js.map +1 -0
- package/dist/tools/media-encoder.d.ts +17 -0
- package/dist/tools/media-encoder.js +180 -0
- package/dist/tools/media-encoder.js.map +1 -0
- package/dist/tools/photoshop.d.ts +17 -0
- package/dist/tools/photoshop.js +202 -0
- package/dist/tools/photoshop.js.map +1 -0
- package/dist/tools/pipelines.d.ts +23 -0
- package/dist/tools/pipelines.js +254 -0
- package/dist/tools/pipelines.js.map +1 -0
- package/dist/tools/premiere.d.ts +26 -0
- package/dist/tools/premiere.js +396 -0
- package/dist/tools/premiere.js.map +1 -0
- package/dist/tools/result.d.ts +17 -0
- package/dist/tools/result.js +51 -0
- package/dist/tools/result.js.map +1 -0
- package/package.json +64 -0
- package/panels/panel-cep/CSXS/manifest.xml +53 -0
- package/panels/panel-cep/bridge-client.js +204 -0
- package/panels/panel-cep/host.jsx +62 -0
- package/panels/panel-cep/icons/logo.svg +61 -0
- package/panels/panel-cep/index.html +48 -0
- package/panels/panel-cep/main.js +173 -0
- package/panels/panel-uxp/README.md +36 -0
- package/panels/panel-uxp/bridge-client.js +204 -0
- package/panels/panel-uxp/commands.js +375 -0
- package/panels/panel-uxp/icons/icon.png +0 -0
- package/panels/panel-uxp/icons/icon@2x.png +0 -0
- package/panels/panel-uxp/icons/logo.svg +61 -0
- package/panels/panel-uxp/index.html +47 -0
- package/panels/panel-uxp/main.js +109 -0
- package/panels/panel-uxp/manifest.json +89 -0
- package/panels/panel-uxp-ppro/README.md +11 -0
- package/panels/panel-uxp-ppro/bridge-client.js +204 -0
- package/panels/panel-uxp-ppro/commands.js +797 -0
- package/panels/panel-uxp-ppro/icons/icon.png +0 -0
- package/panels/panel-uxp-ppro/icons/icon@2x.png +0 -0
- package/panels/panel-uxp-ppro/icons/logo.svg +61 -0
- package/panels/panel-uxp-ppro/index.html +64 -0
- package/panels/panel-uxp-ppro/main.js +113 -0
- package/panels/panel-uxp-ppro/manifest.json +65 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/** Runtime configuration, read once at startup from the environment. */
|
|
2
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { defaultHandshakePath } from "./bridge/handshake.js";
|
|
6
|
+
export const DEFAULT_ILLUSTRATOR_MCP_URL = "http://localhost:18412/v1/mcp";
|
|
7
|
+
const LOG_LEVELS = ["error", "warn", "info", "debug"];
|
|
8
|
+
const LEGACY_ENV_PREFIX = "ADOBE_CC_MCP_";
|
|
9
|
+
const warnedLegacy = new Set();
|
|
10
|
+
/** Read BRAINFERNO_MCP_X, falling back to the pre-rename ADOBE_CC_MCP_X with a one-time warning. */
|
|
11
|
+
export function envValue(name) {
|
|
12
|
+
const v = process.env[name];
|
|
13
|
+
if (v !== undefined)
|
|
14
|
+
return v;
|
|
15
|
+
const legacy = name.replace(/^BRAINFERNO_MCP_/, LEGACY_ENV_PREFIX);
|
|
16
|
+
const l = process.env[legacy];
|
|
17
|
+
if (l !== undefined && !warnedLegacy.has(legacy)) {
|
|
18
|
+
warnedLegacy.add(legacy);
|
|
19
|
+
console.error(`[brainferno-mcp-bridge] WARN ${legacy} is deprecated; rename it to ${name}`);
|
|
20
|
+
}
|
|
21
|
+
return l;
|
|
22
|
+
}
|
|
23
|
+
/** Settings live in ~/.brainferno-mcp-bridge; copy config.json from the pre-rename ~/.adobe-cc-mcp once. */
|
|
24
|
+
export function migrateLegacyUserDir(home = homedir()) {
|
|
25
|
+
const next = join(home, ".brainferno-mcp-bridge");
|
|
26
|
+
const old = join(home, ".adobe-cc-mcp");
|
|
27
|
+
try {
|
|
28
|
+
if (!existsSync(join(next, "config.json")) && existsSync(join(old, "config.json"))) {
|
|
29
|
+
mkdirSync(next, { recursive: true });
|
|
30
|
+
copyFileSync(join(old, "config.json"), join(next, "config.json"));
|
|
31
|
+
try {
|
|
32
|
+
chmodSync(join(next, "config.json"), 0o600);
|
|
33
|
+
}
|
|
34
|
+
catch { /* Windows */ }
|
|
35
|
+
return old;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* best effort */
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/** Keys the installer may write to `~/.brainferno-mcp-bridge/config.json` (mode 600). */
|
|
44
|
+
/** Everything the installer can switch on or off. */
|
|
45
|
+
export const INSTALLABLE_APPS = ["photoshop", "after_effects", "premiere", "illustrator", "audition", "media_encoder"];
|
|
46
|
+
export function parseApps(raw, fallback = INSTALLABLE_APPS) {
|
|
47
|
+
if (raw === undefined || raw.trim() === "" || raw.trim().toLowerCase() === "all")
|
|
48
|
+
return [...fallback];
|
|
49
|
+
const aliases = {
|
|
50
|
+
ps: "photoshop",
|
|
51
|
+
photoshop: "photoshop",
|
|
52
|
+
ae: "after_effects",
|
|
53
|
+
aftereffects: "after_effects",
|
|
54
|
+
after_effects: "after_effects",
|
|
55
|
+
"after-effects": "after_effects",
|
|
56
|
+
ppro: "premiere",
|
|
57
|
+
pr: "premiere",
|
|
58
|
+
premiere: "premiere",
|
|
59
|
+
premierepro: "premiere",
|
|
60
|
+
ai: "illustrator",
|
|
61
|
+
illustrator: "illustrator",
|
|
62
|
+
au: "audition",
|
|
63
|
+
audition: "audition",
|
|
64
|
+
ame: "media_encoder",
|
|
65
|
+
mediaencoder: "media_encoder",
|
|
66
|
+
media_encoder: "media_encoder",
|
|
67
|
+
"media-encoder": "media_encoder",
|
|
68
|
+
};
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const part of raw.split(/[,\s]+/)) {
|
|
71
|
+
const id = aliases[part.trim().toLowerCase()];
|
|
72
|
+
if (!id)
|
|
73
|
+
throw new Error(`Unknown app "${part}". Use: ${INSTALLABLE_APPS.join(", ")}`);
|
|
74
|
+
if (!out.includes(id))
|
|
75
|
+
out.push(id);
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
export function userConfigPath() {
|
|
80
|
+
return join(homedir(), ".brainferno-mcp-bridge", "config.json");
|
|
81
|
+
}
|
|
82
|
+
/** The user config file, or {} when missing/malformed. */
|
|
83
|
+
export function readUserConfig() {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(readFileSync(userConfigPath(), "utf8"));
|
|
86
|
+
const out = {};
|
|
87
|
+
if (Array.isArray(parsed["enabledApps"])) {
|
|
88
|
+
const apps = parsed["enabledApps"].filter((a) => typeof a === "string" && INSTALLABLE_APPS.includes(a));
|
|
89
|
+
out.enabledApps = apps;
|
|
90
|
+
}
|
|
91
|
+
if (typeof parsed["illustratorKey"] === "string")
|
|
92
|
+
out.illustratorKey = parsed["illustratorKey"];
|
|
93
|
+
if (typeof parsed["illustratorUrl"] === "string")
|
|
94
|
+
out.illustratorUrl = parsed["illustratorUrl"];
|
|
95
|
+
if (typeof parsed["httpPort"] === "number")
|
|
96
|
+
out.httpPort = parsed["httpPort"];
|
|
97
|
+
if (typeof parsed["httpHost"] === "string")
|
|
98
|
+
out.httpHost = parsed["httpHost"];
|
|
99
|
+
if (typeof parsed["httpToken"] === "string")
|
|
100
|
+
out.httpToken = parsed["httpToken"];
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The Illustrator delegate key: env first, then the user config file, so the
|
|
109
|
+
* user can paste it once without editing the client config. "" = disabled.
|
|
110
|
+
*/
|
|
111
|
+
function illustratorKeyFromEnvOrFile(file) {
|
|
112
|
+
const fromEnv = envValue("BRAINFERNO_MCP_ILLUSTRATOR_KEY");
|
|
113
|
+
if (fromEnv !== undefined && fromEnv !== "")
|
|
114
|
+
return fromEnv;
|
|
115
|
+
return file.illustratorKey ?? "";
|
|
116
|
+
}
|
|
117
|
+
function intFromEnv(name, fallback, { allowZero = false } = {}) {
|
|
118
|
+
const raw = envValue(name);
|
|
119
|
+
if (raw === undefined || raw === "")
|
|
120
|
+
return fallback;
|
|
121
|
+
const parsed = Number.parseInt(raw, 10);
|
|
122
|
+
const floor = allowZero ? 0 : 1;
|
|
123
|
+
if (!Number.isFinite(parsed) || parsed < floor) {
|
|
124
|
+
throw new Error(`${name} must be an integer >= ${floor}, got ${JSON.stringify(raw)}`);
|
|
125
|
+
}
|
|
126
|
+
return parsed;
|
|
127
|
+
}
|
|
128
|
+
function boolFromEnv(name) {
|
|
129
|
+
const raw = envValue(name);
|
|
130
|
+
return raw === "1" || raw === "true";
|
|
131
|
+
}
|
|
132
|
+
function logLevelFromEnv() {
|
|
133
|
+
const raw = envValue("BRAINFERNO_MCP_LOG_LEVEL");
|
|
134
|
+
if (raw === undefined || raw === "")
|
|
135
|
+
return "info";
|
|
136
|
+
if (!LOG_LEVELS.includes(raw)) {
|
|
137
|
+
throw new Error(`BRAINFERNO_MCP_LOG_LEVEL must be one of ${LOG_LEVELS.join(", ")}, got ${raw}`);
|
|
138
|
+
}
|
|
139
|
+
return raw;
|
|
140
|
+
}
|
|
141
|
+
export function loadConfig() {
|
|
142
|
+
const file = readUserConfig();
|
|
143
|
+
return {
|
|
144
|
+
// Port 0 is allowed so the OS can assign one (used in tests); the handshake
|
|
145
|
+
// file makes the real port discoverable regardless.
|
|
146
|
+
bridgePort: intFromEnv("BRAINFERNO_MCP_BRIDGE_PORT", 7897, { allowZero: true }),
|
|
147
|
+
bridgeToken: envValue("BRAINFERNO_MCP_BRIDGE_TOKEN") ?? "",
|
|
148
|
+
bridgeInsecure: boolFromEnv("BRAINFERNO_MCP_BRIDGE_INSECURE"),
|
|
149
|
+
evalTimeoutMs: intFromEnv("BRAINFERNO_MCP_EVAL_TIMEOUT_MS", 30_000),
|
|
150
|
+
// 0 disables the heartbeat.
|
|
151
|
+
heartbeatIntervalMs: intFromEnv("BRAINFERNO_MCP_HEARTBEAT_MS", 15_000, { allowZero: true }),
|
|
152
|
+
allowRawScripts: boolFromEnv("BRAINFERNO_MCP_ALLOW_RAW_SCRIPTS"),
|
|
153
|
+
handshakeFilePath: envValue("BRAINFERNO_MCP_HANDSHAKE_FILE") ?? defaultHandshakePath(),
|
|
154
|
+
allowedOrigins: (envValue("BRAINFERNO_MCP_ALLOWED_ORIGINS") ?? "")
|
|
155
|
+
.split(",")
|
|
156
|
+
.map((o) => o.trim())
|
|
157
|
+
.filter((o) => o !== ""),
|
|
158
|
+
illustratorMcpUrl: envValue("BRAINFERNO_MCP_ILLUSTRATOR_URL") ?? file.illustratorUrl ?? DEFAULT_ILLUSTRATOR_MCP_URL,
|
|
159
|
+
illustratorMcpKey: illustratorKeyFromEnvOrFile(file),
|
|
160
|
+
ffmpegPath: envValue("BRAINFERNO_MCP_FFMPEG") ?? "ffmpeg",
|
|
161
|
+
ffprobePath: envValue("BRAINFERNO_MCP_FFPROBE") ?? "ffprobe",
|
|
162
|
+
ameWebServicePath: envValue("BRAINFERNO_MCP_AME_WEBSERVICE") ?? "",
|
|
163
|
+
amePort: intFromEnv("BRAINFERNO_MCP_AME_PORT", 0, { allowZero: true }),
|
|
164
|
+
ameIdleMs: intFromEnv("BRAINFERNO_MCP_AME_IDLE_MS", 10 * 60_000, { allowZero: true }),
|
|
165
|
+
httpPort: intFromEnv("BRAINFERNO_MCP_HTTP_PORT", file.httpPort ?? 0, { allowZero: true }),
|
|
166
|
+
httpHost: envValue("BRAINFERNO_MCP_HTTP_HOST") ?? file.httpHost ?? "127.0.0.1",
|
|
167
|
+
httpToken: envValue("BRAINFERNO_MCP_HTTP_TOKEN") ?? file.httpToken ?? "",
|
|
168
|
+
enabledApps: parseApps(envValue("BRAINFERNO_MCP_APPS"), file.enabledApps ?? INSTALLABLE_APPS),
|
|
169
|
+
logLevel: logLevelFromEnv(),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adobe Media Encoder's built-in "Remote AME" web service
|
|
3
|
+
* (`ame_webservice_console.exe`, shipped with every AME since CC 2014).
|
|
4
|
+
*
|
|
5
|
+
* The console starts a hidden AME renderer and serves a tiny XML-over-HTTP
|
|
6
|
+
* API: GET /server, GET/POST/DELETE /job, GET /history. One job at a time;
|
|
7
|
+
* sources can be media files, a .prproj (+ sequence GUID) or FCP XML; the
|
|
8
|
+
* encoder settings come from an .epr preset. Verified on AME 26.3.2 (Windows):
|
|
9
|
+
* listener up in ~5 s, a WAV→MP3 job accepted and finished in <5 s.
|
|
10
|
+
*
|
|
11
|
+
* Facts this driver is built on:
|
|
12
|
+
* - The service binds the address in `ame_webservice_config.ini` beside the
|
|
13
|
+
* exe; with `ip` unset it picks a LAN adapter, not loopback. We probe every
|
|
14
|
+
* local IPv4 address for the configured port.
|
|
15
|
+
* - Killing the console leaves the renderer alive: stop with a tree kill.
|
|
16
|
+
* - Job status strings seen: Queued, Encoding, Success; failures carry
|
|
17
|
+
* "Fail"/"Error" and abort "Abort" in JobStatus, with <Details>.
|
|
18
|
+
*/
|
|
19
|
+
export interface AmeWebServiceOptions {
|
|
20
|
+
/** Path to ame_webservice_console(.exe); "" = auto-detect. */
|
|
21
|
+
exePath: string;
|
|
22
|
+
/** Port override; 0 = read `port` from the ini beside the exe (default 8080). */
|
|
23
|
+
port: number;
|
|
24
|
+
/** Extra command-line arguments for the console. */
|
|
25
|
+
extraArgs: string[];
|
|
26
|
+
/** Stop the service after this long without a job. 0 = never. */
|
|
27
|
+
idleMs: number;
|
|
28
|
+
/** For tests: fixed base URL of an already-running service; skips spawning. */
|
|
29
|
+
baseUrl?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface AmeServerInfo {
|
|
32
|
+
serverStatus: string;
|
|
33
|
+
jobStatus: string;
|
|
34
|
+
jobId: string;
|
|
35
|
+
jobProgress: string;
|
|
36
|
+
details: string;
|
|
37
|
+
serverIp?: string;
|
|
38
|
+
serverPort?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface AmeJobInfo {
|
|
41
|
+
jobId: string;
|
|
42
|
+
jobStatus: string;
|
|
43
|
+
jobProgress: string;
|
|
44
|
+
details: string;
|
|
45
|
+
sourcePresetPath?: string;
|
|
46
|
+
sourceFilePath?: string;
|
|
47
|
+
destinationPath?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface AmeSubmitRequest {
|
|
50
|
+
sourcePath: string;
|
|
51
|
+
presetPath: string;
|
|
52
|
+
destinationPath: string;
|
|
53
|
+
sequenceGuid?: string;
|
|
54
|
+
overwrite?: boolean;
|
|
55
|
+
notificationTarget?: string;
|
|
56
|
+
}
|
|
57
|
+
export declare function isTerminalStatus(status: string): boolean;
|
|
58
|
+
export declare function isSuccessStatus(status: string): boolean;
|
|
59
|
+
/** First text inside <tag>…</tag>, or "" — the payloads are flat XML. */
|
|
60
|
+
export declare function tag(xml: string, name: string): string;
|
|
61
|
+
export declare function parseServer(xml: string): AmeServerInfo;
|
|
62
|
+
export declare function parseJob(xml: string): AmeJobInfo;
|
|
63
|
+
/** History payload: the current job fields plus <CompletedJobs><Job>…</Job></CompletedJobs>. */
|
|
64
|
+
export declare function parseHistory(xml: string): AmeJobInfo[];
|
|
65
|
+
export declare function buildManifest(r: AmeSubmitRequest): string;
|
|
66
|
+
/** Newest "Adobe Media Encoder <year>" console beside the other Adobe apps. */
|
|
67
|
+
export declare function detectConsoleExe(platform?: NodeJS.Platform): Promise<string | null>;
|
|
68
|
+
/** `port = 8080` from the ini beside the exe; 8080 when unreadable. */
|
|
69
|
+
export declare function portFromIni(exePath: string): Promise<number>;
|
|
70
|
+
export declare function httpRequest(baseUrl: string, method: "GET" | "POST" | "DELETE", path: string, body?: string, timeoutMs?: number): Promise<{
|
|
71
|
+
status: number;
|
|
72
|
+
text: string;
|
|
73
|
+
}>;
|
|
74
|
+
/** Loopback first, then every non-internal IPv4 address — the service may bind either. */
|
|
75
|
+
export declare function candidateHosts(): string[];
|
|
76
|
+
export declare class AmeWebService {
|
|
77
|
+
private readonly options;
|
|
78
|
+
private child;
|
|
79
|
+
private baseUrl;
|
|
80
|
+
private idleTimer;
|
|
81
|
+
private starting;
|
|
82
|
+
private exe;
|
|
83
|
+
constructor(options: AmeWebServiceOptions);
|
|
84
|
+
get address(): string | null;
|
|
85
|
+
get isRunning(): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Raw node:http on purpose: the service parses header names case-sensitively
|
|
88
|
+
* and never answers a POST whose `content-length` is lowercase (as `fetch`
|
|
89
|
+
* sends it). Verified live on 26.3.2.
|
|
90
|
+
*/
|
|
91
|
+
private http;
|
|
92
|
+
/** Find a service already answering on any local address (started by us or by the user). */
|
|
93
|
+
discover(port: number): Promise<string | null>;
|
|
94
|
+
private resolveExeAndPort;
|
|
95
|
+
/** Start the console if nothing answers yet; resolves with the base URL. */
|
|
96
|
+
ensureRunning(): Promise<string>;
|
|
97
|
+
private touch;
|
|
98
|
+
server(): Promise<AmeServerInfo>;
|
|
99
|
+
job(): Promise<AmeJobInfo>;
|
|
100
|
+
history(): Promise<AmeJobInfo[]>;
|
|
101
|
+
submit(req: AmeSubmitRequest, opts?: {
|
|
102
|
+
busyWaitMs?: number;
|
|
103
|
+
signal?: AbortSignal;
|
|
104
|
+
}): Promise<AmeJobInfo & {
|
|
105
|
+
submitResult: string;
|
|
106
|
+
}>;
|
|
107
|
+
cancel(jobId: string): Promise<AmeJobInfo>;
|
|
108
|
+
/** Poll /job until the given job reaches a terminal state. */
|
|
109
|
+
waitForJob(jobId: string, opts: {
|
|
110
|
+
timeoutMs: number;
|
|
111
|
+
signal?: AbortSignal;
|
|
112
|
+
onProgress?: (j: AmeJobInfo) => void;
|
|
113
|
+
intervalMs?: number;
|
|
114
|
+
}): Promise<AmeJobInfo>;
|
|
115
|
+
/** Stop the console we started (tree kill: the renderer it spawned goes too). */
|
|
116
|
+
stop(): Promise<void>;
|
|
117
|
+
get executable(): string | null;
|
|
118
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { request } from "node:http";
|
|
4
|
+
import { networkInterfaces } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { log } from "../logging.js";
|
|
7
|
+
const TERMINAL = /success|fail|error|abort|cancel/i;
|
|
8
|
+
export function isTerminalStatus(status) {
|
|
9
|
+
return TERMINAL.test(status);
|
|
10
|
+
}
|
|
11
|
+
export function isSuccessStatus(status) {
|
|
12
|
+
return /success/i.test(status);
|
|
13
|
+
}
|
|
14
|
+
/** First text inside <tag>…</tag>, or "" — the payloads are flat XML. */
|
|
15
|
+
export function tag(xml, name) {
|
|
16
|
+
const m = new RegExp(`<${name}>([\\s\\S]*?)</${name}>`).exec(xml);
|
|
17
|
+
return m ? decodeXml(m[1] ?? "") : "";
|
|
18
|
+
}
|
|
19
|
+
function decodeXml(s) {
|
|
20
|
+
return s.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&").trim();
|
|
21
|
+
}
|
|
22
|
+
function encodeXml(s) {
|
|
23
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
24
|
+
}
|
|
25
|
+
export function parseServer(xml) {
|
|
26
|
+
return {
|
|
27
|
+
serverStatus: tag(xml, "ServerStatus"),
|
|
28
|
+
jobStatus: tag(xml, "JobStatus"),
|
|
29
|
+
jobId: tag(xml, "JobId"),
|
|
30
|
+
jobProgress: tag(xml, "JobProgress"),
|
|
31
|
+
details: tag(xml, "Details"),
|
|
32
|
+
...(tag(xml, "ServerIP") ? { serverIp: tag(xml, "ServerIP") } : {}),
|
|
33
|
+
...(tag(xml, "ServerPort") ? { serverPort: tag(xml, "ServerPort") } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function parseJob(xml) {
|
|
37
|
+
const j = { jobId: tag(xml, "JobId"), jobStatus: tag(xml, "JobStatus"), jobProgress: tag(xml, "JobProgress"), details: tag(xml, "Details") };
|
|
38
|
+
for (const [k, t] of [
|
|
39
|
+
["sourcePresetPath", "SourcePresetPath"],
|
|
40
|
+
["sourceFilePath", "SourceFilePath"],
|
|
41
|
+
["destinationPath", "DestinationPath"],
|
|
42
|
+
]) {
|
|
43
|
+
const v = tag(xml, t);
|
|
44
|
+
if (v)
|
|
45
|
+
j[k] = v;
|
|
46
|
+
}
|
|
47
|
+
return j;
|
|
48
|
+
}
|
|
49
|
+
/** History payload: the current job fields plus <CompletedJobs><Job>…</Job></CompletedJobs>. */
|
|
50
|
+
export function parseHistory(xml) {
|
|
51
|
+
const out = [];
|
|
52
|
+
const re = /<Job>([\s\S]*?)<\/Job>/g;
|
|
53
|
+
let m;
|
|
54
|
+
while ((m = re.exec(xml)) !== null)
|
|
55
|
+
out.push(parseJob(m[1] ?? ""));
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
export function buildManifest(r) {
|
|
59
|
+
const lines = [
|
|
60
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
61
|
+
"<!DOCTYPE manifest>",
|
|
62
|
+
'<manifest version="1.0">',
|
|
63
|
+
`<SourcePresetPath>${encodeXml(r.presetPath)}</SourcePresetPath>`,
|
|
64
|
+
`<SourceFilePath>${encodeXml(r.sourcePath)}</SourceFilePath>`,
|
|
65
|
+
`<DestinationPath>${encodeXml(r.destinationPath)}</DestinationPath>`,
|
|
66
|
+
];
|
|
67
|
+
if (r.overwrite)
|
|
68
|
+
lines.push("<OverwriteDestinationIfPresent>true</OverwriteDestinationIfPresent>");
|
|
69
|
+
if (r.sequenceGuid)
|
|
70
|
+
lines.push(`<SequenceGUID>${encodeXml(r.sequenceGuid)}</SequenceGUID>`);
|
|
71
|
+
if (r.notificationTarget)
|
|
72
|
+
lines.push(`<NotificationTarget>${encodeXml(r.notificationTarget)}</NotificationTarget>`);
|
|
73
|
+
lines.push("</manifest>");
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
76
|
+
/** Newest "Adobe Media Encoder <year>" console beside the other Adobe apps. */
|
|
77
|
+
export async function detectConsoleExe(platform = process.platform) {
|
|
78
|
+
const roots = platform === "win32" ? [join(process.env["ProgramFiles"] ?? "C:\\Program Files", "Adobe")] : platform === "darwin" ? ["/Applications"] : [];
|
|
79
|
+
for (const root of roots) {
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = await readdir(root);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const versions = entries.filter((e) => /^Adobe Media Encoder/i.test(e)).sort().reverse();
|
|
88
|
+
for (const v of versions) {
|
|
89
|
+
const candidates = platform === "win32" ? [join(root, v, "ame_webservice_console.exe")] : [join(root, v, "ame_webservice_console"), join(root, v, `${v}.app`, "Contents", "MacOS", "ame_webservice_console")];
|
|
90
|
+
for (const c of candidates) {
|
|
91
|
+
try {
|
|
92
|
+
await stat(c);
|
|
93
|
+
return c;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* next */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
/** `port = 8080` from the ini beside the exe; 8080 when unreadable. */
|
|
104
|
+
export async function portFromIni(exePath) {
|
|
105
|
+
try {
|
|
106
|
+
const ini = await readFile(join(dirname(exePath), "ame_webservice_config.ini"), "utf8");
|
|
107
|
+
const m = /^\s*port\s*=\s*(\d+)/m.exec(ini);
|
|
108
|
+
if (m)
|
|
109
|
+
return Number(m[1]);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* fall through */
|
|
113
|
+
}
|
|
114
|
+
return 8080;
|
|
115
|
+
}
|
|
116
|
+
export function httpRequest(baseUrl, method, path, body, timeoutMs = 15_000) {
|
|
117
|
+
const url = new URL(baseUrl + path);
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
const headers = { Host: url.host, Connection: "close" };
|
|
120
|
+
if (body !== undefined) {
|
|
121
|
+
headers["Content-Type"] = "application/xml";
|
|
122
|
+
headers["Content-Length"] = Buffer.byteLength(body);
|
|
123
|
+
}
|
|
124
|
+
const req = request({ host: url.hostname, port: Number(url.port) || 80, method, path: url.pathname + url.search, headers }, (res) => {
|
|
125
|
+
let text = "";
|
|
126
|
+
res.setEncoding("utf8");
|
|
127
|
+
res.on("data", (d) => (text += d));
|
|
128
|
+
res.on("end", () => resolve({ status: res.statusCode ?? 0, text }));
|
|
129
|
+
});
|
|
130
|
+
req.setTimeout(timeoutMs, () => req.destroy(new Error(`Media Encoder web service did not answer ${method} ${path} within ${Math.round(timeoutMs / 1000)} s`)));
|
|
131
|
+
req.on("error", reject);
|
|
132
|
+
req.end(body);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/** Loopback first, then every non-internal IPv4 address — the service may bind either. */
|
|
136
|
+
export function candidateHosts() {
|
|
137
|
+
const hosts = ["127.0.0.1"];
|
|
138
|
+
for (const list of Object.values(networkInterfaces())) {
|
|
139
|
+
for (const i of list ?? []) {
|
|
140
|
+
if (i.family === "IPv4" && !i.internal && !hosts.includes(i.address))
|
|
141
|
+
hosts.push(i.address);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return hosts;
|
|
145
|
+
}
|
|
146
|
+
export class AmeWebService {
|
|
147
|
+
options;
|
|
148
|
+
child = null;
|
|
149
|
+
baseUrl = null;
|
|
150
|
+
idleTimer = null;
|
|
151
|
+
starting = null;
|
|
152
|
+
exe = null;
|
|
153
|
+
constructor(options) {
|
|
154
|
+
this.options = options;
|
|
155
|
+
if (options.baseUrl)
|
|
156
|
+
this.baseUrl = options.baseUrl;
|
|
157
|
+
}
|
|
158
|
+
get address() {
|
|
159
|
+
return this.baseUrl;
|
|
160
|
+
}
|
|
161
|
+
get isRunning() {
|
|
162
|
+
return this.baseUrl !== null;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Raw node:http on purpose: the service parses header names case-sensitively
|
|
166
|
+
* and never answers a POST whose `content-length` is lowercase (as `fetch`
|
|
167
|
+
* sends it). Verified live on 26.3.2.
|
|
168
|
+
*/
|
|
169
|
+
http(method, path, body, timeoutMs = 15_000) {
|
|
170
|
+
if (!this.baseUrl)
|
|
171
|
+
return Promise.reject(new Error("Media Encoder web service is not running."));
|
|
172
|
+
return httpRequest(this.baseUrl, method, path, body, timeoutMs);
|
|
173
|
+
}
|
|
174
|
+
/** Find a service already answering on any local address (started by us or by the user). */
|
|
175
|
+
async discover(port) {
|
|
176
|
+
for (const host of candidateHosts()) {
|
|
177
|
+
const url = `http://${host}:${port}`;
|
|
178
|
+
try {
|
|
179
|
+
const res = await httpRequest(url, "GET", "/server", undefined, 1500);
|
|
180
|
+
if (res.status === 200 && /<ServerStatus>/.test(res.text))
|
|
181
|
+
return url;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
/* not here */
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
async resolveExeAndPort() {
|
|
190
|
+
const exe = this.options.exePath || (await detectConsoleExe());
|
|
191
|
+
const port = this.options.port || (exe ? await portFromIni(exe) : 8080);
|
|
192
|
+
return { exe, port };
|
|
193
|
+
}
|
|
194
|
+
/** Start the console if nothing answers yet; resolves with the base URL. */
|
|
195
|
+
async ensureRunning() {
|
|
196
|
+
if (this.baseUrl) {
|
|
197
|
+
try {
|
|
198
|
+
await this.http("GET", "/server", undefined, 3000);
|
|
199
|
+
this.touch();
|
|
200
|
+
return this.baseUrl;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
this.baseUrl = null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (this.starting)
|
|
207
|
+
return this.starting;
|
|
208
|
+
this.starting = (async () => {
|
|
209
|
+
const { exe, port } = await this.resolveExeAndPort();
|
|
210
|
+
const found = await this.discover(port);
|
|
211
|
+
if (found) {
|
|
212
|
+
this.baseUrl = found;
|
|
213
|
+
log.info(`Media Encoder web service already running at ${found}`);
|
|
214
|
+
this.touch();
|
|
215
|
+
return found;
|
|
216
|
+
}
|
|
217
|
+
if (!exe)
|
|
218
|
+
throw new Error("Adobe Media Encoder's ame_webservice_console was not found. Install Media Encoder or set BRAINFERNO_MCP_AME_WEBSERVICE to its path.");
|
|
219
|
+
this.exe = exe;
|
|
220
|
+
log.info(`starting Media Encoder web service: ${exe} ${this.options.extraArgs.join(" ")}`);
|
|
221
|
+
this.child = spawn(exe, this.options.extraArgs, { stdio: "ignore", windowsHide: true, detached: process.platform !== "win32" });
|
|
222
|
+
this.child.on("exit", (code) => {
|
|
223
|
+
log.info(`Media Encoder web service exited (${code})`);
|
|
224
|
+
this.child = null;
|
|
225
|
+
this.baseUrl = null;
|
|
226
|
+
});
|
|
227
|
+
const deadline = Date.now() + 120_000;
|
|
228
|
+
while (Date.now() < deadline) {
|
|
229
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
230
|
+
if (!this.child)
|
|
231
|
+
throw new Error("Media Encoder web service exited during startup.");
|
|
232
|
+
const url = await this.discover(port);
|
|
233
|
+
if (url) {
|
|
234
|
+
this.baseUrl = url;
|
|
235
|
+
this.touch();
|
|
236
|
+
log.info(`Media Encoder web service up at ${url}`);
|
|
237
|
+
return url;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
await this.stop();
|
|
241
|
+
throw new Error(`Media Encoder web service did not answer on port ${port} within 120 s.`);
|
|
242
|
+
})();
|
|
243
|
+
try {
|
|
244
|
+
return await this.starting;
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
this.starting = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
touch() {
|
|
251
|
+
if (this.idleTimer)
|
|
252
|
+
clearTimeout(this.idleTimer);
|
|
253
|
+
if (this.options.idleMs > 0 && this.child) {
|
|
254
|
+
this.idleTimer = setTimeout(() => {
|
|
255
|
+
void this.stop().catch(() => undefined);
|
|
256
|
+
}, this.options.idleMs);
|
|
257
|
+
this.idleTimer.unref();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async server() {
|
|
261
|
+
const r = await this.http("GET", "/server");
|
|
262
|
+
this.touch();
|
|
263
|
+
return parseServer(r.text);
|
|
264
|
+
}
|
|
265
|
+
async job() {
|
|
266
|
+
const r = await this.http("GET", "/job");
|
|
267
|
+
this.touch();
|
|
268
|
+
return parseJob(r.text);
|
|
269
|
+
}
|
|
270
|
+
async history() {
|
|
271
|
+
const r = await this.http("GET", "/history");
|
|
272
|
+
this.touch();
|
|
273
|
+
return parseHistory(r.text);
|
|
274
|
+
}
|
|
275
|
+
async submit(req, opts = {}) {
|
|
276
|
+
const manifest = buildManifest(req);
|
|
277
|
+
const deadline = Date.now() + (opts.busyWaitMs ?? 30 * 60_000);
|
|
278
|
+
for (;;) {
|
|
279
|
+
// The service loads the source before answering; a .prproj comes in through
|
|
280
|
+
// Dynamic Link and can take minutes on a cold renderer.
|
|
281
|
+
const r = await this.http("POST", "/job", manifest, 10 * 60_000);
|
|
282
|
+
this.touch();
|
|
283
|
+
const info = parseJob(r.text);
|
|
284
|
+
const submitResult = tag(r.text, "SubmitResult") || (r.status === 200 ? "Accepted" : `HTTP ${r.status}`);
|
|
285
|
+
if (r.status === 200 && /accept/i.test(submitResult))
|
|
286
|
+
return { ...info, submitResult };
|
|
287
|
+
// One job at a time: "Busy" while another job (maybe submitted elsewhere) runs.
|
|
288
|
+
if (/busy/i.test(submitResult) && Date.now() < deadline && !opts.signal?.aborted) {
|
|
289
|
+
await new Promise((res) => setTimeout(res, 3000));
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
throw new Error(`Media Encoder refused the job (${submitResult}): ${info.details || r.text.slice(0, 300)}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async cancel(jobId) {
|
|
296
|
+
const r = await this.http("DELETE", `/job?jobID=${encodeURIComponent(jobId)}`);
|
|
297
|
+
this.touch();
|
|
298
|
+
return parseJob(r.text);
|
|
299
|
+
}
|
|
300
|
+
/** Poll /job until the given job reaches a terminal state. */
|
|
301
|
+
async waitForJob(jobId, opts) {
|
|
302
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
303
|
+
let last = null;
|
|
304
|
+
while (Date.now() < deadline) {
|
|
305
|
+
if (opts.signal?.aborted) {
|
|
306
|
+
await this.cancel(jobId).catch(() => undefined);
|
|
307
|
+
throw new Error("Media Encoder job cancelled");
|
|
308
|
+
}
|
|
309
|
+
const j = await this.job();
|
|
310
|
+
if (j.jobId === jobId || (!j.jobId && last)) {
|
|
311
|
+
last = j.jobId ? j : last;
|
|
312
|
+
if (j.jobId === jobId) {
|
|
313
|
+
opts.onProgress?.(j);
|
|
314
|
+
if (isTerminalStatus(j.jobStatus))
|
|
315
|
+
return j;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (j.jobId && j.jobId !== jobId && last && isTerminalStatus(last.jobStatus))
|
|
319
|
+
return last;
|
|
320
|
+
await new Promise((r) => setTimeout(r, opts.intervalMs ?? 1500));
|
|
321
|
+
}
|
|
322
|
+
throw new Error(`Media Encoder job ${jobId} did not finish within ${Math.round(opts.timeoutMs / 1000)} s (last status: ${last?.jobStatus ?? "unknown"}).`);
|
|
323
|
+
}
|
|
324
|
+
/** Stop the console we started (tree kill: the renderer it spawned goes too). */
|
|
325
|
+
async stop() {
|
|
326
|
+
if (this.idleTimer) {
|
|
327
|
+
clearTimeout(this.idleTimer);
|
|
328
|
+
this.idleTimer = null;
|
|
329
|
+
}
|
|
330
|
+
const child = this.child;
|
|
331
|
+
this.child = null;
|
|
332
|
+
this.baseUrl = this.options.baseUrl ?? null;
|
|
333
|
+
if (!child || child.pid === undefined)
|
|
334
|
+
return;
|
|
335
|
+
log.info("stopping Media Encoder web service");
|
|
336
|
+
if (process.platform === "win32") {
|
|
337
|
+
await new Promise((resolve) => {
|
|
338
|
+
const k = spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
339
|
+
k.on("close", () => resolve());
|
|
340
|
+
k.on("error", () => resolve());
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
try {
|
|
345
|
+
process.kill(-child.pid, "SIGTERM");
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
child.kill("SIGTERM");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
get executable() {
|
|
353
|
+
return this.exe;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
//# sourceMappingURL=ame-webservice.js.map
|