cli4ai 1.2.0 → 1.2.2

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 (113) hide show
  1. package/README.md +39 -0
  2. package/dist/bin.d.ts +6 -0
  3. package/dist/bin.js +105 -0
  4. package/dist/cli.d.ts +5 -0
  5. package/dist/cli.js +335 -0
  6. package/dist/commands/add.d.ts +11 -0
  7. package/dist/commands/add.js +464 -0
  8. package/dist/commands/browse.d.ts +4 -0
  9. package/dist/commands/browse.js +382 -0
  10. package/dist/commands/config.d.ts +10 -0
  11. package/dist/commands/config.js +121 -0
  12. package/dist/commands/info.d.ts +9 -0
  13. package/dist/commands/info.js +125 -0
  14. package/dist/commands/init.d.ts +10 -0
  15. package/dist/commands/init.js +458 -0
  16. package/dist/commands/list.d.ts +10 -0
  17. package/dist/commands/list.js +76 -0
  18. package/dist/commands/mcp-config.d.ts +10 -0
  19. package/dist/commands/mcp-config.js +49 -0
  20. package/dist/commands/remotes.d.ts +22 -0
  21. package/dist/commands/remotes.js +196 -0
  22. package/dist/commands/remove.d.ts +8 -0
  23. package/dist/commands/remove.js +61 -0
  24. package/dist/commands/routines.d.ts +29 -0
  25. package/dist/commands/routines.js +363 -0
  26. package/dist/commands/run.d.ts +12 -0
  27. package/dist/commands/run.js +104 -0
  28. package/dist/commands/scheduler.d.ts +27 -0
  29. package/dist/commands/scheduler.js +350 -0
  30. package/dist/commands/search.d.ts +9 -0
  31. package/dist/commands/search.js +162 -0
  32. package/dist/commands/secrets.d.ts +28 -0
  33. package/dist/commands/secrets.js +236 -0
  34. package/dist/commands/serve.d.ts +13 -0
  35. package/dist/commands/serve.js +49 -0
  36. package/dist/commands/start.d.ts +8 -0
  37. package/dist/commands/start.js +27 -0
  38. package/dist/commands/update.d.ts +17 -0
  39. package/dist/commands/update.js +210 -0
  40. package/dist/core/config.d.ts +91 -0
  41. package/dist/core/config.js +738 -0
  42. package/dist/core/execute.d.ts +51 -0
  43. package/dist/core/execute.js +475 -0
  44. package/dist/core/link.d.ts +39 -0
  45. package/dist/core/link.js +214 -0
  46. package/dist/core/lockfile.d.ts +63 -0
  47. package/dist/core/lockfile.js +140 -0
  48. package/dist/core/manifest.d.ts +96 -0
  49. package/dist/core/manifest.js +224 -0
  50. package/dist/core/registry.d.ts +74 -0
  51. package/dist/core/registry.js +116 -0
  52. package/dist/core/remote-client.d.ts +98 -0
  53. package/dist/core/remote-client.js +252 -0
  54. package/dist/core/remotes.d.ts +88 -0
  55. package/dist/core/remotes.js +206 -0
  56. package/dist/core/routine-engine.d.ts +124 -0
  57. package/dist/core/routine-engine.js +699 -0
  58. package/dist/core/routines.d.ts +36 -0
  59. package/dist/core/routines.js +132 -0
  60. package/dist/core/scheduler-daemon.d.ts +10 -0
  61. package/dist/core/scheduler-daemon.js +77 -0
  62. package/dist/core/scheduler.d.ts +131 -0
  63. package/dist/core/scheduler.js +492 -0
  64. package/dist/core/secrets.d.ts +48 -0
  65. package/dist/core/secrets.js +384 -0
  66. package/dist/lib/cli.d.ts +84 -0
  67. package/dist/lib/cli.js +216 -0
  68. package/dist/mcp/adapter.d.ts +35 -0
  69. package/dist/mcp/adapter.js +94 -0
  70. package/dist/mcp/config-gen.d.ts +31 -0
  71. package/dist/mcp/config-gen.js +75 -0
  72. package/dist/mcp/server.d.ts +41 -0
  73. package/dist/mcp/server.js +296 -0
  74. package/dist/server/service.d.ts +85 -0
  75. package/dist/server/service.js +304 -0
  76. package/package.json +6 -3
  77. package/src/bin.ts +0 -118
  78. package/src/cli.ts +0 -412
  79. package/src/commands/add.ts +0 -562
  80. package/src/commands/browse.ts +0 -449
  81. package/src/commands/config.ts +0 -154
  82. package/src/commands/info.ts +0 -133
  83. package/src/commands/init.ts +0 -514
  84. package/src/commands/list.ts +0 -95
  85. package/src/commands/mcp-config.ts +0 -69
  86. package/src/commands/remotes.ts +0 -253
  87. package/src/commands/remove.ts +0 -78
  88. package/src/commands/routines.ts +0 -427
  89. package/src/commands/run.ts +0 -127
  90. package/src/commands/scheduler.ts +0 -438
  91. package/src/commands/search.ts +0 -185
  92. package/src/commands/secrets.ts +0 -292
  93. package/src/commands/serve.ts +0 -66
  94. package/src/commands/start.ts +0 -40
  95. package/src/commands/update.ts +0 -252
  96. package/src/core/config.ts +0 -845
  97. package/src/core/execute.ts +0 -569
  98. package/src/core/link.ts +0 -246
  99. package/src/core/lockfile.ts +0 -187
  100. package/src/core/manifest.ts +0 -327
  101. package/src/core/registry.ts +0 -165
  102. package/src/core/remote-client.ts +0 -419
  103. package/src/core/remotes.ts +0 -268
  104. package/src/core/routine-engine.ts +0 -895
  105. package/src/core/routines.ts +0 -171
  106. package/src/core/scheduler-daemon.ts +0 -94
  107. package/src/core/scheduler.ts +0 -606
  108. package/src/core/secrets.ts +0 -430
  109. package/src/lib/cli.ts +0 -261
  110. package/src/mcp/adapter.ts +0 -131
  111. package/src/mcp/config-gen.ts +0 -106
  112. package/src/mcp/server.ts +0 -365
  113. package/src/server/service.ts +0 -434
@@ -0,0 +1,492 @@
1
+ /**
2
+ * Scheduler core - manages scheduled routine execution.
3
+ *
4
+ * Storage structure:
5
+ * ~/.cli4ai/scheduler/
6
+ * ├── scheduler.pid # Daemon PID
7
+ * ├── state.json # Next runs, last results
8
+ * ├── runs/ # Execution records
9
+ * │ └── <routine>-<ts>.json
10
+ * └── logs/
11
+ * └── scheduler.log # Daemon logs
12
+ */
13
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, readdirSync, appendFileSync } from 'fs';
14
+ import { resolve } from 'path';
15
+ import parser from 'cron-parser';
16
+ import { SCHEDULER_DIR, ensureCli4aiHome } from './config.js';
17
+ import { getScheduledRoutines } from './routines.js';
18
+ import { loadRoutineDefinition, runRoutine } from './routine-engine.js';
19
+ // ═══════════════════════════════════════════════════════════════════════════
20
+ // PATHS
21
+ // ═══════════════════════════════════════════════════════════════════════════
22
+ export const SCHEDULER_PID_FILE = resolve(SCHEDULER_DIR, 'scheduler.pid');
23
+ export const SCHEDULER_STATE_FILE = resolve(SCHEDULER_DIR, 'state.json');
24
+ export const SCHEDULER_RUNS_DIR = resolve(SCHEDULER_DIR, 'runs');
25
+ export const SCHEDULER_LOGS_DIR = resolve(SCHEDULER_DIR, 'logs');
26
+ export const SCHEDULER_LOG_FILE = resolve(SCHEDULER_LOGS_DIR, 'scheduler.log');
27
+ // ═══════════════════════════════════════════════════════════════════════════
28
+ // INTERVAL PARSING
29
+ // ═══════════════════════════════════════════════════════════════════════════
30
+ const INTERVAL_PATTERN = /^(\d+)(s|m|h|d)$/;
31
+ const INTERVAL_MULTIPLIERS = {
32
+ s: 1000,
33
+ m: 60 * 1000,
34
+ h: 60 * 60 * 1000,
35
+ d: 24 * 60 * 60 * 1000
36
+ };
37
+ /**
38
+ * Parse an interval string like "30s", "5m", "1h", "1d" into milliseconds.
39
+ */
40
+ export function parseInterval(interval) {
41
+ const match = interval.match(INTERVAL_PATTERN);
42
+ if (!match) {
43
+ throw new Error(`Invalid interval format: ${interval}`);
44
+ }
45
+ const value = parseInt(match[1], 10);
46
+ const unit = match[2];
47
+ return value * INTERVAL_MULTIPLIERS[unit];
48
+ }
49
+ /**
50
+ * Calculate the next run time for a schedule.
51
+ * If both cron and interval are specified, returns the earlier of the two.
52
+ */
53
+ export function getNextRunTime(schedule, after = new Date()) {
54
+ const candidates = [];
55
+ // Calculate from cron expression
56
+ if (schedule.cron) {
57
+ try {
58
+ const options = {
59
+ currentDate: after,
60
+ tz: schedule.timezone
61
+ };
62
+ const cronExpr = parser.parseExpression(schedule.cron, options);
63
+ candidates.push(cronExpr.next().toDate());
64
+ }
65
+ catch {
66
+ // Invalid cron expression - skip
67
+ }
68
+ }
69
+ // Calculate from interval
70
+ if (schedule.interval) {
71
+ try {
72
+ const intervalMs = parseInterval(schedule.interval);
73
+ candidates.push(new Date(after.getTime() + intervalMs));
74
+ }
75
+ catch {
76
+ // Invalid interval - skip
77
+ }
78
+ }
79
+ if (candidates.length === 0) {
80
+ return null;
81
+ }
82
+ // Return the earliest next run time
83
+ return candidates.reduce((earliest, candidate) => candidate < earliest ? candidate : earliest);
84
+ }
85
+ // ═══════════════════════════════════════════════════════════════════════════
86
+ // DAEMON LIFECYCLE
87
+ // ═══════════════════════════════════════════════════════════════════════════
88
+ /**
89
+ * Get the PID of the running daemon, or null if not running.
90
+ */
91
+ export function getDaemonPid() {
92
+ if (!existsSync(SCHEDULER_PID_FILE)) {
93
+ return null;
94
+ }
95
+ try {
96
+ const content = readFileSync(SCHEDULER_PID_FILE, 'utf-8').trim();
97
+ // SECURITY: Validate PID format and bounds
98
+ if (!/^\d+$/.test(content))
99
+ return null;
100
+ const pid = parseInt(content, 10);
101
+ // PIDs must be positive integers within reasonable bounds
102
+ // Max PID varies by OS but is typically 32768-4194304
103
+ if (!Number.isInteger(pid) || pid < 1 || pid > 4194304)
104
+ return null;
105
+ return pid;
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
111
+ /**
112
+ * Check if a process with the given PID is running.
113
+ */
114
+ function isProcessRunning(pid) {
115
+ try {
116
+ // Sending signal 0 doesn't actually send a signal, but checks if process exists
117
+ process.kill(pid, 0);
118
+ return true;
119
+ }
120
+ catch {
121
+ return false;
122
+ }
123
+ }
124
+ /**
125
+ * Check if the scheduler daemon is running.
126
+ */
127
+ export function isDaemonRunning() {
128
+ const pid = getDaemonPid();
129
+ if (pid === null)
130
+ return false;
131
+ return isProcessRunning(pid);
132
+ }
133
+ /**
134
+ * Write the daemon PID file.
135
+ */
136
+ export function writeDaemonPid(pid) {
137
+ ensureSchedulerDirs();
138
+ writeFileSync(SCHEDULER_PID_FILE, String(pid));
139
+ }
140
+ /**
141
+ * Remove the daemon PID file.
142
+ */
143
+ export function removeDaemonPid() {
144
+ if (existsSync(SCHEDULER_PID_FILE)) {
145
+ unlinkSync(SCHEDULER_PID_FILE);
146
+ }
147
+ }
148
+ // ═══════════════════════════════════════════════════════════════════════════
149
+ // STATE MANAGEMENT
150
+ // ═══════════════════════════════════════════════════════════════════════════
151
+ export function ensureSchedulerDirs() {
152
+ ensureCli4aiHome();
153
+ if (!existsSync(SCHEDULER_RUNS_DIR)) {
154
+ mkdirSync(SCHEDULER_RUNS_DIR, { recursive: true });
155
+ }
156
+ if (!existsSync(SCHEDULER_LOGS_DIR)) {
157
+ mkdirSync(SCHEDULER_LOGS_DIR, { recursive: true });
158
+ }
159
+ }
160
+ /**
161
+ * Load scheduler state from disk.
162
+ */
163
+ export function loadSchedulerState() {
164
+ if (!existsSync(SCHEDULER_STATE_FILE)) {
165
+ return null;
166
+ }
167
+ try {
168
+ const content = readFileSync(SCHEDULER_STATE_FILE, 'utf-8');
169
+ return JSON.parse(content);
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ }
175
+ /**
176
+ * Save scheduler state to disk.
177
+ */
178
+ export function saveSchedulerState(state) {
179
+ ensureSchedulerDirs();
180
+ writeFileSync(SCHEDULER_STATE_FILE, JSON.stringify(state, null, 2));
181
+ }
182
+ /**
183
+ * Save a run record to disk.
184
+ */
185
+ export function saveRunRecord(record) {
186
+ ensureSchedulerDirs();
187
+ const filename = `${record.routine}-${new Date(record.startedAt).getTime()}.json`;
188
+ const filepath = resolve(SCHEDULER_RUNS_DIR, filename);
189
+ writeFileSync(filepath, JSON.stringify(record, null, 2));
190
+ }
191
+ /**
192
+ * Get run history for a routine (or all routines).
193
+ */
194
+ export function getRunHistory(routineName, limit = 20) {
195
+ ensureSchedulerDirs();
196
+ if (!existsSync(SCHEDULER_RUNS_DIR)) {
197
+ return [];
198
+ }
199
+ const files = readdirSync(SCHEDULER_RUNS_DIR)
200
+ .filter(f => f.endsWith('.json'))
201
+ .filter(f => !routineName || f.startsWith(`${routineName}-`))
202
+ .sort()
203
+ .reverse()
204
+ .slice(0, limit);
205
+ const records = [];
206
+ for (const file of files) {
207
+ try {
208
+ const content = readFileSync(resolve(SCHEDULER_RUNS_DIR, file), 'utf-8');
209
+ records.push(JSON.parse(content));
210
+ }
211
+ catch {
212
+ // Skip invalid files
213
+ }
214
+ }
215
+ return records;
216
+ }
217
+ export function appendSchedulerLog(level, message) {
218
+ ensureSchedulerDirs();
219
+ const timestamp = new Date().toISOString();
220
+ const line = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
221
+ appendFileSync(SCHEDULER_LOG_FILE, line);
222
+ }
223
+ export class Scheduler {
224
+ state;
225
+ tickIntervalMs;
226
+ projectDir;
227
+ running = false;
228
+ tickTimer = null;
229
+ routineChainByName = new Map();
230
+ constructor(options = {}) {
231
+ this.tickIntervalMs = options.tickIntervalMs ?? 10000; // 10 seconds default
232
+ this.projectDir = options.projectDir;
233
+ // Load or initialize state
234
+ const existingState = loadSchedulerState();
235
+ if (existingState) {
236
+ this.state = existingState;
237
+ }
238
+ else {
239
+ this.state = {
240
+ version: 1,
241
+ startedAt: new Date().toISOString(),
242
+ routines: {}
243
+ };
244
+ }
245
+ }
246
+ /**
247
+ * Refresh the list of scheduled routines from disk.
248
+ */
249
+ refreshRoutines() {
250
+ const scheduled = getScheduledRoutines(this.projectDir);
251
+ // Remove routines that no longer exist or are disabled
252
+ const currentNames = new Set(scheduled.map(r => r.name));
253
+ for (const name of Object.keys(this.state.routines)) {
254
+ if (!currentNames.has(name)) {
255
+ delete this.state.routines[name];
256
+ }
257
+ }
258
+ // Add or update routines
259
+ for (const routine of scheduled) {
260
+ const existing = this.state.routines[routine.name];
261
+ if (existing) {
262
+ // Update schedule if changed
263
+ existing.schedule = routine.schedule;
264
+ existing.path = routine.path;
265
+ // Recalculate next run if schedule changed
266
+ if (!existing.nextRunAt) {
267
+ existing.nextRunAt = getNextRunTime(routine.schedule)?.toISOString() ?? null;
268
+ }
269
+ }
270
+ else {
271
+ // New routine
272
+ this.state.routines[routine.name] = {
273
+ name: routine.name,
274
+ path: routine.path,
275
+ schedule: routine.schedule,
276
+ nextRunAt: getNextRunTime(routine.schedule)?.toISOString() ?? null,
277
+ lastRunAt: null,
278
+ lastStatus: null,
279
+ running: false,
280
+ retryCount: 0
281
+ };
282
+ }
283
+ }
284
+ saveSchedulerState(this.state);
285
+ }
286
+ /**
287
+ * Start the scheduler loop.
288
+ */
289
+ async start() {
290
+ if (this.running)
291
+ return;
292
+ this.running = true;
293
+ this.state.startedAt = new Date().toISOString();
294
+ appendSchedulerLog('info', 'Scheduler started');
295
+ this.refreshRoutines();
296
+ // Run tick loop
297
+ this.scheduleTick();
298
+ }
299
+ /**
300
+ * Stop the scheduler loop.
301
+ */
302
+ async stop() {
303
+ this.running = false;
304
+ if (this.tickTimer) {
305
+ clearTimeout(this.tickTimer);
306
+ this.tickTimer = null;
307
+ }
308
+ appendSchedulerLog('info', 'Scheduler stopped');
309
+ saveSchedulerState(this.state);
310
+ }
311
+ /**
312
+ * Get current scheduler state (for status display).
313
+ */
314
+ getState() {
315
+ return this.state;
316
+ }
317
+ scheduleTick() {
318
+ if (!this.running)
319
+ return;
320
+ this.tickTimer = setTimeout(async () => {
321
+ await this.tick();
322
+ this.scheduleTick();
323
+ }, this.tickIntervalMs);
324
+ }
325
+ isRoutineActive(name) {
326
+ return this.routineChainByName.has(name);
327
+ }
328
+ enqueueRoutineExecution(routine) {
329
+ const name = routine.name;
330
+ const prev = this.routineChainByName.get(name) ?? Promise.resolve();
331
+ const next = prev
332
+ .catch(() => {
333
+ // Keep queue alive even if a previous run rejected unexpectedly.
334
+ })
335
+ .then(async () => {
336
+ try {
337
+ await this.executeRoutine(routine);
338
+ }
339
+ catch (err) {
340
+ appendSchedulerLog('error', `Unexpected error executing routine ${routine.name}: ${err instanceof Error ? err.message : String(err)}`);
341
+ }
342
+ });
343
+ this.routineChainByName.set(name, next);
344
+ next.finally(() => {
345
+ if (this.routineChainByName.get(name) === next) {
346
+ this.routineChainByName.delete(name);
347
+ }
348
+ });
349
+ }
350
+ /**
351
+ * Execute one tick - check for routines that need to run.
352
+ */
353
+ async tick() {
354
+ const now = new Date();
355
+ // Refresh routines periodically to pick up changes
356
+ this.refreshRoutines();
357
+ for (const [name, routine] of Object.entries(this.state.routines)) {
358
+ if (!routine.nextRunAt)
359
+ continue;
360
+ const nextRun = new Date(routine.nextRunAt);
361
+ if (nextRun > now)
362
+ continue;
363
+ const concurrency = routine.schedule.concurrency ?? 'skip';
364
+ // Advance next run time immediately to avoid re-triggering the same run.
365
+ routine.nextRunAt = getNextRunTime(routine.schedule, now)?.toISOString() ?? null;
366
+ saveSchedulerState(this.state);
367
+ if (this.isRoutineActive(name)) {
368
+ if (concurrency === 'skip') {
369
+ appendSchedulerLog('debug', `Skipping ${name}: still running from previous invocation`);
370
+ continue;
371
+ }
372
+ appendSchedulerLog('debug', `Queueing ${name}: previous invocation still running`);
373
+ this.enqueueRoutineExecution(routine);
374
+ continue;
375
+ }
376
+ // Execute the routine (async, doesn't block the tick loop)
377
+ this.enqueueRoutineExecution(routine);
378
+ }
379
+ }
380
+ /**
381
+ * Execute a routine (async, doesn't block the tick loop).
382
+ */
383
+ async executeRoutine(routine) {
384
+ const startedAt = new Date();
385
+ routine.running = true;
386
+ saveSchedulerState(this.state);
387
+ appendSchedulerLog('info', `Starting routine: ${routine.name}`);
388
+ let result = null;
389
+ let error;
390
+ try {
391
+ const def = loadRoutineDefinition(routine.path);
392
+ const invocationDir = this.projectDir ?? process.cwd();
393
+ result = await runRoutine(def, {}, invocationDir);
394
+ }
395
+ catch (err) {
396
+ error = err instanceof Error ? err.message : String(err);
397
+ appendSchedulerLog('error', `Routine ${routine.name} failed: ${error}`);
398
+ }
399
+ const finishedAt = new Date();
400
+ const status = result?.status === 'success' ? 'success' : 'failed';
401
+ // Log routine output
402
+ if (result) {
403
+ for (const step of result.steps) {
404
+ // Log stderr (where progress messages typically go)
405
+ if (step.stderr) {
406
+ for (const line of step.stderr.split('\n')) {
407
+ if (line.trim()) {
408
+ appendSchedulerLog('info', `[${routine.name}] ${line}`);
409
+ }
410
+ }
411
+ }
412
+ // Log stdout if it's not JSON (JSON output is typically for machine consumption)
413
+ if (step.stdout && !step.json) {
414
+ for (const line of step.stdout.split('\n')) {
415
+ if (line.trim()) {
416
+ appendSchedulerLog('info', `[${routine.name}] ${line}`);
417
+ }
418
+ }
419
+ }
420
+ }
421
+ }
422
+ // Save run record
423
+ const record = {
424
+ routine: routine.name,
425
+ startedAt: startedAt.toISOString(),
426
+ finishedAt: finishedAt.toISOString(),
427
+ status,
428
+ exitCode: result?.exitCode ?? 1,
429
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
430
+ retryAttempt: routine.retryCount,
431
+ error
432
+ };
433
+ saveRunRecord(record);
434
+ // Update routine state
435
+ routine.running = false;
436
+ routine.lastRunAt = finishedAt.toISOString();
437
+ routine.lastStatus = status;
438
+ // Handle retries
439
+ const maxRetries = routine.schedule.retries ?? 0;
440
+ if (status === 'failed' && routine.retryCount < maxRetries) {
441
+ routine.retryCount++;
442
+ const retryDelay = routine.schedule.retryDelayMs ?? 60000;
443
+ routine.nextRunAt = new Date(finishedAt.getTime() + retryDelay).toISOString();
444
+ appendSchedulerLog('info', `Scheduling retry ${routine.retryCount}/${maxRetries} for ${routine.name}`);
445
+ }
446
+ else {
447
+ // Reset retry count and calculate next run
448
+ routine.retryCount = 0;
449
+ }
450
+ saveSchedulerState(this.state);
451
+ appendSchedulerLog('info', `Finished routine: ${routine.name} (${status})`);
452
+ }
453
+ /**
454
+ * Manually trigger a routine to run now.
455
+ */
456
+ async runNow(routineName) {
457
+ const routine = this.state.routines[routineName];
458
+ if (!routine) {
459
+ throw new Error(`Routine not found: ${routineName}`);
460
+ }
461
+ const startedAt = new Date();
462
+ let result = null;
463
+ let error;
464
+ try {
465
+ const def = loadRoutineDefinition(routine.path);
466
+ const invocationDir = this.projectDir ?? process.cwd();
467
+ result = await runRoutine(def, {}, invocationDir);
468
+ }
469
+ catch (err) {
470
+ error = err instanceof Error ? err.message : String(err);
471
+ }
472
+ const finishedAt = new Date();
473
+ const status = result?.status === 'success' ? 'success' : 'failed';
474
+ const record = {
475
+ routine: routineName,
476
+ startedAt: startedAt.toISOString(),
477
+ finishedAt: finishedAt.toISOString(),
478
+ status,
479
+ exitCode: result?.exitCode ?? 1,
480
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
481
+ retryAttempt: 0,
482
+ error
483
+ };
484
+ saveRunRecord(record);
485
+ // Update state
486
+ routine.lastRunAt = finishedAt.toISOString();
487
+ routine.lastStatus = status;
488
+ routine.nextRunAt = getNextRunTime(routine.schedule, finishedAt)?.toISOString() ?? null;
489
+ saveSchedulerState(this.state);
490
+ return record;
491
+ }
492
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Secrets management for cli4ai
3
+ *
4
+ * Priority order:
5
+ * 1. Scoped environment variables (C4AI_<PKG>__<KEY>)
6
+ * 2. Environment variables (for CI/CD)
7
+ * 3. Scoped local secrets vault (<pkg>:<key>)
8
+ * 4. Global local secrets vault (~/.cli4ai/secrets.json)
9
+ *
10
+ * Secrets are stored encrypted using a machine-specific key with random entropy.
11
+ *
12
+ * SECURITY: The encryption key is derived from:
13
+ * - Machine hostname
14
+ * - Username
15
+ * - A random 32-byte salt stored in ~/.cli4ai/secrets.salt
16
+ *
17
+ * This ensures that even if an attacker knows the hostname and username,
18
+ * they cannot reconstruct the key without access to the salt file.
19
+ */
20
+ export type SecretSource = 'env_scoped' | 'env' | 'vault_scoped' | 'vault' | 'missing';
21
+ /**
22
+ * Get a secret value (env var takes precedence)
23
+ */
24
+ export declare function getSecret(key: string, packageName?: string): string | undefined;
25
+ /**
26
+ * Set a secret in the vault
27
+ */
28
+ export declare function setSecret(key: string, value: string, packageName?: string): void;
29
+ /**
30
+ * Delete a secret from the vault
31
+ */
32
+ export declare function deleteSecret(key: string, packageName?: string): boolean;
33
+ /**
34
+ * List all secret keys (not values)
35
+ */
36
+ export declare function listSecretKeys(): string[];
37
+ /**
38
+ * Check if a secret exists (in env or vault)
39
+ */
40
+ export declare function hasSecret(key: string, packageName?: string): boolean;
41
+ /**
42
+ * Get secret source (for display)
43
+ */
44
+ export declare function getSecretSource(key: string, packageName?: string): SecretSource;
45
+ /**
46
+ * Export secrets as environment variables (for subprocess)
47
+ */
48
+ export declare function getSecretsAsEnv(keys: string[], packageName?: string): Record<string, string>;