cc-viewer 1.9.12 → 1.9.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +8 -0
  2. package/dist/assets/{App-Xqmx9TR5.js → App-BzV1ieDH.js} +2 -2
  3. package/dist/assets/{MdxEditorPanel-BwTbSU48.js → MdxEditorPanel-DI3B4gsM.js} +1 -1
  4. package/dist/assets/{Mobile-BOXHO9-C.js → Mobile-qBkAnXy3.js} +1 -1
  5. package/dist/assets/{ProxyStatsModal-BzfIUfG1.js → ProxyStatsModal-B8K0AtAq.js} +1 -1
  6. package/dist/assets/{_baseUniq-BvPjRimO.js → _baseUniq-CsF04anE.js} +1 -1
  7. package/dist/assets/{arc-BzZyqc6p.js → arc-BOacYe6N.js} +1 -1
  8. package/dist/assets/{architectureDiagram-Q4EWVU46-B1oszHzA.js → architectureDiagram-Q4EWVU46-BSUEfjC_.js} +1 -1
  9. package/dist/assets/{blockDiagram-DXYQGD6D-Ndp4o_QB.js → blockDiagram-DXYQGD6D-CC37UGtW.js} +1 -1
  10. package/dist/assets/{c4Diagram-AHTNJAMY-DlDE-ALt.js → c4Diagram-AHTNJAMY-IzgRD4uO.js} +1 -1
  11. package/dist/assets/{channel-BhXx9y6q.js → channel-C6PxyQUn.js} +1 -1
  12. package/dist/assets/{chunk-4BX2VUAB-CM-2e6q_.js → chunk-4BX2VUAB-cdQt_dcU.js} +1 -1
  13. package/dist/assets/{chunk-4TB4RGXK-GWLkQKRO.js → chunk-4TB4RGXK-BpTDA_Pb.js} +1 -1
  14. package/dist/assets/{chunk-55IACEB6-B6LOFqRZ.js → chunk-55IACEB6-CcitNo0P.js} +1 -1
  15. package/dist/assets/{chunk-EDXVE4YY-Ct3e09Ku.js → chunk-EDXVE4YY-BjKwZ9I7.js} +1 -1
  16. package/dist/assets/{chunk-FMBD7UC4-DK-vXpZP.js → chunk-FMBD7UC4-vZDVX3tI.js} +1 -1
  17. package/dist/assets/{chunk-OYMX7WX6-CWxZc_DU.js → chunk-OYMX7WX6-DGkYahGo.js} +1 -1
  18. package/dist/assets/{chunk-QZHKN3VN-jp8ISHue.js → chunk-QZHKN3VN-C9065hSE.js} +1 -1
  19. package/dist/assets/{chunk-YZCP3GAM-DtYet6WV.js → chunk-YZCP3GAM-D_pcTg6_.js} +1 -1
  20. package/dist/assets/classDiagram-6PBFFD2Q-BUmU_G-i.js +1 -0
  21. package/dist/assets/classDiagram-v2-HSJHXN6E-BUmU_G-i.js +1 -0
  22. package/dist/assets/clone-CvoOne2K.js +1 -0
  23. package/dist/assets/{cose-bilkent-S5V4N54A-lNC-slVX.js → cose-bilkent-S5V4N54A-BPPFMIGv.js} +1 -1
  24. package/dist/assets/{dagre-KV5264BT-vm6U82qq.js → dagre-KV5264BT-fjzhKO5o.js} +1 -1
  25. package/dist/assets/{diagram-5BDNPKRD-Dq3FXh-U.js → diagram-5BDNPKRD-5L6FzTry.js} +1 -1
  26. package/dist/assets/{diagram-G4DWMVQ6-BxF85wcF.js → diagram-G4DWMVQ6-GOwcGpff.js} +1 -1
  27. package/dist/assets/{diagram-MMDJMWI5-CFCTeMz0.js → diagram-MMDJMWI5-BDsFE25W.js} +1 -1
  28. package/dist/assets/{diagram-TYMM5635-Bui5Azdg.js → diagram-TYMM5635-BqjpbSGH.js} +1 -1
  29. package/dist/assets/{erDiagram-SMLLAGMA-DrpUO1Kb.js → erDiagram-SMLLAGMA-DWO3Dk1v.js} +1 -1
  30. package/dist/assets/{flowDiagram-DWJPFMVM-Bb3amxiK.js → flowDiagram-DWJPFMVM-Br4nNBVd.js} +1 -1
  31. package/dist/assets/{ganttDiagram-T4ZO3ILL-BRwCFM3C.js → ganttDiagram-T4ZO3ILL-9sN-VOhw.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-UUTBAWPF-DVXbBwhs.js → gitGraphDiagram-UUTBAWPF-BsPcTAKr.js} +1 -1
  33. package/dist/assets/{graph-Dh22wYWs.js → graph-o31GzaB6.js} +1 -1
  34. package/dist/assets/{index-CfFFgxxf.js → index-B3b0lRUB.js} +1 -1
  35. package/dist/assets/{index-CWZz2pyj.js → index-CU0gt6m1.js} +2 -2
  36. package/dist/assets/{index-BwZB7UwN.js → index-CXMr3tiK.js} +1 -1
  37. package/dist/assets/{index-UlO7Q5dX.js → index-CuvDJTY7.js} +1 -1
  38. package/dist/assets/{index-Cfm4N4ao.js → index-DAjx9lT3.js} +1 -1
  39. package/dist/assets/{index-xZXOwglE.js → index-DyV81jSF.js} +1 -1
  40. package/dist/assets/{index-CQ1Nswa3.js → index-PDvAAbti.js} +1 -1
  41. package/dist/assets/{index-e8wejtj_.js → index-tZqiJJGq.js} +1 -1
  42. package/dist/assets/{infoDiagram-42DDH7IO-cQPbF7Q6.js → infoDiagram-42DDH7IO-VUMzEo4o.js} +1 -1
  43. package/dist/assets/{ishikawaDiagram-UXIWVN3A-cp-ir90Y.js → ishikawaDiagram-UXIWVN3A-BuzrU3sx.js} +1 -1
  44. package/dist/assets/{journeyDiagram-VCZTEJTY-aoMcyNLC.js → journeyDiagram-VCZTEJTY-BZtR5BAF.js} +1 -1
  45. package/dist/assets/{jszip.min-C7BBsGL5.js → jszip.min-CtodH5MV.js} +1 -1
  46. package/dist/assets/{kanban-definition-6JOO6SKY-Cugr0o0f.js → kanban-definition-6JOO6SKY-Cj_97xXb.js} +1 -1
  47. package/dist/assets/{layout-CL0eiocE.js → layout-Du0I-tlh.js} +1 -1
  48. package/dist/assets/{linear-D6EJvwNs.js → linear-BJtxCASI.js} +1 -1
  49. package/dist/assets/{mermaid.core-CNYcCP19.js → mermaid.core-QlorX0Yw.js} +2 -2
  50. package/dist/assets/{min-dFBWKmZ_.js → min-BVnQfunZ.js} +1 -1
  51. package/dist/assets/{mindmap-definition-QFDTVHPH-4fbr7E2n.js → mindmap-definition-QFDTVHPH-Bh_KL4qU.js} +1 -1
  52. package/dist/assets/{pieDiagram-DEJITSTG-UHWzU_JW.js → pieDiagram-DEJITSTG-Bvv0rC_q.js} +1 -1
  53. package/dist/assets/{quadrantDiagram-34T5L4WZ-e87bZkAb.js → quadrantDiagram-34T5L4WZ-Bw8cGCRU.js} +1 -1
  54. package/dist/assets/{requirementDiagram-MS252O5E-l_97YNSi.js → requirementDiagram-MS252O5E-Cg9jQ71h.js} +1 -1
  55. package/dist/assets/{sankeyDiagram-XADWPNL6-D3JFnltO.js → sankeyDiagram-XADWPNL6-BXJo6-CD.js} +1 -1
  56. package/dist/assets/{seqResourceLoaders-ChbSSNu8.css → seqResourceLoaders-CxUFE36z.css} +1 -1
  57. package/dist/assets/seqResourceLoaders-nGCs11zu.js +2 -0
  58. package/dist/assets/{sequenceDiagram-FGHM5R23-5jDKcsRZ.js → sequenceDiagram-FGHM5R23-D7zpGxwq.js} +1 -1
  59. package/dist/assets/{stateDiagram-FHFEXIEX-D5_gBKn8.js → stateDiagram-FHFEXIEX-Der2CqR1.js} +1 -1
  60. package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BP5uQ5EH.js → stateDiagram-v2-QKLJ7IA2-DetvG_Lc.js} +1 -1
  61. package/dist/assets/{timeline-definition-GMOUNBTQ-7IVQRwK1.js → timeline-definition-GMOUNBTQ-0UenzEZ6.js} +1 -1
  62. package/dist/assets/{vendor-antd-CfuxfYfy.js → vendor-antd-I6I2F7Rv.js} +1 -1
  63. package/dist/assets/{vendor-codemirror-C4Jn4TTc.js → vendor-codemirror-Zkm6UHQg.js} +1 -1
  64. package/dist/assets/{vendor-mdxeditor-BreKt5Hd.js → vendor-mdxeditor-CC09MDyZ.js} +2 -2
  65. package/dist/assets/{vendor-qrcode-CutTqXgh.js → vendor-qrcode-gZpmSeeh.js} +1 -1
  66. package/dist/assets/{vendor-virtuoso-Cc9ovCDf.js → vendor-virtuoso-BGwznQ5y.js} +1 -1
  67. package/dist/assets/{vennDiagram-DHZGUBPP-D6qzGquN.js → vennDiagram-DHZGUBPP-COzYyZ5h.js} +1 -1
  68. package/dist/assets/{wardley-RL74JXVD-DzAFqZmY.js → wardley-RL74JXVD-VZdOtN8t.js} +1 -1
  69. package/dist/assets/{wardleyDiagram-NUSXRM2D-CImP9kv8.js → wardleyDiagram-NUSXRM2D-CVkL75Qy.js} +1 -1
  70. package/dist/assets/{xychartDiagram-5P7HB3ND-Co45rRaM.js → xychartDiagram-5P7HB3ND-6CHBAYyF.js} +1 -1
  71. package/dist/index.html +4 -4
  72. package/node_modules/@ccv/core/src/context-rules.js +6 -2
  73. package/package.json +1 -1
  74. package/server/lib/chat-queue.js +14 -1
  75. package/server/lib/config-backup.js +4 -2
  76. package/server/lib/cron.js +181 -0
  77. package/server/lib/file-access-policy.js +45 -1
  78. package/server/lib/im-deny.js +12 -9
  79. package/server/lib/json-store.js +27 -1
  80. package/server/lib/model-system-prompts.js +4 -1
  81. package/server/lib/project-prefs.js +4 -1
  82. package/server/lib/schedule-model.js +149 -0
  83. package/server/lib/scheduled-task-executor.js +97 -0
  84. package/server/lib/scheduled-task-runs-store.js +99 -0
  85. package/server/lib/scheduled-task-scheduler.js +254 -0
  86. package/server/lib/scheduled-task-store.js +511 -0
  87. package/server/lib/sdk-manager.js +283 -12
  88. package/server/pty-manager.js +13 -2
  89. package/server/routes/preferences.js +24 -1
  90. package/server/routes/scheduled-tasks.js +310 -0
  91. package/server/server.js +135 -0
  92. package/server/system-prompt-templates/presets/kimi-k3.md +4 -0
  93. package/dist/assets/classDiagram-6PBFFD2Q-C0dwAyuU.js +0 -1
  94. package/dist/assets/classDiagram-v2-HSJHXN6E-C0dwAyuU.js +0 -1
  95. package/dist/assets/clone-fiofFlrV.js +0 -1
  96. package/dist/assets/seqResourceLoaders-t8OZr44t.js +0 -2
@@ -0,0 +1,310 @@
1
+ // Scheduled-task REST routes (L3).
2
+ //
3
+ // GET /api/scheduled-tasks?project=&instance= → list (+ serverTime/timezone/spawnAvailable)
4
+ // POST /api/scheduled-tasks → create
5
+ // POST /api/scheduled-tasks/update → update / pause / resume
6
+ // POST /api/scheduled-tasks/delete → delete
7
+ // POST /api/scheduled-tasks/run → Run now
8
+ // POST /api/scheduled-tasks/preview → next 3 base fire times for a schedule
9
+ // GET /api/scheduled-tasks/runs?id=&project= → recent runs for one task
10
+ //
11
+ // Auth: reads need only a logged-in viewer; WRITES are admin + same-origin (a scheduled
12
+ // task injects a prompt into a session / spawns a headless agent — same trust level as
13
+ // the resume/upload writers, mirroring routes/resume.js:492).
14
+ //
15
+ // project → cwd resolution (a task stores an ABSOLUTE cwd, not just a project name, so
16
+ // same-basename workspaces never cross-execute — review §6): explicit project → the
17
+ // bound project when it matches, else an instance/registry match; ambiguous → 409;
18
+ // unresolvable → 404 (never silently fall back to the server's process cwd — P1-6).
19
+ //
20
+ // Boundary: L3 may import L1 (lib/*) and L2 (pty-manager/interceptor). The scheduler
21
+ // singleton is imported directly (L3→L1); server.js owns its init/start/stop.
22
+
23
+ import { _projectName } from '../interceptor.js';
24
+ import { LOG_DIR } from '../../findcc.js';
25
+ import { listLivePtys } from '../pty-manager.js';
26
+ import { sendEventToClients } from '../lib/log-watcher.js';
27
+ import { filterClientsByViewProject } from '../lib/v2/view-router.js';
28
+ import { isAdminReq } from '../lib/is-admin.js';
29
+ import { isSameOriginBrowserRequest } from '../lib/same-origin.js';
30
+ import { reportSwallowed } from '../lib/error-report.js';
31
+ import { normalizeSchedule, nextBaseFor } from '../lib/schedule-model.js';
32
+ import {
33
+ createTask, updateTask, deleteTask, getTask, readScheduledTasks,
34
+ } from '../lib/scheduled-task-store.js';
35
+ import { readScheduledTaskRuns } from '../lib/scheduled-task-runs-store.js';
36
+ import { runTaskNow, spawnAvailable } from '../lib/scheduled-task-scheduler.js';
37
+ import { loadWorkspaces } from '../workspace-registry.js';
38
+ import { listSdkSessionInstances } from '../lib/sdk-session-index.js';
39
+
40
+ const MAX_BODY = 1 << 20;
41
+
42
+ function sendJson(res, code, obj) {
43
+ res.writeHead(code, { 'Content-Type': 'application/json' });
44
+ res.end(JSON.stringify(obj));
45
+ }
46
+
47
+ function _readBody(req, res, deps, cb) {
48
+ let body = '';
49
+ req.on('data', (chunk) => { body += chunk; if (body.length > (deps?.MAX_POST_BODY || MAX_BODY)) req.destroy(); });
50
+ req.on('end', () => {
51
+ let parsed;
52
+ try { parsed = body ? JSON.parse(body) : {}; }
53
+ catch { sendJson(res, 400, { ok: false, error: 'invalid_json' }); return; }
54
+ cb(parsed);
55
+ });
56
+ req.on('error', () => { try { sendJson(res, 400, { ok: false, error: 'invalid_json' }); } catch {} });
57
+ }
58
+
59
+ function _guardWrite(req, res, parsedUrl, isLocal) {
60
+ if (!isAdminReq(req, isLocal) || !isSameOriginBrowserRequest(req, parsedUrl)) {
61
+ sendJson(res, 403, { ok: false, error: 'forbidden' });
62
+ return false;
63
+ }
64
+ return true;
65
+ }
66
+
67
+ /** Broadcast a task_changed frame to the project's viewers. */
68
+ function _broadcastChanged(deps, project, taskId, action) {
69
+ try {
70
+ sendEventToClients(
71
+ filterClientsByViewProject(deps.clients, project, _projectName || ''),
72
+ 'scheduled_tasks',
73
+ { type: 'task_changed', project, taskId, action },
74
+ );
75
+ } catch (err) { reportSwallowed('scheduled-tasks.broadcast', err); }
76
+ }
77
+
78
+ /**
79
+ * Resolve a project name (or explicit instance) to an ABSOLUTE cwd. Returns
80
+ * { ok:true, cwd } | { ok:false, status, error }.
81
+ */
82
+ function _resolveProjectToCwd(project, instance) {
83
+ const bound = _projectName || '';
84
+ // 1. An explicit instance (SDK session / live PTY) pins the cwd EXACTLY — it wins over
85
+ // the project name so two same-basename workspaces never cross-execute (review: the
86
+ // bound-project branch must not shadow a deliberate instance pick).
87
+ if (instance) {
88
+ try {
89
+ const sdkHit = listSdkSessionInstances().find(s => s.instanceKey === instance);
90
+ if (sdkHit && sdkHit.cwd) {
91
+ // Consistency: the instance's cwd must actually belong to the named project,
92
+ // else this is a forged/mismatched project+instance pair (security review).
93
+ if (!project || project === bound || _projectNameOf(sdkHit.cwd) === project) return { ok: true, cwd: sdkHit.cwd };
94
+ return { ok: false, status: 409, error: 'project_ambiguous' };
95
+ }
96
+ } catch { /* fall through */ }
97
+ const ptyHit = listLivePtys().find(p => p.instanceKey === instance && p.ptyKind === 'claude');
98
+ if (ptyHit) {
99
+ if (!project || project === bound || _projectNameOf(ptyHit.cwd) === project) return { ok: true, cwd: ptyHit.cwd };
100
+ return { ok: false, status: 409, error: 'project_ambiguous' };
101
+ }
102
+ // An explicit instance that resolves to nothing is an error, not a silent fallback.
103
+ return { ok: false, status: 404, error: 'project_unresolved' };
104
+ }
105
+ // 2. The bound project (the common case) → the server's own cwd / CCV_PROJECT_DIR.
106
+ if (!project || project === bound) {
107
+ const cwd = process.env.CCV_PROJECT_DIR || (bound ? process.cwd() : null);
108
+ // P1-6: no bound project AND no explicit project → unresolvable (never process.cwd()).
109
+ if (!cwd) return { ok: false, status: 404, error: 'project_unresolved' };
110
+ return { ok: true, cwd };
111
+ }
112
+ // 3. The workspace registry: a unique projectName match → its path.
113
+ let matches = [];
114
+ try {
115
+ matches = loadWorkspaces().filter(w => w.projectName === project && w.path);
116
+ } catch (err) { reportSwallowed('scheduled-tasks.registry', err); }
117
+ if (matches.length === 1) return { ok: true, cwd: matches[0].path };
118
+ if (matches.length > 1) return { ok: false, status: 409, error: 'project_ambiguous' };
119
+ return { ok: false, status: 404, error: 'project_unresolved' };
120
+ }
121
+
122
+ // Project key (sanitized basename) for a cwd, matching the store/UI naming.
123
+ function _projectNameOf(cwd) {
124
+ const base = String(cwd || '').replace(/\\/g, '/').split('/').filter(Boolean).pop() || '';
125
+ return base.replace(/[^a-zA-Z0-9_\-.]/g, '_');
126
+ }
127
+
128
+ /** Public list for a project (bound project when omitted). */
129
+ function listHandler(req, res, parsedUrl) {
130
+ const project = parsedUrl.searchParams.get('project') || (_projectName || '');
131
+ const root = LOG_DIR;
132
+ let reg = { tasks: [], corrupt: false };
133
+ try {
134
+ reg = readScheduledTasks(root);
135
+ } catch (err) {
136
+ reportSwallowed('scheduled-tasks.list', err);
137
+ reg = { tasks: [], corrupt: true };
138
+ }
139
+ if (reg.corrupt) reportSwallowed('scheduled-tasks.list.corrupt', new Error('preferences.json scheduledTasks is corrupt'));
140
+ // Prompt secrecy (review): a project query only earns full prompts when it actually
141
+ // resolves to a known project (bound / registry / live). Otherwise fall back to the
142
+ // summary shape so an arbitrary ?project= can't pull another project's prompt text.
143
+ const resolved = project ? _resolveProjectToCwd(project, parsedUrl.searchParams.get('instance') || '') : { ok: false };
144
+ const fullPrompt = !!project && resolved.ok;
145
+ const tasks = (reg.tasks || []).filter(t => !project || t.project === project);
146
+ const running = new Set();
147
+ for (const t of tasks) if (t.claim) running.add(t.id);
148
+ const shaped = fullPrompt
149
+ ? tasks.map(t => ({ ..._publicTask(t), running: running.has(t.id), ...(reg.legacy ? { legacy: true } : {}) }))
150
+ : tasks.map(t => ({ ..._listTask(t), running: running.has(t.id), ...(reg.legacy ? { legacy: true } : {}) }));
151
+ sendJson(res, 200, {
152
+ tasks: shaped,
153
+ serverTime: Date.now(),
154
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
155
+ spawnAvailable: _spawnAvailableSafe(),
156
+ storeCorrupt: !!reg.corrupt,
157
+ });
158
+ }
159
+
160
+ function _publicTask(t) {
161
+ const { runs, claim, ...rest } = t;
162
+ return rest;
163
+ }
164
+
165
+ // List payload keeps the full prompt OUT (it may carry secrets and every logged-in
166
+ // viewer can read the list — security P2-3). Full prompts only ship when the project
167
+ // resolves (bound / registry / live instance — see listHandler's fullPrompt gate).
168
+ function _listTask(t) {
169
+ const p = _publicTask(t);
170
+ p.promptSummary = String(t.prompt || '').replace(/\s+/g, ' ').trim().slice(0, 200);
171
+ delete p.prompt;
172
+ return p;
173
+ }
174
+
175
+ function _spawnAvailableSafe() {
176
+ try { return spawnAvailable(); } catch { return false; }
177
+ }
178
+
179
+ function createHandler(req, res, parsedUrl, isLocal, deps) {
180
+ if (!_guardWrite(req, res, parsedUrl, isLocal)) return;
181
+ _readBody(req, res, deps, async (body) => {
182
+ const project = (typeof body.project === 'string' && body.project) || (_projectName || '');
183
+ const resolved = _resolveProjectToCwd(project, body.instance);
184
+ if (!resolved.ok) return sendJson(res, resolved.status, { ok: false, error: resolved.error });
185
+ if (body.mode === 'spawn' && !_spawnAvailableSafe()) {
186
+ return sendJson(res, 400, { ok: false, error: 'spawn_unavailable' });
187
+ }
188
+ let out;
189
+ try {
190
+ out = await createTask(LOG_DIR, {
191
+ project, cwd: resolved.cwd, prompt: body.prompt, mode: body.mode,
192
+ schedule: body.schedule, enabled: body.enabled,
193
+ }, Date.now());
194
+ } catch (err) {
195
+ reportSwallowed('scheduled-tasks.create', err);
196
+ return sendJson(res, 500, { ok: false, error: 'store_corrupt' });
197
+ }
198
+ if (!out.ok) return sendJson(res, 400, { ok: false, error: out.reason, detail: out.detail });
199
+ _broadcastChanged(deps, project, out.task.id, 'created');
200
+ sendJson(res, 200, { ok: true, task: _publicTask(out.task) });
201
+ });
202
+ }
203
+
204
+ function updateHandler(req, res, parsedUrl, isLocal, deps) {
205
+ if (!_guardWrite(req, res, parsedUrl, isLocal)) return;
206
+ _readBody(req, res, deps, async (body) => {
207
+ const id = typeof body.id === 'string' ? body.id : '';
208
+ if (!id) return sendJson(res, 400, { ok: false, error: 'task_not_found' });
209
+ let out;
210
+ try {
211
+ out = await updateTask(LOG_DIR, id, {
212
+ prompt: body.prompt, mode: body.mode, schedule: body.schedule, enabled: body.enabled,
213
+ }, Date.now());
214
+ } catch (err) {
215
+ reportSwallowed('scheduled-tasks.update', err);
216
+ return sendJson(res, 500, { ok: false, error: 'store_corrupt' });
217
+ }
218
+ if (!out.ok) return sendJson(res, out.reason === 'task_not_found' ? 404 : 400, { ok: false, error: out.reason, detail: out.detail });
219
+ _broadcastChanged(deps, out.task.project, id, 'updated');
220
+ sendJson(res, 200, { ok: true, task: _publicTask(out.task) });
221
+ });
222
+ }
223
+
224
+ function deleteHandler(req, res, parsedUrl, isLocal, deps) {
225
+ if (!_guardWrite(req, res, parsedUrl, isLocal)) return;
226
+ _readBody(req, res, deps, async (body) => {
227
+ const id = typeof body.id === 'string' ? body.id : '';
228
+ let task = null;
229
+ try { task = getTask(LOG_DIR, id); } catch { /* tolerate */ }
230
+ let out;
231
+ try { out = await deleteTask(LOG_DIR, id, Date.now()); }
232
+ catch (err) {
233
+ reportSwallowed('scheduled-tasks.delete', err);
234
+ return sendJson(res, 500, { ok: false, error: 'store_corrupt' });
235
+ }
236
+ if (!out.ok) {
237
+ const status = out.reason === 'task_not_found' ? 404 : (out.reason === 'task_running' ? 409 : 400);
238
+ return sendJson(res, status, { ok: false, error: out.reason });
239
+ }
240
+ _broadcastChanged(deps, (task && task.project) || body.project || (_projectName || ''), id, 'deleted');
241
+ sendJson(res, 200, { ok: true });
242
+ });
243
+ }
244
+
245
+ function runHandler(req, res, parsedUrl, isLocal, deps) {
246
+ if (!_guardWrite(req, res, parsedUrl, isLocal)) return;
247
+ _readBody(req, res, deps, async (body) => {
248
+ const id = typeof body.id === 'string' ? body.id : '';
249
+ let out;
250
+ try { out = await runTaskNow(id, { manual: true }); }
251
+ catch (err) {
252
+ reportSwallowed('scheduled-tasks.run', err);
253
+ return sendJson(res, 503, { ok: false, error: 'scheduler_unavailable' });
254
+ }
255
+ if (!out.ok) {
256
+ const status = out.reason === 'scheduler_unavailable' ? 503
257
+ : out.reason === 'running' ? 409
258
+ : out.reason === 'not-found' ? 404 : 400;
259
+ return sendJson(res, status, { ok: false, error: out.reason === 'not-found' ? 'task_not_found' : out.reason });
260
+ }
261
+ sendJson(res, 202, { ok: true, runId: out.runId ?? null });
262
+ });
263
+ }
264
+
265
+ function previewHandler(req, res, parsedUrl, isLocal, deps) {
266
+ if (!_guardWrite(req, res, parsedUrl, isLocal)) return;
267
+ _readBody(req, res, deps, (body) => {
268
+ const now = Date.now();
269
+ const norm = normalizeSchedule(body.schedule, now);
270
+ if (!norm.ok) return sendJson(res, 400, { ok: false, error: norm.error, detail: norm.detail });
271
+ const next = [];
272
+ let cursor = now;
273
+ for (let i = 0; i < 3; i++) {
274
+ const t = nextBaseFor(norm.schedule, cursor);
275
+ if (t === null) break;
276
+ next.push(t);
277
+ cursor = t;
278
+ }
279
+ sendJson(res, 200, {
280
+ ok: true, next,
281
+ serverTime: now,
282
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
283
+ });
284
+ });
285
+ }
286
+
287
+ function runsHandler(req, res, parsedUrl) {
288
+ const id = parsedUrl.searchParams.get('id') || '';
289
+ const reqProject = parsedUrl.searchParams.get('project') || (_projectName || '');
290
+ let task = null;
291
+ try { task = getTask(LOG_DIR, id); } catch (err) { reportSwallowed('scheduled-tasks.runs', err); }
292
+ if (!task) return sendJson(res, 404, { ok: false, error: 'task_not_found' });
293
+ // Runs carry the prompt summary + sessionId — scope them to the task's own project so a
294
+ // viewer of project A can't read project B's run history by id (security P2-3).
295
+ if (reqProject && task.project !== reqProject) return sendJson(res, 403, { ok: false, error: 'forbidden' });
296
+ let runs = [];
297
+ try { runs = readScheduledTaskRuns(LOG_DIR, id); }
298
+ catch (err) { reportSwallowed('scheduled-tasks.runs.read', err); }
299
+ sendJson(res, 200, { ok: true, runs: runs.slice(-20), serverTime: Date.now() });
300
+ }
301
+
302
+ export const scheduledTasksRoutes = [
303
+ { method: 'GET', match: 'exact', path: '/api/scheduled-tasks', handler: listHandler },
304
+ { method: 'GET', match: 'exact', path: '/api/scheduled-tasks/runs', handler: runsHandler },
305
+ { method: 'POST', match: 'exact', path: '/api/scheduled-tasks', handler: createHandler },
306
+ { method: 'POST', match: 'exact', path: '/api/scheduled-tasks/update', handler: updateHandler },
307
+ { method: 'POST', match: 'exact', path: '/api/scheduled-tasks/delete', handler: deleteHandler },
308
+ { method: 'POST', match: 'exact', path: '/api/scheduled-tasks/run', handler: runHandler },
309
+ { method: 'POST', match: 'exact', path: '/api/scheduled-tasks/preview', handler: previewHandler },
310
+ ];
package/server/server.js CHANGED
@@ -21,6 +21,7 @@ import { miscRoutes } from './routes/misc.js';
21
21
  import { preferencesRoutes } from './routes/preferences.js';
22
22
  import { projectPrefsRoutes } from './routes/project-prefs.js';
23
23
  import { sessionPinRoutes } from './routes/session-pin.js';
24
+ import { scheduledTasksRoutes } from './routes/scheduled-tasks.js';
24
25
  import { gitRoutes } from './routes/git.js';
25
26
  import { pluginsRoutes } from './routes/plugins.js';
26
27
  import { pluginServicesRoutes } from './routes/plugin-services.js';
@@ -50,6 +51,8 @@ import * as imCore from './lib/im/im-bridge-core.js';
50
51
  import * as imProcMgr from './lib/im/im-process-manager.js';
51
52
  import * as chatQueue from './lib/chat-queue.js';
52
53
  import { reportSwallowed } from './lib/error-report.js';
54
+ import { initScheduledTaskScheduler, startScheduledTaskScheduler, stopScheduledTaskScheduler } from './lib/scheduled-task-scheduler.js';
55
+ import { executeScheduledTask } from './lib/scheduled-task-executor.js';
53
56
  import './lib/adapters/dingtalk-adapter.js'; // side-effect: registers the DingTalk adapter
54
57
  import './lib/adapters/feishu-adapter.js'; // side-effect: registers the Feishu adapter
55
58
  import './lib/adapters/wecom-adapter.js'; // side-effect: registers the WeCom adapter
@@ -843,6 +846,7 @@ const _routes = [
843
846
  ...preferencesRoutes,
844
847
  ...projectPrefsRoutes,
845
848
  ...sessionPinRoutes,
849
+ ...scheduledTasksRoutes,
846
850
  ...gitRoutes,
847
851
  ...pluginsRoutes,
848
852
  ...pluginServicesRoutes,
@@ -1167,6 +1171,130 @@ export async function startViewer() {
1167
1171
  }
1168
1172
  }
1169
1173
 
1174
+ // ─── Scheduled tasks (scheduler wiring) ─────────────────────────────────────
1175
+ // The scheduler is L1 pure logic; every SDK/PTY capability is injected here (server.js
1176
+ // is the only place that knows isSdkMode / LOG_DIR / the SSE client set). Paths are
1177
+ // compared case-insensitively on win32 (workspace-registry's pathEq semantics).
1178
+ function _schedSamePath(a, b) {
1179
+ if (typeof a !== 'string' || typeof b !== 'string' || !a || !b) return false;
1180
+ return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
1181
+ }
1182
+
1183
+ // Process-level capability filter (P0-1): a process only claims a task it can run.
1184
+ // inject needs a LIVE session for the task's cwd in THIS process; spawn needs SDK mode.
1185
+ function _schedCanExecute(task) {
1186
+ try {
1187
+ // A workspace-management process (bound no project / no launched workspace) must never
1188
+ // claim tasks — they belong to a tab's bound workspace (integration P0-1).
1189
+ if (isWorkspaceMode && !_workspaceLaunched) return false;
1190
+ // spawn capability is settled at execute time (the SDK accessors lazy-load on first
1191
+ // execute); the tick-level filter only needs the mode gate so a PTY-mode process
1192
+ // never claims a spawn task it can't run.
1193
+ if (task.mode === 'spawn') return isSdkMode;
1194
+ if (isSdkMode) {
1195
+ // SDK sessions register eagerly at init; if the accessor isn't loaded yet, be
1196
+ // permissive (execute will re-check and report no_live_session honestly).
1197
+ return _sdkListSessions ? _sdkListSessions().some(s => _schedSamePath(s.cwd, task.cwd)) : true;
1198
+ }
1199
+ return _ptyListLive ? _ptyListLive().some(p => p.ptyKind === 'claude' && _schedSamePath(p.cwd, task.cwd)) : true;
1200
+ } catch { return false; }
1201
+ }
1202
+
1203
+ function _schedIsReady() {
1204
+ try {
1205
+ if (isSdkMode) return _sdkListSessions ? _sdkListSessions().length > 0 : true;
1206
+ return _ptyListLive ? _ptyListLive().some(p => p.ptyKind === 'claude') : true;
1207
+ } catch { return false; }
1208
+ }
1209
+
1210
+ async function _schedExecute(task, claim) {
1211
+ // Assemble the io accessors lazily (dynamic import keeps the L2 managers out of the
1212
+ // L1 executor's module graph; server.js is L4 and may import them).
1213
+ if (!_sdkListSessions && isSdkMode) {
1214
+ try {
1215
+ const sdk = await import('./lib/sdk-manager.js');
1216
+ _sdkListSessions = sdk.listSdkSessions;
1217
+ _sdkGetQueueSnapshotFn = sdk.getQueueSnapshot;
1218
+ _sdkSendUserMessageFn = sdk.sendUserMessage;
1219
+ _sdkIsAvailable = sdk.isSdkAvailable;
1220
+ _sdkSpawnEphemeral = sdk.spawnEphemeralSession;
1221
+ } catch (err) { reportSwallowed('server.sched-sdk-import', err); }
1222
+ }
1223
+ if (!_ptyListLive && !isSdkMode) {
1224
+ try {
1225
+ const pm = await import('./pty-manager.js');
1226
+ _ptyListLive = pm.listLivePtys;
1227
+ _ptyGetKindFor = pm.getPtyKindFor;
1228
+ } catch (err) { reportSwallowed('server.sched-pty-import', err); }
1229
+ }
1230
+ const io = {
1231
+ isSdkMode,
1232
+ sdk: {
1233
+ isSdkAvailable: () => (_sdkIsAvailable ? _sdkIsAvailable() : false),
1234
+ listSdkSessions: () => (_sdkListSessions ? _sdkListSessions() : []),
1235
+ getQueueSnapshot: (anchor) => (_sdkGetQueueSnapshotFn ? _sdkGetQueueSnapshotFn(anchor) : []),
1236
+ sendUserMessage: (text, anchor) => (_sdkSendUserMessageFn ? _sdkSendUserMessageFn(text, anchor) : Promise.resolve()),
1237
+ spawnEphemeralSession: (prompt, opts) => (_sdkSpawnEphemeral ? _sdkSpawnEphemeral(prompt, opts) : Promise.resolve({ ok: false, reason: 'sdk_unavailable' })),
1238
+ },
1239
+ pty: {
1240
+ listLivePtys: () => (_ptyListLive ? _ptyListLive() : []),
1241
+ getPtyKindFor: (anchor) => (_ptyGetKindFor ? _ptyGetKindFor(anchor) : null),
1242
+ },
1243
+ chatQueue,
1244
+ samePath: _schedSamePath,
1245
+ };
1246
+ return executeScheduledTask(task, claim, io);
1247
+ }
1248
+
1249
+ // Lazily-resolved capability accessors (populated on first use / after SDK init).
1250
+ let _sdkListSessions = null;
1251
+ let _sdkGetQueueSnapshotFn = null;
1252
+ let _sdkSendUserMessageFn = null;
1253
+ let _sdkIsAvailable = null;
1254
+ let _sdkSpawnEphemeral = null;
1255
+ let _ptyListLive = null;
1256
+ let _ptyGetKindFor = null;
1257
+
1258
+ async function _initScheduledTasks() {
1259
+ try {
1260
+ // Preload the capability accessors BEFORE the first tick, so canExecute's very first
1261
+ // evaluation is real (not the permissive fail-open default). Without this an Electron
1262
+ // management process / tab worker would claim every due task on its first tick only
1263
+ // to fail them (review: integration P0-1).
1264
+ if (isSdkMode) {
1265
+ try {
1266
+ const sdk = await import('./lib/sdk-manager.js');
1267
+ _sdkListSessions = sdk.listSdkSessions;
1268
+ _sdkGetQueueSnapshotFn = sdk.getQueueSnapshot;
1269
+ _sdkSendUserMessageFn = sdk.sendUserMessage;
1270
+ _sdkIsAvailable = sdk.isSdkAvailable;
1271
+ _sdkSpawnEphemeral = sdk.spawnEphemeralSession;
1272
+ } catch (err) { reportSwallowed('server.sched-sdk-preload', err); }
1273
+ } else {
1274
+ try {
1275
+ const pm = await import('./pty-manager.js');
1276
+ _ptyListLive = pm.listLivePtys;
1277
+ _ptyGetKindFor = pm.getPtyKindFor;
1278
+ } catch (err) { reportSwallowed('server.sched-pty-preload', err); }
1279
+ }
1280
+ initScheduledTaskScheduler({
1281
+ getRoot: () => LOG_DIR,
1282
+ getMode: () => ({ sdk: isSdkMode }),
1283
+ getSpawnConfig: () => ({ available: isSdkMode && !!(_sdkIsAvailable && _sdkIsAvailable()) }),
1284
+ canExecute: _schedCanExecute,
1285
+ isReady: _schedIsReady,
1286
+ execute: _schedExecute,
1287
+ broadcast: (project, payload) => {
1288
+ try {
1289
+ sendEventToClients(_filterClientsByViewProject(clients, project), 'scheduled_tasks', payload);
1290
+ } catch (err) { reportSwallowed('server.sched-broadcast', err); }
1291
+ },
1292
+ report: reportSwallowed,
1293
+ });
1294
+ startScheduledTaskScheduler();
1295
+ } catch (err) { reportSwallowed('server.sched-init', err); }
1296
+ }
1297
+
1170
1298
  let _startingPromise = null;
1171
1299
 
1172
1300
  async function _doStart() {
@@ -1363,6 +1491,12 @@ async function _doStart() {
1363
1491
  if (isCliMode) {
1364
1492
  await setupTerminalWebSocket(currentServer);
1365
1493
  }
1494
+ // Scheduled tasks: only a CLI-mode process that is NOT an IM worker runs the
1495
+ // scheduler (an IM worker shares LOG_DIR but has no resident session to inject
1496
+ // into — review P0-1). A workspace-management/static viewer process never does.
1497
+ if (isCliMode && !process.env.CCV_IM_PLATFORM) {
1498
+ await _initScheduledTasks();
1499
+ }
1366
1500
  // Install the unified upgrade router (all modes): plugin service WS prefixes + registered terminal/mux.
1367
1501
  // Install after createServer and before the first request to close the upgrade gap.
1368
1502
  installUpgradeRouter(currentServer);
@@ -3324,6 +3458,7 @@ async function _doStop() {
3324
3458
  // Drop chat-queue timers (rescue / idle poll) so a stop/start cycle never injects a stale
3325
3459
  // queued message into a fresh PTY session.
3326
3460
  try { chatQueue.stop(); } catch { /* best-effort */ }
3461
+ try { stopScheduledTaskScheduler(); } catch { /* best-effort */ }
3327
3462
  _clearTurnActiveHeartbeat();
3328
3463
  // Per-workspace: tear down every scope bucket plus the per-scope session tags
3329
3464
  // and streaming-edge records, so a restarted server never replays a stale
@@ -10,6 +10,10 @@ You are ${model.name}, an interactive coding agent that helps users with softwar
10
10
 
11
11
  IMPORTANT: Never generate or guess URLs unless you are confident they help the user with programming. Prefer URLs the user provides or ones found in local files.
12
12
 
13
+ # Choosing skills
14
+ - At the skill-selection entry point, skip `claude-api` and the skill whose display title is "Building LLM-Powered Applications with Claude": they are the same Anthropic-only API skill, so treat the identifier and title as aliases. Do not invoke it or read its documentation unless the task explicitly requires Claude/Anthropic SDK or API integration. This applies even when Claude Code automatically recommends or triggers it.
15
+ - Working on model-name recognition, model logos, aliases, or context-window classification in an existing project is local application code, NOT LLM API integration. Do not load `claude-api` for these tasks merely because the code mentions models, Claude, or model IDs. Locate the local matcher and its tests first; for a genuinely unrelated skill, do not load it just to determine that it is unrelated.
16
+
13
17
  # Doing tasks
14
18
  - Read the relevant files before acting or answering; ground every claim and change in code you have actually looked at.
15
19
  - Before your first tool call, output a one-sentence action plan; then act.
@@ -1 +0,0 @@
1
- import{s as styles_default,c as classRenderer_v3_unified_default,a as classDiagram_default,C as ClassDB}from"./chunk-4TB4RGXK-GWLkQKRO.js";import{_ as __name}from"./mermaid.core-CNYcCP19.js";import"./chunk-FMBD7UC4-DK-vXpZP.js";import"./chunk-YZCP3GAM-DtYet6WV.js";import"./chunk-55IACEB6-B6LOFqRZ.js";import"./chunk-EDXVE4YY-Ct3e09Ku.js";import"./vendor-markdown-DOJHsAxX.js";import"./vendor-mdxeditor-BreKt5Hd.js";import"./vendor-antd-CfuxfYfy.js";import"./vendor-codemirror-C4Jn4TTc.js";var diagram={parser:classDiagram_default,get db(){return new ClassDB},renderer:classRenderer_v3_unified_default,styles:styles_default,init:__name(cnf=>{cnf.class||(cnf.class={}),cnf.class.arrowMarkerAbsolute=cnf.arrowMarkerAbsolute},"init")};export{diagram};
@@ -1 +0,0 @@
1
- import{s as styles_default,c as classRenderer_v3_unified_default,a as classDiagram_default,C as ClassDB}from"./chunk-4TB4RGXK-GWLkQKRO.js";import{_ as __name}from"./mermaid.core-CNYcCP19.js";import"./chunk-FMBD7UC4-DK-vXpZP.js";import"./chunk-YZCP3GAM-DtYet6WV.js";import"./chunk-55IACEB6-B6LOFqRZ.js";import"./chunk-EDXVE4YY-Ct3e09Ku.js";import"./vendor-markdown-DOJHsAxX.js";import"./vendor-mdxeditor-BreKt5Hd.js";import"./vendor-antd-CfuxfYfy.js";import"./vendor-codemirror-C4Jn4TTc.js";var diagram={parser:classDiagram_default,get db(){return new ClassDB},renderer:classRenderer_v3_unified_default,styles:styles_default,init:__name(cnf=>{cnf.class||(cnf.class={}),cnf.class.arrowMarkerAbsolute=cnf.arrowMarkerAbsolute},"init")};export{diagram};
@@ -1 +0,0 @@
1
- import{b as baseClone}from"./graph-Dh22wYWs.js";function clone(value){return baseClone(value,4)}export{clone as c};