@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 CHANGED
@@ -1,22 +1,34 @@
1
1
  # Impractical Motion CLI
2
2
 
3
- Connect Claude Code to Impractical Motion through its MCP server.
3
+ Connect Claude Code, Codex, Cursor, or OpenCode to Impractical Motion through
4
+ its MCP server.
4
5
 
5
6
  ## Requirements
6
7
 
7
8
  - Node.js 22 or newer
8
- - Claude Code installed and signed in
9
+ - At least one supported coding agent: Claude Code, Codex, Cursor, or OpenCode
9
10
  - Google Chrome or Chromium for frame inspection
10
11
 
11
12
  ## Setup
12
13
 
14
+ ```sh
15
+ npx --yes @impractical-ai/motion setup --agent
16
+ ```
17
+
18
+ Approve the device code in the browser, then restart the detected coding agents.
19
+ Setup installs the global `impractical` MCP configuration for every detected
20
+ Claude Code, Codex, Cursor, and OpenCode client, plus the shared skill for Claude
21
+ Code and Codex. Re-running setup verifies and reuses a working credential,
22
+ preserves unrelated client configuration, and does not require another browser
23
+ authorization.
24
+
25
+ For the full Claude Code plugin workflow, run:
26
+
13
27
  ```sh
14
28
  npx --yes @impractical-ai/motion setup --claude
15
29
  ```
16
30
 
17
- Approve the device code in the browser, then run `/reload-plugins` in Claude Code and type
18
- `continue`. Re-running setup verifies and reuses a working credential, refreshes the user-scoped
19
- plugin, and does not require another browser authorization.
31
+ Then run `/reload-plugins` in Claude Code and type `continue`.
20
32
 
21
33
  Setup writes the API credential to `~/.impractical/config.json` with mode `0600`. Environment
22
34
  variables `IMPRACTICAL_API_KEY`, `IMPRACTICAL_SUPABASE_URL`, and `IMPRACTICAL_APP_URL` take
@@ -24,7 +36,7 @@ precedence over that file.
24
36
 
25
37
  ## MCP server
26
38
 
27
- The installed Claude plugin launches the server automatically. Other MCP clients can run:
39
+ The installer configures supported clients to run:
28
40
 
29
41
  ```sh
30
42
  npx --yes @impractical-ai/motion@latest mcp
@@ -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 ─────────────────────────────────────────────────
@@ -13,13 +13,15 @@ source code lives in the cloud, never in the user's project directory.
13
13
  ## 0. Setup check (once per session)
14
14
 
15
15
  Call the `account_status` tool (`whoami` is its legacy alias). If the
16
- Impractical tools are not available, the Claude plugin is not connected. Tell
17
- the user to run:
16
+ Impractical tools are not available, the current coding agent is not connected.
17
+ Tell the user to run:
18
18
 
19
- npx --yes @impractical-ai/motion setup --claude
19
+ npx --yes @impractical-ai/motion setup --agent
20
20
 
21
- Then ask them to run `/reload-plugins` and type `continue`. If the weekly quota
22
- is exhausted, call `upgrade` to give the user the authenticated billing page.
21
+ Then ask them to restart or reload their coding agent and type `continue`.
22
+ Claude Code users who specifically want the full plugin can run the same command
23
+ with `--claude` instead. If the weekly quota is exhausted, call `upgrade` to give
24
+ the user the authenticated billing page.
23
25
 
24
26
  ## 1. Read the engine before you write anything
25
27
 
@@ -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-M77TS5RO.js";
16
- import "./chunk-EFP4P3WA.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-M77TS5RO.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-EFP4P3WA.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";
@@ -27,6 +36,10 @@ var CLAUDE_MARKETPLACE_NAME = "impractical";
27
36
  var AUTH_VERIFY_MIN_VERSION = "0.1.2";
28
37
  var CLAUDE_INSTALL_COMMAND = "npm install -g @anthropic-ai/claude-code";
29
38
  var CLAUDE_INSTALL_URL = "https://code.claude.com/docs/en/installation";
39
+ var MCP_SERVER_NAME = "impractical";
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;
30
43
  function parseSemver(value) {
31
44
  const match = value.trim().match(
32
45
  /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
@@ -106,6 +119,300 @@ function marketplaceSourceLabel(record) {
106
119
  function commandResult(command, args) {
107
120
  return spawnSync(command, args, { encoding: "utf8", windowsHide: true });
108
121
  }
122
+ function commandAvailable(command) {
123
+ const result = commandResult(command, ["--version"]);
124
+ return !result.error && result.status === 0;
125
+ }
126
+ function verifyAgentPrerequisites() {
127
+ const found = commandAvailable("claude") || commandAvailable("codex") || commandAvailable("cursor-agent") || commandAvailable("cursor") || commandAvailable("opencode");
128
+ if (!found) {
129
+ throw new Error(
130
+ "No supported coding agent was detected. Install Claude Code, Codex, Cursor, or OpenCode, then rerun setup."
131
+ );
132
+ }
133
+ }
134
+ function commandDetail(result) {
135
+ return `${result.stdout ?? ""}
136
+ ${result.stderr ?? ""}`.trim();
137
+ }
138
+ function runMcpCommand(command, args, harness) {
139
+ const result = commandResult(command, args);
140
+ if (!result.error && result.status === 0) return;
141
+ throw new Error(
142
+ `${harness} MCP setup failed: ${commandDetail(result) || result.error?.message || "unknown error"}`
143
+ );
144
+ }
145
+ function hasExpectedMcp(result) {
146
+ if (result.error || result.status !== 0) return false;
147
+ const detail = commandDetail(result);
148
+ return detail.includes("@impractical-ai/motion@latest") && /\bmcp\b/.test(detail);
149
+ }
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) {
211
+ if (!commandAvailable(command)) return void 0;
212
+ const existing = commandResult(command, ["mcp", "get", MCP_SERVER_NAME]);
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
+ }
233
+ }
234
+ return { harness };
235
+ }
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 };
260
+ }
261
+ function readJsonObject(path) {
262
+ if (!existsSync(path)) return {};
263
+ let value;
264
+ try {
265
+ value = JSON.parse(readFileSync(path, "utf8"));
266
+ } catch {
267
+ throw new Error(`Cannot configure MCP because ${path} is not valid JSON.`);
268
+ }
269
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
270
+ throw new Error(`Cannot configure MCP because ${path} must contain a JSON object.`);
271
+ }
272
+ return value;
273
+ }
274
+ function writeJsonObject(path, value) {
275
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
276
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
277
+ `, { mode: 384 });
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
+ }
286
+ function installCursorMcp(home) {
287
+ if (!commandAvailable("cursor-agent") && !commandAvailable("cursor")) return void 0;
288
+ const path = join(home, ".cursor", "mcp.json");
289
+ const config = readJsonObject(path);
290
+ const servers = config.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers) ? config.mcpServers : {};
291
+ const expected = {
292
+ command: MCP_COMMAND[0],
293
+ args: MCP_COMMAND.slice(1)
294
+ };
295
+ if (hasRequiredFields(servers[MCP_SERVER_NAME], expected)) {
296
+ return { harness: "Cursor", path };
297
+ }
298
+ const existing = servers[MCP_SERVER_NAME] && typeof servers[MCP_SERVER_NAME] === "object" && !Array.isArray(servers[MCP_SERVER_NAME]) ? servers[MCP_SERVER_NAME] : {};
299
+ config.mcpServers = {
300
+ ...servers,
301
+ [MCP_SERVER_NAME]: { ...existing, ...expected }
302
+ };
303
+ writeJsonObject(path, config);
304
+ return { harness: "Cursor", path };
305
+ }
306
+ function openCodeConfigHome(home) {
307
+ if (process.env.XDG_CONFIG_HOME?.trim()) return process.env.XDG_CONFIG_HOME.trim();
308
+ if (process.platform === "win32" && process.env.APPDATA?.trim()) return process.env.APPDATA.trim();
309
+ return join(home, ".config");
310
+ }
311
+ function verifyAgentConfigs(home = homedir()) {
312
+ const checks = [];
313
+ if (commandAvailable("cursor-agent") || commandAvailable("cursor")) {
314
+ checks.push({ harness: "Cursor", path: join(home, ".cursor", "mcp.json") });
315
+ }
316
+ if (commandAvailable("opencode")) {
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
+ );
344
+ }
345
+ }
346
+ function installOpenCodeMcp(home) {
347
+ if (!commandAvailable("opencode")) return void 0;
348
+ const path = join(openCodeConfigHome(home), "opencode", "opencode.json");
349
+ const config = readJsonObject(path);
350
+ const servers = config.mcp && typeof config.mcp === "object" && !Array.isArray(config.mcp) ? config.mcp : {};
351
+ const expected = {
352
+ type: "local",
353
+ command: MCP_COMMAND,
354
+ timeout: MCP_TOOL_TIMEOUT_MS
355
+ };
356
+ if (hasRequiredFields(servers[MCP_SERVER_NAME], expected)) {
357
+ return { harness: "OpenCode", path };
358
+ }
359
+ const existing = servers[MCP_SERVER_NAME] && typeof servers[MCP_SERVER_NAME] === "object" && !Array.isArray(servers[MCP_SERVER_NAME]) ? servers[MCP_SERVER_NAME] : {};
360
+ config.mcp = {
361
+ ...servers,
362
+ [MCP_SERVER_NAME]: { ...existing, ...expected }
363
+ };
364
+ writeJsonObject(path, config);
365
+ return { harness: "OpenCode", path };
366
+ }
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 };
415
+ }
109
416
  function requireCommand(command, label) {
110
417
  const result = commandResult(command, ["--version"]);
111
418
  if (result.error || result.status !== 0) {
@@ -288,7 +595,7 @@ function openBrowser(url) {
288
595
  });
289
596
  return !result.error && result.status === 0;
290
597
  }
291
- async function waitForApproval(supabaseUrl, authorization, log) {
598
+ async function waitForApproval(supabaseUrl, authorization, log, setupFlag) {
292
599
  const deadline = Date.parse(authorization.expiresAt);
293
600
  const intervalMs = Math.max(1, authorization.intervalSeconds) * 1e3;
294
601
  let announced = false;
@@ -304,16 +611,20 @@ async function waitForApproval(supabaseUrl, authorization, log) {
304
611
  }
305
612
  await delay(intervalMs);
306
613
  }
307
- throw new Error(`Authorization expired. Run \`${PACKAGE_COMMAND} setup --claude\` again.`);
614
+ throw new Error(`Authorization expired. Run \`${PACKAGE_COMMAND} setup ${setupFlag}\` again.`);
308
615
  }
309
616
  async function setup(options) {
310
617
  if (options.installClaudePlugin) verifyClaudePrerequisites();
618
+ if (options.installAgentMcp) {
619
+ verifyAgentPrerequisites();
620
+ }
311
621
  const corruptConfigBackup = recoverCorruptConfig();
312
622
  if (corruptConfigBackup) {
313
623
  options.log(`Backed up invalid configuration to ${corruptConfigBackup}; starting clean.`);
314
624
  }
315
625
  const supabaseUrl = process.env.IMPRACTICAL_SUPABASE_URL?.trim() || PRODUCTION_SUPABASE_URL;
316
626
  const appUrl = process.env.IMPRACTICAL_APP_URL?.trim() || PRODUCTION_APP_URL;
627
+ const setupFlag = options.installAgentMcp ? "--agent" : "--claude";
317
628
  const existing = loadConfig();
318
629
  let apiKey = existing.supabaseUrl === supabaseUrl ? existing.apiKey : void 0;
319
630
  let verified;
@@ -327,6 +638,7 @@ async function setup(options) {
327
638
  }
328
639
  }
329
640
  if (!apiKey) {
641
+ if (options.installAgentMcp) verifyAgentConfigs();
330
642
  const authorization = await invokeAt(supabaseUrl, "cli-device-auth", {
331
643
  action: "start"
332
644
  });
@@ -340,10 +652,10 @@ async function setup(options) {
340
652
  options.log(opened ? "Opened your browser." : "Could not open a browser automatically; use the link above.");
341
653
  }
342
654
  try {
343
- apiKey = await waitForApproval(supabaseUrl, authorization, options.log);
655
+ apiKey = await waitForApproval(supabaseUrl, authorization, options.log, setupFlag);
344
656
  } catch (error) {
345
657
  if (error instanceof ApiError && error.status === 410) {
346
- throw new Error(`Authorization expired. Run \`${PACKAGE_COMMAND} setup --claude\` again.`);
658
+ throw new Error(`Authorization expired. Run \`${PACKAGE_COMMAND} setup ${setupFlag}\` again.`);
347
659
  }
348
660
  throw error;
349
661
  }
@@ -351,6 +663,7 @@ async function setup(options) {
351
663
  saveConfig({ apiKey, supabaseUrl, appUrl });
352
664
  }
353
665
  const claudePlugin = options.installClaudePlugin ? installClaudePlugin(options.log) : void 0;
666
+ const agentMcpSetup = options.installAgentMcp ? configureAgentMcps() : { configured: [], failures: [] };
354
667
  const installed = options.installAgentSkills ? installSkills() : [];
355
668
  const reconciled = reconcileLegacySkills();
356
669
  options.log("");
@@ -358,16 +671,30 @@ async function setup(options) {
358
671
  for (const skill of installed) {
359
672
  options.log(`Installed /impractical for ${skill.harness}.`);
360
673
  }
674
+ for (const mcp of agentMcpSetup.configured) {
675
+ options.log(`Configured Impractical MCP for ${mcp.harness}.`);
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
+ }
361
681
  for (const skill of reconciled) {
362
682
  options.log(`Refreshed legacy /impractical for ${skill.harness}; previous copy preserved at ${skill.backupPath}.`);
363
683
  }
364
684
  if (claudePlugin) options.log(pluginResultLine(claudePlugin));
365
685
  options.log(`Workspace: ${verified.orgId}${verified.plan ? ` \xB7 ${verified.plan}` : ""}`);
366
686
  options.log("");
367
- options.log("Run /reload-plugins, then type continue.");
368
687
  if (options.installClaudePlugin) {
688
+ options.log("Run /reload-plugins, then type continue.");
689
+ } else if (options.installAgentMcp) {
690
+ options.log("Restart your coding agents, then ask them to use the Impractical MCP.");
691
+ }
692
+ if (options.installClaudePlugin || options.installAgentMcp) {
369
693
  for (const line of authenticatedWhoamiGuidance()) options.log(line);
370
694
  }
695
+ if (agentMcpSetup.failures.length > 0) {
696
+ throw new Error(agentMcpFailureMessage(agentMcpSetup.failures));
697
+ }
371
698
  return { installed, appUrl, supabaseUrl };
372
699
  }
373
700