@mkterswingman/5mghost-wonder 0.0.2 → 0.0.4
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/dist/commands/check.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// - auth: mkterswingman auth JWT/PAT
|
|
6
6
|
// - wecom-cookie: WeCom cookies.json presence + live validity probe
|
|
7
7
|
// - pandoc: CLI on PATH (used by use-skill for docx/pptx text)
|
|
8
|
-
// - soffice: CLI on PATH + real executable (
|
|
8
|
+
// - soffice: CLI on PATH + real executable (optional — only the visual-
|
|
9
|
+
// layout xlsx render and docx/pptx PDF conversion need it; the default
|
|
10
|
+
// JSON read path does not shell out to soffice)
|
|
9
11
|
// - docx-skill: docx SKILL.md present in plugin cache or user skills dir
|
|
10
12
|
// - pptx-skill: pptx SKILL.md present in plugin cache or user skills dir
|
|
11
13
|
// - cache: local export cache directory state (informational)
|
|
@@ -41,12 +43,13 @@ function findOnPath(binName) {
|
|
|
41
43
|
}
|
|
42
44
|
return null;
|
|
43
45
|
}
|
|
44
|
-
function checkExecutable(binName, installHints) {
|
|
46
|
+
function checkExecutable(binName, installHints, optional = false) {
|
|
45
47
|
const found = findOnPath(binName);
|
|
46
48
|
if (!found) {
|
|
47
49
|
return {
|
|
48
50
|
label: binName,
|
|
49
51
|
ok: false,
|
|
52
|
+
optional,
|
|
50
53
|
hint: installHints[process.platform] ?? installHints["default"] ?? `install ${binName}`,
|
|
51
54
|
};
|
|
52
55
|
}
|
|
@@ -60,6 +63,7 @@ function checkExecutable(binName, installHints) {
|
|
|
60
63
|
return {
|
|
61
64
|
label: binName,
|
|
62
65
|
ok: false,
|
|
66
|
+
optional,
|
|
63
67
|
hint: `${binName} is a broken symlink at ${found}. Reinstall: ${installHints[process.platform] ?? installHints["default"]}`,
|
|
64
68
|
detail: found,
|
|
65
69
|
};
|
|
@@ -73,6 +77,7 @@ function checkExecutable(binName, installHints) {
|
|
|
73
77
|
return {
|
|
74
78
|
label: binName,
|
|
75
79
|
ok: false,
|
|
80
|
+
optional,
|
|
76
81
|
hint: `${binName} is on PATH (${found}) but failed to execute. Reinstall: ${installHints[process.platform] ?? installHints["default"]}`,
|
|
77
82
|
detail: `realpath=${realTarget}`,
|
|
78
83
|
};
|
|
@@ -151,13 +156,18 @@ export async function runCheckCommand(_argv, context) {
|
|
|
151
156
|
win32: "winget install pandoc",
|
|
152
157
|
default: "https://pandoc.org/installing.html",
|
|
153
158
|
}));
|
|
154
|
-
// ── LibreOffice soffice
|
|
159
|
+
// ── LibreOffice soffice (optional) ──────────────────────────────────────
|
|
160
|
+
// The default JSON read path (`wonder read <url>` / `--tab`) never touches
|
|
161
|
+
// soffice. It is only needed for (a) the optional xlsx visual-layout render
|
|
162
|
+
// step and (b) docx/pptx → PDF conversion inside the AI-side use skill.
|
|
163
|
+
// Mark as optional so a missing install does not make `wonder check` exit 1.
|
|
155
164
|
items.push(checkExecutable("soffice", {
|
|
156
165
|
darwin: "brew install --cask libreoffice",
|
|
157
166
|
linux: "sudo apt install -y libreoffice (or: sudo dnf install -y libreoffice)",
|
|
158
167
|
win32: "winget install LibreOffice.LibreOffice",
|
|
159
168
|
default: "https://www.libreoffice.org/download",
|
|
160
|
-
}
|
|
169
|
+
},
|
|
170
|
+
/* optional */ true));
|
|
161
171
|
// ── docx / pptx skill files ─────────────────────────────────────────────
|
|
162
172
|
// Phase 5: intentionally NOT checked here.
|
|
163
173
|
//
|
|
@@ -1,14 +1,87 @@
|
|
|
1
1
|
// src/commands/uninstall.ts
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// Uninstall flow:
|
|
3
|
+
// 1. Remove installed wonder skills from every detected AI client via the
|
|
4
|
+
// same manifest used at install time. Receipt gating in `removeSkills`
|
|
5
|
+
// only deletes directories this package owns.
|
|
6
|
+
// 2. Run `npm uninstall -g @mkterswingman/5mghost-wonder`.
|
|
7
|
+
// 3. Print the location of the local data dir (cookies + export cache)
|
|
8
|
+
// and the exact command to remove it. We do NOT delete data
|
|
9
|
+
// automatically — cookies are still reusable if the user reinstalls.
|
|
10
|
+
import { resolveWonderPaths } from "../platform/paths.js";
|
|
4
11
|
import { defaultNpmExecutor } from "../platform/npm.js";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { dirname, resolve } from "node:path";
|
|
5
14
|
export async function runUninstallCommand(_argv, context, executor = defaultNpmExecutor) {
|
|
6
15
|
context.io.stdout("Uninstalling 5mghost-wonder...");
|
|
16
|
+
// Step 1 — best-effort skill removal. Failures here do not block npm.
|
|
17
|
+
await removeInstalledSkills(context);
|
|
18
|
+
// Step 2 — npm uninstall.
|
|
7
19
|
const result = executor(["uninstall", "-g", "@mkterswingman/5mghost-wonder"]);
|
|
8
20
|
if (result.exitCode !== 0) {
|
|
9
21
|
context.io.stderr(`Uninstall failed:\n${result.stderr}`);
|
|
10
22
|
return { exitCode: 1 };
|
|
11
23
|
}
|
|
24
|
+
// Step 3 — tell the user about residual data.
|
|
25
|
+
const paths = resolveWonderPaths({ homeDir: context.homeDir });
|
|
12
26
|
context.io.stdout("Uninstalled successfully.");
|
|
27
|
+
context.io.stdout("");
|
|
28
|
+
context.io.stdout(`Local data kept at: ${paths.wonderDir}\n` +
|
|
29
|
+
` (cookies + export cache; reused if you reinstall)\n` +
|
|
30
|
+
` To remove manually: rm -rf ${paths.wonderDir}`);
|
|
13
31
|
return { exitCode: 0 };
|
|
14
32
|
}
|
|
33
|
+
async function removeInstalledSkills(context) {
|
|
34
|
+
let removeSkills;
|
|
35
|
+
let listDetectedAgents;
|
|
36
|
+
try {
|
|
37
|
+
({ removeSkills, listDetectedAgents } = await import("@mkterswingman/5mghost-agent-skills"));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
context.io.stdout(" (agent-skills SDK unavailable — skipping skill removal)");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const manifestPath = findManifestPath();
|
|
44
|
+
if (!manifestPath) {
|
|
45
|
+
context.io.stdout(" (skills.manifest.json not found — skipping skill removal)");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
let agents;
|
|
49
|
+
try {
|
|
50
|
+
agents = listDetectedAgents();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
context.io.stdout(" (could not detect AI clients — skipping skill removal)");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!agents || agents.length === 0) {
|
|
57
|
+
context.io.stdout(" (no AI clients detected — skipping skill removal)");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const summary = removeSkills({ manifestPath, detectedAgents: agents });
|
|
62
|
+
const removed = summary.results.filter((r) => r.status === "removed");
|
|
63
|
+
if (removed.length > 0) {
|
|
64
|
+
const names = removed.map((r) => `${r.skill}@${r.agent}`).join(", ");
|
|
65
|
+
context.io.stdout(` removed skills: ${names}`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
context.io.stdout(" no wonder-owned skills found in detected AI clients");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
context.io.stdout(` (skill removal failed: ${String(err)})`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the package's `skills.manifest.json` from the compiled CLI.
|
|
77
|
+
* `dist/commands/uninstall.js` lives two levels below the package root.
|
|
78
|
+
*/
|
|
79
|
+
function findManifestPath() {
|
|
80
|
+
try {
|
|
81
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
82
|
+
return resolve(here, "..", "..", "skills.manifest.json");
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
package/dist/wecom/export.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
// All three document types (sheet/doc/slide) share this same flow.
|
|
9
9
|
// URL parsing is in ./url.ts; cookie management is in ./cookies.ts (P1-03).
|
|
10
|
-
import { writeFileSync, mkdirSync } from "fs";
|
|
10
|
+
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
11
11
|
import { join } from "path";
|
|
12
12
|
/** Error thrown by exportWecomDoc() */
|
|
13
13
|
export class ExportError extends Error {
|
|
@@ -202,7 +202,8 @@ async function downloadExportedFile(fileUrl, fileName, saveDir) {
|
|
|
202
202
|
catch (err) {
|
|
203
203
|
throw new ExportError("write_error", `Failed to create save directory "${saveDir}": ${String(err)}`, err);
|
|
204
204
|
}
|
|
205
|
-
const
|
|
205
|
+
const safeName = sanitizeFilename(fileName);
|
|
206
|
+
const filePath = pickNonClobberingPath(saveDir, safeName);
|
|
206
207
|
try {
|
|
207
208
|
writeFileSync(filePath, Buffer.from(buffer));
|
|
208
209
|
}
|
|
@@ -211,6 +212,43 @@ async function downloadExportedFile(fileUrl, fileName, saveDir) {
|
|
|
211
212
|
}
|
|
212
213
|
return { filePath, fileSizeBytes: buffer.byteLength };
|
|
213
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Strip any directory components and disallow control characters. The upstream
|
|
217
|
+
* `fileName` is untrusted: a `../../etc/passwd` or `foo/bar.xlsx` value would
|
|
218
|
+
* otherwise escape `saveDir` via `path.join`. We keep it minimal — basename
|
|
219
|
+
* only, and drop control characters — rather than a full allow-list that
|
|
220
|
+
* would mangle Chinese titles.
|
|
221
|
+
*/
|
|
222
|
+
function sanitizeFilename(fileName) {
|
|
223
|
+
// Strip leading path segments; handles both POSIX and win32 separators.
|
|
224
|
+
let name = fileName.split(/[\\/]/).pop() ?? "";
|
|
225
|
+
// Drop control chars (0x00-0x1F, 0x7F).
|
|
226
|
+
// eslint-disable-next-line no-control-regex
|
|
227
|
+
name = name.replace(/[\x00-\x1f\x7f]/g, "");
|
|
228
|
+
name = name.trim();
|
|
229
|
+
if (name === "" || name === "." || name === "..") {
|
|
230
|
+
return "wecom-download";
|
|
231
|
+
}
|
|
232
|
+
return name;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Return `join(saveDir, name)` unless that exists, in which case append
|
|
236
|
+
* ` (2)`, ` (3)`, … before the extension. Never overwrites existing files.
|
|
237
|
+
*/
|
|
238
|
+
function pickNonClobberingPath(saveDir, name) {
|
|
239
|
+
const base = join(saveDir, name);
|
|
240
|
+
if (!existsSync(base))
|
|
241
|
+
return base;
|
|
242
|
+
const dotIdx = name.lastIndexOf(".");
|
|
243
|
+
const stem = dotIdx > 0 ? name.slice(0, dotIdx) : name;
|
|
244
|
+
const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
|
|
245
|
+
for (let i = 2; i < 1000; i++) {
|
|
246
|
+
const candidate = join(saveDir, `${stem} (${i})${ext}`);
|
|
247
|
+
if (!existsSync(candidate))
|
|
248
|
+
return candidate;
|
|
249
|
+
}
|
|
250
|
+
return join(saveDir, `${stem} (${Date.now()})${ext}`);
|
|
251
|
+
}
|
|
214
252
|
// ---------------------------------------------------------------------------
|
|
215
253
|
// Public API
|
|
216
254
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mkterswingman/5mghost-wonder",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "企微文档读取 CLI — WeCom document reader",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -25,7 +25,7 @@
|
|
|
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",
|
|
28
|
+
"test": "node dist/wecom/url.test.js && node --test tests/sheet-parity.test.mjs && node --test tests/export-sanitize.test.mjs",
|
|
29
29
|
"smoke": "npm run build && node dist/cli.js help > /dev/null",
|
|
30
30
|
"postinstall": "node scripts/postinstall.mjs"
|
|
31
31
|
},
|
|
@@ -73,7 +73,7 @@ If expired or missing, run:
|
|
|
73
73
|
wonder wecom cookie
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
This launches the local Chrome/Edge browser via CDP
|
|
76
|
+
This launches the local Chrome/Edge browser via CDP and blocks until the user scans the QR code in WeCom mobile (or the CDP wait times out). When the command returns, verify:
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
79
|
wonder wecom status
|
|
@@ -106,6 +106,28 @@ Read("/Users/<you>/Downloads/5mghost-wonder/media/image3.png")
|
|
|
106
106
|
|
|
107
107
|
**Note:** Images are full-resolution originals (up to several MB each). Only load images the user specifically asks about.
|
|
108
108
|
|
|
109
|
+
### Viewing visual layout (optional)
|
|
110
|
+
|
|
111
|
+
Use when the cell JSON alone can't answer the question because the sheet's meaning comes from **visual structure** — not from the cell values themselves. Typical signals:
|
|
112
|
+
|
|
113
|
+
- Gantt chart (date columns × task rows, coloured blocks across cell ranges)
|
|
114
|
+
- Calendar (week grid with merged day cells or coloured categories)
|
|
115
|
+
- Status board / roadmap (colour-coded cells indicating stage, owner, priority)
|
|
116
|
+
- Large merge-to-cell ratio in the JSON (`merges.length` is a non-trivial fraction of `cells.length`)
|
|
117
|
+
- User explicitly asks about "how it looks", "颜色", "排版", "这个图表", "这张表的结构"
|
|
118
|
+
|
|
119
|
+
Do **not** run render for plain data tables, lookup sheets, or when the user just wants a value. The render costs ~30 s and ~10+ MB of PDF per file.
|
|
120
|
+
|
|
121
|
+
Render the whole xlsx (one PDF page per tab, preserves layout, merges, fills, borders):
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
soffice --headless \
|
|
125
|
+
--convert-to 'pdf:calc_pdf_Export:{"SinglePageSheets":{"type":"boolean","value":"true"}}' \
|
|
126
|
+
--outdir /tmp/ <path-to-downloaded-xlsx>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Then use the Read tool on the generated PDF. Page N corresponds to the Nth tab in workbook order (same as `tabs[]` in the metadata output).
|
|
130
|
+
|
|
109
131
|
---
|
|
110
132
|
|
|
111
133
|
## docx Workflow (`doc/w3_`, `doc/e2_`)
|
|
@@ -186,20 +208,6 @@ mkdir -p /tmp/wonder-pptx-unpack && cp <path> /tmp/wonder-pptx-unpack/slide.zip
|
|
|
186
208
|
|
|
187
209
|
Then use Read tool on files in `/tmp/wonder-pptx-unpack/ppt/media/`.
|
|
188
210
|
|
|
189
|
-
### ⚠️ Known issue: python-pptx slice crash
|
|
190
|
-
|
|
191
|
-
If you use python-pptx to process WeCom pptx files, **do not use slice syntax**:
|
|
192
|
-
|
|
193
|
-
```python
|
|
194
|
-
# ❌ This crashes on WeCom pptx
|
|
195
|
-
for slide in prs.slides[:5]:
|
|
196
|
-
...
|
|
197
|
-
|
|
198
|
-
# ✅ Use iteration instead
|
|
199
|
-
for slide in prs.slides:
|
|
200
|
-
...
|
|
201
|
-
```
|
|
202
|
-
|
|
203
211
|
---
|
|
204
212
|
|
|
205
213
|
## Unsupported types
|
|
@@ -221,6 +229,7 @@ for slide in prs.slides:
|
|
|
221
229
|
| pptx slice crash | `prs.slides[:N]` → `AttributeError: 'list' object has no attribute 'rId'` | Use `for slide in prs.slides` |
|
|
222
230
|
| Cookie expiry | Cookie valid for 7–30 days | Run `wonder wecom cookie` to refresh |
|
|
223
231
|
| xlsx images are full-size | Original images can be up to 6 MB each | Only read images when user specifically needs them |
|
|
232
|
+
| xlsx visual layout needs soffice | Gantt/calendar/coloured boards lose meaning in JSON alone | Run the optional soffice render step in the xlsx section; CLI does not auto-render |
|
|
224
233
|
| smartpage unsupported | Export API returns 0% progress forever | Manual browser export |
|
|
225
234
|
|
|
226
235
|
---
|