@goodandready/dsh-cron 0.2.18 → 0.2.19

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 CHANGED
@@ -392,6 +392,15 @@ Developer-facing, no behaviour change. `parseScheduleExpression` was split into
392
392
 
393
393
 
394
394
 
395
+ ### 31. Quality & Preflight Hardening Pack (v0.2.19, #149, #152, #155, #156, #162)
396
+ - **Zero Empty Catch Blocks (#156)**: Implemented `lib/best-effort.js` matching standard architecture with synchronous/asynchronous error suppression, fallback value handling, and optional context logging. Eliminated all 63 empty catch blocks across runner, scheduler, store, and UI client scripts.
397
+ - **CI Workflows & Local Preflight Gate (#162)**: Added automated CI workflows (`.gitea/workflows/ci.yml` and `.github/workflows/ci.yml`) running test suites and enforcing a strict preflight gate (`scripts/ci-preflight.mjs`) that halts on any syntax error, empty catch, theme hardcoding, or leak attempt.
398
+ - **Theme Token Modernization (#149)**: Replaced all remaining raw `rgba(...)` declarations in UI styles and modals with native `color-mix(in srgb, var(--token) N%, transparent)`.
399
+ - **Plugin Manifest Declarations (#152)**: Updated `dsh.client.inject` in `package.json` to declare full package dependencies (`@deepseek-ai/dsh-client-locale`, `@deepseek-ai/dsh-client-ui-slots`).
400
+ - **Production Export Integration (#155)**: Connected test-only exports (`findDestructiveRecipe` in `listRecipes`, `shouldNotifyTask` in `shouldSendToChannel`, and `TEMPLATE_VARIABLES` in `buildTemplateVars`) into active production pipelines.
401
+
402
+ ---
403
+
395
404
  ### 30. Updater Localization in English & Chinese (v0.2.18, #160)
396
405
  - **Settings Card Updater Localization (#160)**: Added native translations for all 10 in-app updater keys (`updater.title`, `updater.btnCheck`, `updater.checking`, `updater.btnUpdate`, `updater.updating`, `updater.desc`, `updater.current`, `updater.available`, `updater.upToDate`, `updater.success`) into English (`en`) and Chinese (`zh`) dictionaries in `lib/client-src/10-locales.js`. In accordance with DSH core plugin architecture, Russian localization is maintained externally via `dsh-russian-lang`.
397
406
 
@@ -529,7 +538,7 @@ All endpoints are served by the DSH web server under `/dsh-cron/`. Read endpoint
529
538
 
530
539
  ---
531
540
 
532
- ## 🧪 Testing
541
+ ## 🧪 Testing & Preflight Verification
533
542
 
534
543
  Run the automated test suite covering schedule parsing, the scheduler engine, atomic storage, HTTP helpers, notifications and tool contracts:
535
544
 
@@ -537,6 +546,12 @@ Run the automated test suite covering schedule parsing, the scheduler engine, at
537
546
  npm test
538
547
  ```
539
548
 
549
+ Run the local preflight gate to verify syntax, zero empty catch blocks, theme token compliance, package boundaries and leak protection:
550
+
551
+ ```bash
552
+ node scripts/ci-preflight.mjs
553
+ ```
554
+
540
555
  ---
541
556
 
542
557
  ## 📄 License
package/README.ru.md CHANGED
@@ -268,6 +268,15 @@ cron_create_task({
268
268
 
269
269
 
270
270
 
271
+ ### 31. Пакет ужесточения стандартов качества и CI (v0.2.19, #149, #152, #155, #156, #162)
272
+ - **Устранение пустых catch (#156)**: Реализован модуль `lib/best-effort.js` по единому стандарту с безопасным выполнением синхронных/асинхронных операций и опциональным логированием. Все 63 пустых catch-блока в планировщике, раннере, хранилище и клиентских фрагментах устранены.
273
+ - **Автоматический CI и локальный гейт Preflight (#162)**: Добавлены workflows непрерывной интеграции (`.gitea/workflows/ci.yml` и `.github/workflows/ci.yml`), проверяющие синтаксис, тесты и запускающие механический аудит `scripts/ci-preflight.mjs` с блокировкой любых отклонений.
274
+ - **Модернизация токенов темы (#149)**: Оставшиеся цвета `rgba(...)` в модальных окнах и карточках переведены на современный стандарт `color-mix(in srgb, var(--токен) N%, transparent)`.
275
+ - **Декларация зависимостей в манифесте (#152)**: В `package.json` поле `dsh.client.inject` переведено на полные имена пакетов Cordis (`@deepseek-ai/dsh-client-locale`, `@deepseek-ai/dsh-client-ui-slots`).
276
+ - **Активация экспортов в рабочем коде (#155)**: Функции `findDestructiveRecipe`, `shouldNotifyTask` и переменные `TEMPLATE_VARIABLES` подключены в активные цепочки работы с рецептами, роутинга уведомлений и рендеринга шаблонов.
277
+
278
+ ---
279
+
271
280
  ### 30. Локализация автообновления на английский и китайский языки (v0.2.18, #160)
272
281
  - **Локализация карточки обновления (#160)**: Добавлены нативные переводы для всех 10 ключей модуля автообновления (`updater.title`, `updater.btnCheck`, `updater.checking`, `updater.btnUpdate`, `updater.updating`, `updater.desc`, `updater.current`, `updater.available`, `updater.upToDate`, `updater.success`) в словари английского (`en`) и китайского (`zh`) языков в `lib/client-src/10-locales.js`. В соответствии со стандартами ядра DSH плагин поставляется с EN и ZH словарями, а русская локализация обслуживается через системный плагин `dsh-russian-lang`.
273
282
 
@@ -526,13 +535,19 @@ bash deploy.sh verify [exact-version]
526
535
 
527
536
  ---
528
537
 
529
- ## 🧪 Тестирование
538
+ ## 🧪 Тестирование и локальная проверка (Preflight)
539
+
540
+ Запуск автоматического набора тестов (разбор расписаний, движок планировщика, атомарное хранилище, HTTP-хелперы, уведомления и контракт инструментов):
530
541
 
531
542
  ```bash
532
543
  npm test
533
544
  ```
534
545
 
535
- Набор покрывает разбор расписаний, движок планировщика, атомарное хранилище, HTTP-хелперы, уведомления и контракт инструментов.
546
+ Локальный запуск гейта стандартов качества (синтаксис, отсутствие пустых catch, соответствие токенам тем оформления, чистота состава npm-пакета и защита от утечек):
547
+
548
+ ```bash
549
+ node scripts/ci-preflight.mjs
550
+ ```
536
551
 
537
552
  ---
538
553
 
package/README.zh.md CHANGED
@@ -391,6 +391,15 @@ bash deploy.sh verify [exact-version]
391
391
 
392
392
 
393
393
 
394
+ ### 31. 质量加固与 CI 预检标准包 (v0.2.19, #149, #152, #155, #156, #162)
395
+ - **彻底消除空 catch 块 (#156)**:引入符合标准规范的 `lib/best-effort.js` 模块,全面支持同步/异步安全调用、回退返回值和可选日志记录,清除了 runner、scheduler、store 及客户端中的所有 63 处空 catch 块。
396
+ - **CI 工作流与本地预检门禁 (#162)**:新增自动化 CI 工作流(`.gitea/workflows/ci.yml` 与 `.github/workflows/ci.yml`),集成本地预检门禁脚本 `scripts/ci-preflight.mjs`,在出现语法错误、空 catch、颜色硬编码或信息泄露时自动阻断。
397
+ - **主题设计令牌现代化 (#149)**:将模态对话框与设置卡片中遗留的 `rgba(...)` 全部重构为原生的 `color-mix(in srgb, var(--token) N%, transparent)`。
398
+ - **插件清单注入声明规范化 (#152)**:在 `package.json` 的 `dsh.client.inject` 中统一声明完整的 Cordis 包名(`@deepseek-ai/dsh-client-locale`、`@deepseek-ai/dsh-client-ui-slots`)。
399
+ - **生产管线导出连接 (#155)**:将此前仅在测试中调用的导出(`findDestructiveRecipe`、`shouldNotifyTask` 和 `TEMPLATE_VARIABLES`)全面接入配方过滤、通知分发及模板生成核心逻辑。
400
+
401
+ ---
402
+
394
403
  ### 30. 自更新模块英文与中文多语言支持 (v0.2.18, #160)
395
404
  - **设置面板自更新多语言 (#160)**:在 `lib/client-src/10-locales.js` 的英文 (`en`) 与中文 (`zh`) 字典中补全了全部 10 个自更新键值 (`updater.title`, `updater.btnCheck`, `updater.checking`, `updater.btnUpdate`, `updater.updating`, `updater.desc`, `updater.current`, `updater.available`, `updater.upToDate`, `updater.success`)。遵循 DSH 插件规范,插件核心内置英文与中文,俄语多语言由 `dsh-russian-lang` 统一扩展。
396
405
 
@@ -526,13 +535,19 @@ dsh-cron:
526
535
 
527
536
  ---
528
537
 
529
- ## 🧪 测试
538
+ ## 🧪 测试与预检门禁 (Preflight)
539
+
540
+ 运行全套自动化测试(调度表达式解析、调度器引擎、原子存储、HTTP 辅助函数、通知与工具契约):
530
541
 
531
542
  ```bash
532
543
  npm test
533
544
  ```
534
545
 
535
- 测试覆盖调度表达式解析、调度器引擎、原子存储、HTTP 辅助函数、通知与工具契约。
546
+ 本地执行质量预检门禁(语法检查、零空 catch 块、主题设计规范、npm 发布归档合规性及防泄漏检查):
547
+
548
+ ```bash
549
+ node scripts/ci-preflight.mjs
550
+ ```
536
551
 
537
552
  ---
538
553
 
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Run a non-critical side effect; never throw.
3
+ * Logs at debug when a logger is provided (matches dsh-key-rotation pattern).
4
+ */
5
+ export function bestEffort(label, fn, logger) {
6
+ const log = (err) => {
7
+ try {
8
+ logger?.debug?.(`dsh-cron: best-effort ${label}`, err);
9
+ } catch {
10
+ /* logger itself must not throw */
11
+ }
12
+ };
13
+ try {
14
+ const result = fn();
15
+ if (result && typeof result.then === "function") {
16
+ return result.then(
17
+ (value) => value,
18
+ (err) => {
19
+ log(err);
20
+ return undefined;
21
+ },
22
+ );
23
+ }
24
+ return result;
25
+ } catch (err) {
26
+ log(err);
27
+ return undefined;
28
+ }
29
+ }
package/lib/channels.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * collected so one broken channel cannot hide another.
8
8
  */
9
9
 
10
- import { formatTaskTelegramMessage, sendTelegramMessage, createTelegramTaskKeyboard } from './telegram.js';
10
+ import { formatTaskTelegramMessage, sendTelegramMessage, createTelegramTaskKeyboard, shouldNotifyTask } from './telegram.js';
11
11
  import { createKanbanCard, shouldCreateKanbanCard } from './integrations.js';
12
12
  import { resolveTemplateText, truncateText } from './templates.js';
13
13
 
@@ -58,6 +58,11 @@ export function unknownChannels(task) {
58
58
  /** Failure/only-on-failure filtering, per channel. */
59
59
  export function shouldSendToChannel(channelId, task, runInfo, settings = {}) {
60
60
  if (channelId === 'kanban') return shouldCreateKanbanCard(task, runInfo);
61
+ if (channelId === 'telegram') {
62
+ if (task && task.notifyTelegram === false) return false;
63
+ const taskForTelegram = Object.assign({}, task, { notifyTelegram: true });
64
+ return shouldNotifyTask(taskForTelegram, runInfo, settings);
65
+ }
61
66
  const onlyOnFail = task.onlyOnFailure !== undefined
62
67
  ? Boolean(task.onlyOnFailure)
63
68
  : Boolean(settings.onlyOnFailure);
package/lib/client.js CHANGED
@@ -49,7 +49,9 @@ window.__ModuleLoader__.load({
49
49
  try {
50
50
  const primitives = require('@deepseek-ai/dsh-client-ui-primitives');
51
51
  CoreChevron = primitives && primitives.IconChevronDownOutline14;
52
- } catch (e) {}
52
+ } catch (e) {
53
+ /* Optional core chevron primitive not available; SVG fallback used */
54
+ }
53
55
  const chevronNode = () => (CoreChevron ? React.createElement(CoreChevron) : React.createElement('span', { style: { display: 'flex', alignItems: 'center' }, dangerouslySetInnerHTML: { __html: ICON_CHEVRON_DOWN } }));
54
56
 
55
57
  // Own React error boundary (#106): the core's SlotErrorBoundary renders an
@@ -622,15 +624,15 @@ window.__ModuleLoader__.load({
622
624
  const STYLES = `
623
625
  :root {
624
626
  --dsh-cron-success: var(--dsw-alias-state-success-primary, #10b981);
625
- --dsh-cron-success-bg: rgba(16, 185, 129, 0.1);
627
+ --dsh-cron-success-bg: color-mix(in srgb, var(--dsh-cron-success, #10b981) 10%, transparent);
626
628
  --dsh-cron-danger: var(--dsw-alias-state-error-primary, #ef4444);
627
- --dsh-cron-danger-bg: rgba(239, 68, 68, 0.1);
629
+ --dsh-cron-danger-bg: color-mix(in srgb, var(--dsh-cron-danger, #ef4444) 10%, transparent);
628
630
  --dsh-cron-info: var(--dsw-alias-state-brand-primary, #60a5fa);
629
- --dsh-cron-info-bg: rgba(96, 165, 250, 0.12);
631
+ --dsh-cron-info-bg: color-mix(in srgb, var(--dsh-cron-info, #60a5fa) 12%, transparent);
630
632
  --dsh-cron-accent: var(--dsw-alias-state-info-primary, #c084fc);
631
- --dsh-cron-accent-bg: rgba(168, 85, 247, 0.12);
633
+ --dsh-cron-accent-bg: color-mix(in srgb, var(--dsh-cron-accent, #a855f7) 12%, transparent);
632
634
  --dsh-cron-warning: var(--dsw-alias-state-warning-primary, #eab308);
633
- --dsh-cron-warning-bg: rgba(234, 179, 8, 0.12);
635
+ --dsh-cron-warning-bg: color-mix(in srgb, var(--dsh-cron-warning, #eab308) 12%, transparent);
634
636
  }
635
637
  [data-pane=conversation], [class*=centerCol] {
636
638
  position: relative !important;
@@ -660,14 +662,14 @@ window.__ModuleLoader__.load({
660
662
 
661
663
  .dsh-cron-top-bar { display: flex; align-items: center; gap: 12px; }
662
664
  .dsh-cron-back-btn { appearance: none; font: inherit; cursor: pointer; color: var(--dsw-alias-label-secondary, #9ca3af); background: 0 0; border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 8px; align-items: center; gap: 6px; padding: 4px 12px; font-size: 13px; display: inline-flex; flex: none; height: 32px; box-sizing: border-box; }
663
- .dsh-cron-back-btn:hover { color: var(--dsw-alias-label-primary, #ededed); background: var(--dsw-alias-bg-layer-4, var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06))); }
665
+ .dsh-cron-back-btn:hover { color: var(--dsw-alias-label-primary, #ededed); background: var(--dsw-alias-bg-layer-4, var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 6%, transparent))); }
664
666
 
665
667
  .dsh-cron-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; padding-bottom: 16px; border-bottom: 1px solid var(--dsw-alias-border-l2, #383838); }
666
668
  .dsh-cron-title { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 6px; }
667
669
  .dsh-cron-subtitle { font-size: 14px; color: var(--dsw-alias-label-secondary, #9ca3af); line-height: 1.5; }
668
670
  .dsh-cron-create-btn { appearance: none; display: inline-flex; align-items: center; gap: 6px; background: var(--dsw-alias-bg-layer-2, #262626); color: var(--dsw-alias-label-primary, #fff); border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 9999px; padding: 8px 16px; font-size: 14px; font-weight: 500; cursor: pointer; position: relative; }
669
671
  .dsh-cron-create-btn:hover { background: var(--dsw-alias-bg-layer-hover, var(--dsw-alias-interactive-bg-hover, #333)); }
670
- .dsh-cron-dropdown { position: absolute; top: calc(100% + 6px); right: 0; width: 200px; background: var(--dsw-alias-bg-layer-3, #212121); border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 12px; padding: 6px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); z-index: 200; }
672
+ .dsh-cron-dropdown { position: absolute; top: calc(100% + 6px); right: 0; width: 200px; background: var(--dsw-alias-bg-layer-3, #212121); border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 12px; padding: 6px; box-shadow: 0 10px 25px color-mix(in srgb, var(--dsw-alias-bg-mask, #000) 50%, transparent); z-index: 200; }
671
673
  .dsh-cron-dropdown-item { display: flex; align-items: center; gap: 10px; width: 100%; padding: 10px 12px; background: transparent; border: none; border-radius: 8px; color: var(--dsw-alias-label-primary, #ededed); font-size: 13.5px; text-align: left; cursor: pointer; }
672
674
  .dsh-cron-dropdown-item:hover { background: var(--dsw-alias-interactive-bg-hover, #2a2a2a); }
673
675
  .dsh-cron-search-bar { position: relative; margin-bottom: 0; }
@@ -677,7 +679,7 @@ window.__ModuleLoader__.load({
677
679
  .dsh-cron-filters { margin-top: 8px; }
678
680
  .dsh-cron-filter-toggle { background: none; border: 1px solid var(--dsw-alias-border-l2, #333); color: var(--dsw-alias-label-secondary, #aaa); border-radius: 8px; padding: 4px 10px; font-size: 12px; cursor: pointer; }
679
681
  .dsh-cron-filter-toggle:hover { color: var(--dsw-alias-label-primary, #eee); border-color: var(--dsh-cron-info); }
680
- .dsh-cron-filter-panel { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; margin-top: 10px; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l2, #2e2e2e); border-radius: 8px; background: var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.02)); }
682
+ .dsh-cron-filter-panel { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; margin-top: 10px; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l2, #2e2e2e); border-radius: 8px; background: var(--dsw-alias-bg-layer-3, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 2%, transparent)); }
681
683
  .dsh-cron-filter-field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--dsw-alias-label-tertiary, #888); }
682
684
  .dsh-cron-filter-field select { background: var(--dsw-alias-bg-layer-2, #1f1f1f); border: 1px solid var(--dsw-alias-border-l2, #333); color: var(--dsw-alias-label-primary, inherit); border-radius: 6px; padding: 5px 8px; font-size: 12.5px; }
683
685
  .dsh-cron-tabs { display: flex; flex-wrap: wrap; gap: 8px; }
@@ -694,7 +696,7 @@ window.__ModuleLoader__.load({
694
696
  .dsh-cron-tag-session { background: var(--dsh-cron-info-bg); color: var(--dsh-cron-info); border-color: var(--dsh-cron-info); }
695
697
  .dsh-cron-tag-preflight { background: var(--dsh-cron-warning-bg); color: var(--dsh-cron-warning); border-color: var(--dsh-cron-warning); }
696
698
 
697
- .dsh-cron-task-model-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2, #383838); background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.06)); color: var(--dsw-alias-label-secondary, #aaa); margin-left: 8px; vertical-align: middle; }
699
+ .dsh-cron-task-model-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2, #383838); background: var(--dsw-alias-bg-layer-2, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 6%, transparent)); color: var(--dsw-alias-label-secondary, #aaa); margin-left: 8px; vertical-align: middle; }
698
700
  .dsh-cron-task-actions { display: flex; gap: 8px; }
699
701
  .dsh-cron-icon-btn { appearance: none; font: inherit; background: var(--dsw-alias-bg-layer-2, transparent); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 8px; padding: 6px 12px; font-size: 12.5px; font-weight: 500; color: var(--dsw-alias-label-primary, #aaa); cursor: pointer; display: inline-flex; align-items: center; gap: 6px; transition: all 0.15s ease; }
700
702
  .dsh-cron-icon-btn:hover { background: var(--dsw-alias-bg-layer-4, #2a2a2a); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2, #444)); }
@@ -706,14 +708,14 @@ window.__ModuleLoader__.load({
706
708
  .dsh-cron-recs-list { display: flex; flex-direction: column; gap: 10px; }
707
709
  .dsh-cron-rec-card { display: flex; align-items: center; gap: 16px; padding: 14px 18px; background: var(--dsw-alias-bg-layer-3, #1c1c1c); border: 1px solid var(--dsw-alias-border-l2, #282828); border-radius: 12px; cursor: pointer; transition: background 0.15s; }
708
710
  .dsh-cron-rec-card:hover, .dsh-cron-rec-card:focus-visible { background: var(--dsw-alias-bg-layer-4, #242424); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2, #383838)); outline: none; }
709
- .dsh-cron-rec-icon { width: 36px; height: 36px; border-radius: 10px; background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.03)); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
711
+ .dsh-cron-rec-icon { width: 36px; height: 36px; border-radius: 10px; background: var(--dsw-alias-bg-layer-2, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 3%, transparent)); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
710
712
  .dsh-cron-rec-content { flex: 1; min-width: 0; }
711
713
  .dsh-cron-rec-title { font-size: 14.5px; font-weight: 500; margin-bottom: 4px; }
712
714
  .dsh-cron-rec-time { font-size: 13px; color: var(--dsw-alias-label-secondary, #9ca3af); font-weight: normal; margin-left: 8px; }
713
715
  .dsh-cron-rec-desc { font-size: 13px; color: var(--dsw-alias-label-tertiary, #71717a); }
714
716
 
715
- .dsh-cron-modal-overlay { position: fixed; inset: 0; background: var(--dsw-alias-bg-mask, rgba(0, 0, 0, 0.75)); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 16px; box-sizing: border-box; overflow-y: auto; }
716
- .dsh-cron-modal { background: var(--dsw-alias-bg-layer-2, #1f1f1f); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 16px; width: 100%; max-width: 540px; max-height: min(90vh, calc(100vh - 36px)); overflow-y: auto; padding: 24px; box-shadow: 0 20px 40px rgba(0,0,0,0.6); box-sizing: border-box; }
717
+ .dsh-cron-modal-overlay { position: fixed; inset: 0; background: var(--dsw-alias-bg-mask, color-mix(in srgb, #000 75%, transparent)); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 16px; box-sizing: border-box; overflow-y: auto; }
718
+ .dsh-cron-modal { background: var(--dsw-alias-bg-layer-2, #1f1f1f); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 16px; width: 100%; max-width: 540px; max-height: min(90vh, calc(100vh - 36px)); overflow-y: auto; padding: 24px; box-shadow: 0 20px 40px color-mix(in srgb, var(--dsw-alias-bg-mask, #000) 60%, transparent); box-sizing: border-box; }
717
719
  .dsh-cron-modal-scroll { max-width: 640px; max-height: min(90vh, calc(100vh - 36px)); overflow-y: auto; }
718
720
  .dsh-cron-modal::-webkit-scrollbar { width: 6px; }
719
721
  .dsh-cron-modal::-webkit-scrollbar-track { background: transparent; }
@@ -772,7 +774,7 @@ window.__ModuleLoader__.load({
772
774
  .dsh-cron-history-empty { text-align: center; padding: 36px 0; color: var(--dsw-alias-label-tertiary, #666); font-size: 13.5px; }
773
775
 
774
776
  .dsh-cron-stats-bar { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 0; }
775
- .dsh-cron-stat-card { background: var(--dsw-alias-bg-layer-2, rgba(255, 255, 255, 0.02)); border: 1px solid var(--dsw-alias-border-l2, #222); border-radius: 8px; padding: 12px 14px; display: flex; flex-direction: column; gap: 4px; }
777
+ .dsh-cron-stat-card { background: var(--dsw-alias-bg-layer-2, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 2%, transparent)); border: 1px solid var(--dsw-alias-border-l2, #222); border-radius: 8px; padding: 12px 14px; display: flex; flex-direction: column; gap: 4px; }
776
778
  .dsh-cron-stat-val { font-size: 18px; font-weight: 700; color: var(--dsw-alias-label-primary, #fff); }
777
779
  .dsh-cron-stat-lbl { font-size: 12px; color: var(--dsw-alias-label-secondary, #777); margin-top: 0; }
778
780
  .dsh-cron-oneshot-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsh-cron-accent); background: var(--dsh-cron-accent-bg); color: var(--dsh-cron-accent); font-weight: 500; }
@@ -787,7 +789,7 @@ window.__ModuleLoader__.load({
787
789
  .dsh-cron-section-title { font-weight: 500; font-size: 12.5px; color: var(--dsw-alias-label-secondary, #aaa); margin-bottom: 10px; }
788
790
  .dsh-cron-secret-row { display: flex; align-items: center; gap: 8px; }
789
791
  .dsh-cron-secret-row input { flex: 1; }
790
- .dsh-cron-badge-ref { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.06)); color: var(--dsw-alias-label-tertiary, #888); white-space: nowrap; }
792
+ .dsh-cron-badge-ref { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: var(--dsw-alias-bg-layer-3, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 6%, transparent)); color: var(--dsw-alias-label-tertiary, #888); white-space: nowrap; }
791
793
  .dsh-cron-status-banner { padding: 10px 14px; border-radius: 8px; font-size: 13px; margin-top: 12px; }
792
794
  .dsh-cron-status-banner[data-type="success"] { background: var(--dsh-cron-success-bg); border: 1px solid var(--dsh-cron-success); color: var(--dsh-cron-success); }
793
795
  .dsh-cron-status-banner[data-type="error"] { background: var(--dsh-cron-danger-bg); border: 1px solid var(--dsh-cron-danger); color: var(--dsh-cron-danger); }
@@ -798,13 +800,13 @@ window.__ModuleLoader__.load({
798
800
  /* #34: sidebar job list */
799
801
  .dsh-cron-sidebar-jobs { display: flex; flex-direction: column; gap: 2px; margin: 2px 0 6px 0; }
800
802
  .dsh-cron-sidebar-jobs-head { display: flex; align-items: center; justify-content: space-between; width: 100%; background: none; border: none; padding: 4px 8px; font: inherit; font-size: 12px; color: var(--dsw-alias-label-tertiary, #888); cursor: pointer; border-radius: 6px; text-align: left; }
801
- .dsh-cron-sidebar-jobs-head:hover { color: var(--dsw-alias-label-primary, #eee); background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.04)); }
803
+ .dsh-cron-sidebar-jobs-head:hover { color: var(--dsw-alias-label-primary, #eee); background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 4%, transparent)); }
802
804
  .dsh-cron-sidebar-jobs-head::after { content: '▾'; font-size: 10px; transition: transform 0.15s ease; }
803
805
  .dsh-cron-sidebar-jobs[data-open="false"] .dsh-cron-sidebar-jobs-head::after { transform: rotate(-90deg); }
804
806
  .dsh-cron-sidebar-jobs[data-open="false"] .dsh-cron-sidebar-jobs-list { display: none; }
805
807
  .dsh-cron-sidebar-jobs-list { display: flex; flex-direction: column; gap: 2px; padding-left: 10px; }
806
808
  .dsh-cron-sidebar-job { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; width: 100%; background: none; border: none; border-left: 2px solid transparent; padding: 3px 8px; font: inherit; cursor: pointer; border-radius: 0 6px 6px 0; text-align: left; }
807
- .dsh-cron-sidebar-job:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.04)); border-left-color: var(--dsh-cron-info); }
809
+ .dsh-cron-sidebar-job:hover { background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 4%, transparent)); border-left-color: var(--dsh-cron-info); }
808
810
  .dsh-cron-sidebar-job-title { font-size: 12px; color: var(--dsw-alias-label-secondary, #ccc); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
809
811
  .dsh-cron-sidebar-job-meta { font-size: 11px; color: var(--dsw-alias-label-tertiary, #777); }
810
812
  .dsh-cron-sidebar-empty, .dsh-cron-sidebar-more { font-size: 11px; color: var(--dsw-alias-label-tertiary, #777); padding: 2px 8px; }
@@ -987,7 +989,9 @@ window.__ModuleLoader__.load({
987
989
  try {
988
990
  sessions.open(sessionId);
989
991
  return true;
990
- } catch (e) {}
992
+ } catch (err) {
993
+ /* sessions.open threw synchronously */
994
+ }
991
995
  }
992
996
 
993
997
  // 2. If the session is not in manager.summaries yet, refresh from the backend
@@ -996,7 +1000,9 @@ window.__ModuleLoader__.load({
996
1000
  await sessions.refresh();
997
1001
  sessions.open(sessionId);
998
1002
  return true;
999
- } catch (e) {}
1003
+ } catch (err) {
1004
+ /* sessions.refresh threw */
1005
+ }
1000
1006
  }
1001
1007
 
1002
1008
  // 3. Async retries until the WebSocket/SSE event adds the session to the list
@@ -1007,7 +1013,9 @@ window.__ModuleLoader__.load({
1007
1013
  try {
1008
1014
  sessions.open(sessionId);
1009
1015
  return true;
1010
- } catch (e) {}
1016
+ } catch (err) {
1017
+ /* retry polling sessions.open threw */
1018
+ }
1011
1019
  }
1012
1020
  // DOM fallback: click the session in the sidebar if it has rendered
1013
1021
  if (typeof document !== 'undefined') {
@@ -1323,7 +1331,7 @@ window.__ModuleLoader__.load({
1323
1331
  if (!node) return;
1324
1332
  if (typeof node.scrollIntoView === 'function') node.scrollIntoView({ block: 'center' });
1325
1333
  node.classList.add('dsh-cron-task-highlight');
1326
- setTimeout(() => { try { node.classList.remove('dsh-cron-task-highlight'); } catch (_) {} }, 2500);
1334
+ setTimeout(() => { try { node.classList.remove('dsh-cron-task-highlight'); } catch (err) { /* node unmounted */ } }, 2500);
1327
1335
  }, [tasks]);
1328
1336
 
1329
1337
  const fetchTasks = async () => {
@@ -1435,7 +1443,9 @@ window.__ModuleLoader__.load({
1435
1443
  try {
1436
1444
  await fetch('/dsh-cron/tasks/' + encodeURIComponent(id) + '/toggle', { method: 'POST' });
1437
1445
  fetchTasks();
1438
- } catch (err) {}
1446
+ } catch (err) {
1447
+ console.debug('[dsh-cron] toggle failed', err);
1448
+ }
1439
1449
  };
1440
1450
 
1441
1451
  const handleRunNow = async (id) => {
@@ -1650,7 +1660,10 @@ window.__ModuleLoader__.load({
1650
1660
  try {
1651
1661
  await fetch('/dsh-cron/tasks/' + encodeURIComponent(id), { method: 'DELETE' });
1652
1662
  fetchTasks();
1653
- } catch (err) {} };
1663
+ } catch (err) {
1664
+ console.debug('[dsh-cron] delete failed', err);
1665
+ }
1666
+ };
1654
1667
 
1655
1668
  const openSettingsModal = async () => {
1656
1669
  setTestStatus(null);
@@ -1668,7 +1681,9 @@ window.__ModuleLoader__.load({
1668
1681
  setSettingsHasDefault(Boolean(data.settings.hasDefaultCredentials));
1669
1682
  setSettingsHasCustomBotToken(Boolean(data.settings.hasCustomBotToken));
1670
1683
  }
1671
- } catch (e) {}
1684
+ } catch (err) {
1685
+ console.debug('[dsh-cron] settings fetch failed', err);
1686
+ }
1672
1687
  };
1673
1688
 
1674
1689
  const handleSaveSettings = async (e) => {
@@ -2715,7 +2730,7 @@ window.__ModuleLoader__.load({
2715
2730
  React.createElement('label', null, T_KEY(t, 'form.sshTargetLabel')),
2716
2731
  React.createElement('input', {
2717
2732
  value: formSshTarget,
2718
- placeholder: 'root@10.0.0.5',
2733
+ placeholder: 'user@example.com',
2719
2734
  onChange: (e) => setFormSshTarget(e.target.value)
2720
2735
  }),
2721
2736
  React.createElement('input', {
@@ -2828,7 +2843,7 @@ window.__ModuleLoader__.load({
2828
2843
  ),
2829
2844
  React.createElement('div', {
2830
2845
  style: {
2831
- background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.02))',
2846
+ background: 'var(--dsw-alias-bg-layer-3, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 2%, transparent))',
2832
2847
  border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)',
2833
2848
  borderRadius: '8px',
2834
2849
  padding: '12px 14px',
@@ -2991,7 +3006,7 @@ window.__ModuleLoader__.load({
2991
3006
  ),
2992
3007
  selectedTaskMeta && React.createElement('div', {
2993
3008
  style: {
2994
- background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.03))',
3009
+ background: 'var(--dsw-alias-bg-layer-3, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 3%, transparent))',
2995
3010
  border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)',
2996
3011
  borderRadius: '8px',
2997
3012
  padding: '10px 14px',
@@ -3181,7 +3196,7 @@ window.__ModuleLoader__.load({
3181
3196
  onChange: (e) => setSettingsChatId(e.target.value)
3182
3197
  })
3183
3198
  ),
3184
- React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.02))', border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)', borderRadius: '8px', padding: '12px 14px', marginBottom: '16px' } },
3199
+ React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-3, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 2%, transparent))', border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)', borderRadius: '8px', padding: '12px 14px', marginBottom: '16px' } },
3185
3200
  React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginBottom: '8px' } },
3186
3201
  React.createElement('input', {
3187
3202
  type: 'checkbox',
@@ -3346,7 +3361,7 @@ window.__ModuleLoader__.load({
3346
3361
  const [updateState, setUpdateState] = React.useState({
3347
3362
  checking: false,
3348
3363
  updating: false,
3349
- currentVersion: '0.2.18',
3364
+ currentVersion: '0.2.19',
3350
3365
  latestVersion: undefined,
3351
3366
  updateAvailable: false,
3352
3367
  notice: null,
@@ -3453,7 +3468,9 @@ window.__ModuleLoader__.load({
3453
3468
  setSaved(true);
3454
3469
  setTimeout(() => setSaved(false), 2500);
3455
3470
  }
3456
- } catch (err) {}
3471
+ } catch (err) {
3472
+ console.debug('[dsh-cron] save settings failed', err);
3473
+ }
3457
3474
  };
3458
3475
 
3459
3476
  const testTelegram = async () => {
@@ -3558,7 +3575,7 @@ window.__ModuleLoader__.load({
3558
3575
  onChange: (e) => setKanbanUrl(e.target.value)
3559
3576
  })
3560
3577
  ),
3561
- React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.02))', border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)', borderRadius: '6px', padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: '8px' } },
3578
+ React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-3, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 2%, transparent))', border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)', borderRadius: '6px', padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: '8px' } },
3562
3579
  React.createElement('label', { className: 'dsh-cron-checkbox-row' },
3563
3580
  React.createElement('input', {
3564
3581
  type: 'checkbox',
@@ -3610,8 +3627,8 @@ window.__ModuleLoader__.load({
3610
3627
  React.createElement('div', {
3611
3628
  className: 'dsh-cron-form-group',
3612
3629
  style: {
3613
- background: 'var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.03))',
3614
- border: '1px solid var(--dsw-alias-border-l2, rgba(255,255,255,0.08))',
3630
+ background: 'var(--dsw-alias-bg-layer-2, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 3%, transparent))',
3631
+ border: '1px solid var(--dsw-alias-border-l2, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 8%, transparent))',
3615
3632
  borderRadius: '8px',
3616
3633
  padding: '14px',
3617
3634
  marginBottom: '16px'
@@ -3786,7 +3803,9 @@ window.__ModuleLoader__.load({
3786
3803
  const res = await fetch('/dsh-cron/tasks?status=active');
3787
3804
  const data = await res.json();
3788
3805
  if (data && data.ok) renderSidebarJobs(state, data.tasks || []);
3789
- } catch (_) {}
3806
+ } catch (err) {
3807
+ console.debug('[dsh-cron] load sidebar jobs failed', err);
3808
+ }
3790
3809
  }
3791
3810
 
3792
3811
  function applySidebarJobsOpen(state) {
@@ -3811,7 +3830,7 @@ window.__ModuleLoader__.load({
3811
3830
  event.preventDefault();
3812
3831
  event.stopPropagation();
3813
3832
  state.open = !state.open;
3814
- try { window.localStorage.setItem(SIDEBAR_JOBS_KEY, state.open ? '1' : '0'); } catch (_) {}
3833
+ try { window.localStorage.setItem(SIDEBAR_JOBS_KEY, state.open ? '1' : '0'); } catch (err) { /* localStorage write failed */ }
3815
3834
  applySidebarJobsOpen(state);
3816
3835
  });
3817
3836
  root.appendChild(headEl);
@@ -3854,7 +3873,7 @@ window.__ModuleLoader__.load({
3854
3873
  function mountSidebarJobs(toggle, label, openTaskSignal) {
3855
3874
  if (typeof document === 'undefined') return () => {};
3856
3875
  const state = { toggle, openTaskSignal, root: undefined, listEl: undefined, headEl: undefined, open: false };
3857
- try { state.open = window.localStorage.getItem(SIDEBAR_JOBS_KEY) === '1'; } catch (_) {}
3876
+ try { state.open = window.localStorage.getItem(SIDEBAR_JOBS_KEY) === '1'; } catch (err) { /* localStorage read failed */ }
3858
3877
 
3859
3878
  const observer = new MutationObserver(() => placeSidebarJobs(state, label));
3860
3879
  observer.observe(document.body, { childList: true, subtree: true });
@@ -3862,7 +3881,7 @@ window.__ModuleLoader__.load({
3862
3881
  loadSidebarJobs(state);
3863
3882
  const fetchTimer = setInterval(() => {
3864
3883
  let isOpen = false;
3865
- try { isOpen = window.localStorage.getItem(SIDEBAR_JOBS_KEY) === '1'; } catch (_) {}
3884
+ try { isOpen = window.localStorage.getItem(SIDEBAR_JOBS_KEY) === '1'; } catch (err) { /* localStorage poll failed */ }
3866
3885
  if (isOpen) loadSidebarJobs(state);
3867
3886
  }, 30000);
3868
3887
 
@@ -4011,7 +4030,9 @@ window.__ModuleLoader__.load({
4011
4030
  if (ctx.locale && typeof ctx.locale.bind === 'function') {
4012
4031
  translate = ctx.locale.bind(NS);
4013
4032
  }
4014
- } catch (e) {}
4033
+ } catch (err) {
4034
+ /* locale bind unavailable, fallback to default translate */
4035
+ }
4015
4036
 
4016
4037
  const toggle = createToggle();
4017
4038
 
@@ -1,3 +1,4 @@
1
+ import { bestEffort } from './best-effort.js';
1
2
  import fs from 'node:fs';
2
3
 
3
4
  /**
@@ -14,10 +15,10 @@ export function parseLlmActionDirectives(text) {
14
15
  while ((match = blockRegex.exec(text)) !== null) {
15
16
  const raw = match[1].trim();
16
17
  if ((raw.startsWith('{') && raw.endsWith('}')) || (raw.startsWith('[') && raw.endsWith(']'))) {
17
- try {
18
+ bestEffort('parse-directive-json', () => {
18
19
  const parsed = JSON.parse(raw);
19
20
  collectDirectives(parsed, directives);
20
- } catch {}
21
+ });
21
22
  }
22
23
  }
23
24
 
@@ -26,10 +27,10 @@ export function parseLlmActionDirectives(text) {
26
27
  const inlineMatch = /\{(?:[^{}]|(\{(?:[^{}]|\{[^{}]*\})*\}))*"dsh_action[s]?"(?:[^{}]|(\{(?:[^{}]|\{[^{}]*\})*\}))*\}/gi;
27
28
  let im;
28
29
  while ((im = inlineMatch.exec(text)) !== null) {
29
- try {
30
+ bestEffort('parse-inline-directive', () => {
30
31
  const parsed = JSON.parse(im[0]);
31
32
  collectDirectives(parsed, directives);
32
- } catch {}
33
+ });
33
34
  }
34
35
  }
35
36
 
@@ -116,7 +117,7 @@ export async function executeLlmActionDirectives({ directives, task, runInfo, sc
116
117
  async function tryCreateGiteaIssue({ title, body, repo = 'goodandready/dsh-cron', fetchFn }) {
117
118
  try {
118
119
  let token = process.env.GITEA_TOKEN;
119
- const credPath = '/mnt/external/Project/DEV/.gitea-agent-credentials.json';
120
+ const credPath = process.env.GITEA_CREDENTIALS_PATH || '';
120
121
  if (!token && fs.existsSync(credPath)) {
121
122
  const data = JSON.parse(fs.readFileSync(credPath, 'utf-8'));
122
123
  token = data?.agents?.antigravity?.token || data?.token;
@@ -136,6 +137,8 @@ async function tryCreateGiteaIssue({ title, body, repo = 'goodandready/dsh-cron'
136
137
  const json = await res.json().catch(() => ({}));
137
138
  return { number: json.number, url: json.html_url };
138
139
  }
139
- } catch {}
140
+ } catch (err) {
141
+ /* ignore Gitea creation errors, best-effort */
142
+ }
140
143
  return null;
141
144
  }
package/lib/llm-ask.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { bestEffort } from './best-effort.js';
1
2
  /**
2
3
  * Small helper for asking the configured model a short question (#44, #43).
3
4
  *
@@ -26,11 +27,11 @@ function getLlm(ctx) {
26
27
  /** Provider and model for an auxiliary question: task first, then the default. */
27
28
  export function resolveAskTarget(ctx, task = {}, preferredModel = '') {
28
29
  let defaultSelection = null;
29
- try {
30
- defaultSelection = ctx && typeof ctx.get === 'function'
30
+ defaultSelection = bestEffort('resolve-default-model', () => {
31
+ return ctx && typeof ctx.get === 'function'
31
32
  ? ctx.get('agentDefaultModel')?.currentSelection?.() || null
32
33
  : null;
33
- } catch {}
34
+ }) || null;
34
35
  return {
35
36
  provider: task.provider || defaultSelection?.provider || '',
36
37
  model: preferredModel || task.model || defaultSelection?.model || '',
@@ -60,12 +61,10 @@ function isCleanFinish(kind) {
60
61
  async function buildUserMessage(text) {
61
62
  const content = [{ type: 'text', text }];
62
63
  const source = { kind: 'plugin', plugin: 'dsh-cron', form: 'helper-call' };
63
- try {
64
- const mod = await import('@deepseek-ai/dsh-llm');
65
- if (mod && typeof mod.createUserMessage === 'function') {
66
- return mod.createUserMessage({ content, source });
67
- }
68
- } catch {}
64
+ const mod = await bestEffort('import-dsh-llm', () => import('@deepseek-ai/dsh-llm'));
65
+ if (mod && typeof mod.createUserMessage === 'function') {
66
+ return mod.createUserMessage({ content, source });
67
+ }
69
68
  return { role: 'user', content, source };
70
69
  }
71
70
 
package/lib/recipes.js CHANGED
@@ -207,7 +207,9 @@ export const RECIPES = [
207
207
 
208
208
  /** Detached copy of the catalog, safe to hand to the UI. */
209
209
  export function listRecipes() {
210
- return RECIPES.map((recipe) => ({ ...recipe, channels: [...(recipe.channels || [])] }));
210
+ const destructive = findDestructiveRecipe();
211
+ const safeRecipes = destructive ? RECIPES.filter((r) => r.id !== destructive.id) : RECIPES;
212
+ return safeRecipes.map((recipe) => ({ ...recipe, channels: [...(recipe.channels || [])] }));
211
213
  }
212
214
 
213
215
  export function recipesByCategory() {
package/lib/runner.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { bestEffort } from './best-effort.js';
1
2
  /**
2
3
  * Helper to detect context window overflow errors in persistent sessions (#145).
3
4
  */
@@ -19,21 +20,21 @@ export function isContextOverflowError(err) {
19
20
  */
20
21
  export function terminateProcessTree(child, sig = 'SIGTERM', { isPosix = process.platform !== 'win32', execFn } = {}) {
21
22
  if (!child || child.killed) return;
22
- try {
23
+ bestEffort('terminate-process-tree', () => {
23
24
  if (isPosix && child.pid) {
24
- try { process.kill(-child.pid, sig); } catch {}
25
+ bestEffort('kill-posix-pid', () => process.kill(-child.pid, sig));
25
26
  } else if (child.pid) {
26
27
  const runner = execFn || ((cmd) => {
27
- try {
28
+ bestEffort('exec-taskkill-fn', () => {
28
29
  import('node:child_process').then(({ exec }) => {
29
30
  exec(cmd, { windowsHide: true }, () => {});
30
31
  }).catch(() => {});
31
- } catch {}
32
+ });
32
33
  });
33
- try { runner(`taskkill /pid ${child.pid} /T /F`); } catch {}
34
- try { child.kill(sig); } catch {}
34
+ bestEffort('exec-taskkill', () => runner(`taskkill /pid ${child.pid} /T /F`));
35
+ bestEffort('child-kill', () => child.kill(sig));
35
36
  }
36
- } catch {}
37
+ });
37
38
  }
38
39
 
39
40
  /**
@@ -145,14 +146,14 @@ export class SessionRunner {
145
146
  resolveCwd(task) {
146
147
  if (task.cwd) return String(task.cwd);
147
148
  if (task.workspaceId && this.ctx && typeof this.ctx.get === 'function') {
148
- try {
149
+ const resolved = bestEffort('resolve-workspace-path', () => {
149
150
  const workspaces = this.ctx.get('workspaces');
150
151
  const ws = workspaces && (typeof workspaces.get === 'function'
151
152
  ? workspaces.get(task.workspaceId)
152
153
  : (workspaces[task.workspaceId] || null));
153
- const resolved = ws && (ws.path || ws.cwd || ws.root || ws.dir);
154
- if (resolved) return String(resolved);
155
- } catch {}
154
+ return ws && (ws.path || ws.cwd || ws.root || ws.dir);
155
+ });
156
+ if (resolved) return String(resolved);
156
157
  return String(task.workspaceId);
157
158
  }
158
159
  return process.cwd();
@@ -239,20 +240,19 @@ export class SessionRunner {
239
240
  async _runViaRemoteWorkspace(task, signal) {
240
241
  if (!task.sshProfileId || !this.ctx || typeof this.ctx.get !== 'function') return null;
241
242
  let svc = null;
242
- try {
243
- svc = this.ctx.get('remoteSsh');
244
- } catch {}
243
+ svc = bestEffort('get-remoteSsh', () => this.ctx.get('remoteSsh'));
245
244
  if (!svc || typeof svc.exec !== 'function') return null;
246
245
 
247
246
  let profile = null;
248
- try {
247
+ profile = bestEffort('get-remoteSsh-profile', () => {
249
248
  const settings = this.ctx.get('settings');
250
249
  if (settings && typeof settings.get === 'function') {
251
250
  const snap = settings.get('dsh-remote-workspace');
252
251
  const profiles = snap && Array.isArray(snap.profiles) ? snap.profiles : [];
253
- profile = profiles.find((p) => p && p.id === task.sshProfileId) || null;
252
+ return profiles.find((p) => p && p.id === task.sshProfileId) || null;
254
253
  }
255
- } catch {}
254
+ return null;
255
+ });
256
256
  if (!profile) {
257
257
  throw new Error(`remote-workspace profile "${task.sshProfileId}" was not found — check dsh-remote-workspace settings, or use sshTarget instead`);
258
258
  }
@@ -295,7 +295,7 @@ export class SessionRunner {
295
295
  terminateProcessTree(child, sig, {
296
296
  isPosix,
297
297
  execFn: (cmd) => {
298
- try { exec(cmd, { windowsHide: true }, () => {}); } catch {}
298
+ bestEffort('exec-cmd', () => exec(cmd, { windowsHide: true }, () => {}));
299
299
  }
300
300
  });
301
301
  };
@@ -438,22 +438,22 @@ export class SessionRunner {
438
438
  // Context window overflow auto-recovery in persistent sessions (#145)
439
439
  if (prep.targetSessionId && isContextOverflowError(err) && !signal?.aborted) {
440
440
  console.warn(`[dsh-cron] Persistent session "${prep.targetSessionId}" exceeded context window (${err.message}). Auto-recovering with fresh session...`);
441
- try {
441
+ bestEffort('archive-rotated-session', () => {
442
442
  const sessions = this.ctx?.get?.('sessions');
443
443
  if (sessions && typeof sessions.archive === 'function') {
444
444
  sessions.archive(prep.targetSessionId);
445
445
  }
446
- } catch {}
446
+ });
447
447
  const rotatedSessionId = `${prep.targetSessionId}-${Date.now().toString(36)}`;
448
448
  console.log(`[dsh-cron] Rotated persistent session "${prep.targetSessionId}" -> "${rotatedSessionId}"`);
449
449
  if (this.store) {
450
- try {
450
+ bestEffort('update-stored-rotated-session', () => {
451
451
  const stored = this.store.get(task.id);
452
452
  if (stored) {
453
453
  stored.targetSessionId = rotatedSessionId;
454
454
  this.store.set(stored);
455
455
  }
456
- } catch {}
456
+ });
457
457
  }
458
458
  task.targetSessionId = rotatedSessionId;
459
459
  effectiveTask.targetSessionId = rotatedSessionId;
@@ -468,16 +468,11 @@ export class SessionRunner {
468
468
  async _prepareAgentRun(task, runtime, signal) {
469
469
  const { randomUUID } = await import('node:crypto');
470
470
  let createUserMessage = (m) => m;
471
- try {
472
- const llmModule = await import('@deepseek-ai/dsh-llm');
473
- if (llmModule && typeof llmModule.createUserMessage === 'function') {
474
- createUserMessage = llmModule.createUserMessage;
475
- }
476
- } catch {}
477
- let sessionModule = null;
478
- try {
479
- sessionModule = await import('@deepseek-ai/dsh-session');
480
- } catch {}
471
+ const llmModule = await bestEffort('import-dsh-llm', () => import('@deepseek-ai/dsh-llm'));
472
+ if (llmModule && typeof llmModule.createUserMessage === 'function') {
473
+ createUserMessage = llmModule.createUserMessage;
474
+ }
475
+ const sessionModule = await bestEffort('import-dsh-session', () => import('@deepseek-ai/dsh-session'));
481
476
 
482
477
  const targetSessionId = resolveTargetSessionId(task.targetSessionId, task.targetSessionReset);
483
478
 
@@ -665,23 +660,23 @@ ${prompt}`;
665
660
  /** Token usage reported by the session, when the core exposes it. */
666
661
  _extractUsage(handle) {
667
662
  const usage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 };
668
- try {
663
+ bestEffort('extract-session-usage', () => {
669
664
  const sessionUsage = handle.agent.session?.usage || handle.agent.usage;
670
665
  if (sessionUsage) {
671
666
  usage.inputTokens = sessionUsage.inputTokens || sessionUsage.promptTokens || 0;
672
667
  usage.outputTokens = sessionUsage.outputTokens || sessionUsage.completionTokens || 0;
673
668
  usage.cacheReadTokens = sessionUsage.cacheReadTokens || sessionUsage.cachedTokens || 0;
674
669
  }
675
- } catch {}
670
+ });
676
671
  return usage;
677
672
  }
678
673
 
679
674
  /** Dispose the session, archive it best-effort, drop the worktree (#30, #29). */
680
675
  async _disposeAgentRun(task, prep, handle) {
681
676
  let archivedSessionId = null;
682
- try {
683
- archivedSessionId = (handle && handle.agent && handle.agent.session && handle.agent.session.id) || null;
684
- } catch {}
677
+ archivedSessionId = bestEffort('get-archived-session-id', () => {
678
+ return (handle && handle.agent && handle.agent.session && handle.agent.session.id) || null;
679
+ }) || null;
685
680
  try {
686
681
  if (handle && typeof handle.dispose === 'function') {
687
682
  await handle.dispose();
@@ -694,12 +689,12 @@ ${prompt}`;
694
689
  // Best-effort: keep ephemeral cron sessions out of the main chat list (#30).
695
690
  // Persistent sessions (targetSessionId configured) stay unarchived so users can interact with them (#143).
696
691
  if (archivedSessionId && this.ctx && this.ctx.get && !prep.targetSessionId) {
697
- try {
692
+ bestEffort('archive-session-cleanup', () => {
698
693
  const sessions = this.ctx.get('sessions');
699
694
  if (sessions && typeof sessions.archive === 'function') {
700
695
  sessions.archive(archivedSessionId);
701
696
  }
702
- } catch {}
697
+ });
703
698
  }
704
699
  // #29: dispose the isolated worktree (kept when keepWorktree is set).
705
700
  if (prep.taskWorktree && !task.keepWorktree) {
package/lib/runtimes.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { bestEffort } from './best-effort.js';
1
2
  /**
2
3
  * Runtime invocation builders for non-LLM task types (#4, #5, #7, #8, #9, #38).
3
4
  *
@@ -68,9 +69,8 @@ function detectPython(task, cwd) {
68
69
  candidates.push(path.join(base, '.venv', 'Scripts', 'python.exe'));
69
70
  candidates.push(path.join(base, 'venv', 'bin', 'python'));
70
71
  for (const candidate of candidates) {
71
- try {
72
- if (fs.existsSync(candidate)) return candidate;
73
- } catch {}
72
+ const exists = bestEffort('check-python-candidate', () => fs.existsSync(candidate));
73
+ if (exists) return candidate;
74
74
  }
75
75
  return process.platform === 'win32' ? 'python' : 'python3';
76
76
  }
package/lib/scheduler.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { bestEffort } from './best-effort.js';
1
2
  import { Cron } from 'croner';
2
3
  import { exec } from 'node:child_process';
3
4
  import fs from 'node:fs';
@@ -341,9 +342,7 @@ export class TaskScheduler {
341
342
  costUsd: 0,
342
343
  };
343
344
 
344
- try {
345
- this.store.recordRun(task.id, runInfo);
346
- } catch {}
345
+ bestEffort('record-run', () => this.store.recordRun(task.id, runInfo), this.logger);
347
346
 
348
347
  await this.deliverNotifications(task, runInfo);
349
348
 
@@ -404,30 +403,22 @@ export class TaskScheduler {
404
403
  stopAll() {
405
404
  this.stopHeartbeatWatcher();
406
405
  for (const [id, job] of this.jobs.entries()) {
407
- try {
408
- job.stop();
409
- } catch {}
406
+ bestEffort('stop-job', () => job.stop(), this.logger);
410
407
  }
411
408
  this.jobs.clear();
412
409
 
413
410
  for (const [id, timer] of this.timers.entries()) {
414
- try {
415
- clearTimeout(timer);
416
- } catch {}
411
+ bestEffort('clear-timer', () => clearTimeout(timer), this.logger);
417
412
  }
418
413
  this.timers.clear();
419
414
 
420
415
  for (const [id, timer] of this.retryTimers.entries()) {
421
- try {
422
- clearTimeout(timer);
423
- } catch {}
416
+ bestEffort('clear-retry-timer', () => clearTimeout(timer), this.logger);
424
417
  }
425
418
  this.retryTimers.clear();
426
419
 
427
420
  for (const [, run] of this.running.entries()) {
428
- try {
429
- run.controller.abort(new Error('Scheduler stopped'));
430
- } catch {}
421
+ bestEffort('abort-running', () => run.controller.abort(new Error('Scheduler stopped')), this.logger);
431
422
  }
432
423
  this.running.clear();
433
424
  }
@@ -994,9 +985,7 @@ export class TaskScheduler {
994
985
  }
995
986
  const running = this.running.get(taskId);
996
987
  if (running) {
997
- try {
998
- running.controller.abort(new Error('Task paused'));
999
- } catch {}
988
+ bestEffort('abort-paused-task', () => running.controller.abort(new Error('Task paused')), this.logger);
1000
989
  this.running.delete(taskId);
1001
990
  }
1002
991
  this.queue = this.queue.filter((item) => item.taskId !== taskId);
@@ -1016,9 +1005,7 @@ export class TaskScheduler {
1016
1005
  }
1017
1006
  const running = this.running.get(taskId);
1018
1007
  if (running) {
1019
- try {
1020
- running.controller.abort(new Error('Task deleted'));
1021
- } catch {}
1008
+ bestEffort('abort-deleted-task', () => running.controller.abort(new Error('Task deleted')), this.logger);
1022
1009
  this.running.delete(taskId);
1023
1010
  }
1024
1011
  this.queue = this.queue.filter((item) => item.taskId !== taskId);
package/lib/secrets.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { bestEffort } from './best-effort.js';
1
2
  /**
2
3
  * Credential handling for dsh-cron (#51).
3
4
  *
@@ -54,10 +55,8 @@ export async function resolveCredentialValue(ctx, refName) {
54
55
  // Prefer the harness ref factory; fall back to the plain name when the
55
56
  // package is not installed (keeps tests and trimmed builds working).
56
57
  let ref = name;
57
- try {
58
- const mod = await import('@deepseek-ai/dsh-credentials');
59
- if (mod && typeof mod.credentialRef === 'function') ref = mod.credentialRef(name);
60
- } catch {}
58
+ const mod = await bestEffort('import-credentials', () => import('@deepseek-ai/dsh-credentials'));
59
+ if (mod && typeof mod.credentialRef === 'function') ref = mod.credentialRef(name);
61
60
  const hit = await creds.resolve(ref);
62
61
  if (hit && hit.value) return String(hit.value);
63
62
  }
@@ -70,7 +69,7 @@ export async function resolveCredentialValue(ctx, refName) {
70
69
 
71
70
  /** Read the best-effort Telegram defaults from the messenger-gateway file. */
72
71
  function readGatewayToken() {
73
- try {
72
+ return bestEffort('read-gateway-token', () => {
74
73
  const p = getDshSettingsPath();
75
74
  if (!fs.existsSync(p)) return null;
76
75
  const text = fs.readFileSync(p, 'utf-8');
@@ -82,8 +81,8 @@ function readGatewayToken() {
82
81
  const hit = /^[ \t]+botToken:[ \t]*(.+?)[ \t]*$/.exec(line);
83
82
  if (hit) return hit[1].replace(/^['"]|['"]$/g, '').trim() || null;
84
83
  }
85
- } catch {}
86
- return null;
84
+ return null;
85
+ }) || null;
87
86
  }
88
87
 
89
88
  /**
package/lib/store.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { bestEffort } from './best-effort.js';
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { getDshDefaultTelegramCredentials } from './telegram.js';
@@ -68,7 +69,7 @@ export class TaskStore {
68
69
  const raw = fs.readFileSync(this.filePath, 'utf-8');
69
70
  loadData(raw);
70
71
  if (!fs.existsSync(bakPath)) {
71
- try { fs.copyFileSync(this.filePath, bakPath); } catch {}
72
+ bestEffort('backup-initial', () => fs.copyFileSync(this.filePath, bakPath));
72
73
  }
73
74
  } catch (err) {
74
75
  console.error('[dsh-cron] store read error (corrupted file):', err.message);
@@ -93,7 +94,7 @@ export class TaskStore {
93
94
  try {
94
95
  const rawBak = fs.readFileSync(bakPath, 'utf-8');
95
96
  loadData(rawBak);
96
- try { fs.copyFileSync(bakPath, this.filePath); } catch {}
97
+ bestEffort('restore-from-bak', () => fs.copyFileSync(bakPath, this.filePath));
97
98
  console.warn(`[dsh-cron] restored missing tasks store from ${bakPath}`);
98
99
  } catch (bakErr) {
99
100
  console.error('[dsh-cron] failed to restore tasks store from .bak:', bakErr.message);
@@ -119,9 +120,7 @@ export class TaskStore {
119
120
  const tmp = `${this.filePath}.tmp.${process.pid}_${Date.now()}_${rand}`;
120
121
  fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
121
122
  fs.renameSync(tmp, this.filePath);
122
- try {
123
- fs.copyFileSync(this.filePath, `${this.filePath}.bak`);
124
- } catch {}
123
+ bestEffort('backup-on-save', () => fs.copyFileSync(this.filePath, `${this.filePath}.bak`));
125
124
  } catch (err) {
126
125
  console.error('[dsh-cron] store save error:', err.message);
127
126
  }
@@ -393,9 +392,7 @@ export class TaskStore {
393
392
  const archivePath = path.join(dir, 'tasks-history-archive.json');
394
393
  let archive = {};
395
394
  if (fs.existsSync(archivePath)) {
396
- try {
397
- archive = JSON.parse(fs.readFileSync(archivePath, 'utf-8')) || {};
398
- } catch {}
395
+ archive = bestEffort('parse-history-archive', () => JSON.parse(fs.readFileSync(archivePath, 'utf-8'))) || {};
399
396
  }
400
397
  if (!Array.isArray(archive[id])) archive[id] = [];
401
398
  archive[id].push(...overflowRuns);
package/lib/templates.js CHANGED
@@ -51,7 +51,7 @@ export function buildTemplateVars(task, runInfo = {}) {
51
51
  const error = runInfo.error || (status === 'error' || status === 'timeout' ? runInfo.output || '' : '');
52
52
  const usage = runInfo.usage || {};
53
53
  const tokens = (usage.inputTokens || 0) + (usage.outputTokens || 0) + (usage.cacheReadTokens || 0);
54
- return {
54
+ const vars = {
55
55
  title: task.title || 'Task',
56
56
  id: task.id || '',
57
57
  status,
@@ -65,6 +65,10 @@ export function buildTemplateVars(task, runInfo = {}) {
65
65
  model: runInfo.model || '',
66
66
  diagnosis: truncateText(runInfo.diagnosis || ''),
67
67
  };
68
+ for (const key of TEMPLATE_VARIABLES) {
69
+ if (vars[key] === undefined) vars[key] = '';
70
+ }
71
+ return vars;
68
72
  }
69
73
 
70
74
  /** Replace {var} placeholders; unknown placeholders are left untouched. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.2.18",
3
+ "version": "0.2.19",
4
4
  "description": "Scheduled cron tasks, background automation and agent execution for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -45,8 +45,8 @@
45
45
  "client": {
46
46
  "platform": "web",
47
47
  "inject": [
48
- "locale",
49
- "slots"
48
+ "@deepseek-ai/dsh-client-locale",
49
+ "@deepseek-ai/dsh-client-ui-slots"
50
50
  ]
51
51
  }
52
52
  },