@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.
@@ -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: Output
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: Output
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-7724464Z.js";
16
- import "./chunk-JDN3QHCX.js";
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-7724464Z.js";
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-JDN3QHCX.js";
16
+ } from "./chunk-LJ4YH3YJ.js";
17
17
 
18
18
  // src/onboarding.ts
19
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
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 installNativeMcp(command, harness) {
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 (!hasExpectedMcp(existing)) {
143
- if (command === "claude" && !existing.error && existing.status === 0) {
144
- runMcpCommand(command, ["mcp", "remove", MCP_SERVER_NAME, "-s", "user"], harness);
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
- const addArgs = command === "claude" ? ["mcp", "add", "-s", "user", MCP_SERVER_NAME, "--", ...MCP_COMMAND] : ["mcp", "add", MCP_SERVER_NAME, "--", ...MCP_COMMAND];
147
- runMcpCommand(command, addArgs, harness);
234
+ return { harness };
148
235
  }
149
- return { harness };
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 (JSON.stringify(servers[MCP_SERVER_NAME]) === JSON.stringify(expected)) {
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
- readJsonObject(join(home, ".cursor", "mcp.json"));
314
+ checks.push({ harness: "Cursor", path: join(home, ".cursor", "mcp.json") });
196
315
  }
197
316
  if (commandAvailable("opencode")) {
198
- readJsonObject(join(openCodeConfigHome(home), "opencode", "opencode.json"));
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 (JSON.stringify(servers[MCP_SERVER_NAME]) === JSON.stringify(expected)) {
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 installAgentMcps(home = homedir()) {
221
- return [
222
- installNativeMcp("claude", "Claude Code"),
223
- installNativeMcp("codex", "Codex"),
224
- installCursorMcp(home),
225
- installOpenCodeMcp(home)
226
- ].filter((result) => Boolean(result));
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 configuredMcps = options.installAgentMcp ? installAgentMcps() : [];
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 configuredMcps) {
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-JDN3QHCX.js";
5
+ } from "./chunk-LJ4YH3YJ.js";
6
6
 
7
7
  // src/config.ts
8
- import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync, renameSync } from "node:fs";
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
- mkdirSync(configDir(), { recursive: true });
67
- writeFileSync(configPath(), JSON.stringify(next, null, 2) + "\n");
68
- chmodSync(configPath(), 384);
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 response = await fetch(`${supabaseUrl.replace(/\/$/, "")}/functions/v1/${name}`, {
160
- method: "POST",
161
- headers: {
162
- ...apiKey ? { authorization: `Bearer ${apiKey}` } : {},
163
- ...admissionReceipt ? { "x-impractical-admission": admissionReceipt } : {},
164
- "content-type": "application/json"
165
- },
166
- body: JSON.stringify(body)
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.5",
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-DMAQPSA4.js";
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-7724464Z.js";
15
- import "./chunk-JDN3QHCX.js";
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 = JSON.stringify({
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 } : {}