@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
package/README.md
CHANGED
|
@@ -1,22 +1,34 @@
|
|
|
1
1
|
# Impractical Motion CLI
|
|
2
2
|
|
|
3
|
-
Connect Claude Code to Impractical Motion through
|
|
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
|
|
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
|
-
|
|
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
|
|
39
|
+
The installer configures supported clients to run:
|
|
28
40
|
|
|
29
41
|
```sh
|
|
30
42
|
npx --yes @impractical-ai/motion@latest mcp
|
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 ─────────────────────────────────────────────────
|
|
@@ -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
|
|
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 --
|
|
19
|
+
npx --yes @impractical-ai/motion setup --agent
|
|
20
20
|
|
|
21
|
-
Then ask them to
|
|
22
|
-
|
|
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-
|
|
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";
|
|
@@ -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
|
|
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
|
|
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
|
|