@impractical-ai/motion 0.1.3 → 0.1.5
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/impractical/SKILL.md +7 -5
- package/dist/api-KD6BNKBQ.js +30 -0
- package/dist/chunk-7724464Z.js +236 -0
- package/dist/{chunk-G56RK3HD.js → chunk-DMAQPSA4.js} +198 -306
- package/dist/chunk-JDN3QHCX.js +98 -0
- package/dist/commands-Y35XPDVH.js +1510 -0
- package/dist/index.js +84 -1554
- package/dist/{stdio-VWTZOK6D.js → stdio-ZFPBM6Z6.js} +371 -382
- package/package.json +1 -1
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
|
|
@@ -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
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ApiContractError,
|
|
4
|
+
ApiError,
|
|
5
|
+
admitMcpCall,
|
|
6
|
+
delay,
|
|
7
|
+
invoke,
|
|
8
|
+
invokeAt,
|
|
9
|
+
jobStatus,
|
|
10
|
+
studioLink,
|
|
11
|
+
validateApiResponse,
|
|
12
|
+
waitForJobs,
|
|
13
|
+
withAdmissionReceipt,
|
|
14
|
+
withMeteredCall
|
|
15
|
+
} from "./chunk-7724464Z.js";
|
|
16
|
+
import "./chunk-JDN3QHCX.js";
|
|
17
|
+
export {
|
|
18
|
+
ApiContractError,
|
|
19
|
+
ApiError,
|
|
20
|
+
admitMcpCall,
|
|
21
|
+
delay,
|
|
22
|
+
invoke,
|
|
23
|
+
invokeAt,
|
|
24
|
+
jobStatus,
|
|
25
|
+
studioLink,
|
|
26
|
+
validateApiResponse,
|
|
27
|
+
waitForJobs,
|
|
28
|
+
withAdmissionReceipt,
|
|
29
|
+
withMeteredCall
|
|
30
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
PRODUCTION_APP_URL,
|
|
4
|
+
PRODUCTION_SUPABASE_URL
|
|
5
|
+
} from "./chunk-JDN3QHCX.js";
|
|
6
|
+
|
|
7
|
+
// src/config.ts
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync, renameSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
var configDir = () => join(homedir(), ".impractical");
|
|
12
|
+
var configPath = () => join(configDir(), "config.json");
|
|
13
|
+
function recoverCorruptConfig() {
|
|
14
|
+
const path = configPath();
|
|
15
|
+
if (!existsSync(path)) return null;
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
18
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
19
|
+
throw new SyntaxError("the root value must be an object");
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (!(error instanceof SyntaxError)) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Could not inspect ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
let backupPath = `${path}.corrupt-${Date.now()}`;
|
|
29
|
+
let suffix = 1;
|
|
30
|
+
while (existsSync(backupPath)) backupPath = `${path}.corrupt-${Date.now()}-${suffix++}`;
|
|
31
|
+
renameSync(path, backupPath);
|
|
32
|
+
return backupPath;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function readStoredConfig() {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(readFileSync(configPath(), "utf8"));
|
|
38
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
39
|
+
throw new SyntaxError("the root value must be an object");
|
|
40
|
+
}
|
|
41
|
+
return parsed;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error?.code === "ENOENT") return {};
|
|
44
|
+
if (error instanceof SyntaxError) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Invalid JSON in ${configPath()}: ${error.message}
|
|
47
|
+
Run: npx --yes @impractical-ai/motion setup --agent`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Could not read ${configPath()}: ${error instanceof Error ? error.message : String(error)}`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function loadConfig() {
|
|
56
|
+
const stored = readStoredConfig();
|
|
57
|
+
return {
|
|
58
|
+
apiKey: process.env.IMPRACTICAL_API_KEY ?? stored.apiKey,
|
|
59
|
+
supabaseUrl: process.env.IMPRACTICAL_SUPABASE_URL ?? stored.supabaseUrl ?? PRODUCTION_SUPABASE_URL,
|
|
60
|
+
appUrl: process.env.IMPRACTICAL_APP_URL ?? stored.appUrl ?? PRODUCTION_APP_URL
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function saveConfig(patch) {
|
|
64
|
+
const stored = readStoredConfig();
|
|
65
|
+
const next = { ...stored, ...patch };
|
|
66
|
+
mkdirSync(configDir(), { recursive: true });
|
|
67
|
+
writeFileSync(configPath(), JSON.stringify(next, null, 2) + "\n");
|
|
68
|
+
chmodSync(configPath(), 384);
|
|
69
|
+
return next;
|
|
70
|
+
}
|
|
71
|
+
function requireConfig() {
|
|
72
|
+
const config = loadConfig();
|
|
73
|
+
if (!config.supabaseUrl) {
|
|
74
|
+
throw new Error("Not configured. Run: npx --yes @impractical-ai/motion setup --agent");
|
|
75
|
+
}
|
|
76
|
+
if (!config.apiKey) {
|
|
77
|
+
throw new Error("Not connected. Run: npx --yes @impractical-ai/motion setup --agent");
|
|
78
|
+
}
|
|
79
|
+
return config;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/api.ts
|
|
83
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
84
|
+
import { randomUUID } from "node:crypto";
|
|
85
|
+
var admissionContext = new AsyncLocalStorage();
|
|
86
|
+
var ApiError = class extends Error {
|
|
87
|
+
constructor(status, message, fn, details) {
|
|
88
|
+
super(message);
|
|
89
|
+
this.status = status;
|
|
90
|
+
this.fn = fn;
|
|
91
|
+
this.details = details;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
var ApiContractError = class extends Error {
|
|
95
|
+
constructor(fn, expected) {
|
|
96
|
+
super(`${fn} returned an invalid response: expected ${expected}. Please retry; if this persists, update Impractical Motion or contact support.`);
|
|
97
|
+
this.fn = fn;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
function isRecord(value) {
|
|
101
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
102
|
+
}
|
|
103
|
+
function bodyRecord(body) {
|
|
104
|
+
return isRecord(body) ? body : {};
|
|
105
|
+
}
|
|
106
|
+
function validateApiResponse(name, body, data) {
|
|
107
|
+
if (!isRecord(data)) throw new ApiContractError(name, "a JSON object");
|
|
108
|
+
const input = bodyRecord(body);
|
|
109
|
+
if (name === "workspace-bootstrap") {
|
|
110
|
+
const billing = data.billing;
|
|
111
|
+
if (typeof data.orgId !== "string" || typeof data.plan !== "string" || typeof data.hasAccess !== "boolean" || !isRecord(billing) || typeof billing.status !== "string") {
|
|
112
|
+
throw new ApiContractError(
|
|
113
|
+
name,
|
|
114
|
+
"an orgId, plan, hasAccess flag, and billing status"
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (name === "list-projects") {
|
|
119
|
+
const key = input.projectId ? "project" : "projects";
|
|
120
|
+
const value = data[key];
|
|
121
|
+
if (key === "projects" ? !Array.isArray(value) : !isRecord(value)) {
|
|
122
|
+
throw new ApiContractError(name, `an object with a ${key} ${key === "projects" ? "array" : "object"}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (name === "prepare-generation") {
|
|
126
|
+
if (typeof data.projectId !== "string" || !Array.isArray(data.drafts) || data.drafts.length === 0 || !isRecord(data.drafts[0]) || typeof data.drafts[0].id !== "string") {
|
|
127
|
+
throw new ApiContractError(
|
|
128
|
+
name,
|
|
129
|
+
"a projectId and at least one draft with an id"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (name === "get-draft" && (!isRecord(data.files) || typeof data.draftId !== "string")) {
|
|
134
|
+
throw new ApiContractError(name, "draft metadata and a files object");
|
|
135
|
+
}
|
|
136
|
+
if (name === "save-draft" && typeof data.revision !== "number") {
|
|
137
|
+
throw new ApiContractError(name, "a numeric revision");
|
|
138
|
+
}
|
|
139
|
+
if (name === "job-status" && !Array.isArray(data.jobs)) {
|
|
140
|
+
throw new ApiContractError(name, "an object with a jobs array");
|
|
141
|
+
}
|
|
142
|
+
if (name === "start-export" && typeof data.jobId !== "string") {
|
|
143
|
+
throw new ApiContractError(name, "an object with a jobId");
|
|
144
|
+
}
|
|
145
|
+
if (name === "brand-kits") {
|
|
146
|
+
const key = input.action === "list" ? "brands" : input.action === "get" ? "brand" : null;
|
|
147
|
+
if (key && (key === "brands" ? !Array.isArray(data[key]) : !isRecord(data[key]))) {
|
|
148
|
+
throw new ApiContractError(name, `an object with a ${key} ${key === "brands" ? "array" : "object"}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return data;
|
|
152
|
+
}
|
|
153
|
+
async function invoke(name, body = {}) {
|
|
154
|
+
const { apiKey, supabaseUrl } = requireConfig();
|
|
155
|
+
const receipt = admissionContext.getStore();
|
|
156
|
+
return invokeAt(supabaseUrl, name, body, apiKey, receipt);
|
|
157
|
+
}
|
|
158
|
+
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
|
+
});
|
|
168
|
+
const text = await response.text();
|
|
169
|
+
let data = null;
|
|
170
|
+
try {
|
|
171
|
+
data = text ? JSON.parse(text) : null;
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
if (!response.ok) {
|
|
175
|
+
const details = isRecord(data) ? data : void 0;
|
|
176
|
+
const message = typeof details?.message === "string" && details.message || typeof details?.error === "string" && details.error || text || `${name} failed (${response.status})`;
|
|
177
|
+
throw new ApiError(response.status, message, name, details);
|
|
178
|
+
}
|
|
179
|
+
return validateApiResponse(name, body, data);
|
|
180
|
+
}
|
|
181
|
+
async function admitMcpCall(toolName, callId = randomUUID()) {
|
|
182
|
+
const { apiKey, supabaseUrl } = requireConfig();
|
|
183
|
+
return invokeAt(supabaseUrl, "mcp-admission", { callId, toolName }, apiKey);
|
|
184
|
+
}
|
|
185
|
+
function withAdmissionReceipt(receipt, task) {
|
|
186
|
+
return admissionContext.run(receipt, task);
|
|
187
|
+
}
|
|
188
|
+
async function withMeteredCall(toolName, task) {
|
|
189
|
+
const admission = await admitMcpCall(toolName);
|
|
190
|
+
return withAdmissionReceipt(admission.receipt, task);
|
|
191
|
+
}
|
|
192
|
+
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
193
|
+
async function jobStatus(jobIds) {
|
|
194
|
+
const result = await invoke("job-status", { jobIds });
|
|
195
|
+
return result.jobs;
|
|
196
|
+
}
|
|
197
|
+
async function waitForJobs(jobIds, onTick, deadlineMs = 72e5) {
|
|
198
|
+
const deadline = Date.now() + deadlineMs;
|
|
199
|
+
const terminal = /* @__PURE__ */ new Set();
|
|
200
|
+
let latest = [];
|
|
201
|
+
while (terminal.size < jobIds.length) {
|
|
202
|
+
if (Date.now() >= deadline) throw new Error("Timed out waiting for jobs");
|
|
203
|
+
latest = await jobStatus(jobIds);
|
|
204
|
+
for (const row of latest) {
|
|
205
|
+
if (row.status === "completed" || row.status === "failed" || row.status === "canceled") {
|
|
206
|
+
terminal.add(row.id);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
onTick(latest);
|
|
210
|
+
if (terminal.size < jobIds.length) await delay(2e3);
|
|
211
|
+
}
|
|
212
|
+
return latest;
|
|
213
|
+
}
|
|
214
|
+
function studioLink(appUrl, projectId, slug) {
|
|
215
|
+
const base = (appUrl ?? "http://localhost:5173").replace(/\/$/, "");
|
|
216
|
+
return `${base}/projects/${projectId}/studio${slug ? `?scene=${slug}` : ""}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export {
|
|
220
|
+
recoverCorruptConfig,
|
|
221
|
+
loadConfig,
|
|
222
|
+
saveConfig,
|
|
223
|
+
requireConfig,
|
|
224
|
+
ApiError,
|
|
225
|
+
ApiContractError,
|
|
226
|
+
validateApiResponse,
|
|
227
|
+
invoke,
|
|
228
|
+
invokeAt,
|
|
229
|
+
admitMcpCall,
|
|
230
|
+
withAdmissionReceipt,
|
|
231
|
+
withMeteredCall,
|
|
232
|
+
delay,
|
|
233
|
+
jobStatus,
|
|
234
|
+
waitForJobs,
|
|
235
|
+
studioLink
|
|
236
|
+
};
|