@impractical-ai/motion 0.1.16 → 0.1.18
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/assets/motion-standard.md +46 -0
- package/dist/{api-V7Q3PIWY.js → api-WSATRY3V.js} +2 -2
- package/dist/chunk-2GCSX3YN.js +71 -0
- package/dist/{chunk-CJLGKZMD.js → chunk-FTSZCIMV.js} +1 -1
- package/dist/{stdio-R4RCWJIS.js → chunk-JSEDAJHM.js} +37927 -70070
- package/dist/chunk-M6427SHW.js +97 -0
- package/dist/chunk-QQ6NYC74.js +14552 -0
- package/dist/{chunk-UEPY33MB.js → chunk-TETWFQPX.js} +8 -49
- package/dist/chunk-XWCSIM4L.js +51 -0
- package/dist/chunk-ZEVR6SM6.js +18083 -0
- package/dist/commands-Y723HSJI.js +468 -0
- package/dist/draft-check-BRVH77WB.js +53 -0
- package/dist/guidance-SMXXX463.js +113 -0
- package/dist/index.js +30 -10
- package/dist/stdio-N2RZGMBI.js +23 -0
- package/dist/template-source-KKAKXOHX.js +17 -0
- package/package.json +3 -3
- package/dist/chunk-AP27EKRR.js +0 -71
- package/dist/commands-6F7HITZN.js +0 -1683
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
setup
|
|
4
|
+
} from "./chunk-TETWFQPX.js";
|
|
5
|
+
import "./chunk-XWCSIM4L.js";
|
|
6
|
+
import {
|
|
7
|
+
invoke,
|
|
8
|
+
invokeAt,
|
|
9
|
+
jobStatus,
|
|
10
|
+
loadConfig,
|
|
11
|
+
requireConfig,
|
|
12
|
+
saveConfig,
|
|
13
|
+
studioLink,
|
|
14
|
+
waitForJobs
|
|
15
|
+
} from "./chunk-FTSZCIMV.js";
|
|
16
|
+
import "./chunk-2GCSX3YN.js";
|
|
17
|
+
import "./chunk-77HVPD4G.js";
|
|
18
|
+
|
|
19
|
+
// src/commands.ts
|
|
20
|
+
import { writeFileSync } from "node:fs";
|
|
21
|
+
|
|
22
|
+
// src/media.ts
|
|
23
|
+
import { readFileSync, statSync } from "node:fs";
|
|
24
|
+
import { basename, extname } from "node:path";
|
|
25
|
+
var MAX_UPLOAD_BYTES = 3e8;
|
|
26
|
+
var MIME_BY_EXT = {
|
|
27
|
+
".png": "image/png",
|
|
28
|
+
".jpg": "image/jpeg",
|
|
29
|
+
".jpeg": "image/jpeg",
|
|
30
|
+
".gif": "image/gif",
|
|
31
|
+
".webp": "image/webp",
|
|
32
|
+
".svg": "image/svg+xml",
|
|
33
|
+
".mp4": "video/mp4",
|
|
34
|
+
".webm": "video/webm",
|
|
35
|
+
".mov": "video/quicktime"
|
|
36
|
+
};
|
|
37
|
+
function mediaType(filePath) {
|
|
38
|
+
const type = MIME_BY_EXT[extname(filePath).toLowerCase()];
|
|
39
|
+
if (!type) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Unsupported media type for ${basename(filePath)} \u2014 use PNG, JPEG, GIF, WebP, SVG, MP4, WebM, or MOV`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return type;
|
|
45
|
+
}
|
|
46
|
+
async function uploadAsset(input) {
|
|
47
|
+
const { projectId, draftId, filePath, log } = input;
|
|
48
|
+
const bytes = statSync(filePath).size;
|
|
49
|
+
if (bytes > MAX_UPLOAD_BYTES) throw new Error("Media must be 300 MB or smaller");
|
|
50
|
+
const contentType = mediaType(filePath);
|
|
51
|
+
const fileName = basename(filePath);
|
|
52
|
+
const upload2 = await invoke("prepare-upload", {
|
|
53
|
+
projectId,
|
|
54
|
+
draftId,
|
|
55
|
+
fileName,
|
|
56
|
+
contentType,
|
|
57
|
+
bytes
|
|
58
|
+
});
|
|
59
|
+
const form = new FormData();
|
|
60
|
+
form.append("cacheControl", "3600");
|
|
61
|
+
form.append("", new File([new Uint8Array(readFileSync(filePath))], fileName, { type: contentType }));
|
|
62
|
+
const response = await fetch(upload2.signedUrl, {
|
|
63
|
+
method: "PUT",
|
|
64
|
+
headers: { "x-upsert": "false" },
|
|
65
|
+
body: form
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw new Error(`Upload failed (${response.status}): ${await response.text()}`);
|
|
69
|
+
}
|
|
70
|
+
log?.(`uploaded ${fileName} (${(bytes / 1e6).toFixed(1)} MB)`);
|
|
71
|
+
const registered = await invoke("register-asset", {
|
|
72
|
+
projectId,
|
|
73
|
+
draftId: null,
|
|
74
|
+
storagePath: upload2.storagePath,
|
|
75
|
+
fileName,
|
|
76
|
+
contentType,
|
|
77
|
+
bytes
|
|
78
|
+
});
|
|
79
|
+
return registered.assetId;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ../src/shared/video-clone-command.ts
|
|
83
|
+
var TOKEN = /^[A-Za-z0-9_-]{43}$/;
|
|
84
|
+
var SHARE_HOSTS = /* @__PURE__ */ new Set(["impractical.ai", "www.impractical.ai", "impractical-video-shares.vercel.app"]);
|
|
85
|
+
function videoShareToken(source) {
|
|
86
|
+
if (TOKEN.test(source)) return source;
|
|
87
|
+
try {
|
|
88
|
+
const url = new URL(source);
|
|
89
|
+
const token = url.pathname.match(/^\/v\/([A-Za-z0-9_-]{43})\/?$/)?.[1];
|
|
90
|
+
if (url.protocol === "https:" && SHARE_HOSTS.has(url.host) && !url.username && !url.password && !url.search && !url.hash && token) return token;
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
throw new Error("Use an Impractical video share URL: https://impractical.ai/v/<token>");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/clone-video-project.ts
|
|
97
|
+
async function cloneVideoProject(source, json = false) {
|
|
98
|
+
const token = videoShareToken(source);
|
|
99
|
+
const result = await invoke("video-shares", { action: "clone", token });
|
|
100
|
+
const url = studioLink(requireConfig().appUrl, result.projectId);
|
|
101
|
+
if (json) {
|
|
102
|
+
console.log(JSON.stringify({ ...result, url }, null, 2));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
console.log(result.alreadyForked ? "Your editable copy is ready." : "Copied the project into your workspace.");
|
|
106
|
+
console.log(`Open: ${url}`);
|
|
107
|
+
console.log(`Project: ${result.projectId}`);
|
|
108
|
+
console.log("After restarting your agent, paste:");
|
|
109
|
+
console.log(`Use Impractical to open project ${result.projectId} and help me iterate on this video.`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/video-sharing.ts
|
|
113
|
+
import { openAsBlob } from "node:fs";
|
|
114
|
+
import { readFile, stat } from "node:fs/promises";
|
|
115
|
+
import { basename as basename2, extname as extname2 } from "node:path";
|
|
116
|
+
async function shareVideo(rest, flags) {
|
|
117
|
+
const config = requireConfig();
|
|
118
|
+
const request = async (body) => {
|
|
119
|
+
const response = await fetch(
|
|
120
|
+
`${config.supabaseUrl.replace(/\/$/, "")}/functions/v1/video-shares`,
|
|
121
|
+
{
|
|
122
|
+
method: body ? "POST" : "GET",
|
|
123
|
+
redirect: "error",
|
|
124
|
+
signal: AbortSignal.timeout(45e3),
|
|
125
|
+
headers: {
|
|
126
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
127
|
+
...config.workspace ? { "x-impractical-workspace": config.workspace } : {},
|
|
128
|
+
"content-type": "application/json"
|
|
129
|
+
},
|
|
130
|
+
body: body ? JSON.stringify(body) : void 0
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
const data = await response.json().catch(() => ({}));
|
|
134
|
+
if (!response.ok) throw new Error(data.error || `Video sharing failed (${response.status})`);
|
|
135
|
+
return data;
|
|
136
|
+
};
|
|
137
|
+
if (rest[0] === "list") {
|
|
138
|
+
console.log(JSON.stringify(await request(), null, 2));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (rest[0] === "revoke") {
|
|
142
|
+
if (!rest[1]) throw new Error("Usage: impractical share revoke <share-id>");
|
|
143
|
+
await request({ action: "revoke", id: rest[1] });
|
|
144
|
+
console.log("Share link revoked. Already-open videos may remain playable for up to 5 minutes.");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const allowEditing = rest[0] === "allow-editing" || rest[0] === "attach";
|
|
148
|
+
const viewOnly = rest[0] === "view-only" || rest[0] === "detach";
|
|
149
|
+
if (allowEditing || viewOnly) {
|
|
150
|
+
if (!rest[1] || allowEditing && typeof flags.project !== "string") {
|
|
151
|
+
throw new Error("Usage: impractical share allow-editing <share-id> --project <id> | share view-only <share-id>");
|
|
152
|
+
}
|
|
153
|
+
await request({ action: "attach-project", id: rest[1], projectId: viewOnly ? null : flags.project });
|
|
154
|
+
console.log(allowEditing ? "Editing allowed for this link. Recipients can make their own editable copy." : "This link is now view only. Future project copies are disabled; existing recipient copies remain theirs.");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const filePath = rest[0];
|
|
158
|
+
if (!filePath || extname2(filePath).toLowerCase() !== ".mp4") {
|
|
159
|
+
throw new Error(
|
|
160
|
+
'Usage: impractical share ./video.mp4 [--title "A video for Acme"] [--message "..."] [--references ./public-brand.json] [--project <id>] [--expires-in 7] [--json]'
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
const info = await stat(filePath);
|
|
164
|
+
if (!info.isFile() || info.size < 16 || info.size > 3e8) {
|
|
165
|
+
throw new Error("Choose an MP4 up to 300 MB");
|
|
166
|
+
}
|
|
167
|
+
const days = flags["expires-in"] === void 0 ? null : Number(flags["expires-in"]);
|
|
168
|
+
if (days !== null && (!Number.isFinite(days) || days <= 0 || days > 3650)) {
|
|
169
|
+
throw new Error("--expires-in must be a number of days between 0 and 3650");
|
|
170
|
+
}
|
|
171
|
+
const file = await openAsBlob(filePath, { type: "video/mp4" });
|
|
172
|
+
const header = new Uint8Array(await file.slice(0, 16).arrayBuffer());
|
|
173
|
+
if (String.fromCharCode(...header.slice(4, 8)) !== "ftyp") {
|
|
174
|
+
throw new Error("This file does not have an MP4 header");
|
|
175
|
+
}
|
|
176
|
+
let references;
|
|
177
|
+
if (typeof flags.references === "string") {
|
|
178
|
+
const metadataFile = await stat(flags.references);
|
|
179
|
+
if (!metadataFile.isFile() || metadataFile.size > 4e6) throw new Error("Public references must be a JSON file up to 4 MB");
|
|
180
|
+
references = JSON.parse(await readFile(flags.references, "utf8"));
|
|
181
|
+
}
|
|
182
|
+
const prepared = await request({
|
|
183
|
+
action: "prepare",
|
|
184
|
+
bytes: info.size,
|
|
185
|
+
title: typeof flags.title === "string" ? flags.title : basename2(filePath, extname2(filePath)).slice(0, 160),
|
|
186
|
+
message: typeof flags.message === "string" ? flags.message : "",
|
|
187
|
+
expiresAt: days === null ? null : new Date(Date.now() + days * 864e5).toISOString(),
|
|
188
|
+
...references === void 0 ? {} : { references },
|
|
189
|
+
...typeof flags.project === "string" ? { projectId: flags.project } : {}
|
|
190
|
+
});
|
|
191
|
+
try {
|
|
192
|
+
const form = new FormData();
|
|
193
|
+
form.append("cacheControl", "0");
|
|
194
|
+
form.append("", file, basename2(filePath));
|
|
195
|
+
const response = await fetch(prepared.upload.signedUrl, {
|
|
196
|
+
method: "PUT",
|
|
197
|
+
body: form,
|
|
198
|
+
headers: { "x-upsert": "false" },
|
|
199
|
+
redirect: "error",
|
|
200
|
+
signal: AbortSignal.timeout(15 * 6e4)
|
|
201
|
+
});
|
|
202
|
+
await response.body?.cancel();
|
|
203
|
+
if (!response.ok) throw new Error(`MP4 upload failed (${response.status}). Please try again`);
|
|
204
|
+
const share = await request({ action: "complete", id: prepared.id });
|
|
205
|
+
console.log(flags.json ? JSON.stringify(share, null, 2) : share.url);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
await request({ action: "revoke", id: prepared.id }).catch(() => {
|
|
208
|
+
});
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/commands.ts
|
|
214
|
+
var flagString = (flags, name) => {
|
|
215
|
+
const value = flags[name];
|
|
216
|
+
if (Array.isArray(value)) return value[value.length - 1];
|
|
217
|
+
return typeof value === "string" ? value : void 0;
|
|
218
|
+
};
|
|
219
|
+
var flagBool = (flags, name) => flags[name] === true;
|
|
220
|
+
var out = (line) => console.log(line);
|
|
221
|
+
async function setup2(flags) {
|
|
222
|
+
const source = flagString(flags, "from");
|
|
223
|
+
if (source !== void 0) videoShareToken(source);
|
|
224
|
+
const claude = flagBool(flags, "claude");
|
|
225
|
+
const agent = flagBool(flags, "agent");
|
|
226
|
+
await setup({
|
|
227
|
+
installAgentMcp: agent,
|
|
228
|
+
installAgentSkills: agent,
|
|
229
|
+
installClaudePlugin: claude,
|
|
230
|
+
openBrowser: !flagBool(flags, "no-open"),
|
|
231
|
+
log: out
|
|
232
|
+
});
|
|
233
|
+
if (source !== void 0) await cloneVideoProject(source);
|
|
234
|
+
}
|
|
235
|
+
async function clone(positional, flags) {
|
|
236
|
+
if (positional.length !== 1) throw new Error("Usage: impractical clone <video-share-url> [--json]");
|
|
237
|
+
await cloneVideoProject(positional[0], flagBool(flags, "json"));
|
|
238
|
+
}
|
|
239
|
+
async function login(flags) {
|
|
240
|
+
const patch = {};
|
|
241
|
+
const key = flagString(flags, "key");
|
|
242
|
+
const supabaseUrl = flagString(flags, "supabase-url");
|
|
243
|
+
const appUrl = flagString(flags, "app-url");
|
|
244
|
+
if (key) patch.apiKey = key;
|
|
245
|
+
if (supabaseUrl) patch.supabaseUrl = supabaseUrl;
|
|
246
|
+
if (appUrl) patch.appUrl = appUrl;
|
|
247
|
+
const config = { ...loadConfig(), ...patch };
|
|
248
|
+
if (!config.apiKey || !config.supabaseUrl) {
|
|
249
|
+
out("Missing API key. Run: npx --yes @impractical-ai/motion setup --agent");
|
|
250
|
+
process.exitCode = 1;
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const snapshot = await invokeAt(
|
|
254
|
+
config.supabaseUrl,
|
|
255
|
+
"workspace-bootstrap",
|
|
256
|
+
{},
|
|
257
|
+
config.apiKey,
|
|
258
|
+
void 0,
|
|
259
|
+
// IMPRACTICAL_WORKSPACE, for a multi-workspace key; unset sends nothing.
|
|
260
|
+
config.workspace
|
|
261
|
+
);
|
|
262
|
+
if (Object.keys(patch).length) saveConfig(patch);
|
|
263
|
+
out(`org: ${snapshot.orgId}`);
|
|
264
|
+
out(`plan: ${snapshot.plan} (access: ${snapshot.hasAccess ? "yes" : "no"})`);
|
|
265
|
+
if (snapshot.accessUntil) out(`access until: ${snapshot.accessUntil}`);
|
|
266
|
+
out("Logged in. Credentials stored in ~/.impractical/config.json");
|
|
267
|
+
}
|
|
268
|
+
async function whoami(_flags) {
|
|
269
|
+
const snapshot = await invoke("workspace-bootstrap");
|
|
270
|
+
out(`org: ${snapshot.orgId}`);
|
|
271
|
+
out(`plan: ${snapshot.plan} (access: ${snapshot.hasAccess ? "yes" : "no"})`);
|
|
272
|
+
if (snapshot.accessUntil) out(`access until: ${snapshot.accessUntil}`);
|
|
273
|
+
}
|
|
274
|
+
async function keys(positional, flags) {
|
|
275
|
+
const action = positional[0];
|
|
276
|
+
if (action === "list" || !action) {
|
|
277
|
+
const result = await invoke("create-api-key", {
|
|
278
|
+
action: "list"
|
|
279
|
+
});
|
|
280
|
+
for (const key of result.keys) {
|
|
281
|
+
out(
|
|
282
|
+
`${key.id} ${key.keyPrefix}\u2026 "${key.name}" created ${key.createdAt}` + (key.revokedAt ? " REVOKED" : key.lastUsedAt ? ` last used ${key.lastUsedAt}` : " never used")
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (!result.keys.length) out("No API keys. Create one from the studio web app.");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (action === "revoke") {
|
|
289
|
+
const id = positional[1] ?? flagString(flags, "id");
|
|
290
|
+
if (!id) throw new Error("Usage: impractical keys revoke <keyId>");
|
|
291
|
+
await invoke("create-api-key", { action: "revoke", id });
|
|
292
|
+
out(`Revoked ${id}`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
throw new Error(`Unknown keys action "${action}" (use: list, revoke)`);
|
|
296
|
+
}
|
|
297
|
+
function reportJobs(rows, slugByDraft) {
|
|
298
|
+
for (const row of rows) {
|
|
299
|
+
const label = slugByDraft?.get(row.draft_id) ?? row.draft_id;
|
|
300
|
+
if (row.status === "completed") {
|
|
301
|
+
out(`${label}: completed (revision ${row.revision ?? "?"})`);
|
|
302
|
+
if (row.master?.url) out(` master: ${row.master.url}`);
|
|
303
|
+
} else {
|
|
304
|
+
out(`${label}: ${row.status}${row.error ? ` \u2014 ${row.error}` : ""}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async function status(flags) {
|
|
309
|
+
const projectId = flagString(flags, "project");
|
|
310
|
+
const jobsFlag = flagString(flags, "jobs");
|
|
311
|
+
if (projectId) {
|
|
312
|
+
const result = await invoke("list-projects", { projectId });
|
|
313
|
+
out(`${result.project.name} (${result.project.id})`);
|
|
314
|
+
for (const draft of result.project.drafts) {
|
|
315
|
+
out(` draft ${draft.id} (${draft.slug}) \u2014 ${draft.status}, revision ${draft.source_revision}`);
|
|
316
|
+
}
|
|
317
|
+
for (const job of result.project.jobs.slice(0, 10)) {
|
|
318
|
+
out(` job ${job.id} [${job.draft_id}] \u2014 ${job.status}${job.stage ? ` (${job.stage})` : ""}${job.error ? ` \u2014 ${job.error}` : ""}`);
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (!jobsFlag) throw new Error("Usage: impractical status --project <id> | --jobs <id,id> [--wait]");
|
|
323
|
+
const jobIds = jobsFlag.split(",").map((id) => id.trim());
|
|
324
|
+
if (flagBool(flags, "wait")) {
|
|
325
|
+
const seen = /* @__PURE__ */ new Map();
|
|
326
|
+
const rows = await waitForJobs(jobIds, (latest) => {
|
|
327
|
+
for (const row of latest) {
|
|
328
|
+
const line = `${row.id}: ${row.status}${row.stage ? ` \u2014 ${row.stage}` : ""}`;
|
|
329
|
+
if (seen.get(row.id) !== line) {
|
|
330
|
+
seen.set(row.id, line);
|
|
331
|
+
out(line);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
reportJobs(rows);
|
|
336
|
+
if (rows.some((row) => row.status !== "completed")) process.exitCode = 1;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
reportJobs(await jobStatus(jobIds));
|
|
340
|
+
}
|
|
341
|
+
async function exportDraft(positional, flags) {
|
|
342
|
+
const draftId = positional[0];
|
|
343
|
+
if (!draftId) {
|
|
344
|
+
throw new Error("Usage: impractical export <draftId> [--wait] [--out video.mp4]");
|
|
345
|
+
}
|
|
346
|
+
const started = await invoke("start-export", { draftId });
|
|
347
|
+
out(`Export started (job ${started.jobId}).`);
|
|
348
|
+
if (!flagBool(flags, "wait") && !flagString(flags, "out")) {
|
|
349
|
+
out(`Follow with: impractical status --jobs ${started.jobId} --wait`);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const rows = await waitForJobs([started.jobId], () => {
|
|
353
|
+
}, 36e5);
|
|
354
|
+
const row = rows[0];
|
|
355
|
+
if (row?.status !== "completed" || !row.master?.url) {
|
|
356
|
+
throw new Error(`Export ${row?.status ?? "failed"}${row?.error ? `: ${row.error}` : ""}`);
|
|
357
|
+
}
|
|
358
|
+
out(`master: ${row.master.url}`);
|
|
359
|
+
const outPath = flagString(flags, "out");
|
|
360
|
+
if (outPath) {
|
|
361
|
+
const response = await fetch(row.master.url);
|
|
362
|
+
if (!response.ok) throw new Error(`Download failed (${response.status})`);
|
|
363
|
+
writeFileSync(outPath, new Uint8Array(await response.arrayBuffer()));
|
|
364
|
+
out(`Saved ${(row.master.bytes / 1e6).toFixed(1)} MB to ${outPath}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async function upload(positional, flags) {
|
|
368
|
+
const filePath = positional[0];
|
|
369
|
+
const projectId = flagString(flags, "project");
|
|
370
|
+
const draftId = flagString(flags, "draft");
|
|
371
|
+
if (!filePath || !projectId || !draftId) {
|
|
372
|
+
throw new Error("Usage: impractical upload <file> --project <projectId> --draft <draftId>");
|
|
373
|
+
}
|
|
374
|
+
const assetId = await uploadAsset({ projectId, draftId, filePath, log: out });
|
|
375
|
+
out(`assetId: ${assetId}`);
|
|
376
|
+
}
|
|
377
|
+
async function link(positional, _flags) {
|
|
378
|
+
const config = requireConfig();
|
|
379
|
+
const [projectId, slug] = positional;
|
|
380
|
+
if (!projectId) throw new Error("Usage: impractical link <projectId> [draftSlug]");
|
|
381
|
+
out(studioLink(config.appUrl, projectId, slug));
|
|
382
|
+
}
|
|
383
|
+
async function brand(positional, flags) {
|
|
384
|
+
const action = positional[0];
|
|
385
|
+
if (action === "extract") {
|
|
386
|
+
const url = positional[1];
|
|
387
|
+
if (!url) throw new Error("Usage: impractical brand extract <url> [--wait]");
|
|
388
|
+
const started = await invoke("brand-extraction", {
|
|
389
|
+
action: "start",
|
|
390
|
+
url,
|
|
391
|
+
...flagBool(flags, "reextract") ? { reextract: true } : {}
|
|
392
|
+
});
|
|
393
|
+
out(`Extraction started for "${started.slug}" (run ${started.job}).`);
|
|
394
|
+
if (!flagBool(flags, "wait")) return;
|
|
395
|
+
const deadline = Date.now() + 15e5;
|
|
396
|
+
let lastLine = "";
|
|
397
|
+
while (true) {
|
|
398
|
+
if (Date.now() >= deadline) throw new Error("Brand extraction timed out");
|
|
399
|
+
const statusResult = await invoke("brand-extraction", { action: "status", job: started.job });
|
|
400
|
+
const line = `${statusResult.status}${statusResult.pct != null ? ` ${statusResult.pct}%` : ""}${statusResult.stage ? ` \u2014 ${statusResult.stage}` : ""}`;
|
|
401
|
+
if (line !== lastLine) {
|
|
402
|
+
lastLine = line;
|
|
403
|
+
out(line);
|
|
404
|
+
}
|
|
405
|
+
if (statusResult.status === "done") {
|
|
406
|
+
out(`Brand kit saved as "${statusResult.slug ?? started.slug}". Use it with: impractical generate --brand ${statusResult.slug ?? started.slug} ...`);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (statusResult.status === "failed" || statusResult.error) {
|
|
410
|
+
throw new Error(`Brand extraction failed${statusResult.error ? `: ${statusResult.error}` : ""}`);
|
|
411
|
+
}
|
|
412
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (action === "list" || !action) {
|
|
416
|
+
const result = await invoke("brand-kits", {
|
|
417
|
+
action: "list"
|
|
418
|
+
});
|
|
419
|
+
for (const kit of result.brands) out(`${kit.slug}${kit.name ? ` (${kit.name})` : ""}`);
|
|
420
|
+
if (!result.brands.length) out("No brand kits yet. Extract one with: impractical brand extract <url> --wait");
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (action === "get") {
|
|
424
|
+
const slug = positional[1];
|
|
425
|
+
if (!slug) throw new Error("Usage: impractical brand get <slug>");
|
|
426
|
+
const result = await invoke("brand-kits", { action: "get", slug });
|
|
427
|
+
out(JSON.stringify(result.brand, null, 2));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
throw new Error(`Unknown brand action "${action}" (use: extract, list, get)`);
|
|
431
|
+
}
|
|
432
|
+
function validateMeteredCommand(command, positional, flags) {
|
|
433
|
+
if (command === "status" && !flagString(flags, "project") && !flagString(flags, "jobs")) {
|
|
434
|
+
throw new Error("Usage: impractical status --project <id> | --jobs <id,id> [--wait]");
|
|
435
|
+
}
|
|
436
|
+
if (command === "export" && !positional[0]) {
|
|
437
|
+
throw new Error("Usage: impractical export <draftId> [--wait] [--out video.mp4]");
|
|
438
|
+
}
|
|
439
|
+
if (command === "upload" && (!positional[0] || !flagString(flags, "project") || !flagString(flags, "draft"))) {
|
|
440
|
+
throw new Error("Usage: impractical upload <file> --project <projectId> --draft <draftId>");
|
|
441
|
+
}
|
|
442
|
+
if (command === "brand") {
|
|
443
|
+
const action = positional[0];
|
|
444
|
+
if (action === "extract" && !positional[1]) {
|
|
445
|
+
throw new Error("Usage: impractical brand extract <url> [--wait]");
|
|
446
|
+
}
|
|
447
|
+
if (action === "get" && !positional[1]) {
|
|
448
|
+
throw new Error("Usage: impractical brand get <slug>");
|
|
449
|
+
}
|
|
450
|
+
if (action && !["extract", "list", "get"].includes(action)) {
|
|
451
|
+
throw new Error(`Unknown brand action "${action}" (use: extract, list, get)`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
export {
|
|
456
|
+
brand,
|
|
457
|
+
clone,
|
|
458
|
+
exportDraft,
|
|
459
|
+
keys,
|
|
460
|
+
link,
|
|
461
|
+
login,
|
|
462
|
+
setup2 as setup,
|
|
463
|
+
shareVideo,
|
|
464
|
+
status,
|
|
465
|
+
upload,
|
|
466
|
+
validateMeteredCommand,
|
|
467
|
+
whoami
|
|
468
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
saveChecks
|
|
4
|
+
} from "./chunk-JSEDAJHM.js";
|
|
5
|
+
import "./chunk-QQ6NYC74.js";
|
|
6
|
+
import "./chunk-XWCSIM4L.js";
|
|
7
|
+
import {
|
|
8
|
+
invoke
|
|
9
|
+
} from "./chunk-FTSZCIMV.js";
|
|
10
|
+
import "./chunk-2GCSX3YN.js";
|
|
11
|
+
import "./chunk-77HVPD4G.js";
|
|
12
|
+
|
|
13
|
+
// src/mcp/draft-check.ts
|
|
14
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
15
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
16
|
+
var DRAFT_FILE = /\.(tsx?|jsx?|css|json|html)$/;
|
|
17
|
+
var MAX_FILE = 2e6;
|
|
18
|
+
function readDraft(root) {
|
|
19
|
+
const files = {};
|
|
20
|
+
const walk = (dir) => {
|
|
21
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
22
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
23
|
+
const full = join(dir, entry.name);
|
|
24
|
+
if (entry.isDirectory()) walk(full);
|
|
25
|
+
else if (entry.isFile() && DRAFT_FILE.test(entry.name) && statSync(full).size <= MAX_FILE) {
|
|
26
|
+
files[relative(root, full).split(sep).join("/")] = readFileSync(full, "utf8");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
walk(root);
|
|
31
|
+
return files;
|
|
32
|
+
}
|
|
33
|
+
async function draftCheck(dir, changed) {
|
|
34
|
+
if (!dir) throw new Error("Usage: impractical draft-check --dir <draft folder> [--changed <path> ...]");
|
|
35
|
+
const root = resolve(dir);
|
|
36
|
+
const merged = readDraft(root);
|
|
37
|
+
const written = {};
|
|
38
|
+
for (const path of changed.length ? changed : Object.keys(merged)) {
|
|
39
|
+
const rel = relative(root, resolve(root, path)).split(sep).join("/");
|
|
40
|
+
if (rel in merged) written[rel] = merged[rel];
|
|
41
|
+
}
|
|
42
|
+
if (!Object.keys(written).length) return "";
|
|
43
|
+
const checks = await saveChecks({ invoke }, merged, written);
|
|
44
|
+
return [
|
|
45
|
+
checks.preflightError ? `BUILD PREFLIGHT FAILED: this version of the draft won't build. Fix it and save again:
|
|
46
|
+
${checks.preflightError}` : "",
|
|
47
|
+
...checks.notes,
|
|
48
|
+
checks.advisory
|
|
49
|
+
].filter(Boolean).join("\n\n");
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
draftCheck
|
|
53
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildServerInstructions
|
|
4
|
+
} from "./chunk-ZEVR6SM6.js";
|
|
5
|
+
import "./chunk-M6427SHW.js";
|
|
6
|
+
import {
|
|
7
|
+
VIDEO_UI_GUIDE,
|
|
8
|
+
applyLocalMotionLibraryOverride,
|
|
9
|
+
craftLabEnabled,
|
|
10
|
+
gateReformEnabled,
|
|
11
|
+
motionLabEnabled,
|
|
12
|
+
phasedFlowEnabled,
|
|
13
|
+
qualityLoopEnabled
|
|
14
|
+
} from "./chunk-JSEDAJHM.js";
|
|
15
|
+
import "./chunk-QQ6NYC74.js";
|
|
16
|
+
import "./chunk-TETWFQPX.js";
|
|
17
|
+
import "./chunk-XWCSIM4L.js";
|
|
18
|
+
import {
|
|
19
|
+
invoke,
|
|
20
|
+
withMeteredCall
|
|
21
|
+
} from "./chunk-FTSZCIMV.js";
|
|
22
|
+
import "./chunk-2GCSX3YN.js";
|
|
23
|
+
import "./chunk-77HVPD4G.js";
|
|
24
|
+
|
|
25
|
+
// src/mcp/guidance.ts
|
|
26
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
27
|
+
import { dirname, join, resolve } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
var assetsDir = () => {
|
|
30
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
for (const candidate of [resolve(here, "../assets"), resolve(here, "../../assets")]) {
|
|
32
|
+
try {
|
|
33
|
+
readFileSync(join(candidate, "impractical/SKILL.md"));
|
|
34
|
+
return candidate;
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
throw new Error("the CLI ships no assets folder");
|
|
39
|
+
};
|
|
40
|
+
var fileFor = (key) => `${key.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()}.md`;
|
|
41
|
+
async function collectGuidance(call = (name, body) => withMeteredCall("motion_context", () => invoke(name, body))) {
|
|
42
|
+
const assets = assetsDir();
|
|
43
|
+
const files = [
|
|
44
|
+
{
|
|
45
|
+
file: "mcp-instructions.md",
|
|
46
|
+
title: "The MCP's instructions",
|
|
47
|
+
source: "cli/src/mcp/stdio.ts buildServerInstructions (what every MCP agent gets when it connects)",
|
|
48
|
+
text: buildServerInstructions({
|
|
49
|
+
qualityLoop: qualityLoopEnabled(),
|
|
50
|
+
motionLab: motionLabEnabled(),
|
|
51
|
+
craftLab: craftLabEnabled(),
|
|
52
|
+
gateReform: gateReformEnabled(),
|
|
53
|
+
phasedFlow: phasedFlowEnabled()
|
|
54
|
+
})
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
file: "impractical-skill.md",
|
|
58
|
+
title: "The impractical skill (the workflow)",
|
|
59
|
+
source: "cli/assets/impractical/SKILL.md",
|
|
60
|
+
text: readFileSync(join(assets, "impractical/SKILL.md"), "utf8")
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
file: "motion-standard.md",
|
|
64
|
+
title: "The motion standard (arrive, hold, exit)",
|
|
65
|
+
source: "docs/motion-standard.md (shipped as cli/assets/motion-standard.md)",
|
|
66
|
+
text: readFileSync(join(assets, "motion-standard.md"), "utf8")
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
file: "video-ui-guide.md",
|
|
70
|
+
title: "The video UI guide",
|
|
71
|
+
source: "cli/src/mcp/video-ui-guidelines.ts VIDEO_UI_GUIDE (motion_context leads with it)",
|
|
72
|
+
text: VIDEO_UI_GUIDE
|
|
73
|
+
}
|
|
74
|
+
];
|
|
75
|
+
const library = await applyLocalMotionLibraryOverride(
|
|
76
|
+
await call("motion-library", { brief: "the house guidance", detail: "focused", limit: 1 }),
|
|
77
|
+
"focused"
|
|
78
|
+
);
|
|
79
|
+
const guidance = library.guidance ?? {};
|
|
80
|
+
for (const [key, value] of Object.entries(guidance)) {
|
|
81
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
82
|
+
files.push({
|
|
83
|
+
file: fileFor(key),
|
|
84
|
+
title: `motion_context guidance: ${key}`,
|
|
85
|
+
source: `motion_context guidance.${key} (the motion-library function; built from the repo by scripts/build-motion-library.mjs)`,
|
|
86
|
+
text: value
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return files;
|
|
90
|
+
}
|
|
91
|
+
async function guidanceCommand(dir, call) {
|
|
92
|
+
if (!dir) throw new Error("Usage: impractical guidance --dir <folder>");
|
|
93
|
+
const root = resolve(dir);
|
|
94
|
+
const files = await collectGuidance(call);
|
|
95
|
+
rmSync(root, { recursive: true, force: true });
|
|
96
|
+
mkdirSync(root, { recursive: true });
|
|
97
|
+
for (const { file, text } of files) writeFileSync(join(root, file), text.trimEnd() + "\n");
|
|
98
|
+
const index = [
|
|
99
|
+
"# Impractical guidance",
|
|
100
|
+
"",
|
|
101
|
+
"Everything an agent working through the Impractical MCP is told, one file each. Read every file before you",
|
|
102
|
+
"write a film. Where each comes from (to change one, change its source): docs/agent-guidance.md.",
|
|
103
|
+
"",
|
|
104
|
+
...files.map(({ file, title, source }) => `- ${file}: ${title}. Source: ${source}.`),
|
|
105
|
+
""
|
|
106
|
+
].join("\n");
|
|
107
|
+
writeFileSync(join(root, "README.md"), index);
|
|
108
|
+
return files.map(({ file }) => file).join("\n");
|
|
109
|
+
}
|
|
110
|
+
export {
|
|
111
|
+
collectGuidance,
|
|
112
|
+
guidanceCommand
|
|
113
|
+
};
|