@impractical-ai/motion 0.1.0 → 0.1.2
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 +43 -0
- package/assets/engine.d.ts +7 -1
- package/assets/impractical/SKILL.md +25 -2
- package/dist/chunk-G56RK3HD.js +617 -0
- package/dist/index.js +46 -6
- package/dist/{stdio-5F5UX4VN.js → stdio-Y5CK7GJJ.js} +529 -458
- package/package.json +12 -3
- package/dist/chunk-2RJVXP4V.js +0 -325
package/package.json
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@impractical-ai/motion",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Create and refine Impractical Motion videos from coding agents and the terminal.",
|
|
6
|
+
"homepage": "https://github.com/Openpod/impractical-motion#readme",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Openpod/impractical-motion.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Openpod/impractical-motion/issues"
|
|
13
|
+
},
|
|
6
14
|
"license": "UNLICENSED",
|
|
7
15
|
"type": "module",
|
|
8
16
|
"files": [
|
|
9
17
|
"dist",
|
|
10
|
-
"assets"
|
|
18
|
+
"assets",
|
|
19
|
+
"README.md"
|
|
11
20
|
],
|
|
12
21
|
"bin": {
|
|
13
22
|
"impractical": "dist/index.js"
|
|
@@ -18,7 +27,7 @@
|
|
|
18
27
|
"scripts": {
|
|
19
28
|
"build": "node ../scripts/build-engine-dts.mjs && rm -rf dist && esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --splitting --outdir=dist --external:playwright-core --banner:js=\"#!/usr/bin/env node\" && chmod +x dist/index.js",
|
|
20
29
|
"typecheck": "tsc --noEmit",
|
|
21
|
-
"test": "npm run build && esbuild tests/onboarding.test.ts tests/mcp-tools.test.ts tests/mcp-frames.test.ts tests/engine-dts.test.ts tests/mcp-connect.test.ts --bundle --platform=node --format=esm --packages=external --outdir=dist --out-extension:.js=.mjs && node --test dist/onboarding.test.mjs dist/mcp-tools.test.mjs dist/mcp-frames.test.mjs dist/engine-dts.test.mjs dist/mcp-connect.test.mjs",
|
|
30
|
+
"test": "npm run build && esbuild tests/onboarding.test.ts tests/mcp-tools.test.ts tests/mcp-frames.test.ts tests/engine-dts.test.ts tests/mcp-connect.test.ts tests/mcp-longpoll.test.ts --bundle --platform=node --format=esm --packages=external --outdir=dist --out-extension:.js=.mjs && node --test dist/onboarding.test.mjs dist/mcp-tools.test.mjs dist/mcp-frames.test.mjs dist/engine-dts.test.mjs dist/mcp-connect.test.mjs dist/mcp-longpoll.test.mjs",
|
|
22
31
|
"prepack": "npm run build"
|
|
23
32
|
},
|
|
24
33
|
"publishConfig": {
|
package/dist/chunk-2RJVXP4V.js
DELETED
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
-
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
-
};
|
|
11
|
-
var __export = (target, all) => {
|
|
12
|
-
for (var name in all)
|
|
13
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
-
};
|
|
15
|
-
var __copyProps = (to, from, except, desc) => {
|
|
16
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
-
for (let key of __getOwnPropNames(from))
|
|
18
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
-
}
|
|
21
|
-
return to;
|
|
22
|
-
};
|
|
23
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
-
mod
|
|
30
|
-
));
|
|
31
|
-
|
|
32
|
-
// src/config.ts
|
|
33
|
-
import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
34
|
-
import { homedir } from "node:os";
|
|
35
|
-
import { join } from "node:path";
|
|
36
|
-
|
|
37
|
-
// src/constants.ts
|
|
38
|
-
var PRODUCTION_APP_URL = "https://motion.impractical.ai";
|
|
39
|
-
var PRODUCTION_SUPABASE_URL = "https://uaxjweednprtbhbceify.supabase.co";
|
|
40
|
-
var PACKAGE_NAME = "@impractical-ai/motion";
|
|
41
|
-
var PACKAGE_COMMAND = `npx --yes ${PACKAGE_NAME}`;
|
|
42
|
-
|
|
43
|
-
// src/config.ts
|
|
44
|
-
var configDir = () => join(homedir(), ".impractical");
|
|
45
|
-
var configPath = () => join(configDir(), "config.json");
|
|
46
|
-
function loadConfig() {
|
|
47
|
-
let stored = {};
|
|
48
|
-
try {
|
|
49
|
-
stored = JSON.parse(readFileSync(configPath(), "utf8"));
|
|
50
|
-
} catch {
|
|
51
|
-
}
|
|
52
|
-
return {
|
|
53
|
-
apiKey: process.env.IMPRACTICAL_API_KEY ?? stored.apiKey,
|
|
54
|
-
supabaseUrl: process.env.IMPRACTICAL_SUPABASE_URL ?? stored.supabaseUrl ?? PRODUCTION_SUPABASE_URL,
|
|
55
|
-
appUrl: process.env.IMPRACTICAL_APP_URL ?? stored.appUrl ?? PRODUCTION_APP_URL
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
function saveConfig(patch) {
|
|
59
|
-
let stored = {};
|
|
60
|
-
try {
|
|
61
|
-
stored = JSON.parse(readFileSync(configPath(), "utf8"));
|
|
62
|
-
} catch {
|
|
63
|
-
}
|
|
64
|
-
const next = { ...stored, ...patch };
|
|
65
|
-
mkdirSync(configDir(), { recursive: true });
|
|
66
|
-
writeFileSync(configPath(), JSON.stringify(next, null, 2) + "\n");
|
|
67
|
-
chmodSync(configPath(), 384);
|
|
68
|
-
return next;
|
|
69
|
-
}
|
|
70
|
-
function requireConfig() {
|
|
71
|
-
const config = loadConfig();
|
|
72
|
-
if (!config.supabaseUrl) {
|
|
73
|
-
throw new Error("Not configured. Run: npx --yes @impractical-ai/motion setup --claude");
|
|
74
|
-
}
|
|
75
|
-
if (!config.apiKey) {
|
|
76
|
-
throw new Error("Not connected. Run: npx --yes @impractical-ai/motion setup --claude");
|
|
77
|
-
}
|
|
78
|
-
return config;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// src/api.ts
|
|
82
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
83
|
-
import { randomUUID } from "node:crypto";
|
|
84
|
-
var admissionContext = new AsyncLocalStorage();
|
|
85
|
-
var ApiError = class extends Error {
|
|
86
|
-
constructor(status, message, fn) {
|
|
87
|
-
super(message);
|
|
88
|
-
this.status = status;
|
|
89
|
-
this.fn = fn;
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
async function invoke(name, body = {}) {
|
|
93
|
-
const { apiKey, supabaseUrl } = requireConfig();
|
|
94
|
-
const receipt = admissionContext.getStore();
|
|
95
|
-
return invokeAt(supabaseUrl, name, body, apiKey, receipt);
|
|
96
|
-
}
|
|
97
|
-
async function invokeAt(supabaseUrl, name, body = {}, apiKey, admissionReceipt) {
|
|
98
|
-
const response = await fetch(`${supabaseUrl.replace(/\/$/, "")}/functions/v1/${name}`, {
|
|
99
|
-
method: "POST",
|
|
100
|
-
headers: {
|
|
101
|
-
...apiKey ? { authorization: `Bearer ${apiKey}` } : {},
|
|
102
|
-
...admissionReceipt ? { "x-impractical-admission": admissionReceipt } : {},
|
|
103
|
-
"content-type": "application/json"
|
|
104
|
-
},
|
|
105
|
-
body: JSON.stringify(body)
|
|
106
|
-
});
|
|
107
|
-
const text = await response.text();
|
|
108
|
-
let data = null;
|
|
109
|
-
try {
|
|
110
|
-
data = text ? JSON.parse(text) : null;
|
|
111
|
-
} catch {
|
|
112
|
-
}
|
|
113
|
-
if (!response.ok) {
|
|
114
|
-
const message = data?.error ?? (text || `${name} failed (${response.status})`);
|
|
115
|
-
throw new ApiError(response.status, message, name);
|
|
116
|
-
}
|
|
117
|
-
return data;
|
|
118
|
-
}
|
|
119
|
-
async function admitMcpCall(toolName, callId = randomUUID()) {
|
|
120
|
-
const { apiKey, supabaseUrl } = requireConfig();
|
|
121
|
-
return invokeAt(supabaseUrl, "mcp-admission", { callId, toolName }, apiKey);
|
|
122
|
-
}
|
|
123
|
-
function withAdmissionReceipt(receipt, task) {
|
|
124
|
-
return admissionContext.run(receipt, task);
|
|
125
|
-
}
|
|
126
|
-
async function withMeteredCall(toolName, task) {
|
|
127
|
-
const admission = await admitMcpCall(toolName);
|
|
128
|
-
return withAdmissionReceipt(admission.receipt, task);
|
|
129
|
-
}
|
|
130
|
-
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
131
|
-
async function jobStatus(jobIds) {
|
|
132
|
-
const result = await invoke("job-status", { jobIds });
|
|
133
|
-
return result.jobs;
|
|
134
|
-
}
|
|
135
|
-
async function waitForJobs(jobIds, onTick, deadlineMs = 72e5) {
|
|
136
|
-
const deadline = Date.now() + deadlineMs;
|
|
137
|
-
const terminal = /* @__PURE__ */ new Set();
|
|
138
|
-
let latest = [];
|
|
139
|
-
while (terminal.size < jobIds.length) {
|
|
140
|
-
if (Date.now() >= deadline) throw new Error("Timed out waiting for jobs");
|
|
141
|
-
latest = await jobStatus(jobIds);
|
|
142
|
-
for (const row of latest) {
|
|
143
|
-
if (row.status === "completed" || row.status === "failed" || row.status === "canceled") {
|
|
144
|
-
terminal.add(row.id);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
onTick(latest);
|
|
148
|
-
if (terminal.size < jobIds.length) await delay(2e3);
|
|
149
|
-
}
|
|
150
|
-
return latest;
|
|
151
|
-
}
|
|
152
|
-
function studioLink(appUrl, projectId, slug) {
|
|
153
|
-
const base = (appUrl ?? "http://localhost:5173").replace(/\/$/, "");
|
|
154
|
-
return `${base}/projects/${projectId}/studio${slug ? `?scene=${slug}` : ""}`;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// src/onboarding.ts
|
|
158
|
-
import { chmodSync as chmodSync2, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
159
|
-
import { homedir as homedir2 } from "node:os";
|
|
160
|
-
import { dirname, join as join2 } from "node:path";
|
|
161
|
-
import { fileURLToPath } from "node:url";
|
|
162
|
-
import { spawnSync } from "node:child_process";
|
|
163
|
-
var CLAUDE_MARKETPLACE = "Openpod/impractical-agent-plugins";
|
|
164
|
-
var CLAUDE_PLUGIN = "impractical-motion@impractical";
|
|
165
|
-
function commandResult(command, args) {
|
|
166
|
-
return spawnSync(command, args, { encoding: "utf8", windowsHide: true });
|
|
167
|
-
}
|
|
168
|
-
function requireCommand(command, label) {
|
|
169
|
-
const result = commandResult(command, ["--version"]);
|
|
170
|
-
if (result.error || result.status !== 0) {
|
|
171
|
-
throw new Error(`${label} is required before setup can continue.`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
function hasChrome() {
|
|
175
|
-
const macPaths = [
|
|
176
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
177
|
-
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
178
|
-
];
|
|
179
|
-
if (macPaths.some(existsSync)) return true;
|
|
180
|
-
return ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"].some((command) => {
|
|
181
|
-
const result = spawnSync(command, ["--version"], { stdio: "ignore", windowsHide: true });
|
|
182
|
-
return !result.error && result.status === 0;
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
function verifyClaudePrerequisites() {
|
|
186
|
-
const major = Number(process.versions.node.split(".")[0]);
|
|
187
|
-
if (!Number.isInteger(major) || major < 22) throw new Error("Node.js 22 or newer is required.");
|
|
188
|
-
requireCommand("claude", "Claude Code");
|
|
189
|
-
if (!hasChrome()) throw new Error("Google Chrome or Chromium is required to inspect video frames.");
|
|
190
|
-
}
|
|
191
|
-
function runClaude(args, tolerateAlreadyInstalled = false) {
|
|
192
|
-
const result = commandResult("claude", args);
|
|
193
|
-
if (!result.error && result.status === 0) return;
|
|
194
|
-
const detail = `${result.stdout ?? ""}
|
|
195
|
-
${result.stderr ?? ""}`.trim();
|
|
196
|
-
if (tolerateAlreadyInstalled && /already (?:added|exists|installed|configured)/i.test(detail)) return;
|
|
197
|
-
throw new Error(`Claude plugin setup failed: ${detail || result.error?.message || "unknown error"}`);
|
|
198
|
-
}
|
|
199
|
-
function installClaudePlugin() {
|
|
200
|
-
runClaude(["plugin", "marketplace", "add", CLAUDE_MARKETPLACE], true);
|
|
201
|
-
runClaude(["plugin", "install", CLAUDE_PLUGIN, "--scope", "user"], true);
|
|
202
|
-
runClaude(["plugin", "marketplace", "update", "impractical"], true);
|
|
203
|
-
runClaude(["plugin", "update", CLAUDE_PLUGIN], true);
|
|
204
|
-
}
|
|
205
|
-
var skillSourcePath = () => fileURLToPath(new URL("../assets/impractical/SKILL.md", import.meta.url));
|
|
206
|
-
function installSkills(home = homedir2(), sourcePath = skillSourcePath()) {
|
|
207
|
-
const source = readFileSync2(sourcePath, "utf8");
|
|
208
|
-
const targets = [
|
|
209
|
-
{
|
|
210
|
-
harness: "Claude Code",
|
|
211
|
-
path: join2(home, ".claude", "skills", "impractical", "SKILL.md")
|
|
212
|
-
},
|
|
213
|
-
{
|
|
214
|
-
harness: "Codex",
|
|
215
|
-
path: join2(home, ".codex", "skills", "impractical", "SKILL.md")
|
|
216
|
-
}
|
|
217
|
-
];
|
|
218
|
-
for (const target of targets) {
|
|
219
|
-
mkdirSync2(dirname(target.path), { recursive: true, mode: 448 });
|
|
220
|
-
writeFileSync2(target.path, source, { mode: 420 });
|
|
221
|
-
}
|
|
222
|
-
return targets;
|
|
223
|
-
}
|
|
224
|
-
function browserCommand(url) {
|
|
225
|
-
if (process.platform === "darwin") return { command: "open", args: [url] };
|
|
226
|
-
if (process.platform === "win32") return { command: "cmd", args: ["/c", "start", "", url] };
|
|
227
|
-
return { command: "xdg-open", args: [url] };
|
|
228
|
-
}
|
|
229
|
-
function openBrowser(url) {
|
|
230
|
-
const launch = browserCommand(url);
|
|
231
|
-
const result = spawnSync(launch.command, launch.args, {
|
|
232
|
-
stdio: "ignore",
|
|
233
|
-
windowsHide: true
|
|
234
|
-
});
|
|
235
|
-
return !result.error && result.status === 0;
|
|
236
|
-
}
|
|
237
|
-
async function waitForApproval(supabaseUrl, authorization, log) {
|
|
238
|
-
const deadline = Date.parse(authorization.expiresAt);
|
|
239
|
-
const intervalMs = Math.max(1, authorization.intervalSeconds) * 1e3;
|
|
240
|
-
let announced = false;
|
|
241
|
-
while (Date.now() < deadline) {
|
|
242
|
-
const result = await invokeAt(supabaseUrl, "cli-device-auth", {
|
|
243
|
-
action: "poll",
|
|
244
|
-
deviceCode: authorization.deviceCode
|
|
245
|
-
});
|
|
246
|
-
if (result.status === "authorized") return result.apiKey;
|
|
247
|
-
if (!announced) {
|
|
248
|
-
log("Waiting for browser approval\u2026");
|
|
249
|
-
announced = true;
|
|
250
|
-
}
|
|
251
|
-
await delay(intervalMs);
|
|
252
|
-
}
|
|
253
|
-
throw new Error(`Authorization expired. Run \`${PACKAGE_COMMAND} setup --claude\` again.`);
|
|
254
|
-
}
|
|
255
|
-
async function setup(options) {
|
|
256
|
-
if (options.installClaudePlugin) verifyClaudePrerequisites();
|
|
257
|
-
const supabaseUrl = process.env.IMPRACTICAL_SUPABASE_URL?.trim() || PRODUCTION_SUPABASE_URL;
|
|
258
|
-
const appUrl = process.env.IMPRACTICAL_APP_URL?.trim() || PRODUCTION_APP_URL;
|
|
259
|
-
const authorization = await invokeAt(supabaseUrl, "cli-device-auth", {
|
|
260
|
-
action: "start"
|
|
261
|
-
});
|
|
262
|
-
options.log("");
|
|
263
|
-
options.log("Connect Impractical Motion");
|
|
264
|
-
options.log(`Code: ${authorization.userCode}`);
|
|
265
|
-
options.log(`Open: ${authorization.verificationUri}`);
|
|
266
|
-
options.log("");
|
|
267
|
-
if (options.openBrowser) {
|
|
268
|
-
const opened = openBrowser(authorization.verificationUri);
|
|
269
|
-
options.log(opened ? "Opened your browser." : "Could not open a browser automatically; use the link above.");
|
|
270
|
-
}
|
|
271
|
-
let apiKey;
|
|
272
|
-
try {
|
|
273
|
-
apiKey = await waitForApproval(supabaseUrl, authorization, options.log);
|
|
274
|
-
} catch (error) {
|
|
275
|
-
if (error instanceof ApiError && error.status === 410) {
|
|
276
|
-
throw new Error(`Authorization expired. Run \`${PACKAGE_COMMAND} setup --claude\` again.`);
|
|
277
|
-
}
|
|
278
|
-
throw error;
|
|
279
|
-
}
|
|
280
|
-
saveConfig({ apiKey, supabaseUrl, appUrl });
|
|
281
|
-
const configPath2 = join2(homedir2(), ".impractical", "config.json");
|
|
282
|
-
chmodSync2(configPath2, 384);
|
|
283
|
-
const verified = await invokeAt(
|
|
284
|
-
supabaseUrl,
|
|
285
|
-
"workspace-bootstrap",
|
|
286
|
-
{},
|
|
287
|
-
apiKey
|
|
288
|
-
);
|
|
289
|
-
if (options.installClaudePlugin) installClaudePlugin();
|
|
290
|
-
const installed = options.installAgentSkills ? installSkills() : [];
|
|
291
|
-
options.log("");
|
|
292
|
-
options.log("Connected.");
|
|
293
|
-
for (const skill of installed) {
|
|
294
|
-
options.log(`Installed /impractical for ${skill.harness}.`);
|
|
295
|
-
}
|
|
296
|
-
if (options.installClaudePlugin) {
|
|
297
|
-
options.log(`Installed ${CLAUDE_PLUGIN} for Claude Code (user scope).`);
|
|
298
|
-
}
|
|
299
|
-
options.log(`Workspace: ${verified.orgId}${verified.plan ? ` \xB7 ${verified.plan}` : ""}`);
|
|
300
|
-
options.log("");
|
|
301
|
-
options.log("Run /reload-plugins, then type continue.");
|
|
302
|
-
return { installed, appUrl, supabaseUrl };
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
export {
|
|
306
|
-
__commonJS,
|
|
307
|
-
__export,
|
|
308
|
-
__toESM,
|
|
309
|
-
PRODUCTION_APP_URL,
|
|
310
|
-
PRODUCTION_SUPABASE_URL,
|
|
311
|
-
loadConfig,
|
|
312
|
-
saveConfig,
|
|
313
|
-
requireConfig,
|
|
314
|
-
ApiError,
|
|
315
|
-
invoke,
|
|
316
|
-
invokeAt,
|
|
317
|
-
admitMcpCall,
|
|
318
|
-
withAdmissionReceipt,
|
|
319
|
-
withMeteredCall,
|
|
320
|
-
jobStatus,
|
|
321
|
-
waitForJobs,
|
|
322
|
-
studioLink,
|
|
323
|
-
openBrowser,
|
|
324
|
-
setup
|
|
325
|
-
};
|