@drawcall/design 0.1.0
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 +25 -0
- package/dist/cli-client.d.ts +5 -0
- package/dist/cli-client.js +19 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +332 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +56 -0
- package/dist/image.d.ts +1 -0
- package/dist/image.js +25 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/protocol.d.ts +7 -0
- package/dist/protocol.js +15 -0
- package/dist/skill.generated.d.ts +1 -0
- package/dist/skill.generated.js +2 -0
- package/dist/source.d.ts +10 -0
- package/dist/source.js +38 -0
- package/dist/target.d.ts +18 -0
- package/dist/target.js +40 -0
- package/dist/v1/client.d.ts +10 -0
- package/dist/v1/client.js +16 -0
- package/dist/v1/contract.d.ts +196 -0
- package/dist/v1/contract.js +99 -0
- package/dist/v1/index.d.ts +3 -0
- package/dist/v1/index.js +3 -0
- package/dist/v1/schemas.d.ts +146 -0
- package/dist/v1/schemas.js +143 -0
- package/package.json +55 -0
- package/skills/drawcall-design/SKILL.md +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @drawcall/design
|
|
2
|
+
|
|
3
|
+
The Drawcall Design v1 API contract and its remote CLI.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { v1 } from "@drawcall/design";
|
|
7
|
+
|
|
8
|
+
const design = v1.createClient({ authToken: process.env.DRAWCALL_AUTH_TOKEN });
|
|
9
|
+
const projects = await design.project.list();
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npx @drawcall/design login
|
|
14
|
+
npx @drawcall/design skill
|
|
15
|
+
npx @drawcall/design project create showroom
|
|
16
|
+
npx @drawcall/design frame create showroom/chair --size 800x800 'import * as THREE from "three"; export const scene = new THREE.Group()'
|
|
17
|
+
npx @drawcall/design frame create showroom/reference --size 800x800 --image ./reference.png
|
|
18
|
+
npx @drawcall/design frame create showroom/mood --size 800x800 --image https://example.com/mood.webp
|
|
19
|
+
npx @drawcall/design frame screenshot showroom/chair
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Source is always remote. Commands accept inline text or stdin; there is no checkout or local project
|
|
23
|
+
linking. `frame source replace` accepts a `Record<path, code>` JSON object inline or on stdin.
|
|
24
|
+
Agents should read `npx @drawcall/design skill` before creating or changing code-frame source. MCP
|
|
25
|
+
clients get the same text from `get_skill`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { loadConfig } from "./config.js";
|
|
2
|
+
import { createClient, DEFAULT_BASE_URL, } from "./v1/client.js";
|
|
3
|
+
export class NotLoggedInError extends Error {
|
|
4
|
+
constructor() {
|
|
5
|
+
super("Not logged in. Run `npx @drawcall/design login` first.");
|
|
6
|
+
this.name = "NotLoggedInError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export async function getCliClient(baseUrlOverride) {
|
|
10
|
+
const config = await loadConfig();
|
|
11
|
+
const authToken = process.env.DRAWCALL_AUTH_TOKEN ?? config?.authToken;
|
|
12
|
+
if (!authToken)
|
|
13
|
+
throw new NotLoggedInError();
|
|
14
|
+
const baseUrl = baseUrlOverride ??
|
|
15
|
+
process.env.DESIGN_API_URL ??
|
|
16
|
+
config?.baseUrl ??
|
|
17
|
+
DEFAULT_BASE_URL;
|
|
18
|
+
return createClient({ baseUrl, authToken });
|
|
19
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { Command, Option } from "commander";
|
|
4
|
+
import { clearConfig, getConfigPath, saveConfig } from "./config.js";
|
|
5
|
+
import { getCliClient } from "./cli-client.js";
|
|
6
|
+
import { readImageInput } from "./image.js";
|
|
7
|
+
import { designSkill } from "./skill.generated.js";
|
|
8
|
+
import { parseFrameSize, parseFrameTarget, parseScreenshotTarget, parseSourceFiles, } from "./target.js";
|
|
9
|
+
import { createClient, DEFAULT_BASE_URL } from "./v1/client.js";
|
|
10
|
+
const AUTH_ISSUER_URL = "https://auth.drawcall.ai/api/auth";
|
|
11
|
+
const DEVICE_CLIENT_ID = "drawcall-cli";
|
|
12
|
+
function sourceInput(source) {
|
|
13
|
+
return source === undefined ? {} : { source: { "/index.ts": source } };
|
|
14
|
+
}
|
|
15
|
+
const program = new Command()
|
|
16
|
+
.name("design")
|
|
17
|
+
.description("Design 3D scenes on Drawcall Design")
|
|
18
|
+
.version(readPackageVersion())
|
|
19
|
+
.addOption(new Option("--api <url>", "Design API URL").default(process.env.DESIGN_API_URL, "from DESIGN_API_URL / config / default"));
|
|
20
|
+
program
|
|
21
|
+
.command("skill")
|
|
22
|
+
.description("Print the Drawcall Design skill")
|
|
23
|
+
.action(() => {
|
|
24
|
+
process.stdout.write(designSkill);
|
|
25
|
+
});
|
|
26
|
+
program
|
|
27
|
+
.command("login")
|
|
28
|
+
.description("Sign in with your Drawcall account")
|
|
29
|
+
.action(async (_options, command) => {
|
|
30
|
+
const baseUrlOverride = apiOption(command);
|
|
31
|
+
const baseUrl = baseUrlOverride ?? DEFAULT_BASE_URL;
|
|
32
|
+
const token = await runDeviceLogin();
|
|
33
|
+
const client = createClient({ baseUrl, authToken: token });
|
|
34
|
+
const user = await client.user.me();
|
|
35
|
+
await saveConfig(baseUrlOverride ? { authToken: token, baseUrl } : { authToken: token });
|
|
36
|
+
console.log(`Signed in as ${user.email}. Credentials saved to ${getConfigPath()}.`);
|
|
37
|
+
});
|
|
38
|
+
program
|
|
39
|
+
.command("logout")
|
|
40
|
+
.description("Sign out")
|
|
41
|
+
.action(async () => {
|
|
42
|
+
console.log((await clearConfig()) ? "Signed out." : "Already signed out.");
|
|
43
|
+
});
|
|
44
|
+
const project = program.command("project").description("Manage projects");
|
|
45
|
+
project
|
|
46
|
+
.command("list")
|
|
47
|
+
.description("List projects")
|
|
48
|
+
.action(async (_options, command) => {
|
|
49
|
+
const client = await clientFor(command);
|
|
50
|
+
const projects = await client.project.list();
|
|
51
|
+
if (projects.length === 0) {
|
|
52
|
+
console.log("No projects.");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
for (const item of projects)
|
|
56
|
+
console.log(`${item.name}\t${item.id}`);
|
|
57
|
+
});
|
|
58
|
+
project
|
|
59
|
+
.command("create")
|
|
60
|
+
.description("Create a project")
|
|
61
|
+
.argument("<name>", "Project name")
|
|
62
|
+
.action(async (name, _options, command) => {
|
|
63
|
+
const client = await clientFor(command);
|
|
64
|
+
const created = await client.project.create({ name });
|
|
65
|
+
console.log(`Created ${created.name}\t${created.id}`);
|
|
66
|
+
});
|
|
67
|
+
project
|
|
68
|
+
.command("delete")
|
|
69
|
+
.description("Delete a project")
|
|
70
|
+
.argument("<name>", "Project name")
|
|
71
|
+
.option("-y, --yes", "Confirm deletion", false)
|
|
72
|
+
.action(async (name, options, command) => {
|
|
73
|
+
requireConfirmation(options.yes);
|
|
74
|
+
const client = await clientFor(command);
|
|
75
|
+
await client.project.delete({ project: name });
|
|
76
|
+
console.log(`Deleted ${name}.`);
|
|
77
|
+
});
|
|
78
|
+
const frame = program.command("frame").description("Manage frames");
|
|
79
|
+
frame
|
|
80
|
+
.command("list")
|
|
81
|
+
.description("List frames in a project")
|
|
82
|
+
.argument("<project>", "Project name")
|
|
83
|
+
.action(async (projectName, _options, command) => {
|
|
84
|
+
const client = await clientFor(command);
|
|
85
|
+
const frames = await client.frame.list({ project: projectName });
|
|
86
|
+
if (frames.length === 0) {
|
|
87
|
+
console.log("No frames.");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
for (const item of frames) {
|
|
91
|
+
console.log(`${item.name}\t${item.kind}\t${item.width}x${item.height}\t${item.id}`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
frame
|
|
95
|
+
.command("create")
|
|
96
|
+
.description("Create a code or image frame")
|
|
97
|
+
.argument("<target>", "Project and frame as <project>/<frame>")
|
|
98
|
+
.argument("[source]", "Inline /index.ts source, or - to read it from stdin")
|
|
99
|
+
.requiredOption("-s, --size <width>x<height>", "Authoritative frame size")
|
|
100
|
+
.option("--image <url-or-path>", "Create an image frame from a URL or file")
|
|
101
|
+
.action(async (targetValue, sourceValue, options, command) => {
|
|
102
|
+
const target = parseFrameTarget(targetValue);
|
|
103
|
+
const size = parseFrameSize(options.size);
|
|
104
|
+
if (options.image !== undefined && sourceValue !== undefined) {
|
|
105
|
+
throw new Error("Source and --image cannot be used together");
|
|
106
|
+
}
|
|
107
|
+
const client = await clientFor(command);
|
|
108
|
+
const created = options.image === undefined
|
|
109
|
+
? await client.frame.create({
|
|
110
|
+
project: target.project,
|
|
111
|
+
name: target.frame,
|
|
112
|
+
kind: "code",
|
|
113
|
+
...size,
|
|
114
|
+
...sourceInput(await optionalIndexSource(sourceValue)),
|
|
115
|
+
})
|
|
116
|
+
: await client.frame.create({
|
|
117
|
+
project: target.project,
|
|
118
|
+
name: target.frame,
|
|
119
|
+
kind: "image",
|
|
120
|
+
image: await readImageInput(options.image),
|
|
121
|
+
...size,
|
|
122
|
+
});
|
|
123
|
+
console.log(`Created ${targetValue}\t${created.id}`);
|
|
124
|
+
});
|
|
125
|
+
frame
|
|
126
|
+
.command("rename")
|
|
127
|
+
.description("Rename a frame")
|
|
128
|
+
.argument("<target>", "Project and frame as <project>/<frame>")
|
|
129
|
+
.argument("<name>", "New frame name")
|
|
130
|
+
.action(async (targetValue, name, _options, command) => {
|
|
131
|
+
const client = await clientFor(command);
|
|
132
|
+
const renamed = await client.frame.rename({
|
|
133
|
+
...parseFrameTarget(targetValue),
|
|
134
|
+
name,
|
|
135
|
+
});
|
|
136
|
+
console.log(`Renamed ${targetValue} to ${renamed.name}.`);
|
|
137
|
+
});
|
|
138
|
+
frame
|
|
139
|
+
.command("delete")
|
|
140
|
+
.description("Delete one or more frames")
|
|
141
|
+
.argument("<targets...>", "Frames as <project>/<frame>")
|
|
142
|
+
.option("-y, --yes", "Confirm deletion", false)
|
|
143
|
+
.action(async (targets, options, command) => {
|
|
144
|
+
requireConfirmation(options.yes);
|
|
145
|
+
const client = await clientFor(command);
|
|
146
|
+
for (const targetValue of targets) {
|
|
147
|
+
await client.frame.delete(parseFrameTarget(targetValue));
|
|
148
|
+
console.log(`Deleted ${targetValue}.`);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
frame
|
|
152
|
+
.command("screenshot")
|
|
153
|
+
.description("Render frames and print their screenshot URLs")
|
|
154
|
+
.argument("<targets...>", "Projects or frames as <project> or <project>/<frame>")
|
|
155
|
+
.action(async (targets, _options, command) => {
|
|
156
|
+
const client = await clientFor(command);
|
|
157
|
+
for (const value of targets)
|
|
158
|
+
await screenshotTarget(client, value);
|
|
159
|
+
});
|
|
160
|
+
const source = frame.command("source").description("Manage code-frame source");
|
|
161
|
+
source
|
|
162
|
+
.command("list")
|
|
163
|
+
.description("List source paths")
|
|
164
|
+
.argument("<target>", "Project and frame as <project>/<frame>")
|
|
165
|
+
.action(async (targetValue, _options, command) => {
|
|
166
|
+
const client = await clientFor(command);
|
|
167
|
+
const result = await client.frame.source.list(parseFrameTarget(targetValue));
|
|
168
|
+
for (const path of result.paths)
|
|
169
|
+
console.log(path);
|
|
170
|
+
});
|
|
171
|
+
source
|
|
172
|
+
.command("read")
|
|
173
|
+
.description("Read one source file")
|
|
174
|
+
.argument("<target>", "Project and frame as <project>/<frame>")
|
|
175
|
+
.argument("<path>", "Absolute source path")
|
|
176
|
+
.action(async (targetValue, path, _options, command) => {
|
|
177
|
+
const client = await clientFor(command);
|
|
178
|
+
const result = await client.frame.source.read({
|
|
179
|
+
...parseFrameTarget(targetValue),
|
|
180
|
+
path,
|
|
181
|
+
});
|
|
182
|
+
process.stdout.write(result.code);
|
|
183
|
+
});
|
|
184
|
+
source
|
|
185
|
+
.command("write")
|
|
186
|
+
.description("Create or replace one source file")
|
|
187
|
+
.argument("<target>", "Project and frame as <project>/<frame>")
|
|
188
|
+
.argument("<path>", "Absolute source path")
|
|
189
|
+
.argument("[code]", "Inline code; omit or use - to read stdin")
|
|
190
|
+
.action(async (targetValue, path, codeValue, _options, command) => {
|
|
191
|
+
const code = codeValue === undefined || codeValue === "-"
|
|
192
|
+
? await readStdin()
|
|
193
|
+
: codeValue;
|
|
194
|
+
const client = await clientFor(command);
|
|
195
|
+
await client.frame.source.write({
|
|
196
|
+
...parseFrameTarget(targetValue),
|
|
197
|
+
path,
|
|
198
|
+
code,
|
|
199
|
+
});
|
|
200
|
+
console.log(`Wrote ${path}.`);
|
|
201
|
+
});
|
|
202
|
+
source
|
|
203
|
+
.command("edit")
|
|
204
|
+
.description("Replace text that occurs exactly once in one source file")
|
|
205
|
+
.argument("<target>", "Project and frame as <project>/<frame>")
|
|
206
|
+
.argument("<path>", "Absolute source path")
|
|
207
|
+
.argument("<old-text>", "Text that must occur exactly once")
|
|
208
|
+
.argument("<new-text>", "Replacement text")
|
|
209
|
+
.action(async (targetValue, path, oldText, newText, _options, command) => {
|
|
210
|
+
const client = await clientFor(command);
|
|
211
|
+
await client.frame.source.edit({
|
|
212
|
+
...parseFrameTarget(targetValue),
|
|
213
|
+
path,
|
|
214
|
+
oldText,
|
|
215
|
+
newText,
|
|
216
|
+
});
|
|
217
|
+
console.log(`Edited ${path}.`);
|
|
218
|
+
});
|
|
219
|
+
source
|
|
220
|
+
.command("replace")
|
|
221
|
+
.description("Replace all source files from a Record<path, code> JSON object")
|
|
222
|
+
.argument("<target>", "Project and frame as <project>/<frame>")
|
|
223
|
+
.argument("[files]", "Inline JSON, or omit/use - to read stdin")
|
|
224
|
+
.action(async (targetValue, filesValue, _options, command) => {
|
|
225
|
+
const input = filesValue === undefined || filesValue === "-"
|
|
226
|
+
? await readStdin()
|
|
227
|
+
: filesValue;
|
|
228
|
+
const client = await clientFor(command);
|
|
229
|
+
await client.frame.source.replace({
|
|
230
|
+
...parseFrameTarget(targetValue),
|
|
231
|
+
files: parseSourceFiles(input),
|
|
232
|
+
});
|
|
233
|
+
console.log("Replaced source.");
|
|
234
|
+
});
|
|
235
|
+
source
|
|
236
|
+
.command("delete")
|
|
237
|
+
.description("Delete exact source paths, or all source with a quoted *")
|
|
238
|
+
.argument("<target>", "Project and frame as <project>/<frame>")
|
|
239
|
+
.argument("<paths...>", "Absolute paths, or quoted *")
|
|
240
|
+
.option("-y, --yes", "Confirm deletion", false)
|
|
241
|
+
.action(async (targetValue, paths, options, command) => {
|
|
242
|
+
requireConfirmation(options.yes);
|
|
243
|
+
const client = await clientFor(command);
|
|
244
|
+
await client.frame.source.delete({
|
|
245
|
+
...parseFrameTarget(targetValue),
|
|
246
|
+
paths,
|
|
247
|
+
});
|
|
248
|
+
console.log("Deleted source.");
|
|
249
|
+
});
|
|
250
|
+
if (process.argv.length <= 2) {
|
|
251
|
+
program.outputHelp();
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
program.parseAsync().catch((error) => {
|
|
255
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
256
|
+
process.exitCode = 1;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
async function clientFor(command) {
|
|
260
|
+
return getCliClient(apiOption(command));
|
|
261
|
+
}
|
|
262
|
+
function apiOption(command) {
|
|
263
|
+
const value = command.optsWithGlobals().api;
|
|
264
|
+
return typeof value === "string" ? value : undefined;
|
|
265
|
+
}
|
|
266
|
+
function requireConfirmation(confirmed) {
|
|
267
|
+
if (!confirmed)
|
|
268
|
+
throw new Error("Destructive commands require --yes.");
|
|
269
|
+
}
|
|
270
|
+
async function optionalIndexSource(value) {
|
|
271
|
+
if (value === undefined)
|
|
272
|
+
return undefined;
|
|
273
|
+
return value === "-" ? readStdin() : value;
|
|
274
|
+
}
|
|
275
|
+
async function readStdin() {
|
|
276
|
+
const chunks = [];
|
|
277
|
+
for await (const chunk of process.stdin) {
|
|
278
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
279
|
+
}
|
|
280
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
281
|
+
}
|
|
282
|
+
async function screenshotTarget(client, value) {
|
|
283
|
+
const target = parseScreenshotTarget(value);
|
|
284
|
+
if (target.frame) {
|
|
285
|
+
const result = await client.frame.screenshot(target);
|
|
286
|
+
console.log(`${value}\t${result.url}`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const frames = await client.frame.list({ project: target.project });
|
|
290
|
+
for (const item of frames) {
|
|
291
|
+
const result = await client.frame.screenshot({
|
|
292
|
+
project: target.project,
|
|
293
|
+
frame: item.name,
|
|
294
|
+
});
|
|
295
|
+
console.log(`${target.project}/${item.name}\t${result.url}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function readPackageVersion() {
|
|
299
|
+
const manifest = createRequire(import.meta.url)("../package.json");
|
|
300
|
+
if (!manifest ||
|
|
301
|
+
typeof manifest !== "object" ||
|
|
302
|
+
Array.isArray(manifest) ||
|
|
303
|
+
!("version" in manifest) ||
|
|
304
|
+
typeof manifest.version !== "string") {
|
|
305
|
+
throw new Error("@drawcall/design package.json is missing a valid version");
|
|
306
|
+
}
|
|
307
|
+
return manifest.version;
|
|
308
|
+
}
|
|
309
|
+
async function runDeviceLogin() {
|
|
310
|
+
const [{ default: open }, oauth] = await Promise.all([
|
|
311
|
+
import("open"),
|
|
312
|
+
import("openid-client"),
|
|
313
|
+
]);
|
|
314
|
+
const configuration = await oauth.discovery(new URL(AUTH_ISSUER_URL), DEVICE_CLIENT_ID, undefined, oauth.None());
|
|
315
|
+
const authorization = await oauth.initiateDeviceAuthorization(configuration, {
|
|
316
|
+
scope: "design",
|
|
317
|
+
});
|
|
318
|
+
const verificationUrl = authorization.verification_uri_complete ?? authorization.verification_uri;
|
|
319
|
+
console.log(`Open ${verificationUrl}`);
|
|
320
|
+
console.log(`Code: ${authorization.user_code}`);
|
|
321
|
+
try {
|
|
322
|
+
await open(verificationUrl);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
326
|
+
console.error(`Could not open a browser automatically: ${message}`);
|
|
327
|
+
}
|
|
328
|
+
const tokens = await oauth.pollDeviceAuthorizationGrant(configuration, authorization);
|
|
329
|
+
if (!tokens.access_token)
|
|
330
|
+
throw new Error("Device login completed without an access token.");
|
|
331
|
+
return tokens.access_token;
|
|
332
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const configSchema: z.ZodObject<{
|
|
3
|
+
authToken: z.ZodString;
|
|
4
|
+
baseUrl: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export type Config = z.infer<typeof configSchema>;
|
|
7
|
+
export declare function loadConfig(): Promise<Config | null>;
|
|
8
|
+
export declare function saveConfig(config: Config): Promise<void>;
|
|
9
|
+
export declare function clearConfig(): Promise<boolean>;
|
|
10
|
+
export declare function getConfigPath(): string;
|
|
11
|
+
export {};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
const configSchema = z.object({
|
|
6
|
+
authToken: z.string().min(1),
|
|
7
|
+
baseUrl: z.string().url().optional(),
|
|
8
|
+
});
|
|
9
|
+
function configDirectory() {
|
|
10
|
+
if (process.platform === "win32" && process.env.APPDATA) {
|
|
11
|
+
return path.join(process.env.APPDATA, "drawcall-design");
|
|
12
|
+
}
|
|
13
|
+
const parent = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
|
|
14
|
+
return path.join(parent, "drawcall-design");
|
|
15
|
+
}
|
|
16
|
+
function configPath() {
|
|
17
|
+
return path.join(configDirectory(), "config.json");
|
|
18
|
+
}
|
|
19
|
+
export async function loadConfig() {
|
|
20
|
+
try {
|
|
21
|
+
const value = JSON.parse(await fs.readFile(configPath(), "utf8"));
|
|
22
|
+
return configSchema.parse(value);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (isMissingFile(error))
|
|
26
|
+
return null;
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function saveConfig(config) {
|
|
31
|
+
const directory = configDirectory();
|
|
32
|
+
await fs.mkdir(directory, { recursive: true });
|
|
33
|
+
await fs.chmod(directory, 0o700);
|
|
34
|
+
const file = configPath();
|
|
35
|
+
await fs.writeFile(file, `${JSON.stringify(config, null, 2)}\n`, {
|
|
36
|
+
mode: 0o600,
|
|
37
|
+
});
|
|
38
|
+
await fs.chmod(file, 0o600);
|
|
39
|
+
}
|
|
40
|
+
export async function clearConfig() {
|
|
41
|
+
try {
|
|
42
|
+
await fs.unlink(configPath());
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (isMissingFile(error))
|
|
47
|
+
return false;
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function getConfigPath() {
|
|
52
|
+
return configPath();
|
|
53
|
+
}
|
|
54
|
+
function isMissingFile(error) {
|
|
55
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
56
|
+
}
|
package/dist/image.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readImageInput(value: string): Promise<File | string>;
|
package/dist/image.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { basename, extname } from "node:path";
|
|
3
|
+
import { imageUrlSchema } from "./v1/schemas.js";
|
|
4
|
+
export async function readImageInput(value) {
|
|
5
|
+
if (/^https?:/i.test(value) || value.includes("://")) {
|
|
6
|
+
return imageUrlSchema.parse(value);
|
|
7
|
+
}
|
|
8
|
+
const extension = extname(value).toLowerCase();
|
|
9
|
+
const mediaType = mediaTypeForExtension(extension);
|
|
10
|
+
if (mediaType === undefined) {
|
|
11
|
+
throw new Error("Local images must be PNG, JPEG, or WebP files");
|
|
12
|
+
}
|
|
13
|
+
return new File([await readFile(value)], basename(value), {
|
|
14
|
+
type: mediaType,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function mediaTypeForExtension(extension) {
|
|
18
|
+
if (extension === ".jpeg" || extension === ".jpg")
|
|
19
|
+
return "image/jpeg";
|
|
20
|
+
if (extension === ".png")
|
|
21
|
+
return "image/png";
|
|
22
|
+
if (extension === ".webp")
|
|
23
|
+
return "image/webp";
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * as v1 from "./v1/index.js";
|
|
2
|
+
export { designSkill } from "./skill.generated.js";
|
|
3
|
+
export { deleteSourceFiles, editSourceCode, editSourceFile, SourceEditError, SourceFileNotFoundError, } from "./source.js";
|
|
4
|
+
export { parseFrameSize, parseFrameTarget, parseScreenshotTarget, parseSourceFiles, type FrameTarget, type ScreenshotTarget, } from "./target.js";
|
|
5
|
+
export { inspectionMessageSchema, projectSyncUrl, type InspectionMessage, } from "./protocol.js";
|
|
6
|
+
export type { CodeFrame, CreateCodeFrame, CreateFrame, CreateImageFrame, Frame, ImageFrame, Project, Screenshot, SourceFile, SourceFiles, SourceList, SourceMutation, User, } from "./v1/schemas.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * as v1 from "./v1/index.js";
|
|
2
|
+
export { designSkill } from "./skill.generated.js";
|
|
3
|
+
export { deleteSourceFiles, editSourceCode, editSourceFile, SourceEditError, SourceFileNotFoundError, } from "./source.js";
|
|
4
|
+
export { parseFrameSize, parseFrameTarget, parseScreenshotTarget, parseSourceFiles, } from "./target.js";
|
|
5
|
+
export { inspectionMessageSchema, projectSyncUrl, } from "./protocol.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const inspectionMessageSchema: z.ZodObject<{
|
|
3
|
+
type: z.ZodLiteral<"drawcall:inspection">;
|
|
4
|
+
enabled: z.ZodBoolean;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export type InspectionMessage = z.infer<typeof inspectionMessageSchema>;
|
|
7
|
+
export declare function projectSyncUrl(baseUrl: string, project: string): string;
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const inspectionMessageSchema = z.object({
|
|
3
|
+
type: z.literal("drawcall:inspection"),
|
|
4
|
+
enabled: z.boolean(),
|
|
5
|
+
});
|
|
6
|
+
export function projectSyncUrl(baseUrl, project) {
|
|
7
|
+
const url = new URL(`/api/v1/projects/${encodeURIComponent(project)}/sync`, baseUrl);
|
|
8
|
+
if (url.protocol === "https:")
|
|
9
|
+
url.protocol = "wss:";
|
|
10
|
+
else if (url.protocol === "http:")
|
|
11
|
+
url.protocol = "ws:";
|
|
12
|
+
else
|
|
13
|
+
throw new Error(`Cannot create a WebSocket URL from ${url.protocol}`);
|
|
14
|
+
return url.href;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const designSkill = "---\nname: drawcall-design\ndescription: Create, modify, inspect, and screenshot 3D objects and scenes in Drawcall Design using its MCP tools or @drawcall/design CLI. Use for Drawcall Design projects, frames, and code-frame source. Do not use for full games or applications.\n---\n\n# Drawcall Design\n\nUse the Drawcall Design MCP tools when available. Otherwise, use `npx @drawcall/design`. Drawcall Design is a remote, current-state canvas: inspect the existing project, frames, and source before changing them.\n\nA code frame stores source as a `Record<absolute path, code>`. A renderable frame has exactly one `/index.ts` or `/index.js`. That module must export `scene`, whose value is a `THREE.Object3D`, and may export `camera`, whose value is a `THREE.Camera`.\n\nSource runs as browser ESM. Use relative imports with explicit extensions for other frame files and normal package imports for browser-compatible packages. Top-level await is supported. In TypeScript files, use syntax that can be erased; avoid features that generate JavaScript.\n\nA code frame represents a designed state rather than a running application. Complete asynchronous setup before exporting the scene, and add any lighting or environment the object needs.\n\nCreate image frames from a public HTTP(S) URL through MCP. In the CLI, `frame create --image` accepts either an HTTP(S) URL or a local PNG, JPEG, or WebP file.\n\n```ts\nimport * as THREE from \"three\";\n\nconst root = new THREE.Scene();\n// Build the object or scene.\n\nexport const scene: THREE.Object3D = root;\n\nconst view = new THREE.PerspectiveCamera(35, 1, 0.1, 100);\nview.position.set(4, 3, 5);\nview.lookAt(0, 0, 0);\n\nexport const camera: THREE.Camera = view;\n```\n\nRead source before editing it. Prefer an exact edit for a small change, write one file when replacing that file, and replace the complete source only when the whole frame should change.\n\nAfter every meaningful visual change, take a frame screenshot and inspect it. Iterate until the rendered object, composition, and frame size satisfy the request.\n";
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// Generated from skills/drawcall-design/SKILL.md.
|
|
2
|
+
export const designSkill = '---\nname: drawcall-design\ndescription: Create, modify, inspect, and screenshot 3D objects and scenes in Drawcall Design using its MCP tools or @drawcall/design CLI. Use for Drawcall Design projects, frames, and code-frame source. Do not use for full games or applications.\n---\n\n# Drawcall Design\n\nUse the Drawcall Design MCP tools when available. Otherwise, use `npx @drawcall/design`. Drawcall Design is a remote, current-state canvas: inspect the existing project, frames, and source before changing them.\n\nA code frame stores source as a `Record<absolute path, code>`. A renderable frame has exactly one `/index.ts` or `/index.js`. That module must export `scene`, whose value is a `THREE.Object3D`, and may export `camera`, whose value is a `THREE.Camera`.\n\nSource runs as browser ESM. Use relative imports with explicit extensions for other frame files and normal package imports for browser-compatible packages. Top-level await is supported. In TypeScript files, use syntax that can be erased; avoid features that generate JavaScript.\n\nA code frame represents a designed state rather than a running application. Complete asynchronous setup before exporting the scene, and add any lighting or environment the object needs.\n\nCreate image frames from a public HTTP(S) URL through MCP. In the CLI, `frame create --image` accepts either an HTTP(S) URL or a local PNG, JPEG, or WebP file.\n\n```ts\nimport * as THREE from "three";\n\nconst root = new THREE.Scene();\n// Build the object or scene.\n\nexport const scene: THREE.Object3D = root;\n\nconst view = new THREE.PerspectiveCamera(35, 1, 0.1, 100);\nview.position.set(4, 3, 5);\nview.lookAt(0, 0, 0);\n\nexport const camera: THREE.Camera = view;\n```\n\nRead source before editing it. Prefer an exact edit for a small change, write one file when replacing that file, and replace the complete source only when the whole frame should change.\n\nAfter every meaningful visual change, take a frame screenshot and inspect it. Iterate until the rendered object, composition, and frame size satisfy the request.\n';
|
package/dist/source.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SourceFiles } from "./v1/schemas.js";
|
|
2
|
+
export declare class SourceEditError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class SourceFileNotFoundError extends Error {
|
|
6
|
+
constructor(path: string);
|
|
7
|
+
}
|
|
8
|
+
export declare function editSourceCode(code: string, oldText: string, newText: string): string;
|
|
9
|
+
export declare function editSourceFile(files: SourceFiles, path: string, oldText: string, newText: string): SourceFiles;
|
|
10
|
+
export declare function deleteSourceFiles(files: SourceFiles, paths: string[]): SourceFiles;
|
package/dist/source.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class SourceEditError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "SourceEditError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class SourceFileNotFoundError extends Error {
|
|
8
|
+
constructor(path) {
|
|
9
|
+
super(`Source file not found: ${path}`);
|
|
10
|
+
this.name = "SourceFileNotFoundError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function editSourceCode(code, oldText, newText) {
|
|
14
|
+
if (oldText.length === 0)
|
|
15
|
+
throw new SourceEditError("oldText cannot be empty");
|
|
16
|
+
const match = code.indexOf(oldText);
|
|
17
|
+
if (match === -1)
|
|
18
|
+
throw new SourceEditError("oldText does not occur in the source file");
|
|
19
|
+
if (code.indexOf(oldText, match + 1) !== -1) {
|
|
20
|
+
throw new SourceEditError("oldText occurs more than once in the source file");
|
|
21
|
+
}
|
|
22
|
+
return code.slice(0, match) + newText + code.slice(match + oldText.length);
|
|
23
|
+
}
|
|
24
|
+
export function editSourceFile(files, path, oldText, newText) {
|
|
25
|
+
const code = files[path];
|
|
26
|
+
if (code === undefined)
|
|
27
|
+
throw new SourceFileNotFoundError(path);
|
|
28
|
+
return { ...files, [path]: editSourceCode(code, oldText, newText) };
|
|
29
|
+
}
|
|
30
|
+
export function deleteSourceFiles(files, paths) {
|
|
31
|
+
if (paths.length === 1 && paths[0] === "*")
|
|
32
|
+
return {};
|
|
33
|
+
for (const path of paths) {
|
|
34
|
+
if (!Object.hasOwn(files, path))
|
|
35
|
+
throw new SourceFileNotFoundError(path);
|
|
36
|
+
}
|
|
37
|
+
return Object.fromEntries(Object.entries(files).filter(([path]) => !paths.includes(path)));
|
|
38
|
+
}
|
package/dist/target.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface FrameTarget {
|
|
2
|
+
project: string;
|
|
3
|
+
frame: string;
|
|
4
|
+
}
|
|
5
|
+
export type ScreenshotTarget = {
|
|
6
|
+
project: string;
|
|
7
|
+
frame?: never;
|
|
8
|
+
} | {
|
|
9
|
+
project: string;
|
|
10
|
+
frame: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function parseFrameTarget(value: string): FrameTarget;
|
|
13
|
+
export declare function parseScreenshotTarget(value: string): ScreenshotTarget;
|
|
14
|
+
export declare function parseFrameSize(value: string): {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
};
|
|
18
|
+
export declare function parseSourceFiles(value: string): Record<string, string>;
|
package/dist/target.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { frameNameSchema, projectNameSchema, sourceFilesSchema, } from "./v1/schemas.js";
|
|
3
|
+
export function parseFrameTarget(value) {
|
|
4
|
+
const segments = value.split("/");
|
|
5
|
+
const project = segments[0];
|
|
6
|
+
const frame = segments[1];
|
|
7
|
+
if (segments.length !== 2 || project === undefined || frame === undefined) {
|
|
8
|
+
throw new Error(`Expected <project>/<frame>, received ${JSON.stringify(value)}`);
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
project: projectNameSchema.parse(project),
|
|
12
|
+
frame: frameNameSchema.parse(frame),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function parseScreenshotTarget(value) {
|
|
16
|
+
if (value.includes("/"))
|
|
17
|
+
return parseFrameTarget(value);
|
|
18
|
+
return { project: projectNameSchema.parse(value) };
|
|
19
|
+
}
|
|
20
|
+
export function parseFrameSize(value) {
|
|
21
|
+
const match = /^(\d+)[x×](\d+)$/i.exec(value);
|
|
22
|
+
const widthText = match?.[1];
|
|
23
|
+
const heightText = match?.[2];
|
|
24
|
+
if (widthText === undefined || heightText === undefined) {
|
|
25
|
+
throw new Error(`Expected <width>x<height>, received ${JSON.stringify(value)}`);
|
|
26
|
+
}
|
|
27
|
+
const width = Number(widthText);
|
|
28
|
+
const height = Number(heightText);
|
|
29
|
+
if (!Number.isSafeInteger(width) ||
|
|
30
|
+
!Number.isSafeInteger(height) ||
|
|
31
|
+
width < 1 ||
|
|
32
|
+
height < 1) {
|
|
33
|
+
throw new Error("Frame width and height must be positive safe integers");
|
|
34
|
+
}
|
|
35
|
+
return { width, height };
|
|
36
|
+
}
|
|
37
|
+
export function parseSourceFiles(value) {
|
|
38
|
+
const parsed = JSON.parse(value);
|
|
39
|
+
return sourceFilesSchema.parse(parsed);
|
|
40
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ContractRouterClient } from "@orpc/contract";
|
|
2
|
+
import { type V1Contract } from "./contract.js";
|
|
3
|
+
export declare const DEFAULT_BASE_URL = "https://design.drawcall.ai";
|
|
4
|
+
export type DesignV1Client = ContractRouterClient<V1Contract>;
|
|
5
|
+
export interface DesignV1ClientOptions {
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
fetch?: typeof globalThis.fetch;
|
|
8
|
+
authToken?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function createClient(options?: DesignV1ClientOptions): DesignV1Client;
|