dsh-office-kit 0.4.5 → 0.4.6

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 CHANGED
@@ -138,10 +138,11 @@ never guessed.
138
138
  | PDF create / read / render / `metadata.py` / `page_ops.py` / `redact_region.py` | ok — CJK font applied, metadata written, merge→remove→rotate, region redaction removed 170 chars and 11 drawings, `page-1.png` produced |
139
139
  | Annotation geometry helpers | pymupdf rect conversion, python-pptx overlap scan, A1 text matching run against those files |
140
140
  | Forbidden-vocabulary grep over the delivered docs | empty |
141
+ | macOS 26 arm64, DSH 0.2.1-rc.2, `DSH_OFFICE_PYTHON` = Homebrew 3.13 | full vitest suite with `DSH_OFFICE_REQUIRE_RENDERER=1` passes; docx/pptx/xlsx render and xlsx recalc report `engine: native`, also with the app's Electron binary as the recorded node; PDF create/stamp pick STHeiti |
141
142
 
142
143
  Not claimed (no evidence here): installer or self-heal timings, live GUI
143
- interaction (`/` menu, chip, sidebar preview, annotation overlay), and macOS
144
- renderer support.
144
+ interaction (`/` menu, chip, sidebar preview, annotation overlay), and automatic
145
+ Python discovery on macOS (see #23).
145
146
 
146
147
  ## Explicitly out of scope
147
148
 
@@ -152,6 +153,6 @@ renderer support.
152
153
  so); OCR and scanned-PDF text extraction.
153
154
  - PPTX animations, speaker notes, slide masters; XLSX pivots, macros, data
154
155
  validation, conditional formatting — no spec keys or verbs in the helper.
155
- - Formula evaluation without the DSH LibreOffice kit; macOS/Linux installation
156
- was exercised in CI (Linux runs the WASM engine), but only Windows was checked
157
- by hand.
156
+ - Formula evaluation without the DSH LibreOffice kit; Linux installation was
157
+ exercised in CI (Linux runs the WASM engine); Windows and macOS were checked by
158
+ hand.
@@ -5,7 +5,9 @@ Word's TOC field needs a refresh before its links work. This script instead
5
5
  writes static hyperlinks that any renderer follows: bookmarks on every heading,
6
6
  a contents list at the top (with quick links and back links), and optional
7
7
  Top/Bottom anchors. Because the entries are literal text there is nothing to
8
- update before the file is read.
8
+ update before the file is read. Rerunning on a file this script already
9
+ processed strips the previous navigation first, so the output is rebuilt
10
+ cleanly instead of stacking a second contents block.
9
11
 
10
12
  Exit codes: 0 success, 1 no headings, 2 bad input.
11
13
  """
@@ -59,6 +61,63 @@ def paragraph(style: str | None = None):
59
61
  return node
60
62
 
61
63
 
64
+ def _remove_bookmark(node) -> None:
65
+ """Drop one bookmarkStart and its matching bookmarkEnd wherever they live."""
66
+ ident = node.get(kit.q("id"))
67
+ parent = node.getparent()
68
+ if parent is not None:
69
+ parent.remove(node)
70
+ if ident is None:
71
+ return
72
+ root = parent if parent is not None else node
73
+ while root.getparent() is not None:
74
+ root = root.getparent()
75
+ for end in list(root.iter(kit.q("bookmarkEnd"))):
76
+ if end.get(kit.q("id")) == ident:
77
+ end.getparent().remove(end)
78
+
79
+
80
+ def strip_previous_nav(body) -> int:
81
+ """Remove navigation inserted by an earlier run, so reruns rebuild cleanly.
82
+
83
+ Without this, a second run stacks a second contents block, re-collects the
84
+ first block's title as a chapter, and duplicates the nav_top/nav_toc/
85
+ nav_bottom bookmark names, which makes every back link resolve to the stale
86
+ block. A paragraph is treated as generated when it links to a nav_ anchor or
87
+ carries a structural nav bookmark; a real heading only loses its nav_h
88
+ bookmarks. Returns the number of paragraphs removed.
89
+ """
90
+ removed = 0
91
+ for paragraph_node in list(body.iter(kit.q("p"))):
92
+ bookmarks = [
93
+ node
94
+ for node in paragraph_node.iter(kit.q("bookmarkStart"))
95
+ if (node.get(kit.q("name")) or "").startswith("nav_")
96
+ ]
97
+ links = [
98
+ node
99
+ for node in paragraph_node.iter(kit.q("hyperlink"))
100
+ if (node.get(kit.q("anchor")) or "").startswith("nav_")
101
+ ]
102
+ if not bookmarks and not links:
103
+ continue
104
+ generated = bool(links) or any(
105
+ node.get(kit.q("name")) in (TOP_BOOKMARK, BOTTOM_BOOKMARK, TOC_BOOKMARK)
106
+ for node in bookmarks
107
+ )
108
+ if generated:
109
+ for node in bookmarks:
110
+ _remove_bookmark(node)
111
+ parent = paragraph_node.getparent()
112
+ if parent is not None:
113
+ parent.remove(paragraph_node)
114
+ removed += 1
115
+ else:
116
+ for node in bookmarks:
117
+ _remove_bookmark(node)
118
+ return removed
119
+
120
+
62
121
  def main(argv: list[str] | None = None) -> int:
63
122
  parser = argparse.ArgumentParser(description="Add bookmark anchors and static internal links to headings.")
64
123
  parser.add_argument("input_docx", help="Input .docx")
@@ -77,6 +136,7 @@ def main(argv: list[str] | None = None) -> int:
77
136
  return 2
78
137
 
79
138
  body = doc.body()
139
+ stripped = strip_previous_nav(body)
80
140
  headings = []
81
141
  ident = doc.next_id()
82
142
  for paragraph_node in list(body.iter(kit.q("p"))):
@@ -147,7 +207,7 @@ def main(argv: list[str] | None = None) -> int:
147
207
  body.append(last)
148
208
 
149
209
  doc.save(args.out)
150
- kit.emit({"ok": True, "output": str(Path(args.out)), "headings": headings, "entries": len(block)})
210
+ kit.emit({"ok": True, "output": str(Path(args.out)), "headings": headings, "entries": len(block), "stripped": stripped})
151
211
  return 0
152
212
 
153
213
 
@@ -25,9 +25,9 @@ CJK_FONT_CANDIDATES = {
25
25
  "/usr/share/fonts/opentype/noto/NotoSerifCJK-Regular.ttc",
26
26
  ],
27
27
  "darwin": [
28
- "/System/Library/Fonts/PingFang.ttc",
29
28
  "/System/Library/Fonts/STHeiti Medium.ttc",
30
- "/System/Library/Fonts/Hiragino Sans GB.ttc",
29
+ "/System/Library/Fonts/STHeiti Light.ttc",
30
+ "/System/Library/Fonts/Supplemental/Songti.ttc",
31
31
  ],
32
32
  }
33
33
 
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
2
2
  import { execFile } from "node:child_process";
3
3
  import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
4
4
  import { readFile } from "node:fs/promises";
5
- import { dirname, join } from "node:path";
5
+ import { dirname, join, sep } from "node:path";
6
6
  import process from "node:process";
7
7
  import { promisify } from "node:util";
8
8
  import { BUNDLED_SKILL_RANK } from "@deepseek-ai/dsh-skill";
@@ -55,6 +55,16 @@ const OFFICE_FEATURES = [
55
55
  const PACKAGE_ROOT$1 = fileURLToPath(new URL("..", import.meta.url));
56
56
  const KIT_CLI = "@deepseek-ai/libreoffice-kit/cli";
57
57
  /**
58
+ * Electron reads `app.asar` as a directory but nothing else can, and the helper
59
+ * runs the recorded path from Python. The packaged app ships the kit unpacked.
60
+ */
61
+ function outsideAsar(path) {
62
+ const packed = `${sep}app.asar${sep}`;
63
+ if (!path.includes(packed)) return path;
64
+ const unpacked = path.replace(packed, `${sep}app.asar.unpacked${sep}`);
65
+ return existsSync(unpacked) ? unpacked : path;
66
+ }
67
+ /**
58
68
  * Locate the LibreOffice kit CLI that DeepSeek Harness ships.
59
69
  *
60
70
  * DSH installs third-party plugins into a profile (`~/.dsh/profiles/<name>/node_modules`)
@@ -71,7 +81,7 @@ function resolveKitCli(entry = process.argv[1]) {
71
81
  if (anchor == null) continue;
72
82
  try {
73
83
  const cli = createRequire(anchor).resolve(KIT_CLI);
74
- if (existsSync(cli)) return cli;
84
+ if (existsSync(cli)) return outsideAsar(cli);
75
85
  } catch {}
76
86
  }
77
87
  return null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-office-kit",
3
3
  "type": "module",
4
- "version": "0.4.5",
4
+ "version": "0.4.6",
5
5
  "description": "Office documents, PDF, presentations, and spreadsheets for DeepSeek Harness",
6
6
  "author": "Hairy",
7
7
  "license": "MIT",
@@ -184,6 +184,11 @@ def find_kit() -> tuple[str, str] | None:
184
184
  return node, str(cli)
185
185
 
186
186
 
187
+ def _kit_env() -> dict[str, str]:
188
+ """Environment for a kit run: the recorded node may be the harness's Electron binary."""
189
+ return {**os.environ, "ELECTRON_RUN_AS_NODE": "1"}
190
+
191
+
187
192
  def _kit_call(arguments: list[str], timeout: int = 300) -> dict[str, Any]:
188
193
  """Run one kit command and return its JSON report, failing with the kit's own words."""
189
194
  kit = find_kit()
@@ -195,6 +200,7 @@ def _kit_call(arguments: list[str], timeout: int = 300) -> dict[str, Any]:
195
200
  [node, cli, *arguments],
196
201
  capture_output=True,
197
202
  text=True,
203
+ env=_kit_env(),
198
204
  # The kit always speaks UTF-8; the locale code page would mangle CJK paths.
199
205
  encoding="utf-8",
200
206
  errors="replace",
@@ -231,6 +237,7 @@ def kit_report() -> dict[str, Any]:
231
237
  [node, cli, "capabilities", "--json"],
232
238
  capture_output=True,
233
239
  text=True,
240
+ env=_kit_env(),
234
241
  encoding="utf-8",
235
242
  errors="replace",
236
243
  timeout=120,
@@ -763,8 +770,9 @@ _PDF_CJK_CANDIDATES = [
763
770
  r"C:\Windows\Fonts\msyh.ttf",
764
771
  r"C:\Windows\Fonts\simhei.ttf",
765
772
  r"C:\Windows\Fonts\simsun.ttc",
766
- "/System/Library/Fonts/PingFang.ttc",
767
773
  "/System/Library/Fonts/STHeiti Light.ttc",
774
+ "/System/Library/Fonts/STHeiti Medium.ttc",
775
+ "/System/Library/Fonts/Supplemental/Songti.ttc",
768
776
  "/usr/share/fonts/truetype/winfonts/msyh.ttc",
769
777
  "/usr/share/fonts/truetype/winfonts/msyhl.ttc",
770
778
  "/usr/share/fonts/truetype/winfonts/simhei.ttf",