dsh-office-kit 0.3.3 → 0.4.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.
- package/CAPABILITY.md +23 -20
- package/README.md +1 -1
- package/assets/skills/office-documents/SKILL.md +1 -1
- package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +1 -1
- package/assets/skills/office-documents/render_docx.py +10 -5
- package/assets/skills/office-documents/scripts/render_and_diff.py +1 -1
- package/assets/skills/office-documents/tasks/verify_render.md +4 -4
- package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +14 -15
- package/assets/skills/office-pdf/SKILL.md +1 -1
- package/assets/skills/office-presentations/SKILL.md +8 -7
- package/assets/skills/office-presentations/container_tools/render_slides.py +5 -5
- package/assets/skills/office-presentations/tasks/inspect-and-verify.md +5 -6
- package/assets/skills/office-spreadsheets/SKILL.md +3 -3
- package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +5 -5
- package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +3 -3
- package/assets/skills/shared/annotation-recipes.md +6 -7
- package/assets/skills/shared/check_runtime.py +19 -14
- package/assets/skills/shared/common-workflow.md +3 -3
- package/dist/index.js +107 -68
- package/package.json +2 -1
- package/postinstall.mjs +26 -10
- package/python/office_cli.py +331 -273
package/dist/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
4
|
import { readFile } from "node:fs/promises";
|
|
3
5
|
import { dirname, join } from "node:path";
|
|
4
6
|
import process from "node:process";
|
|
5
|
-
import {
|
|
7
|
+
import { promisify } from "node:util";
|
|
6
8
|
import { BUNDLED_SKILL_RANK } from "@deepseek-ai/dsh-skill";
|
|
7
9
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
8
|
-
import {
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
9
11
|
import { homedir } from "node:os";
|
|
10
12
|
//#region src/features.ts
|
|
11
13
|
/** The four entries, in menu order. */
|
|
@@ -47,6 +49,33 @@ const OFFICE_FEATURES = [
|
|
|
47
49
|
extensions: [".xlsx"]
|
|
48
50
|
}
|
|
49
51
|
];
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/kit.ts
|
|
54
|
+
/** Package root of the installed plugin (dist/ during a build, src/ in a checkout). */
|
|
55
|
+
const PACKAGE_ROOT$1 = fileURLToPath(new URL("..", import.meta.url));
|
|
56
|
+
const KIT_CLI = "@deepseek-ai/libreoffice-kit/cli";
|
|
57
|
+
/**
|
|
58
|
+
* Locate the LibreOffice kit CLI that DeepSeek Harness ships.
|
|
59
|
+
*
|
|
60
|
+
* DSH installs third-party plugins into a profile (`~/.dsh/profiles/<name>/node_modules`)
|
|
61
|
+
* while the kit lives in the harness dependency tree, so resolving relative to this
|
|
62
|
+
* package cannot find it. Resolving from the harness entry point (`process.argv[1]`,
|
|
63
|
+
* e.g. `<dsh>/node_modules/@deepseek-ai/dsh/lib/bin.js`) walks the tree the harness
|
|
64
|
+
* itself loaded from, and resolving from this package covers a checkout or a
|
|
65
|
+
* plugin-local install. `DSH_LIBREOFFICE_KIT_CLI` overrides both.
|
|
66
|
+
*/
|
|
67
|
+
function resolveKitCli(entry = process.argv[1]) {
|
|
68
|
+
const override = process.env.DSH_LIBREOFFICE_KIT_CLI;
|
|
69
|
+
if (override != null && existsSync(override)) return override;
|
|
70
|
+
for (const anchor of [entry, join(PACKAGE_ROOT$1, "package.json")]) {
|
|
71
|
+
if (anchor == null) continue;
|
|
72
|
+
try {
|
|
73
|
+
const cli = createRequire(anchor).resolve(KIT_CLI);
|
|
74
|
+
if (existsSync(cli)) return cli;
|
|
75
|
+
} catch {}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
50
79
|
/** Python packages the helper CLI needs. */
|
|
51
80
|
const PYTHON_PACKAGES = [
|
|
52
81
|
{
|
|
@@ -90,8 +119,8 @@ const PYTHON_PACKAGES = [
|
|
|
90
119
|
package: "mathml2omml"
|
|
91
120
|
}
|
|
92
121
|
];
|
|
93
|
-
const PACKAGE_ROOT
|
|
94
|
-
const POSTINSTALL_SCRIPT = join(PACKAGE_ROOT
|
|
122
|
+
const PACKAGE_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
123
|
+
const POSTINSTALL_SCRIPT = join(PACKAGE_ROOT, "postinstall.mjs");
|
|
95
124
|
/** Upper bound for one self-heal run; the remedy command remains available afterwards. */
|
|
96
125
|
const POSTINSTALL_TIMEOUT = 540 * 1e3;
|
|
97
126
|
/** Failed probes are retried after this delay instead of re-running pip on every status call. */
|
|
@@ -104,15 +133,20 @@ function legacyDataDir() {
|
|
|
104
133
|
const home = homedir();
|
|
105
134
|
return process.platform === "win32" ? join(process.env.LOCALAPPDATA ?? join(home, "AppData", "Local"), "dsh-office-kit") : process.platform === "darwin" ? join(home, "Library", "Application Support", "dsh-office-kit") : join(process.env.XDG_DATA_HOME ?? join(home, ".local", "share"), "dsh-office-kit");
|
|
106
135
|
}
|
|
136
|
+
/** Config location written by versions that ignored the platform branch. */
|
|
137
|
+
function legacyConfigFile() {
|
|
138
|
+
return process.platform === "win32" ? join(configDir(), "config.json") : join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "dsh-office-kit", "config.json");
|
|
139
|
+
}
|
|
107
140
|
/** Resolve per-OS config/data directories and bundled resources. */
|
|
108
141
|
function officePaths() {
|
|
109
142
|
const runtimeDir = join(homedir(), ".dsh", "office", "runtime");
|
|
110
143
|
return {
|
|
111
144
|
configFile: join(configDir(), "config.json"),
|
|
145
|
+
legacyConfigFile: legacyConfigFile(),
|
|
112
146
|
venv: join(runtimeDir, "venv"),
|
|
113
147
|
legacyVenv: join(legacyDataDir(), "venv"),
|
|
114
|
-
cli: join(PACKAGE_ROOT
|
|
115
|
-
skillsDir: join(PACKAGE_ROOT
|
|
148
|
+
cli: join(PACKAGE_ROOT, "python", "office_cli.py"),
|
|
149
|
+
skillsDir: join(PACKAGE_ROOT, "assets", "skills")
|
|
116
150
|
};
|
|
117
151
|
}
|
|
118
152
|
/** One-liner the user can run to repair the runtime without pnpm. */
|
|
@@ -208,26 +242,34 @@ function failedInspection(path, error) {
|
|
|
208
242
|
error: error instanceof Error ? error.message : String(error)
|
|
209
243
|
};
|
|
210
244
|
}
|
|
211
|
-
/**
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Read the interpreter path postinstall recorded; malformed files are ignored.
|
|
247
|
+
* The legacy location is read second so an upgrade does not orphan a ready venv.
|
|
248
|
+
*/
|
|
249
|
+
function savedPython(configFile, legacyConfigFile) {
|
|
250
|
+
const files = legacyConfigFile == null ? [configFile] : [configFile, legacyConfigFile];
|
|
251
|
+
for (const file of files) {
|
|
252
|
+
if (!isFile(file)) continue;
|
|
253
|
+
try {
|
|
254
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
255
|
+
if (typeof parsed.python === "string" && parsed.python.length > 0) return parsed.python;
|
|
256
|
+
} catch {}
|
|
219
257
|
}
|
|
220
258
|
}
|
|
259
|
+
/** The interpreter inside a virtual environment, which differs per platform. */
|
|
260
|
+
function venvPython(venv) {
|
|
261
|
+
return join(venv, process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
|
|
262
|
+
}
|
|
221
263
|
/** PATH-independent candidate list, plugin-owned interpreters first. */
|
|
222
264
|
function candidates(configFile) {
|
|
223
265
|
const paths = officePaths();
|
|
224
266
|
const found = [];
|
|
225
267
|
const env = process.env.DSH_OFFICE_PYTHON;
|
|
226
268
|
if (env != null && env.length > 0) found.push(env);
|
|
227
|
-
const saved = savedPython(configFile);
|
|
269
|
+
const saved = savedPython(configFile, paths.legacyConfigFile);
|
|
228
270
|
if (saved != null) found.push(saved);
|
|
229
|
-
found.push(paths.venv, paths.legacyVenv);
|
|
230
|
-
const marker = join(PACKAGE_ROOT
|
|
271
|
+
found.push(venvPython(paths.venv), venvPython(paths.legacyVenv));
|
|
272
|
+
const marker = join(PACKAGE_ROOT, ".python-path");
|
|
231
273
|
if (isFile(marker)) {
|
|
232
274
|
const text = readFileSync(marker, "utf8").trim();
|
|
233
275
|
if (text.length > 0) found.push(text);
|
|
@@ -391,58 +433,50 @@ function parseSkill(raw, path) {
|
|
|
391
433
|
content: raw.slice(match[0].length).trim()
|
|
392
434
|
};
|
|
393
435
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
"C:\\Program Files\\Kingsoft\\WPS Office",
|
|
406
|
-
"C:\\Program Files (x86)\\Kingsoft\\WPS Office",
|
|
407
|
-
join(process.env.LOCALAPPDATA ?? "", "Kingsoft", "WPS Office")
|
|
408
|
-
].filter(Boolean);
|
|
409
|
-
function wpsInstalled(binary) {
|
|
410
|
-
for (const root of WPS_ROOTS) {
|
|
411
|
-
if (existsSync(join(root, "office6", binary))) return true;
|
|
412
|
-
try {
|
|
413
|
-
for (const version of readdirSync(root)) if (existsSync(join(root, version, "office6", binary))) return true;
|
|
414
|
-
} catch {}
|
|
415
|
-
}
|
|
416
|
-
return false;
|
|
417
|
-
}
|
|
418
|
-
function rendererFacts() {
|
|
419
|
-
const libreoffice = findFile([
|
|
420
|
-
process.env.SOFFICE ?? "",
|
|
421
|
-
"C:\\Program Files\\LibreOffice\\program\\soffice.exe",
|
|
422
|
-
"C:\\Program Files (x86)\\LibreOffice\\program\\soffice.exe",
|
|
423
|
-
"/Applications/LibreOffice.app/Contents/MacOS/soffice",
|
|
424
|
-
"/usr/bin/soffice",
|
|
425
|
-
"/usr/local/bin/soffice"
|
|
426
|
-
].filter(Boolean));
|
|
427
|
-
const officeRoots = ["C:\\Program Files\\Microsoft Office\\root\\Office16", "C:\\Program Files (x86)\\Microsoft Office\\root\\Office16"];
|
|
428
|
-
return {
|
|
429
|
-
libreoffice,
|
|
430
|
-
microsoftOffice: {
|
|
431
|
-
word: officeRoots.some((root) => existsSync(join(root, "WINWORD.EXE"))),
|
|
432
|
-
powerpoint: officeRoots.some((root) => existsSync(join(root, "POWERPNT.EXE"))),
|
|
433
|
-
excel: officeRoots.some((root) => existsSync(join(root, "EXCEL.EXE")))
|
|
434
|
-
},
|
|
435
|
-
kingsoftWps: {
|
|
436
|
-
writer: wpsInstalled(WPS_BINARIES.writer),
|
|
437
|
-
presentation: wpsInstalled(WPS_BINARIES.presentation),
|
|
438
|
-
spreadsheet: wpsInstalled(WPS_BINARIES.spreadsheet)
|
|
439
|
-
}
|
|
436
|
+
const KIT_MISSING = "the DSH LibreOffice kit (@deepseek-ai/libreoffice-kit) was not found: DeepSeek Harness normally injects DSH_LIBREOFFICE_KIT_CLI, so point that variable at <kit>/lib/cli.js if this plugin runs outside the harness";
|
|
437
|
+
const runKit = promisify(execFile);
|
|
438
|
+
async function kitFacts() {
|
|
439
|
+
const cli = resolveKitCli();
|
|
440
|
+
if (cli == null) return {
|
|
441
|
+
available: false,
|
|
442
|
+
node: null,
|
|
443
|
+
cli: null,
|
|
444
|
+
version: null,
|
|
445
|
+
backend: null,
|
|
446
|
+
remedy: KIT_MISSING
|
|
440
447
|
};
|
|
448
|
+
const node = process.env.DSH_NODE ?? process.execPath;
|
|
449
|
+
try {
|
|
450
|
+
const { stdout } = await runKit(node, [
|
|
451
|
+
cli,
|
|
452
|
+
"capabilities",
|
|
453
|
+
"--json"
|
|
454
|
+
], {
|
|
455
|
+
timeout: 12e4,
|
|
456
|
+
maxBuffer: 8 * 1024 * 1024
|
|
457
|
+
});
|
|
458
|
+
const parsed = JSON.parse(stdout);
|
|
459
|
+
return {
|
|
460
|
+
available: true,
|
|
461
|
+
node,
|
|
462
|
+
cli,
|
|
463
|
+
version: typeof parsed.runtime?.version === "string" ? parsed.runtime.version : null,
|
|
464
|
+
backend: typeof parsed.runtime?.backend === "string" ? parsed.runtime.backend : null
|
|
465
|
+
};
|
|
466
|
+
} catch (error) {
|
|
467
|
+
return {
|
|
468
|
+
available: true,
|
|
469
|
+
node,
|
|
470
|
+
cli,
|
|
471
|
+
version: null,
|
|
472
|
+
backend: null,
|
|
473
|
+
error: error instanceof Error ? error.message.split("\n")[0] ?? error.message : String(error)
|
|
474
|
+
};
|
|
475
|
+
}
|
|
441
476
|
}
|
|
442
|
-
const PACKAGE_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
443
477
|
function pluginVersion() {
|
|
444
478
|
try {
|
|
445
|
-
const parsed = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
479
|
+
const parsed = JSON.parse(readFileSync(join(PACKAGE_ROOT$1, "package.json"), "utf8"));
|
|
446
480
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
447
481
|
} catch {
|
|
448
482
|
return "unknown";
|
|
@@ -460,6 +494,11 @@ function runtimeReason(runtime) {
|
|
|
460
494
|
function apply(ctx) {
|
|
461
495
|
const paths = officePaths();
|
|
462
496
|
const candidates = [];
|
|
497
|
+
const kitCli = resolveKitCli();
|
|
498
|
+
if (kitCli != null) {
|
|
499
|
+
process.env.DSH_LIBREOFFICE_KIT_CLI ??= kitCli;
|
|
500
|
+
process.env.DSH_NODE ??= process.execPath;
|
|
501
|
+
}
|
|
463
502
|
for (const feature of OFFICE_FEATURES) {
|
|
464
503
|
const directory = join(paths.skillsDir, feature.skill);
|
|
465
504
|
const path = join(directory, "SKILL.md");
|
|
@@ -521,7 +560,7 @@ function apply(ctx) {
|
|
|
521
560
|
ctx.effect(() => ctx.skills.registerProvider(() => provider), "office-suite: skills");
|
|
522
561
|
ctx.effect(() => ctx.tools.register(defineTool({
|
|
523
562
|
name: "office_status",
|
|
524
|
-
description: "Report the Office runtime state: compatible Python, installed Python packages, helper CLI path, and document renderers (
|
|
563
|
+
description: "Report the Office runtime state: compatible Python, installed Python packages, helper CLI path, and document renderers (the DSH LibreOffice kit). Call this before Office file work.",
|
|
525
564
|
parameters: {},
|
|
526
565
|
output: {
|
|
527
566
|
schema: { type: "string" },
|
|
@@ -560,7 +599,7 @@ function apply(ctx) {
|
|
|
560
599
|
exists: cliExists()
|
|
561
600
|
},
|
|
562
601
|
packages: runtime.packages,
|
|
563
|
-
renderers:
|
|
602
|
+
renderers: await kitFacts(),
|
|
564
603
|
skills: candidates.map((candidate) => candidate.name)
|
|
565
604
|
};
|
|
566
605
|
return JSON.stringify(report, null, 2);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-office-kit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"description": "Office documents, PDF, presentations, and spreadsheets for DeepSeek Harness",
|
|
6
6
|
"author": "Hairy",
|
|
7
7
|
"license": "MIT",
|
|
@@ -110,6 +110,7 @@
|
|
|
110
110
|
"@deepseek-ai/dsh-util-code-language": "0.2.1-rc.2",
|
|
111
111
|
"@deepseek-ai/dsh-util-values": "0.2.1-rc.2",
|
|
112
112
|
"@deepseek-ai/dsh-workspace": "0.2.1-rc.2",
|
|
113
|
+
"@deepseek-ai/libreoffice-kit": "^0.1.5",
|
|
113
114
|
"@types/node": "^26.1.1",
|
|
114
115
|
"@types/react": "^18.2.0",
|
|
115
116
|
"@types/react-dom": "^18.2.0",
|
package/postinstall.mjs
CHANGED
|
@@ -35,8 +35,21 @@ const LEGACY_DATA_DIR = IS_WINDOWS
|
|
|
35
35
|
: join(process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share'), 'dsh-office-kit')
|
|
36
36
|
export const LEGACY_VENV_DIR = join(LEGACY_DATA_DIR, 'venv')
|
|
37
37
|
const LEGACY_VENV_PYTHON = join(LEGACY_VENV_DIR, IS_WINDOWS ? 'Scripts/python.exe' : 'bin/python')
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// Must mirror `configDir()` in src/python.ts: the host half reads this file on
|
|
39
|
+
// every runtime probe, so a platform mismatch here makes a ready venv look missing.
|
|
40
|
+
export const CONFIG_DIR = IS_WINDOWS
|
|
41
|
+
? join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'dsh-office-kit')
|
|
42
|
+
: process.platform === 'darwin'
|
|
43
|
+
? join(homedir(), 'Library', 'Application Support', 'dsh-office-kit')
|
|
44
|
+
: join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'dsh-office-kit')
|
|
45
|
+
export const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
|
|
46
|
+
/**
|
|
47
|
+
* Legacy config location written by versions without the platform branch.
|
|
48
|
+
* Read as a fallback so an existing runtime is not rebuilt on upgrade.
|
|
49
|
+
*/
|
|
50
|
+
const LEGACY_CONFIG_FILE = IS_WINDOWS
|
|
51
|
+
? CONFIG_FILE
|
|
52
|
+
: join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'dsh-office-kit', 'config.json')
|
|
40
53
|
const REQUIREMENTS = join(PACKAGE_ROOT, 'python', 'requirements.txt')
|
|
41
54
|
const LOG_PREFIX = '[dsh-office-kit]'
|
|
42
55
|
|
|
@@ -271,15 +284,18 @@ function verifyImports(python) {
|
|
|
271
284
|
}
|
|
272
285
|
|
|
273
286
|
function readConfig() {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
287
|
+
for (const file of [CONFIG_FILE, LEGACY_CONFIG_FILE]) {
|
|
288
|
+
if (!existsSync(file)) {
|
|
289
|
+
continue
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
return JSON.parse(readFileSync(file, 'utf8'))
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// A malformed file must not hide a usable legacy one.
|
|
296
|
+
}
|
|
282
297
|
}
|
|
298
|
+
return {}
|
|
283
299
|
}
|
|
284
300
|
|
|
285
301
|
function writeConfig(config) {
|