dsh-claude-design 2.6.0 → 2.7.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 +3 -1
- package/lib/client.js +625 -40
- package/lib/index.js +273 -51
- package/package.json +8 -2
- package/skills/claude-design/SKILL.md +21 -1
- package/skills/claude-design/artifact-type/reference/print.md +13 -7
- package/skills/claude-design/scripts/artifact.mjs +234 -45
- package/skills/claude-design/scripts/editor/assets/anthropicons-fizzy-lake-CfC2JIbf.woff2 +0 -0
- package/skills/claude-design/scripts/editor/assets/{index-BZKwYnL9.js → index-DyKwtQBI.js} +1011 -373
- package/skills/claude-design/scripts/editor/assets/index-tSD8GYWj.css +1 -0
- package/skills/claude-design/scripts/editor/index.html +2 -2
- package/skills/claude-design/scripts/editor/share-ui.js +37 -7
- package/skills/claude-design/scripts/server.mjs +859 -105
- package/skills/claude-design/scripts/editor/assets/anthropicons-fizzy-lake-c1fxtHDl.woff2 +0 -0
- package/skills/claude-design/scripts/editor/assets/index-oDSQuawR.css +0 -1
package/lib/index.js
CHANGED
|
@@ -1,16 +1,189 @@
|
|
|
1
|
-
// dsh-claude-design 2.
|
|
1
|
+
// dsh-claude-design 2.7.0: built from baoyu-design.
|
|
2
2
|
var define_PLUGIN_default = { name: "claude-design", provider: "dsh-claude-design", skills: [{ name: "claude-design", description: "Make and revise designs in the Design (canvas) artifact format (project/canvas.json plus one .dc.html Design Component file per artboard) as local files, then open them in a local canvas editor where the user can view the artboards side by side, retype copy and adjust tweaks. Use when the user asks for an editable canvas of artboards in this format, names claude-design or the Design (canvas) type, or wants to revise a folder that holds project/canvas.json. A one-off HTML prototype or design exploration outside this format is baoyu-design's.", comments: true }] };
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
4
5
|
import { randomUUID } from "node:crypto";
|
|
5
6
|
import { statSync } from "node:fs";
|
|
6
7
|
import { readFile } from "node:fs/promises";
|
|
7
|
-
import { delimiter, dirname, isAbsolute, join, sep } from "node:path";
|
|
8
|
+
import { delimiter, dirname, isAbsolute, join as join2, sep } from "node:path";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { basename, join } from "node:path";
|
|
16
|
+
|
|
17
|
+
var listPath = (skill) => `/api/baoyu-design.${skill}.artifacts`;
|
|
18
|
+
var openPath = (skill) => `/api/baoyu-design.${skill}.artifacts.open`;
|
|
19
|
+
|
|
20
|
+
var TITLES = {
|
|
21
|
+
"claude-slides": ["project/deck.json", "title"],
|
|
22
|
+
"claude-design": ["project/canvas.json", "title"],
|
|
23
|
+
"claude-design-system": ["project/design-system.json", "title"],
|
|
24
|
+
"claude-dashboard": ["db/dash/meta.json", "title"],
|
|
25
|
+
"claude-docs": ["doc.json", "name"],
|
|
26
|
+
"claude-motion": ["db/film/meta.json", "title"]
|
|
27
|
+
};
|
|
28
|
+
var OPEN_TIMEOUT_MS = 3e4;
|
|
29
|
+
function dshHome() {
|
|
30
|
+
const set = process.env.DSH_HOME?.trim();
|
|
31
|
+
return set ? set : join(homedir(), ".dsh");
|
|
32
|
+
}
|
|
33
|
+
function artifactsDir() {
|
|
34
|
+
return join(dshHome(), "baoyu-design", "artifacts");
|
|
35
|
+
}
|
|
36
|
+
function recordFile(root) {
|
|
37
|
+
return join(artifactsDir(), `${createHash("sha256").update(root).digest("hex").slice(0, 24)}.json`);
|
|
38
|
+
}
|
|
39
|
+
function artifactTitle(skill, root) {
|
|
40
|
+
const at = TITLES[skill];
|
|
41
|
+
if (at === void 0) return null;
|
|
42
|
+
try {
|
|
43
|
+
const value = JSON.parse(readFileSync(join(root, at[0]), "utf8"))[at[1]];
|
|
44
|
+
const title = typeof value === "string" ? value.trim() : "";
|
|
45
|
+
if (title && title !== "project") return title;
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
return basename(root);
|
|
49
|
+
}
|
|
50
|
+
function isRecord(value) {
|
|
51
|
+
const r = value;
|
|
52
|
+
return typeof r === "object" && r !== null && r.v === 1 && typeof r.root === "string" && typeof r.skill === "string" && typeof r.plugin === "string" && typeof r.sessionId === "string" && typeof r.title === "string" && typeof r.openedAt === "string";
|
|
53
|
+
}
|
|
54
|
+
function readRecord(file) {
|
|
55
|
+
try {
|
|
56
|
+
const value = JSON.parse(readFileSync(file, "utf8"));
|
|
57
|
+
return isRecord(value) ? value : null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function recordOpened(skill, plugin, sessionId, root, now = /* @__PURE__ */ new Date()) {
|
|
63
|
+
const file = recordFile(root);
|
|
64
|
+
const before = readRecord(file);
|
|
65
|
+
const record = {
|
|
66
|
+
v: 1,
|
|
67
|
+
root,
|
|
68
|
+
skill,
|
|
69
|
+
plugin,
|
|
70
|
+
sessionId,
|
|
71
|
+
title: artifactTitle(skill, root) ?? before?.title ?? basename(root),
|
|
72
|
+
openedAt: now.toISOString(),
|
|
73
|
+
firstOpenedAt: before?.root === root ? before.firstOpenedAt ?? before.openedAt : now.toISOString()
|
|
74
|
+
};
|
|
75
|
+
mkdirSync(artifactsDir(), { recursive: true });
|
|
76
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
77
|
+
writeFileSync(tmp, `${JSON.stringify(record, null, 2)}
|
|
78
|
+
`);
|
|
79
|
+
renameSync(tmp, file);
|
|
80
|
+
return record;
|
|
81
|
+
}
|
|
82
|
+
function listArtifacts() {
|
|
83
|
+
let names;
|
|
84
|
+
try {
|
|
85
|
+
names = readdirSync(artifactsDir());
|
|
86
|
+
} catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
const views = [];
|
|
90
|
+
for (const name2 of names) {
|
|
91
|
+
if (!name2.endsWith(".json")) continue;
|
|
92
|
+
const record = readRecord(join(artifactsDir(), name2));
|
|
93
|
+
if (record === null || !existsSync(record.root)) continue;
|
|
94
|
+
views.push({
|
|
95
|
+
root: record.root,
|
|
96
|
+
skill: record.skill,
|
|
97
|
+
plugin: record.plugin,
|
|
98
|
+
sessionId: record.sessionId,
|
|
99
|
+
title: artifactTitle(record.skill, record.root) ?? record.title,
|
|
100
|
+
openedAt: record.openedAt
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return views.sort((a, b) => a.openedAt < b.openedAt ? 1 : a.openedAt > b.openedAt ? -1 : 0);
|
|
104
|
+
}
|
|
105
|
+
function findArtifact(root) {
|
|
106
|
+
const record = readRecord(recordFile(root));
|
|
107
|
+
return record?.root === root ? record : null;
|
|
108
|
+
}
|
|
109
|
+
function scriptEnv(node) {
|
|
110
|
+
const env = { ...process.env };
|
|
111
|
+
if (node.env) {
|
|
112
|
+
const [name2, value] = node.env.split("=");
|
|
113
|
+
if (name2) env[name2] = value ?? "";
|
|
114
|
+
}
|
|
115
|
+
return env;
|
|
116
|
+
}
|
|
117
|
+
function runOpen(node, script, root) {
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
const child = spawn(node.path, [script, "open", root], { env: scriptEnv(node), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
120
|
+
let stdout = "";
|
|
121
|
+
let stderr = "";
|
|
122
|
+
const timer = setTimeout(() => {
|
|
123
|
+
child.kill();
|
|
124
|
+
reject(new Error("the editor did not start in time"));
|
|
125
|
+
}, OPEN_TIMEOUT_MS);
|
|
126
|
+
child.stdout?.setEncoding("utf8").on("data", (chunk) => stdout += chunk);
|
|
127
|
+
child.stderr?.setEncoding("utf8").on("data", (chunk) => stderr += chunk);
|
|
128
|
+
child.on("error", (error) => {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
reject(error);
|
|
131
|
+
});
|
|
132
|
+
child.on("close", (code) => {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
const out = firstJson(stdout);
|
|
135
|
+
if (code === 0 && typeof out?.url === "string") return resolve({ url: out.url, reused: out.reused === true });
|
|
136
|
+
const failure = firstJson(stderr);
|
|
137
|
+
const message = typeof failure?.message === "string" ? failure.message : stderr.trim() || `open exited with ${code ?? "a signal"}`;
|
|
138
|
+
reject(new Error(message));
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function firstJson(text) {
|
|
143
|
+
const start = text.indexOf("{");
|
|
144
|
+
const end = text.lastIndexOf("}");
|
|
145
|
+
if (start < 0 || end <= start) return null;
|
|
146
|
+
try {
|
|
147
|
+
const value = JSON.parse(text.slice(start, end + 1));
|
|
148
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
var json = (body, status = 200) => Response.json(body, { status, headers: { "cache-control": "no-store" } });
|
|
154
|
+
function registerRoutes(routes, skill, opened) {
|
|
155
|
+
routes.register({
|
|
156
|
+
path: listPath(skill.name),
|
|
157
|
+
methods: ["GET"],
|
|
158
|
+
requestBody: "buffered",
|
|
159
|
+
fetch: () => Promise.resolve(json({ artifacts: listArtifacts() }))
|
|
160
|
+
});
|
|
161
|
+
routes.register({
|
|
162
|
+
path: openPath(skill.name),
|
|
163
|
+
methods: ["POST"],
|
|
164
|
+
requestBody: "buffered",
|
|
165
|
+
fetch: async (request) => {
|
|
166
|
+
const root = new URL(request.url).searchParams.get("root") ?? "";
|
|
167
|
+
const record = root ? findArtifact(root) : null;
|
|
168
|
+
if (record === null || record.skill !== skill.name) return json({ error: "not a recorded artifact of this skill" }, 404);
|
|
169
|
+
if (!existsSync(root)) return json({ error: "the folder is gone" }, 404);
|
|
170
|
+
try {
|
|
171
|
+
const { url, reused } = await runOpen(skill.node, skill.script, root);
|
|
172
|
+
const again = recordOpened(skill.name, skill.plugin, record.sessionId, root);
|
|
173
|
+
opened(again);
|
|
174
|
+
return json({ url, reused, sessionId: again.sessionId, title: again.title });
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return json({ error: error.message }, 500);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
var PLUGIN = define_PLUGIN_default;
|
|
183
|
+
var name = PLUGIN.name;
|
|
184
|
+
var inject = ["skills"];
|
|
185
|
+
var BUNDLED_SKILL_RANK = 600;
|
|
186
|
+
var packageRoot = fileURLToPath(new URL("../", import.meta.url));
|
|
14
187
|
function isFile(path) {
|
|
15
188
|
try {
|
|
16
189
|
return statSync(path).isFile();
|
|
@@ -33,18 +206,18 @@ function resolveNode(config = {}) {
|
|
|
33
206
|
if (typeof resourcesPath === "string") roots.push(resourcesPath);
|
|
34
207
|
let dir = dirname(process.execPath);
|
|
35
208
|
for (let i = 0; i < 6; i++) {
|
|
36
|
-
roots.push(
|
|
209
|
+
roots.push(join2(dir, "Resources"), join2(dir, "resources"));
|
|
37
210
|
const parent = dirname(dir);
|
|
38
211
|
if (parent === dir) break;
|
|
39
212
|
dir = parent;
|
|
40
213
|
}
|
|
41
214
|
for (const root of roots) {
|
|
42
|
-
const candidate =
|
|
215
|
+
const candidate = join2(root, ...runtime);
|
|
43
216
|
if (isFile(candidate)) return { path: candidate, from: "desktop runtime" };
|
|
44
217
|
}
|
|
45
218
|
for (const entry of (process.env.PATH ?? "").split(delimiter)) {
|
|
46
219
|
if (entry === "" || !isAbsolute(entry)) continue;
|
|
47
|
-
const candidate =
|
|
220
|
+
const candidate = join2(entry, exe);
|
|
48
221
|
if (isFile(candidate)) return { path: candidate, from: "PATH" };
|
|
49
222
|
}
|
|
50
223
|
return { path: process.execPath, env: "ELECTRON_RUN_AS_NODE=1", from: "electron" };
|
|
@@ -53,7 +226,7 @@ function shellQuote(path) {
|
|
|
53
226
|
return `"${path.replace(/(["\\$`])/g, "\\$1")}"`;
|
|
54
227
|
}
|
|
55
228
|
function runtimeSection(skill, skillDir, node) {
|
|
56
|
-
const script =
|
|
229
|
+
const script = join2(skillDir, "scripts", "artifact.mjs");
|
|
57
230
|
const run = (...args) => [node.env, shellQuote(node.path), shellQuote(script), ...args].filter(Boolean).join(" ");
|
|
58
231
|
const written = skill.scriptAlias === void 0 ? "`node <this skill's folder>/scripts/artifact.mjs <command>`" : `\`node $${skill.scriptAlias} <command>\` (\`${skill.scriptAlias}\` being \`<this skill's folder>/scripts/artifact.mjs\`)`;
|
|
59
232
|
const lines = [
|
|
@@ -97,14 +270,15 @@ function runtimeSection(skill, skillDir, node) {
|
|
|
97
270
|
}
|
|
98
271
|
return lines.join("\n");
|
|
99
272
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
273
|
+
var COMMENT_SOURCE = "baoyu-design-comment";
|
|
274
|
+
var SUMMARY_MAX = 120;
|
|
275
|
+
var SESSION_CHECK_MS = 6e4;
|
|
276
|
+
var RETRY_MS = 5e3;
|
|
277
|
+
var MAX_QUICK_FAILURES = 3;
|
|
278
|
+
var IDLE_CHECK_MS = 500;
|
|
279
|
+
var AGENT_GRACE_MS = 3e4;
|
|
280
|
+
var TURN_START_MS = 3e3;
|
|
281
|
+
var TURN_MAX_MS = 30 * 6e4;
|
|
108
282
|
function jsonObject(text) {
|
|
109
283
|
const trimmed = text.trim();
|
|
110
284
|
const starts = [0];
|
|
@@ -122,6 +296,40 @@ function jsonObject(text) {
|
|
|
122
296
|
}
|
|
123
297
|
return null;
|
|
124
298
|
}
|
|
299
|
+
function jsonEnd(text, start) {
|
|
300
|
+
let depth = 0;
|
|
301
|
+
let inString = false;
|
|
302
|
+
for (let i = start; i < text.length; i++) {
|
|
303
|
+
const c = text[i];
|
|
304
|
+
if (inString) {
|
|
305
|
+
if (c === "\\") i++;
|
|
306
|
+
else if (c === '"') inString = false;
|
|
307
|
+
} else if (c === '"') inString = true;
|
|
308
|
+
else if (c === "{" || c === "[") depth++;
|
|
309
|
+
else if ((c === "}" || c === "]") && --depth === 0) return i;
|
|
310
|
+
}
|
|
311
|
+
return -1;
|
|
312
|
+
}
|
|
313
|
+
function jsonObjects(text) {
|
|
314
|
+
const found = [];
|
|
315
|
+
const starts = text.startsWith("{") ? [0] : [];
|
|
316
|
+
for (let i = text.indexOf("\n{"); i >= 0; i = text.indexOf("\n{", i + 1)) starts.push(i + 1);
|
|
317
|
+
let after = 0;
|
|
318
|
+
for (const start of starts) {
|
|
319
|
+
if (start < after) continue;
|
|
320
|
+
const end = jsonEnd(text, start);
|
|
321
|
+
if (end < 0) continue;
|
|
322
|
+
try {
|
|
323
|
+
const value = JSON.parse(text.slice(start, end + 1));
|
|
324
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
325
|
+
found.push(value);
|
|
326
|
+
after = end + 1;
|
|
327
|
+
}
|
|
328
|
+
} catch {
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return found;
|
|
332
|
+
}
|
|
125
333
|
function bashStdout(result) {
|
|
126
334
|
const value = result.value;
|
|
127
335
|
if (typeof value === "string") return value;
|
|
@@ -132,7 +340,19 @@ function bashStdout(result) {
|
|
|
132
340
|
const text = result.content.map((block) => block.type === "text" ? block.text ?? "" : "").join("\n");
|
|
133
341
|
return text || null;
|
|
134
342
|
}
|
|
135
|
-
|
|
343
|
+
function openedEditor(script, exec, result) {
|
|
344
|
+
if (exec.name !== "bash" || exec.agent === void 0 || result.isError) return null;
|
|
345
|
+
const args = exec.arguments;
|
|
346
|
+
const command = args?.command;
|
|
347
|
+
if (typeof command !== "string" || args?.run_in_background === true) return null;
|
|
348
|
+
const at = command.indexOf(script);
|
|
349
|
+
if (at < 0 || !/(?:^|[\s"'])open(?:[\s"']|$)/.test(command.slice(at + script.length))) return null;
|
|
350
|
+
const stdout = bashStdout(result);
|
|
351
|
+
const opened = stdout === null ? void 0 : jsonObjects(stdout).findLast((o) => typeof o.root === "string" && typeof o.url === "string");
|
|
352
|
+
if (typeof opened?.root !== "string" || typeof opened.url !== "string") return null;
|
|
353
|
+
return { session: String(exec.agent.id), root: opened.root, url: opened.url };
|
|
354
|
+
}
|
|
355
|
+
var bound = (text) => text.length <= SUMMARY_MAX ? text : `${text.slice(0, SUMMARY_MAX - 1)}…`;
|
|
136
356
|
function commentMessage(skill, plugin, out, root) {
|
|
137
357
|
const title = typeof out.title === "string" && out.title ? out.title : root.split(/[\\/]/).pop() || root;
|
|
138
358
|
const url = typeof out.url === "string" ? out.url : "";
|
|
@@ -183,7 +403,7 @@ function commentMessage(skill, plugin, out, root) {
|
|
|
183
403
|
};
|
|
184
404
|
return Object.freeze(message);
|
|
185
405
|
}
|
|
186
|
-
|
|
406
|
+
var CommentWatcher = class {
|
|
187
407
|
watches = /* @__PURE__ */ new Map();
|
|
188
408
|
check;
|
|
189
409
|
ctx;
|
|
@@ -204,23 +424,10 @@ class CommentWatcher {
|
|
|
204
424
|
agent(session) {
|
|
205
425
|
return this.ctx.get("agents")?.get(session);
|
|
206
426
|
}
|
|
207
|
-
/** The agent ran this skill's `open`: see whether its output names an editor, and wait for its comments. */
|
|
208
|
-
observe(exec, result) {
|
|
209
|
-
if (exec.name !== "bash" || exec.agent === void 0 || result.isError) return;
|
|
210
|
-
const args = exec.arguments;
|
|
211
|
-
const command = args?.command;
|
|
212
|
-
if (typeof command !== "string" || args?.run_in_background === true) return;
|
|
213
|
-
const at = command.indexOf(this.script);
|
|
214
|
-
if (at < 0 || !/(?:^|[\s"'])open(?:[\s"']|$)/.test(command.slice(at + this.script.length))) return;
|
|
215
|
-
const stdout = bashStdout(result);
|
|
216
|
-
const opened = stdout === null ? null : jsonObject(stdout);
|
|
217
|
-
if (typeof opened?.root !== "string" || typeof opened.url !== "string") return;
|
|
218
|
-
this.watch(String(exec.agent.id), opened.root);
|
|
219
|
-
}
|
|
220
427
|
watch(session, root) {
|
|
221
428
|
const key = `${session}\0${root}`;
|
|
222
429
|
if (this.watches.has(key)) return;
|
|
223
|
-
const watch = { child: null, stopped: false, quickFailures: 0 };
|
|
430
|
+
const watch = { child: null, stopped: false, quickFailures: 0, graceUntil: Date.now() + AGENT_GRACE_MS };
|
|
224
431
|
this.watches.set(key, watch);
|
|
225
432
|
this.log("info", `waiting for comments on ${root}`);
|
|
226
433
|
this.run(key, session, root, watch);
|
|
@@ -235,15 +442,15 @@ class CommentWatcher {
|
|
|
235
442
|
this.log("info", `stopped waiting for comments (${why})`);
|
|
236
443
|
}
|
|
237
444
|
run(key, session, root, watch) {
|
|
445
|
+
watch.retry = void 0;
|
|
238
446
|
if (watch.stopped) return;
|
|
239
|
-
if (this.agent(session) === void 0)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (name2) env[name2] = value ?? "";
|
|
447
|
+
if (this.agent(session) === void 0) {
|
|
448
|
+
if (Date.now() >= watch.graceUntil) return this.stop(key, watch, "the session is gone");
|
|
449
|
+
watch.retry = setTimeout(() => this.run(key, session, root, watch), IDLE_CHECK_MS);
|
|
450
|
+
return;
|
|
244
451
|
}
|
|
245
452
|
const started = Date.now();
|
|
246
|
-
const child =
|
|
453
|
+
const child = spawn2(this.node.path, [this.script, "comments", root, "--wait"], { env: scriptEnv(this.node), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
247
454
|
watch.child = child;
|
|
248
455
|
let stdout = "";
|
|
249
456
|
let stderr = "";
|
|
@@ -302,14 +509,14 @@ class CommentWatcher {
|
|
|
302
509
|
dropGoneSessions() {
|
|
303
510
|
for (const [key, watch] of [...this.watches]) {
|
|
304
511
|
const session = key.slice(0, key.indexOf("\0"));
|
|
305
|
-
if (this.agent(session) === void 0) this.stop(key, watch, "the session is gone");
|
|
512
|
+
if (this.agent(session) === void 0 && Date.now() >= watch.graceUntil) this.stop(key, watch, "the session is gone");
|
|
306
513
|
}
|
|
307
514
|
}
|
|
308
515
|
dispose() {
|
|
309
516
|
clearInterval(this.check);
|
|
310
517
|
for (const [key, watch] of [...this.watches]) this.stop(key, watch, "the plugin stopped");
|
|
311
518
|
}
|
|
312
|
-
}
|
|
519
|
+
};
|
|
313
520
|
function skillBody(raw, path) {
|
|
314
521
|
const frontmatter = /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/u.exec(raw);
|
|
315
522
|
if (frontmatter === null) throw new Error(`${PLUGIN.name}: ${path} has no YAML frontmatter`);
|
|
@@ -319,8 +526,8 @@ function apply(ctx, config = {}) {
|
|
|
319
526
|
const node = resolveNode(config ?? {});
|
|
320
527
|
const skills = new Map(PLUGIN.skills.map((skill) => [skill.name, skill]));
|
|
321
528
|
const candidates = PLUGIN.skills.map(({ name: skillName, description }) => {
|
|
322
|
-
const directory =
|
|
323
|
-
const path =
|
|
529
|
+
const directory = join2(packageRoot, "skills", skillName);
|
|
530
|
+
const path = join2(directory, "SKILL.md");
|
|
324
531
|
if (!isFile(path)) throw new Error(`${PLUGIN.name}: ${path} is missing`);
|
|
325
532
|
return {
|
|
326
533
|
name: skillName,
|
|
@@ -347,11 +554,24 @@ function apply(ctx, config = {}) {
|
|
|
347
554
|
};
|
|
348
555
|
ctx.skills.registerProvider(() => provider);
|
|
349
556
|
for (const skill of PLUGIN.skills) {
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
ctx.on("tools/result", (exec, result) =>
|
|
354
|
-
|
|
557
|
+
const script = join2(packageRoot, "skills", skill.name, "scripts", "artifact.mjs");
|
|
558
|
+
const watcher = skill.comments ? new CommentWatcher(ctx, skill, script, node) : null;
|
|
559
|
+
if (watcher) ctx.effect(() => () => watcher.dispose(), `${PLUGIN.name}: comment waits`);
|
|
560
|
+
ctx.on("tools/result", (exec, result) => {
|
|
561
|
+
const opened = openedEditor(script, exec, result);
|
|
562
|
+
if (opened === null) return;
|
|
563
|
+
watcher?.watch(opened.session, opened.root);
|
|
564
|
+
try {
|
|
565
|
+
recordOpened(skill.name, PLUGIN.provider, opened.session, opened.root);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
ctx.logger?.warn?.(`${PLUGIN.name}: could not note ${opened.root} for the Artifacts page: ${error.message}`);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
ctx.inject?.(["connection"], (sub) => {
|
|
571
|
+
registerRoutes(sub.connection.fetch, { name: skill.name, plugin: PLUGIN.provider, script, node }, (record) => {
|
|
572
|
+
watcher?.watch(record.sessionId, record.root);
|
|
573
|
+
});
|
|
574
|
+
});
|
|
355
575
|
}
|
|
356
576
|
}
|
|
357
577
|
export {
|
|
@@ -360,7 +580,9 @@ export {
|
|
|
360
580
|
commentMessage,
|
|
361
581
|
inject,
|
|
362
582
|
jsonObject,
|
|
583
|
+
jsonObjects,
|
|
363
584
|
name,
|
|
585
|
+
openedEditor,
|
|
364
586
|
resolveNode,
|
|
365
587
|
runtimeSection
|
|
366
588
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-claude-design",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "DeepSeek Harness plugin: the claude-design skill from baoyu-design. The agent writes canvases of artboards as files and opens them in a local canvas editor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -51,11 +51,17 @@
|
|
|
51
51
|
"client": {
|
|
52
52
|
"platform": "web",
|
|
53
53
|
"inject": [
|
|
54
|
+
"@deepseek-ai/dsh-api-session-controller",
|
|
55
|
+
"@deepseek-ai/dsh-api-workspace-controller",
|
|
56
|
+
"@deepseek-ai/dsh-client-connection",
|
|
54
57
|
"@deepseek-ai/dsh-client-locale",
|
|
55
58
|
"@deepseek-ai/dsh-client-ui-chat",
|
|
56
59
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
60
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
57
61
|
"@deepseek-ai/dsh-client-ui-renderer",
|
|
58
|
-
"@deepseek-ai/dsh-client-ui-sidebar
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-workspace"
|
|
59
65
|
]
|
|
60
66
|
}
|
|
61
67
|
}
|
|
@@ -63,6 +63,8 @@ leave alone stays as it is, and removing an artboard's file is deleting it. The
|
|
|
63
63
|
- `check <root>` → `problems` (0: done); when any, `issues`, each a `file` under `project/` and its fix; `designSystemNotes` when an install looks incomplete; it exits 1 while problems remain. It reads the files only: whether scripts run and how the layout looks is not checked.
|
|
64
64
|
- `open <root>` → `{url, pid, stop, …}`: starts the editor page on this machine in the background (once; later calls reuse it, `reused: true`) and prints its address, 127.0.0.1:4720 (the next free port up when 4720 is taken; `--port n` starts from n, `--port 0` takes any). `stop <root>` ends it (`serve <root>` runs it in the foreground instead). The page saves the user's edits into the same files, and shows yours as soon as you write them. `.artifact-server.json` and `.artifact-server.log` beside `project/` are its own: leave them; so is `comments.json` (the comments, see Comments), which only the comment commands below change.
|
|
65
65
|
- `share <root>` (or `open <root> --lan`): opens the page to the local network and prints a link per role (Sharing, below).
|
|
66
|
+
- `archive <root> [--out file.zip|dir]` → `{file, files, bytes, open}`: the whole folder, history and comments too, as one zip beside it (`--out`: another file or folder), to back it up, move it to another computer or send it to someone; unzipped anywhere, `open` carries on. The owner's Download folder button in the page (between History and Share) does the same. The server's own files and the share links stay out.
|
|
67
|
+
- The Theme tab links to a read-only page of the system in use, `<editor>/__ds/<folder>/`: the copy under `project/ds/<folder>/` as this canvas carries it (its tokens, its components with their `README.md` and types, its `README.md`). Anyone who can open the canvas can open it, and it changes only when the copy does.
|
|
66
68
|
|
|
67
69
|
## One artboard: the skeleton
|
|
68
70
|
|
|
@@ -173,7 +175,7 @@ People comment in the open editor: Comment (or C), then a click on anything on a
|
|
|
173
175
|
- `share <root> --disable <role>` / `--enable <role>` turns a link off or on (`<role>`: one, `editor,viewer`, or `all`); off, it holds everyone who came by it until it is on again. `--reset <role>` gives the role a new link: the old one stops working and the people who came by it are out. `--role <guest>=<role>` gives someone who joined another role (`<guest>`: an id from the `guests` it prints). `--off` stops sharing on the network.
|
|
174
176
|
- Who is here: while someone else has the canvas open, their avatars sit before History; each one's card says which artboard they are working in and whether they are editing, and the canvas shows their cursors and selections. Someone editing an artboard you have in view gets you a short note; nothing is locked (two people on one artboard: see History). After you change a file you show there as Agent for about 20 s.
|
|
175
177
|
- Only the owner's page sends to you (Send to Agent); what guests comment is saved for you to read with `comments <root>`.
|
|
176
|
-
- The links and the people who joined are in `<root>/.sharing.json`: leave it. It is plain http on the local network: anyone there
|
|
178
|
+
- The links and the people who joined are in `<root>/.sharing.json`: leave it. It is plain http on the local network: anyone there can read what passes, and a link is the permission: whoever has one gets its role, so send each only to the people meant to have it, and `--reset` a role's link that went further (the Share panel says so too). When this computer has a Tailscale address (100.64.0.0/10), `share` also prints `tailscale`: the same links at that address, for people on the owner's tailnet from any network (the Share panel: Copy Tailscale link in a link's More menu). Nothing else reaches past the local network; there is no tunnel.
|
|
177
179
|
|
|
178
180
|
## History
|
|
179
181
|
|
|
@@ -185,6 +187,24 @@ The server keeps the canvas's versions in `<root>/history/` (leave it): who chan
|
|
|
185
187
|
- Two people on one artboard: when the page has unsaved edits on an artboard someone else just saved, the two are merged element by element: changes to different elements both stay, and where both changed the same thing the page's change wins ("Yours was kept") and the other person gets a notice. When the merge cannot work it out (an element added, removed or moved on one side while the other edited, both changed its script or head, or the artboard was deleted), a bar names who and which artboard, and nothing is saved until the person picks Use theirs (the page's edits on it go; for a deleted artboard, it is deleted here too), Keep mine (saved over theirs) or Keep both (the artboard takes theirs; the page's version becomes a new artboard beside it, `<name>-my-copy.dc.html`). A version saved over someone's change records whose change it replaced, and that person gets a notice on the page (Compare, Bring back, Dismiss). Compare shows only what that save replaced, and Bring back puts only that back: the other person's changes elsewhere on the artboard stay, and History says "Partly restored". It goes line by line in the file, so two changes on one line come back together. When that drops a change of theirs, the other person gets "Put my change back", which saves over it and counts as replacing a change again (their notice then brings back only what that replaced). Your changes on disk count too: they go through the same merge, and someone editing that artboard may get the bar with "Agent" and keep theirs, so check the file (or History) when it matters.
|
|
186
188
|
- Asked who changed something, read `<root>/history/index.json`: `versions`, oldest first, each `{n, at, by, user: {name}, changes}` (`by`: `"human"`, `"agent"` for you, `"system"` for the first; `changes`: path → sha256, null for a delete; paths like `project/<name>.dc.html`, `project/canvas.json`), with `restoredFrom` or `overwrote` when it brought a version back or replaced someone's change; a content is `<root>/history/objects/<sha256>`, the file's bytes. To put something back, point the person to History (it records who did it), or write the old content yourself, which makes a version of yours.
|
|
187
189
|
|
|
190
|
+
## Keeping it in git
|
|
191
|
+
|
|
192
|
+
`open` writes `<root>/.gitignore` when the folder has none. It leaves out what only this computer's server needs:
|
|
193
|
+
`.artifact-server.json`, `.artifact-server.log`, `.sharing.json` (the share links), `history/` (the versions; git
|
|
194
|
+
keeps its own), save temp files and `.DS_Store`. Everything else, `project/` and `comments.json` included, is the
|
|
195
|
+
canvas and goes in git. A `.gitignore` already there is the person's: leave it. Moving or backing up the canvas is
|
|
196
|
+
copying or cloning the folder (`archive` puts it,
|
|
197
|
+
history included, in one zip); `open` there starts it again.
|
|
198
|
+
|
|
199
|
+
Commit, push or set up git only when asked. Asked to check the canvas before each commit: write
|
|
200
|
+
`.git/hooks/pre-commit` (and `chmod +x` it) in the repository that holds `<root>`, with this skill's folder as an
|
|
201
|
+
absolute path and `<root>` relative to the repository's top (a hook already there: add the line to it):
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
#!/bin/sh
|
|
205
|
+
node "<this skill's folder>/scripts/artifact.mjs" check "<root>" > /dev/null || { echo "The canvas has problems: run artifact.mjs check <root> to see them." >&2; exit 1; }
|
|
206
|
+
```
|
|
207
|
+
|
|
188
208
|
## The references
|
|
189
209
|
|
|
190
210
|
Under `artifact-type/reference/` in this skill's folder: `format.md` (before a first artboard), `craft.md`, `diagrams.md` (FIRST for diagrams), `print.md` (FIRST for print), `questions.md`, `view-state.md` (before
|
|
@@ -62,13 +62,19 @@ canvas made from the type, the same keys of its `boards` entry in `project/canva
|
|
|
62
62
|
there, not on an inner box.
|
|
63
63
|
- Breaks are placed by the exporter, not by CSS: it cuts BETWEEN text
|
|
64
64
|
lines and never through an `<img>`/`<svg>` (place photos as `<img>`,
|
|
65
|
-
never `background-image` — those get cut)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
`<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
never `background-image` — those get cut). A heading (h1–h6) is kept
|
|
66
|
+
whole and on the same page as the first line after it, along with a
|
|
67
|
+
short line just above it such as a section number, and a table's
|
|
68
|
+
`<caption>` and `<thead>` row stay with the row they label, except
|
|
69
|
+
where that would leave more than a quarter of a page empty. Nothing
|
|
70
|
+
else is kept whole — a bordered box, table or card can split between two of
|
|
71
|
+
its lines. `break-before/inside`, `orphans`/`widows` and `@page` do
|
|
72
|
+
nothing here, a `<thead>` is not repeated on later pages, and you
|
|
73
|
+
cannot force a new page. A `<figcaption>` above its image stays with
|
|
74
|
+
it; one below an image taller than ≈230px can land alone on the next
|
|
75
|
+
page, so caption tall images from above. An image taller than ≈950px
|
|
76
|
+
is cut where the page ends. Where a split or a forced page start
|
|
77
|
+
matters, make a Fixed series instead.
|
|
72
78
|
- ONE column of running text: CSS `columns` and side-by-side text columns
|
|
73
79
|
run the whole document's height and are sliced across pages — use them
|
|
74
80
|
only inside a block shorter than a page.
|