@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
|
@@ -1,260 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
for (let key of __getOwnPropNames(from))
|
|
18
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
-
}
|
|
21
|
-
return to;
|
|
22
|
-
};
|
|
23
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
-
mod
|
|
30
|
-
));
|
|
31
|
-
|
|
32
|
-
// src/constants.ts
|
|
33
|
-
var PRODUCTION_APP_URL = "https://motion.impractical.ai";
|
|
34
|
-
var PRODUCTION_SUPABASE_URL = "https://uaxjweednprtbhbceify.supabase.co";
|
|
35
|
-
var PACKAGE_NAME = "@impractical-ai/motion";
|
|
36
|
-
var PACKAGE_VERSION = "0.1.2";
|
|
37
|
-
var PACKAGE_COMMAND = `npx --yes ${PACKAGE_NAME}`;
|
|
38
|
-
|
|
39
|
-
// src/config.ts
|
|
40
|
-
import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync, renameSync } from "node:fs";
|
|
41
|
-
import { homedir } from "node:os";
|
|
42
|
-
import { join } from "node:path";
|
|
43
|
-
var configDir = () => join(homedir(), ".impractical");
|
|
44
|
-
var configPath = () => join(configDir(), "config.json");
|
|
45
|
-
function recoverCorruptConfig() {
|
|
46
|
-
const path = configPath();
|
|
47
|
-
if (!existsSync(path)) return null;
|
|
48
|
-
try {
|
|
49
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
50
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
51
|
-
throw new SyntaxError("the root value must be an object");
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
} catch (error) {
|
|
55
|
-
if (!(error instanceof SyntaxError)) {
|
|
56
|
-
throw new Error(
|
|
57
|
-
`Could not inspect ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
let backupPath = `${path}.corrupt-${Date.now()}`;
|
|
61
|
-
let suffix = 1;
|
|
62
|
-
while (existsSync(backupPath)) backupPath = `${path}.corrupt-${Date.now()}-${suffix++}`;
|
|
63
|
-
renameSync(path, backupPath);
|
|
64
|
-
return backupPath;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
function readStoredConfig() {
|
|
68
|
-
try {
|
|
69
|
-
const parsed = JSON.parse(readFileSync(configPath(), "utf8"));
|
|
70
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
71
|
-
throw new SyntaxError("the root value must be an object");
|
|
72
|
-
}
|
|
73
|
-
return parsed;
|
|
74
|
-
} catch (error) {
|
|
75
|
-
if (error?.code === "ENOENT") return {};
|
|
76
|
-
if (error instanceof SyntaxError) {
|
|
77
|
-
throw new Error(`Invalid JSON in ${configPath()}: ${error.message}`);
|
|
78
|
-
}
|
|
79
|
-
throw new Error(
|
|
80
|
-
`Could not read ${configPath()}: ${error instanceof Error ? error.message : String(error)}`
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
function loadConfig() {
|
|
85
|
-
const stored = readStoredConfig();
|
|
86
|
-
return {
|
|
87
|
-
apiKey: process.env.IMPRACTICAL_API_KEY ?? stored.apiKey,
|
|
88
|
-
supabaseUrl: process.env.IMPRACTICAL_SUPABASE_URL ?? stored.supabaseUrl ?? PRODUCTION_SUPABASE_URL,
|
|
89
|
-
appUrl: process.env.IMPRACTICAL_APP_URL ?? stored.appUrl ?? PRODUCTION_APP_URL
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
function saveConfig(patch) {
|
|
93
|
-
const stored = readStoredConfig();
|
|
94
|
-
const next = { ...stored, ...patch };
|
|
95
|
-
mkdirSync(configDir(), { recursive: true });
|
|
96
|
-
writeFileSync(configPath(), JSON.stringify(next, null, 2) + "\n");
|
|
97
|
-
chmodSync(configPath(), 384);
|
|
98
|
-
return next;
|
|
99
|
-
}
|
|
100
|
-
function requireConfig() {
|
|
101
|
-
const config = loadConfig();
|
|
102
|
-
if (!config.supabaseUrl) {
|
|
103
|
-
throw new Error("Not configured. Run: npx --yes @impractical-ai/motion setup --claude");
|
|
104
|
-
}
|
|
105
|
-
if (!config.apiKey) {
|
|
106
|
-
throw new Error("Not connected. Run: npx --yes @impractical-ai/motion setup --claude");
|
|
107
|
-
}
|
|
108
|
-
return config;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// src/api.ts
|
|
112
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
113
|
-
import { randomUUID } from "node:crypto";
|
|
114
|
-
var admissionContext = new AsyncLocalStorage();
|
|
115
|
-
var ApiError = class extends Error {
|
|
116
|
-
constructor(status, message, fn, details) {
|
|
117
|
-
super(message);
|
|
118
|
-
this.status = status;
|
|
119
|
-
this.fn = fn;
|
|
120
|
-
this.details = details;
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
var ApiContractError = class extends Error {
|
|
124
|
-
constructor(fn, expected) {
|
|
125
|
-
super(`${fn} returned an invalid response: expected ${expected}. Please retry; if this persists, update Impractical Motion or contact support.`);
|
|
126
|
-
this.fn = fn;
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
function isRecord(value) {
|
|
130
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
131
|
-
}
|
|
132
|
-
function bodyRecord(body) {
|
|
133
|
-
return isRecord(body) ? body : {};
|
|
134
|
-
}
|
|
135
|
-
function validateApiResponse(name, body, data) {
|
|
136
|
-
if (!isRecord(data)) throw new ApiContractError(name, "a JSON object");
|
|
137
|
-
const input = bodyRecord(body);
|
|
138
|
-
if (name === "workspace-bootstrap") {
|
|
139
|
-
const billing = data.billing;
|
|
140
|
-
if (typeof data.orgId !== "string" || typeof data.plan !== "string" || typeof data.hasAccess !== "boolean" || !isRecord(billing) || typeof billing.status !== "string") {
|
|
141
|
-
throw new ApiContractError(
|
|
142
|
-
name,
|
|
143
|
-
"an orgId, plan, hasAccess flag, and billing status"
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
if (name === "list-projects") {
|
|
148
|
-
const key = input.projectId ? "project" : "projects";
|
|
149
|
-
const value = data[key];
|
|
150
|
-
if (key === "projects" ? !Array.isArray(value) : !isRecord(value)) {
|
|
151
|
-
throw new ApiContractError(name, `an object with a ${key} ${key === "projects" ? "array" : "object"}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
if (name === "prepare-generation") {
|
|
155
|
-
if (typeof data.projectId !== "string" || !Array.isArray(data.drafts) || data.drafts.length === 0 || !isRecord(data.drafts[0]) || typeof data.drafts[0].id !== "string") {
|
|
156
|
-
throw new ApiContractError(
|
|
157
|
-
name,
|
|
158
|
-
"a projectId and at least one draft with an id"
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (name === "get-draft" && (!isRecord(data.files) || typeof data.draftId !== "string")) {
|
|
163
|
-
throw new ApiContractError(name, "draft metadata and a files object");
|
|
164
|
-
}
|
|
165
|
-
if (name === "save-draft" && typeof data.revision !== "number") {
|
|
166
|
-
throw new ApiContractError(name, "a numeric revision");
|
|
167
|
-
}
|
|
168
|
-
if (name === "job-status" && !Array.isArray(data.jobs)) {
|
|
169
|
-
throw new ApiContractError(name, "an object with a jobs array");
|
|
170
|
-
}
|
|
171
|
-
if (name === "start-export" && typeof data.jobId !== "string") {
|
|
172
|
-
throw new ApiContractError(name, "an object with a jobId");
|
|
173
|
-
}
|
|
174
|
-
if (name === "brand-kits") {
|
|
175
|
-
const key = input.action === "list" ? "brands" : input.action === "get" ? "brand" : null;
|
|
176
|
-
if (key && (key === "brands" ? !Array.isArray(data[key]) : !isRecord(data[key]))) {
|
|
177
|
-
throw new ApiContractError(name, `an object with a ${key} ${key === "brands" ? "array" : "object"}`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return data;
|
|
181
|
-
}
|
|
182
|
-
async function invoke(name, body = {}) {
|
|
183
|
-
const { apiKey, supabaseUrl } = requireConfig();
|
|
184
|
-
const receipt = admissionContext.getStore();
|
|
185
|
-
return invokeAt(supabaseUrl, name, body, apiKey, receipt);
|
|
186
|
-
}
|
|
187
|
-
async function invokeAt(supabaseUrl, name, body = {}, apiKey, admissionReceipt) {
|
|
188
|
-
const response = await fetch(`${supabaseUrl.replace(/\/$/, "")}/functions/v1/${name}`, {
|
|
189
|
-
method: "POST",
|
|
190
|
-
headers: {
|
|
191
|
-
...apiKey ? { authorization: `Bearer ${apiKey}` } : {},
|
|
192
|
-
...admissionReceipt ? { "x-impractical-admission": admissionReceipt } : {},
|
|
193
|
-
"content-type": "application/json"
|
|
194
|
-
},
|
|
195
|
-
body: JSON.stringify(body)
|
|
196
|
-
});
|
|
197
|
-
const text = await response.text();
|
|
198
|
-
let data = null;
|
|
199
|
-
try {
|
|
200
|
-
data = text ? JSON.parse(text) : null;
|
|
201
|
-
} catch {
|
|
202
|
-
}
|
|
203
|
-
if (!response.ok) {
|
|
204
|
-
const details = isRecord(data) ? data : void 0;
|
|
205
|
-
const message = typeof details?.message === "string" && details.message || typeof details?.error === "string" && details.error || text || `${name} failed (${response.status})`;
|
|
206
|
-
throw new ApiError(response.status, message, name, details);
|
|
207
|
-
}
|
|
208
|
-
return validateApiResponse(name, body, data);
|
|
209
|
-
}
|
|
210
|
-
async function admitMcpCall(toolName, callId = randomUUID()) {
|
|
211
|
-
const { apiKey, supabaseUrl } = requireConfig();
|
|
212
|
-
return invokeAt(supabaseUrl, "mcp-admission", { callId, toolName }, apiKey);
|
|
213
|
-
}
|
|
214
|
-
function withAdmissionReceipt(receipt, task) {
|
|
215
|
-
return admissionContext.run(receipt, task);
|
|
216
|
-
}
|
|
217
|
-
async function withMeteredCall(toolName, task) {
|
|
218
|
-
const admission = await admitMcpCall(toolName);
|
|
219
|
-
return withAdmissionReceipt(admission.receipt, task);
|
|
220
|
-
}
|
|
221
|
-
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
222
|
-
async function jobStatus(jobIds) {
|
|
223
|
-
const result = await invoke("job-status", { jobIds });
|
|
224
|
-
return result.jobs;
|
|
225
|
-
}
|
|
226
|
-
async function waitForJobs(jobIds, onTick, deadlineMs = 72e5) {
|
|
227
|
-
const deadline = Date.now() + deadlineMs;
|
|
228
|
-
const terminal = /* @__PURE__ */ new Set();
|
|
229
|
-
let latest = [];
|
|
230
|
-
while (terminal.size < jobIds.length) {
|
|
231
|
-
if (Date.now() >= deadline) throw new Error("Timed out waiting for jobs");
|
|
232
|
-
latest = await jobStatus(jobIds);
|
|
233
|
-
for (const row of latest) {
|
|
234
|
-
if (row.status === "completed" || row.status === "failed" || row.status === "canceled") {
|
|
235
|
-
terminal.add(row.id);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
onTick(latest);
|
|
239
|
-
if (terminal.size < jobIds.length) await delay(2e3);
|
|
240
|
-
}
|
|
241
|
-
return latest;
|
|
242
|
-
}
|
|
243
|
-
function studioLink(appUrl, projectId, slug) {
|
|
244
|
-
const base = (appUrl ?? "http://localhost:5173").replace(/\/$/, "");
|
|
245
|
-
return `${base}/projects/${projectId}/studio${slug ? `?scene=${slug}` : ""}`;
|
|
246
|
-
}
|
|
2
|
+
import {
|
|
3
|
+
ApiError,
|
|
4
|
+
delay,
|
|
5
|
+
invokeAt,
|
|
6
|
+
loadConfig,
|
|
7
|
+
recoverCorruptConfig,
|
|
8
|
+
saveConfig
|
|
9
|
+
} from "./chunk-7724464Z.js";
|
|
10
|
+
import {
|
|
11
|
+
PACKAGE_COMMAND,
|
|
12
|
+
PACKAGE_NAME,
|
|
13
|
+
PACKAGE_VERSION,
|
|
14
|
+
PRODUCTION_APP_URL,
|
|
15
|
+
PRODUCTION_SUPABASE_URL
|
|
16
|
+
} from "./chunk-JDN3QHCX.js";
|
|
247
17
|
|
|
248
18
|
// src/onboarding.ts
|
|
249
|
-
import { existsSync
|
|
250
|
-
import { homedir
|
|
251
|
-
import { dirname, join
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
252
22
|
import { fileURLToPath } from "node:url";
|
|
253
23
|
import { spawnSync } from "node:child_process";
|
|
254
24
|
var CLAUDE_MARKETPLACE = "Openpod/impractical-agent-plugins";
|
|
255
25
|
var CLAUDE_PLUGIN = "impractical-motion@impractical";
|
|
256
26
|
var CLAUDE_MARKETPLACE_NAME = "impractical";
|
|
257
27
|
var AUTH_VERIFY_MIN_VERSION = "0.1.2";
|
|
28
|
+
var CLAUDE_INSTALL_COMMAND = "npm install -g @anthropic-ai/claude-code";
|
|
29
|
+
var CLAUDE_INSTALL_URL = "https://code.claude.com/docs/en/installation";
|
|
30
|
+
var MCP_SERVER_NAME = "impractical";
|
|
31
|
+
var MCP_COMMAND = ["npx", "--yes", `${PACKAGE_NAME}@latest`, "mcp"];
|
|
258
32
|
function parseSemver(value) {
|
|
259
33
|
const match = value.trim().match(
|
|
260
34
|
/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
|
|
@@ -294,20 +68,20 @@ function compareSemver(left, right) {
|
|
|
294
68
|
return 0;
|
|
295
69
|
}
|
|
296
70
|
function readJson(path) {
|
|
297
|
-
if (!
|
|
71
|
+
if (!existsSync(path)) return void 0;
|
|
298
72
|
try {
|
|
299
|
-
return JSON.parse(
|
|
73
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
300
74
|
} catch {
|
|
301
75
|
return void 0;
|
|
302
76
|
}
|
|
303
77
|
}
|
|
304
|
-
var claudeConfigDir = () => process.env.CLAUDE_CONFIG_DIR?.trim() ||
|
|
305
|
-
function marketplaceRecord(
|
|
306
|
-
const value = readJson(
|
|
78
|
+
var claudeConfigDir = () => process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude");
|
|
79
|
+
function marketplaceRecord(configDir) {
|
|
80
|
+
const value = readJson(join(configDir, "plugins", "known_marketplaces.json"));
|
|
307
81
|
return value?.impractical;
|
|
308
82
|
}
|
|
309
|
-
function installedPluginVersion(
|
|
310
|
-
const value = readJson(
|
|
83
|
+
function installedPluginVersion(configDir) {
|
|
84
|
+
const value = readJson(join(configDir, "plugins", "installed_plugins.json"));
|
|
311
85
|
const entries = value?.plugins?.[CLAUDE_PLUGIN];
|
|
312
86
|
if (!Array.isArray(entries)) return void 0;
|
|
313
87
|
const versions = entries.filter((entry) => entry?.scope === "user" && typeof entry.version === "string").map((entry) => entry.version);
|
|
@@ -317,10 +91,10 @@ function installedPluginVersion(configDir2) {
|
|
|
317
91
|
return comparison !== void 0 && comparison > 0 ? version : highest;
|
|
318
92
|
}, void 0);
|
|
319
93
|
}
|
|
320
|
-
function marketplacePluginVersion(
|
|
321
|
-
const record = marketplaceRecord(
|
|
322
|
-
const location = typeof record?.installLocation === "string" ? record.installLocation :
|
|
323
|
-
const value = readJson(
|
|
94
|
+
function marketplacePluginVersion(configDir) {
|
|
95
|
+
const record = marketplaceRecord(configDir);
|
|
96
|
+
const location = typeof record?.installLocation === "string" ? record.installLocation : join(configDir, "plugins", "marketplaces", CLAUDE_MARKETPLACE_NAME);
|
|
97
|
+
const value = readJson(join(location, ".claude-plugin", "marketplace.json"));
|
|
324
98
|
return value?.plugins?.find((plugin) => plugin.name === "impractical-motion")?.version;
|
|
325
99
|
}
|
|
326
100
|
function expectedMarketplace(record) {
|
|
@@ -334,9 +108,133 @@ function marketplaceSourceLabel(record) {
|
|
|
334
108
|
function commandResult(command, args) {
|
|
335
109
|
return spawnSync(command, args, { encoding: "utf8", windowsHide: true });
|
|
336
110
|
}
|
|
111
|
+
function commandAvailable(command) {
|
|
112
|
+
const result = commandResult(command, ["--version"]);
|
|
113
|
+
return !result.error && result.status === 0;
|
|
114
|
+
}
|
|
115
|
+
function verifyAgentPrerequisites() {
|
|
116
|
+
const found = commandAvailable("claude") || commandAvailable("codex") || commandAvailable("cursor-agent") || commandAvailable("cursor") || commandAvailable("opencode");
|
|
117
|
+
if (!found) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
"No supported coding agent was detected. Install Claude Code, Codex, Cursor, or OpenCode, then rerun setup."
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function commandDetail(result) {
|
|
124
|
+
return `${result.stdout ?? ""}
|
|
125
|
+
${result.stderr ?? ""}`.trim();
|
|
126
|
+
}
|
|
127
|
+
function runMcpCommand(command, args, harness) {
|
|
128
|
+
const result = commandResult(command, args);
|
|
129
|
+
if (!result.error && result.status === 0) return;
|
|
130
|
+
throw new Error(
|
|
131
|
+
`${harness} MCP setup failed: ${commandDetail(result) || result.error?.message || "unknown error"}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
function hasExpectedMcp(result) {
|
|
135
|
+
if (result.error || result.status !== 0) return false;
|
|
136
|
+
const detail = commandDetail(result);
|
|
137
|
+
return detail.includes("@impractical-ai/motion@latest") && /\bmcp\b/.test(detail);
|
|
138
|
+
}
|
|
139
|
+
function installNativeMcp(command, harness) {
|
|
140
|
+
if (!commandAvailable(command)) return void 0;
|
|
141
|
+
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);
|
|
145
|
+
}
|
|
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);
|
|
148
|
+
}
|
|
149
|
+
return { harness };
|
|
150
|
+
}
|
|
151
|
+
function readJsonObject(path) {
|
|
152
|
+
if (!existsSync(path)) return {};
|
|
153
|
+
let value;
|
|
154
|
+
try {
|
|
155
|
+
value = JSON.parse(readFileSync(path, "utf8"));
|
|
156
|
+
} catch {
|
|
157
|
+
throw new Error(`Cannot configure MCP because ${path} is not valid JSON.`);
|
|
158
|
+
}
|
|
159
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
160
|
+
throw new Error(`Cannot configure MCP because ${path} must contain a JSON object.`);
|
|
161
|
+
}
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
function writeJsonObject(path, value) {
|
|
165
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
166
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}
|
|
167
|
+
`, { mode: 384 });
|
|
168
|
+
}
|
|
169
|
+
function installCursorMcp(home) {
|
|
170
|
+
if (!commandAvailable("cursor-agent") && !commandAvailable("cursor")) return void 0;
|
|
171
|
+
const path = join(home, ".cursor", "mcp.json");
|
|
172
|
+
const config = readJsonObject(path);
|
|
173
|
+
const servers = config.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers) ? config.mcpServers : {};
|
|
174
|
+
const expected = {
|
|
175
|
+
command: MCP_COMMAND[0],
|
|
176
|
+
args: MCP_COMMAND.slice(1)
|
|
177
|
+
};
|
|
178
|
+
if (JSON.stringify(servers[MCP_SERVER_NAME]) === JSON.stringify(expected)) {
|
|
179
|
+
return { harness: "Cursor", path };
|
|
180
|
+
}
|
|
181
|
+
config.mcpServers = {
|
|
182
|
+
...servers,
|
|
183
|
+
[MCP_SERVER_NAME]: expected
|
|
184
|
+
};
|
|
185
|
+
writeJsonObject(path, config);
|
|
186
|
+
return { harness: "Cursor", path };
|
|
187
|
+
}
|
|
188
|
+
function openCodeConfigHome(home) {
|
|
189
|
+
if (process.env.XDG_CONFIG_HOME?.trim()) return process.env.XDG_CONFIG_HOME.trim();
|
|
190
|
+
if (process.platform === "win32" && process.env.APPDATA?.trim()) return process.env.APPDATA.trim();
|
|
191
|
+
return join(home, ".config");
|
|
192
|
+
}
|
|
193
|
+
function verifyAgentConfigs(home = homedir()) {
|
|
194
|
+
if (commandAvailable("cursor-agent") || commandAvailable("cursor")) {
|
|
195
|
+
readJsonObject(join(home, ".cursor", "mcp.json"));
|
|
196
|
+
}
|
|
197
|
+
if (commandAvailable("opencode")) {
|
|
198
|
+
readJsonObject(join(openCodeConfigHome(home), "opencode", "opencode.json"));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function installOpenCodeMcp(home) {
|
|
202
|
+
if (!commandAvailable("opencode")) return void 0;
|
|
203
|
+
const path = join(openCodeConfigHome(home), "opencode", "opencode.json");
|
|
204
|
+
const config = readJsonObject(path);
|
|
205
|
+
const servers = config.mcp && typeof config.mcp === "object" && !Array.isArray(config.mcp) ? config.mcp : {};
|
|
206
|
+
const expected = {
|
|
207
|
+
type: "local",
|
|
208
|
+
command: MCP_COMMAND
|
|
209
|
+
};
|
|
210
|
+
if (JSON.stringify(servers[MCP_SERVER_NAME]) === JSON.stringify(expected)) {
|
|
211
|
+
return { harness: "OpenCode", path };
|
|
212
|
+
}
|
|
213
|
+
config.mcp = {
|
|
214
|
+
...servers,
|
|
215
|
+
[MCP_SERVER_NAME]: expected
|
|
216
|
+
};
|
|
217
|
+
writeJsonObject(path, config);
|
|
218
|
+
return { harness: "OpenCode", path };
|
|
219
|
+
}
|
|
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));
|
|
227
|
+
}
|
|
337
228
|
function requireCommand(command, label) {
|
|
338
229
|
const result = commandResult(command, ["--version"]);
|
|
339
230
|
if (result.error || result.status !== 0) {
|
|
231
|
+
if (command === "claude") {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`${label} is not installed. Install it, then rerun setup:
|
|
234
|
+
${CLAUDE_INSTALL_COMMAND}
|
|
235
|
+
Official setup guide: ${CLAUDE_INSTALL_URL}`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
340
238
|
throw new Error(`${label} is required before setup can continue.`);
|
|
341
239
|
}
|
|
342
240
|
}
|
|
@@ -345,7 +243,7 @@ function hasChrome() {
|
|
|
345
243
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
346
244
|
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
347
245
|
];
|
|
348
|
-
if (macPaths.some(
|
|
246
|
+
if (macPaths.some(existsSync)) return true;
|
|
349
247
|
return ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"].some((command) => {
|
|
350
248
|
const result = spawnSync(command, ["--version"], { stdio: "ignore", windowsHide: true });
|
|
351
249
|
return !result.error && result.status === 0;
|
|
@@ -381,9 +279,9 @@ ${result.stderr ?? ""}`.trim();
|
|
|
381
279
|
}
|
|
382
280
|
function installClaudePlugin(log = () => {
|
|
383
281
|
}) {
|
|
384
|
-
const
|
|
385
|
-
const beforeVersion = installedPluginVersion(
|
|
386
|
-
let record = marketplaceRecord(
|
|
282
|
+
const configDir = claudeConfigDir();
|
|
283
|
+
const beforeVersion = installedPluginVersion(configDir);
|
|
284
|
+
let record = marketplaceRecord(configDir);
|
|
387
285
|
const preserveSource = Boolean(record && !expectedMarketplace(record));
|
|
388
286
|
if (preserveSource) {
|
|
389
287
|
log(
|
|
@@ -391,12 +289,12 @@ function installClaudePlugin(log = () => {
|
|
|
391
289
|
);
|
|
392
290
|
} else if (!record) {
|
|
393
291
|
runClaude(["plugin", "marketplace", "add", CLAUDE_MARKETPLACE], log, true);
|
|
394
|
-
record = marketplaceRecord(
|
|
292
|
+
record = marketplaceRecord(configDir);
|
|
395
293
|
}
|
|
396
294
|
if (!beforeVersion) {
|
|
397
295
|
runClaude(["plugin", "install", CLAUDE_PLUGIN, "--scope", "user"], log, true);
|
|
398
296
|
} else if (!preserveSource) {
|
|
399
|
-
const advertisedVersion = marketplacePluginVersion(
|
|
297
|
+
const advertisedVersion = marketplacePluginVersion(configDir);
|
|
400
298
|
const comparison = advertisedVersion ? compareSemver(beforeVersion, advertisedVersion) : void 0;
|
|
401
299
|
if (comparison === void 0) {
|
|
402
300
|
log(
|
|
@@ -408,8 +306,8 @@ function installClaudePlugin(log = () => {
|
|
|
408
306
|
);
|
|
409
307
|
} else {
|
|
410
308
|
runClaude(["plugin", "marketplace", "update", CLAUDE_MARKETPLACE_NAME], log, true);
|
|
411
|
-
const refreshedVersion = marketplacePluginVersion(
|
|
412
|
-
const currentVersion = installedPluginVersion(
|
|
309
|
+
const refreshedVersion = marketplacePluginVersion(configDir);
|
|
310
|
+
const currentVersion = installedPluginVersion(configDir) ?? beforeVersion;
|
|
413
311
|
const refreshedComparison = refreshedVersion ? compareSemver(currentVersion, refreshedVersion) : void 0;
|
|
414
312
|
if (refreshedComparison !== void 0 && refreshedComparison < 0) {
|
|
415
313
|
runClaude(["plugin", "update", CLAUDE_PLUGIN], log, true);
|
|
@@ -420,7 +318,7 @@ function installClaudePlugin(log = () => {
|
|
|
420
318
|
}
|
|
421
319
|
}
|
|
422
320
|
}
|
|
423
|
-
const afterVersion = installedPluginVersion(
|
|
321
|
+
const afterVersion = installedPluginVersion(configDir);
|
|
424
322
|
const change = beforeVersion && afterVersion ? compareSemver(afterVersion, beforeVersion) : void 0;
|
|
425
323
|
if (change !== void 0 && change < 0) {
|
|
426
324
|
log(`WARNING: ${CLAUDE_PLUGIN} resolved to ${afterVersion}, below its prior ${beforeVersion}.`);
|
|
@@ -454,44 +352,44 @@ function pluginResultLine(result) {
|
|
|
454
352
|
return `${CLAUDE_PLUGIN} already at ${result.afterVersion}; left unchanged.`;
|
|
455
353
|
}
|
|
456
354
|
var skillSourcePath = () => fileURLToPath(new URL("../assets/impractical/SKILL.md", import.meta.url));
|
|
457
|
-
function installSkills(home =
|
|
458
|
-
const source =
|
|
355
|
+
function installSkills(home = homedir(), sourcePath = skillSourcePath()) {
|
|
356
|
+
const source = readFileSync(sourcePath, "utf8");
|
|
459
357
|
const targets = [
|
|
460
358
|
{
|
|
461
359
|
harness: "Claude Code",
|
|
462
|
-
path:
|
|
360
|
+
path: join(home, ".claude", "skills", "impractical", "SKILL.md")
|
|
463
361
|
},
|
|
464
362
|
{
|
|
465
363
|
harness: "Codex",
|
|
466
|
-
path:
|
|
364
|
+
path: join(home, ".codex", "skills", "impractical", "SKILL.md")
|
|
467
365
|
}
|
|
468
366
|
];
|
|
469
367
|
for (const target of targets) {
|
|
470
|
-
|
|
471
|
-
|
|
368
|
+
mkdirSync(dirname(target.path), { recursive: true, mode: 448 });
|
|
369
|
+
writeFileSync(target.path, source, { mode: 420 });
|
|
472
370
|
}
|
|
473
371
|
return targets;
|
|
474
372
|
}
|
|
475
|
-
function reconcileLegacySkills(home =
|
|
476
|
-
const source =
|
|
373
|
+
function reconcileLegacySkills(home = homedir(), sourcePath = skillSourcePath()) {
|
|
374
|
+
const source = readFileSync(sourcePath, "utf8");
|
|
477
375
|
const targets = [
|
|
478
376
|
{
|
|
479
377
|
harness: "Claude Code",
|
|
480
|
-
path:
|
|
378
|
+
path: join(home, ".claude", "skills", "impractical", "SKILL.md")
|
|
481
379
|
},
|
|
482
380
|
{
|
|
483
381
|
harness: "Codex",
|
|
484
|
-
path:
|
|
382
|
+
path: join(home, ".codex", "skills", "impractical", "SKILL.md")
|
|
485
383
|
}
|
|
486
384
|
];
|
|
487
385
|
const reconciled = [];
|
|
488
386
|
for (const target of targets) {
|
|
489
|
-
if (!
|
|
490
|
-
const previous =
|
|
387
|
+
if (!existsSync(target.path)) continue;
|
|
388
|
+
const previous = readFileSync(target.path, "utf8");
|
|
491
389
|
if (previous === source) continue;
|
|
492
390
|
const backupPath = `${target.path}.pre-plugin`;
|
|
493
|
-
if (!
|
|
494
|
-
|
|
391
|
+
if (!existsSync(backupPath)) writeFileSync(backupPath, previous, { mode: 420 });
|
|
392
|
+
writeFileSync(target.path, source, { mode: 420 });
|
|
495
393
|
reconciled.push({ ...target, backupPath });
|
|
496
394
|
}
|
|
497
395
|
return reconciled;
|
|
@@ -509,7 +407,7 @@ function openBrowser(url) {
|
|
|
509
407
|
});
|
|
510
408
|
return !result.error && result.status === 0;
|
|
511
409
|
}
|
|
512
|
-
async function waitForApproval(supabaseUrl, authorization, log) {
|
|
410
|
+
async function waitForApproval(supabaseUrl, authorization, log, setupFlag) {
|
|
513
411
|
const deadline = Date.parse(authorization.expiresAt);
|
|
514
412
|
const intervalMs = Math.max(1, authorization.intervalSeconds) * 1e3;
|
|
515
413
|
let announced = false;
|
|
@@ -525,16 +423,21 @@ async function waitForApproval(supabaseUrl, authorization, log) {
|
|
|
525
423
|
}
|
|
526
424
|
await delay(intervalMs);
|
|
527
425
|
}
|
|
528
|
-
throw new Error(`Authorization expired. Run \`${PACKAGE_COMMAND} setup
|
|
426
|
+
throw new Error(`Authorization expired. Run \`${PACKAGE_COMMAND} setup ${setupFlag}\` again.`);
|
|
529
427
|
}
|
|
530
428
|
async function setup(options) {
|
|
531
429
|
if (options.installClaudePlugin) verifyClaudePrerequisites();
|
|
430
|
+
if (options.installAgentMcp) {
|
|
431
|
+
verifyAgentPrerequisites();
|
|
432
|
+
verifyAgentConfigs();
|
|
433
|
+
}
|
|
532
434
|
const corruptConfigBackup = recoverCorruptConfig();
|
|
533
435
|
if (corruptConfigBackup) {
|
|
534
436
|
options.log(`Backed up invalid configuration to ${corruptConfigBackup}; starting clean.`);
|
|
535
437
|
}
|
|
536
438
|
const supabaseUrl = process.env.IMPRACTICAL_SUPABASE_URL?.trim() || PRODUCTION_SUPABASE_URL;
|
|
537
439
|
const appUrl = process.env.IMPRACTICAL_APP_URL?.trim() || PRODUCTION_APP_URL;
|
|
440
|
+
const setupFlag = options.installAgentMcp ? "--agent" : "--claude";
|
|
538
441
|
const existing = loadConfig();
|
|
539
442
|
let apiKey = existing.supabaseUrl === supabaseUrl ? existing.apiKey : void 0;
|
|
540
443
|
let verified;
|
|
@@ -561,10 +464,10 @@ async function setup(options) {
|
|
|
561
464
|
options.log(opened ? "Opened your browser." : "Could not open a browser automatically; use the link above.");
|
|
562
465
|
}
|
|
563
466
|
try {
|
|
564
|
-
apiKey = await waitForApproval(supabaseUrl, authorization, options.log);
|
|
467
|
+
apiKey = await waitForApproval(supabaseUrl, authorization, options.log, setupFlag);
|
|
565
468
|
} catch (error) {
|
|
566
469
|
if (error instanceof ApiError && error.status === 410) {
|
|
567
|
-
throw new Error(`Authorization expired. Run \`${PACKAGE_COMMAND} setup
|
|
470
|
+
throw new Error(`Authorization expired. Run \`${PACKAGE_COMMAND} setup ${setupFlag}\` again.`);
|
|
568
471
|
}
|
|
569
472
|
throw error;
|
|
570
473
|
}
|
|
@@ -572,6 +475,7 @@ async function setup(options) {
|
|
|
572
475
|
saveConfig({ apiKey, supabaseUrl, appUrl });
|
|
573
476
|
}
|
|
574
477
|
const claudePlugin = options.installClaudePlugin ? installClaudePlugin(options.log) : void 0;
|
|
478
|
+
const configuredMcps = options.installAgentMcp ? installAgentMcps() : [];
|
|
575
479
|
const installed = options.installAgentSkills ? installSkills() : [];
|
|
576
480
|
const reconciled = reconcileLegacySkills();
|
|
577
481
|
options.log("");
|
|
@@ -579,39 +483,27 @@ async function setup(options) {
|
|
|
579
483
|
for (const skill of installed) {
|
|
580
484
|
options.log(`Installed /impractical for ${skill.harness}.`);
|
|
581
485
|
}
|
|
486
|
+
for (const mcp of configuredMcps) {
|
|
487
|
+
options.log(`Configured Impractical MCP for ${mcp.harness}.`);
|
|
488
|
+
}
|
|
582
489
|
for (const skill of reconciled) {
|
|
583
490
|
options.log(`Refreshed legacy /impractical for ${skill.harness}; previous copy preserved at ${skill.backupPath}.`);
|
|
584
491
|
}
|
|
585
492
|
if (claudePlugin) options.log(pluginResultLine(claudePlugin));
|
|
586
493
|
options.log(`Workspace: ${verified.orgId}${verified.plan ? ` \xB7 ${verified.plan}` : ""}`);
|
|
587
494
|
options.log("");
|
|
588
|
-
options.log("Run /reload-plugins, then type continue.");
|
|
589
495
|
if (options.installClaudePlugin) {
|
|
496
|
+
options.log("Run /reload-plugins, then type continue.");
|
|
497
|
+
} else if (options.installAgentMcp) {
|
|
498
|
+
options.log("Restart your coding agents, then ask them to use the Impractical MCP.");
|
|
499
|
+
}
|
|
500
|
+
if (options.installClaudePlugin || options.installAgentMcp) {
|
|
590
501
|
for (const line of authenticatedWhoamiGuidance()) options.log(line);
|
|
591
502
|
}
|
|
592
503
|
return { installed, appUrl, supabaseUrl };
|
|
593
504
|
}
|
|
594
505
|
|
|
595
506
|
export {
|
|
596
|
-
__commonJS,
|
|
597
|
-
__export,
|
|
598
|
-
__toESM,
|
|
599
|
-
PRODUCTION_APP_URL,
|
|
600
|
-
PRODUCTION_SUPABASE_URL,
|
|
601
|
-
PACKAGE_VERSION,
|
|
602
|
-
loadConfig,
|
|
603
|
-
saveConfig,
|
|
604
|
-
requireConfig,
|
|
605
|
-
ApiError,
|
|
606
|
-
validateApiResponse,
|
|
607
|
-
invoke,
|
|
608
|
-
invokeAt,
|
|
609
|
-
admitMcpCall,
|
|
610
|
-
withAdmissionReceipt,
|
|
611
|
-
withMeteredCall,
|
|
612
|
-
jobStatus,
|
|
613
|
-
waitForJobs,
|
|
614
|
-
studioLink,
|
|
615
507
|
openBrowser,
|
|
616
508
|
setup
|
|
617
509
|
};
|