@mkterswingman/5mghost-wonder 0.0.1

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.
Files changed (39) hide show
  1. package/dist/auth/runtime.js +15 -0
  2. package/dist/cli.js +75 -0
  3. package/dist/commands/auth.js +100 -0
  4. package/dist/commands/check.js +258 -0
  5. package/dist/commands/help.js +38 -0
  6. package/dist/commands/index.js +50 -0
  7. package/dist/commands/read.js +198 -0
  8. package/dist/commands/setup.js +81 -0
  9. package/dist/commands/types.js +4 -0
  10. package/dist/commands/uninstall.js +14 -0
  11. package/dist/commands/update.js +21 -0
  12. package/dist/commands/version.js +8 -0
  13. package/dist/commands/wecom.js +136 -0
  14. package/dist/platform/npm.js +14 -0
  15. package/dist/platform/paths.js +25 -0
  16. package/dist/telemetry/events.js +42 -0
  17. package/dist/telemetry/policy.js +51 -0
  18. package/dist/telemetry/runtime.js +31 -0
  19. package/dist/wecom/browser.js +344 -0
  20. package/dist/wecom/cache.js +119 -0
  21. package/dist/wecom/cookies.js +151 -0
  22. package/dist/wecom/export.js +236 -0
  23. package/dist/wecom/url.js +45 -0
  24. package/dist/wecom/url.test.js +64 -0
  25. package/dist/xlsx/drawing.js +131 -0
  26. package/dist/xlsx/metadata.js +34 -0
  27. package/dist/xlsx/parse-tab.js +124 -0
  28. package/dist/xlsx/shared-strings.js +51 -0
  29. package/dist/xlsx/sheet.js +161 -0
  30. package/dist/xlsx/styles.js +85 -0
  31. package/dist/xlsx/unzip.js +33 -0
  32. package/dist/xlsx/workbook.js +51 -0
  33. package/dist/xlsx/workbook.test.js +19 -0
  34. package/package.json +41 -0
  35. package/scripts/check-export-types.mjs +37 -0
  36. package/scripts/postinstall.mjs +50 -0
  37. package/skills/setup-5mghost-wonder/SKILL.md +245 -0
  38. package/skills/use-5mghost-wonder/SKILL.md +240 -0
  39. package/skills.manifest.json +36 -0
@@ -0,0 +1,198 @@
1
+ // src/commands/read.ts
2
+ // Implements `wonder read <url> [--save <dir>] [--tab <name>]`.
3
+ // Phase 1: URL routing, export, metadata output.
4
+ // Phase 2: --tab cell/merge/image parsing.
5
+ import { parseWecomUrl } from "../wecom/url.js";
6
+ import { exportWecomDoc, ExportError } from "../wecom/export.js";
7
+ import { loadCookies, getCookieStatus } from "../wecom/cookies.js";
8
+ import { resolveWonderPaths } from "../platform/paths.js";
9
+ import { lookupCachedExport, saveExportToCache } from "../wecom/cache.js";
10
+ import { parseTab } from "../xlsx/parse-tab.js";
11
+ export async function runReadCommand(args, context) {
12
+ // --- Parse flags ---
13
+ let url;
14
+ let saveDir;
15
+ let tabName;
16
+ for (let i = 0; i < args.length; i++) {
17
+ if (args[i] === "--save" && args[i + 1]) {
18
+ saveDir = args[++i];
19
+ }
20
+ else if (args[i] === "--tab" && args[i + 1]) {
21
+ tabName = args[++i];
22
+ }
23
+ else if (!args[i].startsWith("-")) {
24
+ url = args[i];
25
+ }
26
+ }
27
+ const writeError = (obj) => {
28
+ context.io.stderr(JSON.stringify(obj));
29
+ };
30
+ // --- URL required ---
31
+ if (!url) {
32
+ writeError({ error: "missing_url", message: "用法:wonder read <url> [--save <dir>]" });
33
+ return { exitCode: 1, telemetry: { outcome: "failure", errorKind: "missing_url" } };
34
+ }
35
+ // --- Parse URL ---
36
+ const parsed = parseWecomUrl(url);
37
+ if (!parsed.ok) {
38
+ if (parsed.reason === "unsupported") {
39
+ const hint = parsed.kind === "smartpage"
40
+ ? "当前版本不支持智能文档(smartpage)。建议在浏览器中手动打开文档,选择导出为 PDF 或复制内容。"
41
+ : "当前版本不支持思维导图(mind)。建议在浏览器中手动打开文档并复制内容。";
42
+ writeError({
43
+ error: "unsupported_type",
44
+ kind: parsed.kind,
45
+ message: hint,
46
+ });
47
+ return {
48
+ exitCode: 2,
49
+ telemetry: { outcome: "failure", errorKind: `unsupported_${parsed.kind}` },
50
+ };
51
+ }
52
+ writeError({ error: "invalid_url", message: "无法识别的 WeCom URL" });
53
+ return { exitCode: 1, telemetry: { outcome: "failure", errorKind: "invalid_url" } };
54
+ }
55
+ const { docId, docType } = parsed;
56
+ // --- Auth pre-check ---
57
+ const paths = resolveWonderPaths({ homeDir: context.homeDir });
58
+ if (context.authSdk?.getAuthStatus) {
59
+ const authStatus = await context.authSdk.getAuthStatus({ homeDir: paths.homeDir });
60
+ if (!authStatus.authenticated) {
61
+ writeError({ error: "not_authenticated", message: "请先执行 wonder auth login" });
62
+ return { exitCode: 1, telemetry: { outcome: "failure", errorKind: "not_authenticated" } };
63
+ }
64
+ }
65
+ // --- Cookie pre-check ---
66
+ const cookieStatus = await getCookieStatus(paths.cookiesPath);
67
+ if (!cookieStatus.valid) {
68
+ writeError({
69
+ error: "cookie_invalid",
70
+ message: "WeCom cookie 无效或已过期,请执行 wonder wecom cookie",
71
+ });
72
+ return { exitCode: 1, telemetry: { outcome: "failure", errorKind: "cookie_invalid" } };
73
+ }
74
+ // Load cookies and convert Record<string,string> → CookieEntry[]
75
+ const rawCookies = loadCookies(paths.cookiesPath);
76
+ const cookies = rawCookies
77
+ ? Object.entries(rawCookies).map(([name, value]) => ({ name, value }))
78
+ : [];
79
+ // --- Resolve save dir ---
80
+ const resolvedSaveDir = saveDir ?? paths.defaultSaveDir;
81
+ // --- Export (with local cache) ---
82
+ const tokValue = rawCookies?.["TOK"] ?? "";
83
+ const cacheDir = paths.cacheDir;
84
+ const noCache = args.includes("--no-cache");
85
+ let result = null;
86
+ if (!noCache && tokValue) {
87
+ const hit = lookupCachedExport({
88
+ cacheDir,
89
+ docId,
90
+ tokValue,
91
+ maxAgeMs: 10 * 60 * 1000,
92
+ });
93
+ if (hit && hit.docType === docType) {
94
+ // Materialize the cached file into the requested saveDir so downstream
95
+ // tools that depend on `--save <dir>` keep finding the file there.
96
+ const { mkdirSync, copyFileSync } = await import("node:fs");
97
+ const { join } = await import("node:path");
98
+ mkdirSync(resolvedSaveDir, { recursive: true });
99
+ const materialisedPath = join(resolvedSaveDir, hit.fileName);
100
+ try {
101
+ copyFileSync(hit.filePath, materialisedPath);
102
+ }
103
+ catch {
104
+ // If copy fails, fall back to the cache path — still functional.
105
+ }
106
+ result = {
107
+ filePath: materialisedPath,
108
+ fileName: hit.fileName,
109
+ fileSizeBytes: hit.fileSizeBytes,
110
+ docType,
111
+ };
112
+ }
113
+ }
114
+ if (!result) {
115
+ try {
116
+ const fresh = await exportWecomDoc({
117
+ docId,
118
+ docType,
119
+ sourceUrl: url,
120
+ cookies,
121
+ saveDir: resolvedSaveDir,
122
+ });
123
+ if (!noCache && tokValue) {
124
+ try {
125
+ saveExportToCache({
126
+ cacheDir,
127
+ docId,
128
+ tokValue,
129
+ sourceFilePath: fresh.filePath,
130
+ fileName: fresh.fileName,
131
+ fileSizeBytes: fresh.fileSizeBytes,
132
+ docType,
133
+ });
134
+ }
135
+ catch {
136
+ // Cache save failure must not break the command.
137
+ }
138
+ }
139
+ result = {
140
+ filePath: fresh.filePath,
141
+ fileName: fresh.fileName,
142
+ fileSizeBytes: fresh.fileSizeBytes,
143
+ docType,
144
+ };
145
+ }
146
+ catch (err) {
147
+ if (err instanceof ExportError) {
148
+ writeError({
149
+ error: "export_failed",
150
+ errorKind: err.kind,
151
+ message: err.message,
152
+ });
153
+ return {
154
+ exitCode: 1,
155
+ telemetry: { outcome: "failure", errorKind: err.kind, errorMessage: err.message },
156
+ };
157
+ }
158
+ const msg = err instanceof Error ? err.message : String(err);
159
+ writeError({ error: "export_failed", message: msg });
160
+ return { exitCode: 1, telemetry: { outcome: "failure", errorKind: "unknown", errorMessage: msg } };
161
+ }
162
+ }
163
+ // --- Build output ---
164
+ if (docType === "sheet") {
165
+ if (tabName !== undefined) {
166
+ // --tab: full parse, return TabOutput
167
+ try {
168
+ const tabOutput = await parseTab(result.filePath, tabName, resolvedSaveDir);
169
+ context.io.stdout(JSON.stringify(tabOutput));
170
+ return { exitCode: 0, telemetry: { outcome: "success" } };
171
+ }
172
+ catch (err) {
173
+ const msg = err instanceof Error ? err.message : String(err);
174
+ writeError({ error: "tab_parse_failed", message: msg });
175
+ return { exitCode: 1, telemetry: { outcome: "failure", errorKind: "tab_parse_failed", errorMessage: msg } };
176
+ }
177
+ }
178
+ // No --tab: return workbook metadata with hidden flags
179
+ const { openXlsx } = await import("../xlsx/unzip.js");
180
+ const { parseWorkbook } = await import("../xlsx/workbook.js");
181
+ const zip = await openXlsx(result.filePath);
182
+ const workbookInfo = await parseWorkbook(zip);
183
+ context.io.stdout(JSON.stringify({
184
+ type: "sheet",
185
+ fileName: result.fileName,
186
+ filePath: result.filePath,
187
+ tabs: workbookInfo.tabs.map((t) => ({
188
+ name: t.name,
189
+ hidden: t.hidden,
190
+ })),
191
+ }));
192
+ return { exitCode: 0, telemetry: { outcome: "success" } };
193
+ }
194
+ // doc or slide
195
+ const outputType = docType === "doc" ? "doc" : "slide";
196
+ context.io.stdout(JSON.stringify({ type: outputType, path: result.filePath }));
197
+ return { exitCode: 0, telemetry: { outcome: "success" } };
198
+ }
@@ -0,0 +1,81 @@
1
+ // src/commands/setup.ts
2
+ // Guided first-time setup: checks current state, prints numbered steps for
3
+ // anything that is missing, then runs `wonder check` to summarise final state.
4
+ //
5
+ // wonder does NOT perform interactive stdin prompts — each step (auth, wecom)
6
+ // is a separate command the user runs. setup is the guide, not the executor.
7
+ import { runCheckCommand } from "./check.js";
8
+ export async function runSetupCommand(_argv, context) {
9
+ context.io.stdout([
10
+ "5mghost-wonder setup",
11
+ "",
12
+ "This wizard checks your current state and tells you what to run.",
13
+ "You can re-run `wonder setup` at any time to see updated status.",
14
+ "",
15
+ "──────────────────────────────────────────────────",
16
+ ].join("\n"));
17
+ // ── Step 1: Auth ─────────────────────────────────────────────────────────
18
+ let authReady = false;
19
+ if (!context.authSdk) {
20
+ context.io.stdout("Step 1 — mkterswingman auth: not configured (sdk unavailable)");
21
+ }
22
+ else {
23
+ try {
24
+ const status = await context.authSdk.getAuthStatus({
25
+ homeDir: context.homeDir,
26
+ });
27
+ if (status.authenticated) {
28
+ authReady = true;
29
+ context.io.stdout("Step 1 — mkterswingman auth: ✓ authenticated");
30
+ }
31
+ else {
32
+ context.io.stdout([
33
+ "Step 1 — mkterswingman auth: not authenticated",
34
+ " → Run: wonder auth login",
35
+ " → Or: wonder auth login --pat <YOUR_TOKEN>",
36
+ ].join("\n"));
37
+ }
38
+ }
39
+ catch (err) {
40
+ context.io.stdout(`Step 1 — mkterswingman auth: error (${String(err)})`);
41
+ }
42
+ }
43
+ // ── Step 2: WeCom cookie ─────────────────────────────────────────────────
44
+ let cookieReady = false;
45
+ try {
46
+ const { getCookieStatus } = await import("../wecom/cookies.js");
47
+ const { resolveWonderPaths } = await import("../platform/paths.js");
48
+ const paths = resolveWonderPaths({ homeDir: context.homeDir });
49
+ const cs = await getCookieStatus(paths.cookiesPath);
50
+ if (cs.exists && cs.valid === true) {
51
+ cookieReady = true;
52
+ context.io.stdout("Step 2 — WeCom cookies: ✓ valid");
53
+ }
54
+ else if (cs.exists && cs.valid === false) {
55
+ context.io.stdout([
56
+ "Step 2 — WeCom cookies: invalid or expired",
57
+ " → Run: wonder wecom cookie",
58
+ " → (Open Chrome, scan QR code when prompted)",
59
+ ].join("\n"));
60
+ }
61
+ else {
62
+ context.io.stdout([
63
+ "Step 2 — WeCom cookies: not configured",
64
+ " → Run: wonder wecom cookie",
65
+ " → (Open Chrome, scan QR code when prompted)",
66
+ ].join("\n"));
67
+ }
68
+ }
69
+ catch (_err) {
70
+ context.io.stdout("Step 2 — WeCom cookies: could not check status (wecom module unavailable)");
71
+ }
72
+ // ── Summary ──────────────────────────────────────────────────────────────
73
+ context.io.stdout("──────────────────────────────────────────────────");
74
+ if (authReady && cookieReady) {
75
+ context.io.stdout("All steps complete. Running `wonder check` to verify...");
76
+ context.io.stdout("");
77
+ return runCheckCommand([], context);
78
+ }
79
+ context.io.stdout("Complete the steps above, then run `wonder check` to verify.");
80
+ return { exitCode: 1 };
81
+ }
@@ -0,0 +1,4 @@
1
+ // src/commands/types.ts
2
+ // Core DI types shared by all commands.
3
+ // All imports use .js extension (NodeNext ESM requirement).
4
+ export {};
@@ -0,0 +1,14 @@
1
+ // src/commands/uninstall.ts
2
+ // Runs `npm uninstall -g @mkterswingman/5mghost-wonder`.
3
+ // NpmExecutor is injectable for unit testing.
4
+ import { defaultNpmExecutor } from "../platform/npm.js";
5
+ export async function runUninstallCommand(_argv, context, executor = defaultNpmExecutor) {
6
+ context.io.stdout("Uninstalling 5mghost-wonder...");
7
+ const result = executor(["uninstall", "-g", "@mkterswingman/5mghost-wonder"]);
8
+ if (result.exitCode !== 0) {
9
+ context.io.stderr(`Uninstall failed:\n${result.stderr}`);
10
+ return { exitCode: 1 };
11
+ }
12
+ context.io.stdout("Uninstalled successfully.");
13
+ return { exitCode: 0 };
14
+ }
@@ -0,0 +1,21 @@
1
+ // src/commands/update.ts
2
+ // Runs `npm install -g @mkterswingman/5mghost-wonder@latest`.
3
+ // NpmExecutor is injectable for unit testing.
4
+ import { defaultNpmExecutor } from "../platform/npm.js";
5
+ export async function runUpdateCommand(_argv, context, executor = defaultNpmExecutor) {
6
+ context.io.stdout("Checking for updates...");
7
+ const result = executor([
8
+ "install",
9
+ "-g",
10
+ "@mkterswingman/5mghost-wonder@latest",
11
+ ]);
12
+ if (result.exitCode !== 0) {
13
+ context.io.stderr(`Update failed:\n${result.stderr}`);
14
+ return {
15
+ exitCode: 1,
16
+ telemetry: { outcome: "failure", errorKind: "npm_update_failed" },
17
+ };
18
+ }
19
+ context.io.stdout("Updated successfully.");
20
+ return { exitCode: 0 };
21
+ }
@@ -0,0 +1,8 @@
1
+ // src/commands/version.ts
2
+ // Prints the installed CLI version from package.json.
3
+ // Uses `with { type: "json" }` assertion — required for NodeNext ESM JSON imports.
4
+ import packageJson from "../../package.json" with { type: "json" };
5
+ export async function runVersionCommand(_argv, context) {
6
+ context.io.stdout(packageJson.version);
7
+ return { exitCode: 0 };
8
+ }
@@ -0,0 +1,136 @@
1
+ // src/commands/wecom.ts
2
+ // CLI dispatch for: wonder wecom cookie | status | set-cookie
3
+ import { resolveWonderPaths } from "../platform/paths.js";
4
+ import { collectWecomCookiesViaBrowser } from "../wecom/browser.js";
5
+ import { getCookieStatus, parseCookieInput, saveCookies, validateCookies, } from "../wecom/cookies.js";
6
+ const KEY_COOKIES = [
7
+ "TOK",
8
+ "wedrive_sid",
9
+ "uid",
10
+ "uid_key",
11
+ "wedrive_ticket",
12
+ "wedrive_skey",
13
+ ];
14
+ // ---------------------------------------------------------------------------
15
+ // Top-level subcommand router
16
+ // ---------------------------------------------------------------------------
17
+ export async function runWecom(argv, context) {
18
+ const [sub, ...rest] = argv;
19
+ switch (sub) {
20
+ case "cookie":
21
+ return runWecomCookie(rest, context);
22
+ case "status":
23
+ return runWecomStatus(rest, context);
24
+ case "set-cookie":
25
+ return runWecomSetCookie(rest, context);
26
+ default:
27
+ context.io.stderr(`Unknown subcommand: wecom ${sub ?? "(none)"}\n` +
28
+ "Usage: wonder wecom cookie | status | set-cookie");
29
+ return { exitCode: 1 };
30
+ }
31
+ }
32
+ // ---------------------------------------------------------------------------
33
+ // wonder wecom cookie
34
+ // ---------------------------------------------------------------------------
35
+ async function runWecomCookie(_argv, context) {
36
+ const paths = resolveWonderPaths({ homeDir: context.homeDir });
37
+ context.io.stdout("Opening Chrome for WeCom login...");
38
+ context.io.stdout("Please scan the QR code in the browser. Waiting up to 3 minutes...");
39
+ let cookies;
40
+ try {
41
+ cookies = await collectWecomCookiesViaBrowser({
42
+ chromeProfilePath: paths.chromeProfilePath,
43
+ io: context.io,
44
+ });
45
+ }
46
+ catch (err) {
47
+ context.io.stderr(`Failed to collect cookies: ${String(err)}`);
48
+ return { exitCode: 1, telemetry: { errorKind: "browser_collect_failed" } };
49
+ }
50
+ saveCookies(paths.cookiesPath, cookies);
51
+ // Live-validate immediately after collection.
52
+ const valid = await validateCookies(cookies);
53
+ if (!valid) {
54
+ context.io.stderr("Cookies were collected but WeCom rejected them (got 401/403).\n" +
55
+ "Please complete the login in the browser, then rerun `wonder wecom cookie`.");
56
+ return {
57
+ exitCode: 1,
58
+ telemetry: { errorKind: "cookies_invalid_after_collect" },
59
+ };
60
+ }
61
+ context.io.stdout(`✅ Cookies saved to ${paths.cookiesPath}`);
62
+ return { exitCode: 0 };
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // wonder wecom status
66
+ // ---------------------------------------------------------------------------
67
+ async function runWecomStatus(_argv, context) {
68
+ const paths = resolveWonderPaths({ homeDir: context.homeDir });
69
+ const status = await getCookieStatus(paths.cookiesPath);
70
+ if (!status.exists) {
71
+ context.io.stderr("No WeCom cookies found.\n" +
72
+ "Run `wonder wecom cookie` to sign in.");
73
+ return { exitCode: 1 };
74
+ }
75
+ if (status.valid === null) {
76
+ context.io.stderr(`Cookie check encountered an error: ${status.error ?? "unknown"}`);
77
+ return {
78
+ exitCode: 1,
79
+ telemetry: { errorKind: "cookie_status_check_failed" },
80
+ };
81
+ }
82
+ if (!status.valid) {
83
+ context.io.stderr("WeCom cookies are expired or invalid.\n" +
84
+ "Run `wonder wecom cookie` to refresh.");
85
+ return { exitCode: 1 };
86
+ }
87
+ context.io.stdout(`✅ WeCom cookies are valid (${status.path})`);
88
+ return { exitCode: 0 };
89
+ }
90
+ // ---------------------------------------------------------------------------
91
+ // wonder wecom set-cookie
92
+ // ---------------------------------------------------------------------------
93
+ async function runWecomSetCookie(argv, context) {
94
+ const paths = resolveWonderPaths({ homeDir: context.homeDir });
95
+ // Prefer argv[0]; fall back to stdin for pipe/heredoc scenarios.
96
+ let rawInput = argv[0] ?? null;
97
+ if (!rawInput) {
98
+ rawInput = await readStdin();
99
+ }
100
+ if (!rawInput?.trim()) {
101
+ context.io.stderr("Usage: wonder wecom set-cookie '<json or cookie-header-string>'\n" +
102
+ ' Example (JSON): wonder wecom set-cookie \'{"TOK":"xxx","wedrive_sid":"yyy"}\'\n' +
103
+ " Example (header): wonder wecom set-cookie 'TOK=xxx; wedrive_sid=yyy'");
104
+ return { exitCode: 1 };
105
+ }
106
+ const cookies = parseCookieInput(rawInput);
107
+ if (!cookies) {
108
+ context.io.stderr("Could not parse cookie input. Provide a valid JSON object or cookie header string.");
109
+ return { exitCode: 1 };
110
+ }
111
+ // Warn about missing key cookies (non-blocking).
112
+ const missingKeys = KEY_COOKIES.filter((k) => !(k in cookies));
113
+ if (missingKeys.length > 0) {
114
+ context.io.stderr(`⚠️ Warning: missing key cookies: ${missingKeys.join(", ")}`);
115
+ }
116
+ saveCookies(paths.cookiesPath, cookies);
117
+ const valid = await validateCookies(cookies);
118
+ if (!valid) {
119
+ context.io.stderr("⚠️ Cookies saved but WeCom returned 401/403 — they may be expired or incomplete.");
120
+ return { exitCode: 1, telemetry: { errorKind: "set_cookie_invalid" } };
121
+ }
122
+ context.io.stdout(`✅ Cookies saved and validated (${paths.cookiesPath})`);
123
+ return { exitCode: 0 };
124
+ }
125
+ // ---------------------------------------------------------------------------
126
+ // Stdin helper
127
+ // ---------------------------------------------------------------------------
128
+ async function readStdin() {
129
+ if (process.stdin.isTTY)
130
+ return "";
131
+ const chunks = [];
132
+ for await (const chunk of process.stdin) {
133
+ chunks.push(chunk);
134
+ }
135
+ return Buffer.concat(chunks).toString("utf8").trim();
136
+ }
@@ -0,0 +1,14 @@
1
+ // src/platform/npm.ts
2
+ // NpmExecutor: injectable wrapper around `npm` CLI.
3
+ // Replaces rover's BunExecutor. Used by update/uninstall commands.
4
+ // spawnSync is synchronous and appropriate here — update/uninstall are
5
+ // user-triggered, single-shot operations.
6
+ import { spawnSync } from "child_process";
7
+ export const defaultNpmExecutor = (args) => {
8
+ const result = spawnSync("npm", args, { encoding: "utf8" });
9
+ return {
10
+ exitCode: result.status ?? 1,
11
+ stdout: result.stdout ?? "",
12
+ stderr: result.stderr ?? "",
13
+ };
14
+ };
@@ -0,0 +1,25 @@
1
+ // src/platform/paths.ts
2
+ // Resolves all filesystem paths used by wonder at runtime.
3
+ // __dirname equivalent uses import.meta.url (NodeNext ESM).
4
+ import { fileURLToPath } from "url";
5
+ import { dirname, resolve } from "path";
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ export function resolveWonderPaths(opts) {
9
+ const homeDir = opts?.homeDir ??
10
+ process.env["HOME"] ??
11
+ process.env["USERPROFILE"] ??
12
+ "";
13
+ const wonderDir = resolve(homeDir, ".wonder");
14
+ // At runtime, __dirname = dist/ (tsc output). The manifest lives one level up.
15
+ const skillsManifestPath = resolve(__dirname, "../skills.manifest.json");
16
+ return {
17
+ homeDir,
18
+ wonderDir,
19
+ cookiesPath: resolve(wonderDir, "cookies.json"),
20
+ chromeProfilePath: resolve(wonderDir, "chrome-profile"),
21
+ cacheDir: resolve(wonderDir, "cache"),
22
+ skillsManifestPath,
23
+ defaultSaveDir: resolve(homeDir, "Downloads", "5mghost-wonder"),
24
+ };
25
+ }
@@ -0,0 +1,42 @@
1
+ // src/telemetry/events.ts
2
+ // Helpers for building RecordCompletionInput from CommandResult + timing.
3
+ import { sanitizeErrorMessage } from "@mkterswingman/5mghost-telemetry";
4
+ /**
5
+ * Build the RecordCompletionInput to pass to telemetry.recordCompletion().
6
+ * Merges timing, exit-code-derived outcome, and any structured telemetry
7
+ * already attached to the CommandResult.
8
+ *
9
+ * Priority: result.telemetry.outcome > exitCode-derived outcome.
10
+ * Note: SDK uses "failure" not "error" for non-success outcomes.
11
+ */
12
+ export function buildCompletionTelemetry(cmdInfo, result, durationMs) {
13
+ const baseOutcome = result.exitCode === 0 ? "success" : "failure";
14
+ const structured = result.telemetry ?? {};
15
+ // Map CommandResult.telemetry.outcome to SDK TelemetryOutcome.
16
+ const rawOutcome = structured.outcome;
17
+ const outcome = rawOutcome === "success" ? "success"
18
+ : rawOutcome === "failure" ? "failure"
19
+ : baseOutcome;
20
+ return {
21
+ toolName: cmdInfo.toolName,
22
+ durationMs,
23
+ outcome,
24
+ ...(structured.errorKind !== undefined
25
+ ? { errorKind: structured.errorKind }
26
+ : {}),
27
+ ...(structured.errorCode !== undefined
28
+ ? { errorCode: structured.errorCode }
29
+ : {}),
30
+ ...(structured.errorMessage !== undefined
31
+ ? { errorMessage: redactTelemetryErrorMessage(structured.errorMessage) }
32
+ : {}),
33
+ };
34
+ }
35
+ /**
36
+ * Sanitize an error message before it is included in a telemetry event.
37
+ * Delegates to the platform SDK's sanitizeErrorMessage which truncates and
38
+ * strips characters beyond MAX_ERROR_MESSAGE_CHARS.
39
+ */
40
+ export function redactTelemetryErrorMessage(message) {
41
+ return sanitizeErrorMessage(message) ?? message;
42
+ }
@@ -0,0 +1,51 @@
1
+ // src/telemetry/policy.ts
2
+ // Maps raw CLI argv to telemetry toolName and classifies errors.
3
+ //
4
+ // toolName naming convention:
5
+ // "wecom cookie" → "wecom_cookie"
6
+ // "wecom set-cookie" → "wecom_set_cookie" (hyphen → underscore)
7
+ // "auth login" → "auth_login"
8
+ // "read <url>" → "read" (URL arg is not a subcommand)
9
+ // "setup" → "setup"
10
+ //
11
+ // SKIP_TELEMETRY: commands that must not generate events.
12
+ // These argv[0] values produce no telemetry event.
13
+ const SKIP_TELEMETRY = new Set(["--help", "-h", "--version", "-v"]);
14
+ /**
15
+ * Resolve telemetry toolName from raw process.argv (already sliced to drop
16
+ * the node/bin prefix). Returns null for commands that should not be tracked.
17
+ */
18
+ export function resolveTelemetryCommand(argv) {
19
+ const [cmd, sub] = argv;
20
+ if (!cmd || SKIP_TELEMETRY.has(cmd))
21
+ return null;
22
+ // Sub-help always skips — user is reading docs, not performing an action.
23
+ if (sub === "--help" || sub === "-h")
24
+ return null;
25
+ // Commands with meaningful named subcommands (not URL/path args).
26
+ if (cmd === "auth" || cmd === "wecom") {
27
+ if (!sub || sub.startsWith("-"))
28
+ return { toolName: cmd };
29
+ // Normalize hyphens: "set-cookie" → "set_cookie"
30
+ const safeSub = sub.replace(/-/g, "_");
31
+ return { toolName: `${cmd}_${safeSub}` };
32
+ }
33
+ // All other commands: argv[1] may be a URL, flag, or absent —
34
+ // use only argv[0] as toolName.
35
+ return { toolName: cmd };
36
+ }
37
+ /**
38
+ * Classify an unhandled top-level CLI error into telemetry metadata.
39
+ * Phase 1: all unhandled errors → "internal" / "UNHANDLED_CLI_ERROR".
40
+ * P1-07 and later phases extend this with WeCom export error types.
41
+ */
42
+ export function classifyUnhandledCliError(toolName, error) {
43
+ const message = error instanceof Error ? error.message : String(error);
44
+ return {
45
+ toolName,
46
+ outcome: "failure",
47
+ errorKind: "internal",
48
+ errorCode: "UNHANDLED_CLI_ERROR",
49
+ errorMessage: message,
50
+ };
51
+ }
@@ -0,0 +1,31 @@
1
+ // src/telemetry/runtime.ts
2
+ // Factory for the wonder telemetry runtime.
3
+ // Returns null on any construction error — telemetry must never crash the CLI.
4
+ import { DEFAULT_AUTH_URL, TokenManager } from "@mkterswingman/5mghost-auth";
5
+ import { TelemetryRuntime, TelemetrySender } from "@mkterswingman/5mghost-telemetry";
6
+ import packageJson from "../../package.json" with { type: "json" };
7
+ export function createWonderTelemetryRuntime(options = {}) {
8
+ try {
9
+ const env = options.env ?? process.env;
10
+ const authUrl = options.authUrl ??
11
+ env["MKTERSWINGMAN_AUTH_URL"] ??
12
+ DEFAULT_AUTH_URL;
13
+ const ingestUrl = options.ingestUrl ??
14
+ env["MKTERSWINGMAN_TELEMETRY_INGEST_URL"] ??
15
+ `${authUrl.replace(/\/$/, "")}/telemetry/events`;
16
+ const tokenManager = new TokenManager({
17
+ authUrl,
18
+ homeDir: options.homeDir,
19
+ });
20
+ return new TelemetryRuntime({
21
+ product: "5mghost-wonder",
22
+ productVersion: packageJson.version,
23
+ homeDir: options.homeDir,
24
+ sender: new TelemetrySender(tokenManager, ingestUrl),
25
+ });
26
+ }
27
+ catch {
28
+ // Construction failure must never propagate to the CLI.
29
+ return null;
30
+ }
31
+ }