@impractical-ai/motion 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,6 +34,58 @@ Setup writes the API credential to `~/.impractical/config.json` with mode `0600`
34
34
  variables `IMPRACTICAL_API_KEY`, `IMPRACTICAL_SUPABASE_URL`, and `IMPRACTICAL_APP_URL` take
35
35
  precedence over that file.
36
36
 
37
+ ## Start from a shared video
38
+
39
+ When the sender includes an editable project, the public video page has an
40
+ **Edit this video** action. Its single command both connects your agent and
41
+ copies the project into your workspace:
42
+
43
+ ```sh
44
+ npx --yes @impractical-ai/motion@0.1.11 setup --agent --from https://impractical.ai/v/SHARE_TOKEN
45
+ ```
46
+
47
+ Use the real URL copied from the page. Sign in when prompted, then restart your
48
+ coding agent. The command prints your new project URL and a prompt containing
49
+ its project ID. Drafts stay in your Impractical workspace and are edited through
50
+ MCP; no source is executed or written into your current directory by this command.
51
+
52
+ Already connected, or retrying an interrupted import:
53
+
54
+ ```sh
55
+ npx --yes @impractical-ai/motion@0.1.11 clone https://impractical.ai/v/SHARE_TOKEN
56
+ ```
57
+
58
+ Repeating the command returns the same copy for that share and workspace. The
59
+ copy is independent of the sender's project. An MP4-only share cannot be cloned.
60
+
61
+ ## Choose who can edit a shared video
62
+
63
+ Each link is **view only by default**. Upload without `--project` to share just
64
+ the video and its public brand information; the page will hide **Edit this video**
65
+ and the API will reject project-copy requests for that link:
66
+
67
+ ```sh
68
+ npx --yes @impractical-ai/motion@0.1.11 share ./video.mp4 --title "For Acme" --references ./public-brand.json
69
+ ```
70
+
71
+ `--project` explicitly lets recipients copy **all editable drafts and assets**
72
+ from that project, so use a project you intend to share:
73
+
74
+ ```sh
75
+ npx --yes @impractical-ai/motion@0.1.11 share ./video.mp4 --title "For Acme" --references ./public-brand.json --project PROJECT_ID
76
+ ```
77
+
78
+ Enable cloning on an existing video share, or disable future clones:
79
+
80
+ ```sh
81
+ npx --yes @impractical-ai/motion@0.1.11 share allow-editing SHARE_ID --project PROJECT_ID
82
+ npx --yes @impractical-ai/motion@0.1.11 share view-only SHARE_ID
83
+ ```
84
+
85
+ Detaching the source or revoking the video does not remove copies already owned
86
+ by recipients. The existing upload API accepts the same opt-in as `projectId`.
87
+ The shorter `share attach` and `share detach` aliases also remain supported.
88
+
37
89
  ## MCP server
38
90
 
39
91
  The installer configures supported clients to run:
@@ -13,8 +13,8 @@ import {
13
13
  waitForJobs,
14
14
  withAdmissionReceipt,
15
15
  withMeteredCall
16
- } from "./chunk-CZANULCU.js";
17
- import "./chunk-6FVZ6IWR.js";
16
+ } from "./chunk-VTHAZQEV.js";
17
+ import "./chunk-HGA2O3JM.js";
18
18
  import "./chunk-77HVPD4G.js";
19
19
  export {
20
20
  ApiContractError,
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@impractical-ai/motion",
6
- version: "0.1.10",
6
+ version: "0.1.11",
7
7
  private: false,
8
8
  description: "Create and refine Impractical Motion videos from coding agents and the terminal.",
9
9
  homepage: "https://github.com/Openpod/impractical-motion#readme",
@@ -56,7 +56,7 @@ var PRODUCTION_APP_URL = "https://motion.impractical.ai";
56
56
  var PRODUCTION_SUPABASE_URL = "https://uaxjweednprtbhbceify.supabase.co";
57
57
  var PACKAGE_NAME = "@impractical-ai/motion";
58
58
  var PACKAGE_VERSION = package_default.version;
59
- var BUILD_COMMIT = "779568a" ? "779568a" : "dev";
59
+ var BUILD_COMMIT = "9f06a02" ? "9f06a02" : "dev";
60
60
  var BUILD_ID = `${PACKAGE_VERSION}+${BUILD_COMMIT}`;
61
61
  var PACKAGE_COMMAND = `npx --yes ${PACKAGE_NAME}`;
62
62
 
@@ -6,14 +6,14 @@ import {
6
6
  loadConfig,
7
7
  recoverCorruptConfig,
8
8
  saveConfig
9
- } from "./chunk-CZANULCU.js";
9
+ } from "./chunk-VTHAZQEV.js";
10
10
  import {
11
11
  PACKAGE_COMMAND,
12
12
  PACKAGE_NAME,
13
13
  PACKAGE_VERSION,
14
14
  PRODUCTION_APP_URL,
15
15
  PRODUCTION_SUPABASE_URL
16
- } from "./chunk-6FVZ6IWR.js";
16
+ } from "./chunk-HGA2O3JM.js";
17
17
 
18
18
  // src/onboarding.ts
19
19
  import {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  PRODUCTION_APP_URL,
4
4
  PRODUCTION_SUPABASE_URL
5
- } from "./chunk-6FVZ6IWR.js";
5
+ } from "./chunk-HGA2O3JM.js";
6
6
 
7
7
  // src/config.ts
8
8
  import {
@@ -141,6 +141,9 @@ function bodyRecord(body) {
141
141
  function validateApiResponse(name, body, data) {
142
142
  if (!isRecord(data)) throw new ApiContractError(name, "a JSON object");
143
143
  const input = bodyRecord(body);
144
+ if (name === "video-shares" && input.action === "clone" && (typeof data.projectId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(data.projectId) || typeof data.alreadyForked !== "boolean")) {
145
+ throw new ApiContractError(name, "a projectId and alreadyForked flag");
146
+ }
144
147
  if (name === "workspace-bootstrap") {
145
148
  const billing = data.billing;
146
149
  if (typeof data.orgId !== "string" || typeof data.plan !== "string" || typeof data.hasAccess !== "boolean" || !isRecord(billing) || typeof billing.status !== "string") {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  setup
4
- } from "./chunk-WB7FRXAG.js";
4
+ } from "./chunk-SN6SLPIC.js";
5
5
  import {
6
6
  invoke,
7
7
  invokeAt,
@@ -11,8 +11,8 @@ import {
11
11
  saveConfig,
12
12
  studioLink,
13
13
  waitForJobs
14
- } from "./chunk-CZANULCU.js";
15
- import "./chunk-6FVZ6IWR.js";
14
+ } from "./chunk-VTHAZQEV.js";
15
+ import "./chunk-HGA2O3JM.js";
16
16
  import "./chunk-77HVPD4G.js";
17
17
 
18
18
  // src/commands.ts
@@ -1204,6 +1204,133 @@ function buildInitialFiles(input) {
1204
1204
  // ../src/shared/video-models.ts
1205
1205
  var DEFAULT_OPENROUTER_VIDEO_MODEL = "anthropic/claude-opus-5";
1206
1206
 
1207
+ // ../src/shared/video-clone-command.ts
1208
+ var TOKEN = /^[A-Za-z0-9_-]{43}$/;
1209
+ var SHARE_HOSTS = /* @__PURE__ */ new Set(["impractical.ai", "www.impractical.ai", "impractical-video-shares.vercel.app"]);
1210
+ function videoShareToken(source) {
1211
+ if (TOKEN.test(source)) return source;
1212
+ try {
1213
+ const url = new URL(source);
1214
+ const token = url.pathname.match(/^\/v\/([A-Za-z0-9_-]{43})\/?$/)?.[1];
1215
+ if (url.protocol === "https:" && SHARE_HOSTS.has(url.host) && !url.username && !url.password && !url.search && !url.hash && token) return token;
1216
+ } catch {
1217
+ }
1218
+ throw new Error("Use an Impractical video share URL: https://impractical.ai/v/<token>");
1219
+ }
1220
+
1221
+ // src/clone-video-project.ts
1222
+ async function cloneVideoProject(source, json = false) {
1223
+ const token = videoShareToken(source);
1224
+ const result = await invoke("video-shares", { action: "clone", token });
1225
+ const url = studioLink(requireConfig().appUrl, result.projectId);
1226
+ if (json) {
1227
+ console.log(JSON.stringify({ ...result, url }, null, 2));
1228
+ return;
1229
+ }
1230
+ console.log(result.alreadyForked ? "Your editable copy is ready." : "Copied the project into your workspace.");
1231
+ console.log(`Open: ${url}`);
1232
+ console.log(`Project: ${result.projectId}`);
1233
+ console.log("After restarting your agent, paste:");
1234
+ console.log(`Use Impractical to open project ${result.projectId} and help me iterate on this video.`);
1235
+ }
1236
+
1237
+ // src/video-sharing.ts
1238
+ import { openAsBlob } from "node:fs";
1239
+ import { readFile, stat } from "node:fs/promises";
1240
+ import { basename as basename2, extname as extname2 } from "node:path";
1241
+ async function shareVideo(rest, flags) {
1242
+ const config = requireConfig();
1243
+ const request = async (body) => {
1244
+ const response = await fetch(
1245
+ `${config.supabaseUrl.replace(/\/$/, "")}/functions/v1/video-shares`,
1246
+ {
1247
+ method: body ? "POST" : "GET",
1248
+ redirect: "error",
1249
+ signal: AbortSignal.timeout(45e3),
1250
+ headers: { authorization: `Bearer ${config.apiKey}`, "content-type": "application/json" },
1251
+ body: body ? JSON.stringify(body) : void 0
1252
+ }
1253
+ );
1254
+ const data = await response.json().catch(() => ({}));
1255
+ if (!response.ok) throw new Error(data.error || `Video sharing failed (${response.status})`);
1256
+ return data;
1257
+ };
1258
+ if (rest[0] === "list") {
1259
+ console.log(JSON.stringify(await request(), null, 2));
1260
+ return;
1261
+ }
1262
+ if (rest[0] === "revoke") {
1263
+ if (!rest[1]) throw new Error("Usage: impractical share revoke <share-id>");
1264
+ await request({ action: "revoke", id: rest[1] });
1265
+ console.log("Share link revoked. Already-open videos may remain playable for up to 5 minutes.");
1266
+ return;
1267
+ }
1268
+ const allowEditing = rest[0] === "allow-editing" || rest[0] === "attach";
1269
+ const viewOnly = rest[0] === "view-only" || rest[0] === "detach";
1270
+ if (allowEditing || viewOnly) {
1271
+ if (!rest[1] || allowEditing && typeof flags.project !== "string") {
1272
+ throw new Error("Usage: impractical share allow-editing <share-id> --project <id> | share view-only <share-id>");
1273
+ }
1274
+ await request({ action: "attach-project", id: rest[1], projectId: viewOnly ? null : flags.project });
1275
+ 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.");
1276
+ return;
1277
+ }
1278
+ const filePath = rest[0];
1279
+ if (!filePath || extname2(filePath).toLowerCase() !== ".mp4") {
1280
+ throw new Error(
1281
+ 'Usage: impractical share ./video.mp4 [--title "A video for Acme"] [--message "..."] [--references ./public-brand.json] [--project <id>] [--expires-in 7] [--json]'
1282
+ );
1283
+ }
1284
+ const info = await stat(filePath);
1285
+ if (!info.isFile() || info.size < 16 || info.size > 3e8) {
1286
+ throw new Error("Choose an MP4 up to 300 MB");
1287
+ }
1288
+ const days = flags["expires-in"] === void 0 ? null : Number(flags["expires-in"]);
1289
+ if (days !== null && (!Number.isFinite(days) || days <= 0 || days > 3650)) {
1290
+ throw new Error("--expires-in must be a number of days between 0 and 3650");
1291
+ }
1292
+ const file = await openAsBlob(filePath, { type: "video/mp4" });
1293
+ const header = new Uint8Array(await file.slice(0, 16).arrayBuffer());
1294
+ if (String.fromCharCode(...header.slice(4, 8)) !== "ftyp") {
1295
+ throw new Error("This file does not have an MP4 header");
1296
+ }
1297
+ let references;
1298
+ if (typeof flags.references === "string") {
1299
+ const metadataFile = await stat(flags.references);
1300
+ if (!metadataFile.isFile() || metadataFile.size > 4e6) throw new Error("Public references must be a JSON file up to 4 MB");
1301
+ references = JSON.parse(await readFile(flags.references, "utf8"));
1302
+ }
1303
+ const prepared = await request({
1304
+ action: "prepare",
1305
+ bytes: info.size,
1306
+ title: typeof flags.title === "string" ? flags.title : basename2(filePath, extname2(filePath)).slice(0, 160),
1307
+ message: typeof flags.message === "string" ? flags.message : "",
1308
+ expiresAt: days === null ? null : new Date(Date.now() + days * 864e5).toISOString(),
1309
+ ...references === void 0 ? {} : { references },
1310
+ ...typeof flags.project === "string" ? { projectId: flags.project } : {}
1311
+ });
1312
+ try {
1313
+ const form = new FormData();
1314
+ form.append("cacheControl", "0");
1315
+ form.append("", file, basename2(filePath));
1316
+ const response = await fetch(prepared.upload.signedUrl, {
1317
+ method: "PUT",
1318
+ body: form,
1319
+ headers: { "x-upsert": "false" },
1320
+ redirect: "error",
1321
+ signal: AbortSignal.timeout(15 * 6e4)
1322
+ });
1323
+ await response.body?.cancel();
1324
+ if (!response.ok) throw new Error(`MP4 upload failed (${response.status}). Please try again`);
1325
+ const share = await request({ action: "complete", id: prepared.id });
1326
+ console.log(flags.json ? JSON.stringify(share, null, 2) : share.url);
1327
+ } catch (error) {
1328
+ await request({ action: "revoke", id: prepared.id }).catch(() => {
1329
+ });
1330
+ throw error;
1331
+ }
1332
+ }
1333
+
1207
1334
  // src/commands.ts
1208
1335
  var flagString = (flags, name) => {
1209
1336
  const value = flags[name];
@@ -1218,6 +1345,8 @@ var flagList = (flags, name) => {
1218
1345
  var flagBool = (flags, name) => flags[name] === true;
1219
1346
  var out = (line) => console.log(line);
1220
1347
  async function setup2(flags) {
1348
+ const source = flagString(flags, "from");
1349
+ if (source !== void 0) videoShareToken(source);
1221
1350
  const claude = flagBool(flags, "claude");
1222
1351
  const agent = flagBool(flags, "agent");
1223
1352
  await setup({
@@ -1227,6 +1356,11 @@ async function setup2(flags) {
1227
1356
  openBrowser: !flagBool(flags, "no-open"),
1228
1357
  log: out
1229
1358
  });
1359
+ if (source !== void 0) await cloneVideoProject(source);
1360
+ }
1361
+ async function clone(positional, flags) {
1362
+ if (positional.length !== 1) throw new Error("Usage: impractical clone <video-share-url> [--json]");
1363
+ await cloneVideoProject(positional[0], flagBool(flags, "json"));
1230
1364
  }
1231
1365
  async function login(flags) {
1232
1366
  const patch = {};
@@ -1522,12 +1656,14 @@ function validateMeteredCommand(command, positional, flags) {
1522
1656
  }
1523
1657
  export {
1524
1658
  brand,
1659
+ clone,
1525
1660
  exportDraft,
1526
1661
  generate,
1527
1662
  keys,
1528
1663
  link,
1529
1664
  login,
1530
1665
  setup2 as setup,
1666
+ shareVideo,
1531
1667
  status,
1532
1668
  upload,
1533
1669
  validateMeteredCommand,
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  PACKAGE_VERSION
4
- } from "./chunk-6FVZ6IWR.js";
4
+ } from "./chunk-HGA2O3JM.js";
5
5
  import "./chunk-77HVPD4G.js";
6
6
 
7
7
  // src/cli-args.ts
@@ -17,6 +17,7 @@ var CLI_OPTIONS = {
17
17
  brand: { type: "string" },
18
18
  asset: { type: "string", multiple: true },
19
19
  project: { type: "string" },
20
+ from: { type: "string" },
20
21
  feedback: { type: "string" },
21
22
  model: { type: "string" },
22
23
  slug: { type: "string" },
@@ -26,6 +27,11 @@ var CLI_OPTIONS = {
26
27
  out: { type: "string" },
27
28
  draft: { type: "string" },
28
29
  id: { type: "string" },
30
+ title: { type: "string" },
31
+ message: { type: "string" },
32
+ references: { type: "string" },
33
+ "expires-in": { type: "string" },
34
+ json: { type: "boolean" },
29
35
  wait: { type: "boolean" },
30
36
  agent: { type: "boolean" },
31
37
  claude: { type: "boolean" },
@@ -48,6 +54,7 @@ var HELP = `impractical \u2014 Impractical Motion from your terminal (and your c
48
54
  Setup
49
55
  setup --claude Sign in and install the Claude plugin + MCP
50
56
  setup --agent Sign in and install MCP for detected coding agents
57
+ --from <video-share-url> Also copy the shared project into your workspace
51
58
  login --key imk_... Manual credential override for development
52
59
  whoami Show org and plan status
53
60
  keys [list|revoke <id>] List or revoke agent API keys
@@ -66,6 +73,14 @@ Edit drafts
66
73
  Editing happens through the MCP server (see "Agents" above) \u2014 draft source
67
74
  lives in the cloud, so there is no working copy to pull or push.
68
75
  link <projectId> [slug] Print the studio tweak-UI URL
76
+ clone <video-share-url> Add an editable copy to your connected workspace
77
+
78
+ Share videos
79
+ share <file.mp4> [--title "..."] [--references brand.json] [--project <id>]
80
+ Omit --project for a view-only link (the default)
81
+ share allow-editing <share-id> --project <id> Let recipients copy and edit
82
+ share view-only <share-id> Hide editing and block future project copies
83
+ share list | share revoke <share-id>
69
84
 
70
85
  Assets & brands
71
86
  upload <file> --project <id> --draft <id>
@@ -97,6 +112,10 @@ async function runCli(args, deps) {
97
112
  switch (command) {
98
113
  case "setup":
99
114
  return commands.setup(flags);
115
+ case "clone":
116
+ return commands.clone(rest, flags);
117
+ case "share":
118
+ return commands.shareVideo(rest, flags);
100
119
  case "motion":
101
120
  if (rest[0] === "wizard") return commands.setup(flags);
102
121
  break;
@@ -140,11 +159,11 @@ async function runCli(args, deps) {
140
159
  // src/index.ts
141
160
  runCli(process.argv.slice(2), {
142
161
  writeLine: (line) => console.log(line),
143
- loadCommands: () => import("./commands-YJFMKK2K.js"),
144
- loadApi: () => import("./api-4WE4LYPB.js"),
145
- loadMcp: () => import("./stdio-S7VGBKNS.js")
162
+ loadCommands: () => import("./commands-EDRH2XMY.js"),
163
+ loadApi: () => import("./api-FMMG3W6J.js"),
164
+ loadMcp: () => import("./stdio-VBPVT2KS.js")
146
165
  }).catch(async (error) => {
147
- const { ApiError } = await import("./api-4WE4LYPB.js");
166
+ const { ApiError } = await import("./api-FMMG3W6J.js");
148
167
  if (error instanceof ApiError) {
149
168
  console.error(`Error (${error.status} from ${error.fn}): ${error.message}`);
150
169
  if (error.status === 429) console.error("Weekly MCP allowance reached. Upgrade or wait for Monday UTC.");
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  openBrowser
4
- } from "./chunk-WB7FRXAG.js";
4
+ } from "./chunk-SN6SLPIC.js";
5
5
  import {
6
6
  ApiError,
7
7
  admitMcpCall,
@@ -11,13 +11,13 @@ import {
11
11
  saveConfig,
12
12
  validateApiResponse,
13
13
  withAdmissionReceipt
14
- } from "./chunk-CZANULCU.js";
14
+ } from "./chunk-VTHAZQEV.js";
15
15
  import {
16
16
  BUILD_COMMIT,
17
17
  PACKAGE_VERSION,
18
18
  PRODUCTION_APP_URL,
19
19
  PRODUCTION_SUPABASE_URL
20
- } from "./chunk-6FVZ6IWR.js";
20
+ } from "./chunk-HGA2O3JM.js";
21
21
  import {
22
22
  __commonJS,
23
23
  __export,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@impractical-ai/motion",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "private": false,
5
5
  "description": "Create and refine Impractical Motion videos from coding agents and the terminal.",
6
6
  "homepage": "https://github.com/Openpod/impractical-motion#readme",