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/dist/index.js CHANGED
@@ -1,11 +1,13 @@
1
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
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 { fileURLToPath } from "node:url";
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 { execFile } from "node:child_process";
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$1 = fileURLToPath(new URL("..", import.meta.url));
94
- const POSTINSTALL_SCRIPT = join(PACKAGE_ROOT$1, "postinstall.mjs");
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$1, "python", "office_cli.py"),
115
- skillsDir: join(PACKAGE_ROOT$1, "assets", "skills")
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
- /** Read the interpreter path postinstall recorded; malformed files are ignored. */
212
- function savedPython(configFile) {
213
- if (!isFile(configFile)) return void 0;
214
- try {
215
- const parsed = JSON.parse(readFileSync(configFile, "utf8"));
216
- return typeof parsed.python === "string" && parsed.python.length > 0 ? parsed.python : void 0;
217
- } catch {
218
- return;
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$1, ".python-path");
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
- function findFile(candidates) {
395
- for (const path of candidates) if (existsSync(path)) return path;
396
- return null;
397
- }
398
- const WPS_BINARIES = {
399
- writer: "wps.exe",
400
- presentation: "wpp.exe",
401
- spreadsheet: "et.exe"
402
- };
403
- const WPS_ROOTS = [
404
- process.env.WPS_OFFICE_HOME ?? "",
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 (LibreOffice, Microsoft Office, or WPS Office). Call this before Office file work.",
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: rendererFacts(),
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.3.3",
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
- const CONFIG_DIR = join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'dsh-office-kit')
39
- const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
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
- if (!existsSync(CONFIG_FILE)) {
275
- return {}
276
- }
277
- try {
278
- return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'))
279
- }
280
- catch {
281
- return {}
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) {