dsh-office-kit 0.2.0 → 0.3.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.
Files changed (197) hide show
  1. package/CAPABILITY.md +147 -68
  2. package/README.md +3 -1
  3. package/assets/skills/excel-live-control/SKILL.md +234 -0
  4. package/assets/skills/excel-live-control/container_tools/live_excel.py +1589 -0
  5. package/assets/skills/excel-live-control/domain_guidance/financial_models.md +86 -0
  6. package/assets/skills/excel-live-control/domain_guidance/healthcare.md +58 -0
  7. package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +53 -0
  8. package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +53 -0
  9. package/assets/skills/excel-live-control/references/office-automation.md +207 -0
  10. package/assets/skills/excel-live-control/references/read_only_qna.md +59 -0
  11. package/assets/skills/excel-live-control/style_guidelines.md +87 -0
  12. package/assets/skills/excel-live-control/workflows/create_workflows.md +78 -0
  13. package/assets/skills/excel-live-control/workflows/edit_workflows.md +72 -0
  14. package/assets/skills/office-documents/SKILL.md +90 -77
  15. package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +86 -0
  16. package/assets/skills/office-documents/ooxml/comments.md +40 -0
  17. package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +66 -0
  18. package/assets/skills/office-documents/ooxml/rels_and_content_types.md +62 -0
  19. package/assets/skills/office-documents/ooxml/tracked_changes.md +53 -0
  20. package/assets/skills/office-documents/references/docx-structure.md +83 -0
  21. package/assets/skills/office-documents/references/template-elicitation.md +49 -0
  22. package/assets/skills/office-documents/references/writing-quality.md +49 -0
  23. package/assets/skills/office-documents/render_docx.py +99 -0
  24. package/assets/skills/office-documents/scripts/a11y_audit.py +201 -0
  25. package/assets/skills/office-documents/scripts/accept_tracked_changes.py +158 -0
  26. package/assets/skills/office-documents/scripts/add_tracked_replacements.py +155 -0
  27. package/assets/skills/office-documents/scripts/append_blocks.py +115 -0
  28. package/assets/skills/office-documents/scripts/apply_template_styles.py +111 -0
  29. package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +109 -0
  30. package/assets/skills/office-documents/scripts/comments_add.py +143 -0
  31. package/assets/skills/office-documents/scripts/comments_apply_patch.py +121 -0
  32. package/assets/skills/office-documents/scripts/comments_extract.py +92 -0
  33. package/assets/skills/office-documents/scripts/comments_strip.py +81 -0
  34. package/assets/skills/office-documents/scripts/content_controls.py +199 -0
  35. package/assets/skills/office-documents/scripts/core_properties.py +131 -0
  36. package/assets/skills/office-documents/scripts/docx_kit.py +594 -0
  37. package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +232 -0
  38. package/assets/skills/office-documents/scripts/docx_table_to_csv.py +106 -0
  39. package/assets/skills/office-documents/scripts/fields_materialize.py +73 -0
  40. package/assets/skills/office-documents/scripts/fields_report.py +66 -0
  41. package/assets/skills/office-documents/scripts/flatten_ref_fields.py +69 -0
  42. package/assets/skills/office-documents/scripts/footnotes_report.py +84 -0
  43. package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +103 -0
  44. package/assets/skills/office-documents/scripts/heading_audit.py +88 -0
  45. package/assets/skills/office-documents/scripts/images_audit.py +87 -0
  46. package/assets/skills/office-documents/scripts/insert_note.py +133 -0
  47. package/assets/skills/office-documents/scripts/insert_ref_fields.py +97 -0
  48. package/assets/skills/office-documents/scripts/insert_toc.py +91 -0
  49. package/assets/skills/office-documents/scripts/internal_nav.py +153 -0
  50. package/assets/skills/office-documents/scripts/make_fixtures.py +176 -0
  51. package/assets/skills/office-documents/scripts/merge_docx_append.py +246 -0
  52. package/assets/skills/office-documents/scripts/privacy_scrub.py +138 -0
  53. package/assets/skills/office-documents/scripts/redact_docx.py +120 -0
  54. package/assets/skills/office-documents/scripts/render_and_diff.py +108 -0
  55. package/assets/skills/office-documents/scripts/replace_text.py +161 -0
  56. package/assets/skills/office-documents/scripts/section_audit.py +98 -0
  57. package/assets/skills/office-documents/scripts/set_protection.py +99 -0
  58. package/assets/skills/office-documents/scripts/style_lint.py +118 -0
  59. package/assets/skills/office-documents/scripts/style_normalize.py +110 -0
  60. package/assets/skills/office-documents/scripts/table_geometry.py +174 -0
  61. package/assets/skills/office-documents/scripts/watermark_add.py +55 -0
  62. package/assets/skills/office-documents/scripts/watermark_audit_remove.py +74 -0
  63. package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +124 -0
  64. package/assets/skills/office-documents/tasks/accessibility_a11y.md +45 -0
  65. package/assets/skills/office-documents/tasks/captions_crossrefs.md +49 -0
  66. package/assets/skills/office-documents/tasks/clean_tracked_changes.md +49 -0
  67. package/assets/skills/office-documents/tasks/comments_manage.md +59 -0
  68. package/assets/skills/office-documents/tasks/compare_diff.md +40 -0
  69. package/assets/skills/office-documents/tasks/create_edit.md +107 -0
  70. package/assets/skills/office-documents/tasks/equations.md +59 -0
  71. package/assets/skills/office-documents/tasks/fields_update.md +44 -0
  72. package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +50 -0
  73. package/assets/skills/office-documents/tasks/footnotes_endnotes.md +38 -0
  74. package/assets/skills/office-documents/tasks/forms_content_controls.md +48 -0
  75. package/assets/skills/office-documents/tasks/headings_numbering.md +55 -0
  76. package/assets/skills/office-documents/tasks/images_figures.md +63 -0
  77. package/assets/skills/office-documents/tasks/multi_doc_merge.md +43 -0
  78. package/assets/skills/office-documents/tasks/navigation_internal_links.md +38 -0
  79. package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +45 -0
  80. package/assets/skills/office-documents/tasks/protection_restrict_editing.md +41 -0
  81. package/assets/skills/office-documents/tasks/read_review.md +50 -0
  82. package/assets/skills/office-documents/tasks/redaction_anonymization.md +42 -0
  83. package/assets/skills/office-documents/tasks/sections_layout.md +54 -0
  84. package/assets/skills/office-documents/tasks/style_lint_normalize.md +44 -0
  85. package/assets/skills/office-documents/tasks/tables_spreadsheets.md +64 -0
  86. package/assets/skills/office-documents/tasks/templates_style_packs.md +53 -0
  87. package/assets/skills/office-documents/tasks/toc_workflow.md +39 -0
  88. package/assets/skills/office-documents/tasks/verify_render.md +50 -0
  89. package/assets/skills/office-documents/tasks/watermarks_background.md +35 -0
  90. package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +47 -0
  91. package/assets/skills/office-documents/troubleshooting/run_splitting.md +36 -0
  92. package/assets/skills/office-pdf/SKILL.md +73 -56
  93. package/assets/skills/office-pdf/references/form-fields.md +56 -0
  94. package/assets/skills/office-pdf/references/geometry.md +44 -0
  95. package/assets/skills/office-pdf/references/layout-quality.md +41 -0
  96. package/assets/skills/office-pdf/references/limits.md +58 -0
  97. package/assets/skills/office-pdf/scripts/extract_text.py +172 -0
  98. package/assets/skills/office-pdf/scripts/forms.py +556 -0
  99. package/assets/skills/office-pdf/scripts/metadata.py +161 -0
  100. package/assets/skills/office-pdf/scripts/page_ops.py +198 -0
  101. package/assets/skills/office-pdf/scripts/redact_region.py +347 -0
  102. package/assets/skills/office-pdf/scripts/render_pages.py +207 -0
  103. package/assets/skills/office-pdf/scripts/split_pages.py +155 -0
  104. package/assets/skills/office-pdf/scripts/stamp_overlay.py +199 -0
  105. package/assets/skills/office-pdf/tasks/annotations.md +98 -0
  106. package/assets/skills/office-pdf/tasks/create.md +75 -0
  107. package/assets/skills/office-pdf/tasks/edit-pages.md +71 -0
  108. package/assets/skills/office-pdf/tasks/forms.md +67 -0
  109. package/assets/skills/office-pdf/tasks/read.md +62 -0
  110. package/assets/skills/office-pdf/tasks/redact.md +72 -0
  111. package/assets/skills/office-pdf/tasks/stamp.md +60 -0
  112. package/assets/skills/office-presentations/SKILL.md +141 -87
  113. package/assets/skills/office-presentations/container_tools/add_slides.py +143 -0
  114. package/assets/skills/office-presentations/container_tools/core_properties.py +127 -0
  115. package/assets/skills/office-presentations/container_tools/create_montage.py +133 -0
  116. package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +292 -0
  117. package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +164 -0
  118. package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +151 -0
  119. package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +206 -0
  120. package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +219 -0
  121. package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +352 -0
  122. package/assets/skills/office-presentations/container_tools/preview_render.py +382 -0
  123. package/assets/skills/office-presentations/container_tools/render_slides.py +146 -0
  124. package/assets/skills/office-presentations/container_tools/replace_text.py +175 -0
  125. package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +236 -0
  126. package/assets/skills/office-presentations/domain_guidance/finance.md +66 -0
  127. package/assets/skills/office-presentations/recipes/README.md +71 -0
  128. package/assets/skills/office-presentations/recipes/autolayout.md +88 -0
  129. package/assets/skills/office-presentations/recipes/connectors.md +83 -0
  130. package/assets/skills/office-presentations/recipes/images.md +80 -0
  131. package/assets/skills/office-presentations/recipes/master-and-layouts.md +127 -0
  132. package/assets/skills/office-presentations/recipes/native-charts.md +87 -0
  133. package/assets/skills/office-presentations/recipes/rich-text-runs.md +86 -0
  134. package/assets/skills/office-presentations/recipes/speaker-notes.md +65 -0
  135. package/assets/skills/office-presentations/recipes/tables.md +88 -0
  136. package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +71 -0
  137. package/assets/skills/office-presentations/recipes/theme-tokens.md +87 -0
  138. package/assets/skills/office-presentations/references/authoring-workflow.md +65 -0
  139. package/assets/skills/office-presentations/references/finalization.md +63 -0
  140. package/assets/skills/office-presentations/references/native-bullets.md +78 -0
  141. package/assets/skills/office-presentations/references/native-evidence.md +67 -0
  142. package/assets/skills/office-presentations/references/style-guidelines.md +66 -0
  143. package/assets/skills/office-presentations/references/templates.md +76 -0
  144. package/assets/skills/office-presentations/routing/google_slides.md +45 -0
  145. package/assets/skills/office-presentations/tasks/apply-annotations.md +83 -0
  146. package/assets/skills/office-presentations/tasks/charts-tables-notes.md +79 -0
  147. package/assets/skills/office-presentations/tasks/create-deck.md +106 -0
  148. package/assets/skills/office-presentations/tasks/edit-deck.md +93 -0
  149. package/assets/skills/office-presentations/tasks/inspect-and-verify.md +107 -0
  150. package/assets/skills/office-presentations/tasks/template-following.md +132 -0
  151. package/assets/skills/office-presentations/template_following_scripts/check_template_fidelity.py +183 -0
  152. package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +161 -0
  153. package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +118 -0
  154. package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +246 -0
  155. package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +192 -0
  156. package/assets/skills/office-spreadsheets/SKILL.md +105 -101
  157. package/assets/skills/office-spreadsheets/container_tools/add_chart.py +243 -0
  158. package/assets/skills/office-spreadsheets/container_tools/add_image.py +114 -0
  159. package/assets/skills/office-spreadsheets/container_tools/append_rows.py +129 -0
  160. package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +313 -0
  161. package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +405 -0
  162. package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +287 -0
  163. package/assets/skills/office-spreadsheets/container_tools/set_cells.py +142 -0
  164. package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +141 -0
  165. package/assets/skills/office-spreadsheets/container_tools/style_cells.py +140 -0
  166. package/assets/skills/office-spreadsheets/recipes/charts.md +66 -0
  167. package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +69 -0
  168. package/assets/skills/office-spreadsheets/recipes/data-tables.md +58 -0
  169. package/assets/skills/office-spreadsheets/recipes/named-ranges.md +41 -0
  170. package/assets/skills/office-spreadsheets/recipes/number-formats.md +72 -0
  171. package/assets/skills/office-spreadsheets/recipes/sparklines.md +43 -0
  172. package/assets/skills/office-spreadsheets/references/charts.md +63 -0
  173. package/assets/skills/office-spreadsheets/references/domain/financial-models.md +40 -0
  174. package/assets/skills/office-spreadsheets/references/domain/healthcare.md +22 -0
  175. package/assets/skills/office-spreadsheets/references/domain/marketing.md +28 -0
  176. package/assets/skills/office-spreadsheets/references/domain/research.md +22 -0
  177. package/assets/skills/office-spreadsheets/references/formula-patterns.md +78 -0
  178. package/assets/skills/office-spreadsheets/references/style-guidelines.md +69 -0
  179. package/assets/skills/office-spreadsheets/references/workbook-structure.md +60 -0
  180. package/assets/skills/office-spreadsheets/routing/google-sheets.md +36 -0
  181. package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +37 -0
  182. package/assets/skills/office-spreadsheets/tasks/annotations.md +51 -0
  183. package/assets/skills/office-spreadsheets/tasks/create-workbook.md +66 -0
  184. package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +38 -0
  185. package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +69 -0
  186. package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +38 -0
  187. package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +57 -0
  188. package/assets/skills/shared/annotation-recipes.md +252 -0
  189. package/assets/skills/shared/check_runtime.py +260 -0
  190. package/assets/skills/shared/common-workflow.md +139 -0
  191. package/assets/skills/shared/scripts/ensure_raster_image.py +187 -0
  192. package/dist/client.js +4 -4
  193. package/dist/client.js.map +1 -1
  194. package/dist/index.js +246 -89
  195. package/package.json +1 -1
  196. package/postinstall.mjs +92 -82
  197. package/python/office_cli.py +213 -254
package/dist/index.js CHANGED
@@ -1,13 +1,12 @@
1
- import { execFile } from "node:child_process";
2
- import { existsSync, readFileSync, statSync } from "node:fs";
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
2
  import { readFile } from "node:fs/promises";
4
- import { join } from "node:path";
3
+ import { dirname, join } from "node:path";
5
4
  import process from "node:process";
6
- import { promisify } from "node:util";
5
+ import { fileURLToPath } from "node:url";
7
6
  import { BUNDLED_SKILL_RANK } from "@deepseek-ai/dsh-skill";
8
7
  import { defineTool } from "@deepseek-ai/dsh-tools";
8
+ import { execFile } from "node:child_process";
9
9
  import { homedir } from "node:os";
10
- import { fileURLToPath } from "node:url";
11
10
  //#region src/features.ts
12
11
  /** The four entries, in menu order. */
13
12
  const OFFICE_FEATURES = [
@@ -91,19 +90,35 @@ const PYTHON_PACKAGES = [
91
90
  package: "mathml2omml"
92
91
  }
93
92
  ];
93
+ const PACKAGE_ROOT$1 = fileURLToPath(new URL("..", import.meta.url));
94
+ const POSTINSTALL_SCRIPT = join(PACKAGE_ROOT$1, "postinstall.mjs");
95
+ /** Upper bound for one self-heal run; the remedy command remains available afterwards. */
96
+ const POSTINSTALL_TIMEOUT = 540 * 1e3;
97
+ /** Failed probes are retried after this delay instead of re-running pip on every status call. */
98
+ const FAILURE_RETRY_DELAY = 300 * 1e3;
99
+ function configDir() {
100
+ const home = homedir();
101
+ return process.platform === "win32" ? join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "dsh-office-kit") : process.platform === "darwin" ? join(home, "Library", "Application Support", "dsh-office-kit") : join(process.env.XDG_CONFIG_HOME ?? join(home, ".config"), "dsh-office-kit");
102
+ }
103
+ function legacyDataDir() {
104
+ const home = homedir();
105
+ 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
+ }
94
107
  /** Resolve per-OS config/data directories and bundled resources. */
95
108
  function officePaths() {
96
- const packageRoot = fileURLToPath(new URL("..", import.meta.url));
97
- const home = homedir();
98
- const configDir = process.platform === "win32" ? join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "dsh-office-kit") : process.platform === "darwin" ? join(home, "Library", "Application Support", "dsh-office-kit") : join(process.env.XDG_CONFIG_HOME ?? join(home, ".config"), "dsh-office-kit");
99
- const dataDir = 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");
109
+ const runtimeDir = join(homedir(), ".dsh", "office", "runtime");
100
110
  return {
101
- configFile: join(configDir, "config.json"),
102
- dataDir,
103
- cli: join(packageRoot, "python", "office_cli.py"),
104
- skillsDir: join(packageRoot, "assets", "skills")
111
+ configFile: join(configDir(), "config.json"),
112
+ venv: join(runtimeDir, "venv"),
113
+ legacyVenv: join(legacyDataDir(), "venv"),
114
+ cli: join(PACKAGE_ROOT$1, "python", "office_cli.py"),
115
+ skillsDir: join(PACKAGE_ROOT$1, "assets", "skills")
105
116
  };
106
117
  }
118
+ /** One-liner the user can run to repair the runtime without pnpm. */
119
+ function runtimeRemedy() {
120
+ return `node "${POSTINSTALL_SCRIPT}"`;
121
+ }
107
122
  function isFile(path) {
108
123
  try {
109
124
  return statSync(path).isFile();
@@ -130,34 +145,68 @@ function run(file, args, timeout = 15e3) {
130
145
  }).stdin?.end();
131
146
  });
132
147
  }
133
- const VERSION_SCRIPT = "import json,sys;print(json.dumps({\"version\":sys.version.split()[0],\"executable\":sys.executable}))";
134
- /** Inspect an absolute interpreter without trusting its path alone. */
135
- async function inspectPython(path) {
136
- try {
137
- const raw = await run(path, ["-c", VERSION_SCRIPT]);
138
- const parsed = JSON.parse(raw);
139
- if (typeof parsed.version !== "string" || typeof parsed.executable !== "string") throw new Error("unexpected interpreter response");
140
- const [majorText, minorText] = parsed.version.split(".");
141
- const major = Number(majorText);
142
- const minor = Number(minorText);
143
- const compatible = major === 3 && minor >= 10 && minor < 14;
144
- return {
145
- path: parsed.executable,
146
- version: parsed.version,
147
- major,
148
- minor,
149
- compatible
150
- };
151
- } catch (error) {
152
- return {
153
- path,
154
- version: "",
155
- major: 0,
156
- minor: 0,
157
- compatible: false,
158
- error: error instanceof Error ? error.message : String(error)
159
- };
160
- }
148
+ const RUNTIME_SCRIPT = `
149
+ import importlib, json, sys
150
+ targets = ${JSON.stringify(PYTHON_PACKAGES)}
151
+ packages = {}
152
+ for item in targets:
153
+ try:
154
+ module = importlib.import_module(item["importName"])
155
+ packages[item["package"]] = {"installed": True, "version": str(getattr(module, "__version__", ""))}
156
+ except Exception as exc:
157
+ packages[item["package"]] = {"installed": False, "error": f"{type(exc).__name__}: {exc}"}
158
+ print(json.dumps({"python": sys.executable, "version": sys.version.split()[0], "packages": packages}))
159
+ `.trim();
160
+ /** Inspect an absolute interpreter: version plus every required import. */
161
+ function inspectPython(path) {
162
+ return run(path, ["-c", RUNTIME_SCRIPT], 6e4).then((raw) => {
163
+ try {
164
+ const parsed = JSON.parse(raw);
165
+ if (typeof parsed.version !== "string" || typeof parsed.python !== "string") throw new Error("unexpected interpreter response");
166
+ const [majorText, minorText] = parsed.version.split(".");
167
+ const major = Number(majorText);
168
+ const minor = Number(minorText);
169
+ const incompatible = major !== 3 || minor < 10 || minor >= 14;
170
+ const packages = PYTHON_PACKAGES.map(({ importName, package: name }) => {
171
+ const entry = parsed.packages?.[name];
172
+ return {
173
+ package: name,
174
+ importName,
175
+ installed: entry?.installed === true,
176
+ ...typeof entry?.version === "string" && entry.version.length > 0 ? { version: entry.version } : {},
177
+ ...typeof entry?.error === "string" && entry.error.length > 0 ? { error: entry.error } : {}
178
+ };
179
+ });
180
+ const missing = packages.filter((entry) => !entry.installed);
181
+ const inspection = {
182
+ path: parsed.python,
183
+ version: parsed.version,
184
+ major,
185
+ minor,
186
+ compatible: !incompatible,
187
+ packages,
188
+ missing: incompatible ? [] : missing
189
+ };
190
+ return incompatible ? {
191
+ ...inspection,
192
+ error: `unsupported Python ${parsed.version}`
193
+ } : inspection;
194
+ } catch (error) {
195
+ return failedInspection(path, error);
196
+ }
197
+ }, (error) => failedInspection(path, error));
198
+ }
199
+ function failedInspection(path, error) {
200
+ return {
201
+ path,
202
+ version: "",
203
+ major: 0,
204
+ minor: 0,
205
+ compatible: false,
206
+ packages: [],
207
+ missing: [],
208
+ error: error instanceof Error ? error.message : String(error)
209
+ };
161
210
  }
162
211
  /** Read the interpreter path postinstall recorded; malformed files are ignored. */
163
212
  function savedPython(configFile) {
@@ -169,14 +218,16 @@ function savedPython(configFile) {
169
218
  return;
170
219
  }
171
220
  }
172
- /** PATH-independent candidate list, most specific first. */
221
+ /** PATH-independent candidate list, plugin-owned interpreters first. */
173
222
  function candidates(configFile) {
223
+ const paths = officePaths();
174
224
  const found = [];
175
225
  const env = process.env.DSH_OFFICE_PYTHON;
176
226
  if (env != null && env.length > 0) found.push(env);
177
227
  const saved = savedPython(configFile);
178
228
  if (saved != null) found.push(saved);
179
- const marker = join(fileURLToPath(new URL("..", import.meta.url)), ".python-path");
229
+ found.push(paths.venv, paths.legacyVenv);
230
+ const marker = join(PACKAGE_ROOT$1, ".python-path");
180
231
  if (isFile(marker)) {
181
232
  const text = readFileSync(marker, "utf8").trim();
182
233
  if (text.length > 0) found.push(text);
@@ -220,16 +271,98 @@ async function launcherInterpreters() {
220
271
  return [];
221
272
  }
222
273
  }
223
- /** Find the first runnable, compatible interpreter; return the best attempt otherwise. */
224
- async function resolvePython(configFile = officePaths().configFile) {
274
+ function statusOf(python) {
275
+ return {
276
+ state: python.compatible && python.missing.length === 0 ? "ok" : "missing-dependencies",
277
+ python,
278
+ packages: python.packages,
279
+ missing: python.missing,
280
+ remedy: runtimeRemedy()
281
+ };
282
+ }
283
+ /** Inspect every candidate; return the first interpreter that proves it has all dependencies. */
284
+ async function inspectRuntime(configFile = officePaths().configFile) {
285
+ let compatible;
225
286
  let last;
226
287
  for (const candidate of [...candidates(configFile), ...await launcherInterpreters()]) for (const expanded of await expandLauncher(candidate)) {
227
288
  if (expanded !== candidate && !isFile(expanded)) continue;
228
289
  const inspection = await inspectPython(expanded);
229
- if (inspection.compatible) return inspection;
290
+ if (inspection.compatible && inspection.missing.length === 0) return statusOf(inspection);
291
+ if (inspection.compatible && compatible == null) compatible = inspection;
230
292
  last = inspection;
231
293
  }
232
- return last;
294
+ if (compatible != null) return statusOf(compatible);
295
+ return {
296
+ state: "no-interpreter",
297
+ packages: [],
298
+ missing: [],
299
+ remedy: runtimeRemedy(),
300
+ ...last?.error == null ? {} : { error: last.error }
301
+ };
302
+ }
303
+ /** Run postinstall.mjs as a child process; resolve with an error summary or undefined. */
304
+ function runPostinstall() {
305
+ return new Promise((resolve) => {
306
+ execFile(process.execPath, [POSTINSTALL_SCRIPT], {
307
+ timeout: POSTINSTALL_TIMEOUT,
308
+ windowsHide: true,
309
+ maxBuffer: 8 * 1024 * 1024,
310
+ env: {
311
+ ...process.env,
312
+ ELECTRON_RUN_AS_NODE: "1",
313
+ DSH_OFFICE_POSTINSTALL: "1"
314
+ }
315
+ }, (error, stdout, stderr) => {
316
+ if (error == null) {
317
+ resolve(void 0);
318
+ return;
319
+ }
320
+ const detail = String(stderr || stdout || error.message).trim().split(/\r?\n/u).slice(-6).join("\n");
321
+ resolve(detail.length > 0 ? detail : error.message);
322
+ });
323
+ });
324
+ }
325
+ let cache;
326
+ let healing;
327
+ /** A cached `ok` is only worth reusing while its interpreter still exists on disk. */
328
+ function runtimeAlive(status) {
329
+ return status.state !== "ok" || status.python != null && isFile(status.python.path);
330
+ }
331
+ /**
332
+ * Resolve a runtime that is proven to work, self-healing via postinstall.mjs when it
333
+ * is missing or incomplete. Never returns a bare interpreter without dependencies.
334
+ * Pass `force` for an explicit status check that must not trust the process cache.
335
+ */
336
+ function ensureRuntime(configFile = officePaths().configFile, force = false) {
337
+ if (!force && cache != null) {
338
+ if (cache.status.state === "ok" ? runtimeAlive(cache.status) : Date.now() - cache.at < FAILURE_RETRY_DELAY) return Promise.resolve(cache.status);
339
+ }
340
+ healing ??= (async () => {
341
+ const probed = await inspectRuntime(configFile);
342
+ if (probed.state === "ok") {
343
+ cache = {
344
+ status: probed,
345
+ at: Date.now()
346
+ };
347
+ return probed;
348
+ }
349
+ const healError = await runPostinstall();
350
+ const status = {
351
+ ...await inspectRuntime(configFile),
352
+ heal: {
353
+ attempted: true,
354
+ ...healError == null ? {} : { error: healError }
355
+ }
356
+ };
357
+ cache = {
358
+ status,
359
+ at: Date.now()
360
+ };
361
+ return status;
362
+ })().finally(() => {
363
+ healing = void 0;
364
+ });
365
+ return healing;
233
366
  }
234
367
  /** Whether the helper CLI file shipped with this package. */
235
368
  function cliExists() {
@@ -237,7 +370,6 @@ function cliExists() {
237
370
  }
238
371
  //#endregion
239
372
  //#region src/index.ts
240
- const execFileAsync = promisify(execFile);
241
373
  /** Services required by this host half. */
242
374
  const inject = ["skills", "tools"];
243
375
  /** Cordis plugin identity used in loader diagnostics. */
@@ -257,6 +389,26 @@ function findFile(candidates) {
257
389
  for (const path of candidates) if (existsSync(path)) return path;
258
390
  return null;
259
391
  }
392
+ const WPS_BINARIES = {
393
+ writer: "wps.exe",
394
+ presentation: "wpp.exe",
395
+ spreadsheet: "et.exe"
396
+ };
397
+ const WPS_ROOTS = [
398
+ process.env.WPS_OFFICE_HOME ?? "",
399
+ "C:\\Program Files\\Kingsoft\\WPS Office",
400
+ "C:\\Program Files (x86)\\Kingsoft\\WPS Office",
401
+ join(process.env.LOCALAPPDATA ?? "", "Kingsoft", "WPS Office")
402
+ ].filter(Boolean);
403
+ function wpsInstalled(binary) {
404
+ for (const root of WPS_ROOTS) {
405
+ if (existsSync(join(root, "office6", binary))) return true;
406
+ try {
407
+ for (const version of readdirSync(root)) if (existsSync(join(root, version, "office6", binary))) return true;
408
+ } catch {}
409
+ }
410
+ return false;
411
+ }
260
412
  function rendererFacts() {
261
413
  const libreoffice = findFile([
262
414
  process.env.SOFFICE ?? "",
@@ -273,36 +425,30 @@ function rendererFacts() {
273
425
  word: officeRoots.some((root) => existsSync(join(root, "WINWORD.EXE"))),
274
426
  powerpoint: officeRoots.some((root) => existsSync(join(root, "POWERPNT.EXE"))),
275
427
  excel: officeRoots.some((root) => existsSync(join(root, "EXCEL.EXE")))
428
+ },
429
+ kingsoftWps: {
430
+ writer: wpsInstalled(WPS_BINARIES.writer),
431
+ presentation: wpsInstalled(WPS_BINARIES.presentation),
432
+ spreadsheet: wpsInstalled(WPS_BINARIES.spreadsheet)
276
433
  }
277
434
  };
278
435
  }
279
- const PACKAGE_PROBE = `
280
- import importlib, json, sys
281
- targets = ${JSON.stringify(PYTHON_PACKAGES)}
282
- out = {}
283
- for item in targets:
284
- try:
285
- module = importlib.import_module(item["importName"])
286
- out[item["package"]] = {"installed": True, "version": str(getattr(module, "__version__", ""))}
287
- except Exception as exc:
288
- out[item["package"]] = {"installed": False, "error": f"{type(exc).__name__}: {exc}"}
289
- print(json.dumps({"python": sys.executable, "version": sys.version.split()[0], "packages": out}))
290
- `.trim();
291
- async function probePackages(python) {
436
+ const PACKAGE_ROOT = fileURLToPath(new URL("..", import.meta.url));
437
+ function pluginVersion() {
292
438
  try {
293
- const { stdout } = await execFileAsync(python, ["-c", PACKAGE_PROBE], {
294
- timeout: 3e4,
295
- windowsHide: true,
296
- maxBuffer: 2 * 1024 * 1024,
297
- env: {
298
- ...process.env,
299
- PYTHONIOENCODING: "utf-8"
300
- }
301
- });
302
- return JSON.parse(String(stdout).trim());
303
- } catch (error) {
304
- return { error: error instanceof Error ? error.message : String(error) };
439
+ const parsed = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
440
+ return typeof parsed.version === "string" ? parsed.version : "unknown";
441
+ } catch {
442
+ return "unknown";
443
+ }
444
+ }
445
+ /** Human-readable reason for a runtime that failed verification. */
446
+ function runtimeReason(runtime) {
447
+ if (runtime.state === "missing-dependencies") {
448
+ const names = runtime.missing.map((entry) => entry.package).join(", ");
449
+ return `Python ${runtime.python?.version ?? ""} is installed but missing packages: ${names}`.replace(" ", " ");
305
450
  }
451
+ return "no compatible Python 3.10–3.13 interpreter was found";
306
452
  }
307
453
  /** Register the four skills and a status tool, and expose the runtime paths in skill bodies. */
308
454
  function apply(ctx) {
@@ -350,12 +496,14 @@ function apply(ctx) {
350
496
  return;
351
497
  }
352
498
  const parsed = parseSkill(raw, path);
353
- const python = await resolvePython(paths.configFile);
354
- const pythonLine = python?.compatible === true ? `\n\n## Office runtime\n\nUse these verified absolute paths for every helper command. Quote paths that contain spaces.\n\n\`\`\`json\n${JSON.stringify({
355
- python: python.path,
499
+ const runtime = await ensureRuntime(paths.configFile);
500
+ const pythonLine = runtime.state === "ok" && runtime.python != null ? `\n\n## Office runtime\n\nUse these verified absolute paths for every helper command. Quote paths that contain spaces. \`skill\` is this skill's own directory: every script it names lives at \`<skill>/scripts/...\`; \`shared\` holds the cross-format workflow notes.\n\n\`\`\`json\n${JSON.stringify({
501
+ python: runtime.python.path,
356
502
  cli: paths.cli,
357
- version: python.version
358
- }, null, 2)}\n\`\`\`` : "\n\n## Office runtime\n\nNo compatible Python was found. Ask the user to run `pnpm rebuild dsh-office-kit` (or `npm rebuild dsh-office-kit`) and check the install output before attempting file work.";
503
+ skill: dirname(path).split("\\").join("/"),
504
+ shared: join(paths.skillsDir, "shared").split("\\").join("/"),
505
+ version: runtime.python.version
506
+ }, null, 2)}\n\`\`\`` : `\n\n## Office runtime\n\nNo working Python runtime is available: ${runtimeReason(runtime)}. Do not run helper commands or claim success. Tell the user to run \`${runtime.remedy}\`, then call \`office_status\` again.`;
359
507
  const { rank: _rank, locator: _locator, ...summary } = candidate;
360
508
  return {
361
509
  ...summary,
@@ -367,7 +515,7 @@ function apply(ctx) {
367
515
  ctx.effect(() => ctx.skills.registerProvider(() => provider), "office-suite: skills");
368
516
  ctx.effect(() => ctx.tools.register(defineTool({
369
517
  name: "office_status",
370
- description: "Report the Office runtime state: compatible Python, installed Python packages, helper CLI path, and document renderers (LibreOffice or Microsoft Office). Call this before Office file work.",
518
+ 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.",
371
519
  parameters: {},
372
520
  output: {
373
521
  schema: { type: "string" },
@@ -376,27 +524,36 @@ function apply(ctx) {
376
524
  text: value
377
525
  }]
378
526
  },
379
- timeoutMs: 45e3,
527
+ timeoutMs: 6e5,
380
528
  async execute() {
381
- const python = await resolvePython(paths.configFile);
382
- const seen = await (python?.compatible === true ? probePackages(python.path) : Promise.resolve(null));
529
+ const runtime = await ensureRuntime(paths.configFile, true);
383
530
  const report = {
384
531
  plugin: {
385
532
  name,
386
- version: "0.1.0"
533
+ version: pluginVersion()
534
+ },
535
+ runtime: {
536
+ state: runtime.state,
537
+ venv: paths.venv,
538
+ ...runtime.state === "ok" ? {} : {
539
+ reason: runtimeReason(runtime),
540
+ remedy: runtime.remedy
541
+ },
542
+ ...runtime.error == null ? {} : { error: runtime.error },
543
+ ...runtime.heal == null ? {} : { heal: runtime.heal }
387
544
  },
388
- python: python == null ? { found: false } : {
545
+ python: runtime.python == null ? { found: false } : {
389
546
  found: true,
390
- path: python.path,
391
- version: python.version,
392
- compatible: python.compatible,
393
- ...python.error == null ? {} : { error: python.error }
547
+ path: runtime.python.path,
548
+ version: runtime.python.version,
549
+ compatible: runtime.python.compatible,
550
+ ...runtime.python.error == null ? {} : { error: runtime.python.error }
394
551
  },
395
552
  cli: {
396
553
  path: paths.cli,
397
554
  exists: cliExists()
398
555
  },
399
- packages: seen,
556
+ packages: runtime.packages,
400
557
  renderers: rendererFacts(),
401
558
  skills: candidates.map((candidate) => candidate.name)
402
559
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-office-kit",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "0.3.1",
5
5
  "description": "Office documents, PDF, presentations, and spreadsheets for DeepSeek Harness",
6
6
  "author": "Hairy",
7
7
  "license": "MIT",