@impractical-ai/motion 0.1.5 → 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/assets/engine.d.ts +38 -2
- package/dist/{api-KD6BNKBQ.js → api-C7KJDYN4.js} +4 -2
- package/dist/{chunk-DMAQPSA4.js → chunk-BUHNO2W2.js} +222 -27
- package/dist/{chunk-7724464Z.js → chunk-KITRCE33.js} +65 -14
- package/dist/{chunk-JDN3QHCX.js → chunk-LJ4YH3YJ.js} +4 -3
- package/dist/{commands-Y35XPDVH.js → commands-P5CDHPDP.js} +28 -4
- package/dist/index.js +5 -5
- package/dist/{stdio-ZFPBM6Z6.js → stdio-GHDI4S7P.js} +505 -325
- package/package.json +4 -3
package/assets/engine.d.ts
CHANGED
|
@@ -598,12 +598,20 @@ export type AudioKit = {
|
|
|
598
598
|
}
|
|
599
599
|
|
|
600
600
|
export type Output = {
|
|
601
|
+
/** Delivery raster dimensions. They do not change scene layout. */
|
|
601
602
|
width: number
|
|
602
603
|
height: number
|
|
603
604
|
fps: number
|
|
604
605
|
render?: RenderConfig
|
|
605
606
|
}
|
|
606
607
|
|
|
608
|
+
/** Resolved renderer contract. `width`/`height` are always logical scene
|
|
609
|
+
* coordinates; only the exporter consumes `delivery` as raster dimensions. */
|
|
610
|
+
export type StageConfig = Output & {
|
|
611
|
+
composition: { width: number; height: number }
|
|
612
|
+
delivery: { width: number; height: number }
|
|
613
|
+
}
|
|
614
|
+
|
|
607
615
|
export type Rect = { left: number; top: number; width: number; height: number }
|
|
608
616
|
|
|
609
617
|
export type FramingTarget = string | Element | Rect | FramingTarget[]
|
|
@@ -642,6 +650,9 @@ export type TextStyleOverride = {
|
|
|
642
650
|
color?: string
|
|
643
651
|
x?: number
|
|
644
652
|
y?: number
|
|
653
|
+
/** Wrapping width in scene px — the side bars reflow text instead of
|
|
654
|
+
* scale-squeezing it. Applied as inline-block + width. */
|
|
655
|
+
width?: number
|
|
645
656
|
hidden?: boolean
|
|
646
657
|
}
|
|
647
658
|
|
|
@@ -652,6 +663,14 @@ export type ObjectPositionOverride = {
|
|
|
652
663
|
scaleY?: number
|
|
653
664
|
rotation?: number
|
|
654
665
|
hidden?: boolean
|
|
666
|
+
/** Plain CSS declarations the studio applies to the object's element —
|
|
667
|
+
* border-radius, background-color, box-shadow and friends. Applied at
|
|
668
|
+
* stage prepare, so they ride the structural rebuild route. */
|
|
669
|
+
styles?: Record<string, string>
|
|
670
|
+
/** Replacement source for the image or video the object carries (the
|
|
671
|
+
* element itself or its first img/video descendant), usually a data URI so
|
|
672
|
+
* renders never depend on the network. */
|
|
673
|
+
src?: string
|
|
655
674
|
}
|
|
656
675
|
|
|
657
676
|
export type AddedTextLayer = {
|
|
@@ -668,6 +687,11 @@ export type SceneOverrides = {
|
|
|
668
687
|
textStyles?: Record<string, TextStyleOverride>
|
|
669
688
|
addedText?: Record<string, AddedTextLayer>
|
|
670
689
|
objects?: Record<string, ObjectPositionOverride> // stable UI wrapper id -> scene-pixel offset
|
|
690
|
+
/** Explicit paint order for editable siblings. Keys include the editable
|
|
691
|
+
* kind (`object:…` / `text:…`) so independently generated ids cannot
|
|
692
|
+
* collide. Values are normalized by the studio whenever an item moves in
|
|
693
|
+
* the stack. */
|
|
694
|
+
layerOrder?: Record<string, number>
|
|
671
695
|
}
|
|
672
696
|
|
|
673
697
|
export type TweenKind =
|
|
@@ -698,12 +722,16 @@ export type TweenInfo = {
|
|
|
698
722
|
stagger?: number
|
|
699
723
|
vars: Record<string, number | string>
|
|
700
724
|
overridden: boolean
|
|
725
|
+
/** The editable layer this tween animates, when its first Element target
|
|
726
|
+
* sits inside one — lets the studio show a layer's choreography. */
|
|
727
|
+
targetObjectId?: string
|
|
728
|
+
targetTextId?: string
|
|
701
729
|
}
|
|
702
730
|
|
|
703
731
|
export type CameraSetInfo = Framing & { id: number; overridden: boolean }
|
|
704
732
|
|
|
705
733
|
export type SceneInspection = {
|
|
706
|
-
config:
|
|
734
|
+
config: StageConfig
|
|
707
735
|
duration: number
|
|
708
736
|
tweens: TweenInfo[]
|
|
709
737
|
cameraSets: CameraSetInfo[]
|
|
@@ -720,7 +748,7 @@ export type DirectionContext = {
|
|
|
720
748
|
gsap: typeof gsap
|
|
721
749
|
tl: gsap.core.Timeline // the master timeline (created paused)
|
|
722
750
|
stage: HTMLElement // scene wrapper the camera transforms
|
|
723
|
-
config:
|
|
751
|
+
config: StageConfig
|
|
724
752
|
q: <T extends HTMLElement>(selector: string) => T
|
|
725
753
|
qa: (selector: string) => HTMLElement[]
|
|
726
754
|
camera: {
|
|
@@ -897,6 +925,14 @@ export type DirectionContext = {
|
|
|
897
925
|
|
|
898
926
|
export type Direction = (ctx: DirectionContext) => void | Promise<void>
|
|
899
927
|
|
|
928
|
+
export type BuiltScene = {
|
|
929
|
+
timeline: gsap.core.Timeline
|
|
930
|
+
runtime: DeterministicStageRuntime
|
|
931
|
+
scene: SceneInspection
|
|
932
|
+
config: StageConfig
|
|
933
|
+
previewOverrides: (overrides: SceneOverrides) => boolean
|
|
934
|
+
}
|
|
935
|
+
|
|
900
936
|
// ─── Named eases ──────────────────────────────────────────────────────
|
|
901
937
|
|
|
902
938
|
// ─── The promo easing set ─────────────────────────────────────────────────
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
ApiContractError,
|
|
4
4
|
ApiError,
|
|
5
|
+
ApiTransportError,
|
|
5
6
|
admitMcpCall,
|
|
6
7
|
delay,
|
|
7
8
|
invoke,
|
|
@@ -12,11 +13,12 @@ import {
|
|
|
12
13
|
waitForJobs,
|
|
13
14
|
withAdmissionReceipt,
|
|
14
15
|
withMeteredCall
|
|
15
|
-
} from "./chunk-
|
|
16
|
-
import "./chunk-
|
|
16
|
+
} from "./chunk-KITRCE33.js";
|
|
17
|
+
import "./chunk-LJ4YH3YJ.js";
|
|
17
18
|
export {
|
|
18
19
|
ApiContractError,
|
|
19
20
|
ApiError,
|
|
21
|
+
ApiTransportError,
|
|
20
22
|
admitMcpCall,
|
|
21
23
|
delay,
|
|
22
24
|
invoke,
|
|
@@ -6,17 +6,26 @@ import {
|
|
|
6
6
|
loadConfig,
|
|
7
7
|
recoverCorruptConfig,
|
|
8
8
|
saveConfig
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-KITRCE33.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-
|
|
16
|
+
} from "./chunk-LJ4YH3YJ.js";
|
|
17
17
|
|
|
18
18
|
// src/onboarding.ts
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
chmodSync,
|
|
21
|
+
existsSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
readFileSync,
|
|
24
|
+
renameSync,
|
|
25
|
+
statSync,
|
|
26
|
+
unlinkSync,
|
|
27
|
+
writeFileSync
|
|
28
|
+
} from "node:fs";
|
|
20
29
|
import { homedir } from "node:os";
|
|
21
30
|
import { dirname, join } from "node:path";
|
|
22
31
|
import { fileURLToPath } from "node:url";
|
|
@@ -29,6 +38,8 @@ var CLAUDE_INSTALL_COMMAND = "npm install -g @anthropic-ai/claude-code";
|
|
|
29
38
|
var CLAUDE_INSTALL_URL = "https://code.claude.com/docs/en/installation";
|
|
30
39
|
var MCP_SERVER_NAME = "impractical";
|
|
31
40
|
var MCP_COMMAND = ["npx", "--yes", `${PACKAGE_NAME}@latest`, "mcp"];
|
|
41
|
+
var MCP_TOOL_TIMEOUT_MS = 6e5;
|
|
42
|
+
var MCP_TOOL_TIMEOUT_SECONDS = MCP_TOOL_TIMEOUT_MS / 1e3;
|
|
32
43
|
function parseSemver(value) {
|
|
33
44
|
const match = value.trim().match(
|
|
34
45
|
/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
|
|
@@ -136,17 +147,116 @@ function hasExpectedMcp(result) {
|
|
|
136
147
|
const detail = commandDetail(result);
|
|
137
148
|
return detail.includes("@impractical-ai/motion@latest") && /\bmcp\b/.test(detail);
|
|
138
149
|
}
|
|
139
|
-
function
|
|
150
|
+
function hasExpectedClaudeMcp(result) {
|
|
151
|
+
if (!hasExpectedMcp(result)) return false;
|
|
152
|
+
return new RegExp(`\\b${MCP_TOOL_TIMEOUT_MS}(?:ms)?\\b`).test(commandDetail(result));
|
|
153
|
+
}
|
|
154
|
+
function hasExpectedCodexMcp(result) {
|
|
155
|
+
if (!hasExpectedMcp(result)) return false;
|
|
156
|
+
return new RegExp(`\\btool_timeout_sec\\s*:\\s*${MCP_TOOL_TIMEOUT_SECONDS}\\b`).test(
|
|
157
|
+
commandDetail(result)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
function codexConfigPath(home) {
|
|
161
|
+
const codexHome = process.env.CODEX_HOME?.trim();
|
|
162
|
+
return join(codexHome || join(home, ".codex"), "config.toml");
|
|
163
|
+
}
|
|
164
|
+
function atomicWrite(path, contents, mode) {
|
|
165
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
166
|
+
const temporary = join(dirname(path), `.${MCP_SERVER_NAME}-${process.pid}-${Date.now()}.tmp`);
|
|
167
|
+
try {
|
|
168
|
+
writeFileSync(temporary, contents, { mode });
|
|
169
|
+
chmodSync(temporary, mode);
|
|
170
|
+
renameSync(temporary, path);
|
|
171
|
+
} finally {
|
|
172
|
+
if (existsSync(temporary)) unlinkSync(temporary);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function upsertCodexToolTimeout(path) {
|
|
176
|
+
if (!existsSync(path)) throw new Error(`Codex MCP registration did not create ${path}.`);
|
|
177
|
+
const previous = readFileSync(path, "utf8");
|
|
178
|
+
const newline = previous.includes("\r\n") ? "\r\n" : "\n";
|
|
179
|
+
const lines = previous.split(/\r?\n/);
|
|
180
|
+
const section = /^\s*\[mcp_servers\.(?:impractical|"impractical")\]\s*(?:#.*)?$/;
|
|
181
|
+
const sectionStart = lines.findIndex((line) => section.test(line));
|
|
182
|
+
if (sectionStart === -1) {
|
|
183
|
+
throw new Error(`Codex MCP registration did not create [mcp_servers.${MCP_SERVER_NAME}].`);
|
|
184
|
+
}
|
|
185
|
+
let sectionEnd = lines.length;
|
|
186
|
+
for (let index = sectionStart + 1; index < lines.length; index++) {
|
|
187
|
+
if (/^\s*\[/.test(lines[index])) {
|
|
188
|
+
sectionEnd = index;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const timeoutLine = /^\s*tool_timeout_sec\s*=/;
|
|
193
|
+
const timeoutIndex = lines.slice(sectionStart + 1, sectionEnd).findIndex((line) => timeoutLine.test(line));
|
|
194
|
+
if (timeoutIndex !== -1 && new RegExp(`^\\s*tool_timeout_sec\\s*=\\s*${MCP_TOOL_TIMEOUT_SECONDS}\\s*(?:#.*)?$`).test(
|
|
195
|
+
lines[sectionStart + 1 + timeoutIndex]
|
|
196
|
+
)) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (timeoutIndex === -1) {
|
|
200
|
+
lines.splice(sectionEnd, 0, `tool_timeout_sec = ${MCP_TOOL_TIMEOUT_SECONDS}`);
|
|
201
|
+
} else {
|
|
202
|
+
lines[sectionStart + 1 + timeoutIndex] = `tool_timeout_sec = ${MCP_TOOL_TIMEOUT_SECONDS}`;
|
|
203
|
+
}
|
|
204
|
+
atomicWrite(path, lines.join(newline), statSync(path).mode & 511);
|
|
205
|
+
}
|
|
206
|
+
function restoreFile(path, existed, contents, mode) {
|
|
207
|
+
if (existed) atomicWrite(path, contents, mode);
|
|
208
|
+
else if (existsSync(path)) unlinkSync(path);
|
|
209
|
+
}
|
|
210
|
+
function installNativeMcp(command, harness, home) {
|
|
140
211
|
if (!commandAvailable(command)) return void 0;
|
|
141
212
|
const existing = commandResult(command, ["mcp", "get", MCP_SERVER_NAME]);
|
|
142
|
-
if (
|
|
143
|
-
if (
|
|
144
|
-
|
|
213
|
+
if (command === "claude") {
|
|
214
|
+
if (!hasExpectedClaudeMcp(existing)) {
|
|
215
|
+
const existingDetail = commandDetail(existing);
|
|
216
|
+
if (!existing.error && existing.status === 0 && !/No MCP server named\b/i.test(existingDetail)) {
|
|
217
|
+
runMcpCommand(command, ["mcp", "remove", MCP_SERVER_NAME, "-s", "user"], harness);
|
|
218
|
+
}
|
|
219
|
+
const definition = JSON.stringify({
|
|
220
|
+
type: "stdio",
|
|
221
|
+
command: MCP_COMMAND[0],
|
|
222
|
+
args: MCP_COMMAND.slice(1),
|
|
223
|
+
timeout: MCP_TOOL_TIMEOUT_MS
|
|
224
|
+
});
|
|
225
|
+
runMcpCommand(
|
|
226
|
+
command,
|
|
227
|
+
["mcp", "add-json", "-s", "user", MCP_SERVER_NAME, definition],
|
|
228
|
+
harness
|
|
229
|
+
);
|
|
230
|
+
if (!hasExpectedClaudeMcp(commandResult(command, ["mcp", "get", MCP_SERVER_NAME]))) {
|
|
231
|
+
throw new Error(`${harness} MCP setup failed validation after registration.`);
|
|
232
|
+
}
|
|
145
233
|
}
|
|
146
|
-
|
|
147
|
-
runMcpCommand(command, addArgs, harness);
|
|
234
|
+
return { harness };
|
|
148
235
|
}
|
|
149
|
-
|
|
236
|
+
const path = codexConfigPath(home);
|
|
237
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
238
|
+
const existed = existsSync(path);
|
|
239
|
+
const previous = existed ? readFileSync(path, "utf8") : "";
|
|
240
|
+
const mode = existed ? statSync(path).mode & 511 : 384;
|
|
241
|
+
try {
|
|
242
|
+
if (!hasExpectedMcp(existing)) {
|
|
243
|
+
runMcpCommand(command, ["mcp", "add", MCP_SERVER_NAME, "--", ...MCP_COMMAND], harness);
|
|
244
|
+
}
|
|
245
|
+
upsertCodexToolTimeout(path);
|
|
246
|
+
if (!hasExpectedCodexMcp(commandResult(command, ["mcp", "get", MCP_SERVER_NAME]))) {
|
|
247
|
+
throw new Error(`${harness} MCP setup failed validation after registration.`);
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
try {
|
|
251
|
+
restoreFile(path, existed, previous, mode);
|
|
252
|
+
} catch (rollbackError) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`${error instanceof Error ? error.message : String(error)} Rollback failed: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
return { harness, path };
|
|
150
260
|
}
|
|
151
261
|
function readJsonObject(path) {
|
|
152
262
|
if (!existsSync(path)) return {};
|
|
@@ -166,6 +276,13 @@ function writeJsonObject(path, value) {
|
|
|
166
276
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}
|
|
167
277
|
`, { mode: 384 });
|
|
168
278
|
}
|
|
279
|
+
function hasRequiredFields(value, required) {
|
|
280
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
281
|
+
const record = value;
|
|
282
|
+
return Object.entries(required).every(
|
|
283
|
+
([key, expected]) => JSON.stringify(record[key]) === JSON.stringify(expected)
|
|
284
|
+
);
|
|
285
|
+
}
|
|
169
286
|
function installCursorMcp(home) {
|
|
170
287
|
if (!commandAvailable("cursor-agent") && !commandAvailable("cursor")) return void 0;
|
|
171
288
|
const path = join(home, ".cursor", "mcp.json");
|
|
@@ -175,12 +292,13 @@ function installCursorMcp(home) {
|
|
|
175
292
|
command: MCP_COMMAND[0],
|
|
176
293
|
args: MCP_COMMAND.slice(1)
|
|
177
294
|
};
|
|
178
|
-
if (
|
|
295
|
+
if (hasRequiredFields(servers[MCP_SERVER_NAME], expected)) {
|
|
179
296
|
return { harness: "Cursor", path };
|
|
180
297
|
}
|
|
298
|
+
const existing = servers[MCP_SERVER_NAME] && typeof servers[MCP_SERVER_NAME] === "object" && !Array.isArray(servers[MCP_SERVER_NAME]) ? servers[MCP_SERVER_NAME] : {};
|
|
181
299
|
config.mcpServers = {
|
|
182
300
|
...servers,
|
|
183
|
-
[MCP_SERVER_NAME]: expected
|
|
301
|
+
[MCP_SERVER_NAME]: { ...existing, ...expected }
|
|
184
302
|
};
|
|
185
303
|
writeJsonObject(path, config);
|
|
186
304
|
return { harness: "Cursor", path };
|
|
@@ -191,11 +309,38 @@ function openCodeConfigHome(home) {
|
|
|
191
309
|
return join(home, ".config");
|
|
192
310
|
}
|
|
193
311
|
function verifyAgentConfigs(home = homedir()) {
|
|
312
|
+
const checks = [];
|
|
194
313
|
if (commandAvailable("cursor-agent") || commandAvailable("cursor")) {
|
|
195
|
-
|
|
314
|
+
checks.push({ harness: "Cursor", path: join(home, ".cursor", "mcp.json") });
|
|
196
315
|
}
|
|
197
316
|
if (commandAvailable("opencode")) {
|
|
198
|
-
|
|
317
|
+
checks.push({
|
|
318
|
+
harness: "OpenCode",
|
|
319
|
+
path: join(openCodeConfigHome(home), "opencode", "opencode.json")
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
const failures = [];
|
|
323
|
+
for (const check of checks) {
|
|
324
|
+
try {
|
|
325
|
+
readJsonObject(check.path);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
failures.push({
|
|
328
|
+
harness: check.harness,
|
|
329
|
+
path: check.path,
|
|
330
|
+
message: error instanceof Error ? error.message : String(error),
|
|
331
|
+
remediation: agentMcpRemediation(check.harness, check.path)
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (failures.length > 0) {
|
|
336
|
+
const detail = failures.map(
|
|
337
|
+
(failure) => `${failure.harness}: ${failure.message}
|
|
338
|
+
Remediation: ${failure.remediation}`
|
|
339
|
+
).join("\n");
|
|
340
|
+
throw new Error(
|
|
341
|
+
`Device authorization was not started because ${failures.length} coding agent configuration${failures.length === 1 ? "" : "s"} must be repaired first.
|
|
342
|
+
${detail}`
|
|
343
|
+
);
|
|
199
344
|
}
|
|
200
345
|
}
|
|
201
346
|
function installOpenCodeMcp(home) {
|
|
@@ -205,25 +350,68 @@ function installOpenCodeMcp(home) {
|
|
|
205
350
|
const servers = config.mcp && typeof config.mcp === "object" && !Array.isArray(config.mcp) ? config.mcp : {};
|
|
206
351
|
const expected = {
|
|
207
352
|
type: "local",
|
|
208
|
-
command: MCP_COMMAND
|
|
353
|
+
command: MCP_COMMAND,
|
|
354
|
+
timeout: MCP_TOOL_TIMEOUT_MS
|
|
209
355
|
};
|
|
210
|
-
if (
|
|
356
|
+
if (hasRequiredFields(servers[MCP_SERVER_NAME], expected)) {
|
|
211
357
|
return { harness: "OpenCode", path };
|
|
212
358
|
}
|
|
359
|
+
const existing = servers[MCP_SERVER_NAME] && typeof servers[MCP_SERVER_NAME] === "object" && !Array.isArray(servers[MCP_SERVER_NAME]) ? servers[MCP_SERVER_NAME] : {};
|
|
213
360
|
config.mcp = {
|
|
214
361
|
...servers,
|
|
215
|
-
[MCP_SERVER_NAME]: expected
|
|
362
|
+
[MCP_SERVER_NAME]: { ...existing, ...expected }
|
|
216
363
|
};
|
|
217
364
|
writeJsonObject(path, config);
|
|
218
365
|
return { harness: "OpenCode", path };
|
|
219
366
|
}
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
367
|
+
function agentMcpRemediation(harness, path) {
|
|
368
|
+
if (path) {
|
|
369
|
+
return `Repair ${path} so it contains a valid configuration object without deleting unrelated entries, then rerun \`${PACKAGE_COMMAND} setup --agent\`.`;
|
|
370
|
+
}
|
|
371
|
+
return `Inspect the ${harness} MCP configuration for \`${MCP_SERVER_NAME}\`, then rerun \`${PACKAGE_COMMAND} setup --agent\`.`;
|
|
372
|
+
}
|
|
373
|
+
function agentMcpFailureMessage(failures) {
|
|
374
|
+
const detail = failures.map((failure) => `${failure.harness}: ${failure.message}
|
|
375
|
+
Remediation: ${failure.remediation}`).join("\n");
|
|
376
|
+
return `Could not configure ${failures.length} coding agent${failures.length === 1 ? "" : "s"}. Successful agent configurations were preserved.
|
|
377
|
+
${detail}`;
|
|
378
|
+
}
|
|
379
|
+
function configureAgentMcps(home = homedir()) {
|
|
380
|
+
const configured = [];
|
|
381
|
+
const failures = [];
|
|
382
|
+
const installers = [
|
|
383
|
+
{ harness: "Claude Code", install: () => installNativeMcp("claude", "Claude Code", home) },
|
|
384
|
+
{
|
|
385
|
+
harness: "Codex",
|
|
386
|
+
path: codexConfigPath(home),
|
|
387
|
+
install: () => installNativeMcp("codex", "Codex", home)
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
harness: "Cursor",
|
|
391
|
+
path: join(home, ".cursor", "mcp.json"),
|
|
392
|
+
install: () => installCursorMcp(home)
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
harness: "OpenCode",
|
|
396
|
+
path: join(openCodeConfigHome(home), "opencode", "opencode.json"),
|
|
397
|
+
install: () => installOpenCodeMcp(home)
|
|
398
|
+
}
|
|
399
|
+
];
|
|
400
|
+
for (const installer of installers) {
|
|
401
|
+
try {
|
|
402
|
+
const result = installer.install();
|
|
403
|
+
if (result) configured.push(result);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
406
|
+
failures.push({
|
|
407
|
+
harness: installer.harness,
|
|
408
|
+
path: installer.path,
|
|
409
|
+
message,
|
|
410
|
+
remediation: agentMcpRemediation(installer.harness, installer.path)
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return { configured, failures };
|
|
227
415
|
}
|
|
228
416
|
function requireCommand(command, label) {
|
|
229
417
|
const result = commandResult(command, ["--version"]);
|
|
@@ -429,7 +617,6 @@ async function setup(options) {
|
|
|
429
617
|
if (options.installClaudePlugin) verifyClaudePrerequisites();
|
|
430
618
|
if (options.installAgentMcp) {
|
|
431
619
|
verifyAgentPrerequisites();
|
|
432
|
-
verifyAgentConfigs();
|
|
433
620
|
}
|
|
434
621
|
const corruptConfigBackup = recoverCorruptConfig();
|
|
435
622
|
if (corruptConfigBackup) {
|
|
@@ -451,6 +638,7 @@ async function setup(options) {
|
|
|
451
638
|
}
|
|
452
639
|
}
|
|
453
640
|
if (!apiKey) {
|
|
641
|
+
if (options.installAgentMcp) verifyAgentConfigs();
|
|
454
642
|
const authorization = await invokeAt(supabaseUrl, "cli-device-auth", {
|
|
455
643
|
action: "start"
|
|
456
644
|
});
|
|
@@ -475,7 +663,7 @@ async function setup(options) {
|
|
|
475
663
|
saveConfig({ apiKey, supabaseUrl, appUrl });
|
|
476
664
|
}
|
|
477
665
|
const claudePlugin = options.installClaudePlugin ? installClaudePlugin(options.log) : void 0;
|
|
478
|
-
const
|
|
666
|
+
const agentMcpSetup = options.installAgentMcp ? configureAgentMcps() : { configured: [], failures: [] };
|
|
479
667
|
const installed = options.installAgentSkills ? installSkills() : [];
|
|
480
668
|
const reconciled = reconcileLegacySkills();
|
|
481
669
|
options.log("");
|
|
@@ -483,9 +671,13 @@ async function setup(options) {
|
|
|
483
671
|
for (const skill of installed) {
|
|
484
672
|
options.log(`Installed /impractical for ${skill.harness}.`);
|
|
485
673
|
}
|
|
486
|
-
for (const mcp of
|
|
674
|
+
for (const mcp of agentMcpSetup.configured) {
|
|
487
675
|
options.log(`Configured Impractical MCP for ${mcp.harness}.`);
|
|
488
676
|
}
|
|
677
|
+
for (const failure of agentMcpSetup.failures) {
|
|
678
|
+
options.log(`Could not configure Impractical MCP for ${failure.harness}: ${failure.message}`);
|
|
679
|
+
options.log(`Remediation for ${failure.harness}: ${failure.remediation}`);
|
|
680
|
+
}
|
|
489
681
|
for (const skill of reconciled) {
|
|
490
682
|
options.log(`Refreshed legacy /impractical for ${skill.harness}; previous copy preserved at ${skill.backupPath}.`);
|
|
491
683
|
}
|
|
@@ -500,6 +692,9 @@ async function setup(options) {
|
|
|
500
692
|
if (options.installClaudePlugin || options.installAgentMcp) {
|
|
501
693
|
for (const line of authenticatedWhoamiGuidance()) options.log(line);
|
|
502
694
|
}
|
|
695
|
+
if (agentMcpSetup.failures.length > 0) {
|
|
696
|
+
throw new Error(agentMcpFailureMessage(agentMcpSetup.failures));
|
|
697
|
+
}
|
|
503
698
|
return { installed, appUrl, supabaseUrl };
|
|
504
699
|
}
|
|
505
700
|
|
|
@@ -2,10 +2,19 @@
|
|
|
2
2
|
import {
|
|
3
3
|
PRODUCTION_APP_URL,
|
|
4
4
|
PRODUCTION_SUPABASE_URL
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-LJ4YH3YJ.js";
|
|
6
6
|
|
|
7
7
|
// src/config.ts
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
chmodSync,
|
|
10
|
+
existsSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
unlinkSync,
|
|
15
|
+
writeFileSync
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { randomBytes } from "node:crypto";
|
|
9
18
|
import { homedir } from "node:os";
|
|
10
19
|
import { join } from "node:path";
|
|
11
20
|
var configDir = () => join(homedir(), ".impractical");
|
|
@@ -63,9 +72,25 @@ function loadConfig() {
|
|
|
63
72
|
function saveConfig(patch) {
|
|
64
73
|
const stored = readStoredConfig();
|
|
65
74
|
const next = { ...stored, ...patch };
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
const directory = configDir();
|
|
76
|
+
const path = configPath();
|
|
77
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
78
|
+
chmodSync(directory, 448);
|
|
79
|
+
const temporary = join(
|
|
80
|
+
directory,
|
|
81
|
+
`.config-${process.pid}-${randomBytes(8).toString("hex")}.tmp`
|
|
82
|
+
);
|
|
83
|
+
try {
|
|
84
|
+
writeFileSync(temporary, JSON.stringify(next, null, 2) + "\n", {
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
flag: "wx",
|
|
87
|
+
mode: 384
|
|
88
|
+
});
|
|
89
|
+
chmodSync(temporary, 384);
|
|
90
|
+
renameSync(temporary, path);
|
|
91
|
+
} finally {
|
|
92
|
+
if (existsSync(temporary)) unlinkSync(temporary);
|
|
93
|
+
}
|
|
69
94
|
return next;
|
|
70
95
|
}
|
|
71
96
|
function requireConfig() {
|
|
@@ -97,6 +122,16 @@ var ApiContractError = class extends Error {
|
|
|
97
122
|
this.fn = fn;
|
|
98
123
|
}
|
|
99
124
|
};
|
|
125
|
+
var ApiTransportError = class extends Error {
|
|
126
|
+
constructor(fn, endpoint, reason) {
|
|
127
|
+
super(
|
|
128
|
+
`Could not reach ${endpoint} for ${fn}. Cause: ${reason}. Check your network and IMPRACTICAL_SUPABASE_URL, then retry. If this persists, run \`npx --yes @impractical-ai/motion setup --agent\`.`
|
|
129
|
+
);
|
|
130
|
+
this.fn = fn;
|
|
131
|
+
this.endpoint = endpoint;
|
|
132
|
+
this.reason = reason;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
100
135
|
function isRecord(value) {
|
|
101
136
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
102
137
|
}
|
|
@@ -156,15 +191,30 @@ async function invoke(name, body = {}) {
|
|
|
156
191
|
return invokeAt(supabaseUrl, name, body, apiKey, receipt);
|
|
157
192
|
}
|
|
158
193
|
async function invokeAt(supabaseUrl, name, body = {}, apiKey, admissionReceipt) {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
194
|
+
const requestUrl = `${supabaseUrl.replace(/\/$/, "")}/functions/v1/${name}`;
|
|
195
|
+
let response;
|
|
196
|
+
try {
|
|
197
|
+
response = await fetch(requestUrl, {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: {
|
|
200
|
+
...apiKey ? { authorization: `Bearer ${apiKey}` } : {},
|
|
201
|
+
...admissionReceipt ? { "x-impractical-admission": admissionReceipt } : {},
|
|
202
|
+
"content-type": "application/json"
|
|
203
|
+
},
|
|
204
|
+
body: JSON.stringify(body)
|
|
205
|
+
});
|
|
206
|
+
} catch (error) {
|
|
207
|
+
let endpoint = `configured API/functions/v1/${encodeURIComponent(name)}`;
|
|
208
|
+
try {
|
|
209
|
+
const configured = new URL(supabaseUrl);
|
|
210
|
+
endpoint = `${configured.protocol}//${configured.host}/functions/v1/${encodeURIComponent(name)}`;
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
const cause = error instanceof Error ? error.cause : void 0;
|
|
214
|
+
const code = isRecord(cause) && typeof cause.code === "string" ? cause.code : void 0;
|
|
215
|
+
const reason = code && /^[A-Z0-9_]+$/.test(code) ? code : "network transport error";
|
|
216
|
+
throw new ApiTransportError(name, endpoint, reason);
|
|
217
|
+
}
|
|
168
218
|
const text = await response.text();
|
|
169
219
|
let data = null;
|
|
170
220
|
try {
|
|
@@ -223,6 +273,7 @@ export {
|
|
|
223
273
|
requireConfig,
|
|
224
274
|
ApiError,
|
|
225
275
|
ApiContractError,
|
|
276
|
+
ApiTransportError,
|
|
226
277
|
validateApiResponse,
|
|
227
278
|
invoke,
|
|
228
279
|
invokeAt,
|
|
@@ -32,7 +32,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
32
32
|
// package.json
|
|
33
33
|
var package_default = {
|
|
34
34
|
name: "@impractical-ai/motion",
|
|
35
|
-
version: "0.1.
|
|
35
|
+
version: "0.1.6",
|
|
36
36
|
private: false,
|
|
37
37
|
description: "Create and refine Impractical Motion videos from coding agents and the terminal.",
|
|
38
38
|
homepage: "https://github.com/Openpod/impractical-motion#readme",
|
|
@@ -57,15 +57,16 @@ var package_default = {
|
|
|
57
57
|
node: ">=22"
|
|
58
58
|
},
|
|
59
59
|
scripts: {
|
|
60
|
-
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',
|
|
60
|
+
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',
|
|
61
61
|
typecheck: "tsc --noEmit",
|
|
62
|
-
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",
|
|
62
|
+
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",
|
|
63
63
|
prepack: "npm run build"
|
|
64
64
|
},
|
|
65
65
|
publishConfig: {
|
|
66
66
|
access: "public"
|
|
67
67
|
},
|
|
68
68
|
devDependencies: {
|
|
69
|
+
"@types/node": "^26.1.0",
|
|
69
70
|
esbuild: "^0.25.0",
|
|
70
71
|
typescript: "^5.6.3"
|
|
71
72
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
setup
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-BUHNO2W2.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-
|
|
15
|
-
import "./chunk-
|
|
14
|
+
} from "./chunk-KITRCE33.js";
|
|
15
|
+
import "./chunk-LJ4YH3YJ.js";
|
|
16
16
|
|
|
17
17
|
// src/commands.ts
|
|
18
18
|
import { readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
@@ -1142,6 +1142,26 @@ function parseSceneFiles(pasted) {
|
|
|
1142
1142
|
return files;
|
|
1143
1143
|
}
|
|
1144
1144
|
|
|
1145
|
+
// ../src/shared/local-project.ts
|
|
1146
|
+
var LOCAL_PROJECT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
1147
|
+
var LOCAL_SCENE_NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
1148
|
+
function isLocalProjectId(value) {
|
|
1149
|
+
return typeof value === "string" && LOCAL_PROJECT_ID_RE.test(value);
|
|
1150
|
+
}
|
|
1151
|
+
function localProjectMarker(input) {
|
|
1152
|
+
if (!isLocalProjectId(input.projectId) || !LOCAL_SCENE_NAME_RE.test(input.sceneName)) {
|
|
1153
|
+
throw new Error("A local project requires a valid project and scene identity");
|
|
1154
|
+
}
|
|
1155
|
+
return `${JSON.stringify({
|
|
1156
|
+
projectId: input.projectId,
|
|
1157
|
+
sceneName: input.sceneName,
|
|
1158
|
+
createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1159
|
+
valueProp: input.valueProp.trim(),
|
|
1160
|
+
...input.brand ? { brand: input.brand } : {}
|
|
1161
|
+
})}
|
|
1162
|
+
`;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1145
1165
|
// ../src/shared/initial-files.ts
|
|
1146
1166
|
var TITLE_TEMPLATE = TEMPLATES.find((t) => t.id === "title-card");
|
|
1147
1167
|
var BLANK_DIRECTION = TEMPLATES.find((t) => t.id === "blank").generate({});
|
|
@@ -1162,7 +1182,11 @@ function buildInitialFiles(input) {
|
|
|
1162
1182
|
}
|
|
1163
1183
|
})
|
|
1164
1184
|
};
|
|
1165
|
-
const projectMarker =
|
|
1185
|
+
const projectMarker = input.localProject ? localProjectMarker({
|
|
1186
|
+
...input.localProject,
|
|
1187
|
+
valueProp: input.valueProp,
|
|
1188
|
+
brand: input.brand?.slug
|
|
1189
|
+
}) : JSON.stringify({
|
|
1166
1190
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1167
1191
|
valueProp: input.valueProp.trim(),
|
|
1168
1192
|
...input.brand ? { brand: input.brand.slug } : {}
|