premiere-pro-mcp 1.12.2 → 1.13.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/README.md +27 -13
- package/artifacts/MCPBridgeCEP.zxp +0 -0
- package/cep-plugin/CSXS/manifest.xml +3 -3
- package/cep-plugin/index.html +1 -1
- package/cep-plugin/updater.cjs +1 -1
- package/dist/http-server.js +19 -2
- package/dist/index.js +1 -1
- package/dist/intake/project-intake.d.ts +112 -0
- package/dist/intake/project-intake.js +555 -0
- package/dist/security/capabilities.js +1 -0
- package/dist/server.js +2 -0
- package/dist/tools/project-intake.d.ts +41 -0
- package/dist/tools/project-intake.js +120 -0
- package/dist/workflows/catalog.d.ts +7 -2
- package/dist/workflows/catalog.js +6 -0
- package/dist/workflows/tool-metadata.js +1 -0
- package/docs/supported-actions.md +4 -3
- package/package.json +2 -2
- package/uxp-plugin/manifest.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.13.0] - 2026-08-22
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added `preview_project_intake`, a bounded, inspect-only project intake tool
|
|
14
|
+
that evaluates Premiere project organization against a facility-supplied
|
|
15
|
+
template and returns redacted findings plus proposed actions without changing
|
|
16
|
+
the project.
|
|
17
|
+
- Added a deterministic intake rules engine, a guided workflow entry, a public
|
|
18
|
+
facts page, and design-partner/security pilot contracts for human-supervised
|
|
19
|
+
assistant-editor adoption.
|
|
20
|
+
|
|
21
|
+
### Safety
|
|
22
|
+
|
|
23
|
+
- File paths remain redacted unless explicitly requested, recursive capture and
|
|
24
|
+
outputs are bounded, and the intake workflow does not mutate or persist
|
|
25
|
+
project data. Automated tests passed, while licensed-host execution remains a
|
|
26
|
+
separate gate because Premiere 2026 hung before the CEP panel could open.
|
|
27
|
+
|
|
9
28
|
## [1.12.2] - 2026-08-22
|
|
10
29
|
|
|
11
30
|
### Fixed
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**Give compatible AI assistants structured control over supported Adobe Premiere Pro workflows.**
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
288 core tools across 34 modules, 4 resources, and 11 guided workflows. A connected UXP host adds 50 capability-gated tools.
|
|
10
10
|
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](https://nodejs.org)
|
|
@@ -29,9 +29,9 @@ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that l
|
|
|
29
29
|
"Add the B-roll clips to V2, apply a cross dissolve between each, color correct them to match the A-roll, and export a 1080p ProRes."
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
The AI handles the entire workflow through
|
|
32
|
+
The AI handles the entire workflow through 288 core tools spanning the supported ExtendScript, QE DOM, revisioned project-context retrieval, safe edit-planning, project-intake preview, and connection-verification surfaces. A compatible, authenticated UXP panel adds 50 documented, capability-gated tools without replacing the production CEP bridge.
|
|
33
33
|
|
|
34
|
-
### Latest release: 1.
|
|
34
|
+
### Latest release: 1.13.0
|
|
35
35
|
|
|
36
36
|
- **MOGRT text and graphics:** `set_effect_property` now accepts string-backed
|
|
37
37
|
parameters with safe serialization and readback status.
|
|
@@ -41,18 +41,32 @@ The AI handles the entire workflow through 287 core tools spanning the supported
|
|
|
41
41
|
- **Verification clarity:** property readback remains distinct from playback or
|
|
42
42
|
exported-frame verification in a licensed Premiere Pro host.
|
|
43
43
|
|
|
44
|
-
See the [v1.
|
|
44
|
+
See the [v1.13.0 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.13.0)
|
|
45
45
|
for complete details. Live installation in Premiere Pro still requires host verification.
|
|
46
46
|
|
|
47
47
|
---
|
|
48
48
|
|
|
49
|
+
## For editors evaluating an AI workflow
|
|
50
|
+
|
|
51
|
+
Before an assistant changes an active project, use the
|
|
52
|
+
[Premiere Pro AI workflow checklist](https://premiere-pro-mcp.com/blog/premiere-pro-ai-workflow-checklist/)
|
|
53
|
+
to define the target and no-change boundaries, verify the local connection, request
|
|
54
|
+
a bounded plan, and inspect the returned result. It is a practical starting point for
|
|
55
|
+
assistant editors and post leads testing a repeatable workflow on a duplicate project
|
|
56
|
+
or small test sequence.
|
|
57
|
+
|
|
58
|
+
For product context, see the [Adobe Premiere AI Assistant and MCP comparison](https://premiere-pro-mcp.com/blog/adobe-premiere-ai-assistant-vs-mcp/)
|
|
59
|
+
and the [Claude Desktop setup guide](https://premiere-pro-mcp.com/blog/claude-desktop-premiere-pro-mcp-setup/).
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
49
63
|
## Quick Start
|
|
50
64
|
|
|
51
65
|
### Easiest supported path: Claude Desktop
|
|
52
66
|
|
|
53
|
-
1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.
|
|
67
|
+
1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.13.0/premiere-pro-mcp-1.13.0.mcpb).
|
|
54
68
|
2. In Claude Desktop, open **Settings > Extensions > Advanced settings > Install Extension**, select the downloaded bundle, and restart Claude Desktop.
|
|
55
|
-
3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.
|
|
69
|
+
3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.13.0/MCPBridgeCEP.zxp). Open it with your trusted ZXP installer. If your computer has no ZXP installer, use the npm connector installer in **Advanced setup** below.
|
|
56
70
|
4. Restart Premiere, open a project, then open **Window > Extensions > MCP for Adobe Premiere Pro**.
|
|
57
71
|
5. In Claude, enter: `Safely check my Premiere connection with verify_premiere_connection. Make no changes.`
|
|
58
72
|
|
|
@@ -276,11 +290,11 @@ From a clone of this repository:
|
|
|
276
290
|
```bash
|
|
277
291
|
codex plugin marketplace add .
|
|
278
292
|
codex plugin add premiere-pro@premiere-pro-mcp
|
|
279
|
-
npx -y premiere-pro-mcp@1.
|
|
293
|
+
npx -y premiere-pro-mcp@1.13.0 --install-cep
|
|
280
294
|
```
|
|
281
295
|
|
|
282
296
|
Restart Premiere Pro and start a new Codex session after installation. The plugin
|
|
283
|
-
launches `premiere-pro-mcp@1.
|
|
297
|
+
launches `premiere-pro-mcp@1.13.0` through `npx`; the separate CEP installation is
|
|
284
298
|
required because the MCP server communicates with the running Premiere host through
|
|
285
299
|
the local bridge.
|
|
286
300
|
|
|
@@ -300,7 +314,7 @@ For Claude Code, add this repository as a marketplace and install the plugin:
|
|
|
300
314
|
Then install the Premiere bridge and start a new Claude Code session:
|
|
301
315
|
|
|
302
316
|
```bash
|
|
303
|
-
npx -y premiere-pro-mcp@1.
|
|
317
|
+
npx -y premiere-pro-mcp@1.13.0 --install-cep
|
|
304
318
|
```
|
|
305
319
|
|
|
306
320
|
The Claude Code package lives in
|
|
@@ -339,7 +353,7 @@ installed separately.
|
|
|
339
353
|
QE-backed tools are reported as `experimental` because QE is undocumented and can vary between Premiere builds. Authority availability is reported separately from implementation support, so disabling `edit`, for example, does not incorrectly label editing tools as unsupported. Static metadata never claims that a Premiere operation succeeded; use `ping` and inspect each tool result for runtime evidence.
|
|
340
354
|
|
|
341
355
|
MCP `tools/list` is filtered to the active authority profile. The default
|
|
342
|
-
`inspect,edit,export,filesystem` profile advertises
|
|
356
|
+
`inspect,edit,export,filesystem` profile advertises 286 of the 288 registered
|
|
343
357
|
tools and omits `execute_extendscript` and `evaluate_expression`, which require
|
|
344
358
|
explicit `unsafe-script` authority. `ping` and `get_capabilities` remain visible
|
|
345
359
|
under every profile so a restricted or misconfigured server can still explain
|
|
@@ -494,7 +508,7 @@ The file-based IPC bridge is simple, reliable, and works across macOS and Window
|
|
|
494
508
|
|
|
495
509
|
---
|
|
496
510
|
|
|
497
|
-
## Tools (
|
|
511
|
+
## Tools (288 core total; 286 under the default profile; 336 with a connected UXP bridge)
|
|
498
512
|
|
|
499
513
|
The [complete supported-actions catalog](docs/supported-actions.md) lists every
|
|
500
514
|
registered core tool, the two tools restricted behind explicit `unsafe-script`
|
|
@@ -694,7 +708,7 @@ Track targeting, batch operations, markers, audio levels, motion/transform, meta
|
|
|
694
708
|
|
|
695
709
|
## MCP Resources
|
|
696
710
|
|
|
697
|
-
The server exposes four LLM context resources and
|
|
711
|
+
The server exposes four LLM context resources and eleven workflow prompts:
|
|
698
712
|
|
|
699
713
|
| Resource URI | Description |
|
|
700
714
|
| :----------- | :---------- |
|
|
@@ -801,7 +815,7 @@ premiere-pro-mcp/
|
|
|
801
815
|
├── src/
|
|
802
816
|
│ ├── index.ts # Entry point — stdio transport setup
|
|
803
817
|
│ ├── http-server.ts # Entry point — HTTP/SSE transport (Fly.io / remote)
|
|
804
|
-
│ ├── server.ts # MCP server — registers
|
|
818
|
+
│ ├── server.ts # MCP server — registers 288 tools, filtered by authority profile
|
|
805
819
|
│ ├── bridge/
|
|
806
820
|
│ │ ├── file-bridge.ts # File-based IPC (write .jsx, poll .json)
|
|
807
821
|
│ │ └── script-builder.ts # ExtendScript generator with ES3 helpers
|
|
Binary file
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.premiere.bridge" ExtensionBundleVersion="1.
|
|
2
|
+
<ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.premiere.bridge" ExtensionBundleVersion="1.13.0" ExtensionBundleName="MCP for Adobe Premiere Pro">
|
|
3
3
|
<ExtensionList>
|
|
4
|
-
<Extension Id="com.mcp.premiere.bridge.panel" Version="1.
|
|
5
|
-
<Extension Id="com.mcp.premiere.bridge.headless" Version="1.
|
|
4
|
+
<Extension Id="com.mcp.premiere.bridge.panel" Version="1.13.0"/>
|
|
5
|
+
<Extension Id="com.mcp.premiere.bridge.headless" Version="1.13.0"/>
|
|
6
6
|
</ExtensionList>
|
|
7
7
|
<ExecutionEnvironment>
|
|
8
8
|
<HostList>
|
package/cep-plugin/index.html
CHANGED
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
<section class="update-section" aria-live="polite">
|
|
82
82
|
<div class="update-copy">
|
|
83
83
|
<span class="section-label">Connector updates</span>
|
|
84
|
-
<strong id="updateTitle">Version 1.
|
|
84
|
+
<strong id="updateTitle">Version 1.13.0</strong>
|
|
85
85
|
<span id="updateDetail">Checking for updates…</span>
|
|
86
86
|
</div>
|
|
87
87
|
<button id="btnUpdate" class="button button-update" onclick="handleUpdateClick()" type="button" disabled>
|
package/cep-plugin/updater.cjs
CHANGED
package/dist/http-server.js
CHANGED
|
@@ -89,8 +89,25 @@ function serveLanding(req, res, scriptNonce) {
|
|
|
89
89
|
relativePath.startsWith(`..${path.sep}`) ||
|
|
90
90
|
path.isAbsolute(relativePath))
|
|
91
91
|
return false;
|
|
92
|
-
if (!fs.existsSync(filePath))
|
|
93
|
-
|
|
92
|
+
if (!fs.existsSync(filePath)) {
|
|
93
|
+
// Next static exports store dynamic-route Flight payloads in nested folders
|
|
94
|
+
// (for example, __next.blog/$d$slug/__PAGE__.txt), while the client asks
|
|
95
|
+
// for a flattened filename (__next.blog.$d$slug.__PAGE__.txt). Map only
|
|
96
|
+
// that generated shape after every original URL segment has passed the
|
|
97
|
+
// containment check above. Without this, guide-to-guide navigation falls
|
|
98
|
+
// back to a full-document load and emits avoidable 404s.
|
|
99
|
+
const requestedFlightFile = safeSegments.at(-1);
|
|
100
|
+
const flightParts = requestedFlightFile?.split(".") ?? [];
|
|
101
|
+
const isNestedFlightPayload = flightParts.length >= 4 &&
|
|
102
|
+
flightParts[0] === "__next" &&
|
|
103
|
+
flightParts.at(-1) === "txt";
|
|
104
|
+
if (!isNestedFlightPayload)
|
|
105
|
+
return false;
|
|
106
|
+
const remappedFlightPath = path.join(LANDING_DIR, ...safeSegments.slice(0, -1), `${flightParts[0]}.${flightParts[1]}`, ...flightParts.slice(2, -2), `${flightParts.at(-2)}.txt`);
|
|
107
|
+
if (!fs.existsSync(remappedFlightPath))
|
|
108
|
+
return false;
|
|
109
|
+
filePath = remappedFlightPath;
|
|
110
|
+
}
|
|
94
111
|
let fileStats;
|
|
95
112
|
try {
|
|
96
113
|
fileStats = fs.statSync(filePath);
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ function debugLog(message) {
|
|
|
21
21
|
const args = process.argv.slice(2);
|
|
22
22
|
if (args.includes("--help") || args.includes("-h")) {
|
|
23
23
|
console.log(`
|
|
24
|
-
premiere-pro-mcp — MCP server for Adobe Premiere Pro (
|
|
24
|
+
premiere-pro-mcp — MCP server for Adobe Premiere Pro (286 default-profile tools)
|
|
25
25
|
|
|
26
26
|
Usage:
|
|
27
27
|
premiere-pro-mcp Start the MCP server (stdio transport)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export declare const PROJECT_INTAKE_TEMPLATE_SCHEMA_VERSION = 1;
|
|
2
|
+
export declare const PROJECT_INTAKE_REPORT_SCHEMA_VERSION = 1;
|
|
3
|
+
export declare const MAX_PROJECT_INTAKE_ITEMS = 2000;
|
|
4
|
+
export declare const MAX_PROJECT_INTAKE_RULES = 64;
|
|
5
|
+
export declare const MAX_PROJECT_INTAKE_FINDINGS = 12200;
|
|
6
|
+
export type IntakeCertainty = "observed" | "unavailable" | "not_checked";
|
|
7
|
+
export type IntakeSeverity = "error" | "warning" | "info";
|
|
8
|
+
export type IntakeStatus = "ready" | "needs_attention" | "incomplete";
|
|
9
|
+
export interface RequiredBinRule {
|
|
10
|
+
name: string;
|
|
11
|
+
parentPath?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ApprovedPathPrefix {
|
|
14
|
+
id: string;
|
|
15
|
+
prefix: string;
|
|
16
|
+
}
|
|
17
|
+
export interface OrganizationMatchRule {
|
|
18
|
+
filenamePrefixes?: string[];
|
|
19
|
+
extensions?: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface OrganizationRule {
|
|
22
|
+
id: string;
|
|
23
|
+
destinationBinName: string;
|
|
24
|
+
match: OrganizationMatchRule;
|
|
25
|
+
colorIndex?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface FacilityIntakeTemplate {
|
|
28
|
+
schemaVersion: typeof PROJECT_INTAKE_TEMPLATE_SCHEMA_VERSION;
|
|
29
|
+
id: string;
|
|
30
|
+
version: string;
|
|
31
|
+
requiredBins: RequiredBinRule[];
|
|
32
|
+
allowedExtensions: string[];
|
|
33
|
+
allowedFrameRates: number[];
|
|
34
|
+
proxyPolicy: "ignore" | "report_missing" | "require";
|
|
35
|
+
approvedPathPrefixes: ApprovedPathPrefix[];
|
|
36
|
+
requiredEvidence: Array<"extension" | "frame_rate" | "offline" | "proxy" | "path">;
|
|
37
|
+
organizationRules: OrganizationRule[];
|
|
38
|
+
}
|
|
39
|
+
export interface ProjectIntakeItem {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
type: "clip" | "bin" | "sequence" | "other";
|
|
43
|
+
parentId?: string;
|
|
44
|
+
treePath?: string;
|
|
45
|
+
mediaPath?: string;
|
|
46
|
+
offline?: boolean;
|
|
47
|
+
hasProxy?: boolean;
|
|
48
|
+
frameRate?: number;
|
|
49
|
+
}
|
|
50
|
+
export interface ProjectIntakeSnapshot {
|
|
51
|
+
project: {
|
|
52
|
+
id: string;
|
|
53
|
+
name?: string;
|
|
54
|
+
};
|
|
55
|
+
items: ProjectIntakeItem[];
|
|
56
|
+
truncated: boolean;
|
|
57
|
+
unavailableEvidence: string[];
|
|
58
|
+
}
|
|
59
|
+
export interface ProjectIntakeFinding {
|
|
60
|
+
code: string;
|
|
61
|
+
severity: IntakeSeverity;
|
|
62
|
+
certainty: IntakeCertainty;
|
|
63
|
+
itemId?: string;
|
|
64
|
+
expected?: Record<string, unknown>;
|
|
65
|
+
observed?: Record<string, unknown>;
|
|
66
|
+
}
|
|
67
|
+
export interface ProjectIntakeOrganizationPlan {
|
|
68
|
+
applied: false;
|
|
69
|
+
planDigest: string;
|
|
70
|
+
proposedBins: Array<{
|
|
71
|
+
name: string;
|
|
72
|
+
exists: boolean;
|
|
73
|
+
createIfApproved: boolean;
|
|
74
|
+
}>;
|
|
75
|
+
proposedMoves: Array<{
|
|
76
|
+
projectItemId: string;
|
|
77
|
+
expectedParentId?: string;
|
|
78
|
+
destinationBinName: string;
|
|
79
|
+
ruleId: string;
|
|
80
|
+
colorIndex?: number;
|
|
81
|
+
}>;
|
|
82
|
+
}
|
|
83
|
+
export interface ProjectIntakeReport {
|
|
84
|
+
schemaVersion: typeof PROJECT_INTAKE_REPORT_SCHEMA_VERSION;
|
|
85
|
+
applied: false;
|
|
86
|
+
status: IntakeStatus;
|
|
87
|
+
project: {
|
|
88
|
+
id: string;
|
|
89
|
+
name?: string;
|
|
90
|
+
revision: string;
|
|
91
|
+
};
|
|
92
|
+
template: {
|
|
93
|
+
id: string;
|
|
94
|
+
version: string;
|
|
95
|
+
digest: string;
|
|
96
|
+
};
|
|
97
|
+
capture: {
|
|
98
|
+
itemCount: number;
|
|
99
|
+
truncated: boolean;
|
|
100
|
+
pathDisclosure: "redacted" | "requested";
|
|
101
|
+
};
|
|
102
|
+
findings: ProjectIntakeFinding[];
|
|
103
|
+
organizationPlan: ProjectIntakeOrganizationPlan;
|
|
104
|
+
limitations: string[];
|
|
105
|
+
}
|
|
106
|
+
export interface BuildProjectIntakeOptions {
|
|
107
|
+
includePaths?: boolean;
|
|
108
|
+
}
|
|
109
|
+
export declare function validateFacilityIntakeTemplate(value: unknown): FacilityIntakeTemplate;
|
|
110
|
+
export declare function validateProjectIntakeSnapshot(value: unknown): ProjectIntakeSnapshot;
|
|
111
|
+
export declare function buildProjectIntakeReport(snapshotValue: unknown, templateValue: unknown, options?: BuildProjectIntakeOptions): ProjectIntakeReport;
|
|
112
|
+
//# sourceMappingURL=project-intake.d.ts.map
|
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export const PROJECT_INTAKE_TEMPLATE_SCHEMA_VERSION = 1;
|
|
3
|
+
export const PROJECT_INTAKE_REPORT_SCHEMA_VERSION = 1;
|
|
4
|
+
export const MAX_PROJECT_INTAKE_ITEMS = 2_000;
|
|
5
|
+
export const MAX_PROJECT_INTAKE_RULES = 64;
|
|
6
|
+
// A clip can produce six independent findings (extension, frame rate, offline,
|
|
7
|
+
// proxy, path, and ambiguous organization), plus bounded project-level findings.
|
|
8
|
+
export const MAX_PROJECT_INTAKE_FINDINGS = 12_200;
|
|
9
|
+
const REQUIRED_EVIDENCE = new Set(["extension", "frame_rate", "offline", "proxy", "path"]);
|
|
10
|
+
const ITEM_TYPES = new Set(["clip", "bin", "sequence", "other"]);
|
|
11
|
+
function sha256(value) {
|
|
12
|
+
return createHash("sha256").update(value).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
function stableStringify(value) {
|
|
15
|
+
if (value === null || typeof value !== "object")
|
|
16
|
+
return JSON.stringify(value);
|
|
17
|
+
if (Array.isArray(value))
|
|
18
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
19
|
+
const record = value;
|
|
20
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
|
|
21
|
+
}
|
|
22
|
+
function digest(value) {
|
|
23
|
+
return sha256(stableStringify(value));
|
|
24
|
+
}
|
|
25
|
+
function asRecord(value, label) {
|
|
26
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
27
|
+
throw new Error(`${label} must be an object`);
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function assertOnlyKeys(record, allowed, label) {
|
|
31
|
+
for (const key of Object.keys(record)) {
|
|
32
|
+
if (!allowed.includes(key))
|
|
33
|
+
throw new Error(`${label}.${key} is not supported`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function requiredText(value, label, maxLength) {
|
|
37
|
+
if (typeof value !== "string")
|
|
38
|
+
throw new Error(`${label} must be a string`);
|
|
39
|
+
const normalized = value.trim();
|
|
40
|
+
if (!normalized)
|
|
41
|
+
throw new Error(`${label} must not be empty`);
|
|
42
|
+
if (normalized.length > maxLength)
|
|
43
|
+
throw new Error(`${label} exceeds ${maxLength} characters`);
|
|
44
|
+
return normalized;
|
|
45
|
+
}
|
|
46
|
+
function optionalText(value, label, maxLength) {
|
|
47
|
+
if (value === undefined)
|
|
48
|
+
return undefined;
|
|
49
|
+
return requiredText(value, label, maxLength);
|
|
50
|
+
}
|
|
51
|
+
function optionalBoolean(value, label) {
|
|
52
|
+
if (value === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
if (typeof value !== "boolean")
|
|
55
|
+
throw new Error(`${label} must be a boolean`);
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
function boundedArray(value, label, maxItems) {
|
|
59
|
+
if (value === undefined)
|
|
60
|
+
return [];
|
|
61
|
+
if (!Array.isArray(value))
|
|
62
|
+
throw new Error(`${label} must be an array`);
|
|
63
|
+
if (value.length > maxItems)
|
|
64
|
+
throw new Error(`${label} must contain at most ${maxItems} entries`);
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function normalizeExtension(value, label) {
|
|
68
|
+
const extension = requiredText(value, label, 32).replace(/^\.+/, "").toLocaleLowerCase();
|
|
69
|
+
if (!/^[a-z0-9]{1,16}$/.test(extension))
|
|
70
|
+
throw new Error(`${label} must be a simple file extension`);
|
|
71
|
+
return extension;
|
|
72
|
+
}
|
|
73
|
+
function normalizeExtensions(value, label) {
|
|
74
|
+
const values = boundedArray(value, label, MAX_PROJECT_INTAKE_RULES);
|
|
75
|
+
const normalized = values.map((entry, index) => normalizeExtension(entry, `${label}[${index}]`));
|
|
76
|
+
if (new Set(normalized).size !== normalized.length)
|
|
77
|
+
throw new Error(`${label} contains duplicate extensions`);
|
|
78
|
+
return normalized.sort();
|
|
79
|
+
}
|
|
80
|
+
function normalizePrefixes(value, label) {
|
|
81
|
+
const values = boundedArray(value, label, MAX_PROJECT_INTAKE_RULES);
|
|
82
|
+
const normalized = values.map((entry, index) => requiredText(entry, `${label}[${index}]`, 255).toLocaleLowerCase());
|
|
83
|
+
if (new Set(normalized).size !== normalized.length)
|
|
84
|
+
throw new Error(`${label} contains duplicate filename prefixes`);
|
|
85
|
+
return normalized.sort();
|
|
86
|
+
}
|
|
87
|
+
function normalizeFrameRates(value, label) {
|
|
88
|
+
const values = boundedArray(value, label, MAX_PROJECT_INTAKE_RULES);
|
|
89
|
+
const normalized = values.map((entry, index) => {
|
|
90
|
+
if (typeof entry !== "number" || !Number.isFinite(entry) || entry < 1 || entry > 240) {
|
|
91
|
+
throw new Error(`${label}[${index}] must be a finite frame rate from 1 through 240`);
|
|
92
|
+
}
|
|
93
|
+
return Number(entry.toFixed(6));
|
|
94
|
+
});
|
|
95
|
+
if (new Set(normalized).size !== normalized.length)
|
|
96
|
+
throw new Error(`${label} contains duplicate frame rates`);
|
|
97
|
+
return normalized.sort((left, right) => left - right);
|
|
98
|
+
}
|
|
99
|
+
function normalizeTreePath(value) {
|
|
100
|
+
return value
|
|
101
|
+
.replace(/\\/g, "/")
|
|
102
|
+
.split("/")
|
|
103
|
+
.map((part) => part.trim())
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
.join("/")
|
|
106
|
+
.toLocaleLowerCase();
|
|
107
|
+
}
|
|
108
|
+
function normalizeFilesystemPath(value) {
|
|
109
|
+
return value.replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\/+$/, "").toLocaleLowerCase();
|
|
110
|
+
}
|
|
111
|
+
function fileExtension(item) {
|
|
112
|
+
const source = item.mediaPath ?? item.name;
|
|
113
|
+
const leaf = source.replace(/\\/g, "/").split("/").at(-1) ?? "";
|
|
114
|
+
const index = leaf.lastIndexOf(".");
|
|
115
|
+
if (index <= 0 || index === leaf.length - 1)
|
|
116
|
+
return undefined;
|
|
117
|
+
const value = leaf.slice(index + 1).toLocaleLowerCase();
|
|
118
|
+
return /^[a-z0-9]{1,16}$/.test(value) ? value : undefined;
|
|
119
|
+
}
|
|
120
|
+
function parentTreePath(item) {
|
|
121
|
+
if (!item.treePath)
|
|
122
|
+
return undefined;
|
|
123
|
+
const path = normalizeTreePath(item.treePath);
|
|
124
|
+
const parts = path.split("/");
|
|
125
|
+
if (parts.at(-1) === item.name.trim().toLocaleLowerCase())
|
|
126
|
+
parts.pop();
|
|
127
|
+
return parts.join("/") || undefined;
|
|
128
|
+
}
|
|
129
|
+
function pathEvidence(mediaPath, includePaths) {
|
|
130
|
+
return {
|
|
131
|
+
mediaPathHash: sha256(normalizeFilesystemPath(mediaPath)),
|
|
132
|
+
...(includePaths ? { mediaPath } : {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function isRequired(template, evidence) {
|
|
136
|
+
return template.requiredEvidence.includes(evidence)
|
|
137
|
+
|| (evidence === "extension" && template.allowedExtensions.length > 0)
|
|
138
|
+
|| (evidence === "frame_rate" && template.allowedFrameRates.length > 0)
|
|
139
|
+
|| (evidence === "proxy" && template.proxyPolicy === "require")
|
|
140
|
+
|| (evidence === "path" && template.approvedPathPrefixes.length > 0);
|
|
141
|
+
}
|
|
142
|
+
function findingSort(left, right) {
|
|
143
|
+
const severity = { error: 0, warning: 1, info: 2 };
|
|
144
|
+
return severity[left.severity] - severity[right.severity]
|
|
145
|
+
|| left.code.localeCompare(right.code)
|
|
146
|
+
|| (left.itemId ?? "").localeCompare(right.itemId ?? "")
|
|
147
|
+
|| stableStringify(left.expected).localeCompare(stableStringify(right.expected));
|
|
148
|
+
}
|
|
149
|
+
function pushFinding(findings, finding) {
|
|
150
|
+
if (findings.length >= MAX_PROJECT_INTAKE_FINDINGS)
|
|
151
|
+
return;
|
|
152
|
+
findings.push(finding);
|
|
153
|
+
}
|
|
154
|
+
export function validateFacilityIntakeTemplate(value) {
|
|
155
|
+
const input = asRecord(value, "template");
|
|
156
|
+
assertOnlyKeys(input, [
|
|
157
|
+
"schemaVersion", "id", "version", "requiredBins", "allowedExtensions", "allowedFrameRates",
|
|
158
|
+
"proxyPolicy", "approvedPathPrefixes", "requiredEvidence", "organizationRules",
|
|
159
|
+
], "template");
|
|
160
|
+
if (input.schemaVersion !== PROJECT_INTAKE_TEMPLATE_SCHEMA_VERSION) {
|
|
161
|
+
throw new Error(`template.schemaVersion must be ${PROJECT_INTAKE_TEMPLATE_SCHEMA_VERSION}`);
|
|
162
|
+
}
|
|
163
|
+
const requiredBins = boundedArray(input.requiredBins, "template.requiredBins", MAX_PROJECT_INTAKE_RULES).map((entry, index) => {
|
|
164
|
+
const rule = asRecord(entry, `template.requiredBins[${index}]`);
|
|
165
|
+
assertOnlyKeys(rule, ["name", "parentPath"], `template.requiredBins[${index}]`);
|
|
166
|
+
return {
|
|
167
|
+
name: requiredText(rule.name, `template.requiredBins[${index}].name`, 255),
|
|
168
|
+
...(optionalText(rule.parentPath, `template.requiredBins[${index}].parentPath`, 1024) ? {
|
|
169
|
+
parentPath: optionalText(rule.parentPath, `template.requiredBins[${index}].parentPath`, 1024),
|
|
170
|
+
} : {}),
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
const binKeys = requiredBins.map((rule) => `${normalizeTreePath(rule.parentPath ?? "")}\u0000${rule.name.toLocaleLowerCase()}`);
|
|
174
|
+
if (new Set(binKeys).size !== binKeys.length)
|
|
175
|
+
throw new Error("template.requiredBins contains duplicate bin requirements");
|
|
176
|
+
const approvedPathPrefixes = boundedArray(input.approvedPathPrefixes, "template.approvedPathPrefixes", MAX_PROJECT_INTAKE_RULES).map((entry, index) => {
|
|
177
|
+
const prefix = asRecord(entry, `template.approvedPathPrefixes[${index}]`);
|
|
178
|
+
assertOnlyKeys(prefix, ["id", "prefix"], `template.approvedPathPrefixes[${index}]`);
|
|
179
|
+
return {
|
|
180
|
+
id: requiredText(prefix.id, `template.approvedPathPrefixes[${index}].id`, 128),
|
|
181
|
+
prefix: requiredText(prefix.prefix, `template.approvedPathPrefixes[${index}].prefix`, 4096),
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
if (new Set(approvedPathPrefixes.map((prefix) => prefix.id)).size !== approvedPathPrefixes.length) {
|
|
185
|
+
throw new Error("template.approvedPathPrefixes contains duplicate ids");
|
|
186
|
+
}
|
|
187
|
+
const requiredEvidence = boundedArray(input.requiredEvidence, "template.requiredEvidence", 5).map((entry, index) => {
|
|
188
|
+
if (typeof entry !== "string" || !REQUIRED_EVIDENCE.has(entry)) {
|
|
189
|
+
throw new Error(`template.requiredEvidence[${index}] must be extension, frame_rate, offline, proxy, or path`);
|
|
190
|
+
}
|
|
191
|
+
return entry;
|
|
192
|
+
});
|
|
193
|
+
if (new Set(requiredEvidence).size !== requiredEvidence.length)
|
|
194
|
+
throw new Error("template.requiredEvidence contains duplicate entries");
|
|
195
|
+
const organizationRules = boundedArray(input.organizationRules, "template.organizationRules", MAX_PROJECT_INTAKE_RULES).map((entry, index) => {
|
|
196
|
+
const rule = asRecord(entry, `template.organizationRules[${index}]`);
|
|
197
|
+
assertOnlyKeys(rule, ["id", "destinationBinName", "match", "colorIndex"], `template.organizationRules[${index}]`);
|
|
198
|
+
const match = asRecord(rule.match, `template.organizationRules[${index}].match`);
|
|
199
|
+
assertOnlyKeys(match, ["filenamePrefixes", "extensions"], `template.organizationRules[${index}].match`);
|
|
200
|
+
const filenamePrefixes = normalizePrefixes(match.filenamePrefixes, `template.organizationRules[${index}].match.filenamePrefixes`);
|
|
201
|
+
const extensions = normalizeExtensions(match.extensions, `template.organizationRules[${index}].match.extensions`);
|
|
202
|
+
if (!filenamePrefixes.length && !extensions.length) {
|
|
203
|
+
throw new Error(`template.organizationRules[${index}].match must include a filename prefix or extension`);
|
|
204
|
+
}
|
|
205
|
+
const colorIndex = rule.colorIndex;
|
|
206
|
+
if (colorIndex !== undefined && (!Number.isInteger(colorIndex) || typeof colorIndex !== "number" || colorIndex < 0 || colorIndex > 14)) {
|
|
207
|
+
throw new Error(`template.organizationRules[${index}].colorIndex must be an integer from 0 through 14`);
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
id: requiredText(rule.id, `template.organizationRules[${index}].id`, 128),
|
|
211
|
+
destinationBinName: requiredText(rule.destinationBinName, `template.organizationRules[${index}].destinationBinName`, 255),
|
|
212
|
+
match: {
|
|
213
|
+
...(filenamePrefixes.length ? { filenamePrefixes } : {}),
|
|
214
|
+
...(extensions.length ? { extensions } : {}),
|
|
215
|
+
},
|
|
216
|
+
...(colorIndex === undefined ? {} : { colorIndex }),
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
if (new Set(organizationRules.map((rule) => rule.id)).size !== organizationRules.length) {
|
|
220
|
+
throw new Error("template.organizationRules contains duplicate ids");
|
|
221
|
+
}
|
|
222
|
+
const proxyPolicy = input.proxyPolicy === undefined ? "ignore" : input.proxyPolicy;
|
|
223
|
+
if (proxyPolicy !== "ignore" && proxyPolicy !== "report_missing" && proxyPolicy !== "require") {
|
|
224
|
+
throw new Error("template.proxyPolicy must be ignore, report_missing, or require");
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
schemaVersion: PROJECT_INTAKE_TEMPLATE_SCHEMA_VERSION,
|
|
228
|
+
id: requiredText(input.id, "template.id", 128),
|
|
229
|
+
version: requiredText(input.version, "template.version", 128),
|
|
230
|
+
requiredBins: requiredBins.sort((left, right) => `${left.parentPath ?? ""}/${left.name}`.localeCompare(`${right.parentPath ?? ""}/${right.name}`)),
|
|
231
|
+
allowedExtensions: normalizeExtensions(input.allowedExtensions, "template.allowedExtensions"),
|
|
232
|
+
allowedFrameRates: normalizeFrameRates(input.allowedFrameRates, "template.allowedFrameRates"),
|
|
233
|
+
proxyPolicy,
|
|
234
|
+
approvedPathPrefixes: approvedPathPrefixes.sort((left, right) => left.id.localeCompare(right.id)),
|
|
235
|
+
requiredEvidence: requiredEvidence.sort(),
|
|
236
|
+
organizationRules: organizationRules.sort((left, right) => left.id.localeCompare(right.id)),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
export function validateProjectIntakeSnapshot(value) {
|
|
240
|
+
const input = asRecord(value, "snapshot");
|
|
241
|
+
assertOnlyKeys(input, ["project", "items", "truncated", "unavailableEvidence"], "snapshot");
|
|
242
|
+
const project = asRecord(input.project, "snapshot.project");
|
|
243
|
+
assertOnlyKeys(project, ["id", "name"], "snapshot.project");
|
|
244
|
+
const items = boundedArray(input.items, "snapshot.items", MAX_PROJECT_INTAKE_ITEMS).map((entry, index) => {
|
|
245
|
+
const item = asRecord(entry, `snapshot.items[${index}]`);
|
|
246
|
+
assertOnlyKeys(item, ["id", "name", "type", "parentId", "treePath", "mediaPath", "offline", "hasProxy", "frameRate"], `snapshot.items[${index}]`);
|
|
247
|
+
if (typeof item.type !== "string" || !ITEM_TYPES.has(item.type)) {
|
|
248
|
+
throw new Error(`snapshot.items[${index}].type must be clip, bin, sequence, or other`);
|
|
249
|
+
}
|
|
250
|
+
if (item.frameRate !== undefined && (typeof item.frameRate !== "number" || !Number.isFinite(item.frameRate) || item.frameRate < 1 || item.frameRate > 240)) {
|
|
251
|
+
throw new Error(`snapshot.items[${index}].frameRate must be a finite frame rate from 1 through 240`);
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
id: requiredText(item.id, `snapshot.items[${index}].id`, 512),
|
|
255
|
+
name: requiredText(item.name, `snapshot.items[${index}].name`, 255),
|
|
256
|
+
type: item.type,
|
|
257
|
+
...(optionalText(item.parentId, `snapshot.items[${index}].parentId`, 512) ? { parentId: optionalText(item.parentId, `snapshot.items[${index}].parentId`, 512) } : {}),
|
|
258
|
+
...(optionalText(item.treePath, `snapshot.items[${index}].treePath`, 4096) ? { treePath: optionalText(item.treePath, `snapshot.items[${index}].treePath`, 4096) } : {}),
|
|
259
|
+
...(optionalText(item.mediaPath, `snapshot.items[${index}].mediaPath`, 4096) ? { mediaPath: optionalText(item.mediaPath, `snapshot.items[${index}].mediaPath`, 4096) } : {}),
|
|
260
|
+
...(optionalBoolean(item.offline, `snapshot.items[${index}].offline`) === undefined ? {} : { offline: optionalBoolean(item.offline, `snapshot.items[${index}].offline`) }),
|
|
261
|
+
...(optionalBoolean(item.hasProxy, `snapshot.items[${index}].hasProxy`) === undefined ? {} : { hasProxy: optionalBoolean(item.hasProxy, `snapshot.items[${index}].hasProxy`) }),
|
|
262
|
+
...(item.frameRate === undefined ? {} : { frameRate: Number(item.frameRate.toFixed(6)) }),
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
if (new Set(items.map((item) => item.id)).size !== items.length)
|
|
266
|
+
throw new Error("snapshot.items contains duplicate ids");
|
|
267
|
+
const unavailableEvidence = boundedArray(input.unavailableEvidence, "snapshot.unavailableEvidence", MAX_PROJECT_INTAKE_RULES).map((entry, index) => requiredText(entry, `snapshot.unavailableEvidence[${index}]`, 128));
|
|
268
|
+
if (new Set(unavailableEvidence).size !== unavailableEvidence.length)
|
|
269
|
+
throw new Error("snapshot.unavailableEvidence contains duplicate entries");
|
|
270
|
+
if (typeof input.truncated !== "boolean")
|
|
271
|
+
throw new Error("snapshot.truncated must be a boolean");
|
|
272
|
+
return {
|
|
273
|
+
project: {
|
|
274
|
+
id: requiredText(project.id, "snapshot.project.id", 512),
|
|
275
|
+
...(optionalText(project.name, "snapshot.project.name", 255) ? { name: optionalText(project.name, "snapshot.project.name", 255) } : {}),
|
|
276
|
+
},
|
|
277
|
+
items: items.sort((left, right) => left.id.localeCompare(right.id)),
|
|
278
|
+
truncated: input.truncated,
|
|
279
|
+
unavailableEvidence: unavailableEvidence.sort(),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function ruleMatches(item, rule) {
|
|
283
|
+
const name = item.name.toLocaleLowerCase();
|
|
284
|
+
const extension = fileExtension(item);
|
|
285
|
+
const prefixes = rule.match.filenamePrefixes ?? [];
|
|
286
|
+
const extensions = rule.match.extensions ?? [];
|
|
287
|
+
return (!prefixes.length || prefixes.some((prefix) => name.startsWith(prefix)))
|
|
288
|
+
&& (!extensions.length || (extension !== undefined && extensions.includes(extension)));
|
|
289
|
+
}
|
|
290
|
+
export function buildProjectIntakeReport(snapshotValue, templateValue, options = {}) {
|
|
291
|
+
const snapshot = validateProjectIntakeSnapshot(snapshotValue);
|
|
292
|
+
const template = validateFacilityIntakeTemplate(templateValue);
|
|
293
|
+
const includePaths = options.includePaths === true;
|
|
294
|
+
const templateDigest = digest(template);
|
|
295
|
+
const projectRevision = digest(snapshot);
|
|
296
|
+
const findings = [];
|
|
297
|
+
const requiredUnavailable = new Set();
|
|
298
|
+
if (snapshot.truncated) {
|
|
299
|
+
pushFinding(findings, {
|
|
300
|
+
code: "CAPTURE_TRUNCATED",
|
|
301
|
+
severity: "error",
|
|
302
|
+
certainty: "unavailable",
|
|
303
|
+
expected: { maximumItems: MAX_PROJECT_INTAKE_ITEMS },
|
|
304
|
+
observed: { itemCount: snapshot.items.length },
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
for (const evidence of snapshot.unavailableEvidence) {
|
|
308
|
+
const normalized = evidence.toLocaleLowerCase();
|
|
309
|
+
if (REQUIRED_EVIDENCE.has(normalized) && isRequired(template, normalized)) {
|
|
310
|
+
requiredUnavailable.add(normalized);
|
|
311
|
+
pushFinding(findings, {
|
|
312
|
+
code: "REQUIRED_EVIDENCE_UNAVAILABLE",
|
|
313
|
+
severity: "error",
|
|
314
|
+
certainty: "unavailable",
|
|
315
|
+
expected: { evidence: normalized },
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const bins = snapshot.items.filter((item) => item.type === "bin");
|
|
320
|
+
for (const rule of template.requiredBins) {
|
|
321
|
+
const nameMatches = bins.filter((item) => item.name.toLocaleLowerCase() === rule.name.toLocaleLowerCase());
|
|
322
|
+
if (!nameMatches.length) {
|
|
323
|
+
pushFinding(findings, {
|
|
324
|
+
code: "REQUIRED_BIN_MISSING",
|
|
325
|
+
severity: "error",
|
|
326
|
+
certainty: "observed",
|
|
327
|
+
expected: { name: rule.name, ...(rule.parentPath ? { parentPath: rule.parentPath } : {}) },
|
|
328
|
+
});
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (!rule.parentPath)
|
|
332
|
+
continue;
|
|
333
|
+
const expectedParent = normalizeTreePath(rule.parentPath);
|
|
334
|
+
const pathKnown = nameMatches.some((item) => parentTreePath(item) !== undefined);
|
|
335
|
+
const parentMatches = nameMatches.filter((item) => {
|
|
336
|
+
const parent = parentTreePath(item);
|
|
337
|
+
return parent === expectedParent || parent?.endsWith(`/${expectedParent}`) === true;
|
|
338
|
+
});
|
|
339
|
+
if (parentMatches.length)
|
|
340
|
+
continue;
|
|
341
|
+
if (!pathKnown) {
|
|
342
|
+
requiredUnavailable.add("bin_parent_path");
|
|
343
|
+
pushFinding(findings, {
|
|
344
|
+
code: "REQUIRED_BIN_PARENT_UNAVAILABLE",
|
|
345
|
+
severity: "error",
|
|
346
|
+
certainty: "unavailable",
|
|
347
|
+
expected: { name: rule.name, parentPath: rule.parentPath },
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
pushFinding(findings, {
|
|
352
|
+
code: "REQUIRED_BIN_WRONG_PARENT",
|
|
353
|
+
severity: "error",
|
|
354
|
+
certainty: "observed",
|
|
355
|
+
expected: { name: rule.name, parentPath: rule.parentPath },
|
|
356
|
+
observed: { matchingBinIds: nameMatches.map((item) => item.id).sort() },
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const clips = snapshot.items.filter((item) => item.type === "clip");
|
|
361
|
+
for (const item of clips) {
|
|
362
|
+
const extension = fileExtension(item);
|
|
363
|
+
if (template.allowedExtensions.length || isRequired(template, "extension")) {
|
|
364
|
+
if (!extension) {
|
|
365
|
+
requiredUnavailable.add("extension");
|
|
366
|
+
pushFinding(findings, {
|
|
367
|
+
code: "EXTENSION_UNAVAILABLE",
|
|
368
|
+
severity: "error",
|
|
369
|
+
certainty: "unavailable",
|
|
370
|
+
itemId: item.id,
|
|
371
|
+
expected: { allowedExtensions: template.allowedExtensions },
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
else if (!template.allowedExtensions.includes(extension)) {
|
|
375
|
+
pushFinding(findings, {
|
|
376
|
+
code: "EXTENSION_NOT_ALLOWED",
|
|
377
|
+
severity: "error",
|
|
378
|
+
certainty: "observed",
|
|
379
|
+
itemId: item.id,
|
|
380
|
+
expected: { allowedExtensions: template.allowedExtensions },
|
|
381
|
+
observed: { extension },
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (template.allowedFrameRates.length || isRequired(template, "frame_rate")) {
|
|
386
|
+
if (item.frameRate === undefined) {
|
|
387
|
+
requiredUnavailable.add("frame_rate");
|
|
388
|
+
pushFinding(findings, {
|
|
389
|
+
code: "FRAME_RATE_UNAVAILABLE",
|
|
390
|
+
severity: "error",
|
|
391
|
+
certainty: "unavailable",
|
|
392
|
+
itemId: item.id,
|
|
393
|
+
expected: { allowedFrameRates: template.allowedFrameRates },
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
else if (!template.allowedFrameRates.some((rate) => Math.abs(rate - item.frameRate) <= 0.000001)) {
|
|
397
|
+
pushFinding(findings, {
|
|
398
|
+
code: "FRAME_RATE_NOT_ALLOWED",
|
|
399
|
+
severity: "error",
|
|
400
|
+
certainty: "observed",
|
|
401
|
+
itemId: item.id,
|
|
402
|
+
expected: { allowedFrameRates: template.allowedFrameRates },
|
|
403
|
+
observed: { frameRate: item.frameRate },
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (item.offline === true) {
|
|
408
|
+
pushFinding(findings, {
|
|
409
|
+
code: "OFFLINE_MEDIA",
|
|
410
|
+
severity: "error",
|
|
411
|
+
certainty: "observed",
|
|
412
|
+
itemId: item.id,
|
|
413
|
+
observed: { offline: true },
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
else if (item.offline === undefined && isRequired(template, "offline")) {
|
|
417
|
+
requiredUnavailable.add("offline");
|
|
418
|
+
pushFinding(findings, {
|
|
419
|
+
code: "OFFLINE_STATE_UNAVAILABLE",
|
|
420
|
+
severity: "error",
|
|
421
|
+
certainty: "unavailable",
|
|
422
|
+
itemId: item.id,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
if (template.proxyPolicy !== "ignore" || isRequired(template, "proxy")) {
|
|
426
|
+
if (item.hasProxy === false) {
|
|
427
|
+
pushFinding(findings, {
|
|
428
|
+
code: "PROXY_MISSING",
|
|
429
|
+
severity: template.proxyPolicy === "require" ? "error" : "warning",
|
|
430
|
+
certainty: "observed",
|
|
431
|
+
itemId: item.id,
|
|
432
|
+
expected: { proxyPolicy: template.proxyPolicy },
|
|
433
|
+
observed: { hasProxy: false },
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
else if (item.hasProxy === undefined) {
|
|
437
|
+
if (template.proxyPolicy === "require" || isRequired(template, "proxy")) {
|
|
438
|
+
requiredUnavailable.add("proxy");
|
|
439
|
+
pushFinding(findings, {
|
|
440
|
+
code: "PROXY_STATE_UNAVAILABLE",
|
|
441
|
+
severity: "error",
|
|
442
|
+
certainty: "unavailable",
|
|
443
|
+
itemId: item.id,
|
|
444
|
+
expected: { proxyPolicy: template.proxyPolicy },
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
pushFinding(findings, {
|
|
449
|
+
code: "PROXY_STATE_NOT_CHECKED",
|
|
450
|
+
severity: "info",
|
|
451
|
+
certainty: "not_checked",
|
|
452
|
+
itemId: item.id,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (template.approvedPathPrefixes.length || isRequired(template, "path")) {
|
|
458
|
+
if (!item.mediaPath) {
|
|
459
|
+
requiredUnavailable.add("path");
|
|
460
|
+
pushFinding(findings, {
|
|
461
|
+
code: "MEDIA_PATH_UNAVAILABLE",
|
|
462
|
+
severity: "error",
|
|
463
|
+
certainty: "unavailable",
|
|
464
|
+
itemId: item.id,
|
|
465
|
+
expected: { approvedPathPrefixIds: template.approvedPathPrefixes.map((prefix) => prefix.id) },
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
const normalizedPath = normalizeFilesystemPath(item.mediaPath);
|
|
470
|
+
const matches = template.approvedPathPrefixes.filter((prefix) => normalizedPath === normalizeFilesystemPath(prefix.prefix)
|
|
471
|
+
|| normalizedPath.startsWith(`${normalizeFilesystemPath(prefix.prefix)}/`));
|
|
472
|
+
if (!matches.length) {
|
|
473
|
+
pushFinding(findings, {
|
|
474
|
+
code: "MEDIA_PATH_NOT_APPROVED",
|
|
475
|
+
severity: "error",
|
|
476
|
+
certainty: "observed",
|
|
477
|
+
itemId: item.id,
|
|
478
|
+
expected: { approvedPathPrefixIds: template.approvedPathPrefixes.map((prefix) => prefix.id) },
|
|
479
|
+
observed: pathEvidence(item.mediaPath, includePaths),
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const existingBinNames = new Set(bins.map((item) => item.name.toLocaleLowerCase()));
|
|
486
|
+
const proposedMoves = [];
|
|
487
|
+
for (const item of clips) {
|
|
488
|
+
const matches = template.organizationRules.filter((rule) => ruleMatches(item, rule));
|
|
489
|
+
if (matches.length > 1) {
|
|
490
|
+
pushFinding(findings, {
|
|
491
|
+
code: "AMBIGUOUS_ORGANIZATION_RULES",
|
|
492
|
+
severity: "warning",
|
|
493
|
+
certainty: "observed",
|
|
494
|
+
itemId: item.id,
|
|
495
|
+
observed: { matchingRuleIds: matches.map((rule) => rule.id).sort() },
|
|
496
|
+
});
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
const rule = matches[0];
|
|
500
|
+
if (!rule)
|
|
501
|
+
continue;
|
|
502
|
+
proposedMoves.push({
|
|
503
|
+
projectItemId: item.id,
|
|
504
|
+
...(item.parentId ? { expectedParentId: item.parentId } : {}),
|
|
505
|
+
destinationBinName: rule.destinationBinName,
|
|
506
|
+
ruleId: rule.id,
|
|
507
|
+
...(rule.colorIndex === undefined ? {} : { colorIndex: rule.colorIndex }),
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
proposedMoves.sort((left, right) => left.projectItemId.localeCompare(right.projectItemId));
|
|
511
|
+
const proposedBins = [...new Set(proposedMoves.map((move) => move.destinationBinName))]
|
|
512
|
+
.sort((left, right) => left.localeCompare(right))
|
|
513
|
+
.map((name) => ({
|
|
514
|
+
name,
|
|
515
|
+
exists: existingBinNames.has(name.toLocaleLowerCase()),
|
|
516
|
+
createIfApproved: !existingBinNames.has(name.toLocaleLowerCase()),
|
|
517
|
+
}));
|
|
518
|
+
const planDigest = digest({ templateDigest, projectRevision, proposedBins, proposedMoves });
|
|
519
|
+
findings.sort(findingSort);
|
|
520
|
+
const incomplete = snapshot.truncated || requiredUnavailable.size > 0;
|
|
521
|
+
const status = incomplete
|
|
522
|
+
? "incomplete"
|
|
523
|
+
: findings.some((finding) => finding.severity === "error" || finding.severity === "warning")
|
|
524
|
+
? "needs_attention"
|
|
525
|
+
: "ready";
|
|
526
|
+
return {
|
|
527
|
+
schemaVersion: PROJECT_INTAKE_REPORT_SCHEMA_VERSION,
|
|
528
|
+
applied: false,
|
|
529
|
+
status,
|
|
530
|
+
project: {
|
|
531
|
+
id: snapshot.project.id,
|
|
532
|
+
...(snapshot.project.name ? { name: snapshot.project.name } : {}),
|
|
533
|
+
revision: projectRevision,
|
|
534
|
+
},
|
|
535
|
+
template: { id: template.id, version: template.version, digest: templateDigest },
|
|
536
|
+
capture: {
|
|
537
|
+
itemCount: snapshot.items.length,
|
|
538
|
+
truncated: snapshot.truncated,
|
|
539
|
+
pathDisclosure: includePaths ? "requested" : "redacted",
|
|
540
|
+
},
|
|
541
|
+
findings,
|
|
542
|
+
organizationPlan: {
|
|
543
|
+
applied: false,
|
|
544
|
+
planDigest,
|
|
545
|
+
proposedBins,
|
|
546
|
+
proposedMoves,
|
|
547
|
+
},
|
|
548
|
+
limitations: [
|
|
549
|
+
"This is a read-only plan. It does not create bins, rename items, move media, apply labels, or change Premiere.",
|
|
550
|
+
...(snapshot.truncated ? ["The project capture was truncated, so this report is incomplete."] : []),
|
|
551
|
+
...(requiredUnavailable.size ? ["Required host evidence was unavailable; inspect the affected items before using this report for compliance decisions."] : []),
|
|
552
|
+
],
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
//# sourceMappingURL=project-intake.js.map
|
|
@@ -47,6 +47,7 @@ const INSPECT_TOOL_NAMES = new Set([
|
|
|
47
47
|
"create_context_edit_plan",
|
|
48
48
|
"create_editorial_plan",
|
|
49
49
|
"preview_editorial_plan",
|
|
50
|
+
"preview_project_intake",
|
|
50
51
|
]);
|
|
51
52
|
// detect_silence reads a media file from disk and shells out to ffmpeg. It
|
|
52
53
|
// changes nothing in Premiere, so classifying it as "edit" would overstate what
|
package/dist/server.js
CHANGED
|
@@ -32,6 +32,7 @@ import { getAvSettingsTools } from "./tools/av-settings.js";
|
|
|
32
32
|
import { getRecoveryTools } from "./tools/recovery.js";
|
|
33
33
|
import { getProjectContextTools } from "./tools/project-context.js";
|
|
34
34
|
import { getEditorialPlanTools } from "./tools/editorial-plans.js";
|
|
35
|
+
import { getProjectIntakeTools } from "./tools/project-intake.js";
|
|
35
36
|
import { getUxpTools } from "./tools/uxp.js";
|
|
36
37
|
import { guardToolHandler, isToolPermitted, resolveCapabilities, } from "./security/index.js";
|
|
37
38
|
import { EXTENDSCRIPT_REFERENCE } from "./resources/extendscript-reference.js";
|
|
@@ -240,6 +241,7 @@ function collectTools(bridgeOptions, capabilities, uxpBridge, telemetry, cacheab
|
|
|
240
241
|
...getRecoveryTools(bridgeOptions),
|
|
241
242
|
...getProjectContextTools(bridgeOptions),
|
|
242
243
|
...getEditorialPlanTools({ uxpBridge }),
|
|
244
|
+
...getProjectIntakeTools(bridgeOptions),
|
|
243
245
|
...(uxpBridge ? getUxpTools(uxpBridge) : {}),
|
|
244
246
|
};
|
|
245
247
|
Object.assign(tools, getHealthTools(bridgeOptions, capabilities, () => tools, {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type BridgeOptions, type CommandResult } from "../bridge/file-bridge.js";
|
|
2
|
+
export interface ProjectIntakeToolDependencies {
|
|
3
|
+
captureSnapshot?: (maxItems: number) => Promise<CommandResult>;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Capture only the bounded fields required by the pure intake evaluator. The
|
|
7
|
+
* script does not require an active sequence and never changes Premiere.
|
|
8
|
+
*/
|
|
9
|
+
export declare function projectIntakeCaptureScript(maxItems: number): string;
|
|
10
|
+
export declare function getProjectIntakeTools(bridgeOptions: BridgeOptions, dependencies?: ProjectIntakeToolDependencies): {
|
|
11
|
+
preview_project_intake: {
|
|
12
|
+
description: string;
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object";
|
|
15
|
+
additionalProperties: boolean;
|
|
16
|
+
properties: {
|
|
17
|
+
template: {
|
|
18
|
+
type: string;
|
|
19
|
+
description: string;
|
|
20
|
+
};
|
|
21
|
+
include_paths: {
|
|
22
|
+
type: string;
|
|
23
|
+
description: string;
|
|
24
|
+
};
|
|
25
|
+
max_items: {
|
|
26
|
+
type: string;
|
|
27
|
+
minimum: number;
|
|
28
|
+
maximum: number;
|
|
29
|
+
description: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
required: string[];
|
|
33
|
+
};
|
|
34
|
+
handler: (args: {
|
|
35
|
+
template: unknown;
|
|
36
|
+
include_paths?: boolean;
|
|
37
|
+
max_items?: number;
|
|
38
|
+
}) => Promise<CommandResult>;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=project-intake.d.ts.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { buildToolScript } from "../bridge/script-builder.js";
|
|
2
|
+
import { sendCommand } from "../bridge/file-bridge.js";
|
|
3
|
+
import { buildProjectIntakeReport, MAX_PROJECT_INTAKE_ITEMS, } from "../intake/project-intake.js";
|
|
4
|
+
function boundedMaxItems(value) {
|
|
5
|
+
if (value === undefined)
|
|
6
|
+
return MAX_PROJECT_INTAKE_ITEMS;
|
|
7
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > MAX_PROJECT_INTAKE_ITEMS) {
|
|
8
|
+
throw new Error(`max_items must be an integer from 1 through ${MAX_PROJECT_INTAKE_ITEMS}`);
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Capture only the bounded fields required by the pure intake evaluator. The
|
|
14
|
+
* script does not require an active sequence and never changes Premiere.
|
|
15
|
+
*/
|
|
16
|
+
export function projectIntakeCaptureScript(maxItems) {
|
|
17
|
+
return buildToolScript(`
|
|
18
|
+
var project = app.project;
|
|
19
|
+
if (!project || !project.rootItem) return __error("No project is open");
|
|
20
|
+
|
|
21
|
+
var maximumItems = ${maxItems};
|
|
22
|
+
var items = [];
|
|
23
|
+
var truncated = false;
|
|
24
|
+
|
|
25
|
+
function itemType(item) {
|
|
26
|
+
if (item.type === 2) return "bin";
|
|
27
|
+
if (item.type === 1 || item.type === 4) return "clip";
|
|
28
|
+
return "other";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function walk(parent, parentId, depth) {
|
|
32
|
+
if (!parent || !parent.children || depth > 32) {
|
|
33
|
+
if (depth > 32) truncated = true;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (var i = 0; i < parent.children.numItems; i++) {
|
|
37
|
+
if (items.length >= maximumItems) { truncated = true; return; }
|
|
38
|
+
var item = parent.children[i];
|
|
39
|
+
var entry = {
|
|
40
|
+
id: String(item.nodeId),
|
|
41
|
+
name: String(item.name),
|
|
42
|
+
type: itemType(item),
|
|
43
|
+
parentId: String(parentId || "root")
|
|
44
|
+
};
|
|
45
|
+
try { if (item.treePath) entry.treePath = String(item.treePath); } catch(e) {}
|
|
46
|
+
try {
|
|
47
|
+
var mediaPath = item.getMediaPath();
|
|
48
|
+
if (mediaPath) entry.mediaPath = String(mediaPath);
|
|
49
|
+
} catch(e) {}
|
|
50
|
+
try { entry.offline = Boolean(item.isOffline()); } catch(e) {}
|
|
51
|
+
try { entry.hasProxy = Boolean(item.hasProxy()); } catch(e) {}
|
|
52
|
+
try {
|
|
53
|
+
var interpretation = item.getFootageInterpretation();
|
|
54
|
+
if (interpretation && interpretation.frameRate) entry.frameRate = Number(interpretation.frameRate);
|
|
55
|
+
} catch(e) {}
|
|
56
|
+
items.push(entry);
|
|
57
|
+
if (item.type === 2) walk(item, item.nodeId, depth + 1);
|
|
58
|
+
if (truncated && items.length >= maximumItems) return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
walk(project.rootItem, "root", 0);
|
|
63
|
+
var projectId = "";
|
|
64
|
+
try { if (project.documentID) projectId = String(project.documentID); } catch(e) {}
|
|
65
|
+
if (!projectId) projectId = String(project.name);
|
|
66
|
+
|
|
67
|
+
return __result({
|
|
68
|
+
project: { id: projectId, name: String(project.name) },
|
|
69
|
+
items: items,
|
|
70
|
+
truncated: truncated,
|
|
71
|
+
unavailableEvidence: []
|
|
72
|
+
});
|
|
73
|
+
`);
|
|
74
|
+
}
|
|
75
|
+
export function getProjectIntakeTools(bridgeOptions, dependencies = {}) {
|
|
76
|
+
const capture = dependencies.captureSnapshot
|
|
77
|
+
?? ((maxItems) => sendCommand(projectIntakeCaptureScript(maxItems), bridgeOptions));
|
|
78
|
+
return {
|
|
79
|
+
preview_project_intake: {
|
|
80
|
+
description: "Inspect a bounded Premiere project against an explicit facility intake template and return a path-redacted report plus a non-mutating organization proposal. It never changes Premiere or persists the template.",
|
|
81
|
+
parameters: {
|
|
82
|
+
type: "object",
|
|
83
|
+
additionalProperties: false,
|
|
84
|
+
properties: {
|
|
85
|
+
template: {
|
|
86
|
+
type: "object",
|
|
87
|
+
description: "Versioned facility policy. Unknown fields and unsafe matching expressions are rejected by the intake engine.",
|
|
88
|
+
},
|
|
89
|
+
include_paths: {
|
|
90
|
+
type: "boolean",
|
|
91
|
+
description: "Include observed media paths in findings. Defaults to false; hashes are returned instead.",
|
|
92
|
+
},
|
|
93
|
+
max_items: {
|
|
94
|
+
type: "integer",
|
|
95
|
+
minimum: 1,
|
|
96
|
+
maximum: MAX_PROJECT_INTAKE_ITEMS,
|
|
97
|
+
description: `Maximum project items to inspect; defaults to ${MAX_PROJECT_INTAKE_ITEMS}. Truncation returns an incomplete report.`,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
required: ["template"],
|
|
101
|
+
},
|
|
102
|
+
handler: async (args) => {
|
|
103
|
+
try {
|
|
104
|
+
const maxItems = boundedMaxItems(args.max_items);
|
|
105
|
+
const captured = await capture(maxItems);
|
|
106
|
+
if (!captured.success)
|
|
107
|
+
return captured;
|
|
108
|
+
return {
|
|
109
|
+
success: true,
|
|
110
|
+
data: buildProjectIntakeReport(captured.data, args.template, { includePaths: args.include_paths === true }),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=project-intake.js.map
|
|
@@ -4,6 +4,11 @@ export declare const WORKFLOW_CATALOG: readonly [{
|
|
|
4
4
|
readonly title: "Assemble a rough cut";
|
|
5
5
|
readonly summary: "Inspect the project, import media, create or select a sequence, assemble clips, then verify and save.";
|
|
6
6
|
readonly recommendedTools: readonly ["get_premiere_state", "import_media", "create_sequence", "add_to_timeline", "get_sequence_structure", "save_project"];
|
|
7
|
+
}, {
|
|
8
|
+
readonly id: "project-intake";
|
|
9
|
+
readonly title: "Preview project intake";
|
|
10
|
+
readonly summary: "Inspect a bounded project against an explicit facility template and return a path-redacted intake report and non-mutating organization proposal.";
|
|
11
|
+
readonly recommendedTools: readonly ["verify_premiere_connection", "preview_project_intake"];
|
|
7
12
|
}, {
|
|
8
13
|
readonly id: "dialogue-cleanup";
|
|
9
14
|
readonly title: "Clean up dialogue";
|
|
@@ -53,8 +58,8 @@ export declare const WORKFLOW_CATALOG: readonly [{
|
|
|
53
58
|
export declare const WORKFLOW_RESOURCE: string;
|
|
54
59
|
export declare const WORKFLOW_PROMPTS: {
|
|
55
60
|
name: string;
|
|
56
|
-
title: "Assemble a rough cut" | "Clean up dialogue" | "Caption and style a sequence" | "Prepare a delivery export" | "Build a context-aware rough cut" | "Plan a transcript-driven rough cut" | "Plan project organization" | "Plan a reviewed stringout" | "Review a caption artifact" | "Plan platform cutdowns";
|
|
57
|
-
description: "Inspect the project, import media, create or select a sequence, assemble clips, then verify and save." | "Inspect audio tracks, normalize dialogue, apply conservative cleanup, and verify levels before saving." | "Inspect the active sequence, create captions, apply styling, verify timing, and save." | "Validate the sequence and destination, export with an explicit preset, then report the produced artifact." | "Capture reusable project context, retrieve only evidence relevant to the edit intent, create a revision-guarded plan, then preview before applying." | "Export Premiere's native transcript, select revision-locked source ranges, map them to verified 1x placements in a duplicate sequence, then execute the descending cut plan with re-query verification." | "Capture project context, supply explicit editorial categories, create and preview a review-only organization plan, then use its guarded apply route with stable-ID bin operations. Direct organize_project_items_uxp use is advanced/manual only." | "Retrieve relevant local evidence, produce a review-only stringout plan, resolve selected project items, then create and verify a new sequence." | "Use existing transcript evidence to review a supplied caption artifact, import it deliberately, create a caption track, and verify playback before delivery." | "Capture source-sequence context, propose bounded platform dimensions, review derivative sequence routes, then create and verify each cutdown deliberately.";
|
|
61
|
+
title: "Assemble a rough cut" | "Preview project intake" | "Clean up dialogue" | "Caption and style a sequence" | "Prepare a delivery export" | "Build a context-aware rough cut" | "Plan a transcript-driven rough cut" | "Plan project organization" | "Plan a reviewed stringout" | "Review a caption artifact" | "Plan platform cutdowns";
|
|
62
|
+
description: "Inspect the project, import media, create or select a sequence, assemble clips, then verify and save." | "Inspect a bounded project against an explicit facility template and return a path-redacted intake report and non-mutating organization proposal." | "Inspect audio tracks, normalize dialogue, apply conservative cleanup, and verify levels before saving." | "Inspect the active sequence, create captions, apply styling, verify timing, and save." | "Validate the sequence and destination, export with an explicit preset, then report the produced artifact." | "Capture reusable project context, retrieve only evidence relevant to the edit intent, create a revision-guarded plan, then preview before applying." | "Export Premiere's native transcript, select revision-locked source ranges, map them to verified 1x placements in a duplicate sequence, then execute the descending cut plan with re-query verification." | "Capture project context, supply explicit editorial categories, create and preview a review-only organization plan, then use its guarded apply route with stable-ID bin operations. Direct organize_project_items_uxp use is advanced/manual only." | "Retrieve relevant local evidence, produce a review-only stringout plan, resolve selected project items, then create and verify a new sequence." | "Use existing transcript evidence to review a supplied caption artifact, import it deliberately, create a caption track, and verify playback before delivery." | "Capture source-sequence context, propose bounded platform dimensions, review derivative sequence routes, then create and verify each cutdown deliberately.";
|
|
58
63
|
argsSchema: {
|
|
59
64
|
goal: z.ZodString;
|
|
60
65
|
constraints: z.ZodOptional<z.ZodString>;
|
|
@@ -6,6 +6,12 @@ export const WORKFLOW_CATALOG = [
|
|
|
6
6
|
summary: "Inspect the project, import media, create or select a sequence, assemble clips, then verify and save.",
|
|
7
7
|
recommendedTools: ["get_premiere_state", "import_media", "create_sequence", "add_to_timeline", "get_sequence_structure", "save_project"],
|
|
8
8
|
},
|
|
9
|
+
{
|
|
10
|
+
id: "project-intake",
|
|
11
|
+
title: "Preview project intake",
|
|
12
|
+
summary: "Inspect a bounded project against an explicit facility template and return a path-redacted intake report and non-mutating organization proposal.",
|
|
13
|
+
recommendedTools: ["verify_premiere_connection", "preview_project_intake"],
|
|
14
|
+
},
|
|
9
15
|
{
|
|
10
16
|
id: "dialogue-cleanup",
|
|
11
17
|
title: "Clean up dialogue",
|
|
@@ -3,6 +3,7 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
3
3
|
"create_context_edit_plan",
|
|
4
4
|
"create_editorial_plan",
|
|
5
5
|
"preview_editorial_plan",
|
|
6
|
+
"preview_project_intake",
|
|
6
7
|
]);
|
|
7
8
|
const DESTRUCTIVE_PREFIXES = ["delete_", "remove_", "ripple_delete", "close_"];
|
|
8
9
|
const DESTRUCTIVE_TOOLS = new Set(["manage_project_context"]);
|
|
@@ -8,11 +8,11 @@ descriptions, action enums, authority visibility, and counts stay aligned with t
|
|
|
8
8
|
|
|
9
9
|
| Surface | Count | Availability |
|
|
10
10
|
| --- | ---: | --- |
|
|
11
|
-
| Registered core actions |
|
|
12
|
-
| Default-profile core actions |
|
|
11
|
+
| Registered core actions | 288 | CEP/local server catalog; host and authority checks still apply |
|
|
12
|
+
| Default-profile core actions | 286 | Advertised with `inspect,edit,export,filesystem` |
|
|
13
13
|
| Restricted core actions | 2 | Require explicit `unsafe-script` authority |
|
|
14
14
|
| Authenticated UXP additions | 50 | Advertised only while a compatible authenticated UXP panel is connected |
|
|
15
|
-
| Default profile with UXP |
|
|
15
|
+
| Default profile with UXP | 336 | 286 core plus 50 UXP tools |
|
|
16
16
|
|
|
17
17
|
## How to read support
|
|
18
18
|
|
|
@@ -218,6 +218,7 @@ operation” when the tool has no enum-based mode.
|
|
|
218
218
|
| `play_timeline` | Default profile | Single operation | Start playback of the active sequence timeline. Uses QE DOM. |
|
|
219
219
|
| `preview_edit_plan` | Default profile | Single operation | Validate and preview a compound timeline edit without changing Premiere. Returns a confirmation token required by apply_edit_plan. |
|
|
220
220
|
| `preview_editorial_plan` | Default profile | Single operation | Revalidate an exact server-issued editorial plan against the saved project-context revisions and return an opaque confirmation token. This tool is read-only and cannot apply the plan. |
|
|
221
|
+
| `preview_project_intake` | Default profile | Single operation | Inspect a bounded Premiere project against an explicit facility intake template and return a path-redacted report plus a non-mutating organization proposal. It never changes Premiere or persists the template. |
|
|
221
222
|
| `razor_all_tracks` | Default profile | `track_type`: `video`, `audio`, `both` | Razor (split) all clips at the playhead position across all tracks, or at a specific time. |
|
|
222
223
|
| `redo` | Default profile | Single operation | Redo the last undone action in Premiere Pro. |
|
|
223
224
|
| `refresh_media` | Default profile | Single operation | Refresh a project item to pick up changes to the source file |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "premiere-pro-mcp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP server for Adobe Premiere Pro with
|
|
3
|
+
"version": "1.13.0",
|
|
4
|
+
"description": "MCP server for Adobe Premiere Pro with 288 core AI video editing tools and a capability-aware UXP bridge for supported Premiere workflows.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"type": "module",
|
package/uxp-plugin/manifest.json
CHANGED