skimpyclaw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +230 -0
- package/dist/__tests__/agent.test.d.ts +1 -0
- package/dist/__tests__/agent.test.js +131 -0
- package/dist/__tests__/api.test.d.ts +1 -0
- package/dist/__tests__/api.test.js +1227 -0
- package/dist/__tests__/audit.test.d.ts +1 -0
- package/dist/__tests__/audit.test.js +122 -0
- package/dist/__tests__/cache.test.d.ts +1 -0
- package/dist/__tests__/cache.test.js +65 -0
- package/dist/__tests__/channels.test.d.ts +1 -0
- package/dist/__tests__/channels.test.js +85 -0
- package/dist/__tests__/cli.integration.test.d.ts +1 -0
- package/dist/__tests__/cli.integration.test.js +16 -0
- package/dist/__tests__/cli.test.d.ts +1 -0
- package/dist/__tests__/cli.test.js +230 -0
- package/dist/__tests__/code-agents-executor.test.d.ts +1 -0
- package/dist/__tests__/code-agents-executor.test.js +75 -0
- package/dist/__tests__/code-agents-orchestrator.test.d.ts +1 -0
- package/dist/__tests__/code-agents-orchestrator.test.js +149 -0
- package/dist/__tests__/code-agents-parser.test.d.ts +1 -0
- package/dist/__tests__/code-agents-parser.test.js +39 -0
- package/dist/__tests__/code-agents-utils.test.d.ts +1 -0
- package/dist/__tests__/code-agents-utils.test.js +41 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +46 -0
- package/dist/__tests__/cron.test.d.ts +1 -0
- package/dist/__tests__/cron.test.js +66 -0
- package/dist/__tests__/dashboard-mode.test.d.ts +1 -0
- package/dist/__tests__/dashboard-mode.test.js +145 -0
- package/dist/__tests__/dashboard.test.d.ts +1 -0
- package/dist/__tests__/dashboard.test.js +43 -0
- package/dist/__tests__/doctor.formatters.test.d.ts +1 -0
- package/dist/__tests__/doctor.formatters.test.js +65 -0
- package/dist/__tests__/doctor.index.test.d.ts +1 -0
- package/dist/__tests__/doctor.index.test.js +48 -0
- package/dist/__tests__/doctor.runner.test.d.ts +1 -0
- package/dist/__tests__/doctor.runner.test.js +204 -0
- package/dist/__tests__/exec-approval.test.d.ts +1 -0
- package/dist/__tests__/exec-approval.test.js +323 -0
- package/dist/__tests__/file-lock.test.d.ts +1 -0
- package/dist/__tests__/file-lock.test.js +92 -0
- package/dist/__tests__/langfuse.test.d.ts +1 -0
- package/dist/__tests__/langfuse.test.js +40 -0
- package/dist/__tests__/model-selection.test.d.ts +1 -0
- package/dist/__tests__/model-selection.test.js +62 -0
- package/dist/__tests__/orchestrator.test.d.ts +1 -0
- package/dist/__tests__/orchestrator.test.js +425 -0
- package/dist/__tests__/providers-init.test.d.ts +1 -0
- package/dist/__tests__/providers-init.test.js +32 -0
- package/dist/__tests__/providers-routing.test.d.ts +1 -0
- package/dist/__tests__/providers-routing.test.js +25 -0
- package/dist/__tests__/providers-utils.test.d.ts +1 -0
- package/dist/__tests__/providers-utils.test.js +54 -0
- package/dist/__tests__/security.test.d.ts +1 -0
- package/dist/__tests__/security.test.js +22 -0
- package/dist/__tests__/sessions.test.d.ts +1 -0
- package/dist/__tests__/sessions.test.js +147 -0
- package/dist/__tests__/setup.test.d.ts +1 -0
- package/dist/__tests__/setup.test.js +114 -0
- package/dist/__tests__/skills.test.d.ts +1 -0
- package/dist/__tests__/skills.test.js +333 -0
- package/dist/__tests__/subagent.test.d.ts +1 -0
- package/dist/__tests__/subagent.test.js +240 -0
- package/dist/__tests__/telegram-utils.test.d.ts +1 -0
- package/dist/__tests__/telegram-utils.test.js +22 -0
- package/dist/__tests__/telegram.test.d.ts +1 -0
- package/dist/__tests__/telegram.test.js +42 -0
- package/dist/__tests__/token-efficiency.test.d.ts +1 -0
- package/dist/__tests__/token-efficiency.test.js +38 -0
- package/dist/__tests__/tool-guard.test.d.ts +1 -0
- package/dist/__tests__/tool-guard.test.js +105 -0
- package/dist/__tests__/tools.test.d.ts +1 -0
- package/dist/__tests__/tools.test.js +589 -0
- package/dist/__tests__/usage.test.d.ts +1 -0
- package/dist/__tests__/usage.test.js +197 -0
- package/dist/__tests__/voice.test.d.ts +1 -0
- package/dist/__tests__/voice.test.js +214 -0
- package/dist/agent.d.ts +24 -0
- package/dist/agent.js +269 -0
- package/dist/api.d.ts +3 -0
- package/dist/api.js +943 -0
- package/dist/audit.d.ts +26 -0
- package/dist/audit.js +121 -0
- package/dist/cache.d.ts +8 -0
- package/dist/cache.js +24 -0
- package/dist/channels/telegram/handlers.d.ts +41 -0
- package/dist/channels/telegram/handlers.js +498 -0
- package/dist/channels/telegram/index.d.ts +14 -0
- package/dist/channels/telegram/index.js +326 -0
- package/dist/channels/telegram/types.d.ts +26 -0
- package/dist/channels/telegram/types.js +31 -0
- package/dist/channels/telegram/utils.d.ts +25 -0
- package/dist/channels/telegram/utils.js +256 -0
- package/dist/channels.d.ts +11 -0
- package/dist/channels.js +118 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +768 -0
- package/dist/code-agents/executor.d.ts +5 -0
- package/dist/code-agents/executor.js +463 -0
- package/dist/code-agents/index.d.ts +22 -0
- package/dist/code-agents/index.js +199 -0
- package/dist/code-agents/orchestrator.d.ts +23 -0
- package/dist/code-agents/orchestrator.js +403 -0
- package/dist/code-agents/parser.d.ts +21 -0
- package/dist/code-agents/parser.js +197 -0
- package/dist/code-agents/registry.d.ts +27 -0
- package/dist/code-agents/registry.js +147 -0
- package/dist/code-agents/types.d.ts +66 -0
- package/dist/code-agents/types.js +4 -0
- package/dist/code-agents/utils.d.ts +36 -0
- package/dist/code-agents/utils.js +236 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +123 -0
- package/dist/cron.d.ts +49 -0
- package/dist/cron.js +400 -0
- package/dist/dashboard/assets/index-CZJCvMSN.js +65 -0
- package/dist/dashboard/assets/index-EAg6lqF5.css +1 -0
- package/dist/dashboard/favicon.svg +3 -0
- package/dist/dashboard/index.html +21 -0
- package/dist/dashboard-frontend.d.ts +7 -0
- package/dist/dashboard-frontend.js +86 -0
- package/dist/dashboard.d.ts +8 -0
- package/dist/dashboard.js +4071 -0
- package/dist/digests.d.ts +36 -0
- package/dist/digests.js +338 -0
- package/dist/discord.d.ts +8 -0
- package/dist/discord.js +828 -0
- package/dist/doctor/checks.d.ts +18 -0
- package/dist/doctor/checks.js +368 -0
- package/dist/doctor/formatters.d.ts +3 -0
- package/dist/doctor/formatters.js +44 -0
- package/dist/doctor/index.d.ts +8 -0
- package/dist/doctor/index.js +7 -0
- package/dist/doctor/runner.d.ts +3 -0
- package/dist/doctor/runner.js +109 -0
- package/dist/doctor/types.d.ts +20 -0
- package/dist/doctor/types.js +1 -0
- package/dist/exec-approval.d.ts +101 -0
- package/dist/exec-approval.js +432 -0
- package/dist/file-lock.d.ts +34 -0
- package/dist/file-lock.js +81 -0
- package/dist/gateway.d.ts +8 -0
- package/dist/gateway.js +114 -0
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.js +101 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +75 -0
- package/dist/langfuse.d.ts +34 -0
- package/dist/langfuse.js +145 -0
- package/dist/mcp-context-a8c.d.ts +13 -0
- package/dist/mcp-context-a8c.js +34 -0
- package/dist/model-selection.d.ts +18 -0
- package/dist/model-selection.js +50 -0
- package/dist/orchestrator.d.ts +15 -0
- package/dist/orchestrator.js +676 -0
- package/dist/providers/anthropic.d.ts +7 -0
- package/dist/providers/anthropic.js +319 -0
- package/dist/providers/codex.d.ts +17 -0
- package/dist/providers/codex.js +508 -0
- package/dist/providers/content.d.ts +21 -0
- package/dist/providers/content.js +55 -0
- package/dist/providers/index.d.ts +13 -0
- package/dist/providers/index.js +138 -0
- package/dist/providers/observability.d.ts +19 -0
- package/dist/providers/observability.js +94 -0
- package/dist/providers/openai.d.ts +10 -0
- package/dist/providers/openai.js +310 -0
- package/dist/providers/tool-guard.d.ts +30 -0
- package/dist/providers/tool-guard.js +89 -0
- package/dist/providers/types.d.ts +34 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/utils.d.ts +65 -0
- package/dist/providers/utils.js +199 -0
- package/dist/security.d.ts +8 -0
- package/dist/security.js +113 -0
- package/dist/service.d.ts +8 -0
- package/dist/service.js +38 -0
- package/dist/sessions.d.ts +35 -0
- package/dist/sessions.js +142 -0
- package/dist/setup.d.ts +36 -0
- package/dist/setup.js +821 -0
- package/dist/skills-types.d.ts +65 -0
- package/dist/skills-types.js +2 -0
- package/dist/skills.d.ts +32 -0
- package/dist/skills.js +260 -0
- package/dist/subagent.d.ts +19 -0
- package/dist/subagent.js +376 -0
- package/dist/telegram.d.ts +2 -0
- package/dist/telegram.js +11 -0
- package/dist/tools/bash-tool.d.ts +3 -0
- package/dist/tools/bash-tool.js +59 -0
- package/dist/tools/browser-tool.d.ts +3 -0
- package/dist/tools/browser-tool.js +265 -0
- package/dist/tools/definitions.d.ts +432 -0
- package/dist/tools/definitions.js +181 -0
- package/dist/tools/execute-context.d.ts +26 -0
- package/dist/tools/execute-context.js +1 -0
- package/dist/tools/file-tools.d.ts +8 -0
- package/dist/tools/file-tools.js +67 -0
- package/dist/tools/path-utils.d.ts +1 -0
- package/dist/tools/path-utils.js +8 -0
- package/dist/tools.d.ts +24 -0
- package/dist/tools.js +281 -0
- package/dist/types.d.ts +259 -0
- package/dist/types.js +2 -0
- package/dist/usage.d.ts +76 -0
- package/dist/usage.js +150 -0
- package/dist/voice.d.ts +37 -0
- package/dist/voice.js +461 -0
- package/package.json +70 -0
- package/templates/AGENTS.md +38 -0
- package/templates/BOOT.md +23 -0
- package/templates/BOOTSTRAP.md +26 -0
- package/templates/HEARTBEAT.md +5 -0
- package/templates/IDENTITY.md +5 -0
- package/templates/MEMORY.md +24 -0
- package/templates/SOUL.md +92 -0
- package/templates/TOOLS.md +30 -0
- package/templates/USER.md +31 -0
package/dist/cron.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
// Cron scheduler using croner
|
|
2
|
+
import { Cron } from 'croner';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { existsSync, mkdirSync, appendFileSync, readFileSync, watch } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { getLogsDir, getConfigPath, loadConfig } from './config.js';
|
|
7
|
+
import { runAgentTurn } from './agent.js';
|
|
8
|
+
import { startTrace, addEvent, endTrace } from './audit.js';
|
|
9
|
+
import { sendActiveChannelProactiveMessage, sendActiveChannelProactiveVoice, getActiveChannelId } from './channels.js';
|
|
10
|
+
import { parseAndSaveDigest } from './digests.js';
|
|
11
|
+
import { synthesizeSpeech } from './voice.js';
|
|
12
|
+
const scheduledJobs = new Map();
|
|
13
|
+
let configWatcher = null;
|
|
14
|
+
function getCronLogDir() {
|
|
15
|
+
const dir = join(getLogsDir(), 'cron');
|
|
16
|
+
if (!existsSync(dir)) {
|
|
17
|
+
mkdirSync(dir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
return dir;
|
|
20
|
+
}
|
|
21
|
+
function getCronLogPath(jobId) {
|
|
22
|
+
const date = new Date().toISOString().split('T')[0];
|
|
23
|
+
return join(getCronLogDir(), `${jobId}-${date}.log`);
|
|
24
|
+
}
|
|
25
|
+
function writeCronLog(entry) {
|
|
26
|
+
const logPath = getCronLogPath(entry.jobId);
|
|
27
|
+
const separator = '\n' + '='.repeat(60) + '\n';
|
|
28
|
+
const header = `[${entry.startedAt}] ${entry.jobName} (${entry.jobId})`;
|
|
29
|
+
const status = `Status: ${entry.status}`;
|
|
30
|
+
const duration = entry.durationMs != null ? `Duration: ${(entry.durationMs / 1000).toFixed(1)}s` : '';
|
|
31
|
+
const error = entry.error ? `Error: ${entry.error}` : '';
|
|
32
|
+
const output = entry.output ? `Output:\n${entry.output}` : '';
|
|
33
|
+
const lines = [separator, header, status, duration, error, output]
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.join('\n');
|
|
36
|
+
appendFileSync(logPath, lines + '\n');
|
|
37
|
+
}
|
|
38
|
+
function appendCronLogLine(jobId, line) {
|
|
39
|
+
const logPath = getCronLogPath(jobId);
|
|
40
|
+
const timestamp = new Date().toISOString();
|
|
41
|
+
appendFileSync(logPath, `[${timestamp}] ${line}\n`);
|
|
42
|
+
}
|
|
43
|
+
// Track currently running jobs for status queries
|
|
44
|
+
const runningJobs = new Map();
|
|
45
|
+
export function getCronRunStatus() {
|
|
46
|
+
return {
|
|
47
|
+
running: Array.from(runningJobs.keys()),
|
|
48
|
+
recent: Array.from(runningJobs.values()),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function initCron(config) {
|
|
52
|
+
// Clear existing jobs
|
|
53
|
+
for (const job of scheduledJobs.values()) {
|
|
54
|
+
job.job.stop();
|
|
55
|
+
}
|
|
56
|
+
scheduledJobs.clear();
|
|
57
|
+
// Schedule new jobs
|
|
58
|
+
for (const jobDef of config.cron.jobs) {
|
|
59
|
+
scheduleJob(jobDef, config);
|
|
60
|
+
}
|
|
61
|
+
console.log(`[cron] Initialized ${scheduledJobs.size} jobs`);
|
|
62
|
+
// Watch config.json for changes and re-initialize cron jobs
|
|
63
|
+
if (configWatcher) {
|
|
64
|
+
configWatcher.close();
|
|
65
|
+
configWatcher = null;
|
|
66
|
+
}
|
|
67
|
+
let debounceTimer = null;
|
|
68
|
+
try {
|
|
69
|
+
configWatcher = watch(getConfigPath(), () => {
|
|
70
|
+
if (debounceTimer)
|
|
71
|
+
clearTimeout(debounceTimer);
|
|
72
|
+
debounceTimer = setTimeout(() => {
|
|
73
|
+
try {
|
|
74
|
+
const newConfig = loadConfig();
|
|
75
|
+
console.log('[cron] Config changed, reloading cron jobs...');
|
|
76
|
+
for (const job of scheduledJobs.values()) {
|
|
77
|
+
job.job.stop();
|
|
78
|
+
}
|
|
79
|
+
scheduledJobs.clear();
|
|
80
|
+
for (const jobDef of newConfig.cron.jobs) {
|
|
81
|
+
scheduleJob(jobDef, newConfig);
|
|
82
|
+
}
|
|
83
|
+
console.log(`[cron] Reloaded ${scheduledJobs.size} jobs`);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
console.error('[cron] Failed to reload config:', err);
|
|
87
|
+
}
|
|
88
|
+
}, 1000);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
console.warn('[cron] Could not watch config file:', err);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function scheduleJob(jobDef, config) {
|
|
96
|
+
if (jobDef.schedule.kind !== 'cron') {
|
|
97
|
+
console.warn(`[cron] Unsupported schedule kind: ${jobDef.schedule.kind}`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const cronJob = new Cron(jobDef.schedule.expr, {
|
|
101
|
+
timezone: jobDef.schedule.tz || 'America/Chicago',
|
|
102
|
+
}, async () => {
|
|
103
|
+
console.log(`[cron] Running job: ${jobDef.name}`);
|
|
104
|
+
await executeJobPayload(jobDef, config);
|
|
105
|
+
});
|
|
106
|
+
scheduledJobs.set(jobDef.id, {
|
|
107
|
+
id: jobDef.id,
|
|
108
|
+
name: jobDef.name,
|
|
109
|
+
job: cronJob,
|
|
110
|
+
nextRun: cronJob.nextRun() ?? undefined,
|
|
111
|
+
});
|
|
112
|
+
console.log(`[cron] Scheduled: ${jobDef.name} (${jobDef.schedule.expr})`);
|
|
113
|
+
}
|
|
114
|
+
async function executeJobPayload(jobDef, config) {
|
|
115
|
+
const logEntry = {
|
|
116
|
+
jobId: jobDef.id,
|
|
117
|
+
jobName: jobDef.name,
|
|
118
|
+
startedAt: new Date().toISOString(),
|
|
119
|
+
status: 'running',
|
|
120
|
+
};
|
|
121
|
+
runningJobs.set(jobDef.id, logEntry);
|
|
122
|
+
// Log start immediately
|
|
123
|
+
appendCronLogLine(jobDef.id, `=== STARTED: ${jobDef.name} (${jobDef.id}) ===`);
|
|
124
|
+
appendCronLogLine(jobDef.id, `Model: ${jobDef.model || 'default'}`);
|
|
125
|
+
appendCronLogLine(jobDef.id, `Payload: ${jobDef.payload.kind}`);
|
|
126
|
+
// Notify channel at start
|
|
127
|
+
try {
|
|
128
|
+
await sendActiveChannelProactiveMessage(config, `🔄 Cron starting: ${jobDef.name}`);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Non-critical
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
if (jobDef.payload.kind === 'agentTurn') {
|
|
135
|
+
const message = expandVariables(resolveMessageSource(jobDef.payload.message || ''));
|
|
136
|
+
appendCronLogLine(jobDef.id, `Agent turn started (prompt: ${message.slice(0, 100)}...)`);
|
|
137
|
+
const response = await runAgentTurn(config.agents.default, message, config, jobDef.model, jobDef.payload.tools, undefined, {
|
|
138
|
+
channel: getActiveChannelId() || 'telegram',
|
|
139
|
+
trigger: 'cron',
|
|
140
|
+
sessionId: jobDef.id,
|
|
141
|
+
metadata: { jobName: jobDef.name },
|
|
142
|
+
});
|
|
143
|
+
appendCronLogLine(jobDef.id, `Agent turn completed (${response.length} chars)`);
|
|
144
|
+
// Parse dual output (voice + text) if delimiters present
|
|
145
|
+
const { voice: voicePortion, text: textPortion } = parseDualOutput(response);
|
|
146
|
+
if (voicePortion) {
|
|
147
|
+
appendCronLogLine(jobDef.id, `Dual output parsed: voice=${voicePortion.length} chars, text=${textPortion.length} chars`);
|
|
148
|
+
}
|
|
149
|
+
// Use text portion for log output and notifications
|
|
150
|
+
logEntry.output = textPortion.slice(0, 5000);
|
|
151
|
+
// Parse and save digest from the text portion
|
|
152
|
+
try {
|
|
153
|
+
parseAndSaveDigest(jobDef.id, jobDef.name, textPortion);
|
|
154
|
+
appendCronLogLine(jobDef.id, 'Digest saved');
|
|
155
|
+
}
|
|
156
|
+
catch (digestErr) {
|
|
157
|
+
const errMsg = digestErr instanceof Error ? digestErr.message : String(digestErr);
|
|
158
|
+
appendCronLogLine(jobDef.id, `Failed to save digest: ${errMsg}`);
|
|
159
|
+
}
|
|
160
|
+
// Synthesize and send voice if configured
|
|
161
|
+
if (jobDef.payload.sendAsVoice && config.voice) {
|
|
162
|
+
try {
|
|
163
|
+
// Use voice portion if available, fall back to text
|
|
164
|
+
const voiceContent = voicePortion || textPortion;
|
|
165
|
+
appendCronLogLine(jobDef.id, `Synthesizing voice (${voicePortion ? 'voice portion' : 'full text fallback'})...`);
|
|
166
|
+
const speech = await synthesizeSpeech(voiceContent, config.voice);
|
|
167
|
+
appendCronLogLine(jobDef.id, `Voice synthesized (${speech.format}, ${speech.provider}, ${speech.buffer.length} bytes)`);
|
|
168
|
+
const sent = await sendActiveChannelProactiveVoice(config, speech.buffer, speech.format);
|
|
169
|
+
if (sent) {
|
|
170
|
+
appendCronLogLine(jobDef.id, 'Voice message sent to active channel');
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
appendCronLogLine(jobDef.id, 'No active channel for voice message');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (voiceErr) {
|
|
177
|
+
const errMsg = voiceErr instanceof Error ? voiceErr.message : String(voiceErr);
|
|
178
|
+
appendCronLogLine(jobDef.id, `Voice synthesis failed: ${errMsg}`);
|
|
179
|
+
// Non-fatal — text notification still sends
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else if (jobDef.payload.kind === 'script') {
|
|
184
|
+
const scriptTraceId = startTrace('cron');
|
|
185
|
+
addEvent(scriptTraceId, {
|
|
186
|
+
type: 'script_start',
|
|
187
|
+
summary: `${jobDef.name}: ${(jobDef.payload.script || '').slice(0, 100)}`,
|
|
188
|
+
durationMs: 0,
|
|
189
|
+
});
|
|
190
|
+
appendCronLogLine(jobDef.id, `Script started: ${(jobDef.payload.script || '').slice(0, 100)}`);
|
|
191
|
+
try {
|
|
192
|
+
const output = await executeScript(jobDef);
|
|
193
|
+
logEntry.output = output.slice(0, 50000);
|
|
194
|
+
appendCronLogLine(jobDef.id, `Script completed (${output.length} chars)`);
|
|
195
|
+
addEvent(scriptTraceId, {
|
|
196
|
+
type: 'script_complete',
|
|
197
|
+
summary: `Output: ${output.slice(0, 150)}`,
|
|
198
|
+
durationMs: 0,
|
|
199
|
+
});
|
|
200
|
+
await endTrace(scriptTraceId, 'ok');
|
|
201
|
+
}
|
|
202
|
+
catch (scriptErr) {
|
|
203
|
+
const scriptErrMsg = scriptErr instanceof Error ? scriptErr.message : String(scriptErr);
|
|
204
|
+
addEvent(scriptTraceId, {
|
|
205
|
+
type: 'script_error',
|
|
206
|
+
summary: scriptErrMsg.slice(0, 150),
|
|
207
|
+
durationMs: 0,
|
|
208
|
+
});
|
|
209
|
+
await endTrace(scriptTraceId, 'error');
|
|
210
|
+
throw scriptErr;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
logEntry.status = 'success';
|
|
214
|
+
appendCronLogLine(jobDef.id, `=== COMPLETED: success ===`);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
218
|
+
logEntry.status = msg.includes('TIMEOUT') || msg.includes('timed out') ? 'timeout' : 'error';
|
|
219
|
+
logEntry.error = msg;
|
|
220
|
+
appendCronLogLine(jobDef.id, `=== FAILED: ${logEntry.status} — ${msg.slice(0, 200)} ===`);
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
logEntry.finishedAt = new Date().toISOString();
|
|
225
|
+
logEntry.durationMs = new Date(logEntry.finishedAt).getTime() - new Date(logEntry.startedAt).getTime();
|
|
226
|
+
writeCronLog(logEntry);
|
|
227
|
+
runningJobs.delete(jobDef.id);
|
|
228
|
+
const elapsed = (logEntry.durationMs / 1000).toFixed(1);
|
|
229
|
+
console.log(`[cron] Job ${jobDef.id} finished: ${logEntry.status} (${elapsed}s)`);
|
|
230
|
+
// Notify active channel
|
|
231
|
+
try {
|
|
232
|
+
const icon = logEntry.status === 'success' ? '✅' : logEntry.status === 'timeout' ? '⏰' : '❌';
|
|
233
|
+
let notification = `${icon} Cron: ${jobDef.name} — ${logEntry.status} (${elapsed}s)`;
|
|
234
|
+
if (logEntry.error) {
|
|
235
|
+
notification += `\nError: ${logEntry.error.slice(0, 200)}`;
|
|
236
|
+
}
|
|
237
|
+
// Send status + output preview (truncated at 4000 chars for Telegram limit)
|
|
238
|
+
if (logEntry.status === 'success' && logEntry.output) {
|
|
239
|
+
const output = logEntry.output.length > 4000
|
|
240
|
+
? logEntry.output.slice(0, 4000) + '...'
|
|
241
|
+
: logEntry.output;
|
|
242
|
+
notification += `\n\n${output}`;
|
|
243
|
+
}
|
|
244
|
+
await sendActiveChannelProactiveMessage(config, notification);
|
|
245
|
+
}
|
|
246
|
+
catch (notifyErr) {
|
|
247
|
+
console.error(`[cron] Failed to send notification: ${notifyErr}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function executeScript(jobDef) {
|
|
252
|
+
const script = expandVariables(jobDef.payload.script || '');
|
|
253
|
+
if (!script) {
|
|
254
|
+
throw new Error(`Script payload is empty for job: ${jobDef.id}`);
|
|
255
|
+
}
|
|
256
|
+
const cwd = jobDef.payload.cwd;
|
|
257
|
+
if (cwd && !existsSync(cwd)) {
|
|
258
|
+
throw new Error(`Working directory does not exist: ${cwd}`);
|
|
259
|
+
}
|
|
260
|
+
const timeoutMs = jobDef.payload.timeoutMs || 600000; // 10 min default
|
|
261
|
+
return new Promise((resolve, reject) => {
|
|
262
|
+
const startTime = Date.now();
|
|
263
|
+
console.log(`[cron:script] Running: ${script.slice(0, 100)}${script.length > 100 ? '...' : ''}`);
|
|
264
|
+
if (cwd)
|
|
265
|
+
console.log(`[cron:script] cwd: ${cwd}`);
|
|
266
|
+
const child = exec(script, {
|
|
267
|
+
cwd: cwd || undefined,
|
|
268
|
+
timeout: timeoutMs,
|
|
269
|
+
env: { ...process.env },
|
|
270
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB output buffer
|
|
271
|
+
}, (error, stdout, stderr) => {
|
|
272
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
273
|
+
if (error) {
|
|
274
|
+
console.error(`[cron:script] Failed after ${elapsed}s: ${error.message}`);
|
|
275
|
+
if (stderr)
|
|
276
|
+
console.error(`[cron:script] stderr: ${stderr.slice(0, 500)}`);
|
|
277
|
+
reject(error);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
console.log(`[cron:script] Completed in ${elapsed}s`);
|
|
281
|
+
if (stdout) {
|
|
282
|
+
const lines = stdout.trim().split('\n');
|
|
283
|
+
const preview = lines.length > 5
|
|
284
|
+
? lines.slice(0, 3).join('\n') + `\n... (${lines.length} lines total)`
|
|
285
|
+
: stdout.trim();
|
|
286
|
+
console.log(`[cron:script] Output:\n${preview}`);
|
|
287
|
+
}
|
|
288
|
+
resolve(stdout || '');
|
|
289
|
+
});
|
|
290
|
+
// Log PID for debugging
|
|
291
|
+
if (child.pid) {
|
|
292
|
+
console.log(`[cron:script] PID: ${child.pid}`);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* If message is a path to a .md file, read and return its contents.
|
|
298
|
+
* Supports ~ home expansion. Otherwise returns the string as-is.
|
|
299
|
+
*/
|
|
300
|
+
function resolveMessageSource(message) {
|
|
301
|
+
const trimmed = message.trim();
|
|
302
|
+
if (!trimmed.endsWith('.md'))
|
|
303
|
+
return message;
|
|
304
|
+
// Expand ~ to home directory
|
|
305
|
+
const resolved = trimmed.startsWith('~/')
|
|
306
|
+
? join(process.env.HOME || '', trimmed.slice(2))
|
|
307
|
+
: trimmed;
|
|
308
|
+
if (existsSync(resolved)) {
|
|
309
|
+
console.log(`[cron] Loading prompt from file: ${resolved}`);
|
|
310
|
+
return readFileSync(resolved, 'utf-8');
|
|
311
|
+
}
|
|
312
|
+
// Fallback: check ~/.skimpyclaw/prompts/ directory
|
|
313
|
+
const promptsDir = join(process.env.HOME || '', '.skimpyclaw', 'prompts', trimmed);
|
|
314
|
+
if (existsSync(promptsDir)) {
|
|
315
|
+
console.log(`[cron] Loading prompt from prompts dir: ${promptsDir}`);
|
|
316
|
+
return readFileSync(promptsDir, 'utf-8');
|
|
317
|
+
}
|
|
318
|
+
console.warn(`[cron] Could not resolve prompt file: ${trimmed}`);
|
|
319
|
+
// Not a valid file path — treat as regular message text
|
|
320
|
+
return message;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Parse dual-output format with ---VOICE--- and ---TEXT--- delimiters.
|
|
324
|
+
* Returns voice (concise, no links) and text (full detail) portions.
|
|
325
|
+
* If delimiters not found, returns full response as text with voice = null (backward compatible).
|
|
326
|
+
*/
|
|
327
|
+
export function parseDualOutput(response) {
|
|
328
|
+
const voiceMarker = '---VOICE---';
|
|
329
|
+
const textMarker = '---TEXT---';
|
|
330
|
+
const voiceIdx = response.indexOf(voiceMarker);
|
|
331
|
+
const textIdx = response.indexOf(textMarker);
|
|
332
|
+
if (voiceIdx === -1 || textIdx === -1) {
|
|
333
|
+
// No delimiters — backward compatible
|
|
334
|
+
return { voice: null, text: response };
|
|
335
|
+
}
|
|
336
|
+
// Extract content between markers
|
|
337
|
+
const voiceStart = voiceIdx + voiceMarker.length;
|
|
338
|
+
const voice = response.slice(voiceStart, textIdx).trim();
|
|
339
|
+
const text = response.slice(textIdx + textMarker.length).trim();
|
|
340
|
+
return {
|
|
341
|
+
voice: voice || null,
|
|
342
|
+
text: text || response,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function expandVariables(message) {
|
|
346
|
+
const now = new Date();
|
|
347
|
+
const date = now.toLocaleDateString('en-US', {
|
|
348
|
+
month: 'long',
|
|
349
|
+
day: 'numeric',
|
|
350
|
+
year: 'numeric',
|
|
351
|
+
});
|
|
352
|
+
return message
|
|
353
|
+
.replace(/\{\{date\}\}/g, date)
|
|
354
|
+
.replace(/\{\{time\}\}/g, now.toLocaleTimeString())
|
|
355
|
+
.replace(/\{\{iso\}\}/g, now.toISOString());
|
|
356
|
+
}
|
|
357
|
+
export function getCronJobs() {
|
|
358
|
+
return Array.from(scheduledJobs.values()).map(j => ({
|
|
359
|
+
id: j.id,
|
|
360
|
+
name: j.name,
|
|
361
|
+
nextRun: j.job.nextRun() ?? undefined,
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
export async function runCronJob(id, config) {
|
|
365
|
+
const jobDef = config.cron.jobs.find(j => j.id === id);
|
|
366
|
+
if (!jobDef) {
|
|
367
|
+
throw new Error(`Cron job not found: ${id}`);
|
|
368
|
+
}
|
|
369
|
+
await executeJobPayload(jobDef, config);
|
|
370
|
+
}
|
|
371
|
+
export function getCronJobDetails(config) {
|
|
372
|
+
return config.cron.jobs.map(jobDef => {
|
|
373
|
+
const scheduled = scheduledJobs.get(jobDef.id);
|
|
374
|
+
return {
|
|
375
|
+
id: jobDef.id,
|
|
376
|
+
name: jobDef.name,
|
|
377
|
+
schedule: {
|
|
378
|
+
kind: jobDef.schedule.kind,
|
|
379
|
+
expr: jobDef.schedule.expr,
|
|
380
|
+
tz: jobDef.schedule.tz,
|
|
381
|
+
},
|
|
382
|
+
payload: {
|
|
383
|
+
kind: jobDef.payload.kind,
|
|
384
|
+
message: jobDef.payload.message,
|
|
385
|
+
},
|
|
386
|
+
model: jobDef.model,
|
|
387
|
+
nextRun: scheduled?.job.nextRun() ?? undefined,
|
|
388
|
+
};
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
export function stopCron() {
|
|
392
|
+
if (configWatcher) {
|
|
393
|
+
configWatcher.close();
|
|
394
|
+
configWatcher = null;
|
|
395
|
+
}
|
|
396
|
+
for (const job of scheduledJobs.values()) {
|
|
397
|
+
job.job.stop();
|
|
398
|
+
}
|
|
399
|
+
scheduledJobs.clear();
|
|
400
|
+
}
|