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.
- package/README.md +59 -0
- package/agent-docs/README.md +397 -0
- package/agent-docs/docs/compaction.md +390 -0
- package/agent-docs/docs/custom-provider.md +580 -0
- package/agent-docs/docs/development.md +69 -0
- package/agent-docs/docs/extensions.md +1971 -0
- package/agent-docs/docs/json.md +79 -0
- package/agent-docs/docs/keybindings.md +174 -0
- package/agent-docs/docs/models.md +293 -0
- package/agent-docs/docs/packages.md +209 -0
- package/agent-docs/docs/prompt-templates.md +67 -0
- package/agent-docs/docs/providers.md +186 -0
- package/agent-docs/docs/rpc.md +1317 -0
- package/agent-docs/docs/sdk.md +962 -0
- package/agent-docs/docs/session.md +412 -0
- package/agent-docs/docs/settings.md +223 -0
- package/agent-docs/docs/shell-aliases.md +13 -0
- package/agent-docs/docs/skills.md +231 -0
- package/agent-docs/docs/terminal-setup.md +70 -0
- package/agent-docs/docs/termux.md +127 -0
- package/agent-docs/docs/themes.md +295 -0
- package/agent-docs/docs/tree.md +219 -0
- package/agent-docs/docs/tui.md +887 -0
- package/agent-docs/docs/windows.md +17 -0
- package/agent-docs/examples/README.md +25 -0
- package/agent-docs/examples/extensions/README.md +205 -0
- package/agent-docs/examples/extensions/antigravity-image-gen.ts +447 -0
- package/agent-docs/examples/extensions/auto-commit-on-exit.ts +49 -0
- package/agent-docs/examples/extensions/bash-spawn-hook.ts +30 -0
- package/agent-docs/examples/extensions/bookmark.ts +50 -0
- package/agent-docs/examples/extensions/built-in-tool-renderer.ts +256 -0
- package/agent-docs/examples/extensions/claude-rules.ts +86 -0
- package/agent-docs/examples/extensions/commands.ts +75 -0
- package/agent-docs/examples/extensions/confirm-destructive.ts +59 -0
- package/agent-docs/examples/extensions/custom-compaction.ts +126 -0
- package/agent-docs/examples/extensions/custom-footer.ts +63 -0
- package/agent-docs/examples/extensions/custom-header.ts +73 -0
- package/agent-docs/examples/extensions/custom-provider-anthropic/index.ts +660 -0
- package/agent-docs/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
- package/agent-docs/examples/extensions/custom-provider-anthropic/package.json +19 -0
- package/agent-docs/examples/extensions/custom-provider-gitlab-duo/index.ts +362 -0
- package/agent-docs/examples/extensions/custom-provider-gitlab-duo/package.json +16 -0
- package/agent-docs/examples/extensions/custom-provider-gitlab-duo/test.ts +88 -0
- package/agent-docs/examples/extensions/custom-provider-qwen-cli/index.ts +349 -0
- package/agent-docs/examples/extensions/custom-provider-qwen-cli/package.json +16 -0
- package/agent-docs/examples/extensions/dirty-repo-guard.ts +56 -0
- package/agent-docs/examples/extensions/doom-overlay/README.md +46 -0
- package/agent-docs/examples/extensions/doom-overlay/doom/build.sh +152 -0
- package/agent-docs/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -0
- package/agent-docs/examples/extensions/doom-overlay/doom-component.ts +133 -0
- package/agent-docs/examples/extensions/doom-overlay/doom-engine.ts +186 -0
- package/agent-docs/examples/extensions/doom-overlay/doom-keys.ts +108 -0
- package/agent-docs/examples/extensions/doom-overlay/index.ts +74 -0
- package/agent-docs/examples/extensions/doom-overlay/wad-finder.ts +51 -0
- package/agent-docs/examples/extensions/dynamic-resources/SKILL.md +8 -0
- package/agent-docs/examples/extensions/dynamic-resources/dynamic.json +79 -0
- package/agent-docs/examples/extensions/dynamic-resources/dynamic.md +5 -0
- package/agent-docs/examples/extensions/dynamic-resources/index.ts +15 -0
- package/agent-docs/examples/extensions/dynamic-tools.ts +77 -0
- package/agent-docs/examples/extensions/event-bus.ts +43 -0
- package/agent-docs/examples/extensions/file-trigger.ts +41 -0
- package/agent-docs/examples/extensions/git-checkpoint.ts +53 -0
- package/agent-docs/examples/extensions/handoff.ts +155 -0
- package/agent-docs/examples/extensions/hello.ts +25 -0
- package/agent-docs/examples/extensions/inline-bash.ts +94 -0
- package/agent-docs/examples/extensions/input-transform.ts +43 -0
- package/agent-docs/examples/extensions/interactive-shell.ts +209 -0
- package/agent-docs/examples/extensions/mac-system-theme.ts +47 -0
- package/agent-docs/examples/extensions/message-renderer.ts +59 -0
- package/agent-docs/examples/extensions/minimal-mode.ts +430 -0
- package/agent-docs/examples/extensions/modal-editor.ts +90 -0
- package/agent-docs/examples/extensions/model-status.ts +31 -0
- package/agent-docs/examples/extensions/notify.ts +55 -0
- package/agent-docs/examples/extensions/overlay-qa-tests.ts +936 -0
- package/agent-docs/examples/extensions/overlay-test.ts +159 -0
- package/agent-docs/examples/extensions/permission-gate.ts +37 -0
- package/agent-docs/examples/extensions/pirate.ts +47 -0
- package/agent-docs/examples/extensions/plan-mode/README.md +65 -0
- package/agent-docs/examples/extensions/plan-mode/index.ts +363 -0
- package/agent-docs/examples/extensions/plan-mode/utils.ts +173 -0
- package/agent-docs/examples/extensions/preset.ts +418 -0
- package/agent-docs/examples/extensions/protected-paths.ts +30 -0
- package/agent-docs/examples/extensions/qna.ts +122 -0
- package/agent-docs/examples/extensions/question.ts +278 -0
- package/agent-docs/examples/extensions/questionnaire.ts +440 -0
- package/agent-docs/examples/extensions/rainbow-editor.ts +90 -0
- package/agent-docs/examples/extensions/reload-runtime.ts +37 -0
- package/agent-docs/examples/extensions/rpc-demo.ts +124 -0
- package/agent-docs/examples/extensions/sandbox/index.ts +324 -0
- package/agent-docs/examples/extensions/sandbox/package-lock.json +92 -0
- package/agent-docs/examples/extensions/sandbox/package.json +19 -0
- package/agent-docs/examples/extensions/send-user-message.ts +97 -0
- package/agent-docs/examples/extensions/session-name.ts +27 -0
- package/agent-docs/examples/extensions/shutdown-command.ts +69 -0
- package/agent-docs/examples/extensions/snake.ts +343 -0
- package/agent-docs/examples/extensions/space-invaders.ts +566 -0
- package/agent-docs/examples/extensions/ssh.ts +233 -0
- package/agent-docs/examples/extensions/status-line.ts +40 -0
- package/agent-docs/examples/extensions/subagent/README.md +172 -0
- package/agent-docs/examples/extensions/subagent/agents/planner.md +37 -0
- package/agent-docs/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/agent-docs/examples/extensions/subagent/agents/scout.md +50 -0
- package/agent-docs/examples/extensions/subagent/agents/worker.md +24 -0
- package/agent-docs/examples/extensions/subagent/agents.ts +130 -0
- package/agent-docs/examples/extensions/subagent/index.ts +1068 -0
- package/agent-docs/examples/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/agent-docs/examples/extensions/subagent/prompts/implement.md +10 -0
- package/agent-docs/examples/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/agent-docs/examples/extensions/summarize.ts +206 -0
- package/agent-docs/examples/extensions/system-prompt-header.ts +17 -0
- package/agent-docs/examples/extensions/timed-confirm.ts +72 -0
- package/agent-docs/examples/extensions/titlebar-spinner.ts +58 -0
- package/agent-docs/examples/extensions/todo.ts +314 -0
- package/agent-docs/examples/extensions/tool-override.ts +146 -0
- package/agent-docs/examples/extensions/tools.ts +145 -0
- package/agent-docs/examples/extensions/trigger-compact.ts +40 -0
- package/agent-docs/examples/extensions/truncated-tool.ts +194 -0
- package/agent-docs/examples/extensions/widget-placement.ts +17 -0
- package/agent-docs/examples/extensions/with-deps/index.ts +37 -0
- package/agent-docs/examples/extensions/with-deps/package-lock.json +31 -0
- package/agent-docs/examples/extensions/with-deps/package.json +22 -0
- package/agent-docs/examples/rpc-extension-ui.ts +654 -0
- package/agent-docs/examples/sdk/01-minimal.ts +22 -0
- package/agent-docs/examples/sdk/02-custom-model.ts +48 -0
- package/agent-docs/examples/sdk/03-custom-prompt.ts +55 -0
- package/agent-docs/examples/sdk/04-skills.ts +53 -0
- package/agent-docs/examples/sdk/05-tools.ts +56 -0
- package/agent-docs/examples/sdk/06-extensions.ts +88 -0
- package/agent-docs/examples/sdk/07-context-files.ts +40 -0
- package/agent-docs/examples/sdk/08-prompt-templates.ts +47 -0
- package/agent-docs/examples/sdk/09-api-keys-and-oauth.ts +48 -0
- package/agent-docs/examples/sdk/10-settings.ts +54 -0
- package/agent-docs/examples/sdk/11-sessions.ts +48 -0
- package/agent-docs/examples/sdk/12-full-control.ts +82 -0
- package/agent-docs/examples/sdk/README.md +144 -0
- package/agent-docs/xll-skill.md +61 -0
- package/agent-docs/xll-spec.md +110 -0
- package/dist/cli/args.js +290 -0
- package/dist/cli/config-selector.js +31 -0
- package/dist/cli/file-processor.js +79 -0
- package/dist/cli/list-models.js +92 -0
- package/dist/cli/package-commands.js +210 -0
- package/dist/cli/report-settings-errors.js +11 -0
- package/dist/cli/session-picker.js +34 -0
- package/dist/cli.js +19 -0
- package/dist/config.js +288 -0
- package/dist/core/abort.js +15 -0
- package/dist/core/agent-loop.js +352 -0
- package/dist/core/agent-session.js +2019 -0
- package/dist/core/agent.js +410 -0
- package/dist/core/auth-storage.js +456 -0
- package/dist/core/bash-executor.js +222 -0
- package/dist/core/compaction/branch-summarization.js +242 -0
- package/dist/core/compaction/compaction.js +610 -0
- package/dist/core/compaction/index.js +7 -0
- package/dist/core/compaction/utils.js +139 -0
- package/dist/core/defaults.js +6 -0
- package/dist/core/diagnostics.js +2 -0
- package/dist/core/event-bus.js +25 -0
- package/dist/core/exec.js +71 -0
- package/dist/core/export-html/ansi-to-html.js +256 -0
- package/dist/core/export-html/index.js +238 -0
- package/dist/core/export-html/session-view-model.js +342 -0
- package/dist/core/export-html/template.css +1110 -0
- package/dist/core/export-html/template.html +76 -0
- package/dist/core/export-html/template.js +1990 -0
- package/dist/core/export-html/tool-renderer.js +63 -0
- package/dist/core/export-html/vendor/highlight.min.js +7725 -0
- package/dist/core/export-html/vendor/marked.min.js +1803 -0
- package/dist/core/extensions/index.js +9 -0
- package/dist/core/extensions/loader.js +422 -0
- package/dist/core/extensions/runner.js +651 -0
- package/dist/core/extensions/types.js +35 -0
- package/dist/core/extensions/wrapper.js +102 -0
- package/dist/core/footer-data-provider.js +162 -0
- package/dist/core/index.js +9 -0
- package/dist/core/keybindings.js +153 -0
- package/dist/core/messages.js +133 -0
- package/dist/core/model-registry.js +539 -0
- package/dist/core/model-resolver.js +370 -0
- package/dist/core/package-manager.js +1485 -0
- package/dist/core/prompt-templates.js +253 -0
- package/dist/core/resolve-config-value.js +59 -0
- package/dist/core/resource-loader.js +700 -0
- package/dist/core/sdk.js +197 -0
- package/dist/core/session-bash.js +99 -0
- package/dist/core/session-compaction.js +165 -0
- package/dist/core/session-manager.js +1153 -0
- package/dist/core/session-models.js +99 -0
- package/dist/core/session-retry.js +155 -0
- package/dist/core/settings-manager.js +572 -0
- package/dist/core/skills.js +382 -0
- package/dist/core/slash-commands.js +31 -0
- package/dist/core/system-prompt.js +161 -0
- package/dist/core/theme.js +770 -0
- package/dist/core/timings.js +26 -0
- package/dist/core/tools/bash.js +258 -0
- package/dist/core/tools/edit-diff.js +245 -0
- package/dist/core/tools/edit.js +148 -0
- package/dist/core/tools/find.js +208 -0
- package/dist/core/tools/grep.js +246 -0
- package/dist/core/tools/index.js +67 -0
- package/dist/core/tools/ls.js +123 -0
- package/dist/core/tools/path-utils.js +81 -0
- package/dist/core/tools/read.js +160 -0
- package/dist/core/tools/truncate.js +70 -0
- package/dist/core/tools/write.js +82 -0
- package/dist/custom/agents/action.js +13 -0
- package/dist/custom/agents/document-reader.js +70 -0
- package/dist/custom/agents/general.js +26 -0
- package/dist/custom/agents/index.js +49 -0
- package/dist/custom/agents/installation.js +13 -0
- package/dist/custom/agents/types.js +7 -0
- package/dist/custom/auth/refresh-timer.js +33 -0
- package/dist/custom/auth/shortcut-oauth.js +145 -0
- package/dist/custom/constants.js +21 -0
- package/dist/custom/context/workbook-summary.js +73 -0
- package/dist/custom/credits/shortcut-credits.js +29 -0
- package/dist/custom/cron/cron-daemon-entry.js +18 -0
- package/dist/custom/cron/daemon-ipc.js +131 -0
- package/dist/custom/cron/daemon.js +224 -0
- package/dist/custom/cron/jobs.js +226 -0
- package/dist/custom/cron/run-log.js +51 -0
- package/dist/custom/cron/schedule.js +72 -0
- package/dist/custom/cron/status-line.js +98 -0
- package/dist/custom/cron/store.js +87 -0
- package/dist/custom/cron/types.js +8 -0
- package/dist/custom/dev/index.js +59 -0
- package/dist/custom/dev/trace-export.js +58 -0
- package/dist/custom/ensure-excel.js +63 -0
- package/dist/custom/excel-config.js +36 -0
- package/dist/custom/preflight.js +422 -0
- package/dist/custom/prompts/action.js +100 -0
- package/dist/custom/prompts/api.js +66 -0
- package/dist/custom/prompts/installation.js +124 -0
- package/dist/custom/prompts/shared.js +138 -0
- package/dist/custom/providers/llm-usage.js +42 -0
- package/dist/custom/providers/message-converter.js +74 -0
- package/dist/custom/providers/provider-ids.js +9 -0
- package/dist/custom/providers/register-openai-codex-provider.js +27 -0
- package/dist/custom/providers/register-shortcut-provider.js +52 -0
- package/dist/custom/providers/shortcut-invoke.js +117 -0
- package/dist/custom/providers/shortcut-stream.js +252 -0
- package/dist/custom/providers/sse-protocol.js +38 -0
- package/dist/custom/sync-xll.js +130 -0
- package/dist/custom/tools/cron.js +413 -0
- package/dist/custom/tools/excel-exec.js +167 -0
- package/dist/custom/tools/excel-range.js +50 -0
- package/dist/custom/tools/llm-analysis.js +265 -0
- package/dist/custom/tools/render-helpers.js +38 -0
- package/dist/custom/tools/switch-mode.js +94 -0
- package/dist/custom/tools/task/agents.js +6 -0
- package/dist/custom/tools/task/index.js +8 -0
- package/dist/custom/tools/task/render.js +348 -0
- package/dist/custom/tools/task/subprocess.js +320 -0
- package/dist/custom/tools/task/task.js +205 -0
- package/dist/custom/tools/todo-list.js +195 -0
- package/dist/custom/tracing/session-upload.js +93 -0
- package/dist/index.js +45 -0
- package/dist/main.js +613 -0
- package/dist/migrations.js +265 -0
- package/dist/modes/index.js +8 -0
- package/dist/modes/interactive/components/armin.js +337 -0
- package/dist/modes/interactive/components/assistant-message.js +94 -0
- package/dist/modes/interactive/components/bash-execution.js +171 -0
- package/dist/modes/interactive/components/bordered-loader.js +51 -0
- package/dist/modes/interactive/components/branch-summary-message.js +45 -0
- package/dist/modes/interactive/components/compaction-summary-message.js +46 -0
- package/dist/modes/interactive/components/config-selector.js +488 -0
- package/dist/modes/interactive/components/countdown-timer.js +33 -0
- package/dist/modes/interactive/components/custom-editor.js +93 -0
- package/dist/modes/interactive/components/custom-message.js +81 -0
- package/dist/modes/interactive/components/daxnuts.js +140 -0
- package/dist/modes/interactive/components/diff.js +133 -0
- package/dist/modes/interactive/components/dynamic-border.js +21 -0
- package/dist/modes/interactive/components/extension-editor.js +105 -0
- package/dist/modes/interactive/components/extension-input.js +61 -0
- package/dist/modes/interactive/components/extension-selector.js +78 -0
- package/dist/modes/interactive/components/footer.js +309 -0
- package/dist/modes/interactive/components/index.js +33 -0
- package/dist/modes/interactive/components/keybinding-hints.js +61 -0
- package/dist/modes/interactive/components/layout.js +64 -0
- package/dist/modes/interactive/components/login-dialog.js +148 -0
- package/dist/modes/interactive/components/model-selector.js +237 -0
- package/dist/modes/interactive/components/oauth-selector.js +111 -0
- package/dist/modes/interactive/components/session-selector-search.js +157 -0
- package/dist/modes/interactive/components/session-selector.js +860 -0
- package/dist/modes/interactive/components/settings-selector.js +123 -0
- package/dist/modes/interactive/components/show-images-selector.js +35 -0
- package/dist/modes/interactive/components/skill-invocation-message.js +48 -0
- package/dist/modes/interactive/components/theme-selector.js +47 -0
- package/dist/modes/interactive/components/thinking-selector.js +47 -0
- package/dist/modes/interactive/components/tool-execution.js +789 -0
- package/dist/modes/interactive/components/tool-group.js +106 -0
- package/dist/modes/interactive/components/tree-selector.js +962 -0
- package/dist/modes/interactive/components/user-message-selector.js +115 -0
- package/dist/modes/interactive/components/user-message.js +48 -0
- package/dist/modes/interactive/components/visual-truncate.js +33 -0
- package/dist/modes/interactive/file-attachments.js +135 -0
- package/dist/modes/interactive/interactive-mode.js +3775 -0
- package/dist/modes/interactive/theme/dark.json +85 -0
- package/dist/modes/interactive/theme/light.json +85 -0
- package/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/dist/modes/interactive/theme/theme.js +177 -0
- package/dist/modes/print-mode.js +101 -0
- package/dist/modes/rpc/rpc-client.js +387 -0
- package/dist/modes/rpc/rpc-mode.js +509 -0
- package/dist/modes/rpc/rpc-types.js +8 -0
- package/dist/subagent-entry.js +145 -0
- package/dist/tool-names.js +34 -0
- package/dist/tui/autocomplete.js +596 -0
- package/dist/tui/components/box.js +104 -0
- package/dist/tui/components/cancellable-loader.js +35 -0
- package/dist/tui/components/editor.js +1679 -0
- package/dist/tui/components/image.js +69 -0
- package/dist/tui/components/input.js +433 -0
- package/dist/tui/components/loader.js +49 -0
- package/dist/tui/components/markdown.js +629 -0
- package/dist/tui/components/select-list.js +152 -0
- package/dist/tui/components/settings-list.js +185 -0
- package/dist/tui/components/spacer.js +23 -0
- package/dist/tui/components/text.js +89 -0
- package/dist/tui/components/truncated-text.js +51 -0
- package/dist/tui/editor-component.js +2 -0
- package/dist/tui/fuzzy.js +107 -0
- package/dist/tui/get-east-asian-width/index.js +32 -0
- package/dist/tui/get-east-asian-width/lookup.js +404 -0
- package/dist/tui/index.js +32 -0
- package/dist/tui/keybindings.js +114 -0
- package/dist/tui/keys.js +959 -0
- package/dist/tui/kill-ring.js +44 -0
- package/dist/tui/stdin-buffer.js +317 -0
- package/dist/tui/terminal-image.js +288 -0
- package/dist/tui/terminal.js +249 -0
- package/dist/tui/tui/autocomplete.js +596 -0
- package/dist/tui/tui/components/box.js +106 -0
- package/dist/tui/tui/components/cancellable-loader.js +35 -0
- package/dist/tui/tui/components/editor.js +1679 -0
- package/dist/tui/tui/components/image.js +69 -0
- package/dist/tui/tui/components/input.js +433 -0
- package/dist/tui/tui/components/loader.js +49 -0
- package/dist/tui/tui/components/markdown.js +629 -0
- package/dist/tui/tui/components/select-list.js +152 -0
- package/dist/tui/tui/components/settings-list.js +185 -0
- package/dist/tui/tui/components/spacer.js +23 -0
- package/dist/tui/tui/components/text.js +91 -0
- package/dist/tui/tui/components/truncated-text.js +51 -0
- package/dist/tui/tui/editor-component.js +2 -0
- package/dist/tui/tui/fuzzy.js +107 -0
- package/dist/tui/tui/get-east-asian-width/index.js +32 -0
- package/dist/tui/tui/get-east-asian-width/lookup.js +404 -0
- package/dist/tui/tui/index.js +32 -0
- package/dist/tui/tui/keybindings.js +114 -0
- package/dist/tui/tui/keys.js +959 -0
- package/dist/tui/tui/kill-ring.js +44 -0
- package/dist/tui/tui/stdin-buffer.js +317 -0
- package/dist/tui/tui/terminal-image.js +288 -0
- package/dist/tui/tui/terminal.js +249 -0
- package/dist/tui/tui/tui.js +955 -0
- package/dist/tui/tui/undo-stack.js +25 -0
- package/dist/tui/tui/utils.js +800 -0
- package/dist/tui/tui.js +955 -0
- package/dist/tui/undo-stack.js +25 -0
- package/dist/tui/utils.js +800 -0
- package/dist/utils/changelog.js +87 -0
- package/dist/utils/clipboard-image.js +164 -0
- package/dist/utils/clipboard-native.js +14 -0
- package/dist/utils/clipboard.js +67 -0
- package/dist/utils/frontmatter.js +26 -0
- package/dist/utils/git.js +166 -0
- package/dist/utils/image-convert.js +35 -0
- package/dist/utils/image-resize.js +183 -0
- package/dist/utils/mime.js +26 -0
- package/dist/utils/photon.js +121 -0
- package/dist/utils/shell.js +217 -0
- package/dist/utils/sleep.js +17 -0
- package/dist/utils/tools-manager.js +259 -0
- package/package.json +78 -0
- package/skills/excel-com-api/SKILL.md +74 -0
- package/skills/excel-com-api/excel-type-library.py +27767 -0
- package/skills/excel-com-api/office-type-library.py +10867 -0
- package/skills/integrations/SKILL.md +138 -0
- package/skills/integrations/alphasense.md +457 -0
- package/skills/integrations/bloomberg.md +803 -0
- package/skills/integrations/calcbench.md +315 -0
- package/skills/integrations/capiq.md +848 -0
- package/skills/integrations/dynamics-365-finance.md +354 -0
- package/skills/integrations/earnings_transcripts.md +387 -0
- package/skills/integrations/factset.md +758 -0
- package/skills/integrations/ice-fixed-income.md +344 -0
- package/skills/integrations/moodys-analytics.md +313 -0
- package/skills/integrations/morningstar.md +433 -0
- package/skills/integrations/nasdaq-data-link.md +249 -0
- package/skills/integrations/pitchbook.md +413 -0
- package/skills/integrations/preqin.md +422 -0
- package/skills/integrations/quickbooks.md +289 -0
- package/skills/integrations/quickfs.md +314 -0
- package/skills/integrations/refinitiv.md +473 -0
- package/skills/integrations/sage-intacct.md +401 -0
- package/skills/integrations/visible-alpha.md +320 -0
- package/skills/integrations/xero.md +393 -0
- package/skills/integrations/ycharts.md +306 -0
- package/skills/pdf-creation/SKILL.md +93 -0
- package/skills/pdf-extraction/SKILL.md +32 -0
- package/skills/powerpoint-creation/SKILL.md +110 -0
- package/skills/sec-edgar/SKILL.md +127 -0
- package/skills/sec-edgar/sec_to_pdf.py +109 -0
- package/xll/ShortcutXL.xll +0 -0
- package/xll/modules/debug_render.py +272 -0
- package/xll/modules/gameboy.py +241 -0
- package/xll/modules/pong.py +188 -0
- package/xll/modules/shortcut_xl/__init__.py +18 -0
- package/xll/modules/shortcut_xl/_categorize.py +200 -0
- package/xll/modules/shortcut_xl/_com.py +108 -0
- package/xll/modules/shortcut_xl/_format.py +252 -0
- package/xll/modules/shortcut_xl/_log.py +12 -0
- package/xll/modules/shortcut_xl/_managed.py +116 -0
- package/xll/modules/shortcut_xl/_registry.py +44 -0
- package/xll/modules/shortcut_xl/_threading.py +161 -0
- package/xll/modules/shortcut_xl/_tracking.py +283 -0
- package/xll/modules/stocks.py +100 -0
- package/xll/python3.dll +0 -0
- package/xll/python312.dll +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shortcut OAuth Provider for the agent.
|
|
3
|
+
*
|
|
4
|
+
* Implements the pi-ai OAuthProviderInterface using Shortcut's Device Code Flow (RFC 8628).
|
|
5
|
+
* Users run `/login`, get a code, open browser to authorize, and the agent polls for tokens.
|
|
6
|
+
*
|
|
7
|
+
* Token refresh uses Shortcut's `POST /auth/refresh` endpoint with body mode (token_delivery: "body").
|
|
8
|
+
*/
|
|
9
|
+
import { SHORTCUT_AUTH_URL as AUTH_SERVICE_URL } from '../constants.js';
|
|
10
|
+
const PLATFORM = 'agent-cli';
|
|
11
|
+
// Refresh tokens 2 minutes before expiry to avoid mid-request failures.
|
|
12
|
+
// The web client uses 5 min, but the agent only refreshes reactively (on getApiKey),
|
|
13
|
+
// so we use a shorter buffer — just enough to cover an in-flight streaming request.
|
|
14
|
+
const REFRESH_BUFFER_MS = 2 * 60 * 1000;
|
|
15
|
+
// PKCE utilities (inlined to avoid cross-package import issues)
|
|
16
|
+
const BASE64URL_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
17
|
+
function generateCodeVerifier() {
|
|
18
|
+
const array = new Uint8Array(64);
|
|
19
|
+
crypto.getRandomValues(array);
|
|
20
|
+
return Array.from(array, (byte) => BASE64URL_CHARSET[byte % 64]).join('');
|
|
21
|
+
}
|
|
22
|
+
async function generateCodeChallenge(codeVerifier) {
|
|
23
|
+
const data = new TextEncoder().encode(codeVerifier);
|
|
24
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
25
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
26
|
+
const base64 = btoa(String.fromCharCode(...hashArray));
|
|
27
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Shortcut OAuth provider config (without `id` — pi-ai adds that from the provider name).
|
|
31
|
+
*/
|
|
32
|
+
export const shortcutOAuth = {
|
|
33
|
+
name: 'Shortcut',
|
|
34
|
+
usesCallbackServer: false,
|
|
35
|
+
async login(callbacks) {
|
|
36
|
+
// 1. Initiate device code flow
|
|
37
|
+
const codeVerifier = generateCodeVerifier();
|
|
38
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
39
|
+
const codeResponse = await fetch(`${AUTH_SERVICE_URL}/auth/device/code`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
code_challenge: codeChallenge,
|
|
44
|
+
platform: PLATFORM
|
|
45
|
+
}),
|
|
46
|
+
signal: callbacks.signal
|
|
47
|
+
});
|
|
48
|
+
if (!codeResponse.ok) {
|
|
49
|
+
const data = await codeResponse.json().catch(() => ({}));
|
|
50
|
+
throw new Error(data.error ?? `Failed to initiate device code: ${codeResponse.status}`);
|
|
51
|
+
}
|
|
52
|
+
const deviceCode = await codeResponse.json();
|
|
53
|
+
// 2. Tell the user to open the browser
|
|
54
|
+
callbacks.onAuth({
|
|
55
|
+
url: deviceCode.verification_uri_complete,
|
|
56
|
+
instructions: `Your code is: ${deviceCode.user_code}`
|
|
57
|
+
});
|
|
58
|
+
// 3. Poll for authorization
|
|
59
|
+
let interval = deviceCode.interval ?? 5;
|
|
60
|
+
const deadline = Date.now() + deviceCode.expires_in * 1000;
|
|
61
|
+
while (Date.now() < deadline) {
|
|
62
|
+
if (callbacks.signal?.aborted) {
|
|
63
|
+
throw new Error('Login cancelled');
|
|
64
|
+
}
|
|
65
|
+
await new Promise((resolve, reject) => {
|
|
66
|
+
const timeoutId = setTimeout(resolve, interval * 1000);
|
|
67
|
+
if (callbacks.signal) {
|
|
68
|
+
callbacks.signal.addEventListener('abort', () => {
|
|
69
|
+
clearTimeout(timeoutId);
|
|
70
|
+
reject(new Error('Login cancelled'));
|
|
71
|
+
}, { once: true });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
let response;
|
|
75
|
+
try {
|
|
76
|
+
response = await fetch(`${AUTH_SERVICE_URL}/auth/device/token`, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
device_code: deviceCode.device_code,
|
|
81
|
+
code_verifier: codeVerifier,
|
|
82
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
|
83
|
+
}),
|
|
84
|
+
signal: callbacks.signal
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Network error — keep polling
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (response.ok) {
|
|
92
|
+
const tokens = await response.json();
|
|
93
|
+
return {
|
|
94
|
+
access: tokens.accessToken,
|
|
95
|
+
refresh: tokens.refreshToken,
|
|
96
|
+
expires: tokens.accessExpiresAt - REFRESH_BUFFER_MS
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const errorData = await response.json().catch(() => ({ error: 'unknown' }));
|
|
100
|
+
const errorType = errorData.error;
|
|
101
|
+
if (errorType === 'authorization_pending') {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
else if (errorType === 'slow_down') {
|
|
105
|
+
interval = errorData.interval ?? interval + 5;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
else if (errorType === 'expired_token') {
|
|
109
|
+
throw new Error('Device code expired. Please try /login again.');
|
|
110
|
+
}
|
|
111
|
+
else if (errorType === 'access_denied') {
|
|
112
|
+
throw new Error('Authorization denied.');
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
throw new Error(errorData.error_description ?? errorData.error ?? 'Login failed');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
throw new Error('Device code expired. Please try /login again.');
|
|
119
|
+
},
|
|
120
|
+
async refreshToken(credentials) {
|
|
121
|
+
const response = await fetch(`${AUTH_SERVICE_URL}/auth/refresh`, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { 'Content-Type': 'application/json' },
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
refreshToken: credentials.refresh,
|
|
126
|
+
token_delivery: 'body',
|
|
127
|
+
platform: PLATFORM
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
const data = await response.json().catch(() => ({}));
|
|
132
|
+
throw new Error(data.error ?? `Token refresh failed: ${response.status}`);
|
|
133
|
+
}
|
|
134
|
+
const tokens = await response.json();
|
|
135
|
+
return {
|
|
136
|
+
access: tokens.accessToken,
|
|
137
|
+
refresh: tokens.refreshToken,
|
|
138
|
+
expires: tokens.accessExpiresAt - REFRESH_BUFFER_MS
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
getApiKey(credentials) {
|
|
142
|
+
return credentials.access;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
//# sourceMappingURL=shortcut-oauth.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shortcut production constants.
|
|
3
|
+
*
|
|
4
|
+
* These are the defaults when no .env file is present (i.e. installed product).
|
|
5
|
+
* For local development, create a .env file to override — see .env.development.
|
|
6
|
+
*
|
|
7
|
+
* Every env var listed here can be overridden via process.env.
|
|
8
|
+
*/
|
|
9
|
+
// -- Shortcut infrastructure URLs ------------------------------------------
|
|
10
|
+
/** LLM proxy — routes LLM calls through the Python backend (auth, credits, model allowlist). */
|
|
11
|
+
export const SHORTCUT_LLM_PROXY_URL = process.env.SHORTCUT_LLM_PROXY_URL ?? 'https://agent.shortcut.ai';
|
|
12
|
+
/** Auth service — device code OAuth flow, token refresh. */
|
|
13
|
+
export const SHORTCUT_AUTH_URL = process.env.SHORTCUT_AUTH_URL ?? 'https://auth.shortcut.ai';
|
|
14
|
+
/** API service — credit balance, billing. */
|
|
15
|
+
export const SHORTCUT_API_URL = process.env.SHORTCUT_API_URL ?? 'https://api.shortcut.ai';
|
|
16
|
+
// -- Dev mode --------------------------------------------------------------
|
|
17
|
+
// DEV_MODE lives in config.ts (layer-0) so modes/ can import it without boundary violations.
|
|
18
|
+
// -- Excel XLL -------------------------------------------------------------
|
|
19
|
+
/** ShortcutXL HTTP server (always localhost — runs in-process with Excel). */
|
|
20
|
+
export const EXCEL_HTTP_URL = process.env.EXCEL_HTTP_URL ?? 'http://127.0.0.1:8080';
|
|
21
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch a workbook summary from the running Excel instance via ShortcutXL.
|
|
3
|
+
*
|
|
4
|
+
* POSTs a small Python snippet to the XLL /exec endpoint that reads:
|
|
5
|
+
* - All open workbooks (not just the active one)
|
|
6
|
+
* - All sheet names with their used ranges per workbook
|
|
7
|
+
* - Which workbook and sheet are currently active
|
|
8
|
+
*
|
|
9
|
+
* Output format: first line is comma-separated workbook names (for easy parsing),
|
|
10
|
+
* followed by the full summary for the LLM.
|
|
11
|
+
*/
|
|
12
|
+
import { EXCEL_HTTP_URL } from '../constants.js';
|
|
13
|
+
const SUMMARY_CODE = `
|
|
14
|
+
from shortcut_xl import xl_app
|
|
15
|
+
app = xl_app()
|
|
16
|
+
if app.Workbooks.Count == 0:
|
|
17
|
+
print("")
|
|
18
|
+
else:
|
|
19
|
+
active_wb = app.ActiveWorkbook
|
|
20
|
+
active_ws = app.ActiveSheet
|
|
21
|
+
active_wb_name = active_wb.Name if active_wb else None
|
|
22
|
+
active_ws_name = active_ws.Name if active_ws else None
|
|
23
|
+
names = [wb.Name for wb in app.Workbooks]
|
|
24
|
+
print(",".join(names))
|
|
25
|
+
for wb in app.Workbooks:
|
|
26
|
+
wb_marker = " (active)" if wb.Name == active_wb_name else ""
|
|
27
|
+
print(f"Workbook: {wb.Name}{wb_marker}")
|
|
28
|
+
print("Sheets and their used ranges:")
|
|
29
|
+
for ws in wb.Worksheets:
|
|
30
|
+
used = ws.UsedRange
|
|
31
|
+
addr = used.Address.replace("$", "")
|
|
32
|
+
ws_marker = " (active)" if wb.Name == active_wb_name and ws.Name == active_ws_name else ""
|
|
33
|
+
print(f" {ws.Name}: {addr}{ws_marker}")
|
|
34
|
+
`.trim();
|
|
35
|
+
/**
|
|
36
|
+
* Fetch the current workbook summary from Excel.
|
|
37
|
+
* Returns the raw summary string or null if unavailable.
|
|
38
|
+
* First line is comma-separated workbook names; rest is the full summary.
|
|
39
|
+
*/
|
|
40
|
+
export async function fetchWorkbookSummary(httpUrl) {
|
|
41
|
+
const baseUrl = httpUrl ?? EXCEL_HTTP_URL;
|
|
42
|
+
try {
|
|
43
|
+
const response = await fetch(`${baseUrl}/exec`, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'Content-Type': 'application/json' },
|
|
46
|
+
body: JSON.stringify({ code: SUMMARY_CODE }),
|
|
47
|
+
signal: AbortSignal.timeout(3000)
|
|
48
|
+
});
|
|
49
|
+
const result = (await response.json());
|
|
50
|
+
const output = result.ok ? result.output?.trim() : '';
|
|
51
|
+
return output || null;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Extract workbook names from the first line of a summary. */
|
|
58
|
+
export function parseWorkbookNames(summary) {
|
|
59
|
+
const firstLine = summary.split('\n')[0];
|
|
60
|
+
return firstLine
|
|
61
|
+
? firstLine
|
|
62
|
+
.split(',')
|
|
63
|
+
.map((n) => n.trim())
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
: [];
|
|
66
|
+
}
|
|
67
|
+
/** Format summary as an LLM context block (strips the names header line). */
|
|
68
|
+
export function formatSummaryForLlm(summary) {
|
|
69
|
+
const lines = summary.split('\n');
|
|
70
|
+
const body = lines.slice(1).join('\n');
|
|
71
|
+
return `\n\n# Brief summary of the current open workbooks and sheets *(NOT an attachment)*\n\n${body}`;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=workbook-summary.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shortcut credit balance fetcher.
|
|
3
|
+
*
|
|
4
|
+
* Fetches the user's remaining credits from the Shortcut API
|
|
5
|
+
* and exposes them for display in the footer.
|
|
6
|
+
*/
|
|
7
|
+
import { SHORTCUT_API_URL as API_BASE_URL } from '../constants.js';
|
|
8
|
+
/**
|
|
9
|
+
* Fetch the current credit balance for a Shortcut-authenticated user.
|
|
10
|
+
* Returns null on any failure (network, auth, etc.) — never throws.
|
|
11
|
+
*/
|
|
12
|
+
export async function fetchCreditBalance(accessToken) {
|
|
13
|
+
try {
|
|
14
|
+
const response = await fetch(`${API_BASE_URL}/api/credits/balance`, {
|
|
15
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
16
|
+
});
|
|
17
|
+
if (!response.ok)
|
|
18
|
+
return null;
|
|
19
|
+
const data = await response.json();
|
|
20
|
+
return {
|
|
21
|
+
creditsRemaining: data.creditsRemaining ?? 0,
|
|
22
|
+
isUnlimited: data.isUnlimited ?? false
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=shortcut-credits.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cron daemon entry point.
|
|
3
|
+
*
|
|
4
|
+
* Called via `node dist/cli.js cron-daemon` (dispatched from main.ts).
|
|
5
|
+
* Minimal startup: auth refresh + daemon tick loop. No TUI, no tools.
|
|
6
|
+
*/
|
|
7
|
+
import { AuthStorage } from '../../core/auth-storage.js';
|
|
8
|
+
import { startProactiveRefresh } from '../auth/refresh-timer.js';
|
|
9
|
+
import { SHORTCUT_PROVIDER_ID } from '../providers/provider-ids.js';
|
|
10
|
+
import { startDaemonLoop } from './daemon.js';
|
|
11
|
+
export async function runCronDaemon() {
|
|
12
|
+
// Keep OAuth tokens fresh for spawned agent processes
|
|
13
|
+
const authStorage = AuthStorage.create();
|
|
14
|
+
startProactiveRefresh(authStorage, SHORTCUT_PROVIDER_ID);
|
|
15
|
+
// Start the tick loop (runs forever until SIGTERM)
|
|
16
|
+
await startDaemonLoop();
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=cron-daemon-entry.js.map
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon IPC — start, stop, and query the cron daemon process.
|
|
3
|
+
*
|
|
4
|
+
* The daemon is a detached child process that outlives the parent.
|
|
5
|
+
* Communication is via PID file + signals (no sockets needed).
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { resolveOwnBinary } from '../../config.js';
|
|
11
|
+
import { getCronDir } from './store.js';
|
|
12
|
+
export function getDaemonPidPath() {
|
|
13
|
+
return path.join(getCronDir(), 'daemon.pid');
|
|
14
|
+
}
|
|
15
|
+
function getDaemonLogPath() {
|
|
16
|
+
return path.join(getCronDir(), 'daemon.log');
|
|
17
|
+
}
|
|
18
|
+
/** Check if the daemon is alive by reading the PID file and pinging the process. */
|
|
19
|
+
export function getDaemonStatus() {
|
|
20
|
+
let pid;
|
|
21
|
+
try {
|
|
22
|
+
const raw = fs.readFileSync(getDaemonPidPath(), 'utf-8').trim();
|
|
23
|
+
pid = parseInt(raw, 10);
|
|
24
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
25
|
+
return { running: false };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return { running: false };
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
process.kill(pid, 0); // signal 0 = existence check
|
|
32
|
+
return { running: true, pid };
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Stale PID file — process is gone
|
|
36
|
+
cleanupPidFile();
|
|
37
|
+
return { running: false };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function isDaemonRunning() {
|
|
41
|
+
return getDaemonStatus().running;
|
|
42
|
+
}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Start
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
/** Spawn the daemon as a detached background process. Returns the PID. */
|
|
47
|
+
export async function startDaemon() {
|
|
48
|
+
const status = getDaemonStatus();
|
|
49
|
+
if (status.running && status.pid) {
|
|
50
|
+
return { pid: status.pid, alreadyRunning: true };
|
|
51
|
+
}
|
|
52
|
+
await fs.promises.mkdir(getCronDir(), { recursive: true, mode: 0o700 });
|
|
53
|
+
const [cmd, ...args] = resolveOwnBinary();
|
|
54
|
+
// Wrap fd in try/finally to prevent leak on spawn error
|
|
55
|
+
const logFd = fs.openSync(getDaemonLogPath(), 'a');
|
|
56
|
+
let child;
|
|
57
|
+
try {
|
|
58
|
+
child = spawn(cmd, [...args, 'cron-daemon'], {
|
|
59
|
+
detached: true,
|
|
60
|
+
stdio: ['ignore', logFd, logFd],
|
|
61
|
+
cwd: process.env.HOME ?? process.cwd()
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
fs.closeSync(logFd);
|
|
66
|
+
}
|
|
67
|
+
// Guard against undefined pid (spawn failure)
|
|
68
|
+
if (!child.pid) {
|
|
69
|
+
throw new Error('Failed to spawn cron daemon process');
|
|
70
|
+
}
|
|
71
|
+
const pid = child.pid;
|
|
72
|
+
child.unref();
|
|
73
|
+
// PID file is written by the daemon itself (daemon.ts startDaemonLoop).
|
|
74
|
+
// Poll briefly for it to appear so callers get an accurate status.
|
|
75
|
+
for (let i = 0; i < 20; i++) {
|
|
76
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
77
|
+
try {
|
|
78
|
+
fs.accessSync(getDaemonPidPath());
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Not yet
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { pid, alreadyRunning: false };
|
|
86
|
+
}
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Stop
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
/** Stop the daemon by sending SIGTERM. Returns true if the daemon was running. */
|
|
91
|
+
export async function stopDaemon() {
|
|
92
|
+
const status = getDaemonStatus();
|
|
93
|
+
if (!status.running || !status.pid)
|
|
94
|
+
return false;
|
|
95
|
+
try {
|
|
96
|
+
process.kill(status.pid, 'SIGTERM');
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Process already gone
|
|
100
|
+
}
|
|
101
|
+
// Wait for process to exit before cleaning up PID file
|
|
102
|
+
const pid = status.pid;
|
|
103
|
+
let exited = false;
|
|
104
|
+
for (let i = 0; i < 20; i++) {
|
|
105
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
106
|
+
try {
|
|
107
|
+
process.kill(pid, 0);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
exited = true;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (exited) {
|
|
115
|
+
cleanupPidFile();
|
|
116
|
+
}
|
|
117
|
+
// If not exited after 1s, leave PID file so isDaemonRunning() still reports true
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Helpers
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
function cleanupPidFile() {
|
|
124
|
+
try {
|
|
125
|
+
fs.unlinkSync(getDaemonPidPath());
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Best effort
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=daemon-ipc.js.map
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cron daemon — background timer loop that executes due jobs.
|
|
3
|
+
*
|
|
4
|
+
* Runs as a detached Node process (spawned via daemon-ipc.ts).
|
|
5
|
+
* On each tick: load store → find due jobs → execute sequentially → save.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { resolveOwnBinary } from '../../config.js';
|
|
11
|
+
import { getDaemonPidPath } from './daemon-ipc.js';
|
|
12
|
+
import { applyJobResult, isJobDue, normalizeJobState } from './jobs.js';
|
|
13
|
+
import { appendRunLog, truncateRunOutput } from './run-log.js';
|
|
14
|
+
import { clearStoreCache, getCronStorePath, loadCronStore, saveCronStore } from './store.js';
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Constants
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
/** Tick interval — poll this often. */
|
|
19
|
+
const TICK_INTERVAL_MS = 60_000;
|
|
20
|
+
/** Timeout for a single job execution. */
|
|
21
|
+
const JOB_TIMEOUT_MS = 10 * 60_000; // 10 minutes
|
|
22
|
+
/** Max captured output per stream (stdout/stderr) to prevent OOM. */
|
|
23
|
+
const MAX_OUTPUT_BYTES = 1024 * 1024; // 1 MB
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// State
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
let tickTimer = null;
|
|
28
|
+
let stopping = false;
|
|
29
|
+
let activeChild = null;
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Job execution
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
/** Spawn the agent binary in print mode for a job's payload. */
|
|
34
|
+
function spawnAgentForJob(job) {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
const [cmd, ...baseArgs] = resolveOwnBinary();
|
|
37
|
+
const args = [
|
|
38
|
+
...baseArgs,
|
|
39
|
+
'-p',
|
|
40
|
+
'--no-session',
|
|
41
|
+
...(job.payload.model ? ['--model', job.payload.model] : []),
|
|
42
|
+
job.payload.message
|
|
43
|
+
];
|
|
44
|
+
let stdout = '';
|
|
45
|
+
let stderr = '';
|
|
46
|
+
let settled = false;
|
|
47
|
+
const child = nodeSpawn(cmd, args, {
|
|
48
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
49
|
+
cwd: process.env.HOME ?? process.cwd()
|
|
50
|
+
});
|
|
51
|
+
activeChild = child;
|
|
52
|
+
child.stdout?.on('data', (chunk) => {
|
|
53
|
+
if (stdout.length < MAX_OUTPUT_BYTES)
|
|
54
|
+
stdout += chunk.toString();
|
|
55
|
+
});
|
|
56
|
+
child.stderr?.on('data', (chunk) => {
|
|
57
|
+
if (stderr.length < MAX_OUTPUT_BYTES)
|
|
58
|
+
stderr += chunk.toString();
|
|
59
|
+
});
|
|
60
|
+
let escalation = null;
|
|
61
|
+
const timeout = setTimeout(() => {
|
|
62
|
+
child.kill('SIGTERM');
|
|
63
|
+
stderr += '\n[daemon] Job timed out';
|
|
64
|
+
// Escalate to SIGKILL if process doesn't exit within 5s
|
|
65
|
+
escalation = setTimeout(() => child.kill('SIGKILL'), 5_000);
|
|
66
|
+
escalation.unref();
|
|
67
|
+
}, JOB_TIMEOUT_MS);
|
|
68
|
+
child.on('close', (code) => {
|
|
69
|
+
if (settled)
|
|
70
|
+
return;
|
|
71
|
+
settled = true;
|
|
72
|
+
activeChild = null;
|
|
73
|
+
clearTimeout(timeout);
|
|
74
|
+
if (escalation)
|
|
75
|
+
clearTimeout(escalation);
|
|
76
|
+
resolve({
|
|
77
|
+
exitCode: code ?? 1,
|
|
78
|
+
output: stdout.trim(),
|
|
79
|
+
error: stderr.trim()
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
child.on('error', (err) => {
|
|
83
|
+
if (settled)
|
|
84
|
+
return;
|
|
85
|
+
settled = true;
|
|
86
|
+
activeChild = null;
|
|
87
|
+
clearTimeout(timeout);
|
|
88
|
+
if (escalation)
|
|
89
|
+
clearTimeout(escalation);
|
|
90
|
+
resolve({
|
|
91
|
+
exitCode: 1,
|
|
92
|
+
output: '',
|
|
93
|
+
error: err.message
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async function executeJob(job, storePath) {
|
|
99
|
+
const startedAt = Date.now();
|
|
100
|
+
log(`Executing job "${job.name}" (${job.id})`);
|
|
101
|
+
const { exitCode, output, error } = await spawnAgentForJob(job);
|
|
102
|
+
const endedAt = Date.now();
|
|
103
|
+
const result = {
|
|
104
|
+
status: exitCode === 0 ? 'ok' : 'error',
|
|
105
|
+
error: exitCode !== 0 ? error || `exit code ${exitCode}` : undefined,
|
|
106
|
+
startedAt,
|
|
107
|
+
endedAt
|
|
108
|
+
};
|
|
109
|
+
const shouldDelete = applyJobResult(job, result);
|
|
110
|
+
// Reload store before saving to pick up any concurrent changes
|
|
111
|
+
clearStoreCache();
|
|
112
|
+
const store = await loadCronStore(storePath);
|
|
113
|
+
const storeJob = store.jobs.find((j) => j.id === job.id);
|
|
114
|
+
if (storeJob) {
|
|
115
|
+
// Selectively merge only fields that applyJobResult modifies,
|
|
116
|
+
// preserving any concurrent user changes to other state fields
|
|
117
|
+
storeJob.state.runningAtMs = job.state.runningAtMs;
|
|
118
|
+
storeJob.state.lastRunAtMs = job.state.lastRunAtMs;
|
|
119
|
+
storeJob.state.lastRunStatus = job.state.lastRunStatus;
|
|
120
|
+
storeJob.state.lastDurationMs = job.state.lastDurationMs;
|
|
121
|
+
storeJob.state.lastError = job.state.lastError;
|
|
122
|
+
storeJob.state.consecutiveErrors = job.state.consecutiveErrors;
|
|
123
|
+
storeJob.state.nextRunAtMs = job.state.nextRunAtMs;
|
|
124
|
+
storeJob.enabled = job.enabled;
|
|
125
|
+
storeJob.updatedAtMs = job.updatedAtMs;
|
|
126
|
+
if (shouldDelete) {
|
|
127
|
+
store.jobs = store.jobs.filter((j) => j.id !== job.id);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
await saveCronStore(storePath, store);
|
|
131
|
+
await appendRunLog({
|
|
132
|
+
jobId: job.id,
|
|
133
|
+
jobName: job.name,
|
|
134
|
+
startedAtMs: startedAt,
|
|
135
|
+
endedAtMs: endedAt,
|
|
136
|
+
durationMs: endedAt - startedAt,
|
|
137
|
+
status: result.status,
|
|
138
|
+
error: result.error,
|
|
139
|
+
output: output ? truncateRunOutput(output) : undefined
|
|
140
|
+
});
|
|
141
|
+
const statusMsg = result.status === 'ok' ? `completed in ${endedAt - startedAt}ms` : `failed: ${result.error}`;
|
|
142
|
+
log(`Job "${job.name}" ${statusMsg}`);
|
|
143
|
+
}
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Tick loop
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
async function tick(storePath) {
|
|
148
|
+
if (stopping)
|
|
149
|
+
return;
|
|
150
|
+
try {
|
|
151
|
+
clearStoreCache();
|
|
152
|
+
const store = await loadCronStore(storePath);
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
// Normalize all jobs (clear stuck runs, repair missing schedules)
|
|
155
|
+
let storeChanged = false;
|
|
156
|
+
for (const job of store.jobs) {
|
|
157
|
+
if (normalizeJobState(job, now))
|
|
158
|
+
storeChanged = true;
|
|
159
|
+
}
|
|
160
|
+
if (storeChanged) {
|
|
161
|
+
await saveCronStore(storePath, store);
|
|
162
|
+
}
|
|
163
|
+
// Find and execute due jobs sequentially
|
|
164
|
+
const dueJobs = store.jobs.filter((j) => isJobDue(j, now));
|
|
165
|
+
for (const job of dueJobs) {
|
|
166
|
+
if (stopping)
|
|
167
|
+
break;
|
|
168
|
+
// Mark as running
|
|
169
|
+
job.state.runningAtMs = Date.now();
|
|
170
|
+
await saveCronStore(storePath, store);
|
|
171
|
+
await executeJob(job, storePath);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
log(`Tick error: ${err instanceof Error ? err.message : String(err)}`);
|
|
176
|
+
}
|
|
177
|
+
armTimer(storePath);
|
|
178
|
+
}
|
|
179
|
+
function armTimer(storePath) {
|
|
180
|
+
if (stopping)
|
|
181
|
+
return;
|
|
182
|
+
if (tickTimer)
|
|
183
|
+
clearTimeout(tickTimer);
|
|
184
|
+
tickTimer = setTimeout(() => void tick(storePath), TICK_INTERVAL_MS);
|
|
185
|
+
tickTimer.unref();
|
|
186
|
+
}
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Lifecycle
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
function log(msg) {
|
|
191
|
+
const ts = new Date().toISOString();
|
|
192
|
+
process.stdout.write(`[${ts}] ${msg}\n`);
|
|
193
|
+
}
|
|
194
|
+
/** Start the daemon tick loop. Call from the daemon entry point. */
|
|
195
|
+
export async function startDaemonLoop(storePath = getCronStorePath()) {
|
|
196
|
+
log('Cron daemon starting');
|
|
197
|
+
// Write PID file
|
|
198
|
+
await fs.promises.mkdir(path.dirname(getDaemonPidPath()), { recursive: true, mode: 0o700 });
|
|
199
|
+
await fs.promises.writeFile(getDaemonPidPath(), String(process.pid), { mode: 0o600 });
|
|
200
|
+
// Handle shutdown signals
|
|
201
|
+
const shutdown = () => {
|
|
202
|
+
log('Cron daemon shutting down');
|
|
203
|
+
stopping = true;
|
|
204
|
+
if (tickTimer) {
|
|
205
|
+
clearTimeout(tickTimer);
|
|
206
|
+
tickTimer = null;
|
|
207
|
+
}
|
|
208
|
+
if (activeChild) {
|
|
209
|
+
activeChild.kill('SIGTERM');
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
fs.unlinkSync(getDaemonPidPath());
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// best effort
|
|
216
|
+
}
|
|
217
|
+
process.exit(0);
|
|
218
|
+
};
|
|
219
|
+
process.on('SIGTERM', shutdown);
|
|
220
|
+
process.on('SIGINT', shutdown);
|
|
221
|
+
// Run first tick immediately
|
|
222
|
+
await tick(storePath);
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=daemon.js.map
|