dsh-office-kit 0.3.3 → 0.4.0

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,12 @@
1
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
3
  import { readFile } from "node:fs/promises";
3
4
  import { dirname, join } from "node:path";
4
5
  import process from "node:process";
5
6
  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";
9
10
  import { homedir } from "node:os";
10
11
  //#region src/features.ts
11
12
  /** The four entries, in menu order. */
@@ -104,11 +105,16 @@ function legacyDataDir() {
104
105
  const home = homedir();
105
106
  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
107
  }
108
+ /** Config location written by versions that ignored the platform branch. */
109
+ function legacyConfigFile() {
110
+ return process.platform === "win32" ? join(configDir(), "config.json") : join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "dsh-office-kit", "config.json");
111
+ }
107
112
  /** Resolve per-OS config/data directories and bundled resources. */
108
113
  function officePaths() {
109
114
  const runtimeDir = join(homedir(), ".dsh", "office", "runtime");
110
115
  return {
111
116
  configFile: join(configDir(), "config.json"),
117
+ legacyConfigFile: legacyConfigFile(),
112
118
  venv: join(runtimeDir, "venv"),
113
119
  legacyVenv: join(legacyDataDir(), "venv"),
114
120
  cli: join(PACKAGE_ROOT$1, "python", "office_cli.py"),
@@ -208,25 +214,33 @@ function failedInspection(path, error) {
208
214
  error: error instanceof Error ? error.message : String(error)
209
215
  };
210
216
  }
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;
217
+ /**
218
+ * Read the interpreter path postinstall recorded; malformed files are ignored.
219
+ * The legacy location is read second so an upgrade does not orphan a ready venv.
220
+ */
221
+ function savedPython(configFile, legacyConfigFile) {
222
+ const files = legacyConfigFile == null ? [configFile] : [configFile, legacyConfigFile];
223
+ for (const file of files) {
224
+ if (!isFile(file)) continue;
225
+ try {
226
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
227
+ if (typeof parsed.python === "string" && parsed.python.length > 0) return parsed.python;
228
+ } catch {}
219
229
  }
220
230
  }
231
+ /** The interpreter inside a virtual environment, which differs per platform. */
232
+ function venvPython(venv) {
233
+ return join(venv, process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
234
+ }
221
235
  /** PATH-independent candidate list, plugin-owned interpreters first. */
222
236
  function candidates(configFile) {
223
237
  const paths = officePaths();
224
238
  const found = [];
225
239
  const env = process.env.DSH_OFFICE_PYTHON;
226
240
  if (env != null && env.length > 0) found.push(env);
227
- const saved = savedPython(configFile);
241
+ const saved = savedPython(configFile, paths.legacyConfigFile);
228
242
  if (saved != null) found.push(saved);
229
- found.push(paths.venv, paths.legacyVenv);
243
+ found.push(venvPython(paths.venv), venvPython(paths.legacyVenv));
230
244
  const marker = join(PACKAGE_ROOT$1, ".python-path");
231
245
  if (isFile(marker)) {
232
246
  const text = readFileSync(marker, "utf8").trim();
@@ -391,55 +405,61 @@ function parseSkill(raw, path) {
391
405
  content: raw.slice(match[0].length).trim()
392
406
  };
393
407
  }
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 {}
408
+ const KIT_MISSING = "the DSH LibreOffice kit (@deepseek-ai/libreoffice-kit) was not found: install this plugin inside the harness tree, or point DSH_LIBREOFFICE_KIT_CLI at <kit>/lib/cli.js";
409
+ const PACKAGE_ROOT = fileURLToPath(new URL("..", import.meta.url));
410
+ const runKit = promisify(execFile);
411
+ /** Locate the kit the same way python/office_cli.py does, so both halves agree. */
412
+ function findKitCli() {
413
+ const override = process.env.DSH_LIBREOFFICE_KIT_CLI;
414
+ if (override != null && existsSync(override)) return override;
415
+ let directory = PACKAGE_ROOT;
416
+ for (;;) {
417
+ const candidate = join(directory, "node_modules", "@deepseek-ai", "libreoffice-kit", "lib", "cli.js");
418
+ if (existsSync(candidate)) return candidate;
419
+ const parent = dirname(directory);
420
+ if (parent === directory) return null;
421
+ directory = parent;
415
422
  }
416
- return false;
417
423
  }
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
- }
424
+ async function kitFacts() {
425
+ const cli = findKitCli();
426
+ if (cli == null) return {
427
+ available: false,
428
+ node: null,
429
+ cli: null,
430
+ version: null,
431
+ backend: null,
432
+ remedy: KIT_MISSING
440
433
  };
434
+ const node = process.env.DSH_NODE ?? process.execPath;
435
+ try {
436
+ const { stdout } = await runKit(node, [
437
+ cli,
438
+ "capabilities",
439
+ "--json"
440
+ ], {
441
+ timeout: 12e4,
442
+ maxBuffer: 8 * 1024 * 1024
443
+ });
444
+ const parsed = JSON.parse(stdout);
445
+ return {
446
+ available: true,
447
+ node,
448
+ cli,
449
+ version: typeof parsed.runtime?.version === "string" ? parsed.runtime.version : null,
450
+ backend: typeof parsed.runtime?.backend === "string" ? parsed.runtime.backend : null
451
+ };
452
+ } catch (error) {
453
+ return {
454
+ available: true,
455
+ node,
456
+ cli,
457
+ version: null,
458
+ backend: null,
459
+ error: error instanceof Error ? error.message.split("\n")[0] ?? error.message : String(error)
460
+ };
461
+ }
441
462
  }
442
- const PACKAGE_ROOT = fileURLToPath(new URL("..", import.meta.url));
443
463
  function pluginVersion() {
444
464
  try {
445
465
  const parsed = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
@@ -521,7 +541,7 @@ function apply(ctx) {
521
541
  ctx.effect(() => ctx.skills.registerProvider(() => provider), "office-suite: skills");
522
542
  ctx.effect(() => ctx.tools.register(defineTool({
523
543
  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.",
544
+ 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
545
  parameters: {},
526
546
  output: {
527
547
  schema: { type: "string" },
@@ -560,7 +580,7 @@ function apply(ctx) {
560
580
  exists: cliExists()
561
581
  },
562
582
  packages: runtime.packages,
563
- renderers: rendererFacts(),
583
+ renderers: await kitFacts(),
564
584
  skills: candidates.map((candidate) => candidate.name)
565
585
  };
566
586
  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.0",
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) {