@goodandready/dsh-cron 0.2.49 → 0.2.50

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/lib/api.js CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { randomUUID, createHash } from 'node:crypto';
9
9
  import { parseScheduleExpression } from './scheduler.js';
10
- import { normalizeTaskType, CODE_EXECUTING_TYPES } from './runtimes.js';
10
+ import { normalizeTaskType, CODE_EXECUTING_TYPES, EXTERNAL_RUNTIMES } from './runtimes.js';
11
11
  import { CHANNEL_IDS, unknownChannelIds } from './channels.js';
12
12
  import { applyTaskPatch } from './task-patch.js';
13
13
  import { MANAGED_BY_CONFIG, configOwnedMessage } from './config-jobs.js';
@@ -265,7 +265,7 @@ function buildTaskRecord(store, body, taskType, parsed) {
265
265
  misfirePolicy: ['skip', 'runOnce', 'catchUpAll'].includes(body.misfirePolicy) ? body.misfirePolicy : (current ? (current.misfirePolicy || 'skip') : 'skip'),
266
266
  maxRetries: body.maxRetries !== undefined ? Math.max(0, Number(body.maxRetries) || 0) : (current ? (Number(current.maxRetries) || 0) : 0),
267
267
  retryBackoffMs: body.retryBackoffMs !== undefined ? Math.max(1000, Number(body.retryBackoffMs) || 30000) : (current ? (Number(current.retryBackoffMs) || 30000) : 30000),
268
- permissionPreset: ['default', 'read-only', 'workspace-write', 'full'].includes(body.permissionPreset) ? body.permissionPreset : (current ? (current.permissionPreset || 'default') : 'default'),
268
+ permissionPreset: !EXTERNAL_RUNTIMES.includes(taskType) && ['default', 'read-only', 'workspace-write', 'full'].includes(body.permissionPreset) ? body.permissionPreset : (current && !EXTERNAL_RUNTIMES.includes(taskType) ? (current.permissionPreset || 'default') : 'default'),
269
269
  ...mergeExecutionFields(current, body, parsed),
270
270
  };
271
271
  }
package/lib/client.js CHANGED
@@ -137,6 +137,7 @@ window.__ModuleLoader__.load({
137
137
  'modal.params': 'Parameters',
138
138
  'modal.history': 'Run history ({count})',
139
139
  'modal.cancel': 'Cancel',
140
+ 'modal.confirmDiscard': 'Discard unsaved changes?',
140
141
  'modal.save': 'Save changes',
141
142
  'modal.create': 'Create task',
142
143
  'modal.close': 'Close',
@@ -492,6 +493,7 @@ window.__ModuleLoader__.load({
492
493
  'modal.params': '配置参数',
493
494
  'modal.history': '运行历史 ({count})',
494
495
  'modal.cancel': '取消',
496
+ 'modal.confirmDiscard': '放弃未保存的内容?',
495
497
  'modal.save': '保存修改',
496
498
  'modal.create': '创建任务',
497
499
  'history.loading': '正在加载历史记录...',
@@ -1338,6 +1340,80 @@ window.__ModuleLoader__.load({
1338
1340
  const [formWorkflowName, setFormWorkflowName] = React.useState('');
1339
1341
  const [stats, setStats] = React.useState(null);
1340
1342
 
1343
+ const initialFormSnapshotRef = React.useRef(null);
1344
+
1345
+ const getFormSnapshot = () => JSON.stringify({
1346
+ title: String(formTitle || '').trim(),
1347
+ schedule: String(formSchedule || '').trim(),
1348
+ type: String(formType || 'llm'),
1349
+ prompt: String(formPrompt || '').trim(),
1350
+ provider: String(formProvider || ''),
1351
+ model: String(formModel || ''),
1352
+ fallbackModel: String(formFallbackModel || ''),
1353
+ silentRule: String(formSilentRule || ''),
1354
+ inspectOnFailure: Boolean(formInspectOnFailure),
1355
+ delivery: String(formDelivery || 'isolated'),
1356
+ notifyTelegram: Boolean(formNotifyTelegram),
1357
+ channels: Array.isArray(formChannels) ? [...formChannels].sort() : [],
1358
+ template: String(formTemplate || '').trim(),
1359
+ onlyOnFailure: Boolean(formOnlyOnFailure),
1360
+ timeoutSeconds: Number(formTimeoutSeconds) || 1800,
1361
+ overlapPolicy: String(formOverlapPolicy || 'skip'),
1362
+ kanbanMode: String(formKanbanMode || 'none'),
1363
+ onSuccess: String(formOnSuccess || '').trim(),
1364
+ onFailure: String(formOnFailure || '').trim(),
1365
+ heartbeatIntervalSeconds: Number(formHeartbeatIntervalSeconds) || 0,
1366
+ gracePeriodSeconds: Number(formGracePeriodSeconds) || 300,
1367
+ preflightType: String(formPreflightType || 'none'),
1368
+ preflightTarget: String(formPreflightTarget || '').trim(),
1369
+ priority: Number(formPriority) || 5,
1370
+ concurrencyGroup: String(formConcurrencyGroup || 'default').trim(),
1371
+ selfHealingCommand: String(formSelfHealingCommand || '').trim(),
1372
+ autoDiagnose: Boolean(formAutoDiagnose),
1373
+ timezone: String(formTimezone || '').trim(),
1374
+ misfirePolicy: String(formMisfirePolicy || 'skip'),
1375
+ maxRetries: Number(formMaxRetries) || 0,
1376
+ permissionPreset: String(formPermissionPreset || 'default'),
1377
+ agentPreset: String(formAgentPreset || '').trim(),
1378
+ targetSessionId: String(formTargetSessionId || '').trim(),
1379
+ targetSessionReset: String(formTargetSessionReset || 'never'),
1380
+ env: String(formEnv || '').trim(),
1381
+ cwd: String(formCwd || '').trim(),
1382
+ workspaceId: String(formWorkspaceId || '').trim(),
1383
+ worktree: Boolean(formWorktree),
1384
+ keepWorktree: Boolean(formKeepWorktree),
1385
+ httpMethod: String(formHttpMethod || 'GET'),
1386
+ httpUrl: String(formHttpUrl || '').trim(),
1387
+ httpHttpHeaders: String(formHttpHeaders || '').trim(),
1388
+ httpBody: String(formHttpBody || '').trim(),
1389
+ sshProfileId: String(formSshProfileId || '').trim(),
1390
+ sshTarget: String(formSshTarget || '').trim(),
1391
+ sshKeyPath: String(formSshKeyPath || '').trim(),
1392
+ dockerImage: String(formDockerImage || '').trim(),
1393
+ nodePath: String(formNodePath || '').trim(),
1394
+ pythonPath: String(formPythonPath || '').trim(),
1395
+ skillName: String(formSkillName || '').trim(),
1396
+ workflowName: String(formWorkflowName || '').trim(),
1397
+ });
1398
+
1399
+ const isFormDirty = () => {
1400
+ if (!manualModalOpen) return false;
1401
+ if (initialFormSnapshotRef.current !== null) {
1402
+ return getFormSnapshot() !== initialFormSnapshotRef.current;
1403
+ }
1404
+ return Boolean(formTitle.trim() || formPrompt.trim());
1405
+ };
1406
+
1407
+ const closeManualModalWithConfirm = () => {
1408
+ if (isFormDirty()) {
1409
+ if (!confirm(T_KEY(t, 'modal.confirmDiscard') || 'Discard unsaved changes?')) {
1410
+ return;
1411
+ }
1412
+ }
1413
+ initialFormSnapshotRef.current = null;
1414
+ setManualModalOpen(false);
1415
+ };
1416
+
1341
1417
  // Live execution indicator (#35): a light 1s tick only while some task
1342
1418
  // is actually running.
1343
1419
  const tasksRef = React.useRef([]);
@@ -1379,7 +1455,7 @@ window.__ModuleLoader__.load({
1379
1455
  const onKey = (e) => {
1380
1456
  if (e.key === 'Escape') {
1381
1457
  if (dropdownOpen) { setDropdownOpen(false); return; }
1382
- if (manualModalOpen) { setManualModalOpen(false); return; }
1458
+ if (manualModalOpen) { closeManualModalWithConfirm(); return; }
1383
1459
  if (settingsModalOpen) { setSettingsModalOpen(false); return; }
1384
1460
  if (dshModalOpen && !dshLoading) { setDshModalOpen(false); return; }
1385
1461
  if (onClose) onClose();
@@ -2018,6 +2094,59 @@ window.__ModuleLoader__.load({
2018
2094
  setFormModel(defaultModelInfo?.model || '');
2019
2095
  if (defaultModelInfo?.provider) loadModels(defaultModelInfo.provider);
2020
2096
  }
2097
+ initialFormSnapshotRef.current = JSON.stringify({
2098
+ title: String((preset && preset.title) || '').trim(),
2099
+ schedule: String((preset && preset.schedule) || (preset ? '' : '0 9 * * 1-5')).trim(),
2100
+ type: String((preset && preset.type) || 'llm'),
2101
+ prompt: String((preset && preset.prompt) || '').trim(),
2102
+ provider: String(defaultModelInfo?.provider || ''),
2103
+ model: String(defaultModelInfo?.model || ''),
2104
+ fallbackModel: String((preset && preset.fallbackModel) || '').trim(),
2105
+ silentRule: String((preset && preset.silentRule) || '').trim(),
2106
+ inspectOnFailure: Boolean(preset && preset.inspectOnFailure),
2107
+ delivery: 'isolated',
2108
+ notifyTelegram: false,
2109
+ channels: Array.isArray(preset && preset.channels) ? [...preset.channels].filter((c) => DELIVERY_CHANNELS.includes(c)).sort() : [],
2110
+ template: '',
2111
+ onlyOnFailure: Boolean(preset && preset.onlyOnFailure),
2112
+ timeoutSeconds: 1800,
2113
+ overlapPolicy: 'skip',
2114
+ kanbanMode: 'none',
2115
+ onSuccess: '',
2116
+ onFailure: '',
2117
+ heartbeatIntervalSeconds: 0,
2118
+ gracePeriodSeconds: 300,
2119
+ preflightType: 'none',
2120
+ preflightTarget: '',
2121
+ priority: 5,
2122
+ concurrencyGroup: 'default',
2123
+ selfHealingCommand: '',
2124
+ autoDiagnose: false,
2125
+ timezone: '',
2126
+ misfirePolicy: 'skip',
2127
+ maxRetries: 0,
2128
+ permissionPreset: 'default',
2129
+ agentPreset: '',
2130
+ targetSessionId: '',
2131
+ targetSessionReset: 'never',
2132
+ env: '',
2133
+ cwd: '',
2134
+ workspaceId: '',
2135
+ worktree: false,
2136
+ keepWorktree: false,
2137
+ httpMethod: 'GET',
2138
+ httpUrl: '',
2139
+ httpHttpHeaders: '',
2140
+ httpBody: '',
2141
+ sshProfileId: '',
2142
+ sshTarget: '',
2143
+ sshKeyPath: '',
2144
+ dockerImage: '',
2145
+ nodePath: '',
2146
+ pythonPath: '',
2147
+ skillName: '',
2148
+ workflowName: '',
2149
+ });
2021
2150
  setManualModalOpen(true);
2022
2151
  };
2023
2152
 
@@ -2131,7 +2260,7 @@ window.__ModuleLoader__.load({
2131
2260
  timezone: formTimezone.trim(),
2132
2261
  misfirePolicy: formMisfirePolicy,
2133
2262
  maxRetries: Number(formMaxRetries) || 0,
2134
- permissionPreset: formPermissionPreset,
2263
+ permissionPreset: isAgentType ? formPermissionPreset : 'default',
2135
2264
  agentPreset: formAgentPreset.trim() || undefined,
2136
2265
  targetSessionId: formTargetSessionId.trim() || undefined,
2137
2266
  targetSessionReset: formTargetSessionReset || 'never',
@@ -2179,6 +2308,7 @@ window.__ModuleLoader__.load({
2179
2308
  body: JSON.stringify(payload)
2180
2309
  });
2181
2310
  if (!data.ok) { alert(data.error || T_KEY(t, 'settings.saveError', { error: '' })); return; }
2311
+ initialFormSnapshotRef.current = null;
2182
2312
  setManualModalOpen(false);
2183
2313
  fetchTasks();
2184
2314
  } catch (err) { alert(err.message); }
@@ -2563,7 +2693,12 @@ window.__ModuleLoader__.load({
2563
2693
  ),
2564
2694
 
2565
2695
  // 1. Manual creation / Edit modal
2566
- manualModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setManualModalOpen(false) },
2696
+ manualModalOpen && React.createElement('div', {
2697
+ className: 'dsh-cron-modal-overlay',
2698
+ onClick: (e) => {
2699
+ if (e.target === e.currentTarget) closeManualModalWithConfirm();
2700
+ }
2701
+ },
2567
2702
  React.createElement('div', {
2568
2703
  className: 'dsh-cron-modal dsh-cron-modal-scroll',
2569
2704
  style: { maxWidth: '640px' },
@@ -2666,7 +2801,7 @@ window.__ModuleLoader__.load({
2666
2801
  React.createElement('button', {
2667
2802
  type: 'button',
2668
2803
  className: 'dsh-cron-btn-primary',
2669
- onClick: () => setManualModalOpen(false)
2804
+ onClick: closeManualModalWithConfirm
2670
2805
  }, T_KEY(t, 'modal.close'))
2671
2806
  )
2672
2807
  ) :
@@ -2780,7 +2915,32 @@ window.__ModuleLoader__.load({
2780
2915
  )
2781
2916
  )
2782
2917
  ),
2783
- React.createElement('div', { className: 'dsh-cron-form-row' },
2918
+ AGENT_FORM_TYPES.includes(formType) ? (
2919
+ React.createElement('div', { className: 'dsh-cron-form-row' },
2920
+ React.createElement('div', { className: 'dsh-cron-form-group' },
2921
+ React.createElement('label', null, T_KEY(t, 'form.retriesLabel')),
2922
+ React.createElement('input', {
2923
+ type: 'number',
2924
+ min: 0,
2925
+ value: formMaxRetries,
2926
+ placeholder: '0',
2927
+ onChange: (e) => setFormMaxRetries(e.target.value)
2928
+ })
2929
+ ),
2930
+ React.createElement('div', { className: 'dsh-cron-form-group' },
2931
+ React.createElement('label', null, T_KEY(t, 'form.permissionLabel')),
2932
+ React.createElement('select', {
2933
+ value: formPermissionPreset,
2934
+ onChange: (e) => setFormPermissionPreset(e.target.value)
2935
+ },
2936
+ React.createElement('option', { value: 'default' }, T_KEY(t, 'form.permissionDefault')),
2937
+ React.createElement('option', { value: 'read-only' }, T_KEY(t, 'form.permissionReadonly')),
2938
+ React.createElement('option', { value: 'workspace-write' }, T_KEY(t, 'form.permissionWrite')),
2939
+ React.createElement('option', { value: 'full' }, T_KEY(t, 'form.permissionFull'))
2940
+ )
2941
+ )
2942
+ )
2943
+ ) : (
2784
2944
  React.createElement('div', { className: 'dsh-cron-form-group' },
2785
2945
  React.createElement('label', null, T_KEY(t, 'form.retriesLabel')),
2786
2946
  React.createElement('input', {
@@ -2790,18 +2950,6 @@ window.__ModuleLoader__.load({
2790
2950
  placeholder: '0',
2791
2951
  onChange: (e) => setFormMaxRetries(e.target.value)
2792
2952
  })
2793
- ),
2794
- React.createElement('div', { className: 'dsh-cron-form-group' },
2795
- React.createElement('label', null, T_KEY(t, 'form.permissionLabel')),
2796
- React.createElement('select', {
2797
- value: formPermissionPreset,
2798
- onChange: (e) => setFormPermissionPreset(e.target.value)
2799
- },
2800
- React.createElement('option', { value: 'default' }, T_KEY(t, 'form.permissionDefault')),
2801
- React.createElement('option', { value: 'read-only' }, T_KEY(t, 'form.permissionReadonly')),
2802
- React.createElement('option', { value: 'workspace-write' }, T_KEY(t, 'form.permissionWrite')),
2803
- React.createElement('option', { value: 'full' }, T_KEY(t, 'form.permissionFull'))
2804
- )
2805
2953
  )
2806
2954
  ),
2807
2955
  React.createElement('div', { className: 'dsh-cron-form-group' },
@@ -3249,7 +3397,7 @@ window.__ModuleLoader__.load({
3249
3397
  React.createElement('button', {
3250
3398
  type: 'button',
3251
3399
  className: 'dsh-cron-btn-secondary',
3252
- onClick: () => setManualModalOpen(false)
3400
+ onClick: closeManualModalWithConfirm
3253
3401
  }, T_KEY(t, 'modal.cancel')),
3254
3402
  React.createElement('button', {
3255
3403
  type: 'submit',
package/lib/cron-tool.js CHANGED
@@ -150,7 +150,7 @@ export const cronToolParameters = {
150
150
  permissionPreset: {
151
151
  type: 'string',
152
152
  enum: ['default', 'read-only', 'workspace-write', 'full'],
153
- description: 'Permission preset applied to the task session',
153
+ description: 'Permission preset applied to the task session (only used for agent session tasks: llm, skill, workflow)',
154
154
  },
155
155
  kanbanMode: {
156
156
  type: 'string',
@@ -1,4 +1,4 @@
1
- import { normalizeTaskType } from './runtimes.js';
1
+ import { normalizeTaskType, EXTERNAL_RUNTIMES } from './runtimes.js';
2
2
  import { validateTaskType } from './task-transfer.js';
3
3
  import { parseScheduleExpression } from './scheduler.js';
4
4
  import { CHANNEL_IDS } from './channels.js';
@@ -44,7 +44,7 @@ export function executeCreateTask(store, scheduler, args) {
44
44
  misfirePolicy: ['skip', 'runOnce', 'catchUpAll'].includes(args.misfirePolicy) ? args.misfirePolicy : 'skip',
45
45
  maxRetries: Number(args.maxRetries) > 0 ? Number(args.maxRetries) : 0,
46
46
  retryBackoffMs: Number(args.retryBackoffMs) > 0 ? Number(args.retryBackoffMs) : 30000,
47
- permissionPreset: ['default', 'read-only', 'workspace-write', 'full'].includes(args.permissionPreset) ? args.permissionPreset : 'default',
47
+ permissionPreset: !EXTERNAL_RUNTIMES.includes(taskType) && ['default', 'read-only', 'workspace-write', 'full'].includes(args.permissionPreset) ? args.permissionPreset : 'default',
48
48
  kanbanMode: args.kanbanMode || 'none',
49
49
  channels: Array.isArray(args.channels) ? args.channels.filter((c) => CHANNEL_IDS.includes(c)) : undefined,
50
50
  template: args.template ? String(args.template) : '',
package/lib/task-patch.js CHANGED
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { parseScheduleExpression } from './scheduler.js';
12
- import { normalizeTaskType, CODE_EXECUTING_TYPES } from './runtimes.js';
12
+ import { normalizeTaskType, CODE_EXECUTING_TYPES, EXTERNAL_RUNTIMES } from './runtimes.js';
13
13
  import { PATCHABLE_TASK_FIELDS, validateTaskType } from './task-transfer.js';
14
14
  import { pickPatchableFields, restoreTaskSecrets } from './http-utils.js';
15
15
  import { unknownChannelIds } from './channels.js';
@@ -95,6 +95,10 @@ export function buildTaskPatch(current, body) {
95
95
  if (patch.tokenLimit !== undefined) {
96
96
  patch.tokenLimit = patch.tokenLimit === null ? null : (Number(patch.tokenLimit) > 0 ? Number(patch.tokenLimit) : null);
97
97
  }
98
+ const effectiveType = patch.type !== undefined ? normalizeTaskType(patch.type) : current.type;
99
+ if (EXTERNAL_RUNTIMES.includes(effectiveType) && patch.permissionPreset !== undefined) {
100
+ patch.permissionPreset = 'default';
101
+ }
98
102
  // Issue #310 / GH #5: strip undefined properties from patch
99
103
  for (const k of Object.keys(patch)) {
100
104
  if (patch[k] === undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.2.49",
3
+ "version": "0.2.50",
4
4
  "description": "Background automation runner for DSH: isolated agent runs, script/HTTP/SSH/Docker runtimes, cost guard, notifications, heartbeats.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",