howone 0.1.52 → 0.2.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/bin/index.mjs +362 -5
- package/package.json +1 -1
- package/templates/vite/.howone/skills/web-clone/LICENSE +21 -0
- package/templates/vite/.howone/skills/web-clone/README.md +179 -0
- package/templates/vite/.howone/skills/web-clone/SKILL.md +243 -0
- package/templates/vite/.howone/skills/web-clone/references/assessment.md +77 -0
- package/templates/vite/.howone/skills/web-clone/references/complex-playbooks.md +46 -0
- package/templates/vite/.howone/skills/web-clone/references/deliverables.md +144 -0
- package/templates/vite/.howone/skills/web-clone/references/design-dna.md +125 -0
- package/templates/vite/.howone/skills/web-clone/references/effect-extraction.md +73 -0
- package/templates/vite/.howone/skills/web-clone/references/marbles-case.md +31 -0
- package/templates/vite/.howone/skills/web-clone/references/reverse-engineering.md +34 -0
- package/templates/vite/.howone/skills/web-clone/references/static-mirror.md +72 -0
- package/templates/vite/.howone/skills/web-clone/scripts/asset-harvest.mjs +101 -0
- package/templates/vite/.howone/skills/web-clone/scripts/audit-clone.mjs +151 -0
- package/templates/vite/.howone/skills/web-clone/scripts/compare-recon.mjs +265 -0
- package/templates/vite/.howone/skills/web-clone/scripts/dna-scaffold.mjs +214 -0
- package/templates/vite/.howone/skills/web-clone/scripts/init-clone.mjs +136 -0
- package/templates/vite/.howone/skills/web-clone/scripts/interaction-probe.mjs +314 -0
- package/templates/vite/.howone/skills/web-clone/scripts/lib/playwright-loader.mjs +39 -0
- package/templates/vite/.howone/skills/web-clone/scripts/mirror-site.mjs +121 -0
- package/templates/vite/.howone/skills/web-clone/scripts/network-capture.mjs +127 -0
- package/templates/vite/.howone/skills/web-clone/scripts/recon-site.mjs +235 -0
- package/templates/vite/.howone/skills/web-clone/scripts/route-crawl.mjs +228 -0
- package/templates/vite/.howone/skills/web-clone/scripts/sourcemap-hunt.mjs +112 -0
- package/templates/vite/.howone/skills/web-clone/scripts/visual-diff.mjs +161 -0
package/bin/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
|
-
import { readdir, rename, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { readFile, readdir, rename, stat, writeFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { promisify } from "node:util";
|
|
@@ -62,6 +62,18 @@ async function main() {
|
|
|
62
62
|
await runDeploy(command.options);
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
|
+
if (scope === "capabilities" || scope === "capability" || scope === "caps") {
|
|
66
|
+
await runCapabilitiesCommand(subcommand, rest, command.options);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (scope === "skills" || scope === "skill") {
|
|
70
|
+
await runSkillsCommand(subcommand, rest, command.options);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (scope === "run") {
|
|
74
|
+
await runCapabilityRun(compact([subcommand, ...rest]), command.options);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
65
77
|
if (scope === "ai" && subcommand === "run") {
|
|
66
78
|
await runAiRun(rest, command.options);
|
|
67
79
|
return;
|
|
@@ -87,9 +99,22 @@ function parseCommand(args) {
|
|
|
87
99
|
"next",
|
|
88
100
|
"force",
|
|
89
101
|
"yes",
|
|
90
|
-
"json"
|
|
102
|
+
"json",
|
|
103
|
+
"stream"
|
|
91
104
|
],
|
|
92
|
-
string: [
|
|
105
|
+
string: [
|
|
106
|
+
"template",
|
|
107
|
+
"cwd",
|
|
108
|
+
"scope",
|
|
109
|
+
"input",
|
|
110
|
+
"output",
|
|
111
|
+
"prompt",
|
|
112
|
+
"api-url",
|
|
113
|
+
"token",
|
|
114
|
+
"status",
|
|
115
|
+
"reason"
|
|
116
|
+
],
|
|
117
|
+
number: ["limit"],
|
|
93
118
|
configuration: { "camel-case-expansion": false }
|
|
94
119
|
});
|
|
95
120
|
return {
|
|
@@ -103,6 +128,16 @@ function parseCommand(args) {
|
|
|
103
128
|
yes: Boolean(parsed.yes),
|
|
104
129
|
json: Boolean(parsed.json),
|
|
105
130
|
cwd: typeof parsed.cwd === "string" ? parsed.cwd : void 0,
|
|
131
|
+
scope: typeof parsed.scope === "string" ? parsed.scope : void 0,
|
|
132
|
+
input: typeof parsed.input === "string" ? parsed.input : void 0,
|
|
133
|
+
output: typeof parsed.output === "string" ? parsed.output : void 0,
|
|
134
|
+
prompt: typeof parsed.prompt === "string" ? parsed.prompt : void 0,
|
|
135
|
+
apiUrl: typeof parsed["api-url"] === "string" ? parsed["api-url"] : void 0,
|
|
136
|
+
token: typeof parsed.token === "string" ? parsed.token : void 0,
|
|
137
|
+
stream: Boolean(parsed.stream),
|
|
138
|
+
status: typeof parsed.status === "string" ? parsed.status : void 0,
|
|
139
|
+
limit: typeof parsed.limit === "number" && Number.isFinite(parsed.limit) ? parsed.limit : void 0,
|
|
140
|
+
reason: typeof parsed.reason === "string" ? parsed.reason : void 0,
|
|
106
141
|
help: Boolean(parsed.help) || hasHelp,
|
|
107
142
|
version: Boolean(parsed.version) || hasVersion
|
|
108
143
|
}
|
|
@@ -182,6 +217,244 @@ async function runAiRun(pathArgs, options) {
|
|
|
182
217
|
}
|
|
183
218
|
printPending("howone ai run", result.message ?? "AI run is not implemented yet.");
|
|
184
219
|
}
|
|
220
|
+
async function runCapabilitiesCommand(subcommand, pathArgs, options) {
|
|
221
|
+
if (!subcommand || subcommand === "search") {
|
|
222
|
+
const query = pathArgs.join(" ").trim();
|
|
223
|
+
if (!query) throw new CliError("E_QUERY_REQUIRED", "Search query is required.");
|
|
224
|
+
await runCapabilitySearch(query, options);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (subcommand === "list") {
|
|
228
|
+
await runCapabilityList(options);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (subcommand === "inspect" || subcommand === "show") {
|
|
232
|
+
const ref = pathArgs[0];
|
|
233
|
+
if (!ref) throw new CliError("E_CAPABILITY_REF_REQUIRED", "Capability reference is required.");
|
|
234
|
+
await runCapabilityInspect(ref, options);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (subcommand === "proposals") {
|
|
238
|
+
await runCapabilityProposals(options);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (subcommand === "apply") {
|
|
242
|
+
const proposalId = pathArgs[0];
|
|
243
|
+
if (!proposalId) throw new CliError("E_PROPOSAL_ID_REQUIRED", "Capability proposal id is required.");
|
|
244
|
+
await runCapabilityProposalAction("apply", proposalId, options);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (subcommand === "reject") {
|
|
248
|
+
const proposalId = pathArgs[0];
|
|
249
|
+
if (!proposalId) throw new CliError("E_PROPOSAL_ID_REQUIRED", "Capability proposal id is required.");
|
|
250
|
+
await runCapabilityProposalAction("reject", proposalId, options);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
throw new CliError("E_COMMAND_UNKNOWN", `Unknown capabilities command: ${["capabilities", subcommand].join(" ")}`);
|
|
254
|
+
}
|
|
255
|
+
async function runSkillsCommand(subcommand, pathArgs, options) {
|
|
256
|
+
const ref = pathArgs[0];
|
|
257
|
+
if (subcommand === "export") {
|
|
258
|
+
if (!ref) throw new CliError("E_SKILL_REF_REQUIRED", "Skill reference is required.");
|
|
259
|
+
await runSkillProjection("export", ref, options);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (subcommand === "install") {
|
|
263
|
+
if (!ref) throw new CliError("E_SKILL_REF_REQUIRED", "Skill reference is required.");
|
|
264
|
+
await runSkillProjection("install", ref, options);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (subcommand === "publish") {
|
|
268
|
+
if (!ref) throw new CliError("E_SKILL_REF_REQUIRED", "Skill reference is required.");
|
|
269
|
+
await runSkillProjection("publish", ref, options);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
throw new CliError("E_COMMAND_UNKNOWN", `Unknown skills command: ${["skills", subcommand].filter(Boolean).join(" ")}`);
|
|
273
|
+
}
|
|
274
|
+
async function runCapabilitySearch(query, options) {
|
|
275
|
+
const params = new URLSearchParams({ q: query });
|
|
276
|
+
if (options.scope) params.set("scope", options.scope);
|
|
277
|
+
await printRegistryResult("capabilities search", await callHowoneRegistryApi(`/api/capabilities/search?${params}`, {
|
|
278
|
+
options,
|
|
279
|
+
requireAuth: options.scope !== "public"
|
|
280
|
+
}), options);
|
|
281
|
+
}
|
|
282
|
+
async function runCapabilityList(options) {
|
|
283
|
+
const params = new URLSearchParams();
|
|
284
|
+
if (options.scope) params.set("scope", options.scope);
|
|
285
|
+
await printRegistryResult("capabilities list", await callHowoneRegistryApi(`/api/capabilities${params.size ? `?${params}` : ""}`, {
|
|
286
|
+
options,
|
|
287
|
+
requireAuth: options.scope !== "public"
|
|
288
|
+
}), options);
|
|
289
|
+
}
|
|
290
|
+
async function runCapabilityInspect(ref, options) {
|
|
291
|
+
const result = await callHowoneRegistryApi(`/api/capabilities/${encodeURIComponent(ref)}`, {
|
|
292
|
+
options,
|
|
293
|
+
requireAuth: options.scope !== "public"
|
|
294
|
+
});
|
|
295
|
+
await printRegistryResult(`capabilities inspect ${ref}`, result, options);
|
|
296
|
+
}
|
|
297
|
+
async function runCapabilityProposals(options) {
|
|
298
|
+
const params = new URLSearchParams();
|
|
299
|
+
if (options.status) params.set("status", options.status);
|
|
300
|
+
if (options.limit) params.set("limit", String(options.limit));
|
|
301
|
+
await printRegistryResult("capabilities proposals", await callHowoneRegistryApi(`/api/capabilities/proposals${params.size ? `?${params}` : ""}`, {
|
|
302
|
+
options,
|
|
303
|
+
requireAuth: true
|
|
304
|
+
}), options);
|
|
305
|
+
}
|
|
306
|
+
async function runCapabilityProposalAction(action, proposalId, options) {
|
|
307
|
+
const result = await callHowoneRegistryApi(`/api/capabilities/proposals/${encodeURIComponent(proposalId)}/${action}`, {
|
|
308
|
+
options,
|
|
309
|
+
requireAuth: true,
|
|
310
|
+
init: {
|
|
311
|
+
method: "POST",
|
|
312
|
+
headers: { "Content-Type": "application/json" },
|
|
313
|
+
body: JSON.stringify(action === "reject" ? { reason: options.reason } : {})
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
await printRegistryResult(`capabilities ${action} ${proposalId}`, result, options);
|
|
317
|
+
}
|
|
318
|
+
async function runCapabilityRun(pathArgs, options) {
|
|
319
|
+
const actionRef = pathArgs[0];
|
|
320
|
+
if (!actionRef && !options.prompt) throw new CliError("E_ACTION_REF_REQUIRED", "Capability action reference is required, or use --prompt for query-based routing.");
|
|
321
|
+
const input = await readCapabilityRunInput(options);
|
|
322
|
+
const body = {
|
|
323
|
+
...actionRef ? { actionRef } : {},
|
|
324
|
+
...options.prompt ? { prompt: options.prompt } : {},
|
|
325
|
+
input,
|
|
326
|
+
stream: Boolean(options.stream)
|
|
327
|
+
};
|
|
328
|
+
const result = await callHowoneRegistryApi("/api/capabilities/run", {
|
|
329
|
+
options,
|
|
330
|
+
requireAuth: true,
|
|
331
|
+
init: {
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers: { "Content-Type": "application/json" },
|
|
334
|
+
body: JSON.stringify(body)
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
await printRegistryResult(actionRef ? `run ${actionRef}` : "run query", result, options);
|
|
338
|
+
}
|
|
339
|
+
async function runSkillProjection(action, ref, options) {
|
|
340
|
+
const method = action === "export" ? "GET" : "POST";
|
|
341
|
+
const result = await callHowoneRegistryApi(`/api/capabilities/skills/${encodeURIComponent(ref)}/${action}`, {
|
|
342
|
+
options,
|
|
343
|
+
requireAuth: action !== "export",
|
|
344
|
+
init: { method }
|
|
345
|
+
});
|
|
346
|
+
await printRegistryResult(`skills ${action} ${ref}`, result, options);
|
|
347
|
+
}
|
|
348
|
+
async function readCapabilityRunInput(options) {
|
|
349
|
+
if (!options.input) return {};
|
|
350
|
+
const raw = options.input.trim();
|
|
351
|
+
const content = raw.startsWith("{") || raw.startsWith("[") ? raw : await readFile(path.resolve(options.cwd ?? process.cwd(), raw), "utf-8");
|
|
352
|
+
try {
|
|
353
|
+
const parsed = JSON.parse(content);
|
|
354
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new CliError("E_INPUT_INVALID", "Capability input must be a JSON object.");
|
|
355
|
+
return parsed;
|
|
356
|
+
} catch (error) {
|
|
357
|
+
if (error instanceof CliError) throw error;
|
|
358
|
+
throw new CliError("E_INPUT_INVALID", error instanceof Error ? error.message : "Invalid JSON input.");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function resolveHowoneApiUrl(options) {
|
|
362
|
+
return (options.apiUrl ?? process.env.HOWONE_API_URL ?? process.env.NEXT_PUBLIC_SERVER_URL ?? "https://howone.dev").replace(/\/+$/, "");
|
|
363
|
+
}
|
|
364
|
+
function resolveHowoneApiToken(options) {
|
|
365
|
+
return options.token ?? process.env.HOWONE_API_TOKEN ?? process.env.HOWONE_AGENT_TOKEN;
|
|
366
|
+
}
|
|
367
|
+
async function callHowoneRegistryApi(apiPath, args) {
|
|
368
|
+
const token = resolveHowoneApiToken(args.options);
|
|
369
|
+
if (args.requireAuth && !token) throw new CliError("E_AUTH_REQUIRED", "Set HOWONE_API_TOKEN or pass --token to access private/workspace capabilities.");
|
|
370
|
+
const url = `${resolveHowoneApiUrl(args.options)}${apiPath.startsWith("/") ? apiPath : `/${apiPath}`}`;
|
|
371
|
+
const headers = new Headers(args.init?.headers);
|
|
372
|
+
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
373
|
+
headers.set("Accept", args.options.stream ? "text/event-stream, application/json" : "application/json");
|
|
374
|
+
const response = await fetch(url, {
|
|
375
|
+
...args.init,
|
|
376
|
+
headers
|
|
377
|
+
});
|
|
378
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
379
|
+
if (args.options.stream && contentType.includes("text/event-stream")) return readSseResponse(response);
|
|
380
|
+
const text = await response.text();
|
|
381
|
+
const body = text ? parseJsonOrText(text) : null;
|
|
382
|
+
if (!response.ok) throw new CliError("E_API_REQUEST_FAILED", `HowOne API request failed: HTTP ${response.status}`, {
|
|
383
|
+
status: response.status,
|
|
384
|
+
body
|
|
385
|
+
});
|
|
386
|
+
return body;
|
|
387
|
+
}
|
|
388
|
+
async function readSseResponse(response) {
|
|
389
|
+
if (!response.ok) throw new CliError("E_API_REQUEST_FAILED", `HowOne API request failed: HTTP ${response.status}`);
|
|
390
|
+
if (!response.body) return {
|
|
391
|
+
ok: true,
|
|
392
|
+
events: []
|
|
393
|
+
};
|
|
394
|
+
const reader = response.body.getReader();
|
|
395
|
+
const decoder = new TextDecoder();
|
|
396
|
+
const events = [];
|
|
397
|
+
let buffer = "";
|
|
398
|
+
while (true) {
|
|
399
|
+
const { value, done } = await reader.read();
|
|
400
|
+
if (done) break;
|
|
401
|
+
buffer += decoder.decode(value, { stream: true });
|
|
402
|
+
let boundary = buffer.indexOf("\n\n");
|
|
403
|
+
while (boundary >= 0) {
|
|
404
|
+
const chunk = buffer.slice(0, boundary).trim();
|
|
405
|
+
buffer = buffer.slice(boundary + 2);
|
|
406
|
+
if (chunk) {
|
|
407
|
+
events.push(chunk);
|
|
408
|
+
console.log(chunk);
|
|
409
|
+
}
|
|
410
|
+
boundary = buffer.indexOf("\n\n");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const tail = buffer.trim();
|
|
414
|
+
if (tail) {
|
|
415
|
+
events.push(tail);
|
|
416
|
+
console.log(tail);
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
ok: true,
|
|
420
|
+
events
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function parseJsonOrText(text) {
|
|
424
|
+
try {
|
|
425
|
+
return JSON.parse(text);
|
|
426
|
+
} catch {
|
|
427
|
+
return text;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async function printRegistryResult(command, result, options) {
|
|
431
|
+
if (options.output) await writeFile(path.resolve(options.cwd ?? process.cwd(), options.output), `${JSON.stringify(result, null, 2)}\n`, "utf-8");
|
|
432
|
+
if (options.json || options.output) {
|
|
433
|
+
if (!options.output) console.log(JSON.stringify(result, null, 2));
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
console.log(formatPanel(pc.bold(command), [formatRegistrySummary(result)]));
|
|
437
|
+
}
|
|
438
|
+
function formatRegistrySummary(result) {
|
|
439
|
+
if (typeof result === "string") return result;
|
|
440
|
+
if (!result || typeof result !== "object") return String(result);
|
|
441
|
+
const record = result;
|
|
442
|
+
const data = record.data ?? record.results ?? record.capabilities ?? record.actions;
|
|
443
|
+
if (Array.isArray(data)) {
|
|
444
|
+
if (!data.length) return "No capabilities found.";
|
|
445
|
+
return data.slice(0, 12).map((item) => {
|
|
446
|
+
if (!item || typeof item !== "object") return `- ${String(item)}`;
|
|
447
|
+
const row = item;
|
|
448
|
+
const name = row.name ?? row.title ?? row.slug ?? row.actionRef ?? row.proposalSlug ?? row.id ?? "capability";
|
|
449
|
+
const description = row.description ?? row.summary ?? row.reason ?? row.status;
|
|
450
|
+
return `- ${String(name)}${description ? `: ${String(description)}` : ""}`;
|
|
451
|
+
}).join("\n");
|
|
452
|
+
}
|
|
453
|
+
const name = record.name ?? record.title ?? record.slug ?? record.actionRef ?? record.proposalSlug ?? record.id;
|
|
454
|
+
const description = record.description ?? record.summary ?? record.reason ?? record.status;
|
|
455
|
+
if (name || description) return [name ? String(name) : void 0, description ? String(description) : void 0].filter(Boolean).join("\n");
|
|
456
|
+
return JSON.stringify(result, null, 2);
|
|
457
|
+
}
|
|
185
458
|
async function resolveCreatePlan(pathArgs, options, cwd, isInteractive) {
|
|
186
459
|
const template = resolveTemplateOption(options);
|
|
187
460
|
const projectName = pathArgs[0] === "template" ? pathArgs[1] : pathArgs[0];
|
|
@@ -417,6 +690,18 @@ function printHelp(pathArgs) {
|
|
|
417
690
|
printReservedHelp("howone deploy", "Deploy support is reserved and not implemented yet.");
|
|
418
691
|
return;
|
|
419
692
|
}
|
|
693
|
+
if (topic === "capabilities" || topic === "capabilities search") {
|
|
694
|
+
printCapabilitiesHelp();
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (topic === "skills") {
|
|
698
|
+
printSkillsHelp();
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (topic === "run") {
|
|
702
|
+
printRunHelp();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
420
705
|
if (topic === "ai" || topic === "ai run") {
|
|
421
706
|
printReservedHelp("howone ai run [aiId]", "AI run support is reserved and not implemented yet.");
|
|
422
707
|
return;
|
|
@@ -429,6 +714,11 @@ function printRootHelp() {
|
|
|
429
714
|
"",
|
|
430
715
|
pc.bold("Commands"),
|
|
431
716
|
formatCommand("init app [name]", "Create an app from a template"),
|
|
717
|
+
formatCommand("capabilities search <query>", "Search callable HowOne capabilities"),
|
|
718
|
+
formatCommand("capabilities inspect <ref>", "Inspect a capability package or action"),
|
|
719
|
+
formatCommand("capabilities proposals", "List generated capability proposals"),
|
|
720
|
+
formatCommand("run <package.action>", "Run a callable capability action"),
|
|
721
|
+
formatCommand("skills export <ref>", "Export a skill projection for a capability"),
|
|
432
722
|
formatCommand("create", "Reserved placeholder"),
|
|
433
723
|
formatCommand("deploy", "Reserved placeholder"),
|
|
434
724
|
formatCommand("ai run [aiId]", "Reserved placeholder"),
|
|
@@ -439,8 +729,10 @@ function printRootHelp() {
|
|
|
439
729
|
"",
|
|
440
730
|
pc.bold("Examples"),
|
|
441
731
|
` ${pc.cyan("howone init app my-app --template vite")}`,
|
|
442
|
-
` ${pc.cyan("howone
|
|
443
|
-
` ${pc.cyan("howone
|
|
732
|
+
` ${pc.cyan("howone capabilities search \"海报生成\" --scope public")}`,
|
|
733
|
+
` ${pc.cyan("howone capabilities proposals --status pending_review")}`,
|
|
734
|
+
` ${pc.cyan("howone run poster-generator.createPoster --input input.json --output result.json")}`,
|
|
735
|
+
` ${pc.cyan("howone run --prompt \"帮我生成一张 AI 发布会海报\" --stream")}`
|
|
444
736
|
]));
|
|
445
737
|
}
|
|
446
738
|
function printInitHelp() {
|
|
@@ -469,6 +761,71 @@ function printReservedHelp(command, description) {
|
|
|
469
761
|
`${pc.yellow("Pending")} This command is available as a stable placeholder.`
|
|
470
762
|
]));
|
|
471
763
|
}
|
|
764
|
+
function printCapabilitiesHelp() {
|
|
765
|
+
console.log(formatPanel(`${pc.bold("HowOne capabilities")}`, [
|
|
766
|
+
`${pc.dim("Usage")} howone capabilities <command> [options]`,
|
|
767
|
+
"",
|
|
768
|
+
pc.bold("Commands"),
|
|
769
|
+
formatCommand("search <query>", "Search private, workspace, installed, or public capabilities"),
|
|
770
|
+
formatCommand("list", "List capabilities in a scope"),
|
|
771
|
+
formatCommand("inspect <ref>", "Inspect package/action schemas and runtime bindings"),
|
|
772
|
+
formatCommand("proposals", "List generated proposals for the current user"),
|
|
773
|
+
formatCommand("apply <proposalId>", "Apply a generated proposal to the registry"),
|
|
774
|
+
formatCommand("reject <proposalId>", "Reject a generated proposal"),
|
|
775
|
+
"",
|
|
776
|
+
pc.bold("Options"),
|
|
777
|
+
formatCommand("--scope <scope>", "private, workspace, installed, public, or global"),
|
|
778
|
+
formatCommand("--status <status>", "Filter proposal list by status"),
|
|
779
|
+
formatCommand("--limit <number>", "Limit result count"),
|
|
780
|
+
formatCommand("--reason <text>", "Reason for rejecting a proposal"),
|
|
781
|
+
formatCommand("--api-url <url>", "HowOne registry API origin"),
|
|
782
|
+
formatCommand("--token <token>", "Agent API token; defaults to HOWONE_API_TOKEN"),
|
|
783
|
+
formatCommand("--json", "Print machine-readable JSON output"),
|
|
784
|
+
"",
|
|
785
|
+
pc.bold("Examples"),
|
|
786
|
+
` ${pc.cyan("howone capabilities search \"海报生成\" --scope public")}`,
|
|
787
|
+
` ${pc.cyan("howone capabilities inspect poster-generator.createPoster")}`,
|
|
788
|
+
` ${pc.cyan("howone capabilities proposals --status pending_review")}`,
|
|
789
|
+
` ${pc.cyan("howone capabilities apply 00000000-0000-0000-0000-000000000000")}`
|
|
790
|
+
]));
|
|
791
|
+
}
|
|
792
|
+
function printRunHelp() {
|
|
793
|
+
console.log(formatPanel(`${pc.bold("Run a capability")}`, [
|
|
794
|
+
`${pc.dim("Usage")} howone run <package.action> [options]`,
|
|
795
|
+
"",
|
|
796
|
+
pc.bold("Options"),
|
|
797
|
+
formatCommand("--input <json|file>", "JSON object or path to JSON input file"),
|
|
798
|
+
formatCommand("--prompt <text>", "Natural-language query routed through the registry"),
|
|
799
|
+
formatCommand("--output <file>", "Write JSON result to a file"),
|
|
800
|
+
formatCommand("--stream", "Request SSE and print events as they arrive"),
|
|
801
|
+
formatCommand("--api-url <url>", "HowOne registry API origin"),
|
|
802
|
+
formatCommand("--token <token>", "Agent API token; defaults to HOWONE_API_TOKEN"),
|
|
803
|
+
formatCommand("--json", "Print machine-readable JSON output"),
|
|
804
|
+
"",
|
|
805
|
+
pc.bold("Examples"),
|
|
806
|
+
` ${pc.cyan("howone run poster-generator.createPoster --input input.json")}`,
|
|
807
|
+
` ${pc.cyan("howone run --prompt \"帮我生成一张 AI 发布会海报\" --stream")}`
|
|
808
|
+
]));
|
|
809
|
+
}
|
|
810
|
+
function printSkillsHelp() {
|
|
811
|
+
console.log(formatPanel(`${pc.bold("HowOne skill projections")}`, [
|
|
812
|
+
`${pc.dim("Usage")} howone skills <command> <ref> [options]`,
|
|
813
|
+
"",
|
|
814
|
+
pc.bold("Commands"),
|
|
815
|
+
formatCommand("export <ref>", "Export an agent-facing skill artifact"),
|
|
816
|
+
formatCommand("install <ref>", "Install a public or workspace skill projection"),
|
|
817
|
+
formatCommand("publish <ref>", "Request publication of a capability skill projection"),
|
|
818
|
+
"",
|
|
819
|
+
pc.bold("Options"),
|
|
820
|
+
formatCommand("--api-url <url>", "HowOne registry API origin"),
|
|
821
|
+
formatCommand("--token <token>", "Agent API token; defaults to HOWONE_API_TOKEN"),
|
|
822
|
+
formatCommand("--json", "Print machine-readable JSON output"),
|
|
823
|
+
"",
|
|
824
|
+
pc.bold("Examples"),
|
|
825
|
+
` ${pc.cyan("howone skills export poster-generator")}`,
|
|
826
|
+
` ${pc.cyan("howone skills install poster-generator")}`
|
|
827
|
+
]));
|
|
828
|
+
}
|
|
472
829
|
function printPending(command, message) {
|
|
473
830
|
console.log(formatPanel(pc.bold(command), [`${pc.yellow("Pending")} ${message}`]));
|
|
474
831
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jane (@xiaoerzhan / 小耳)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# 🪞 Web Clone · 网站复刻方法论
|
|
2
|
+
|
|
3
|
+
> Reproduce any website — from a single-file static page to a WebGL-heavy interactive demo — without faking it from AI hallucinations.
|
|
4
|
+
> 把任何一个网站(从单文件静态页到 WebGL 重前端 demo)忠实复刻出来 —— 不靠 AI 臆测代码硬抄。
|
|
5
|
+
|
|
6
|
+
**English** · [中文](#中文)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## What this skill does
|
|
11
|
+
|
|
12
|
+
You see a website you love. You want to clone it — to learn how it works, to remix it into your own thing, or to run it offline. AI tools love to produce *plausible-looking* "clone analysis" documents full of code blocks that are entirely fabricated and break the moment you run them.
|
|
13
|
+
|
|
14
|
+
This skill is a **methodology plus executable probes that put real source first**, with a 6-step decision tree covering:
|
|
15
|
+
|
|
16
|
+
- Static HTML/CSS sites → `wget --mirror`
|
|
17
|
+
- React / Vue / Next content sites → rebuild on a template
|
|
18
|
+
- Multi-page sites → crawl internal routes before designing templates
|
|
19
|
+
- Interactive sites → record hover / click / scroll / canvas-drag states before cloning
|
|
20
|
+
- **WebGL / Canvas / Three.js heavy frontends** → reverse-engineer real source, line by line; when there's no findable source, runtime frame-capture + a **baseline-first replay gate** with `SOURCE / PARTIAL / GUESS` evidence grading
|
|
21
|
+
- **Static-built sites (Astro / Vite SSG / Hugo), even WebGL-heavy** → `mirror-site.mjs` full-scroll-captures and mirrors every deployed asset (incl. runtime-fetched `.sog/.buf/.wasm`) for a true 1:1 clone — for a static site, "get the real source" = "mirror the whole deployed bundle"
|
|
22
|
+
- Verify in a real browser, document the truth, replace content with yours
|
|
23
|
+
- **Visual / rebrand modes** → distill a versionable `design-dna.json` (design tokens + style + effects), then "keep the DNA, swap the content"
|
|
24
|
+
|
|
25
|
+
## Iron rule: real source first, never copy AI-guessed code
|
|
26
|
+
|
|
27
|
+
> Any AI-written "clone analysis" — treat the conceptual skeleton as a hint, but **assume every executable code block is hallucinated** until you've verified it against real source code line by line.
|
|
28
|
+
|
|
29
|
+
**Case in point** (see `references/marbles-case.md`): A real WebGL marbles demo uses **analytic ray-sphere intersection** + an SVG `feDisplacementMap` to refract the live DOM. An AI analysis confidently described it as **ray-marching + SDF + sampling the DOM as a texture** — two entirely different implementations. Copying the AI version produces nothing like the original and runs many times slower.
|
|
30
|
+
|
|
31
|
+
So **step 1 is always: get the real source**.
|
|
32
|
+
|
|
33
|
+
## What's in here
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
claude-skill-web-clone/
|
|
37
|
+
├── SKILL.md ← The methodology (loaded by Claude Code / Codex / Cursor)
|
|
38
|
+
├── README.md
|
|
39
|
+
├── LICENSE ← MIT
|
|
40
|
+
├── follow-wechat-qrcode.jpg
|
|
41
|
+
└── references/
|
|
42
|
+
├── reverse-engineering.md ← How to dissect a WebGL/Canvas frontend, line by line
|
|
43
|
+
├── effect-extraction.md ← Evidence grading + baseline-first gate for extracting effects
|
|
44
|
+
├── static-mirror.md ← 1:1 clone of static-built sites (Astro/Vite SSG/Hugo) via full asset mirror
|
|
45
|
+
├── design-dna.md ← Structured design-identity layer (visual / rebrand modes)
|
|
46
|
+
├── marbles-case.md ← Flagship case: real architecture vs AI hallucination
|
|
47
|
+
└── deliverables.md ← NOTES.md / TEARDOWN.md / RECON/ templates
|
|
48
|
+
└── scripts/
|
|
49
|
+
├── recon-site.mjs ← Browser screenshots + DOM/framework/resource signals
|
|
50
|
+
├── mirror-site.mjs ← Full-scroll capture + mirror every same-origin asset (static-site 1:1 clone)
|
|
51
|
+
├── route-crawl.mjs ← Same-site route map + screenshot per route
|
|
52
|
+
├── interaction-probe.mjs ← Scroll/hover/click/canvas-drag state evidence
|
|
53
|
+
├── network-capture.mjs ← XHR/fetch capture for SPA fixtures
|
|
54
|
+
├── asset-harvest.mjs ← Download discovered source assets
|
|
55
|
+
├── dna-scaffold.mjs ← Build a design-dna.json skeleton, prefilled from recon
|
|
56
|
+
└── visual-diff.mjs ← Pixel comparison for original vs clone screenshots
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
This is a [Claude Code skill](https://docs.claude.com/en/docs/agents-and-tools/skills). Drop it into your skills directory:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone https://github.com/Jane-xiaoer/claude-skill-web-clone.git ~/.claude/skills/web-clone
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Then use it by saying things like:
|
|
68
|
+
- "复刻这个网站: https://..."
|
|
69
|
+
- "Clone this site for me"
|
|
70
|
+
- "Reverse-engineer this WebGL demo"
|
|
71
|
+
- "把这个站抄下来改成我的"
|
|
72
|
+
|
|
73
|
+
Claude / Codex will load `SKILL.md` and walk the decision tree.
|
|
74
|
+
|
|
75
|
+
## The decision tree (one-screen summary)
|
|
76
|
+
|
|
77
|
+
| Step | What to do |
|
|
78
|
+
|---|---|
|
|
79
|
+
| **1** | `gh api search/repositories?q=<name>` — find the real source on GitHub FIRST |
|
|
80
|
+
| **2** | If no source: browser-probe the site, crawl routes, capture network, and probe interactions |
|
|
81
|
+
| **3** | Pick the path: `wget` mirror / template rebuild / **WebGL reverse-engineering** / theme market |
|
|
82
|
+
| **4** | Set up `~/projects/website-clones/<name>-clone/` (or your equivalent), keep `index-original.html` read-only |
|
|
83
|
+
| **5** | Strip tracking, write `NOTES.md` + `TEARDOWN.md`, verify in a real browser with screenshots |
|
|
84
|
+
| **6** | Replace text / media / brand colors with yours |
|
|
85
|
+
|
|
86
|
+
## License & attribution discipline
|
|
87
|
+
|
|
88
|
+
Before deploying anything publicly, **check the source repo's license**:
|
|
89
|
+
|
|
90
|
+
| License | What you can do |
|
|
91
|
+
|---|---|
|
|
92
|
+
| MIT / Apache / BSD / Unlicense | Modify, redeploy, just keep credits |
|
|
93
|
+
| **NONE / unstated** | Default = **All Rights Reserved**. Local learning only, must credit, **do not redeploy publicly without permission** |
|
|
94
|
+
| Proprietary / explicitly forbidden | Read-only learning, no copying, no deployment |
|
|
95
|
+
|
|
96
|
+
⚠️ "Code is on GitHub" ≠ "code is MIT". Many viral demos have no LICENSE file and default to All Rights Reserved.
|
|
97
|
+
|
|
98
|
+
## Flagship case
|
|
99
|
+
|
|
100
|
+
`marbles-case.md` documents the full reverse-engineering of [chiuhans111/marbles](https://github.com/chiuhans111/marbles) — a 1067-line single-file WebGL + SVG-filter glass marbles interactive demo — and contrasts the real architecture with an AI-generated clone analysis that got the core mechanism completely backwards. This is the canonical example of why you must verify against real source.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 中文
|
|
105
|
+
|
|
106
|
+
### 这个 skill 是干什么的
|
|
107
|
+
|
|
108
|
+
你看到一个喜欢的网站,想复刻 —— 学它怎么做、改成你自己的版本、或者离线跑起来。AI 工具最爱产出**看起来煞有其事**的"复刻分析文档",里面的代码块几乎全是臆造的,照抄必崩。
|
|
109
|
+
|
|
110
|
+
这个 skill 把"真源码至上"做成可重复的流程,**6 步决策树**覆盖三大分支:
|
|
111
|
+
- 静态 HTML/CSS 站 → `wget --mirror`
|
|
112
|
+
- React / Vue / Next 内容站 → 重建模板灌内容
|
|
113
|
+
- **WebGL / Canvas / Three.js 重前端** → 逆向真源码,逐行核对;找不到源码时用运行时帧捕获 + **baseline-first 复现闸门** + `SOURCE/PARTIAL/GUESS` 证据分级
|
|
114
|
+
- **静态构建站(Astro/Vite SSG/Hugo),哪怕重 WebGL** → `mirror-site.mjs` 全程滚动捕获 + 镜像每一个部署资产(含运行时 fetch 的 `.sog/.buf/.wasm`),做真 1:1 忠实复刻——对静态站,"拿到真源码"="镜像部署资产整套"(范例:Lusion oryzo.ai,高斯泼溅,hero 像素 diff 5/5)
|
|
115
|
+
- **视觉复刻 / 内容爆改模式** → 把"那个站的感觉"蒸馏成可版本化的 `design-dna.json`(设计 token + 风格 + 特效),然后"DNA 留着、内容换掉"
|
|
116
|
+
- 浏览器真验证,如实记录,最后替换成你自己的内容
|
|
117
|
+
|
|
118
|
+
### 头号铁律:真源码至上,绝不抄 AI 臆造代码
|
|
119
|
+
|
|
120
|
+
> 任何 AI 写的"复刻分析",**概念骨架可以参考,但里面的可执行代码块默认全是臆造的**,必须用真源码核对,否则照抄必崩。
|
|
121
|
+
|
|
122
|
+
**实证案例**:一份 AI 分析文档把原站"解析法求光线-球体交点 + 把光学结果编码成位移图 + 交给 SVG `feDisplacementMap` 扭曲真实 DOM"的真架构,臆造成了"ray-marching + SDF + 把 DOM 当纹理采样"—— 两套完全不同的实现,照抄做不出原效果且慢 N 倍。详见 `references/marbles-case.md`。
|
|
123
|
+
|
|
124
|
+
### 安装
|
|
125
|
+
|
|
126
|
+
这是一个 [Claude Code skill](https://docs.claude.com/en/docs/agents-and-tools/skills)。clone 到你的 skills 目录:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
git clone https://github.com/Jane-xiaoer/claude-skill-web-clone.git ~/.claude/skills/web-clone
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
然后跟 Claude / Codex / Cursor 说:
|
|
133
|
+
- 「复刻这个网站: https://...」
|
|
134
|
+
- 「把这个站抄下来改成我的」
|
|
135
|
+
- 「逆向拆解这个 WebGL demo」
|
|
136
|
+
|
|
137
|
+
AI 会自动加载 `SKILL.md` 走决策树。
|
|
138
|
+
|
|
139
|
+
### 旗舰范例
|
|
140
|
+
|
|
141
|
+
`references/marbles-case.md` 拆解了 [chiuhans111/marbles](https://github.com/chiuhans111/marbles) —— 1067 行单文件 WebGL + SVG filter 玻璃弹珠站,真架构对比 AI 分析文档的错误,是"为什么必须真源码核对"的标准教材。
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 致谢 / Credits
|
|
146
|
+
|
|
147
|
+
- **方法论沉淀来源 / Origin**:Jane 的克隆中枢 `~/projects/website-clones/` 工作流(2026-05-27 marbles 案例跑通后抽出 skill)
|
|
148
|
+
- **旗舰范例原作 / Flagship case author**:[chiuhans111/marbles](https://github.com/chiuhans111/marbles) by Hans Chiu — 没有这份单文件 1067 行的玻璃弹珠源码就没有这个 skill 的"真源码至上"铁律
|
|
149
|
+
- **设计身份层 schema / Design-DNA schema**:`references/design-dna.md` 的三维 DNA JSON 改编自 [zanwei/design-dna](https://github.com/zanwei/design-dna)(MIT),用于"视觉复刻 / 内容爆改"模式
|
|
150
|
+
- **特效提取纪律 / Effect-extraction discipline**:`references/effect-extraction.md` 的证据分级 + baseline-first 闸门受 [lixiaolin94/skills · web-shader-extractor](https://github.com/lixiaolin94/skills) 启发(该仓库无 LICENSE,本 skill 只借方法概念、用自己的话重写,未复制其代码或原文)
|
|
151
|
+
- **迭代打磨 / Iteration**:Jane(`@xiaoerzhan`) + Claude Code(多轮 reverse-engineering)
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT. Use it freely, remix it, ship it. Attribution welcome but not required.
|
|
158
|
+
|
|
159
|
+
如果这个 skill 让你免于在 AI 臆造代码上浪费一个下午,欢迎 ⭐ 这个仓库。
|
|
160
|
+
If this skill saves you a frustrating afternoon of debugging fake AI code, ⭐ the repo.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 📱 关注作者 / Follow Me
|
|
165
|
+
|
|
166
|
+
如果这个仓库对你有帮助,欢迎关注我。后面我会持续更新更多 AI Skill、设计方法、网站美学和创意工作流。
|
|
167
|
+
|
|
168
|
+
If this repo helped you, follow me for more AI skills, design systems, web aesthetics, and creative workflows.
|
|
169
|
+
|
|
170
|
+
- X (Twitter): [@xiaoerzhan](https://x.com/xiaoerzhan)
|
|
171
|
+
- 微信公众号 / WeChat Official Account: 扫码关注 / Scan to follow
|
|
172
|
+
|
|
173
|
+
<p align="center">
|
|
174
|
+
<img src="./follow-wechat-qrcode.jpg" alt="Jane WeChat Official Account QR code" width="300" />
|
|
175
|
+
</p>
|
|
176
|
+
|
|
177
|
+
<p align="center"><strong>中文:</strong>欢迎关注我的公众号,一起研究 AI Skill、设计原则、网站表达和创意工作流。</p>
|
|
178
|
+
|
|
179
|
+
<p align="center"><strong>English:</strong> Follow my WeChat Official Account for more AI skills, design principles, web aesthetics, and creative workflows.</p>
|