@mkterswingman/5mghost-wonder 0.0.13 → 0.0.14

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.
@@ -0,0 +1,76 @@
1
+ // src/commands/browser.ts
2
+ // Experimental browser runtime commands.
3
+ import { resolveWonderPaths } from "../platform/paths.js";
4
+ import { parseWecomUrl } from "../wecom/url.js";
5
+ import { runBrowserNoExportProbe } from "../wecom/browser-probe.js";
6
+ export async function runBrowserCommand(argv, context) {
7
+ const [sub, ...rest] = argv;
8
+ switch (sub) {
9
+ case "probe":
10
+ return runBrowserProbe(rest, context);
11
+ default:
12
+ context.io.stderr(`Unknown subcommand: browser ${sub ?? "(none)"}\n` +
13
+ "Usage: wonder browser probe <url> [--headed] [--timeout-ms <ms>] [--save <dir>]");
14
+ return { exitCode: 1 };
15
+ }
16
+ }
17
+ async function runBrowserProbe(args, context) {
18
+ let url;
19
+ let saveDir;
20
+ let timeoutMs;
21
+ let headed = false;
22
+ for (let i = 0; i < args.length; i++) {
23
+ const arg = args[i];
24
+ if (arg === "--save" && args[i + 1]) {
25
+ saveDir = args[++i];
26
+ }
27
+ else if (arg === "--timeout-ms" && args[i + 1]) {
28
+ timeoutMs = Number(args[++i]);
29
+ }
30
+ else if (arg === "--headed") {
31
+ headed = true;
32
+ }
33
+ else if (!arg.startsWith("-")) {
34
+ url = arg;
35
+ }
36
+ }
37
+ if (!url) {
38
+ context.io.stderr(JSON.stringify({
39
+ error: "missing_url",
40
+ message: "用法:wonder browser probe <url> [--headed] [--timeout-ms <ms>] [--save <dir>]",
41
+ }));
42
+ return { exitCode: 1, telemetry: { outcome: "failure", errorKind: "missing_url" } };
43
+ }
44
+ const parsed = parseWecomUrl(url);
45
+ if (!parsed.ok) {
46
+ context.io.stderr(JSON.stringify({
47
+ error: "invalid_url",
48
+ message: "无法识别的 WeCom URL",
49
+ }));
50
+ return { exitCode: 1, telemetry: { outcome: "failure", errorKind: "invalid_url" } };
51
+ }
52
+ if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs < 1000 || timeoutMs > 60_000)) {
53
+ context.io.stderr(JSON.stringify({
54
+ error: "invalid_timeout",
55
+ message: "--timeout-ms must be between 1000 and 60000",
56
+ }));
57
+ return { exitCode: 1, telemetry: { outcome: "failure", errorKind: "invalid_timeout" } };
58
+ }
59
+ const paths = resolveWonderPaths({ homeDir: context.homeDir });
60
+ const result = await runBrowserNoExportProbe({
61
+ url,
62
+ chromeProfilePath: paths.chromeProfilePath,
63
+ saveDir,
64
+ headed,
65
+ timeoutMs,
66
+ io: context.io,
67
+ });
68
+ context.io.stdout(JSON.stringify(result));
69
+ return {
70
+ exitCode: result.status === "fail" ? 1 : 0,
71
+ telemetry: {
72
+ outcome: result.status === "fail" ? "failure" : "success",
73
+ errorKind: result.status === "fail" ? "browser_probe_failed" : undefined,
74
+ },
75
+ };
76
+ }
@@ -30,6 +30,11 @@ export function renderHelpText() {
30
30
  " doc/slide → download file + path JSON",
31
31
  " read <url> --tab <name> Read specific xlsx tab (cells + merges + images)",
32
32
  " read <url> --save <dir> Output directory (default: ~/Downloads/5mghost-wonder/)",
33
+ "",
34
+ "Experimental browser runtime:",
35
+ " browser probe <url> Open in Wonder browser profile and capture",
36
+ " no-export evidence summaries",
37
+ " browser probe <url> --headed Show the browser while probing",
33
38
  ].join("\n");
34
39
  }
35
40
  export async function runHelpCommand(io) {
@@ -11,6 +11,7 @@ import { runSetupCommand } from "./setup.js";
11
11
  import { runAuthCommand } from "./auth.js";
12
12
  import { runWecom } from "./wecom.js";
13
13
  import { runReadCommand } from "./read.js";
14
+ import { runBrowserCommand } from "./browser.js";
14
15
  export async function dispatchWonderCommand(argv, context) {
15
16
  const [cmd, ...rest] = argv;
16
17
  switch (cmd) {
@@ -30,6 +31,8 @@ export async function dispatchWonderCommand(argv, context) {
30
31
  return runWecom(rest, context);
31
32
  case "read":
32
33
  return runRead(rest, context);
34
+ case "browser":
35
+ return runBrowserCommand(rest, context);
33
36
  case "--help":
34
37
  case "-h":
35
38
  case "help":
@@ -2,7 +2,7 @@
2
2
  // Factory for the wonder telemetry runtime.
3
3
  // Returns null on any construction error — telemetry must never crash the CLI.
4
4
  import { DEFAULT_AUTH_URL, TokenManager } from "@mkterswingman/5mghost-auth";
5
- import { TelemetryRuntime, TelemetrySender } from "@mkterswingman/5mghost-telemetry";
5
+ import { TelemetryRuntime, TelemetrySender, } from "@mkterswingman/5mghost-telemetry";
6
6
  import packageJson from "../../package.json" with { type: "json" };
7
7
  export function createWonderTelemetryRuntime(options = {}) {
8
8
  try {
@@ -17,11 +17,12 @@ export function createWonderTelemetryRuntime(options = {}) {
17
17
  authUrl,
18
18
  homeDir: options.homeDir,
19
19
  });
20
+ const sender = new SafeTelemetrySender(new TelemetrySender(tokenManager, ingestUrl));
20
21
  return new TelemetryRuntime({
21
22
  product: "5mghost-wonder",
22
23
  productVersion: packageJson.version,
23
24
  homeDir: options.homeDir,
24
- sender: new TelemetrySender(tokenManager, ingestUrl),
25
+ sender,
25
26
  });
26
27
  }
27
28
  catch {
@@ -29,3 +30,23 @@ export function createWonderTelemetryRuntime(options = {}) {
29
30
  return null;
30
31
  }
31
32
  }
33
+ class SafeTelemetrySender {
34
+ delegate;
35
+ constructor(delegate) {
36
+ this.delegate = delegate;
37
+ }
38
+ async sendBatch(events) {
39
+ try {
40
+ return await this.delegate.sendBatch(events);
41
+ }
42
+ catch (err) {
43
+ const errorName = err instanceof Error ? err.name : typeof err;
44
+ return {
45
+ disposition: {
46
+ retryable: true,
47
+ reason: `transport:${errorName}`,
48
+ },
49
+ };
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,402 @@
1
+ // src/wecom/browser-probe.ts
2
+ // Experimental browser runtime probe for no-export reads. This intentionally
3
+ // records evidence summaries, not raw network bodies or cookies.
4
+ import { spawn } from "node:child_process";
5
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
6
+ import { createServer } from "node:net";
7
+ import { join, resolve } from "node:path";
8
+ import WebSocket from "ws";
9
+ const CDP_CALL_TIMEOUT_MS = 5000;
10
+ const BROWSER_START_TIMEOUT_MS = 20_000;
11
+ const DEFAULT_CAPTURE_MS = 8_000;
12
+ const MAX_NETWORK_CANDIDATES = 40;
13
+ let cdpNextId = 1;
14
+ export async function runBrowserNoExportProbe(options) {
15
+ const timeoutMs = options.timeoutMs ?? DEFAULT_CAPTURE_MS;
16
+ const browser = findInstalledBrowser(options.executablePath);
17
+ const port = await getFreePort();
18
+ const endpoint = `http://127.0.0.1:${port}`;
19
+ const child = spawnBrowser({
20
+ browser,
21
+ port,
22
+ profilePath: options.chromeProfilePath,
23
+ url: options.url,
24
+ headed: options.headed ?? false,
25
+ });
26
+ try {
27
+ await waitForDebugger(endpoint, BROWSER_START_TIMEOUT_MS);
28
+ const pageWsUrl = await waitForPageWebSocket(endpoint, options.url, BROWSER_START_TIMEOUT_MS);
29
+ const socket = await openDevToolsSocket(pageWsUrl);
30
+ const responses = [];
31
+ try {
32
+ socket.addEventListener("message", (event) => {
33
+ const payload = safeJsonParse(String(event.data));
34
+ if (payload?.method !== "Network.responseReceived")
35
+ return;
36
+ const response = payload.params?.response;
37
+ const requestId = payload.params?.requestId;
38
+ if (!response?.url || !requestId)
39
+ return;
40
+ if (!isInterestingNetworkUrl(response.url, response.mimeType ?? ""))
41
+ return;
42
+ responses.push({
43
+ requestId,
44
+ url: response.url,
45
+ status: response.status ?? 0,
46
+ mimeType: response.mimeType ?? "",
47
+ });
48
+ });
49
+ await sendCdpCommand(socket, "Network.enable");
50
+ await sendCdpCommand(socket, "Page.enable");
51
+ await sendCdpCommand(socket, "Runtime.enable");
52
+ await sendCdpCommand(socket, "Page.navigate", { url: options.url });
53
+ await delay(timeoutMs);
54
+ const pageInfo = await evaluatePageInfo(socket);
55
+ const networkCandidates = await collectNetworkCandidates(socket, responses);
56
+ const evidence = ["browser-page"];
57
+ if (pageInfo.visibleTextSample)
58
+ evidence.push("visible-text");
59
+ if (pageInfo.runtimeGlobals.length > 0)
60
+ evidence.push("runtime-globals");
61
+ if (networkCandidates.length > 0)
62
+ evidence.push("network-response-summary");
63
+ const missing = [
64
+ "raw structured document model",
65
+ "merge ranges",
66
+ "image original resources",
67
+ "image anchors",
68
+ ];
69
+ const result = {
70
+ mode: "browser-no-export-probe",
71
+ status: "partial",
72
+ url: redactUrl(options.url),
73
+ finalUrl: pageInfo.finalUrl ? redactUrl(pageInfo.finalUrl) : undefined,
74
+ title: pageInfo.title,
75
+ loginLikely: pageInfo.loginLikely,
76
+ visibleTextSample: pageInfo.visibleTextSample,
77
+ runtimeGlobals: pageInfo.runtimeGlobals,
78
+ networkCandidates,
79
+ evidence,
80
+ missing,
81
+ };
82
+ if (options.saveDir) {
83
+ mkdirSync(options.saveDir, { recursive: true });
84
+ const savedPath = resolve(options.saveDir, `wonder-browser-probe-${Date.now()}.json`);
85
+ writeFileSync(savedPath, JSON.stringify(result, null, 2), { mode: 0o600 });
86
+ result.savedPath = savedPath;
87
+ }
88
+ return result;
89
+ }
90
+ finally {
91
+ socket.close();
92
+ }
93
+ }
94
+ catch (err) {
95
+ return {
96
+ mode: "browser-no-export-probe",
97
+ status: "fail",
98
+ url: redactUrl(options.url),
99
+ loginLikely: false,
100
+ runtimeGlobals: [],
101
+ networkCandidates: [],
102
+ evidence: [],
103
+ missing: [err instanceof Error ? err.message : String(err)],
104
+ };
105
+ }
106
+ finally {
107
+ await terminateBrowserProcess(child);
108
+ }
109
+ }
110
+ async function evaluatePageInfo(socket) {
111
+ const expression = `(() => {
112
+ const text = (document.body && document.body.innerText || "").replace(/\\s+/g, " ").trim();
113
+ const globals = Object.keys(window)
114
+ .filter((key) => /doc|sheet|wecom|weixin|store|redux|editor|canvas/i.test(key))
115
+ .slice(0, 80);
116
+ const loginLikely = /登录|扫码|二维码|login|sign in/i.test(text);
117
+ return {
118
+ finalUrl: location.href,
119
+ title: document.title,
120
+ loginLikely,
121
+ visibleTextSample: text.slice(0, 2000),
122
+ runtimeGlobals: globals
123
+ };
124
+ })()`;
125
+ const payload = await sendCdpCommand(socket, "Runtime.evaluate", {
126
+ expression,
127
+ returnByValue: true,
128
+ awaitPromise: false,
129
+ });
130
+ const value = payload.result?.value;
131
+ return {
132
+ finalUrl: value?.finalUrl,
133
+ title: value?.title,
134
+ loginLikely: value?.loginLikely ?? false,
135
+ visibleTextSample: value?.visibleTextSample,
136
+ runtimeGlobals: value?.runtimeGlobals ?? [],
137
+ };
138
+ }
139
+ async function collectNetworkCandidates(socket, responses) {
140
+ const unique = new Map();
141
+ for (const response of responses) {
142
+ if (unique.size >= MAX_NETWORK_CANDIDATES)
143
+ break;
144
+ unique.set(response.requestId, response);
145
+ }
146
+ const candidates = [];
147
+ for (const response of unique.values()) {
148
+ let body = "";
149
+ let base64Encoded = false;
150
+ try {
151
+ const payload = await sendCdpCommand(socket, "Network.getResponseBody", { requestId: response.requestId });
152
+ body = payload.body ?? "";
153
+ base64Encoded = payload.base64Encoded ?? false;
154
+ }
155
+ catch {
156
+ // Some responses are streamed, cached, too large, or not retained.
157
+ }
158
+ const signals = detectBodySignals(body, response.mimeType);
159
+ candidates.push({
160
+ url: redactUrl(response.url),
161
+ status: response.status,
162
+ mimeType: response.mimeType,
163
+ bodyLength: body ? body.length : undefined,
164
+ base64Encoded: body ? base64Encoded : undefined,
165
+ signals,
166
+ });
167
+ }
168
+ return candidates;
169
+ }
170
+ function detectBodySignals(body, mimeType) {
171
+ const haystack = `${mimeType}\n${body.slice(0, 200_000)}`;
172
+ const signals = [];
173
+ if (/merge|merged|rowspan|colspan|mergeCell|mergeCells/i.test(haystack))
174
+ signals.push("merge-like");
175
+ if (/image|img|pic|picture|media|download_url|url/i.test(haystack))
176
+ signals.push("image-like");
177
+ if (/cell|row|column|sheet|workbook|table/i.test(haystack))
178
+ signals.push("table-like");
179
+ if (/text|paragraph|content|doc|delta|operation|op/i.test(haystack))
180
+ signals.push("document-like");
181
+ if (/websocket|realtime|sync/i.test(haystack))
182
+ signals.push("realtime-like");
183
+ return signals;
184
+ }
185
+ function spawnBrowser(args) {
186
+ mkdirSync(args.profilePath, { recursive: true });
187
+ const browserArgs = [
188
+ `--remote-debugging-port=${args.port}`,
189
+ `--user-data-dir=${args.profilePath}`,
190
+ "--no-first-run",
191
+ "--no-default-browser-check",
192
+ "--disable-features=DialMediaRouteProvider",
193
+ ];
194
+ if (!args.headed) {
195
+ browserArgs.push("--headless=new");
196
+ browserArgs.push("--disable-gpu");
197
+ }
198
+ browserArgs.push(args.url);
199
+ return spawn(args.browser.executablePath, browserArgs, {
200
+ stdio: "ignore",
201
+ detached: process.platform !== "win32",
202
+ });
203
+ }
204
+ function getKnownBrowserPaths() {
205
+ if (process.platform === "darwin") {
206
+ return [
207
+ {
208
+ label: "Google Chrome",
209
+ executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
210
+ },
211
+ {
212
+ label: "Microsoft Edge",
213
+ executablePath: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
214
+ },
215
+ ];
216
+ }
217
+ if (process.platform === "win32") {
218
+ const lad = process.env["LOCALAPPDATA"] ?? "";
219
+ const pf = process.env["PROGRAMFILES"] ?? "C:\\Program Files";
220
+ const pf86 = process.env["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)";
221
+ return [
222
+ { label: "Google Chrome", executablePath: join(lad, "Google", "Chrome", "Application", "chrome.exe") },
223
+ { label: "Google Chrome", executablePath: join(pf, "Google", "Chrome", "Application", "chrome.exe") },
224
+ { label: "Google Chrome", executablePath: join(pf86, "Google", "Chrome", "Application", "chrome.exe") },
225
+ { label: "Microsoft Edge", executablePath: join(pf, "Microsoft", "Edge", "Application", "msedge.exe") },
226
+ { label: "Microsoft Edge", executablePath: join(pf86, "Microsoft", "Edge", "Application", "msedge.exe") },
227
+ { label: "Microsoft Edge", executablePath: join(lad, "Microsoft", "Edge", "Application", "msedge.exe") },
228
+ ];
229
+ }
230
+ return [
231
+ { label: "Google Chrome", executablePath: "/usr/bin/google-chrome-stable" },
232
+ { label: "Google Chrome", executablePath: "/usr/bin/google-chrome" },
233
+ { label: "Chromium", executablePath: "/usr/bin/chromium" },
234
+ { label: "Chromium", executablePath: "/usr/bin/chromium-browser" },
235
+ { label: "Microsoft Edge", executablePath: "/usr/bin/microsoft-edge" },
236
+ ];
237
+ }
238
+ function findInstalledBrowser(executablePath) {
239
+ if (executablePath) {
240
+ if (!existsSync(executablePath)) {
241
+ throw new Error(`Configured browser not found: ${executablePath}`);
242
+ }
243
+ return { label: "Configured browser", executablePath };
244
+ }
245
+ for (const candidate of getKnownBrowserPaths()) {
246
+ if (existsSync(candidate.executablePath))
247
+ return candidate;
248
+ }
249
+ throw new Error("Could not find a supported Chrome/Edge installation");
250
+ }
251
+ async function getFreePort() {
252
+ return new Promise((resolve, reject) => {
253
+ const server = createServer();
254
+ server.listen(0, "127.0.0.1", () => {
255
+ const address = server.address();
256
+ if (!address || typeof address === "string") {
257
+ server.close();
258
+ reject(new Error("Failed to allocate a browser debugging port"));
259
+ return;
260
+ }
261
+ const port = address.port;
262
+ server.close((err) => {
263
+ if (err)
264
+ reject(err);
265
+ else
266
+ resolve(port);
267
+ });
268
+ });
269
+ server.on("error", reject);
270
+ });
271
+ }
272
+ async function waitForDebugger(endpoint, timeoutMs) {
273
+ const startedAt = Date.now();
274
+ while (Date.now() - startedAt < timeoutMs) {
275
+ try {
276
+ const res = await fetch(`${endpoint}/json/version`);
277
+ if (res.ok)
278
+ return;
279
+ }
280
+ catch {
281
+ // Startup race.
282
+ }
283
+ await delay(500);
284
+ }
285
+ throw new Error("Browser did not expose the remote debugging endpoint in time");
286
+ }
287
+ async function waitForPageWebSocket(endpoint, expectedUrl, timeoutMs) {
288
+ const startedAt = Date.now();
289
+ while (Date.now() - startedAt < timeoutMs) {
290
+ try {
291
+ const res = await fetch(`${endpoint}/json/list`);
292
+ if (res.ok) {
293
+ const targets = (await res.json());
294
+ const expectedHost = new URL(expectedUrl).host;
295
+ const target = targets.find((t) => t.type === "page" &&
296
+ t.webSocketDebuggerUrl &&
297
+ (t.url?.includes(expectedHost) || targets.length === 1));
298
+ if (target?.webSocketDebuggerUrl)
299
+ return target.webSocketDebuggerUrl;
300
+ }
301
+ }
302
+ catch {
303
+ // Startup race.
304
+ }
305
+ await delay(500);
306
+ }
307
+ throw new Error("Browser did not expose a page DevTools websocket in time");
308
+ }
309
+ async function openDevToolsSocket(url) {
310
+ return new Promise((resolve, reject) => {
311
+ const socket = new WebSocket(url);
312
+ const timer = setTimeout(() => {
313
+ socket.close();
314
+ reject(new Error("Timed out connecting to browser DevTools websocket"));
315
+ }, CDP_CALL_TIMEOUT_MS);
316
+ socket.once("open", () => {
317
+ clearTimeout(timer);
318
+ resolve(socket);
319
+ });
320
+ socket.once("error", () => {
321
+ clearTimeout(timer);
322
+ reject(new Error("Failed to connect to browser DevTools websocket"));
323
+ });
324
+ });
325
+ }
326
+ async function sendCdpCommand(socket, method, params = {}) {
327
+ const id = cdpNextId++;
328
+ return new Promise((resolve, reject) => {
329
+ const timer = setTimeout(() => {
330
+ socket.removeEventListener("message", handleMessage);
331
+ reject(new Error(`CDP timeout: ${method}`));
332
+ }, CDP_CALL_TIMEOUT_MS);
333
+ const handleMessage = (event) => {
334
+ const payload = safeJsonParse(String(event.data));
335
+ if (payload?.id !== id)
336
+ return;
337
+ clearTimeout(timer);
338
+ socket.removeEventListener("message", handleMessage);
339
+ if (payload.error)
340
+ reject(new Error(payload.error.message ?? `CDP error: ${method}`));
341
+ else
342
+ resolve((payload.result ?? {}));
343
+ };
344
+ socket.addEventListener("message", handleMessage);
345
+ socket.send(JSON.stringify({ id, method, params }));
346
+ });
347
+ }
348
+ async function terminateBrowserProcess(child) {
349
+ if (child.exitCode !== null)
350
+ return;
351
+ if (process.platform !== "win32" && child.pid) {
352
+ try {
353
+ process.kill(-child.pid, "SIGTERM");
354
+ }
355
+ catch {
356
+ child.kill();
357
+ }
358
+ }
359
+ else {
360
+ child.kill();
361
+ }
362
+ await delay(500);
363
+ if (child.exitCode === null) {
364
+ try {
365
+ if (process.platform !== "win32" && child.pid)
366
+ process.kill(-child.pid, "SIGKILL");
367
+ else
368
+ child.kill("SIGKILL");
369
+ }
370
+ catch {
371
+ // Already exited.
372
+ }
373
+ }
374
+ }
375
+ function isInterestingNetworkUrl(url, mimeType) {
376
+ if (!/doc\.weixin\.qq\.com|weixin\.qq\.com|wecom/i.test(url))
377
+ return false;
378
+ return /json|text|javascript|octet-stream|protobuf|grpc|xml|html/i.test(mimeType) ||
379
+ /cgi|api|doc|sheet|media|image|file|sync|ws/i.test(url);
380
+ }
381
+ function redactUrl(rawUrl) {
382
+ try {
383
+ const u = new URL(rawUrl);
384
+ u.search = "";
385
+ u.hash = "";
386
+ return u.toString();
387
+ }
388
+ catch {
389
+ return rawUrl.split("?")[0] ?? rawUrl;
390
+ }
391
+ }
392
+ function safeJsonParse(value) {
393
+ try {
394
+ return JSON.parse(value);
395
+ }
396
+ catch {
397
+ return null;
398
+ }
399
+ }
400
+ function delay(ms) {
401
+ return new Promise((resolve) => setTimeout(resolve, ms));
402
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkterswingman/5mghost-wonder",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "企微文档读取 CLI — WeCom document reader",
5
5
  "type": "module",
6
6
  "engines": {
@@ -25,7 +25,8 @@
25
25
  "scripts": {
26
26
  "build": "rm -rf dist && tsc && chmod +x dist/cli.js",
27
27
  "typecheck": "tsc --noEmit",
28
- "test": "node dist/wecom/url.test.js && node --test tests/sheet-parity.test.mjs && node --test tests/export-sanitize.test.mjs && node --test tests/format.test.mjs && node --test tests/cookies-validation.test.mjs",
28
+ "check:skills": "node scripts/check-skills.mjs",
29
+ "test": "npm run check:skills && node dist/wecom/url.test.js && node --test tests/sheet-parity.test.mjs && node --test tests/export-sanitize.test.mjs && node --test tests/format.test.mjs && node --test tests/cookies-validation.test.mjs",
29
30
  "smoke": "npm run build && node dist/cli.js help > /dev/null",
30
31
  "postinstall": "node scripts/postinstall.mjs"
31
32
  },
@@ -0,0 +1,37 @@
1
+ // scripts/check-skills.mjs
2
+ // Lightweight packaging guard for bundled skills. It verifies that router
3
+ // skills do not point to missing reference files.
4
+
5
+ import { readFileSync, existsSync } from "node:fs";
6
+ import { dirname, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
10
+ const skillPath = resolve(root, "skills/use-5mghost-wonder/SKILL.md");
11
+ const skillDir = dirname(skillPath);
12
+ const skillText = readFileSync(skillPath, "utf8");
13
+
14
+ const refs = new Set();
15
+ const referencePattern = /`(references\/[^`]+\.md)`/g;
16
+ let match;
17
+ while ((match = referencePattern.exec(skillText)) !== null) {
18
+ refs.add(match[1]);
19
+ }
20
+
21
+ if (refs.size === 0) {
22
+ throw new Error("use-5mghost-wonder/SKILL.md does not reference any workflow files");
23
+ }
24
+
25
+ const missing = [];
26
+ for (const ref of refs) {
27
+ const filePath = resolve(skillDir, ref);
28
+ if (!existsSync(filePath)) {
29
+ missing.push(ref);
30
+ }
31
+ }
32
+
33
+ if (missing.length > 0) {
34
+ throw new Error(`Missing use-5mghost-wonder references: ${missing.join(", ")}`);
35
+ }
36
+
37
+ console.log(`✅ use-5mghost-wonder references OK (${refs.size} files)`);
@@ -7,9 +7,9 @@ description: Use this skill when the user wants to install or set up wonder, say
7
7
 
8
8
  ## Skill version
9
9
 
10
- This skill matches **wonder 0.0.13**.
10
+ This skill matches **wonder 0.0.14**.
11
11
 
12
- Once the CLI is installed in Step 1, run `wonder --version`. If the output does not equal `0.0.13`, the CLI on disk has drifted from the skill text loaded in this session. Ask the user to run `/update-5mghost-wonder`, then **start a fresh AI session** (`/exit` and re-enter, or open a new chat) — skill text already loaded into a running session does not refresh after `wonder update`, even though the file on disk has been replaced.
12
+ Once the CLI is installed in Step 1, run `wonder --version`. If the output does not equal `0.0.14`, the CLI on disk has drifted from the skill text loaded in this session. Ask the user to run `/update-5mghost-wonder`, then **start a fresh AI session** (`/exit` and re-enter, or open a new chat) — skill text already loaded into a running session does not refresh after `wonder update`, even though the file on disk has been replaced.
13
13
 
14
14
  After a successful first install, also remind the user to start a fresh AI session before invoking `/use-5mghost-wonder` for the first time. The skill files were just written to disk; the current session never loaded them.
15
15
 
@@ -7,7 +7,7 @@ description: Use this skill when the user wants to update or upgrade wonder, say
7
7
 
8
8
  ## Skill version
9
9
 
10
- This skill matches **wonder 0.0.13**.
10
+ This skill matches **wonder 0.0.14**.
11
11
 
12
12
  ---
13
13