@impractical-ai/motion 0.1.4 → 0.1.6
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 +18 -6
- package/assets/engine.d.ts +38 -2
- package/assets/impractical/SKILL.md +7 -5
- package/dist/{api-64X3G4M2.js → api-C7KJDYN4.js} +4 -2
- package/dist/{chunk-KC6BJJXH.js → chunk-BUHNO2W2.js} +335 -8
- package/dist/{chunk-M77TS5RO.js → chunk-KITRCE33.js} +68 -17
- package/dist/{chunk-EFP4P3WA.js → chunk-LJ4YH3YJ.js} +4 -3
- package/dist/{commands-WYBUEYA6.js → commands-P5CDHPDP.js} +33 -10
- package/dist/index.js +8 -8
- package/dist/{stdio-JCRFEHDW.js → stdio-GHDI4S7P.js} +206 -41
- package/package.json +4 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
openBrowser
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-BUHNO2W2.js";
|
|
5
5
|
import {
|
|
6
6
|
ApiError,
|
|
7
7
|
admitMcpCall,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
saveConfig,
|
|
12
12
|
validateApiResponse,
|
|
13
13
|
withAdmissionReceipt
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-KITRCE33.js";
|
|
15
15
|
import {
|
|
16
16
|
PACKAGE_VERSION,
|
|
17
17
|
PRODUCTION_APP_URL,
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
__commonJS,
|
|
20
20
|
__export,
|
|
21
21
|
__toESM
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-LJ4YH3YJ.js";
|
|
23
23
|
|
|
24
24
|
// node_modules/ajv/dist/compile/codegen/code.js
|
|
25
25
|
var require_code = __commonJS({
|
|
@@ -30993,6 +30993,23 @@ var StdioServerTransport = class {
|
|
|
30993
30993
|
}
|
|
30994
30994
|
};
|
|
30995
30995
|
|
|
30996
|
+
// ../supabase/functions/_shared/usage-policy.ts
|
|
30997
|
+
var FREE_WEEKLY_LIMIT = 200;
|
|
30998
|
+
var FREE_WEEKLY_VIDEO_ESTIMATE = "usually 4-5 videos";
|
|
30999
|
+
var ADMISSION_RECEIPT_TTL_SECONDS = 3 * 60 * 60;
|
|
31000
|
+
var UNMETERED_MCP_TOOLS = /* @__PURE__ */ new Set([
|
|
31001
|
+
"connect",
|
|
31002
|
+
"account_status",
|
|
31003
|
+
"whoami",
|
|
31004
|
+
"upgrade",
|
|
31005
|
+
"studio_link",
|
|
31006
|
+
// This tool has a packaged local fallback and is explicitly required before
|
|
31007
|
+
// authentication-dependent work. Admission must not block that fallback.
|
|
31008
|
+
"engine_types"
|
|
31009
|
+
]);
|
|
31010
|
+
var encoder = new TextEncoder();
|
|
31011
|
+
var decoder = new TextDecoder();
|
|
31012
|
+
|
|
30996
31013
|
// src/mcp/tools.ts
|
|
30997
31014
|
var json2 = (value) => ({ text: JSON.stringify(value, null, 2) });
|
|
30998
31015
|
var fail = (text) => ({ text, isError: true });
|
|
@@ -31111,24 +31128,74 @@ var whoami = {
|
|
|
31111
31128
|
};
|
|
31112
31129
|
var upgradeTool = {
|
|
31113
31130
|
name: "upgrade",
|
|
31114
|
-
title: "
|
|
31115
|
-
description: "
|
|
31131
|
+
title: "Explain Pro and get the billing link",
|
|
31132
|
+
description: "Explain what Pro costs and unlocks for this workspace, and return the authenticated billing URL. Only a workspace admin can buy \u2014 checking out subscribes the whole workspace. Set open=true only after the user explicitly asks to open it in their browser.",
|
|
31116
31133
|
inputSchema: {
|
|
31117
31134
|
open: external_exports.boolean().optional().describe("Open billing in the user's browser. Defaults to false.")
|
|
31118
31135
|
},
|
|
31119
31136
|
local: true,
|
|
31120
31137
|
handler: async (args, ctx) => {
|
|
31121
31138
|
const url2 = `${(ctx.appUrl ?? "https://motion.impractical.ai").replace(/\/$/, "")}/account/billing`;
|
|
31139
|
+
const account = await ctx.invoke("workspace-bootstrap").catch(() => null);
|
|
31140
|
+
const role = account?.role === "admin" ? "admin" : "member";
|
|
31141
|
+
const canPurchase = role === "admin";
|
|
31142
|
+
const plan = account?.plan ?? "free";
|
|
31143
|
+
const seats = Math.max(1, account?.billing?.seatCount ?? 1);
|
|
31144
|
+
const limit = account?.quota?.limit ?? FREE_WEEKLY_LIMIT;
|
|
31145
|
+
const remaining = account?.quota?.remaining ?? 0;
|
|
31122
31146
|
const opened = args.open === true ? ctx.openExternal?.(url2) ?? false : false;
|
|
31123
|
-
return {
|
|
31147
|
+
return json2({
|
|
31148
|
+
plan,
|
|
31149
|
+
role,
|
|
31150
|
+
canPurchase,
|
|
31151
|
+
price: { amountUsd: 50, per: "user / month" },
|
|
31152
|
+
billedFor: `${seats} seat${seats === 1 ? "" : "s"} \u2014 every admin and member is a paid seat`,
|
|
31153
|
+
unlocks: [
|
|
31154
|
+
"Unlimited videos and exports \u2014 no weekly ceiling",
|
|
31155
|
+
"Iterate on one video as many times as it takes",
|
|
31156
|
+
"A dedicated allowance per person, not shared across workspaces"
|
|
31157
|
+
],
|
|
31158
|
+
// Exact, because we meter calls. How many videos that is depends on how
|
|
31159
|
+
// hard they iterate, so do not turn it into a per-user video count.
|
|
31160
|
+
freePlanNow: limit >= 1e6 ? "This workspace is already on Pro." : `${remaining} of ${limit} free calls left this week (${FREE_WEEKLY_VIDEO_ESTIMATE}).`,
|
|
31161
|
+
url: url2,
|
|
31162
|
+
opened,
|
|
31163
|
+
instructions: "Summarise the value in one or two sentences, then give the URL. Do not open a browser unless asked. " + (canPurchase ? "Checking out subscribes the whole workspace \u2014 say so before they act on it." : "Only a workspace admin can approve the purchase, so tell this user to ask an admin to upgrade.")
|
|
31164
|
+
});
|
|
31124
31165
|
}
|
|
31125
31166
|
};
|
|
31126
31167
|
var listProjects = {
|
|
31127
31168
|
name: "list_projects",
|
|
31128
31169
|
title: "List video projects",
|
|
31129
|
-
description: "List
|
|
31130
|
-
inputSchema: {
|
|
31131
|
-
|
|
31170
|
+
description: "List video projects in the connected workspace, newest first. Large workspaces: pass limit/offset to page instead of relying on a truncated dump, and nameFilter to search.",
|
|
31171
|
+
inputSchema: {
|
|
31172
|
+
limit: external_exports.number().int().min(1).max(200).optional().describe("Return at most this many projects (default: all)."),
|
|
31173
|
+
offset: external_exports.number().int().min(0).optional().describe("Skip this many projects, for paging."),
|
|
31174
|
+
nameFilter: external_exports.string().optional().describe("Case-insensitive substring match on the project name/slug.")
|
|
31175
|
+
},
|
|
31176
|
+
handler: async (args, ctx) => {
|
|
31177
|
+
const result = await ctx.invoke("list-projects", {});
|
|
31178
|
+
let projects = result.projects ?? [];
|
|
31179
|
+
const total = projects.length;
|
|
31180
|
+
const filter = typeof args.nameFilter === "string" ? args.nameFilter.trim().toLowerCase() : "";
|
|
31181
|
+
if (filter) {
|
|
31182
|
+
projects = projects.filter(
|
|
31183
|
+
(project) => `${project.name ?? ""} ${project.slug ?? ""}`.toLowerCase().includes(filter)
|
|
31184
|
+
);
|
|
31185
|
+
}
|
|
31186
|
+
const matched = projects.length;
|
|
31187
|
+
const offset = typeof args.offset === "number" ? args.offset : 0;
|
|
31188
|
+
const limit = typeof args.limit === "number" ? args.limit : projects.length;
|
|
31189
|
+
projects = projects.slice(offset, offset + limit);
|
|
31190
|
+
return json2({
|
|
31191
|
+
total,
|
|
31192
|
+
matched,
|
|
31193
|
+
offset,
|
|
31194
|
+
returned: projects.length,
|
|
31195
|
+
hasMore: offset + projects.length < matched,
|
|
31196
|
+
projects
|
|
31197
|
+
});
|
|
31198
|
+
}
|
|
31132
31199
|
};
|
|
31133
31200
|
var getProject = {
|
|
31134
31201
|
name: "get_project",
|
|
@@ -31194,7 +31261,7 @@ var createVideo = {
|
|
|
31194
31261
|
var listDraftFiles = {
|
|
31195
31262
|
name: "list_draft_files",
|
|
31196
31263
|
title: "List a draft's files",
|
|
31197
|
-
description: "List the source files of a draft with their sizes and the current revision. Cheap orientation \u2014 prefer this over reading every file.",
|
|
31264
|
+
description: "List the source files of a draft with their sizes and the current revision. Cheap orientation \u2014 prefer this over reading every file. If notes.json is present, it holds the user's queued timecoded edit requests \u2014 read it before directing.",
|
|
31198
31265
|
inputSchema: { draftId: external_exports.string().describe("The draft id.") },
|
|
31199
31266
|
handler: async (args, ctx) => {
|
|
31200
31267
|
const draft = await getDraft(ctx, String(args.draftId));
|
|
@@ -31212,10 +31279,12 @@ var listDraftFiles = {
|
|
|
31212
31279
|
var readDraftFile = {
|
|
31213
31280
|
name: "read_draft_file",
|
|
31214
31281
|
title: "Read one draft file",
|
|
31215
|
-
description: "Read the contents of a single file in a draft.",
|
|
31282
|
+
description: "Read the contents of a single file in a draft. notes.json, when present, is the user's edit queue: [{ id, at, text, done, mentions? }] \u2014 timecoded requests written while watching. Address open items and set done: true on each one you handle.",
|
|
31216
31283
|
inputSchema: {
|
|
31217
31284
|
draftId: external_exports.string().describe("The draft id."),
|
|
31218
|
-
path: external_exports.string().describe("File path within the draft, e.g. 'direction.ts'.")
|
|
31285
|
+
path: external_exports.string().describe("File path within the draft, e.g. 'direction.ts'."),
|
|
31286
|
+
startLine: external_exports.number().int().min(1).optional().describe("1-based first line to return \u2014 page large files instead of truncating."),
|
|
31287
|
+
lineCount: external_exports.number().int().min(1).optional().describe("Number of lines to return from startLine.")
|
|
31219
31288
|
},
|
|
31220
31289
|
handler: async (args, ctx) => {
|
|
31221
31290
|
const draft = await getDraft(ctx, String(args.draftId));
|
|
@@ -31226,7 +31295,20 @@ var readDraftFile = {
|
|
|
31226
31295
|
`No file "${path}" in draft ${args.draftId}. Available: ${Object.keys(draft.files).sort().join(", ")}`
|
|
31227
31296
|
);
|
|
31228
31297
|
}
|
|
31229
|
-
|
|
31298
|
+
const lines = content.split("\n");
|
|
31299
|
+
const startLine = typeof args.startLine === "number" ? Math.max(1, args.startLine) : 1;
|
|
31300
|
+
const lineCount = typeof args.lineCount === "number" ? Math.max(1, args.lineCount) : lines.length;
|
|
31301
|
+
if (startLine === 1 && lineCount >= lines.length) return { text: content };
|
|
31302
|
+
const slice = lines.slice(startLine - 1, startLine - 1 + lineCount);
|
|
31303
|
+
if (slice.length === 0) {
|
|
31304
|
+
return {
|
|
31305
|
+
text: `[no lines returned: startLine ${startLine} is beyond ${lines.length} lines \u2014 page with startLine/lineCount]`
|
|
31306
|
+
};
|
|
31307
|
+
}
|
|
31308
|
+
return {
|
|
31309
|
+
text: `[lines ${startLine}-${startLine - 1 + slice.length} of ${lines.length} \u2014 page with startLine/lineCount]
|
|
31310
|
+
` + slice.join("\n")
|
|
31311
|
+
};
|
|
31230
31312
|
}
|
|
31231
31313
|
};
|
|
31232
31314
|
var writeDraftFiles = {
|
|
@@ -31298,12 +31380,13 @@ var jobStatus = {
|
|
|
31298
31380
|
const jobIds = args.jobIds;
|
|
31299
31381
|
if (!args.wait) return json2(await ctx.invoke("job-status", { jobIds }));
|
|
31300
31382
|
const budgetMs = waitBudgetMs(args.waitSeconds, JOB_WAIT_SECONDS);
|
|
31301
|
-
const
|
|
31383
|
+
const now = ctx.now ?? Date.now;
|
|
31384
|
+
const started = now();
|
|
31302
31385
|
const deadline = started + budgetMs;
|
|
31303
31386
|
let rows = [];
|
|
31304
31387
|
let consecutiveErrors = 0;
|
|
31305
31388
|
let lastError = "";
|
|
31306
|
-
while (
|
|
31389
|
+
while (now() < deadline) {
|
|
31307
31390
|
if (progress?.signal?.aborted) break;
|
|
31308
31391
|
try {
|
|
31309
31392
|
const result = await ctx.invoke("job-status", { jobIds });
|
|
@@ -31317,11 +31400,12 @@ var jobStatus = {
|
|
|
31317
31400
|
}
|
|
31318
31401
|
}
|
|
31319
31402
|
progress?.report?.({
|
|
31320
|
-
progress: Math.min(
|
|
31403
|
+
progress: Math.min(now() - started, budgetMs),
|
|
31321
31404
|
total: budgetMs,
|
|
31322
31405
|
message: rows.length ? rows.map((row) => describeJob(row.id, row, consecutiveErrors, lastError)).join("; ") : jobIds.join(", ")
|
|
31323
31406
|
});
|
|
31324
|
-
|
|
31407
|
+
const remainingMs = deadline - now();
|
|
31408
|
+
if (remainingMs > 0) await nap(ctx, Math.min(2e3, remainingMs));
|
|
31325
31409
|
}
|
|
31326
31410
|
return json2({
|
|
31327
31411
|
timedOut: true,
|
|
@@ -31503,15 +31587,25 @@ var PREVIEW_URL = `${PREVIEW_ORIGIN}/stage.html`;
|
|
|
31503
31587
|
var FRAME_TYPE = "jpeg";
|
|
31504
31588
|
var FRAME_QUALITY = 80;
|
|
31505
31589
|
var FRAME_MIME = "image/jpeg";
|
|
31506
|
-
async function launchChromium() {
|
|
31590
|
+
async function launchChromium(launcher = chromium) {
|
|
31591
|
+
if (process.env.CI) {
|
|
31592
|
+
try {
|
|
31593
|
+
return await launcher.launch({ headless: true });
|
|
31594
|
+
} catch (error51) {
|
|
31595
|
+
throw new Error(
|
|
31596
|
+
"CI requires the Playwright-managed Chromium pinned by cli/package-lock.json. Run `npx playwright-core install chromium`; system Chrome fallback is disabled in CI.",
|
|
31597
|
+
{ cause: error51 }
|
|
31598
|
+
);
|
|
31599
|
+
}
|
|
31600
|
+
}
|
|
31507
31601
|
if (process.env.PLAYWRIGHT_CHANNEL === "chrome") {
|
|
31508
|
-
return
|
|
31602
|
+
return launcher.launch({ channel: "chrome", headless: true });
|
|
31509
31603
|
}
|
|
31510
31604
|
try {
|
|
31511
|
-
return await
|
|
31605
|
+
return await launcher.launch({ headless: true });
|
|
31512
31606
|
} catch {
|
|
31513
31607
|
try {
|
|
31514
|
-
return await
|
|
31608
|
+
return await launcher.launch({ channel: "chrome", headless: true });
|
|
31515
31609
|
} catch (error51) {
|
|
31516
31610
|
throw new Error(
|
|
31517
31611
|
`No usable Chromium found for frame capture. Install Google Chrome, or run \`npx playwright install chromium\`. If Chrome is installed but blocked by Gatekeeper, set PLAYWRIGHT_CHANNEL=chrome.
|
|
@@ -31842,21 +31936,6 @@ ${notes.join("\n")}`.trim(),
|
|
|
31842
31936
|
}
|
|
31843
31937
|
};
|
|
31844
31938
|
|
|
31845
|
-
// ../supabase/functions/_shared/usage-policy.ts
|
|
31846
|
-
var ADMISSION_RECEIPT_TTL_SECONDS = 3 * 60 * 60;
|
|
31847
|
-
var UNMETERED_MCP_TOOLS = /* @__PURE__ */ new Set([
|
|
31848
|
-
"connect",
|
|
31849
|
-
"account_status",
|
|
31850
|
-
"whoami",
|
|
31851
|
-
"upgrade",
|
|
31852
|
-
"studio_link",
|
|
31853
|
-
// This tool has a packaged local fallback and is explicitly required before
|
|
31854
|
-
// authentication-dependent work. Admission must not block that fallback.
|
|
31855
|
-
"engine_types"
|
|
31856
|
-
]);
|
|
31857
|
-
var encoder = new TextEncoder();
|
|
31858
|
-
var decoder = new TextDecoder();
|
|
31859
|
-
|
|
31860
31939
|
// src/mcp/stdio.ts
|
|
31861
31940
|
var engineTypesTool = {
|
|
31862
31941
|
name: "engine_types",
|
|
@@ -31910,6 +31989,82 @@ function toolProgress(extra) {
|
|
|
31910
31989
|
}
|
|
31911
31990
|
};
|
|
31912
31991
|
}
|
|
31992
|
+
function formatUsageNotice(notice) {
|
|
31993
|
+
if (typeof notice === "string") return `Quota warning: ${notice}`;
|
|
31994
|
+
const value = notice;
|
|
31995
|
+
if (!value?.message) return "";
|
|
31996
|
+
return value.level === "nearly_spent" || value.level === "spent" ? `${value.message} Mention \`upgrade\` only if it is relevant to what they are doing right now.` : `${value.message}`;
|
|
31997
|
+
}
|
|
31998
|
+
function sanitizeResetTime(value) {
|
|
31999
|
+
if (typeof value !== "string" || value.length > 64) return null;
|
|
32000
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/.exec(value);
|
|
32001
|
+
if (!match) return null;
|
|
32002
|
+
const [, yearText, monthText, dayText, hourText, minuteText, secondText] = match;
|
|
32003
|
+
const [year, month, day, hour, minute, second] = [yearText, monthText, dayText, hourText, minuteText, secondText].map(Number);
|
|
32004
|
+
const daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
32005
|
+
if (month < 1 || month > 12 || day < 1 || day > daysInMonth || hour > 23 || minute > 59 || second > 59) return null;
|
|
32006
|
+
const timestamp = new Date(value);
|
|
32007
|
+
return Number.isNaN(timestamp.getTime()) ? null : timestamp.toISOString();
|
|
32008
|
+
}
|
|
32009
|
+
function sanitizeUpgradeUrl(value, appUrl) {
|
|
32010
|
+
if (typeof value !== "string" || typeof appUrl !== "string" || value.length > 2048 || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
32011
|
+
return null;
|
|
32012
|
+
}
|
|
32013
|
+
try {
|
|
32014
|
+
const url2 = new URL(value);
|
|
32015
|
+
const billingUrl = new URL("/account/billing", appUrl);
|
|
32016
|
+
if (url2.protocol !== "https:" || url2.origin !== billingUrl.origin || url2.pathname !== billingUrl.pathname || url2.username || url2.password || !url2.hostname) return null;
|
|
32017
|
+
url2.search = "";
|
|
32018
|
+
url2.hash = "";
|
|
32019
|
+
return url2.toString();
|
|
32020
|
+
} catch {
|
|
32021
|
+
return null;
|
|
32022
|
+
}
|
|
32023
|
+
}
|
|
32024
|
+
function formatWeeklyQuota(appUrl, details) {
|
|
32025
|
+
const resetTime = sanitizeResetTime(details.resetTime);
|
|
32026
|
+
const upgradeUrl = sanitizeUpgradeUrl(details.upgradeUrl, appUrl);
|
|
32027
|
+
const spent = details.spent;
|
|
32028
|
+
const spentCalls = Number.isSafeInteger(spent?.calls) && Number(spent?.calls) >= 0 ? Number(spent?.calls) : null;
|
|
32029
|
+
const options = [
|
|
32030
|
+
{
|
|
32031
|
+
id: "upgrade",
|
|
32032
|
+
label: "Review upgrade options",
|
|
32033
|
+
tool: "upgrade",
|
|
32034
|
+
...upgradeUrl ? { url: upgradeUrl } : {}
|
|
32035
|
+
},
|
|
32036
|
+
{
|
|
32037
|
+
id: "wait",
|
|
32038
|
+
label: "Wait for the weekly reset"
|
|
32039
|
+
}
|
|
32040
|
+
];
|
|
32041
|
+
const response = {
|
|
32042
|
+
error: "weekly_mcp_quota_exceeded",
|
|
32043
|
+
headline: "The free weekly MCP allowance has been used.",
|
|
32044
|
+
message: "No work was started for this call.",
|
|
32045
|
+
...spentCalls === null ? {} : { spent: { calls: spentCalls } },
|
|
32046
|
+
...resetTime ? { resetTime } : {},
|
|
32047
|
+
scopeNote: "Free calls are shared across every Free workspace you belong to.",
|
|
32048
|
+
options,
|
|
32049
|
+
...upgradeUrl ? { upgradeUrl } : {}
|
|
32050
|
+
};
|
|
32051
|
+
return JSON.stringify(response, null, 2);
|
|
32052
|
+
}
|
|
32053
|
+
function formatGenericRateLimit(toolName, appUrl, details) {
|
|
32054
|
+
const resetTime = sanitizeResetTime(details?.resetTime);
|
|
32055
|
+
const upgradeUrl = sanitizeUpgradeUrl(details?.upgradeUrl, appUrl);
|
|
32056
|
+
const metadata = [
|
|
32057
|
+
resetTime ? `Reset time: ${resetTime}` : null,
|
|
32058
|
+
upgradeUrl ? `Upgrade: ${upgradeUrl}` : null
|
|
32059
|
+
].filter((line) => line !== null);
|
|
32060
|
+
return [
|
|
32061
|
+
`${toolName} failed: Rate limit exceeded.`,
|
|
32062
|
+
"",
|
|
32063
|
+
"No work was started for this call because the service returned a rate limit.",
|
|
32064
|
+
...metadata,
|
|
32065
|
+
"Call `upgrade` for plan options, or retry after the limit resets. `account_status` remains available without using quota."
|
|
32066
|
+
].join("\n");
|
|
32067
|
+
}
|
|
31913
32068
|
function register(server, tool, ctx) {
|
|
31914
32069
|
server.registerTool(
|
|
31915
32070
|
tool.name,
|
|
@@ -31925,15 +32080,25 @@ function register(server, tool, ctx) {
|
|
|
31925
32080
|
const output = admission?.receipt && ctx.withAdmission ? await ctx.withAdmission(admission.receipt, execute) : await execute();
|
|
31926
32081
|
if (admission?.warning) output.text += `
|
|
31927
32082
|
|
|
31928
|
-
|
|
32083
|
+
${formatUsageNotice(admission.warning)}`;
|
|
31929
32084
|
return toContent(output);
|
|
31930
32085
|
} catch (error51) {
|
|
31931
32086
|
const status = error51.status;
|
|
31932
32087
|
const message = error51 instanceof Error ? error51.message : String(error51);
|
|
31933
32088
|
const details = error51 instanceof ApiError ? error51.details : void 0;
|
|
31934
|
-
|
|
31935
|
-
|
|
31936
|
-
|
|
32089
|
+
if (status === 429 && details?.error === "weekly_mcp_quota_exceeded") {
|
|
32090
|
+
return toContent({
|
|
32091
|
+
text: formatWeeklyQuota(ctx.appUrl, details),
|
|
32092
|
+
isError: true
|
|
32093
|
+
});
|
|
32094
|
+
}
|
|
32095
|
+
if (status === 429) {
|
|
32096
|
+
return toContent({
|
|
32097
|
+
text: formatGenericRateLimit(tool.name, ctx.appUrl, details),
|
|
32098
|
+
isError: true
|
|
32099
|
+
});
|
|
32100
|
+
}
|
|
32101
|
+
const hint = status === 401 || isNotConnected(message) ? "\n\nCall the `connect` tool to authenticate this workspace." : status === 402 ? "\n\nThe workspace needs an active subscription or trial." : "";
|
|
31937
32102
|
return toContent({ text: `${tool.name} failed: ${message}${hint}`, isError: true });
|
|
31938
32103
|
}
|
|
31939
32104
|
}
|
|
@@ -31971,7 +32136,7 @@ function createServer(ctx) {
|
|
|
31971
32136
|
// agent that skips motion_context still cannot produce slop by default.
|
|
31972
32137
|
// The full language (ease table, blur doctrine, camera grammar, beat
|
|
31973
32138
|
// sheets) ships live in motion_context's `guidance.motionDirection`.
|
|
31974
|
-
instructions: "Impractical Motion makes studio-quality motion-graphics videos. Draft source lives in the cloud \u2014 there is no working copy on disk, so edit through these tools rather than writing files.\n\nYou are the director. Call engine_types first to learn the API, then motion_context to study the current house library and production references, then create_video, then write App.tsx (the stage markup) and direction.ts (the GSAP choreography) with edit_draft_file / write_draft_files. Every save returns its committed revision and preview-build job id immediately. Follow that id with job_status and fix any build error it returns. Call get_frames to SEE the result before telling the user it is done, then call export_video, follow its returned job id with job_status, and hand the user the studio_link.\n\nJOBS OUTLIVE TOOL CALLS. Job-producing mutations return their durable job id BEFORE any wait; resume that exact id with job_status({ jobIds: [...], wait: true }). Never start a second export or re-apply an edit after a committed revision. If the MCP client kills a call and shows only a timeout, assume the acknowledgment may have been lost: DO NOT blindly retry a mutating or metered tool. Verify state first. For a write/edit, use list_draft_files and read_draft_file to confirm the intended source and revision. For an export or other project mutation, use get_project/list_projects to check whether it already started or completed. brand_extract returns a durable run handle; continue that exact jobId in status mode and never re-extract blindly. If its start acknowledgement is lost, check brand_list/brand_get before starting again. Retry only when those reads prove the mutation did not commit; otherwise continue from the observed state.\n\nTHE MOTION FLOOR \u2014 these hold even if you read nothing else:\n1. Beat out the story before writing code: every beat needs a time, a thing on screen, and a move. A beat with no move is not a beat.\n2. Text enters on RISE \u2014 text.reveal(el, 'rise') on promo-out, lines ~0.2s apart. Do NOT default type to 'blur': word-by-word de-blur reads as out-of-focus and a staggered blur turns to mush. Blur is for whole-frame moves \u2014 fx.spike(el, 'blur') alongside a whip or dive, where it hides the cut. Never animate blur above 20px.\n3. Every ease is a named house curve \u2014 ease-out-* for entrances, ease-in-out-* for travel and ALL camera moves, ease-in-* for exits and dives only. Never leave an ease unspecified, and never put an ease-in-* on an entrance.\n4. Motion is fast: entrances 0.4-0.6s, exits shorter, line staggers ~0.2s apart, nothing over 1s except a deliberate hold drift or slow push.\n5. Nothing is ever static \u2014 every hold carries a slow drift or camera push. Cuts are HARD (tl.set), never crossfades.\n6. Determinism: no Date.now, Math.random, setTimeout, or CSS transitions. The exporter renders by seeking, so wall-clock motion shimmers or vanishes.\n7. Never claim a video is done without calling get_frames and looking at the frames."
|
|
32139
|
+
instructions: "Impractical Motion makes studio-quality motion-graphics videos. Draft source lives in the cloud \u2014 there is no working copy on disk, so edit through these tools rather than writing files.\n\nTHE USER'S EDITS COME FIRST. While watching a draft in the Studio the user queues timecoded edit requests into its notes.json (\"Edits for agent\"). Before directing an existing draft, read_draft_file notes.json (it may not exist \u2014 that is fine); address the open items, and mark each one you have handled by setting its `done` to true with edit_draft_file, leaving the rest of the entry intact. The Studio shows the user what you resolved.\n\nYou are the director. Call engine_types first to learn the API, then motion_context to study the current house library and production references, then create_video, then write App.tsx (the stage markup) and direction.ts (the GSAP choreography) with edit_draft_file / write_draft_files. Every save returns its committed revision and preview-build job id immediately. Follow that id with job_status and fix any build error it returns. Call get_frames to SEE the result before telling the user it is done, then call export_video, follow its returned job id with job_status, and hand the user the studio_link.\n\nJOBS OUTLIVE TOOL CALLS. Job-producing mutations return their durable job id BEFORE any wait; resume that exact id with job_status({ jobIds: [...], wait: true }). Never start a second export or re-apply an edit after a committed revision. If the MCP client kills a call and shows only a timeout, assume the acknowledgment may have been lost: DO NOT blindly retry a mutating or metered tool. Verify state first. For a write/edit, use list_draft_files and read_draft_file to confirm the intended source and revision. For an export or other project mutation, use get_project/list_projects to check whether it already started or completed. brand_extract returns a durable run handle; continue that exact jobId in status mode and never re-extract blindly. If its start acknowledgement is lost, check brand_list/brand_get before starting again. Retry only when those reads prove the mutation did not commit; otherwise continue from the observed state.\n\nTHE MOTION FLOOR \u2014 these hold even if you read nothing else:\n1. Beat out the story before writing code: every beat needs a time, a thing on screen, and a move. A beat with no move is not a beat.\n2. Text enters on RISE \u2014 text.reveal(el, 'rise') on promo-out, lines ~0.2s apart. Do NOT default type to 'blur': word-by-word de-blur reads as out-of-focus and a staggered blur turns to mush. Blur is for whole-frame moves \u2014 fx.spike(el, 'blur') alongside a whip or dive, where it hides the cut. Never animate blur above 20px.\n3. Every ease is a named house curve \u2014 ease-out-* for entrances, ease-in-out-* for travel and ALL camera moves, ease-in-* for exits and dives only. Never leave an ease unspecified, and never put an ease-in-* on an entrance.\n4. Motion is fast: entrances 0.4-0.6s, exits shorter, line staggers ~0.2s apart, nothing over 1s except a deliberate hold drift or slow push.\n5. Nothing is ever static \u2014 every hold carries a slow drift or camera push. Cuts are HARD (tl.set), never crossfades.\n6. Determinism: no Date.now, Math.random, setTimeout, or CSS transitions. The exporter renders by seeking, so wall-clock motion shimmers or vanishes.\n7. Never claim a video is done without calling get_frames and looking at the frames."
|
|
31975
32140
|
}
|
|
31976
32141
|
);
|
|
31977
32142
|
for (const tool of TOOLS) register(server, tool, validatedCtx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@impractical-ai/motion",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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",
|
|
@@ -25,15 +25,16 @@
|
|
|
25
25
|
"node": ">=22"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
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",
|
|
28
|
+
"build": "node ../scripts/verify-locked-cli-install.mjs && node ../scripts/build-engine-dts.mjs --cli-only && rm -rf dist && ./node_modules/esbuild/bin/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",
|
|
29
29
|
"typecheck": "tsc --noEmit",
|
|
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",
|
|
30
|
+
"test": "npm run typecheck && 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 tests/mcp-agent-timeout.test.ts tests/api-hardening.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 dist/mcp-agent-timeout.test.mjs dist/api-hardening.test.mjs",
|
|
31
31
|
"prepack": "npm run build"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
+
"@types/node": "^26.1.0",
|
|
37
38
|
"esbuild": "^0.25.0",
|
|
38
39
|
"typescript": "^5.6.3"
|
|
39
40
|
},
|