shortcutxl 0.2.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.
Files changed (423) hide show
  1. package/README.md +59 -0
  2. package/agent-docs/README.md +397 -0
  3. package/agent-docs/docs/compaction.md +390 -0
  4. package/agent-docs/docs/custom-provider.md +580 -0
  5. package/agent-docs/docs/development.md +69 -0
  6. package/agent-docs/docs/extensions.md +1971 -0
  7. package/agent-docs/docs/json.md +79 -0
  8. package/agent-docs/docs/keybindings.md +174 -0
  9. package/agent-docs/docs/models.md +293 -0
  10. package/agent-docs/docs/packages.md +209 -0
  11. package/agent-docs/docs/prompt-templates.md +67 -0
  12. package/agent-docs/docs/providers.md +186 -0
  13. package/agent-docs/docs/rpc.md +1317 -0
  14. package/agent-docs/docs/sdk.md +962 -0
  15. package/agent-docs/docs/session.md +412 -0
  16. package/agent-docs/docs/settings.md +223 -0
  17. package/agent-docs/docs/shell-aliases.md +13 -0
  18. package/agent-docs/docs/skills.md +231 -0
  19. package/agent-docs/docs/terminal-setup.md +70 -0
  20. package/agent-docs/docs/termux.md +127 -0
  21. package/agent-docs/docs/themes.md +295 -0
  22. package/agent-docs/docs/tree.md +219 -0
  23. package/agent-docs/docs/tui.md +887 -0
  24. package/agent-docs/docs/windows.md +17 -0
  25. package/agent-docs/examples/README.md +25 -0
  26. package/agent-docs/examples/extensions/README.md +205 -0
  27. package/agent-docs/examples/extensions/antigravity-image-gen.ts +447 -0
  28. package/agent-docs/examples/extensions/auto-commit-on-exit.ts +49 -0
  29. package/agent-docs/examples/extensions/bash-spawn-hook.ts +30 -0
  30. package/agent-docs/examples/extensions/bookmark.ts +50 -0
  31. package/agent-docs/examples/extensions/built-in-tool-renderer.ts +256 -0
  32. package/agent-docs/examples/extensions/claude-rules.ts +86 -0
  33. package/agent-docs/examples/extensions/commands.ts +75 -0
  34. package/agent-docs/examples/extensions/confirm-destructive.ts +59 -0
  35. package/agent-docs/examples/extensions/custom-compaction.ts +126 -0
  36. package/agent-docs/examples/extensions/custom-footer.ts +63 -0
  37. package/agent-docs/examples/extensions/custom-header.ts +73 -0
  38. package/agent-docs/examples/extensions/custom-provider-anthropic/index.ts +660 -0
  39. package/agent-docs/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
  40. package/agent-docs/examples/extensions/custom-provider-anthropic/package.json +19 -0
  41. package/agent-docs/examples/extensions/custom-provider-gitlab-duo/index.ts +362 -0
  42. package/agent-docs/examples/extensions/custom-provider-gitlab-duo/package.json +16 -0
  43. package/agent-docs/examples/extensions/custom-provider-gitlab-duo/test.ts +88 -0
  44. package/agent-docs/examples/extensions/custom-provider-qwen-cli/index.ts +349 -0
  45. package/agent-docs/examples/extensions/custom-provider-qwen-cli/package.json +16 -0
  46. package/agent-docs/examples/extensions/dirty-repo-guard.ts +56 -0
  47. package/agent-docs/examples/extensions/doom-overlay/README.md +46 -0
  48. package/agent-docs/examples/extensions/doom-overlay/doom/build.sh +152 -0
  49. package/agent-docs/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -0
  50. package/agent-docs/examples/extensions/doom-overlay/doom-component.ts +133 -0
  51. package/agent-docs/examples/extensions/doom-overlay/doom-engine.ts +186 -0
  52. package/agent-docs/examples/extensions/doom-overlay/doom-keys.ts +108 -0
  53. package/agent-docs/examples/extensions/doom-overlay/index.ts +74 -0
  54. package/agent-docs/examples/extensions/doom-overlay/wad-finder.ts +51 -0
  55. package/agent-docs/examples/extensions/dynamic-resources/SKILL.md +8 -0
  56. package/agent-docs/examples/extensions/dynamic-resources/dynamic.json +79 -0
  57. package/agent-docs/examples/extensions/dynamic-resources/dynamic.md +5 -0
  58. package/agent-docs/examples/extensions/dynamic-resources/index.ts +15 -0
  59. package/agent-docs/examples/extensions/dynamic-tools.ts +77 -0
  60. package/agent-docs/examples/extensions/event-bus.ts +43 -0
  61. package/agent-docs/examples/extensions/file-trigger.ts +41 -0
  62. package/agent-docs/examples/extensions/git-checkpoint.ts +53 -0
  63. package/agent-docs/examples/extensions/handoff.ts +155 -0
  64. package/agent-docs/examples/extensions/hello.ts +25 -0
  65. package/agent-docs/examples/extensions/inline-bash.ts +94 -0
  66. package/agent-docs/examples/extensions/input-transform.ts +43 -0
  67. package/agent-docs/examples/extensions/interactive-shell.ts +209 -0
  68. package/agent-docs/examples/extensions/mac-system-theme.ts +47 -0
  69. package/agent-docs/examples/extensions/message-renderer.ts +59 -0
  70. package/agent-docs/examples/extensions/minimal-mode.ts +430 -0
  71. package/agent-docs/examples/extensions/modal-editor.ts +90 -0
  72. package/agent-docs/examples/extensions/model-status.ts +31 -0
  73. package/agent-docs/examples/extensions/notify.ts +55 -0
  74. package/agent-docs/examples/extensions/overlay-qa-tests.ts +936 -0
  75. package/agent-docs/examples/extensions/overlay-test.ts +159 -0
  76. package/agent-docs/examples/extensions/permission-gate.ts +37 -0
  77. package/agent-docs/examples/extensions/pirate.ts +47 -0
  78. package/agent-docs/examples/extensions/plan-mode/README.md +65 -0
  79. package/agent-docs/examples/extensions/plan-mode/index.ts +363 -0
  80. package/agent-docs/examples/extensions/plan-mode/utils.ts +173 -0
  81. package/agent-docs/examples/extensions/preset.ts +418 -0
  82. package/agent-docs/examples/extensions/protected-paths.ts +30 -0
  83. package/agent-docs/examples/extensions/qna.ts +122 -0
  84. package/agent-docs/examples/extensions/question.ts +278 -0
  85. package/agent-docs/examples/extensions/questionnaire.ts +440 -0
  86. package/agent-docs/examples/extensions/rainbow-editor.ts +90 -0
  87. package/agent-docs/examples/extensions/reload-runtime.ts +37 -0
  88. package/agent-docs/examples/extensions/rpc-demo.ts +124 -0
  89. package/agent-docs/examples/extensions/sandbox/index.ts +324 -0
  90. package/agent-docs/examples/extensions/sandbox/package-lock.json +92 -0
  91. package/agent-docs/examples/extensions/sandbox/package.json +19 -0
  92. package/agent-docs/examples/extensions/send-user-message.ts +97 -0
  93. package/agent-docs/examples/extensions/session-name.ts +27 -0
  94. package/agent-docs/examples/extensions/shutdown-command.ts +69 -0
  95. package/agent-docs/examples/extensions/snake.ts +343 -0
  96. package/agent-docs/examples/extensions/space-invaders.ts +566 -0
  97. package/agent-docs/examples/extensions/ssh.ts +233 -0
  98. package/agent-docs/examples/extensions/status-line.ts +40 -0
  99. package/agent-docs/examples/extensions/subagent/README.md +172 -0
  100. package/agent-docs/examples/extensions/subagent/agents/planner.md +37 -0
  101. package/agent-docs/examples/extensions/subagent/agents/reviewer.md +35 -0
  102. package/agent-docs/examples/extensions/subagent/agents/scout.md +50 -0
  103. package/agent-docs/examples/extensions/subagent/agents/worker.md +24 -0
  104. package/agent-docs/examples/extensions/subagent/agents.ts +130 -0
  105. package/agent-docs/examples/extensions/subagent/index.ts +1068 -0
  106. package/agent-docs/examples/extensions/subagent/prompts/implement-and-review.md +10 -0
  107. package/agent-docs/examples/extensions/subagent/prompts/implement.md +10 -0
  108. package/agent-docs/examples/extensions/subagent/prompts/scout-and-plan.md +9 -0
  109. package/agent-docs/examples/extensions/summarize.ts +206 -0
  110. package/agent-docs/examples/extensions/system-prompt-header.ts +17 -0
  111. package/agent-docs/examples/extensions/timed-confirm.ts +72 -0
  112. package/agent-docs/examples/extensions/titlebar-spinner.ts +58 -0
  113. package/agent-docs/examples/extensions/todo.ts +314 -0
  114. package/agent-docs/examples/extensions/tool-override.ts +146 -0
  115. package/agent-docs/examples/extensions/tools.ts +145 -0
  116. package/agent-docs/examples/extensions/trigger-compact.ts +40 -0
  117. package/agent-docs/examples/extensions/truncated-tool.ts +194 -0
  118. package/agent-docs/examples/extensions/widget-placement.ts +17 -0
  119. package/agent-docs/examples/extensions/with-deps/index.ts +37 -0
  120. package/agent-docs/examples/extensions/with-deps/package-lock.json +31 -0
  121. package/agent-docs/examples/extensions/with-deps/package.json +22 -0
  122. package/agent-docs/examples/rpc-extension-ui.ts +654 -0
  123. package/agent-docs/examples/sdk/01-minimal.ts +22 -0
  124. package/agent-docs/examples/sdk/02-custom-model.ts +48 -0
  125. package/agent-docs/examples/sdk/03-custom-prompt.ts +55 -0
  126. package/agent-docs/examples/sdk/04-skills.ts +53 -0
  127. package/agent-docs/examples/sdk/05-tools.ts +56 -0
  128. package/agent-docs/examples/sdk/06-extensions.ts +88 -0
  129. package/agent-docs/examples/sdk/07-context-files.ts +40 -0
  130. package/agent-docs/examples/sdk/08-prompt-templates.ts +47 -0
  131. package/agent-docs/examples/sdk/09-api-keys-and-oauth.ts +48 -0
  132. package/agent-docs/examples/sdk/10-settings.ts +54 -0
  133. package/agent-docs/examples/sdk/11-sessions.ts +48 -0
  134. package/agent-docs/examples/sdk/12-full-control.ts +82 -0
  135. package/agent-docs/examples/sdk/README.md +144 -0
  136. package/agent-docs/xll-skill.md +61 -0
  137. package/agent-docs/xll-spec.md +110 -0
  138. package/dist/cli/args.js +290 -0
  139. package/dist/cli/config-selector.js +31 -0
  140. package/dist/cli/file-processor.js +79 -0
  141. package/dist/cli/list-models.js +92 -0
  142. package/dist/cli/package-commands.js +210 -0
  143. package/dist/cli/report-settings-errors.js +11 -0
  144. package/dist/cli/session-picker.js +34 -0
  145. package/dist/cli.js +19 -0
  146. package/dist/config.js +288 -0
  147. package/dist/core/abort.js +15 -0
  148. package/dist/core/agent-loop.js +352 -0
  149. package/dist/core/agent-session.js +2019 -0
  150. package/dist/core/agent.js +410 -0
  151. package/dist/core/auth-storage.js +456 -0
  152. package/dist/core/bash-executor.js +222 -0
  153. package/dist/core/compaction/branch-summarization.js +242 -0
  154. package/dist/core/compaction/compaction.js +610 -0
  155. package/dist/core/compaction/index.js +7 -0
  156. package/dist/core/compaction/utils.js +139 -0
  157. package/dist/core/defaults.js +6 -0
  158. package/dist/core/diagnostics.js +2 -0
  159. package/dist/core/event-bus.js +25 -0
  160. package/dist/core/exec.js +71 -0
  161. package/dist/core/export-html/ansi-to-html.js +256 -0
  162. package/dist/core/export-html/index.js +238 -0
  163. package/dist/core/export-html/session-view-model.js +342 -0
  164. package/dist/core/export-html/template.css +1110 -0
  165. package/dist/core/export-html/template.html +76 -0
  166. package/dist/core/export-html/template.js +1990 -0
  167. package/dist/core/export-html/tool-renderer.js +63 -0
  168. package/dist/core/export-html/vendor/highlight.min.js +7725 -0
  169. package/dist/core/export-html/vendor/marked.min.js +1803 -0
  170. package/dist/core/extensions/index.js +9 -0
  171. package/dist/core/extensions/loader.js +422 -0
  172. package/dist/core/extensions/runner.js +651 -0
  173. package/dist/core/extensions/types.js +35 -0
  174. package/dist/core/extensions/wrapper.js +102 -0
  175. package/dist/core/footer-data-provider.js +162 -0
  176. package/dist/core/index.js +9 -0
  177. package/dist/core/keybindings.js +153 -0
  178. package/dist/core/messages.js +133 -0
  179. package/dist/core/model-registry.js +539 -0
  180. package/dist/core/model-resolver.js +370 -0
  181. package/dist/core/package-manager.js +1485 -0
  182. package/dist/core/prompt-templates.js +253 -0
  183. package/dist/core/resolve-config-value.js +59 -0
  184. package/dist/core/resource-loader.js +700 -0
  185. package/dist/core/sdk.js +197 -0
  186. package/dist/core/session-bash.js +99 -0
  187. package/dist/core/session-compaction.js +165 -0
  188. package/dist/core/session-manager.js +1153 -0
  189. package/dist/core/session-models.js +99 -0
  190. package/dist/core/session-retry.js +155 -0
  191. package/dist/core/settings-manager.js +572 -0
  192. package/dist/core/skills.js +382 -0
  193. package/dist/core/slash-commands.js +31 -0
  194. package/dist/core/system-prompt.js +161 -0
  195. package/dist/core/theme.js +770 -0
  196. package/dist/core/timings.js +26 -0
  197. package/dist/core/tools/bash.js +258 -0
  198. package/dist/core/tools/edit-diff.js +245 -0
  199. package/dist/core/tools/edit.js +148 -0
  200. package/dist/core/tools/find.js +208 -0
  201. package/dist/core/tools/grep.js +246 -0
  202. package/dist/core/tools/index.js +67 -0
  203. package/dist/core/tools/ls.js +123 -0
  204. package/dist/core/tools/path-utils.js +81 -0
  205. package/dist/core/tools/read.js +160 -0
  206. package/dist/core/tools/truncate.js +70 -0
  207. package/dist/core/tools/write.js +82 -0
  208. package/dist/custom/agents/action.js +13 -0
  209. package/dist/custom/agents/document-reader.js +70 -0
  210. package/dist/custom/agents/general.js +26 -0
  211. package/dist/custom/agents/index.js +49 -0
  212. package/dist/custom/agents/installation.js +13 -0
  213. package/dist/custom/agents/types.js +7 -0
  214. package/dist/custom/auth/refresh-timer.js +33 -0
  215. package/dist/custom/auth/shortcut-oauth.js +145 -0
  216. package/dist/custom/constants.js +21 -0
  217. package/dist/custom/context/workbook-summary.js +73 -0
  218. package/dist/custom/credits/shortcut-credits.js +29 -0
  219. package/dist/custom/cron/cron-daemon-entry.js +18 -0
  220. package/dist/custom/cron/daemon-ipc.js +131 -0
  221. package/dist/custom/cron/daemon.js +224 -0
  222. package/dist/custom/cron/jobs.js +226 -0
  223. package/dist/custom/cron/run-log.js +51 -0
  224. package/dist/custom/cron/schedule.js +72 -0
  225. package/dist/custom/cron/status-line.js +98 -0
  226. package/dist/custom/cron/store.js +87 -0
  227. package/dist/custom/cron/types.js +8 -0
  228. package/dist/custom/dev/index.js +59 -0
  229. package/dist/custom/dev/trace-export.js +58 -0
  230. package/dist/custom/ensure-excel.js +63 -0
  231. package/dist/custom/excel-config.js +36 -0
  232. package/dist/custom/preflight.js +422 -0
  233. package/dist/custom/prompts/action.js +100 -0
  234. package/dist/custom/prompts/api.js +66 -0
  235. package/dist/custom/prompts/installation.js +124 -0
  236. package/dist/custom/prompts/shared.js +138 -0
  237. package/dist/custom/providers/llm-usage.js +42 -0
  238. package/dist/custom/providers/message-converter.js +74 -0
  239. package/dist/custom/providers/provider-ids.js +9 -0
  240. package/dist/custom/providers/register-openai-codex-provider.js +27 -0
  241. package/dist/custom/providers/register-shortcut-provider.js +52 -0
  242. package/dist/custom/providers/shortcut-invoke.js +117 -0
  243. package/dist/custom/providers/shortcut-stream.js +252 -0
  244. package/dist/custom/providers/sse-protocol.js +38 -0
  245. package/dist/custom/sync-xll.js +130 -0
  246. package/dist/custom/tools/cron.js +413 -0
  247. package/dist/custom/tools/excel-exec.js +167 -0
  248. package/dist/custom/tools/excel-range.js +50 -0
  249. package/dist/custom/tools/llm-analysis.js +265 -0
  250. package/dist/custom/tools/render-helpers.js +38 -0
  251. package/dist/custom/tools/switch-mode.js +94 -0
  252. package/dist/custom/tools/task/agents.js +6 -0
  253. package/dist/custom/tools/task/index.js +8 -0
  254. package/dist/custom/tools/task/render.js +348 -0
  255. package/dist/custom/tools/task/subprocess.js +320 -0
  256. package/dist/custom/tools/task/task.js +205 -0
  257. package/dist/custom/tools/todo-list.js +195 -0
  258. package/dist/custom/tracing/session-upload.js +93 -0
  259. package/dist/index.js +45 -0
  260. package/dist/main.js +613 -0
  261. package/dist/migrations.js +265 -0
  262. package/dist/modes/index.js +8 -0
  263. package/dist/modes/interactive/components/armin.js +337 -0
  264. package/dist/modes/interactive/components/assistant-message.js +94 -0
  265. package/dist/modes/interactive/components/bash-execution.js +171 -0
  266. package/dist/modes/interactive/components/bordered-loader.js +51 -0
  267. package/dist/modes/interactive/components/branch-summary-message.js +45 -0
  268. package/dist/modes/interactive/components/compaction-summary-message.js +46 -0
  269. package/dist/modes/interactive/components/config-selector.js +488 -0
  270. package/dist/modes/interactive/components/countdown-timer.js +33 -0
  271. package/dist/modes/interactive/components/custom-editor.js +93 -0
  272. package/dist/modes/interactive/components/custom-message.js +81 -0
  273. package/dist/modes/interactive/components/daxnuts.js +140 -0
  274. package/dist/modes/interactive/components/diff.js +133 -0
  275. package/dist/modes/interactive/components/dynamic-border.js +21 -0
  276. package/dist/modes/interactive/components/extension-editor.js +105 -0
  277. package/dist/modes/interactive/components/extension-input.js +61 -0
  278. package/dist/modes/interactive/components/extension-selector.js +78 -0
  279. package/dist/modes/interactive/components/footer.js +309 -0
  280. package/dist/modes/interactive/components/index.js +33 -0
  281. package/dist/modes/interactive/components/keybinding-hints.js +61 -0
  282. package/dist/modes/interactive/components/layout.js +64 -0
  283. package/dist/modes/interactive/components/login-dialog.js +148 -0
  284. package/dist/modes/interactive/components/model-selector.js +237 -0
  285. package/dist/modes/interactive/components/oauth-selector.js +111 -0
  286. package/dist/modes/interactive/components/session-selector-search.js +157 -0
  287. package/dist/modes/interactive/components/session-selector.js +860 -0
  288. package/dist/modes/interactive/components/settings-selector.js +123 -0
  289. package/dist/modes/interactive/components/show-images-selector.js +35 -0
  290. package/dist/modes/interactive/components/skill-invocation-message.js +48 -0
  291. package/dist/modes/interactive/components/theme-selector.js +47 -0
  292. package/dist/modes/interactive/components/thinking-selector.js +47 -0
  293. package/dist/modes/interactive/components/tool-execution.js +789 -0
  294. package/dist/modes/interactive/components/tool-group.js +106 -0
  295. package/dist/modes/interactive/components/tree-selector.js +962 -0
  296. package/dist/modes/interactive/components/user-message-selector.js +115 -0
  297. package/dist/modes/interactive/components/user-message.js +48 -0
  298. package/dist/modes/interactive/components/visual-truncate.js +33 -0
  299. package/dist/modes/interactive/file-attachments.js +135 -0
  300. package/dist/modes/interactive/interactive-mode.js +3775 -0
  301. package/dist/modes/interactive/theme/dark.json +85 -0
  302. package/dist/modes/interactive/theme/light.json +85 -0
  303. package/dist/modes/interactive/theme/theme-schema.json +335 -0
  304. package/dist/modes/interactive/theme/theme.js +177 -0
  305. package/dist/modes/print-mode.js +101 -0
  306. package/dist/modes/rpc/rpc-client.js +387 -0
  307. package/dist/modes/rpc/rpc-mode.js +509 -0
  308. package/dist/modes/rpc/rpc-types.js +8 -0
  309. package/dist/subagent-entry.js +145 -0
  310. package/dist/tool-names.js +34 -0
  311. package/dist/tui/autocomplete.js +596 -0
  312. package/dist/tui/components/box.js +104 -0
  313. package/dist/tui/components/cancellable-loader.js +35 -0
  314. package/dist/tui/components/editor.js +1679 -0
  315. package/dist/tui/components/image.js +69 -0
  316. package/dist/tui/components/input.js +433 -0
  317. package/dist/tui/components/loader.js +49 -0
  318. package/dist/tui/components/markdown.js +629 -0
  319. package/dist/tui/components/select-list.js +152 -0
  320. package/dist/tui/components/settings-list.js +185 -0
  321. package/dist/tui/components/spacer.js +23 -0
  322. package/dist/tui/components/text.js +89 -0
  323. package/dist/tui/components/truncated-text.js +51 -0
  324. package/dist/tui/editor-component.js +2 -0
  325. package/dist/tui/fuzzy.js +107 -0
  326. package/dist/tui/get-east-asian-width/index.js +32 -0
  327. package/dist/tui/get-east-asian-width/lookup.js +404 -0
  328. package/dist/tui/index.js +32 -0
  329. package/dist/tui/keybindings.js +114 -0
  330. package/dist/tui/keys.js +959 -0
  331. package/dist/tui/kill-ring.js +44 -0
  332. package/dist/tui/stdin-buffer.js +317 -0
  333. package/dist/tui/terminal-image.js +288 -0
  334. package/dist/tui/terminal.js +249 -0
  335. package/dist/tui/tui/autocomplete.js +596 -0
  336. package/dist/tui/tui/components/box.js +106 -0
  337. package/dist/tui/tui/components/cancellable-loader.js +35 -0
  338. package/dist/tui/tui/components/editor.js +1679 -0
  339. package/dist/tui/tui/components/image.js +69 -0
  340. package/dist/tui/tui/components/input.js +433 -0
  341. package/dist/tui/tui/components/loader.js +49 -0
  342. package/dist/tui/tui/components/markdown.js +629 -0
  343. package/dist/tui/tui/components/select-list.js +152 -0
  344. package/dist/tui/tui/components/settings-list.js +185 -0
  345. package/dist/tui/tui/components/spacer.js +23 -0
  346. package/dist/tui/tui/components/text.js +91 -0
  347. package/dist/tui/tui/components/truncated-text.js +51 -0
  348. package/dist/tui/tui/editor-component.js +2 -0
  349. package/dist/tui/tui/fuzzy.js +107 -0
  350. package/dist/tui/tui/get-east-asian-width/index.js +32 -0
  351. package/dist/tui/tui/get-east-asian-width/lookup.js +404 -0
  352. package/dist/tui/tui/index.js +32 -0
  353. package/dist/tui/tui/keybindings.js +114 -0
  354. package/dist/tui/tui/keys.js +959 -0
  355. package/dist/tui/tui/kill-ring.js +44 -0
  356. package/dist/tui/tui/stdin-buffer.js +317 -0
  357. package/dist/tui/tui/terminal-image.js +288 -0
  358. package/dist/tui/tui/terminal.js +249 -0
  359. package/dist/tui/tui/tui.js +955 -0
  360. package/dist/tui/tui/undo-stack.js +25 -0
  361. package/dist/tui/tui/utils.js +800 -0
  362. package/dist/tui/tui.js +955 -0
  363. package/dist/tui/undo-stack.js +25 -0
  364. package/dist/tui/utils.js +800 -0
  365. package/dist/utils/changelog.js +87 -0
  366. package/dist/utils/clipboard-image.js +164 -0
  367. package/dist/utils/clipboard-native.js +14 -0
  368. package/dist/utils/clipboard.js +67 -0
  369. package/dist/utils/frontmatter.js +26 -0
  370. package/dist/utils/git.js +166 -0
  371. package/dist/utils/image-convert.js +35 -0
  372. package/dist/utils/image-resize.js +183 -0
  373. package/dist/utils/mime.js +26 -0
  374. package/dist/utils/photon.js +121 -0
  375. package/dist/utils/shell.js +217 -0
  376. package/dist/utils/sleep.js +17 -0
  377. package/dist/utils/tools-manager.js +259 -0
  378. package/package.json +78 -0
  379. package/skills/excel-com-api/SKILL.md +74 -0
  380. package/skills/excel-com-api/excel-type-library.py +27767 -0
  381. package/skills/excel-com-api/office-type-library.py +10867 -0
  382. package/skills/integrations/SKILL.md +138 -0
  383. package/skills/integrations/alphasense.md +457 -0
  384. package/skills/integrations/bloomberg.md +803 -0
  385. package/skills/integrations/calcbench.md +315 -0
  386. package/skills/integrations/capiq.md +848 -0
  387. package/skills/integrations/dynamics-365-finance.md +354 -0
  388. package/skills/integrations/earnings_transcripts.md +387 -0
  389. package/skills/integrations/factset.md +758 -0
  390. package/skills/integrations/ice-fixed-income.md +344 -0
  391. package/skills/integrations/moodys-analytics.md +313 -0
  392. package/skills/integrations/morningstar.md +433 -0
  393. package/skills/integrations/nasdaq-data-link.md +249 -0
  394. package/skills/integrations/pitchbook.md +413 -0
  395. package/skills/integrations/preqin.md +422 -0
  396. package/skills/integrations/quickbooks.md +289 -0
  397. package/skills/integrations/quickfs.md +314 -0
  398. package/skills/integrations/refinitiv.md +473 -0
  399. package/skills/integrations/sage-intacct.md +401 -0
  400. package/skills/integrations/visible-alpha.md +320 -0
  401. package/skills/integrations/xero.md +393 -0
  402. package/skills/integrations/ycharts.md +306 -0
  403. package/skills/pdf-creation/SKILL.md +93 -0
  404. package/skills/pdf-extraction/SKILL.md +32 -0
  405. package/skills/powerpoint-creation/SKILL.md +110 -0
  406. package/skills/sec-edgar/SKILL.md +127 -0
  407. package/skills/sec-edgar/sec_to_pdf.py +109 -0
  408. package/xll/ShortcutXL.xll +0 -0
  409. package/xll/modules/debug_render.py +272 -0
  410. package/xll/modules/gameboy.py +241 -0
  411. package/xll/modules/pong.py +188 -0
  412. package/xll/modules/shortcut_xl/__init__.py +18 -0
  413. package/xll/modules/shortcut_xl/_categorize.py +200 -0
  414. package/xll/modules/shortcut_xl/_com.py +108 -0
  415. package/xll/modules/shortcut_xl/_format.py +252 -0
  416. package/xll/modules/shortcut_xl/_log.py +12 -0
  417. package/xll/modules/shortcut_xl/_managed.py +116 -0
  418. package/xll/modules/shortcut_xl/_registry.py +44 -0
  419. package/xll/modules/shortcut_xl/_threading.py +161 -0
  420. package/xll/modules/shortcut_xl/_tracking.py +283 -0
  421. package/xll/modules/stocks.py +100 -0
  422. package/xll/python3.dll +0 -0
  423. package/xll/python312.dll +0 -0
@@ -0,0 +1,252 @@
1
+ """Cell diff formatter — port of cell-diff-formatter.ts.
2
+
3
+ Produces a human-readable summary of dirty cells for the LLM,
4
+ with Good / Issues breakdown and row sampling.
5
+ """
6
+
7
+ import re
8
+ from ._categorize import (
9
+ categorize_cells,
10
+ GOOD, LARGE_PERCENTAGE, HARDCODED_NUMBER,
11
+ HARDCODED_NUMBER_IN_FORMULA, INVALID_FORMULA,
12
+ is_com_error, com_error_to_str,
13
+ )
14
+ from ._tracking import _col_to_letter
15
+
16
+ # Display constants
17
+ MAX_ROWS_TO_SHOW = 10
18
+ MAX_SAMPLES_PER_ROW = 5
19
+ DECIMAL_PLACES = 3
20
+ MAX_FORMAT_DECIMALS = 2
21
+ UNDEFINED_SYMBOL = '\u2205' # ∅
22
+ NO_CHANGES_MSG = 'No changes made.'
23
+
24
+ # (category_key, must_fix)
25
+ _PROBLEM_SECTIONS = (
26
+ (LARGE_PERCENTAGE, False),
27
+ (HARDCODED_NUMBER, False),
28
+ (HARDCODED_NUMBER_IN_FORMULA, False),
29
+ (INVALID_FORMULA, True),
30
+ )
31
+
32
+ # ---------- address parsing ----------
33
+
34
+ _ADDR_RE = re.compile(r'^(\$?)([A-Z]+)(\$?)(\d+)$', re.IGNORECASE)
35
+
36
+
37
+ def _parse_address(addr):
38
+ """Parse 'B12' -> (col_index_0based, row_0based)."""
39
+ m = _ADDR_RE.match(addr)
40
+ if not m:
41
+ return (0, 0)
42
+ letters = m.group(2).upper()
43
+ col = 0
44
+ for ch in letters:
45
+ col = col * 26 + (ord(ch) - 64)
46
+ return (col - 1, int(m.group(4)) - 1)
47
+
48
+
49
+ def _col_letter(col_0):
50
+ """0-based column index to letter(s). 0->A, 25->Z, 26->AA."""
51
+ return _col_to_letter(col_0 + 1)
52
+
53
+
54
+ # ---------- value formatting ----------
55
+
56
+ def format_value(value, number_format=None):
57
+ """Format a raw cell value for display."""
58
+ if value is None:
59
+ return UNDEFINED_SYMBOL
60
+ if isinstance(value, bool):
61
+ return 'TRUE' if value else 'FALSE'
62
+ if is_com_error(value):
63
+ return com_error_to_str(value)
64
+ if isinstance(value, str):
65
+ return 'EMPTY' if len(value) == 0 else value
66
+ # COM may return Decimal or other numeric types — coerce to float
67
+ if not isinstance(value, (int, float)):
68
+ try:
69
+ value = float(value)
70
+ except (TypeError, ValueError):
71
+ return str(value)
72
+
73
+ if not number_format:
74
+ return '0' if value == 0 else f'{value:.{DECIMAL_PLACES}f}'
75
+
76
+ fmt = number_format
77
+ is_pct = '%' in fmt
78
+ currency_m = re.search(r'[$\xa3\u20ac\xa5\u20b9\u20a9\u20aa\u20b1\u0e3f]|kr', fmt)
79
+ currency = currency_m.group(0) if currency_m else None
80
+ dec_m = re.search(r'\.([0#]+)', fmt)
81
+ decimals = min(len(dec_m.group(1)), MAX_FORMAT_DECIMALS) if dec_m else MAX_FORMAT_DECIMALS
82
+ use_parens = '(' in fmt
83
+
84
+ num = value * 100 if is_pct else value
85
+ abs_str = f'{abs(num):,.{decimals}f}'
86
+ neg = num < 0
87
+
88
+ if is_pct:
89
+ return f'({abs_str}%)' if neg and use_parens else f'{"-" if neg else ""}{abs_str}%'
90
+ if currency:
91
+ return (f'({currency}{abs_str})' if neg and use_parens
92
+ else f'{"-" if neg else ""}{currency}{abs_str}')
93
+ return f'({abs_str})' if neg and use_parens else f'{"-" if neg else ""}{abs_str}'
94
+
95
+
96
+ def _display_value(cell):
97
+ """Get display string for a cell — prefer displayValue, then format with numberFormat."""
98
+ dv = cell.get('displayValue')
99
+ if dv is not None:
100
+ return 'EMPTY' if dv == '' else dv
101
+ return format_value(cell.get('value'), cell.get('numberFormat'))
102
+
103
+
104
+ # ---------- sampling ----------
105
+
106
+ def _sample_first_last(items, max_n):
107
+ """Sample up to max_n items, always including first and last.
108
+
109
+ Uses evenly-spaced indices for deterministic output."""
110
+ n = len(items)
111
+ if n <= max_n:
112
+ return list(items)
113
+ if max_n <= 0:
114
+ return []
115
+ if max_n == 1:
116
+ return [items[0]]
117
+ # Evenly spaced indices including 0 and n-1
118
+ indices = [round(i * (n - 1) / (max_n - 1)) for i in range(max_n)]
119
+ return [items[i] for i in indices]
120
+
121
+
122
+ # ---------- filtering ----------
123
+
124
+ def _should_include(cell, issues):
125
+ """Include all changes except trivial None↔None (no actual change)."""
126
+ value = cell.get('value')
127
+ old = cell.get('oldValue')
128
+ if value is None and old is None:
129
+ return False
130
+ return True
131
+
132
+
133
+ # ---------- grouping ----------
134
+
135
+ def _group_by_sheet_row(entries):
136
+ """Group (cell, issues) entries by sheet -> row.
137
+ Returns {sheet: {row: [(cell, issues, col), ...]}}
138
+ """
139
+ grouped = {}
140
+ for cell, issues in entries:
141
+ if not _should_include(cell, issues):
142
+ continue
143
+ col, row = _parse_address(cell.get('address', 'A1'))
144
+ sheet = cell.get('sheet', 'Sheet1')
145
+ grouped.setdefault(sheet, {}).setdefault(row, []).append((cell, issues, col))
146
+ # Sort within each row by column
147
+ for sheet_rows in grouped.values():
148
+ for row_entries in sheet_rows.values():
149
+ row_entries.sort(key=lambda t: t[2])
150
+ return grouped
151
+
152
+
153
+ # ---------- row formatting ----------
154
+
155
+ def _format_row(sheet, row, entries, indent=2, show_issues=True, show_count=True):
156
+ """Format a single row with sampled cells."""
157
+ ind = ' ' * indent
158
+ sind = ' ' * (indent + 2)
159
+ lines = []
160
+
161
+ cols = [e[2] for e in entries]
162
+ min_c, max_c = min(cols), max(cols)
163
+ col_range = (_col_letter(min_c) if min_c == max_c
164
+ else f'{_col_letter(min_c)}-{_col_letter(max_c)}')
165
+
166
+ count_suffix = f': {len(entries)} cells' if show_count else ':'
167
+ lines.append(f'{ind}{sheet}!Row {row + 1} ({col_range}){count_suffix}')
168
+
169
+ samples = _sample_first_last(entries, MAX_SAMPLES_PER_ROW)
170
+ for cell, issues, _col in samples:
171
+ fmt = cell.get('numberFormat')
172
+ old_str = format_value(cell.get('oldValue'), fmt)
173
+ new_str = _display_value(cell)
174
+ # Show formula text as annotation when relevant (not category names —
175
+ # the section header already tells you the category)
176
+ ann = f' [{cell["formula"]}]' if show_issues and cell.get('formula') else ''
177
+ lines.append(f'{sind}\u2192 {cell.get("address", "?")}: {old_str} -> {new_str}{ann}')
178
+
179
+ if len(entries) > MAX_SAMPLES_PER_ROW:
180
+ lines.append(f'{sind}... and {len(entries) - MAX_SAMPLES_PER_ROW} more cells')
181
+
182
+ return lines
183
+
184
+
185
+ # ---------- section formatting ----------
186
+
187
+ def _format_section(entries, title, must_fix=False):
188
+ """Format a titled section of (cell, issues) entries."""
189
+ filtered = [(c, iss) for c, iss in entries if _should_include(c, iss)]
190
+ if not filtered:
191
+ return ''
192
+
193
+ prefix = 'MUST FIX: ' if must_fix else ''
194
+ lines = [f'{prefix}{title}: {len(filtered)} total cells']
195
+
196
+ grouped = _group_by_sheet_row(filtered)
197
+ # Flatten to sorted list of (sheet, row, entries)
198
+ all_rows = []
199
+ for sheet in sorted(grouped):
200
+ for row in sorted(grouped[sheet]):
201
+ all_rows.append((sheet, row, grouped[sheet][row]))
202
+
203
+ selected = _sample_first_last(all_rows, MAX_ROWS_TO_SHOW)
204
+ for sheet, row, row_entries in selected:
205
+ lines.extend(_format_row(sheet, row, row_entries))
206
+
207
+ if len(all_rows) > MAX_ROWS_TO_SHOW:
208
+ lines.append(f' ... and {len(all_rows) - MAX_ROWS_TO_SHOW} more rows')
209
+
210
+ return '\n'.join(lines)
211
+
212
+
213
+ # ---------- public API ----------
214
+
215
+ def format_cell_diff(dirty_cells):
216
+ """Format a list of dirty cell dicts into a human-readable summary.
217
+
218
+ This is the main entry point — call it with the raw dirty_cells list
219
+ from DirtyTracker.collect_changes(). It categorizes issues and produces
220
+ the same style of output as the Office.js client's cell-diff-formatter.
221
+
222
+ Returns a string ready to be printed to stdout for the LLM to see.
223
+ """
224
+ if not dirty_cells:
225
+ return NO_CHANGES_MSG
226
+
227
+ cats = categorize_cells(dirty_cells)
228
+
229
+ sections = [
230
+ '--- CELL DIFF SUMMARY ---',
231
+ f'({UNDEFINED_SYMBOL} = undefined/empty value)\n',
232
+ ]
233
+
234
+ # Good cells
235
+ if cats[GOOD]:
236
+ good_entries = [(c, []) for c in cats[GOOD]]
237
+ s = _format_section(good_entries, 'Changed without issues')
238
+ if s:
239
+ sections.append(s)
240
+
241
+ # Problem cells — driven by _PROBLEM_SECTIONS config
242
+ problem_parts = []
243
+ for cat_key, must_fix in _PROBLEM_SECTIONS:
244
+ if cats[cat_key]:
245
+ s = _format_section(cats[cat_key], cat_key, must_fix=must_fix)
246
+ if s:
247
+ problem_parts.append(s)
248
+
249
+ if problem_parts:
250
+ sections.append('Cells that need review:\n' + '\n\n'.join(problem_parts))
251
+
252
+ return '\n\n'.join(sections) if len(sections) > 2 else NO_CHANGES_MSG
@@ -0,0 +1,12 @@
1
+ """Logging utility for ShortcutXL."""
2
+
3
+ import os
4
+ import datetime
5
+
6
+
7
+ def xl_log(msg):
8
+ """Append a timestamped message to %TEMP%\\shortcutxl.log."""
9
+ log_path = os.path.join(os.environ.get("TEMP", "."), "shortcutxl.log")
10
+ timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
11
+ with open(log_path, "a", encoding="utf-8") as f:
12
+ f.write(f"[{timestamp}] {msg}\n")
@@ -0,0 +1,116 @@
1
+ """run_managed — execute a function against Excel on the main thread."""
2
+
3
+ import threading
4
+ from ._log import xl_log
5
+ from ._com import xl_app
6
+ from ._threading import _run_on_main
7
+ from ._tracking import DirtyTracker, TrackedApp
8
+
9
+ # Excel xlCalculation enum values
10
+ _XL_CALCULATION_MANUAL = -4135
11
+
12
+
13
+ def run_managed(fn):
14
+ """Run fn(app) on the main thread with Excel state managed.
15
+
16
+ Suspends Calculation/Events/Alerts, runs fn(app) with a tracking
17
+ proxy, collects dirty cells, then restores Excel state.
18
+
19
+ All COM calls happen directly on the main thread — no cross-apartment
20
+ marshaling overhead.
21
+
22
+ Returns the list of dirty cell dicts.
23
+
24
+ Usage::
25
+
26
+ def do_work(app):
27
+ app.ActiveSheet.Range("A1").Value = 42
28
+
29
+ dirty = run_managed(do_work)
30
+ """
31
+
32
+ def _work():
33
+ app = xl_app()
34
+ has_workbooks = app.Workbooks.Count > 0
35
+ orig_ev = app.EnableEvents
36
+ orig_da = app.DisplayAlerts
37
+ orig_calc = app.Calculation if has_workbooks else None
38
+ if has_workbooks:
39
+ app.Calculation = _XL_CALCULATION_MANUAL
40
+ app.EnableEvents = False
41
+ app.DisplayAlerts = False
42
+ tracker = DirtyTracker(app)
43
+ tracked = TrackedApp(app, tracker)
44
+ dirty = []
45
+ try:
46
+ fn(tracked)
47
+ finally:
48
+ # Collect changes BEFORE restoring calc so we only see
49
+ # direct writes, not formula recalculations.
50
+ try:
51
+ dirty.extend(tracker.collect_changes())
52
+ except Exception as e:
53
+ xl_log(f"run_managed: collect_changes failed: {e}")
54
+ # Each restoration is independent — don't let one failure
55
+ # prevent the others from being restored.
56
+ try:
57
+ app.DisplayAlerts = orig_da
58
+ except Exception:
59
+ pass
60
+ try:
61
+ app.EnableEvents = orig_ev
62
+ except Exception:
63
+ pass
64
+ try:
65
+ # If user's calc mode was manual, restoring it won't
66
+ # trigger a recalc — force one so dependents are fresh.
67
+ if orig_calc == _XL_CALCULATION_MANUAL and app.Workbooks.Count > 0:
68
+ app.Calculate()
69
+ except Exception:
70
+ pass
71
+ try:
72
+ # Restore original calc mode if we had workbooks,
73
+ # or reset to automatic if fn created the first workbook.
74
+ if orig_calc is not None:
75
+ app.Calculation = orig_calc
76
+ elif app.Workbooks.Count > 0:
77
+ # fn created workbooks — calc was never saved, reset
78
+ # to automatic so we don't leave it stuck on manual.
79
+ app.Calculation = -4105 # xlCalculationAutomatic
80
+ except Exception:
81
+ pass
82
+ return dirty
83
+
84
+ return _run_on_main(_work)
85
+
86
+
87
+ def xl_batch(fn):
88
+ """Run fn(app) on the main thread with ScreenUpdating disabled."""
89
+ def _batched():
90
+ import pythoncom
91
+ app = xl_app()
92
+ app.ScreenUpdating = False
93
+ try:
94
+ fn(app)
95
+ finally:
96
+ app.ScreenUpdating = True
97
+ pythoncom.PumpWaitingMessages()
98
+ _run_on_main(_batched)
99
+
100
+
101
+ def schedule_call(fn, delay_seconds=0.0):
102
+ """Schedule fn() to be called after delay_seconds on a background thread."""
103
+ import time
104
+
105
+ def _run():
106
+ if delay_seconds > 0:
107
+ time.sleep(delay_seconds)
108
+ try:
109
+ fn()
110
+ except Exception as e:
111
+ import traceback
112
+ xl_log(f"schedule_call error: {e}\n{traceback.format_exc()}")
113
+
114
+ t = threading.Thread(target=_run, daemon=True)
115
+ t.start()
116
+ return t
@@ -0,0 +1,44 @@
1
+ """@xl_func decorator — marks functions for registration as Excel UDFs.
2
+
3
+ The C runtime scans _registry at xlAutoOpen time to register functions with Excel.
4
+ """
5
+
6
+ import inspect
7
+
8
+ # Internal registry: list of (name, callable, n_args)
9
+ _registry = []
10
+
11
+
12
+ def xl_func(fn=None, *, name=None):
13
+ """Decorator — mark a function as an Excel UDF.
14
+
15
+ Usage:
16
+ @xl_func
17
+ def my_func(a, b):
18
+ return a + b
19
+
20
+ @xl_func(name="myCustomName")
21
+ def my_func(a, b):
22
+ return a + b
23
+ """
24
+ def _register(f):
25
+ excel_name = name if name else f.__name__
26
+ sig = inspect.signature(f)
27
+ n_args = len([
28
+ p for p in sig.parameters.values()
29
+ if p.kind in (
30
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
31
+ inspect.Parameter.POSITIONAL_ONLY,
32
+ )
33
+ ])
34
+ # Check if already registered (e.g. after a hot-reload)
35
+ for i, (n, _, _) in enumerate(_registry):
36
+ if n == excel_name:
37
+ _registry[i] = (excel_name, f, n_args)
38
+ return f
39
+ _registry.append((excel_name, f, n_args))
40
+ return f
41
+
42
+ if fn is not None:
43
+ return _register(fn)
44
+ return _register
@@ -0,0 +1,161 @@
1
+ """Main-thread work queue for ShortcutXL.
2
+
3
+ Python win32com cannot call Excel COM from background threads reliably
4
+ (GetActiveObject fails from MTA, Dispatch creates a new instance).
5
+ This matches the pattern used by PyXLL, Excel-DNA, and xlwings:
6
+ background threads enqueue work, a timer on the main thread drains it.
7
+ """
8
+
9
+ import threading
10
+ import queue
11
+ import ctypes
12
+ from ctypes import wintypes
13
+ from ._log import xl_log
14
+
15
+
16
+ # COM error code: Excel is busy (VBA_E_IGNORE = 0x800AC472)
17
+ _VBA_E_IGNORE = -2146777998
18
+
19
+ _MAX_ITEMS_PER_TICK = 64 # cap per timer tick to avoid starving Excel
20
+ _MAX_BUSY_RETRIES = 2400 # ~120s at 50ms intervals before giving up
21
+ _DRAIN_INTERVAL_MS = 50 # SetTimer interval for queue drain
22
+ _DEFAULT_TIMEOUT_S = 30.0 # max wait for main-thread dispatch + execution
23
+
24
+
25
+ class ExecutionTimeout(BaseException):
26
+ """Raised on the main thread when a caller's timeout expires mid-execution.
27
+
28
+ Inherits from BaseException (not Exception) so that user code's
29
+ ``except Exception:`` blocks cannot swallow it."""
30
+ pass
31
+
32
+
33
+ def _is_excel_busy(e):
34
+ """Check if a COM exception is VBA_E_IGNORE (Excel is busy)."""
35
+ if not hasattr(e, 'hresult'):
36
+ return False
37
+ if e.hresult == _VBA_E_IGNORE:
38
+ return True
39
+ ei = getattr(e, 'excepinfo', None)
40
+ if ei and len(ei) > 5 and ei[5] == _VBA_E_IGNORE:
41
+ return True
42
+ return False
43
+
44
+
45
+ # Persist main thread ID in builtins so it survives hot-reload.
46
+ import builtins
47
+ _main_thread_id = getattr(builtins, '_shortcutxl_main_thread_id', None)
48
+ if _main_thread_id is None:
49
+ _main_thread_id = threading.current_thread().ident
50
+ builtins._shortcutxl_main_thread_id = _main_thread_id
51
+
52
+ _work_queue = queue.Queue()
53
+ _draining = False
54
+
55
+
56
+ def _drain_queue():
57
+ """Drain pending work items. Runs on the main thread from the timer.
58
+
59
+ The reentrancy guard is essential: work items may call
60
+ PumpWaitingMessages(), which can dispatch another WM_TIMER and
61
+ re-enter this function. Without the guard the queue can be
62
+ drained out-of-order and done_events can be missed."""
63
+ global _draining
64
+ if _draining:
65
+ return
66
+ _draining = True
67
+ try:
68
+ for _ in range(_MAX_ITEMS_PER_TICK):
69
+ try:
70
+ fn, result_box, done_event = _work_queue.get_nowait()
71
+ except queue.Empty:
72
+ break
73
+ if result_box.get('_cancelled'):
74
+ continue
75
+ result_box['_running'] = True
76
+ try:
77
+ result_box['result'] = fn()
78
+ result_box['error'] = None
79
+ done_event.set()
80
+ except ExecutionTimeout:
81
+ result_box['error'] = RuntimeError(
82
+ "Execution interrupted: caller timed out")
83
+ done_event.set()
84
+ except Exception as e:
85
+ if _is_excel_busy(e):
86
+ result_box['_retries'] = result_box.get('_retries', 0) + 1
87
+ if result_box['_retries'] < _MAX_BUSY_RETRIES:
88
+ _work_queue.put((fn, result_box, done_event))
89
+ break
90
+ xl_log(f"_drain_queue: Excel busy exceeded {_MAX_BUSY_RETRIES} retries")
91
+ result_box['error'] = e
92
+ done_event.set()
93
+ finally:
94
+ _draining = False
95
+
96
+
97
+ # SetTimer callback — called by Windows on the main thread during message pump
98
+ _TIMERPROC = ctypes.WINFUNCTYPE(
99
+ None, wintypes.HWND, ctypes.c_uint, ctypes.c_size_t, wintypes.DWORD)
100
+
101
+
102
+ def _on_timer(hwnd, msg, id_event, dw_time):
103
+ _drain_queue()
104
+
105
+
106
+ # prevent GC of the callback (prevents crash when timer fires)
107
+ _timer_proc_ref = _TIMERPROC(_on_timer)
108
+
109
+
110
+ def _start_drain_timer():
111
+ """Create a 50ms timer on the main thread. Safe to call on hot-reload."""
112
+ try:
113
+ old_id = getattr(builtins, '_shortcutxl_timer_id', None)
114
+ if old_id:
115
+ ctypes.windll.user32.KillTimer(None, ctypes.c_size_t(old_id))
116
+ timer_id = ctypes.windll.user32.SetTimer(
117
+ None, ctypes.c_size_t(0), _DRAIN_INTERVAL_MS, _timer_proc_ref)
118
+ builtins._shortcutxl_timer_id = timer_id
119
+ xl_log(f"_start_drain_timer: timer_id={timer_id}")
120
+ except Exception as e:
121
+ xl_log(f"_start_drain_timer: FAILED — {e}")
122
+ import traceback
123
+ xl_log(traceback.format_exc())
124
+
125
+
126
+ # Only start the drain timer on the main thread (xlAutoOpen).
127
+ if threading.current_thread().ident == _main_thread_id:
128
+ _start_drain_timer()
129
+
130
+
131
+ def _run_on_main(fn, timeout=_DEFAULT_TIMEOUT_S):
132
+ """Run fn() on the main thread and return its result. Blocks the caller.
133
+ If already on the main thread, calls fn() directly."""
134
+ if threading.current_thread().ident == _main_thread_id:
135
+ return fn()
136
+ result_box = {'_retries': 0}
137
+ done_event = threading.Event()
138
+ _work_queue.put((fn, result_box, done_event))
139
+ if not done_event.wait(timeout=timeout):
140
+ if result_box.get('_running'):
141
+ # fn is mid-execution on the main thread — inject an async
142
+ # exception to interrupt it. This fires at the next Python
143
+ # bytecode boundary (won't interrupt a blocking C call, but
144
+ # will fire between COM calls / sleep increments).
145
+ ctypes.pythonapi.PyThreadState_SetAsyncExc(
146
+ ctypes.c_ulong(_main_thread_id),
147
+ ctypes.py_object(ExecutionTimeout))
148
+ # Race guard: fn may have completed between the timeout and
149
+ # the injection. If done_event is already set, the exception
150
+ # would land on the NEXT work item — clear it immediately.
151
+ if done_event.is_set():
152
+ ctypes.pythonapi.PyThreadState_SetAsyncExc(
153
+ ctypes.c_ulong(_main_thread_id), None)
154
+ elif not done_event.wait(timeout=5.0):
155
+ xl_log("_run_on_main: fn did not stop after async exception")
156
+ else:
157
+ result_box['_cancelled'] = True
158
+ raise RuntimeError("_run_on_main: timed out waiting for main thread")
159
+ if result_box.get('error') is not None:
160
+ raise result_box['error']
161
+ return result_box.get('result')