onbuzz 4.9.13 → 4.10.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/node_modules/glob/README.md +31 -5
- package/node_modules/glob/dist/commonjs/glob.d.ts +8 -0
- package/node_modules/glob/dist/commonjs/glob.d.ts.map +1 -1
- package/node_modules/glob/dist/commonjs/glob.js +2 -1
- package/node_modules/glob/dist/commonjs/glob.js.map +1 -1
- package/node_modules/glob/dist/commonjs/index.min.js +3 -3
- package/node_modules/glob/dist/commonjs/index.min.js.map +4 -4
- package/node_modules/glob/dist/commonjs/pattern.d.ts +3 -0
- package/node_modules/glob/dist/commonjs/pattern.d.ts.map +1 -1
- package/node_modules/glob/dist/commonjs/pattern.js +4 -0
- package/node_modules/glob/dist/commonjs/pattern.js.map +1 -1
- package/node_modules/glob/dist/esm/glob.d.ts +8 -0
- package/node_modules/glob/dist/esm/glob.d.ts.map +1 -1
- package/node_modules/glob/dist/esm/glob.js +2 -1
- package/node_modules/glob/dist/esm/glob.js.map +1 -1
- package/node_modules/glob/dist/esm/index.min.js +3 -3
- package/node_modules/glob/dist/esm/index.min.js.map +4 -4
- package/node_modules/glob/dist/esm/pattern.d.ts +3 -0
- package/node_modules/glob/dist/esm/pattern.d.ts.map +1 -1
- package/node_modules/glob/dist/esm/pattern.js +4 -0
- package/node_modules/glob/dist/esm/pattern.js.map +1 -1
- package/node_modules/{@isaacs → glob/node_modules}/balanced-match/README.md +7 -10
- package/node_modules/{@isaacs → glob/node_modules}/balanced-match/package.json +7 -18
- package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/README.md +3 -6
- package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.js +6 -4
- package/node_modules/glob/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -0
- package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.js +6 -4
- package/node_modules/glob/node_modules/brace-expansion/dist/esm/index.js.map +1 -0
- package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/package.json +11 -7
- package/node_modules/glob/node_modules/minimatch/README.md +76 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts +4 -2
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js +309 -55
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js +2 -4
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js +4 -4
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts +81 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js +232 -134
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js +8 -8
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts +4 -2
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js +309 -55
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js +2 -4
- package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js +4 -4
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts +81 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.js +232 -134
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js +8 -8
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/package.json +17 -11
- package/node_modules/glob/package.json +10 -13
- package/node_modules/minipass/LICENSE.md +55 -0
- package/node_modules/minipass/dist/commonjs/index.d.ts +12 -16
- package/node_modules/minipass/dist/commonjs/index.d.ts.map +1 -1
- package/node_modules/minipass/dist/commonjs/index.js +13 -3
- package/node_modules/minipass/dist/commonjs/index.js.map +1 -1
- package/node_modules/minipass/dist/esm/index.d.ts +12 -16
- package/node_modules/minipass/dist/esm/index.d.ts.map +1 -1
- package/node_modules/minipass/dist/esm/index.js +3 -1
- package/node_modules/minipass/dist/esm/index.js.map +1 -1
- package/node_modules/minipass/package.json +9 -14
- package/node_modules/path-scurry/node_modules/lru-cache/README.md +96 -10
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts +1400 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js +1726 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js.map +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js +10 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.cjs.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.d.cts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts +109 -32
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts.map +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js +334 -197
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js.map +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js.map +4 -4
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.js +9 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts +1400 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js +1726 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js.map +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js +10 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js +10 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js +4 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts +1400 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js +1722 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js.map +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.d.mts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.mjs.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.js +19 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts +109 -32
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts.map +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js +333 -196
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js.map +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js.map +4 -4
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js +6 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts +1400 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js +1722 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js.map +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/package.json +71 -18
- package/node_modules/path-scurry/package.json +8 -24
- package/package.json +1 -1
- package/scripts/debug-balance-probe.mjs +35 -35
- package/scripts/push-image.sh +43 -43
- package/scripts/setup-acr.sh +65 -65
- package/scripts/verify-optional-deps.js +96 -1
- package/src/__tests__/composioCliFlags.test.js +239 -239
- package/src/analyzers/CSSAnalyzer.js +298 -297
- package/src/analyzers/ConfigValidator.js +691 -690
- package/src/analyzers/ESLintAnalyzer.js +320 -320
- package/src/analyzers/JavaScriptAnalyzer.js +260 -261
- package/src/analyzers/PrettierFormatter.js +246 -247
- package/src/analyzers/PythonAnalyzer.js +283 -283
- package/src/analyzers/SecurityAnalyzer.js +729 -729
- package/src/analyzers/SparrowAnalyzer.js +341 -341
- package/src/analyzers/TypeScriptAnalyzer.js +247 -247
- package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -41
- package/src/analyzers/__tests__/ConfigValidator.test.js +362 -362
- package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -40
- package/src/analyzers/__tests__/PythonAnalyzer.test.js +205 -208
- package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -303
- package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -187
- package/src/analyzers/codeCloneDetector/analyzer.js +344 -344
- package/src/analyzers/codeCloneDetector/detector.js +250 -250
- package/src/analyzers/codeCloneDetector/index.js +194 -192
- package/src/analyzers/codeCloneDetector/parser.js +199 -199
- package/src/core/__tests__/agentPool.test.js +866 -866
- package/src/core/__tests__/agentPoolAutoResume.test.js +209 -209
- package/src/core/__tests__/agentPoolWakeOnMessage.test.js +315 -315
- package/src/core/__tests__/agentScheduler.emptyResponseChatStall.test.js +213 -213
- package/src/core/__tests__/agentScheduler.errorCategorisation.test.js +246 -246
- package/src/core/__tests__/agentScheduler.firstChunkTimeout.test.js +138 -138
- package/src/core/__tests__/agentScheduler.modeTransitions.test.js +233 -233
- package/src/core/__tests__/agentScheduler.nativePromptPick.test.js +319 -319
- package/src/core/__tests__/agentScheduler.taskLifecycleInstruction.test.js +78 -78
- package/src/core/__tests__/agentScheduler.visualizer.test.js +258 -258
- package/src/core/__tests__/flowCheckpointStore.test.js +140 -140
- package/src/core/__tests__/flowEndToEnd.test.js +565 -565
- package/src/core/__tests__/flowFieldMapping.test.js +188 -189
- package/src/core/__tests__/flowLintClientMirror.test.js +96 -98
- package/src/core/__tests__/flowSavePayload.test.js +170 -169
- package/src/core/__tests__/flowTemplates.test.js +311 -311
- package/src/core/__tests__/flowVersionStore.test.js +123 -123
- package/src/core/__tests__/messageProcessor.test.js +669 -669
- package/src/core/__tests__/stateManager.test.js +0 -1
- package/src/core/agentPool.js +2474 -2475
- package/src/core/agentScheduler.js +1 -4
- package/src/core/contextManager.js +708 -708
- package/src/core/flowExecutor.js +1510 -1510
- package/src/core/flowFieldMapping.js +136 -138
- package/src/core/messageProcessor.js +953 -954
- package/src/core/orchestrator.js +593 -595
- package/src/core/stateManager.js +1765 -1752
- package/src/index.js +1221 -1221
- package/src/interfaces/__tests__/archivedAgentDelete.test.js +207 -207
- package/src/interfaces/__tests__/bulkAgentRoute.test.js +361 -361
- package/src/interfaces/__tests__/imageServing.test.js +228 -228
- package/src/interfaces/__tests__/remoteSessionAuth.test.js +308 -308
- package/src/interfaces/__tests__/videoJobsRoutes.test.js +178 -179
- package/src/interfaces/__tests__/webServer.marketplace.test.js +629 -629
- package/src/interfaces/schedulerRoutes.js +50 -50
- package/src/interfaces/terminal/__tests__/smoke/connection.test.js +341 -350
- package/src/interfaces/terminal/__tests__/smoke/enhancements.test.js +156 -156
- package/src/interfaces/terminal/__tests__/smoke/imports.test.js +325 -330
- package/src/interfaces/terminal/__tests__/smoke/tools.test.js +385 -388
- package/src/interfaces/terminal/api/session.js +265 -266
- package/src/interfaces/terminal/api/websocket.js +496 -497
- package/src/interfaces/terminal/components/AgentCreator.js +691 -705
- package/src/interfaces/terminal/components/AgentEditor.js +676 -678
- package/src/interfaces/terminal/components/AgentSwitcher.js +331 -330
- package/src/interfaces/terminal/components/ErrorPanel.js +263 -264
- package/src/interfaces/terminal/components/Header.js +28 -28
- package/src/interfaces/terminal/components/Layout.js +598 -603
- package/src/interfaces/terminal/components/MessageList.js +280 -281
- package/src/interfaces/terminal/components/SettingsPanel.js +410 -415
- package/src/interfaces/terminal/components/StatusBar.js +2 -0
- package/src/interfaces/terminal/index.js +168 -168
- package/src/interfaces/terminal/state/useAgentControl.js +496 -496
- package/src/interfaces/terminal/state/useAgents.js +537 -537
- package/src/interfaces/terminal/state/useMessages.js +629 -630
- package/src/interfaces/terminal/state/useTools.js +554 -554
- package/src/interfaces/terminal/utils/debugLogger.js +44 -44
- package/src/interfaces/terminal/utils/settingsStorage.js +232 -232
- package/src/interfaces/webServer.js +7578 -7579
- package/src/interfaces/webServer.js.bak +7046 -7046
- package/src/modules/fileExplorer/__tests__/zipDownload.test.js +237 -237
- package/src/modules/fileExplorer/controller.js +470 -469
- package/src/modules/fileExplorer/routes.js +285 -286
- package/src/modules/widget/__tests__/isDisabled.test.js +41 -41
- package/src/modules/widget/__tests__/routes.test.js +677 -678
- package/src/modules/widget/__tests__/runtime.test.js +401 -401
- package/src/modules/widget/__tests__/versioning.test.js +309 -309
- package/src/modules/widget/__tests__/webComponentRuntime.test.js +565 -565
- package/src/modules/widget/__tests__/widgetTool.test.js +316 -316
- package/src/modules/widget/routes.js +435 -435
- package/src/modules/widget/runtime/bundle.js +640 -640
- package/src/modules/widget/runtime/webComponentBundle.js +470 -470
- package/src/modules/widget/schema.js +182 -181
- package/src/modules/widget/widgetTool.js +1389 -1389
- package/src/services/__tests__/agentActivityService.test.js +401 -402
- package/src/services/__tests__/benchmarkService.test.js +184 -184
- package/src/services/__tests__/contextInjectionService.test.js +246 -246
- package/src/services/__tests__/conversationQuery.test.js +721 -723
- package/src/services/__tests__/credentialVault.test.js +469 -469
- package/src/services/__tests__/discordService.integration.test.js +638 -639
- package/src/services/__tests__/flowContextService.test.js +590 -590
- package/src/services/__tests__/memoryService.test.js +1 -1
- package/src/services/__tests__/messageSource.test.js +380 -380
- package/src/services/__tests__/modelRouterNaming.test.js +111 -111
- package/src/services/__tests__/projectDetector.test.js +34 -34
- package/src/services/__tests__/promptService.test.js +242 -242
- package/src/services/__tests__/telegramService.test.js +941 -941
- package/src/services/__tests__/tokenCountingService.test.js +48 -48
- package/src/services/agentActivityService.js +419 -420
- package/src/services/aiService.js +2997 -3001
- package/src/services/apiKeyManager.js +359 -359
- package/src/services/benchmarkService.js +196 -196
- package/src/services/codebaseKnowledgeService.js +2 -2
- package/src/services/composioService.js +738 -738
- package/src/services/conversationCompactionService.js +1258 -1257
- package/src/services/credentialVault.js +685 -685
- package/src/services/discordService.js +792 -793
- package/src/services/embeddings/__tests__/azureCustomProvider.test.js +232 -232
- package/src/services/embeddings/__tests__/embeddingService.test.js +417 -417
- package/src/services/embeddings/__tests__/localProvider.test.js +263 -263
- package/src/services/embeddings/autoRecall.js +218 -219
- package/src/services/embeddings/indexers/__tests__/agentIndexer.test.js +232 -232
- package/src/services/embeddings/indexers/__tests__/memoryIndexer.test.js +418 -418
- package/src/services/embeddings/indexers/__tests__/reminisceIndexer.test.js +356 -357
- package/src/services/embeddings/indexers/__tests__/skillsIndexer.test.js +145 -145
- package/src/services/embeddings/indexers/__tests__/taskIndexer.test.js +146 -146
- package/src/services/embeddings/indexers/composioIndexer.js +279 -279
- package/src/services/embeddings/providerInterface.js +206 -206
- package/src/services/embeddings/providers/localProvider.js +11 -7
- package/src/services/embeddings/providers/openaiProvider.js +101 -101
- package/src/services/embeddings/vectorStore/inMemoryJsonStore.js +356 -356
- package/src/services/errorHandler.js +809 -809
- package/src/services/flowContextService.js +586 -586
- package/src/services/grounding/MockAdapter.js +125 -125
- package/src/services/modelRouterService.js +26 -31
- package/src/services/modelsService.js +322 -322
- package/src/services/ollamaService.js +452 -452
- package/src/services/projectDetector.js +403 -404
- package/src/services/promptService.js +418 -418
- package/src/services/qualityInspector.js +795 -795
- package/src/services/scheduleService.js +726 -726
- package/src/services/serviceRegistry.js +386 -386
- package/src/services/telegrafBot.js +174 -174
- package/src/services/telegramService.js +1972 -1972
- package/src/services/visualEditorBridge.js +1033 -1033
- package/src/services/visualEditorServer.js +1769 -1774
- package/src/services/whatsappService.js +667 -668
- package/src/tools/__tests__/agentCommunicationTool.findAgent.test.js +226 -226
- package/src/tools/__tests__/agentCommunicationTool.test.js +3 -3
- package/src/tools/__tests__/agentDelayTool.test.js +342 -342
- package/src/tools/__tests__/baseTool.test.js +3 -3
- package/src/tools/__tests__/codeMapTool.test.js +915 -915
- package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -309
- package/src/tools/__tests__/fileTreeTool.test.js +274 -274
- package/src/tools/__tests__/filesystemTool.test.js +815 -815
- package/src/tools/__tests__/foundryWebSearchTool.test.js +252 -252
- package/src/tools/__tests__/imageTool.validator.test.js +194 -194
- package/src/tools/__tests__/jobDoneTool.test.js +580 -581
- package/src/tools/__tests__/memoryTool.forgetStale.test.js +272 -272
- package/src/tools/__tests__/memoryTool.reminisce.test.js +2 -2
- package/src/tools/__tests__/memoryTool.reminisceSemanticSearch.test.js +301 -301
- package/src/tools/__tests__/memoryTool.semanticSearch.test.js +405 -405
- package/src/tools/__tests__/memoryTool.teamPool.test.js +293 -293
- package/src/tools/__tests__/memoryTool.test.js +1 -1
- package/src/tools/__tests__/seekTool.test.js +282 -282
- package/src/tools/__tests__/skillsTool.search.test.js +164 -164
- package/src/tools/__tests__/skillsTool.test.js +226 -226
- package/src/tools/__tests__/staticAnalysisTool.test.js +509 -509
- package/src/tools/__tests__/taskManagerTool.discipline.test.js +137 -137
- package/src/tools/__tests__/taskManagerTool.search.test.js +143 -143
- package/src/tools/__tests__/taskManagerTool.test.js +866 -866
- package/src/tools/__tests__/terminalTool.test.js +448 -448
- package/src/tools/__tests__/toolShapeForgiveness.test.js +259 -260
- package/src/tools/__tests__/userPromptTool.test.js +297 -297
- package/src/tools/__tests__/videoTool.jobs.test.js +147 -147
- package/src/tools/__tests__/webTool.e2e.test.js +609 -603
- package/src/tools/__tests__/webTool.unit.test.js +195 -195
- package/src/tools/__tests__/webTool.visionModel.test.js +75 -75
- package/src/tools/agentCommunicationTool.js +8 -10
- package/src/tools/agentDelayTool.js +496 -497
- package/src/tools/asyncToolManager.js +602 -603
- package/src/tools/baseTool.js +12 -11
- package/src/tools/cloneDetectionTool.js +576 -581
- package/src/tools/codeMapTool.js +0 -6
- package/src/tools/composioTool.js +617 -617
- package/src/tools/dependencyResolverTool.js +1211 -1212
- package/src/tools/desktop/DesktopTool.js +629 -638
- package/src/tools/desktop/__tests__/DesktopTool.e2e.test.js +306 -306
- package/src/tools/desktop/__tests__/DesktopTool.test.js +507 -507
- package/src/tools/desktop/__tests__/osController.test.js +364 -364
- package/src/tools/desktop/osController.js +491 -491
- package/src/tools/docxTool.js +623 -623
- package/src/tools/excelTool.js +636 -636
- package/src/tools/fileContentReplaceTool.js +5 -7
- package/src/tools/fileSystemTool.js +12 -19
- package/src/tools/fileTreeTool.js +840 -840
- package/src/tools/foundryWebSearchTool.js +273 -273
- package/src/tools/helpTool.js +198 -198
- package/src/tools/imageTool.js +1397 -1397
- package/src/tools/importAnalyzerTool.js +1056 -1056
- package/src/tools/jobDoneTool.js +495 -495
- package/src/tools/memoryTool.js +1 -1
- package/src/tools/office/pres/__tests__/presSystem.test.js +365 -365
- package/src/tools/office/pres/archetypes/agenda.js +61 -61
- package/src/tools/office/pres/archetypes/bentoGrid.js +218 -219
- package/src/tools/office/pres/archetypes/bigStat.js +140 -142
- package/src/tools/office/pres/archetypes/closing.js +70 -70
- package/src/tools/office/pres/archetypes/hero.js +70 -70
- package/src/tools/office/pres/archetypes/productHero.js +93 -94
- package/src/tools/office/pres/archetypes/table.js +73 -74
- package/src/tools/office/pres/backgrounds/orb.js +66 -66
- package/src/tools/office/pres/components.js +422 -423
- package/src/tools/officeTool.js +441 -441
- package/src/tools/pdfTool.js +625 -627
- package/src/tools/platformControlTool.js +1081 -1081
- package/src/tools/seekTool.js +917 -918
- package/src/tools/skillsTool.js +1 -1
- package/src/tools/staticAnalysisTool.js +2143 -2146
- package/src/tools/taskManagerTool.js +3324 -3324
- package/src/tools/terminalTool.js +2615 -2618
- package/src/tools/videoTool.js +1303 -1303
- package/src/tools/visionTool.js +508 -508
- package/src/tools/visualEditorTool.js +1289 -1290
- package/src/tools/webTool.js +3368 -3368
- package/src/tools/whatsappTool.js +464 -464
- package/src/types/__tests__/agent.test.js +499 -499
- package/src/types/__tests__/contextReference.test.js +606 -606
- package/src/types/__tests__/conversation.test.js +555 -555
- package/src/types/__tests__/toolCommand.test.js +584 -584
- package/src/types/contextReference.js +974 -971
- package/src/types/conversation.js +729 -729
- package/src/types/toolCommand.js +746 -746
- package/src/utilities/__tests__/attachmentValidator.test.js +80 -80
- package/src/utilities/__tests__/auditReport.test.js +328 -328
- package/src/utilities/__tests__/directoryAccessManager.test.js +388 -388
- package/src/utilities/__tests__/jsonRepair.test.js +103 -104
- package/src/utilities/__tests__/modeTransitionReasons.test.js +105 -105
- package/src/utilities/__tests__/platformUtils.test.js +80 -87
- package/src/utilities/__tests__/structuredFileValidator.test.js +261 -263
- package/src/utilities/__tests__/toolConstants.test.js +92 -94
- package/src/utilities/__tests__/useIsTouchDevice.detect.test.js +114 -114
- package/src/utilities/__tests__/webUiUtilSync.test.js +117 -117
- package/src/utilities/attachmentValidator.js +284 -288
- package/src/utilities/authCache.js.backup-1779570472481 +121 -121
- package/src/utilities/browserStealth.js +631 -630
- package/src/utilities/configManager.js +616 -617
- package/src/utilities/directoryAccessManager.js +564 -565
- package/src/utilities/fileProcessor.js +308 -307
- package/src/utilities/humanBehavior.js +454 -453
- package/src/utilities/logger.js +479 -479
- package/src/utilities/structuredFileValidator.js +696 -699
- package/src/utilities/tagParser.js +5 -10
- package/src/utilities/userDataDir.js +308 -308
- package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.js.map +0 -1
- package/node_modules/@isaacs/brace-expansion/dist/esm/index.js.map +0 -1
- package/node_modules/minipass/LICENSE +0 -15
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/LICENSE.md +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/package.json +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/package.json +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/LICENSE +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/package.json +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/package.json +0 -0
|
@@ -1,726 +1,726 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ScheduleService - Manages scheduled/recurring tasks
|
|
3
|
-
*
|
|
4
|
-
* Purpose:
|
|
5
|
-
* - Parse cron expressions and calculate next run times
|
|
6
|
-
* - Persist schedules to disk (survives restarts)
|
|
7
|
-
* - Check and trigger due schedules each tick
|
|
8
|
-
* - Push prompts to agents (switching to agent mode) or execute flows
|
|
9
|
-
* - Sync with OS scheduler (crontab/schtasks) for execution when system is down
|
|
10
|
-
* - Track execution history
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { promises as fs } from 'fs';
|
|
14
|
-
import path from 'path';
|
|
15
|
-
import { execSync } from 'child_process';
|
|
16
|
-
import { getUserDataDir } from '../utilities/userDataDir.js';
|
|
17
|
-
import { resolveModuleFilename } from '../utilities/esmCjsPath.js';
|
|
18
|
-
|
|
19
|
-
// ========================
|
|
20
|
-
// Lightweight cron parser (no external dependency)
|
|
21
|
-
// Supports: minute hour dayOfMonth month dayOfWeek
|
|
22
|
-
// Fields: 0-59 0-23 1-31 1-12 0-6 (Sun=0)
|
|
23
|
-
// Supports: *, */n, n, n-m, n,m,o
|
|
24
|
-
// ========================
|
|
25
|
-
|
|
26
|
-
function parseCronField(field, min, max) {
|
|
27
|
-
const values = new Set();
|
|
28
|
-
|
|
29
|
-
for (const part of field.split(',')) {
|
|
30
|
-
if (part === '*') {
|
|
31
|
-
for (let i = min; i <= max; i++) values.add(i);
|
|
32
|
-
} else if (part.includes('/')) {
|
|
33
|
-
const [range, stepStr] = part.split('/');
|
|
34
|
-
const step = parseInt(stepStr, 10);
|
|
35
|
-
let start = min;
|
|
36
|
-
let end = max;
|
|
37
|
-
if (range !== '*') {
|
|
38
|
-
if (range.includes('-')) {
|
|
39
|
-
[start, end] = range.split('-').map(Number);
|
|
40
|
-
} else {
|
|
41
|
-
start = parseInt(range, 10);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
for (let i = start; i <= end; i += step) values.add(i);
|
|
45
|
-
} else if (part.includes('-')) {
|
|
46
|
-
const [a, b] = part.split('-').map(Number);
|
|
47
|
-
for (let i = a; i <= b; i++) values.add(i);
|
|
48
|
-
} else {
|
|
49
|
-
values.add(parseInt(part, 10));
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return values;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function parseCron(expression) {
|
|
57
|
-
const parts = expression.trim().split(/\s+/);
|
|
58
|
-
if (parts.length !== 5) {
|
|
59
|
-
throw new Error(`Invalid cron expression: "${expression}" — expected 5 fields (minute hour dayOfMonth month dayOfWeek)`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
minutes: parseCronField(parts[0], 0, 59),
|
|
64
|
-
hours: parseCronField(parts[1], 0, 23),
|
|
65
|
-
daysOfMonth: parseCronField(parts[2], 1, 31),
|
|
66
|
-
months: parseCronField(parts[3], 1, 12),
|
|
67
|
-
daysOfWeek: parseCronField(parts[4], 0, 6)
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function cronMatchesDate(parsed, date) {
|
|
72
|
-
return (
|
|
73
|
-
parsed.minutes.has(date.getMinutes()) &&
|
|
74
|
-
parsed.hours.has(date.getHours()) &&
|
|
75
|
-
parsed.daysOfMonth.has(date.getDate()) &&
|
|
76
|
-
parsed.months.has(date.getMonth() + 1) &&
|
|
77
|
-
parsed.daysOfWeek.has(date.getDay())
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function getNextCronDate(parsed, after = new Date()) {
|
|
82
|
-
// Walk forward minute by minute (max 2 years)
|
|
83
|
-
const limit = 2 * 365 * 24 * 60;
|
|
84
|
-
const d = new Date(after);
|
|
85
|
-
d.setSeconds(0, 0);
|
|
86
|
-
d.setMinutes(d.getMinutes() + 1);
|
|
87
|
-
|
|
88
|
-
for (let i = 0; i < limit; i++) {
|
|
89
|
-
if (cronMatchesDate(parsed, d)) return new Date(d);
|
|
90
|
-
d.setMinutes(d.getMinutes() + 1);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ========================
|
|
97
|
-
// Predefined presets for user-friendly schedule creation
|
|
98
|
-
// ========================
|
|
99
|
-
|
|
100
|
-
const CRON_PRESETS = {
|
|
101
|
-
'every-minute': '* * * * *',
|
|
102
|
-
'every-5-minutes': '*/5 * * * *',
|
|
103
|
-
'every-15-minutes': '*/15 * * * *',
|
|
104
|
-
'every-30-minutes': '*/30 * * * *',
|
|
105
|
-
'every-hour': '0 * * * *',
|
|
106
|
-
'every-6-hours': '0 */6 * * *',
|
|
107
|
-
'every-12-hours': '0 */12 * * *',
|
|
108
|
-
'daily': '0 9 * * *',
|
|
109
|
-
'daily-morning': '0 8 * * *',
|
|
110
|
-
'daily-evening': '0 18 * * *',
|
|
111
|
-
'weekdays': '0 9 * * 1-5',
|
|
112
|
-
'weekends': '0 10 * * 0,6',
|
|
113
|
-
'weekly-monday': '0 9 * * 1',
|
|
114
|
-
'monthly': '0 9 1 * *'
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
class ScheduleService {
|
|
118
|
-
constructor(logger) {
|
|
119
|
-
this.logger = logger;
|
|
120
|
-
this.schedules = new Map(); // id → schedule object
|
|
121
|
-
this.configPath = path.join(getUserDataDir(), 'schedules.json');
|
|
122
|
-
this.checkIntervalMs = 30000; // Check every 30 seconds
|
|
123
|
-
this.checkTimer = null;
|
|
124
|
-
|
|
125
|
-
// Dependencies (injected)
|
|
126
|
-
this.agentPool = null;
|
|
127
|
-
this.messageProcessor = null;
|
|
128
|
-
this.flowExecutor = null;
|
|
129
|
-
this.webSocketManager = null;
|
|
130
|
-
this.orchestrator = null;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ========================
|
|
134
|
-
// Dependency injection
|
|
135
|
-
// ========================
|
|
136
|
-
|
|
137
|
-
setAgentPool(agentPool) { this.agentPool = agentPool; }
|
|
138
|
-
setMessageProcessor(messageProcessor) { this.messageProcessor = messageProcessor; }
|
|
139
|
-
setFlowExecutor(flowExecutor) { this.flowExecutor = flowExecutor; }
|
|
140
|
-
setWebSocketManager(wsManager) { this.webSocketManager = wsManager; }
|
|
141
|
-
setOrchestrator(orchestrator) { this.orchestrator = orchestrator; }
|
|
142
|
-
|
|
143
|
-
// ========================
|
|
144
|
-
// Lifecycle
|
|
145
|
-
// ========================
|
|
146
|
-
|
|
147
|
-
async initialize() {
|
|
148
|
-
await this._loadSchedules();
|
|
149
|
-
this.logger.info('ScheduleService initialized', { scheduleCount: this.schedules.size });
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
start() {
|
|
153
|
-
if (this.checkTimer) return;
|
|
154
|
-
|
|
155
|
-
this.checkTimer = setInterval(() => {
|
|
156
|
-
this._tick().catch(err =>
|
|
157
|
-
this.logger.error('Schedule tick error', { error: err.message })
|
|
158
|
-
);
|
|
159
|
-
}, this.checkIntervalMs);
|
|
160
|
-
|
|
161
|
-
this.logger.info('ScheduleService started', { intervalMs: this.checkIntervalMs });
|
|
162
|
-
|
|
163
|
-
// Run an immediate check for any missed schedules
|
|
164
|
-
this._tick().catch(err =>
|
|
165
|
-
this.logger.error('Initial schedule tick error', { error: err.message })
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
// Sync all schedules to OS scheduler (non-blocking)
|
|
169
|
-
this.syncAllToOS().catch(err =>
|
|
170
|
-
this.logger.warn('Failed to sync schedules to OS on startup', { error: err.message })
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
stop() {
|
|
175
|
-
if (this.checkTimer) {
|
|
176
|
-
clearInterval(this.checkTimer);
|
|
177
|
-
this.checkTimer = null;
|
|
178
|
-
this.logger.info('ScheduleService stopped');
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ========================
|
|
183
|
-
// CRUD
|
|
184
|
-
// ========================
|
|
185
|
-
|
|
186
|
-
async createSchedule(config) {
|
|
187
|
-
const {
|
|
188
|
-
name,
|
|
189
|
-
prompt,
|
|
190
|
-
targetType, // 'agent' or 'flow'
|
|
191
|
-
targetId, // agent ID or flow ID
|
|
192
|
-
cronExpression, // raw cron or preset name
|
|
193
|
-
enabled = true,
|
|
194
|
-
description = '',
|
|
195
|
-
startDate = null, // ISO date string — schedule starts after this date
|
|
196
|
-
endDate = null, // ISO date string — schedule stops after this date
|
|
197
|
-
maxRuns = null, // number — auto-disable after N executions (null = unlimited)
|
|
198
|
-
runOnce = false // boolean — run once then auto-disable
|
|
199
|
-
} = config;
|
|
200
|
-
|
|
201
|
-
if (!name) throw new Error('Schedule name is required');
|
|
202
|
-
if (!prompt) throw new Error('Prompt is required');
|
|
203
|
-
if (!targetType || !['agent', 'flow'].includes(targetType)) {
|
|
204
|
-
throw new Error('targetType must be "agent" or "flow"');
|
|
205
|
-
}
|
|
206
|
-
if (!targetId) throw new Error('targetId is required');
|
|
207
|
-
if (!cronExpression) throw new Error('cronExpression is required');
|
|
208
|
-
|
|
209
|
-
// Resolve preset or validate cron
|
|
210
|
-
const resolvedCron = CRON_PRESETS[cronExpression] || cronExpression;
|
|
211
|
-
const parsed = parseCron(resolvedCron); // throws if invalid
|
|
212
|
-
const nextRun = getNextCronDate(parsed);
|
|
213
|
-
|
|
214
|
-
const id = `schedule-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
|
|
215
|
-
|
|
216
|
-
const schedule = {
|
|
217
|
-
id,
|
|
218
|
-
name,
|
|
219
|
-
description,
|
|
220
|
-
prompt,
|
|
221
|
-
targetType,
|
|
222
|
-
targetId,
|
|
223
|
-
cronExpression: resolvedCron,
|
|
224
|
-
cronPreset: CRON_PRESETS[cronExpression] ? cronExpression : null,
|
|
225
|
-
enabled,
|
|
226
|
-
startDate: startDate || null,
|
|
227
|
-
endDate: endDate || null,
|
|
228
|
-
maxRuns: maxRuns != null ? parseInt(maxRuns) : null,
|
|
229
|
-
runOnce: !!runOnce,
|
|
230
|
-
nextRun: nextRun ? nextRun.toISOString() : null,
|
|
231
|
-
lastRun: null,
|
|
232
|
-
lastRunStatus: null,
|
|
233
|
-
runCount: 0,
|
|
234
|
-
createdAt: new Date().toISOString(),
|
|
235
|
-
updatedAt: new Date().toISOString()
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
this.schedules.set(id, schedule);
|
|
239
|
-
await this._saveSchedules();
|
|
240
|
-
|
|
241
|
-
// Sync to OS scheduler (non-blocking, non-fatal)
|
|
242
|
-
this._syncToOS(schedule).catch(() => {});
|
|
243
|
-
|
|
244
|
-
this._broadcast('schedule_created', schedule);
|
|
245
|
-
this.logger.info('Schedule created', { id, name, cron: resolvedCron, targetType, targetId });
|
|
246
|
-
|
|
247
|
-
return schedule;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
async updateSchedule(id, updates) {
|
|
251
|
-
const schedule = this.schedules.get(id);
|
|
252
|
-
if (!schedule) throw new Error(`Schedule not found: ${id}`);
|
|
253
|
-
|
|
254
|
-
// If cron changed, reparse and recalculate next run
|
|
255
|
-
if (updates.cronExpression) {
|
|
256
|
-
const resolvedCron = CRON_PRESETS[updates.cronExpression] || updates.cronExpression;
|
|
257
|
-
parseCron(resolvedCron); // validate
|
|
258
|
-
updates.cronExpression = resolvedCron;
|
|
259
|
-
updates.cronPreset = CRON_PRESETS[updates.cronExpression] ? updates.cronExpression : null;
|
|
260
|
-
const parsed = parseCron(resolvedCron);
|
|
261
|
-
updates.nextRun = getNextCronDate(parsed)?.toISOString() || null;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// If re-enabled, recalculate next run
|
|
265
|
-
if (updates.enabled === true && !schedule.enabled) {
|
|
266
|
-
const parsed = parseCron(updates.cronExpression || schedule.cronExpression);
|
|
267
|
-
updates.nextRun = getNextCronDate(parsed)?.toISOString() || null;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const allowedFields = ['name', 'description', 'prompt', 'targetType', 'targetId', 'cronExpression', 'cronPreset', 'enabled', 'nextRun'];
|
|
271
|
-
for (const key of allowedFields) {
|
|
272
|
-
if (updates[key] !== undefined) {
|
|
273
|
-
schedule[key] = updates[key];
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
schedule.updatedAt = new Date().toISOString();
|
|
277
|
-
|
|
278
|
-
await this._saveSchedules();
|
|
279
|
-
|
|
280
|
-
// Sync to OS scheduler (non-blocking, non-fatal)
|
|
281
|
-
this._syncToOS(schedule).catch(() => {});
|
|
282
|
-
|
|
283
|
-
this._broadcast('schedule_updated', schedule);
|
|
284
|
-
this.logger.info('Schedule updated', { id, updates: Object.keys(updates) });
|
|
285
|
-
|
|
286
|
-
return schedule;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async deleteSchedule(id) {
|
|
290
|
-
if (!this.schedules.has(id)) throw new Error(`Schedule not found: ${id}`);
|
|
291
|
-
|
|
292
|
-
// Remove from OS scheduler first
|
|
293
|
-
this._removeFromOS(id).catch(() => {});
|
|
294
|
-
|
|
295
|
-
this.schedules.delete(id);
|
|
296
|
-
await this._saveSchedules();
|
|
297
|
-
|
|
298
|
-
this._broadcast('schedule_deleted', { id });
|
|
299
|
-
this.logger.info('Schedule deleted', { id });
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
getSchedule(id) {
|
|
303
|
-
return this.schedules.get(id) || null;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
listSchedules() {
|
|
307
|
-
return Array.from(this.schedules.values()).sort((a, b) =>
|
|
308
|
-
new Date(b.createdAt) - new Date(a.createdAt)
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
getPresets() {
|
|
313
|
-
return { ...CRON_PRESETS };
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// ========================
|
|
317
|
-
// Tick — check and fire due schedules
|
|
318
|
-
// ========================
|
|
319
|
-
|
|
320
|
-
async _tick() {
|
|
321
|
-
const now = new Date();
|
|
322
|
-
|
|
323
|
-
for (const schedule of this.schedules.values()) {
|
|
324
|
-
if (!schedule.enabled || !schedule.nextRun) continue;
|
|
325
|
-
|
|
326
|
-
// Check if schedule has expired (endDate passed)
|
|
327
|
-
if (schedule.endDate && now > new Date(schedule.endDate)) {
|
|
328
|
-
schedule.enabled = false;
|
|
329
|
-
schedule.nextRun = null;
|
|
330
|
-
schedule.updatedAt = now.toISOString();
|
|
331
|
-
this._broadcast('schedule_updated', schedule);
|
|
332
|
-
this.logger.info('Schedule expired (end date reached)', { id: schedule.id, name: schedule.name });
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Check if start date hasn't arrived yet
|
|
337
|
-
if (schedule.startDate && now < new Date(schedule.startDate)) {
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const nextRun = new Date(schedule.nextRun);
|
|
342
|
-
if (now >= nextRun) {
|
|
343
|
-
// Fire!
|
|
344
|
-
await this._executeSchedule(schedule);
|
|
345
|
-
|
|
346
|
-
schedule.lastRun = now.toISOString();
|
|
347
|
-
schedule.runCount++;
|
|
348
|
-
schedule.updatedAt = now.toISOString();
|
|
349
|
-
|
|
350
|
-
// Check if schedule should auto-disable (runOnce or maxRuns reached)
|
|
351
|
-
if (schedule.runOnce || (schedule.maxRuns != null && schedule.runCount >= schedule.maxRuns)) {
|
|
352
|
-
schedule.enabled = false;
|
|
353
|
-
schedule.nextRun = null;
|
|
354
|
-
this._broadcast('schedule_updated', schedule);
|
|
355
|
-
this.logger.info('Schedule auto-disabled', {
|
|
356
|
-
id: schedule.id, name: schedule.name,
|
|
357
|
-
reason: schedule.runOnce ? 'run-once' : `maxRuns (${schedule.maxRuns}) reached`
|
|
358
|
-
});
|
|
359
|
-
} else {
|
|
360
|
-
// Calculate next run
|
|
361
|
-
const parsed = parseCron(schedule.cronExpression);
|
|
362
|
-
const next = getNextCronDate(parsed, now);
|
|
363
|
-
schedule.nextRun = next ? next.toISOString() : null;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
await this._saveSchedules();
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
async _executeSchedule(schedule) {
|
|
372
|
-
this.logger.info('Executing scheduled task', {
|
|
373
|
-
id: schedule.id,
|
|
374
|
-
name: schedule.name,
|
|
375
|
-
targetType: schedule.targetType,
|
|
376
|
-
targetId: schedule.targetId
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
this._broadcast('schedule_triggered', {
|
|
380
|
-
id: schedule.id,
|
|
381
|
-
name: schedule.name,
|
|
382
|
-
targetType: schedule.targetType,
|
|
383
|
-
targetId: schedule.targetId,
|
|
384
|
-
triggeredAt: new Date().toISOString()
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
try {
|
|
388
|
-
if (schedule.targetType === 'agent') {
|
|
389
|
-
await this._executeAgentSchedule(schedule);
|
|
390
|
-
schedule.lastRunStatus = 'success';
|
|
391
|
-
} else if (schedule.targetType === 'flow') {
|
|
392
|
-
await this._executeFlowSchedule(schedule);
|
|
393
|
-
schedule.lastRunStatus = 'success';
|
|
394
|
-
}
|
|
395
|
-
} catch (error) {
|
|
396
|
-
schedule.lastRunStatus = 'error';
|
|
397
|
-
schedule.lastRunError = error.message;
|
|
398
|
-
this.logger.error('Scheduled task execution failed', {
|
|
399
|
-
id: schedule.id,
|
|
400
|
-
error: error.message
|
|
401
|
-
});
|
|
402
|
-
this._broadcast('schedule_error', {
|
|
403
|
-
id: schedule.id,
|
|
404
|
-
name: schedule.name,
|
|
405
|
-
error: error.message
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
async _executeAgentSchedule(schedule) {
|
|
411
|
-
if (!this.agentPool) throw new Error('AgentPool not available');
|
|
412
|
-
|
|
413
|
-
const agent = await this.agentPool.getAgent(schedule.targetId);
|
|
414
|
-
if (!agent) throw new Error(`Target agent not found: ${schedule.targetId}`);
|
|
415
|
-
|
|
416
|
-
// Switch agent to agent mode (autonomous)
|
|
417
|
-
const { AGENT_MODES } = await import('../utilities/constants.js');
|
|
418
|
-
|
|
419
|
-
// Get a session ID — use the first WebSocket connection or a synthetic one
|
|
420
|
-
let sessionId = `schedule-${schedule.id}-${Date.now()}`;
|
|
421
|
-
if (this.webSocketManager?.connections) {
|
|
422
|
-
for (const conn of this.webSocketManager.connections.values()) {
|
|
423
|
-
if (conn.sessionId) {
|
|
424
|
-
sessionId = conn.sessionId;
|
|
425
|
-
break;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Switch to agent mode
|
|
431
|
-
await this.agentPool.updateAgent(schedule.targetId, {
|
|
432
|
-
mode: AGENT_MODES.AGENT,
|
|
433
|
-
sessionId
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
// Push the prompt as a user message
|
|
437
|
-
await this.agentPool.addUserMessage(schedule.targetId, {
|
|
438
|
-
role: 'user',
|
|
439
|
-
content: `[Scheduled Task: ${schedule.name}]\n\n${schedule.prompt}`,
|
|
440
|
-
timestamp: new Date().toISOString(),
|
|
441
|
-
source: 'schedule',
|
|
442
|
-
scheduleId: schedule.id
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
this.logger.info('Prompt pushed to agent via schedule', {
|
|
446
|
-
scheduleId: schedule.id,
|
|
447
|
-
agentId: schedule.targetId,
|
|
448
|
-
agentName: agent.name
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
async _executeFlowSchedule(schedule) {
|
|
453
|
-
if (!this.flowExecutor) throw new Error('FlowExecutor not available');
|
|
454
|
-
|
|
455
|
-
let sessionId = `schedule-${schedule.id}-${Date.now()}`;
|
|
456
|
-
if (this.webSocketManager?.connections) {
|
|
457
|
-
for (const conn of this.webSocketManager.connections.values()) {
|
|
458
|
-
if (conn.sessionId) {
|
|
459
|
-
sessionId = conn.sessionId;
|
|
460
|
-
break;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
await this.flowExecutor.executeFlow(
|
|
466
|
-
schedule.targetId,
|
|
467
|
-
{ userInput: schedule.prompt, source: 'schedule', scheduleId: schedule.id },
|
|
468
|
-
sessionId
|
|
469
|
-
);
|
|
470
|
-
|
|
471
|
-
this.logger.info('Flow executed via schedule', {
|
|
472
|
-
scheduleId: schedule.id,
|
|
473
|
-
flowId: schedule.targetId
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// ========================
|
|
478
|
-
// Persistence
|
|
479
|
-
// ========================
|
|
480
|
-
|
|
481
|
-
async _loadSchedules() {
|
|
482
|
-
try {
|
|
483
|
-
const data = await fs.readFile(this.configPath, 'utf-8');
|
|
484
|
-
const arr = JSON.parse(data);
|
|
485
|
-
this.schedules.clear();
|
|
486
|
-
for (const s of arr) {
|
|
487
|
-
this.schedules.set(s.id, s);
|
|
488
|
-
}
|
|
489
|
-
this.logger.info('Schedules loaded from disk', { count: arr.length });
|
|
490
|
-
} catch (err) {
|
|
491
|
-
if (err.code !== 'ENOENT') {
|
|
492
|
-
this.logger.warn('Failed to load schedules', { error: err.message });
|
|
493
|
-
}
|
|
494
|
-
// First run or corrupted — start fresh
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
async _saveSchedules() {
|
|
499
|
-
try {
|
|
500
|
-
const dir = path.dirname(this.configPath);
|
|
501
|
-
await fs.mkdir(dir, { recursive: true });
|
|
502
|
-
const arr = Array.from(this.schedules.values());
|
|
503
|
-
await fs.writeFile(this.configPath, JSON.stringify(arr, null, 2), 'utf-8');
|
|
504
|
-
} catch (err) {
|
|
505
|
-
this.logger.error('Failed to save schedules', { error: err.message });
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// ========================
|
|
510
|
-
// WebSocket broadcasting
|
|
511
|
-
// ========================
|
|
512
|
-
|
|
513
|
-
_broadcast(type, data) {
|
|
514
|
-
if (this.webSocketManager?.broadcast) {
|
|
515
|
-
this.webSocketManager.broadcast({ type, ...data });
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// ========================
|
|
520
|
-
// OS Scheduler Sync
|
|
521
|
-
// Registers/unregisters cron jobs (Linux/macOS) or schtasks (Windows)
|
|
522
|
-
// so schedules fire even when Loxia is shut down.
|
|
523
|
-
// ========================
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* Find the loxia CLI binary path
|
|
527
|
-
*/
|
|
528
|
-
_findLoxiaBin() {
|
|
529
|
-
try {
|
|
530
|
-
// Check if installed globally
|
|
531
|
-
const which = process.platform === 'win32' ? 'where loxia' : 'which loxia';
|
|
532
|
-
return execSync(which, { encoding: 'utf-8' }).trim().split('\n')[0];
|
|
533
|
-
} catch {
|
|
534
|
-
// Fallback to local bin
|
|
535
|
-
const __dirname = path.dirname(resolveModuleFilename(import.meta.url));
|
|
536
|
-
const localBin = path.resolve(__dirname, '../../bin/cli.js');
|
|
537
|
-
return `node "${localBin}"`;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/**
|
|
542
|
-
* Sync a single schedule to the OS scheduler.
|
|
543
|
-
* Called after create/update.
|
|
544
|
-
*/
|
|
545
|
-
async _syncToOS(schedule) {
|
|
546
|
-
if (!schedule.enabled) {
|
|
547
|
-
await this._removeFromOS(schedule.id);
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
try {
|
|
552
|
-
const loxiaBin = this._findLoxiaBin();
|
|
553
|
-
const command = `${loxiaBin} trigger-schedule ${schedule.id}`;
|
|
554
|
-
|
|
555
|
-
if (process.platform === 'win32') {
|
|
556
|
-
await this._syncToWindows(schedule, command);
|
|
557
|
-
} else {
|
|
558
|
-
await this._syncToCrontab(schedule, command);
|
|
559
|
-
}
|
|
560
|
-
} catch (err) {
|
|
561
|
-
// Non-fatal — internal scheduler still works as fallback
|
|
562
|
-
this.logger.warn('Failed to sync schedule to OS scheduler', {
|
|
563
|
-
id: schedule.id,
|
|
564
|
-
error: err.message
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Remove a schedule from the OS scheduler.
|
|
571
|
-
* Called after delete or disable.
|
|
572
|
-
*/
|
|
573
|
-
async _removeFromOS(scheduleId) {
|
|
574
|
-
try {
|
|
575
|
-
if (process.platform === 'win32') {
|
|
576
|
-
this._removeFromWindows(scheduleId);
|
|
577
|
-
} else {
|
|
578
|
-
this._removeFromCrontab(scheduleId);
|
|
579
|
-
}
|
|
580
|
-
} catch (err) {
|
|
581
|
-
this.logger.warn('Failed to remove schedule from OS scheduler', {
|
|
582
|
-
id: scheduleId,
|
|
583
|
-
error: err.message
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* Sync all enabled schedules to OS. Called on startup.
|
|
590
|
-
*/
|
|
591
|
-
async syncAllToOS() {
|
|
592
|
-
for (const schedule of this.schedules.values()) {
|
|
593
|
-
await this._syncToOS(schedule);
|
|
594
|
-
}
|
|
595
|
-
this.logger.info('Synced all schedules to OS scheduler');
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// --- Linux/macOS: crontab ---
|
|
599
|
-
|
|
600
|
-
_syncToCrontab(schedule, command) {
|
|
601
|
-
const tag = `# loxia-schedule:${schedule.id}`;
|
|
602
|
-
|
|
603
|
-
// Read current crontab
|
|
604
|
-
let existing = '';
|
|
605
|
-
try {
|
|
606
|
-
existing = execSync('crontab -l 2>/dev/null', { encoding: 'utf-8' });
|
|
607
|
-
} catch {
|
|
608
|
-
// No crontab yet
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// Remove old entry for this schedule
|
|
612
|
-
const lines = existing.split('\n').filter(l => !l.includes(`loxia-schedule:${schedule.id}`));
|
|
613
|
-
|
|
614
|
-
// Add new entry
|
|
615
|
-
lines.push(`${schedule.cronExpression} ${command} ${tag}`);
|
|
616
|
-
|
|
617
|
-
// Write back
|
|
618
|
-
const newCrontab = lines.filter(l => l.trim() !== '').join('\n') + '\n';
|
|
619
|
-
execSync('crontab -', { input: newCrontab, encoding: 'utf-8' });
|
|
620
|
-
|
|
621
|
-
this.logger.info('Synced schedule to crontab', { id: schedule.id, cron: schedule.cronExpression });
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
_removeFromCrontab(scheduleId) {
|
|
625
|
-
let existing
|
|
626
|
-
try {
|
|
627
|
-
existing = execSync('crontab -l 2>/dev/null', { encoding: 'utf-8' });
|
|
628
|
-
} catch {
|
|
629
|
-
return; // No crontab
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
const lines = existing.split('\n').filter(l => !l.includes(`loxia-schedule:${scheduleId}`));
|
|
633
|
-
const newCrontab = lines.filter(l => l.trim() !== '').join('\n') + '\n';
|
|
634
|
-
execSync('crontab -', { input: newCrontab, encoding: 'utf-8' });
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// --- Windows: schtasks ---
|
|
638
|
-
|
|
639
|
-
_syncToWindows(schedule, command) {
|
|
640
|
-
const taskName = `LoxiaSchedule_${schedule.id}`;
|
|
641
|
-
|
|
642
|
-
// Remove existing task if any
|
|
643
|
-
try {
|
|
644
|
-
execSync(`schtasks /Delete /TN "${taskName}" /F 2>nul`, { encoding: 'utf-8' });
|
|
645
|
-
} catch {
|
|
646
|
-
// Task may not exist
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// schtasks doesn't support cron directly — we map common patterns
|
|
650
|
-
// For complex crons, we rely on the internal scheduler as fallback
|
|
651
|
-
const scArgs = this._cronToSchtasksArgs(schedule.cronExpression);
|
|
652
|
-
if (!scArgs) {
|
|
653
|
-
this.logger.warn('Complex cron not supported by Windows Task Scheduler, using internal scheduler only', {
|
|
654
|
-
id: schedule.id,
|
|
655
|
-
cron: schedule.cronExpression
|
|
656
|
-
});
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
execSync(
|
|
661
|
-
`schtasks /Create /TN "${taskName}" /TR "${command}" ${scArgs} /F`,
|
|
662
|
-
{ encoding: 'utf-8' }
|
|
663
|
-
);
|
|
664
|
-
|
|
665
|
-
this.logger.info('Synced schedule to Windows Task Scheduler', { id: schedule.id, taskName });
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
_removeFromWindows(scheduleId) {
|
|
669
|
-
const taskName = `LoxiaSchedule_${scheduleId}`;
|
|
670
|
-
try {
|
|
671
|
-
execSync(`schtasks /Delete /TN "${taskName}" /F 2>nul`, { encoding: 'utf-8' });
|
|
672
|
-
} catch {
|
|
673
|
-
// Task may not exist
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* Convert simple cron patterns to schtasks /SC arguments.
|
|
679
|
-
* Returns null for complex patterns that schtasks can't express.
|
|
680
|
-
*/
|
|
681
|
-
_cronToSchtasksArgs(cron) {
|
|
682
|
-
const parts = cron.trim().split(/\s+/);
|
|
683
|
-
const [min, hour, dom, mon, dow] = parts;
|
|
684
|
-
|
|
685
|
-
// Every N minutes: */N * * * *
|
|
686
|
-
if (min.startsWith('*/') && hour === '*' && dom === '*' && mon === '*' && dow === '*') {
|
|
687
|
-
const n = parseInt(min.slice(2), 10);
|
|
688
|
-
return `/SC MINUTE /MO ${n}`;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// Every hour at minute M: M * * * *
|
|
692
|
-
if (/^\d+$/.test(min) && hour === '*' && dom === '*' && mon === '*' && dow === '*') {
|
|
693
|
-
return `/SC HOURLY /ST 00:${min.padStart(2, '0')}`;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// Daily at H:M: M H * * *
|
|
697
|
-
if (/^\d+$/.test(min) && /^\d+$/.test(hour) && dom === '*' && mon === '*' && dow === '*') {
|
|
698
|
-
return `/SC DAILY /ST ${hour.padStart(2, '0')}:${min.padStart(2, '0')}`;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// Weekly on specific days: M H * * D
|
|
702
|
-
if (/^\d+$/.test(min) && /^\d+$/.test(hour) && dom === '*' && mon === '*' && /^[\d,-]+$/.test(dow)) {
|
|
703
|
-
const dayMap = { 0: 'SUN', 1: 'MON', 2: 'TUE', 3: 'WED', 4: 'THU', 5: 'FRI', 6: 'SAT' };
|
|
704
|
-
const days = dow.split(',').flatMap(part => {
|
|
705
|
-
if (part.includes('-')) {
|
|
706
|
-
const [a, b] = part.split('-').map(Number);
|
|
707
|
-
const result = [];
|
|
708
|
-
for (let i = a; i <= b; i++) result.push(dayMap[i] || '');
|
|
709
|
-
return result;
|
|
710
|
-
}
|
|
711
|
-
return [dayMap[parseInt(part, 10)] || ''];
|
|
712
|
-
}).filter(Boolean).join(',');
|
|
713
|
-
if (days) return `/SC WEEKLY /D ${days} /ST ${hour.padStart(2, '0')}:${min.padStart(2, '0')}`;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// Monthly on specific day: M H D * *
|
|
717
|
-
if (/^\d+$/.test(min) && /^\d+$/.test(hour) && /^\d+$/.test(dom) && mon === '*' && dow === '*') {
|
|
718
|
-
return `/SC MONTHLY /D ${dom} /ST ${hour.padStart(2, '0')}:${min.padStart(2, '0')}`;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
return null; // Complex pattern — fallback to internal scheduler
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
export default ScheduleService;
|
|
726
|
-
export { CRON_PRESETS, parseCron, cronMatchesDate, getNextCronDate };
|
|
1
|
+
/**
|
|
2
|
+
* ScheduleService - Manages scheduled/recurring tasks
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* - Parse cron expressions and calculate next run times
|
|
6
|
+
* - Persist schedules to disk (survives restarts)
|
|
7
|
+
* - Check and trigger due schedules each tick
|
|
8
|
+
* - Push prompts to agents (switching to agent mode) or execute flows
|
|
9
|
+
* - Sync with OS scheduler (crontab/schtasks) for execution when system is down
|
|
10
|
+
* - Track execution history
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { promises as fs } from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { execSync } from 'child_process';
|
|
16
|
+
import { getUserDataDir } from '../utilities/userDataDir.js';
|
|
17
|
+
import { resolveModuleFilename } from '../utilities/esmCjsPath.js';
|
|
18
|
+
|
|
19
|
+
// ========================
|
|
20
|
+
// Lightweight cron parser (no external dependency)
|
|
21
|
+
// Supports: minute hour dayOfMonth month dayOfWeek
|
|
22
|
+
// Fields: 0-59 0-23 1-31 1-12 0-6 (Sun=0)
|
|
23
|
+
// Supports: *, */n, n, n-m, n,m,o
|
|
24
|
+
// ========================
|
|
25
|
+
|
|
26
|
+
function parseCronField(field, min, max) {
|
|
27
|
+
const values = new Set();
|
|
28
|
+
|
|
29
|
+
for (const part of field.split(',')) {
|
|
30
|
+
if (part === '*') {
|
|
31
|
+
for (let i = min; i <= max; i++) values.add(i);
|
|
32
|
+
} else if (part.includes('/')) {
|
|
33
|
+
const [range, stepStr] = part.split('/');
|
|
34
|
+
const step = parseInt(stepStr, 10);
|
|
35
|
+
let start = min;
|
|
36
|
+
let end = max;
|
|
37
|
+
if (range !== '*') {
|
|
38
|
+
if (range.includes('-')) {
|
|
39
|
+
[start, end] = range.split('-').map(Number);
|
|
40
|
+
} else {
|
|
41
|
+
start = parseInt(range, 10);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (let i = start; i <= end; i += step) values.add(i);
|
|
45
|
+
} else if (part.includes('-')) {
|
|
46
|
+
const [a, b] = part.split('-').map(Number);
|
|
47
|
+
for (let i = a; i <= b; i++) values.add(i);
|
|
48
|
+
} else {
|
|
49
|
+
values.add(parseInt(part, 10));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return values;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseCron(expression) {
|
|
57
|
+
const parts = expression.trim().split(/\s+/);
|
|
58
|
+
if (parts.length !== 5) {
|
|
59
|
+
throw new Error(`Invalid cron expression: "${expression}" — expected 5 fields (minute hour dayOfMonth month dayOfWeek)`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
minutes: parseCronField(parts[0], 0, 59),
|
|
64
|
+
hours: parseCronField(parts[1], 0, 23),
|
|
65
|
+
daysOfMonth: parseCronField(parts[2], 1, 31),
|
|
66
|
+
months: parseCronField(parts[3], 1, 12),
|
|
67
|
+
daysOfWeek: parseCronField(parts[4], 0, 6)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cronMatchesDate(parsed, date) {
|
|
72
|
+
return (
|
|
73
|
+
parsed.minutes.has(date.getMinutes()) &&
|
|
74
|
+
parsed.hours.has(date.getHours()) &&
|
|
75
|
+
parsed.daysOfMonth.has(date.getDate()) &&
|
|
76
|
+
parsed.months.has(date.getMonth() + 1) &&
|
|
77
|
+
parsed.daysOfWeek.has(date.getDay())
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getNextCronDate(parsed, after = new Date()) {
|
|
82
|
+
// Walk forward minute by minute (max 2 years)
|
|
83
|
+
const limit = 2 * 365 * 24 * 60;
|
|
84
|
+
const d = new Date(after);
|
|
85
|
+
d.setSeconds(0, 0);
|
|
86
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < limit; i++) {
|
|
89
|
+
if (cronMatchesDate(parsed, d)) return new Date(d);
|
|
90
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ========================
|
|
97
|
+
// Predefined presets for user-friendly schedule creation
|
|
98
|
+
// ========================
|
|
99
|
+
|
|
100
|
+
const CRON_PRESETS = {
|
|
101
|
+
'every-minute': '* * * * *',
|
|
102
|
+
'every-5-minutes': '*/5 * * * *',
|
|
103
|
+
'every-15-minutes': '*/15 * * * *',
|
|
104
|
+
'every-30-minutes': '*/30 * * * *',
|
|
105
|
+
'every-hour': '0 * * * *',
|
|
106
|
+
'every-6-hours': '0 */6 * * *',
|
|
107
|
+
'every-12-hours': '0 */12 * * *',
|
|
108
|
+
'daily': '0 9 * * *',
|
|
109
|
+
'daily-morning': '0 8 * * *',
|
|
110
|
+
'daily-evening': '0 18 * * *',
|
|
111
|
+
'weekdays': '0 9 * * 1-5',
|
|
112
|
+
'weekends': '0 10 * * 0,6',
|
|
113
|
+
'weekly-monday': '0 9 * * 1',
|
|
114
|
+
'monthly': '0 9 1 * *'
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
class ScheduleService {
|
|
118
|
+
constructor(logger) {
|
|
119
|
+
this.logger = logger;
|
|
120
|
+
this.schedules = new Map(); // id → schedule object
|
|
121
|
+
this.configPath = path.join(getUserDataDir(), 'schedules.json');
|
|
122
|
+
this.checkIntervalMs = 30000; // Check every 30 seconds
|
|
123
|
+
this.checkTimer = null;
|
|
124
|
+
|
|
125
|
+
// Dependencies (injected)
|
|
126
|
+
this.agentPool = null;
|
|
127
|
+
this.messageProcessor = null;
|
|
128
|
+
this.flowExecutor = null;
|
|
129
|
+
this.webSocketManager = null;
|
|
130
|
+
this.orchestrator = null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ========================
|
|
134
|
+
// Dependency injection
|
|
135
|
+
// ========================
|
|
136
|
+
|
|
137
|
+
setAgentPool(agentPool) { this.agentPool = agentPool; }
|
|
138
|
+
setMessageProcessor(messageProcessor) { this.messageProcessor = messageProcessor; }
|
|
139
|
+
setFlowExecutor(flowExecutor) { this.flowExecutor = flowExecutor; }
|
|
140
|
+
setWebSocketManager(wsManager) { this.webSocketManager = wsManager; }
|
|
141
|
+
setOrchestrator(orchestrator) { this.orchestrator = orchestrator; }
|
|
142
|
+
|
|
143
|
+
// ========================
|
|
144
|
+
// Lifecycle
|
|
145
|
+
// ========================
|
|
146
|
+
|
|
147
|
+
async initialize() {
|
|
148
|
+
await this._loadSchedules();
|
|
149
|
+
this.logger.info('ScheduleService initialized', { scheduleCount: this.schedules.size });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
start() {
|
|
153
|
+
if (this.checkTimer) return;
|
|
154
|
+
|
|
155
|
+
this.checkTimer = setInterval(() => {
|
|
156
|
+
this._tick().catch(err =>
|
|
157
|
+
this.logger.error('Schedule tick error', { error: err.message })
|
|
158
|
+
);
|
|
159
|
+
}, this.checkIntervalMs);
|
|
160
|
+
|
|
161
|
+
this.logger.info('ScheduleService started', { intervalMs: this.checkIntervalMs });
|
|
162
|
+
|
|
163
|
+
// Run an immediate check for any missed schedules
|
|
164
|
+
this._tick().catch(err =>
|
|
165
|
+
this.logger.error('Initial schedule tick error', { error: err.message })
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Sync all schedules to OS scheduler (non-blocking)
|
|
169
|
+
this.syncAllToOS().catch(err =>
|
|
170
|
+
this.logger.warn('Failed to sync schedules to OS on startup', { error: err.message })
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
stop() {
|
|
175
|
+
if (this.checkTimer) {
|
|
176
|
+
clearInterval(this.checkTimer);
|
|
177
|
+
this.checkTimer = null;
|
|
178
|
+
this.logger.info('ScheduleService stopped');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ========================
|
|
183
|
+
// CRUD
|
|
184
|
+
// ========================
|
|
185
|
+
|
|
186
|
+
async createSchedule(config) {
|
|
187
|
+
const {
|
|
188
|
+
name,
|
|
189
|
+
prompt,
|
|
190
|
+
targetType, // 'agent' or 'flow'
|
|
191
|
+
targetId, // agent ID or flow ID
|
|
192
|
+
cronExpression, // raw cron or preset name
|
|
193
|
+
enabled = true,
|
|
194
|
+
description = '',
|
|
195
|
+
startDate = null, // ISO date string — schedule starts after this date
|
|
196
|
+
endDate = null, // ISO date string — schedule stops after this date
|
|
197
|
+
maxRuns = null, // number — auto-disable after N executions (null = unlimited)
|
|
198
|
+
runOnce = false // boolean — run once then auto-disable
|
|
199
|
+
} = config;
|
|
200
|
+
|
|
201
|
+
if (!name) throw new Error('Schedule name is required');
|
|
202
|
+
if (!prompt) throw new Error('Prompt is required');
|
|
203
|
+
if (!targetType || !['agent', 'flow'].includes(targetType)) {
|
|
204
|
+
throw new Error('targetType must be "agent" or "flow"');
|
|
205
|
+
}
|
|
206
|
+
if (!targetId) throw new Error('targetId is required');
|
|
207
|
+
if (!cronExpression) throw new Error('cronExpression is required');
|
|
208
|
+
|
|
209
|
+
// Resolve preset or validate cron
|
|
210
|
+
const resolvedCron = CRON_PRESETS[cronExpression] || cronExpression;
|
|
211
|
+
const parsed = parseCron(resolvedCron); // throws if invalid
|
|
212
|
+
const nextRun = getNextCronDate(parsed);
|
|
213
|
+
|
|
214
|
+
const id = `schedule-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
|
|
215
|
+
|
|
216
|
+
const schedule = {
|
|
217
|
+
id,
|
|
218
|
+
name,
|
|
219
|
+
description,
|
|
220
|
+
prompt,
|
|
221
|
+
targetType,
|
|
222
|
+
targetId,
|
|
223
|
+
cronExpression: resolvedCron,
|
|
224
|
+
cronPreset: CRON_PRESETS[cronExpression] ? cronExpression : null,
|
|
225
|
+
enabled,
|
|
226
|
+
startDate: startDate || null,
|
|
227
|
+
endDate: endDate || null,
|
|
228
|
+
maxRuns: maxRuns != null ? parseInt(maxRuns) : null,
|
|
229
|
+
runOnce: !!runOnce,
|
|
230
|
+
nextRun: nextRun ? nextRun.toISOString() : null,
|
|
231
|
+
lastRun: null,
|
|
232
|
+
lastRunStatus: null,
|
|
233
|
+
runCount: 0,
|
|
234
|
+
createdAt: new Date().toISOString(),
|
|
235
|
+
updatedAt: new Date().toISOString()
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
this.schedules.set(id, schedule);
|
|
239
|
+
await this._saveSchedules();
|
|
240
|
+
|
|
241
|
+
// Sync to OS scheduler (non-blocking, non-fatal)
|
|
242
|
+
this._syncToOS(schedule).catch(() => {});
|
|
243
|
+
|
|
244
|
+
this._broadcast('schedule_created', schedule);
|
|
245
|
+
this.logger.info('Schedule created', { id, name, cron: resolvedCron, targetType, targetId });
|
|
246
|
+
|
|
247
|
+
return schedule;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async updateSchedule(id, updates) {
|
|
251
|
+
const schedule = this.schedules.get(id);
|
|
252
|
+
if (!schedule) throw new Error(`Schedule not found: ${id}`);
|
|
253
|
+
|
|
254
|
+
// If cron changed, reparse and recalculate next run
|
|
255
|
+
if (updates.cronExpression) {
|
|
256
|
+
const resolvedCron = CRON_PRESETS[updates.cronExpression] || updates.cronExpression;
|
|
257
|
+
parseCron(resolvedCron); // validate
|
|
258
|
+
updates.cronExpression = resolvedCron;
|
|
259
|
+
updates.cronPreset = CRON_PRESETS[updates.cronExpression] ? updates.cronExpression : null;
|
|
260
|
+
const parsed = parseCron(resolvedCron);
|
|
261
|
+
updates.nextRun = getNextCronDate(parsed)?.toISOString() || null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// If re-enabled, recalculate next run
|
|
265
|
+
if (updates.enabled === true && !schedule.enabled) {
|
|
266
|
+
const parsed = parseCron(updates.cronExpression || schedule.cronExpression);
|
|
267
|
+
updates.nextRun = getNextCronDate(parsed)?.toISOString() || null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const allowedFields = ['name', 'description', 'prompt', 'targetType', 'targetId', 'cronExpression', 'cronPreset', 'enabled', 'nextRun'];
|
|
271
|
+
for (const key of allowedFields) {
|
|
272
|
+
if (updates[key] !== undefined) {
|
|
273
|
+
schedule[key] = updates[key];
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
schedule.updatedAt = new Date().toISOString();
|
|
277
|
+
|
|
278
|
+
await this._saveSchedules();
|
|
279
|
+
|
|
280
|
+
// Sync to OS scheduler (non-blocking, non-fatal)
|
|
281
|
+
this._syncToOS(schedule).catch(() => {});
|
|
282
|
+
|
|
283
|
+
this._broadcast('schedule_updated', schedule);
|
|
284
|
+
this.logger.info('Schedule updated', { id, updates: Object.keys(updates) });
|
|
285
|
+
|
|
286
|
+
return schedule;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async deleteSchedule(id) {
|
|
290
|
+
if (!this.schedules.has(id)) throw new Error(`Schedule not found: ${id}`);
|
|
291
|
+
|
|
292
|
+
// Remove from OS scheduler first
|
|
293
|
+
this._removeFromOS(id).catch(() => {});
|
|
294
|
+
|
|
295
|
+
this.schedules.delete(id);
|
|
296
|
+
await this._saveSchedules();
|
|
297
|
+
|
|
298
|
+
this._broadcast('schedule_deleted', { id });
|
|
299
|
+
this.logger.info('Schedule deleted', { id });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
getSchedule(id) {
|
|
303
|
+
return this.schedules.get(id) || null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
listSchedules() {
|
|
307
|
+
return Array.from(this.schedules.values()).sort((a, b) =>
|
|
308
|
+
new Date(b.createdAt) - new Date(a.createdAt)
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
getPresets() {
|
|
313
|
+
return { ...CRON_PRESETS };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ========================
|
|
317
|
+
// Tick — check and fire due schedules
|
|
318
|
+
// ========================
|
|
319
|
+
|
|
320
|
+
async _tick() {
|
|
321
|
+
const now = new Date();
|
|
322
|
+
|
|
323
|
+
for (const schedule of this.schedules.values()) {
|
|
324
|
+
if (!schedule.enabled || !schedule.nextRun) continue;
|
|
325
|
+
|
|
326
|
+
// Check if schedule has expired (endDate passed)
|
|
327
|
+
if (schedule.endDate && now > new Date(schedule.endDate)) {
|
|
328
|
+
schedule.enabled = false;
|
|
329
|
+
schedule.nextRun = null;
|
|
330
|
+
schedule.updatedAt = now.toISOString();
|
|
331
|
+
this._broadcast('schedule_updated', schedule);
|
|
332
|
+
this.logger.info('Schedule expired (end date reached)', { id: schedule.id, name: schedule.name });
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Check if start date hasn't arrived yet
|
|
337
|
+
if (schedule.startDate && now < new Date(schedule.startDate)) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const nextRun = new Date(schedule.nextRun);
|
|
342
|
+
if (now >= nextRun) {
|
|
343
|
+
// Fire!
|
|
344
|
+
await this._executeSchedule(schedule);
|
|
345
|
+
|
|
346
|
+
schedule.lastRun = now.toISOString();
|
|
347
|
+
schedule.runCount++;
|
|
348
|
+
schedule.updatedAt = now.toISOString();
|
|
349
|
+
|
|
350
|
+
// Check if schedule should auto-disable (runOnce or maxRuns reached)
|
|
351
|
+
if (schedule.runOnce || (schedule.maxRuns != null && schedule.runCount >= schedule.maxRuns)) {
|
|
352
|
+
schedule.enabled = false;
|
|
353
|
+
schedule.nextRun = null;
|
|
354
|
+
this._broadcast('schedule_updated', schedule);
|
|
355
|
+
this.logger.info('Schedule auto-disabled', {
|
|
356
|
+
id: schedule.id, name: schedule.name,
|
|
357
|
+
reason: schedule.runOnce ? 'run-once' : `maxRuns (${schedule.maxRuns}) reached`
|
|
358
|
+
});
|
|
359
|
+
} else {
|
|
360
|
+
// Calculate next run
|
|
361
|
+
const parsed = parseCron(schedule.cronExpression);
|
|
362
|
+
const next = getNextCronDate(parsed, now);
|
|
363
|
+
schedule.nextRun = next ? next.toISOString() : null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
await this._saveSchedules();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async _executeSchedule(schedule) {
|
|
372
|
+
this.logger.info('Executing scheduled task', {
|
|
373
|
+
id: schedule.id,
|
|
374
|
+
name: schedule.name,
|
|
375
|
+
targetType: schedule.targetType,
|
|
376
|
+
targetId: schedule.targetId
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
this._broadcast('schedule_triggered', {
|
|
380
|
+
id: schedule.id,
|
|
381
|
+
name: schedule.name,
|
|
382
|
+
targetType: schedule.targetType,
|
|
383
|
+
targetId: schedule.targetId,
|
|
384
|
+
triggeredAt: new Date().toISOString()
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
if (schedule.targetType === 'agent') {
|
|
389
|
+
await this._executeAgentSchedule(schedule);
|
|
390
|
+
schedule.lastRunStatus = 'success';
|
|
391
|
+
} else if (schedule.targetType === 'flow') {
|
|
392
|
+
await this._executeFlowSchedule(schedule);
|
|
393
|
+
schedule.lastRunStatus = 'success';
|
|
394
|
+
}
|
|
395
|
+
} catch (error) {
|
|
396
|
+
schedule.lastRunStatus = 'error';
|
|
397
|
+
schedule.lastRunError = error.message;
|
|
398
|
+
this.logger.error('Scheduled task execution failed', {
|
|
399
|
+
id: schedule.id,
|
|
400
|
+
error: error.message
|
|
401
|
+
});
|
|
402
|
+
this._broadcast('schedule_error', {
|
|
403
|
+
id: schedule.id,
|
|
404
|
+
name: schedule.name,
|
|
405
|
+
error: error.message
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async _executeAgentSchedule(schedule) {
|
|
411
|
+
if (!this.agentPool) throw new Error('AgentPool not available');
|
|
412
|
+
|
|
413
|
+
const agent = await this.agentPool.getAgent(schedule.targetId);
|
|
414
|
+
if (!agent) throw new Error(`Target agent not found: ${schedule.targetId}`);
|
|
415
|
+
|
|
416
|
+
// Switch agent to agent mode (autonomous)
|
|
417
|
+
const { AGENT_MODES } = await import('../utilities/constants.js');
|
|
418
|
+
|
|
419
|
+
// Get a session ID — use the first WebSocket connection or a synthetic one
|
|
420
|
+
let sessionId = `schedule-${schedule.id}-${Date.now()}`;
|
|
421
|
+
if (this.webSocketManager?.connections) {
|
|
422
|
+
for (const conn of this.webSocketManager.connections.values()) {
|
|
423
|
+
if (conn.sessionId) {
|
|
424
|
+
sessionId = conn.sessionId;
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Switch to agent mode
|
|
431
|
+
await this.agentPool.updateAgent(schedule.targetId, {
|
|
432
|
+
mode: AGENT_MODES.AGENT,
|
|
433
|
+
sessionId
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Push the prompt as a user message
|
|
437
|
+
await this.agentPool.addUserMessage(schedule.targetId, {
|
|
438
|
+
role: 'user',
|
|
439
|
+
content: `[Scheduled Task: ${schedule.name}]\n\n${schedule.prompt}`,
|
|
440
|
+
timestamp: new Date().toISOString(),
|
|
441
|
+
source: 'schedule',
|
|
442
|
+
scheduleId: schedule.id
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
this.logger.info('Prompt pushed to agent via schedule', {
|
|
446
|
+
scheduleId: schedule.id,
|
|
447
|
+
agentId: schedule.targetId,
|
|
448
|
+
agentName: agent.name
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async _executeFlowSchedule(schedule) {
|
|
453
|
+
if (!this.flowExecutor) throw new Error('FlowExecutor not available');
|
|
454
|
+
|
|
455
|
+
let sessionId = `schedule-${schedule.id}-${Date.now()}`;
|
|
456
|
+
if (this.webSocketManager?.connections) {
|
|
457
|
+
for (const conn of this.webSocketManager.connections.values()) {
|
|
458
|
+
if (conn.sessionId) {
|
|
459
|
+
sessionId = conn.sessionId;
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
await this.flowExecutor.executeFlow(
|
|
466
|
+
schedule.targetId,
|
|
467
|
+
{ userInput: schedule.prompt, source: 'schedule', scheduleId: schedule.id },
|
|
468
|
+
sessionId
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
this.logger.info('Flow executed via schedule', {
|
|
472
|
+
scheduleId: schedule.id,
|
|
473
|
+
flowId: schedule.targetId
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ========================
|
|
478
|
+
// Persistence
|
|
479
|
+
// ========================
|
|
480
|
+
|
|
481
|
+
async _loadSchedules() {
|
|
482
|
+
try {
|
|
483
|
+
const data = await fs.readFile(this.configPath, 'utf-8');
|
|
484
|
+
const arr = JSON.parse(data);
|
|
485
|
+
this.schedules.clear();
|
|
486
|
+
for (const s of arr) {
|
|
487
|
+
this.schedules.set(s.id, s);
|
|
488
|
+
}
|
|
489
|
+
this.logger.info('Schedules loaded from disk', { count: arr.length });
|
|
490
|
+
} catch (err) {
|
|
491
|
+
if (err.code !== 'ENOENT') {
|
|
492
|
+
this.logger.warn('Failed to load schedules', { error: err.message });
|
|
493
|
+
}
|
|
494
|
+
// First run or corrupted — start fresh
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async _saveSchedules() {
|
|
499
|
+
try {
|
|
500
|
+
const dir = path.dirname(this.configPath);
|
|
501
|
+
await fs.mkdir(dir, { recursive: true });
|
|
502
|
+
const arr = Array.from(this.schedules.values());
|
|
503
|
+
await fs.writeFile(this.configPath, JSON.stringify(arr, null, 2), 'utf-8');
|
|
504
|
+
} catch (err) {
|
|
505
|
+
this.logger.error('Failed to save schedules', { error: err.message });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ========================
|
|
510
|
+
// WebSocket broadcasting
|
|
511
|
+
// ========================
|
|
512
|
+
|
|
513
|
+
_broadcast(type, data) {
|
|
514
|
+
if (this.webSocketManager?.broadcast) {
|
|
515
|
+
this.webSocketManager.broadcast({ type, ...data });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ========================
|
|
520
|
+
// OS Scheduler Sync
|
|
521
|
+
// Registers/unregisters cron jobs (Linux/macOS) or schtasks (Windows)
|
|
522
|
+
// so schedules fire even when Loxia is shut down.
|
|
523
|
+
// ========================
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Find the loxia CLI binary path
|
|
527
|
+
*/
|
|
528
|
+
_findLoxiaBin() {
|
|
529
|
+
try {
|
|
530
|
+
// Check if installed globally
|
|
531
|
+
const which = process.platform === 'win32' ? 'where loxia' : 'which loxia';
|
|
532
|
+
return execSync(which, { encoding: 'utf-8' }).trim().split('\n')[0];
|
|
533
|
+
} catch {
|
|
534
|
+
// Fallback to local bin
|
|
535
|
+
const __dirname = path.dirname(resolveModuleFilename(import.meta.url));
|
|
536
|
+
const localBin = path.resolve(__dirname, '../../bin/cli.js');
|
|
537
|
+
return `node "${localBin}"`;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Sync a single schedule to the OS scheduler.
|
|
543
|
+
* Called after create/update.
|
|
544
|
+
*/
|
|
545
|
+
async _syncToOS(schedule) {
|
|
546
|
+
if (!schedule.enabled) {
|
|
547
|
+
await this._removeFromOS(schedule.id);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
const loxiaBin = this._findLoxiaBin();
|
|
553
|
+
const command = `${loxiaBin} trigger-schedule ${schedule.id}`;
|
|
554
|
+
|
|
555
|
+
if (process.platform === 'win32') {
|
|
556
|
+
await this._syncToWindows(schedule, command);
|
|
557
|
+
} else {
|
|
558
|
+
await this._syncToCrontab(schedule, command);
|
|
559
|
+
}
|
|
560
|
+
} catch (err) {
|
|
561
|
+
// Non-fatal — internal scheduler still works as fallback
|
|
562
|
+
this.logger.warn('Failed to sync schedule to OS scheduler', {
|
|
563
|
+
id: schedule.id,
|
|
564
|
+
error: err.message
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Remove a schedule from the OS scheduler.
|
|
571
|
+
* Called after delete or disable.
|
|
572
|
+
*/
|
|
573
|
+
async _removeFromOS(scheduleId) {
|
|
574
|
+
try {
|
|
575
|
+
if (process.platform === 'win32') {
|
|
576
|
+
this._removeFromWindows(scheduleId);
|
|
577
|
+
} else {
|
|
578
|
+
this._removeFromCrontab(scheduleId);
|
|
579
|
+
}
|
|
580
|
+
} catch (err) {
|
|
581
|
+
this.logger.warn('Failed to remove schedule from OS scheduler', {
|
|
582
|
+
id: scheduleId,
|
|
583
|
+
error: err.message
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Sync all enabled schedules to OS. Called on startup.
|
|
590
|
+
*/
|
|
591
|
+
async syncAllToOS() {
|
|
592
|
+
for (const schedule of this.schedules.values()) {
|
|
593
|
+
await this._syncToOS(schedule);
|
|
594
|
+
}
|
|
595
|
+
this.logger.info('Synced all schedules to OS scheduler');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// --- Linux/macOS: crontab ---
|
|
599
|
+
|
|
600
|
+
_syncToCrontab(schedule, command) {
|
|
601
|
+
const tag = `# loxia-schedule:${schedule.id}`;
|
|
602
|
+
|
|
603
|
+
// Read current crontab
|
|
604
|
+
let existing = '';
|
|
605
|
+
try {
|
|
606
|
+
existing = execSync('crontab -l 2>/dev/null', { encoding: 'utf-8' });
|
|
607
|
+
} catch {
|
|
608
|
+
// No crontab yet
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Remove old entry for this schedule
|
|
612
|
+
const lines = existing.split('\n').filter(l => !l.includes(`loxia-schedule:${schedule.id}`));
|
|
613
|
+
|
|
614
|
+
// Add new entry
|
|
615
|
+
lines.push(`${schedule.cronExpression} ${command} ${tag}`);
|
|
616
|
+
|
|
617
|
+
// Write back
|
|
618
|
+
const newCrontab = lines.filter(l => l.trim() !== '').join('\n') + '\n';
|
|
619
|
+
execSync('crontab -', { input: newCrontab, encoding: 'utf-8' });
|
|
620
|
+
|
|
621
|
+
this.logger.info('Synced schedule to crontab', { id: schedule.id, cron: schedule.cronExpression });
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
_removeFromCrontab(scheduleId) {
|
|
625
|
+
let existing;
|
|
626
|
+
try {
|
|
627
|
+
existing = execSync('crontab -l 2>/dev/null', { encoding: 'utf-8' });
|
|
628
|
+
} catch {
|
|
629
|
+
return; // No crontab
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const lines = existing.split('\n').filter(l => !l.includes(`loxia-schedule:${scheduleId}`));
|
|
633
|
+
const newCrontab = lines.filter(l => l.trim() !== '').join('\n') + '\n';
|
|
634
|
+
execSync('crontab -', { input: newCrontab, encoding: 'utf-8' });
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// --- Windows: schtasks ---
|
|
638
|
+
|
|
639
|
+
_syncToWindows(schedule, command) {
|
|
640
|
+
const taskName = `LoxiaSchedule_${schedule.id}`;
|
|
641
|
+
|
|
642
|
+
// Remove existing task if any
|
|
643
|
+
try {
|
|
644
|
+
execSync(`schtasks /Delete /TN "${taskName}" /F 2>nul`, { encoding: 'utf-8' });
|
|
645
|
+
} catch {
|
|
646
|
+
// Task may not exist
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// schtasks doesn't support cron directly — we map common patterns
|
|
650
|
+
// For complex crons, we rely on the internal scheduler as fallback
|
|
651
|
+
const scArgs = this._cronToSchtasksArgs(schedule.cronExpression);
|
|
652
|
+
if (!scArgs) {
|
|
653
|
+
this.logger.warn('Complex cron not supported by Windows Task Scheduler, using internal scheduler only', {
|
|
654
|
+
id: schedule.id,
|
|
655
|
+
cron: schedule.cronExpression
|
|
656
|
+
});
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
execSync(
|
|
661
|
+
`schtasks /Create /TN "${taskName}" /TR "${command}" ${scArgs} /F`,
|
|
662
|
+
{ encoding: 'utf-8' }
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
this.logger.info('Synced schedule to Windows Task Scheduler', { id: schedule.id, taskName });
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
_removeFromWindows(scheduleId) {
|
|
669
|
+
const taskName = `LoxiaSchedule_${scheduleId}`;
|
|
670
|
+
try {
|
|
671
|
+
execSync(`schtasks /Delete /TN "${taskName}" /F 2>nul`, { encoding: 'utf-8' });
|
|
672
|
+
} catch {
|
|
673
|
+
// Task may not exist
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Convert simple cron patterns to schtasks /SC arguments.
|
|
679
|
+
* Returns null for complex patterns that schtasks can't express.
|
|
680
|
+
*/
|
|
681
|
+
_cronToSchtasksArgs(cron) {
|
|
682
|
+
const parts = cron.trim().split(/\s+/);
|
|
683
|
+
const [min, hour, dom, mon, dow] = parts;
|
|
684
|
+
|
|
685
|
+
// Every N minutes: */N * * * *
|
|
686
|
+
if (min.startsWith('*/') && hour === '*' && dom === '*' && mon === '*' && dow === '*') {
|
|
687
|
+
const n = parseInt(min.slice(2), 10);
|
|
688
|
+
return `/SC MINUTE /MO ${n}`;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Every hour at minute M: M * * * *
|
|
692
|
+
if (/^\d+$/.test(min) && hour === '*' && dom === '*' && mon === '*' && dow === '*') {
|
|
693
|
+
return `/SC HOURLY /ST 00:${min.padStart(2, '0')}`;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Daily at H:M: M H * * *
|
|
697
|
+
if (/^\d+$/.test(min) && /^\d+$/.test(hour) && dom === '*' && mon === '*' && dow === '*') {
|
|
698
|
+
return `/SC DAILY /ST ${hour.padStart(2, '0')}:${min.padStart(2, '0')}`;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Weekly on specific days: M H * * D
|
|
702
|
+
if (/^\d+$/.test(min) && /^\d+$/.test(hour) && dom === '*' && mon === '*' && /^[\d,-]+$/.test(dow)) {
|
|
703
|
+
const dayMap = { 0: 'SUN', 1: 'MON', 2: 'TUE', 3: 'WED', 4: 'THU', 5: 'FRI', 6: 'SAT' };
|
|
704
|
+
const days = dow.split(',').flatMap(part => {
|
|
705
|
+
if (part.includes('-')) {
|
|
706
|
+
const [a, b] = part.split('-').map(Number);
|
|
707
|
+
const result = [];
|
|
708
|
+
for (let i = a; i <= b; i++) result.push(dayMap[i] || '');
|
|
709
|
+
return result;
|
|
710
|
+
}
|
|
711
|
+
return [dayMap[parseInt(part, 10)] || ''];
|
|
712
|
+
}).filter(Boolean).join(',');
|
|
713
|
+
if (days) return `/SC WEEKLY /D ${days} /ST ${hour.padStart(2, '0')}:${min.padStart(2, '0')}`;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Monthly on specific day: M H D * *
|
|
717
|
+
if (/^\d+$/.test(min) && /^\d+$/.test(hour) && /^\d+$/.test(dom) && mon === '*' && dow === '*') {
|
|
718
|
+
return `/SC MONTHLY /D ${dom} /ST ${hour.padStart(2, '0')}:${min.padStart(2, '0')}`;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return null; // Complex pattern — fallback to internal scheduler
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
export default ScheduleService;
|
|
726
|
+
export { CRON_PRESETS, parseCron, cronMatchesDate, getNextCronDate };
|