skimpyclaw 0.3.14 → 0.4.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 +47 -37
- package/dist/__tests__/adapter-types.test.d.ts +4 -0
- package/dist/__tests__/adapter-types.test.js +63 -0
- package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
- package/dist/__tests__/anthropic-adapter.test.js +264 -0
- package/dist/__tests__/api.test.js +0 -1
- package/dist/__tests__/cli.integration.test.js +2 -4
- package/dist/__tests__/cli.test.js +0 -1
- package/dist/__tests__/code-agents-notifications.test.js +137 -0
- package/dist/__tests__/code-agents-parser.test.js +19 -1
- package/dist/__tests__/code-agents-preflight.test.js +3 -28
- package/dist/__tests__/code-agents-utils.test.js +34 -9
- package/dist/__tests__/code-agents-worktrees.test.js +116 -0
- package/dist/__tests__/codex-adapter.test.js +184 -0
- package/dist/__tests__/codex-auth.test.js +66 -0
- package/dist/__tests__/codex-provider-gating.test.js +35 -0
- package/dist/__tests__/codex-unified-loop.test.js +111 -0
- package/dist/__tests__/config-security.test.js +127 -0
- package/dist/__tests__/config.test.js +23 -0
- package/dist/__tests__/context-manager.test.js +243 -164
- package/dist/__tests__/cron-run.test.js +250 -0
- package/dist/__tests__/cron.test.js +12 -38
- package/dist/__tests__/digests.test.js +67 -0
- package/dist/__tests__/discord-attachments.test.js +211 -0
- package/dist/__tests__/discord-docs.test.d.ts +1 -0
- package/dist/__tests__/discord-docs.test.js +27 -0
- package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-agents.test.js +115 -0
- package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-context.test.js +42 -0
- package/dist/__tests__/doctor.formatters.test.js +4 -4
- package/dist/__tests__/doctor.index.test.js +1 -1
- package/dist/__tests__/doctor.runner.test.js +3 -15
- package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
- package/dist/__tests__/env-sanitizer.test.js +45 -0
- package/dist/__tests__/exec-approval.test.js +61 -0
- package/dist/__tests__/fetch-tool.test.d.ts +1 -0
- package/dist/__tests__/fetch-tool.test.js +85 -0
- package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
- package/dist/__tests__/gateway-status-auth.test.js +72 -0
- package/dist/__tests__/heartbeat.test.js +3 -3
- package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
- package/dist/__tests__/interactive-sessions.test.js +96 -0
- package/dist/__tests__/langfuse.test.js +6 -18
- package/dist/__tests__/model-selection.test.js +3 -4
- package/dist/__tests__/providers-init.test.js +2 -8
- package/dist/__tests__/providers-routing.test.js +1 -1
- package/dist/__tests__/providers-utils.test.js +13 -3
- package/dist/__tests__/sessions.test.js +14 -10
- package/dist/__tests__/setup.test.js +12 -29
- package/dist/__tests__/skills.test.js +10 -7
- package/dist/__tests__/stream-formatter.test.d.ts +1 -0
- package/dist/__tests__/stream-formatter.test.js +114 -0
- package/dist/__tests__/token-efficiency.test.js +131 -15
- package/dist/__tests__/tool-loop.test.d.ts +4 -0
- package/dist/__tests__/tool-loop.test.js +505 -0
- package/dist/__tests__/tools.test.js +101 -276
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +14 -0
- package/dist/__tests__/voice.test.js +21 -0
- package/dist/agent.js +35 -4
- package/dist/api.js +113 -37
- package/dist/channels/discord/attachments.d.ts +50 -0
- package/dist/channels/discord/attachments.js +137 -0
- package/dist/channels/discord/delegation.d.ts +5 -0
- package/dist/channels/discord/delegation.js +136 -0
- package/dist/channels/discord/handlers.js +694 -7
- package/dist/channels/discord/index.d.ts +16 -1
- package/dist/channels/discord/index.js +64 -1
- package/dist/channels/discord/thread-agents.d.ts +54 -0
- package/dist/channels/discord/thread-agents.js +323 -0
- package/dist/channels/discord/threads.d.ts +58 -0
- package/dist/channels/discord/threads.js +192 -0
- package/dist/channels/discord/types.js +4 -2
- package/dist/channels/discord/utils.d.ts +16 -0
- package/dist/channels/discord/utils.js +86 -6
- package/dist/channels/telegram/index.d.ts +1 -1
- package/dist/channels/telegram/types.js +1 -1
- package/dist/channels/telegram/utils.js +9 -3
- package/dist/channels.d.ts +1 -1
- package/dist/cli.js +20 -400
- package/dist/code-agents/executor.d.ts +1 -1
- package/dist/code-agents/executor.js +101 -45
- package/dist/code-agents/index.d.ts +2 -7
- package/dist/code-agents/index.js +111 -80
- package/dist/code-agents/interactive-resume.d.ts +6 -0
- package/dist/code-agents/interactive-resume.js +98 -0
- package/dist/code-agents/interactive-sessions.d.ts +20 -0
- package/dist/code-agents/interactive-sessions.js +132 -0
- package/dist/code-agents/parser.js +5 -1
- package/dist/code-agents/registry.d.ts +7 -1
- package/dist/code-agents/registry.js +11 -23
- package/dist/code-agents/stream-formatter.d.ts +8 -0
- package/dist/code-agents/stream-formatter.js +92 -0
- package/dist/code-agents/types.d.ts +16 -24
- package/dist/code-agents/utils.d.ts +35 -11
- package/dist/code-agents/utils.js +349 -95
- package/dist/code-agents/worktrees.d.ts +37 -0
- package/dist/code-agents/worktrees.js +116 -0
- package/dist/config.d.ts +2 -4
- package/dist/config.js +123 -23
- package/dist/cron.d.ts +1 -6
- package/dist/cron.js +175 -82
- package/dist/dashboard/assets/index-B345aOO-.js +65 -0
- package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/digests.d.ts +1 -0
- package/dist/digests.js +132 -42
- package/dist/doctor/checks.d.ts +0 -3
- package/dist/doctor/checks.js +1 -108
- package/dist/doctor/runner.js +1 -4
- package/dist/env-sanitizer.d.ts +2 -0
- package/dist/env-sanitizer.js +61 -0
- package/dist/exec-approval.d.ts +11 -1
- package/dist/exec-approval.js +17 -4
- package/dist/gateway.d.ts +3 -1
- package/dist/gateway.js +17 -7
- package/dist/heartbeat.js +1 -6
- package/dist/langfuse.js +3 -29
- package/dist/model-selection.js +3 -1
- package/dist/providers/adapter.d.ts +118 -0
- package/dist/providers/adapter.js +6 -0
- package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
- package/dist/providers/adapters/anthropic-adapter.js +204 -0
- package/dist/providers/adapters/codex-adapter.d.ts +26 -0
- package/dist/providers/adapters/codex-adapter.js +203 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +10 -272
- package/dist/providers/codex.d.ts +21 -0
- package/dist/providers/codex.js +149 -330
- package/dist/providers/content.d.ts +1 -1
- package/dist/providers/content.js +2 -2
- package/dist/providers/context-manager.d.ts +18 -6
- package/dist/providers/context-manager.js +199 -223
- package/dist/providers/index.d.ts +9 -1
- package/dist/providers/index.js +73 -64
- package/dist/providers/loop-utils.d.ts +20 -0
- package/dist/providers/loop-utils.js +30 -0
- package/dist/providers/tool-loop.d.ts +12 -0
- package/dist/providers/tool-loop.js +251 -0
- package/dist/providers/utils.d.ts +19 -3
- package/dist/providers/utils.js +100 -29
- package/dist/secure-store.d.ts +8 -0
- package/dist/secure-store.js +80 -0
- package/dist/service.js +3 -28
- package/dist/sessions.d.ts +3 -0
- package/dist/sessions.js +147 -18
- package/dist/setup-templates.js +13 -25
- package/dist/setup.d.ts +10 -6
- package/dist/setup.js +84 -292
- package/dist/skills.js +3 -11
- package/dist/tools/agent-delegation.d.ts +19 -0
- package/dist/tools/agent-delegation.js +49 -0
- package/dist/tools/bash-tool.js +89 -34
- package/dist/tools/definitions.d.ts +199 -302
- package/dist/tools/definitions.js +70 -123
- package/dist/tools/execute-context.d.ts +13 -4
- package/dist/tools/fetch-tool.js +109 -13
- package/dist/tools/file-tools.js +7 -1
- package/dist/tools.d.ts +7 -7
- package/dist/tools.js +133 -151
- package/dist/types.d.ts +37 -30
- package/dist/utils.js +4 -6
- package/dist/voice.d.ts +1 -1
- package/dist/voice.js +17 -4
- package/package.json +33 -23
- package/templates/TOOLS.md +0 -27
- package/dist/__tests__/audit.test.js +0 -122
- package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
- package/dist/__tests__/code-agents-sandbox.test.js +0 -163
- package/dist/__tests__/orchestrator.test.js +0 -425
- package/dist/__tests__/sandbox-bridge.test.js +0 -116
- package/dist/__tests__/sandbox-manager.test.js +0 -144
- package/dist/__tests__/sandbox-mount-security.test.js +0 -139
- package/dist/__tests__/sandbox-runtime.test.js +0 -176
- package/dist/__tests__/subagent.test.js +0 -240
- package/dist/__tests__/telegram.test.js +0 -42
- package/dist/code-agents/orchestrator.d.ts +0 -29
- package/dist/code-agents/orchestrator.js +0 -694
- package/dist/code-agents/worktree.d.ts +0 -40
- package/dist/code-agents/worktree.js +0 -215
- package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
- package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
- package/dist/dashboard.d.ts +0 -8
- package/dist/dashboard.js +0 -4071
- package/dist/discord.d.ts +0 -8
- package/dist/discord.js +0 -792
- package/dist/mcp-context-a8c.d.ts +0 -13
- package/dist/mcp-context-a8c.js +0 -34
- package/dist/orchestrator.d.ts +0 -15
- package/dist/orchestrator.js +0 -676
- package/dist/providers/openai.d.ts +0 -10
- package/dist/providers/openai.js +0 -355
- package/dist/sandbox/bridge.d.ts +0 -5
- package/dist/sandbox/bridge.js +0 -63
- package/dist/sandbox/index.d.ts +0 -5
- package/dist/sandbox/index.js +0 -4
- package/dist/sandbox/manager.d.ts +0 -7
- package/dist/sandbox/manager.js +0 -100
- package/dist/sandbox/mount-security.d.ts +0 -12
- package/dist/sandbox/mount-security.js +0 -122
- package/dist/sandbox/runtime.d.ts +0 -39
- package/dist/sandbox/runtime.js +0 -192
- package/dist/sandbox-utils.d.ts +0 -6
- package/dist/sandbox-utils.js +0 -36
- package/dist/subagent.d.ts +0 -19
- package/dist/subagent.js +0 -407
- package/dist/telegram.d.ts +0 -2
- package/dist/telegram.js +0 -11
- package/dist/tools/browser-tool.d.ts +0 -3
- package/dist/tools/browser-tool.js +0 -266
- package/sandbox/Dockerfile +0 -40
- /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
- /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
- /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
- /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
package/dist/cron.js
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
import { Cron } from 'croner';
|
|
3
3
|
import { exec } from 'child_process';
|
|
4
4
|
import { existsSync, mkdirSync, appendFileSync, readFileSync, watch } from 'fs';
|
|
5
|
-
import { join } from 'path';
|
|
5
|
+
import { join, resolve } from 'path';
|
|
6
6
|
import { getLogsDir, getConfigPath, loadConfig, resolveAllowedPaths } from './config.js';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import { runAgentTurn } from './agent.js';
|
|
9
9
|
import { startTrace, addEvent, endTrace } from './audit.js';
|
|
10
10
|
import { sendActiveChannelProactiveMessage, sendActiveChannelProactiveVoice, getActiveChannelId } from './channels.js';
|
|
11
|
+
import { sendToDiscordThread, sendToDiscordThreadWithVoice } from './channels/discord/index.js';
|
|
11
12
|
import { parseAndSaveDigest } from './digests.js';
|
|
12
13
|
import { synthesizeSpeech } from './voice.js';
|
|
13
14
|
import { toErrorMessage } from './utils.js';
|
|
15
|
+
import { sanitizeCronEnv } from './env-sanitizer.js';
|
|
14
16
|
function safeTimezone(tz) {
|
|
15
17
|
const fallback = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
16
18
|
if (!tz)
|
|
@@ -24,7 +26,6 @@ function safeTimezone(tz) {
|
|
|
24
26
|
return fallback;
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
|
-
import { ensureContainer, SANDBOX_DEFAULTS, sandboxBash } from './sandbox/index.js';
|
|
28
29
|
const scheduledJobs = new Map();
|
|
29
30
|
let configWatcher = null;
|
|
30
31
|
function getCronLogDir() {
|
|
@@ -58,12 +59,97 @@ function appendCronLogLine(jobId, line) {
|
|
|
58
59
|
}
|
|
59
60
|
// Track currently running jobs for status queries
|
|
60
61
|
const runningJobs = new Map();
|
|
62
|
+
const activeExecutions = new Set();
|
|
61
63
|
export function getCronRunStatus() {
|
|
62
64
|
return {
|
|
63
65
|
running: Array.from(runningJobs.keys()),
|
|
64
66
|
recent: Array.from(runningJobs.values()),
|
|
65
67
|
};
|
|
66
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Send a cron notification to the configured target.
|
|
71
|
+
* If discordThreadId is set, only routes to that thread.
|
|
72
|
+
* Active-channel fallback only applies when no discordThreadId is configured.
|
|
73
|
+
* Optionally includes voice attachment if voiceBuffer and voiceFormat are provided.
|
|
74
|
+
* Returns true if sent successfully.
|
|
75
|
+
*/
|
|
76
|
+
async function sendCronNotification(config, message, discordThreadId, voiceBuffer, voiceFormat) {
|
|
77
|
+
if (discordThreadId) {
|
|
78
|
+
try {
|
|
79
|
+
// Use voice-enabled sender for Discord threads if voice is provided
|
|
80
|
+
if (voiceBuffer && voiceFormat) {
|
|
81
|
+
const sent = await sendToDiscordThreadWithVoice(discordThreadId, message, voiceBuffer, voiceFormat);
|
|
82
|
+
if (sent)
|
|
83
|
+
return true;
|
|
84
|
+
console.error(`[cron] Failed to send to Discord thread ${discordThreadId} with voice; active-channel fallback disabled for thread-targeted jobs`);
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const sent = await sendToDiscordThread(discordThreadId, message);
|
|
88
|
+
if (sent)
|
|
89
|
+
return true;
|
|
90
|
+
console.error(`[cron] Failed to send to Discord thread ${discordThreadId}; active-channel fallback disabled for thread-targeted jobs`);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
const errorText = err instanceof Error ? err.message : String(err);
|
|
95
|
+
console.error(`[cron] Discord thread delivery failed for ${discordThreadId}; active-channel fallback disabled: ${errorText}`);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Fallback to active channel for non-thread-targeted jobs
|
|
100
|
+
if (voiceBuffer && voiceFormat) {
|
|
101
|
+
// Send voice to active channel if provided
|
|
102
|
+
await sendActiveChannelProactiveVoice(config, voiceBuffer, voiceFormat);
|
|
103
|
+
}
|
|
104
|
+
return sendActiveChannelProactiveMessage(config, message);
|
|
105
|
+
}
|
|
106
|
+
function resolveDiscordThreadTarget(jobDef) {
|
|
107
|
+
const threadId = jobDef.payload.discordThreadId?.trim();
|
|
108
|
+
if (!threadId)
|
|
109
|
+
return undefined;
|
|
110
|
+
// Discord snowflakes are numeric IDs. Validate eagerly so invalid config falls back cleanly.
|
|
111
|
+
if (!/^\d{17,20}$/.test(threadId)) {
|
|
112
|
+
console.warn(`[cron] Invalid discordThreadId for job "${jobDef.id}": ${JSON.stringify(jobDef.payload.discordThreadId)}`);
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
return threadId;
|
|
116
|
+
}
|
|
117
|
+
const CRON_AGENT_RETRY_DELAYS_MS = [5000, 15000];
|
|
118
|
+
export function isRetryableCronAgentError(err) {
|
|
119
|
+
const message = toErrorMessage(err).toLowerCase();
|
|
120
|
+
return [
|
|
121
|
+
'codex api 503',
|
|
122
|
+
'upstream connect error',
|
|
123
|
+
'connection refused',
|
|
124
|
+
'connection reset',
|
|
125
|
+
'remote connection failure',
|
|
126
|
+
'fetch failed',
|
|
127
|
+
'econnreset',
|
|
128
|
+
'econnrefused',
|
|
129
|
+
'etimedout',
|
|
130
|
+
'overloaded_error',
|
|
131
|
+
'temporarily unavailable',
|
|
132
|
+
].some(pattern => message.includes(pattern));
|
|
133
|
+
}
|
|
134
|
+
async function sleep(ms) {
|
|
135
|
+
await new Promise(resolve => setTimeout(resolve, ms));
|
|
136
|
+
}
|
|
137
|
+
async function runCronAgentTurnWithRetry(jobId, run) {
|
|
138
|
+
for (let attempt = 0; attempt <= CRON_AGENT_RETRY_DELAYS_MS.length; attempt++) {
|
|
139
|
+
try {
|
|
140
|
+
return await run();
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
if (attempt >= CRON_AGENT_RETRY_DELAYS_MS.length || !isRetryableCronAgentError(err)) {
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
const delayMs = CRON_AGENT_RETRY_DELAYS_MS[attempt];
|
|
147
|
+
appendCronLogLine(jobId, `Agent turn transient failure; retrying in ${(delayMs / 1000).toFixed(0)}s (${attempt + 1}/${CRON_AGENT_RETRY_DELAYS_MS.length}): ${toErrorMessage(err).slice(0, 180)}`);
|
|
148
|
+
await sleep(delayMs);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
throw new Error('Unreachable cron retry state');
|
|
152
|
+
}
|
|
67
153
|
export function initCron(config) {
|
|
68
154
|
// Clear existing jobs
|
|
69
155
|
for (const job of scheduledJobs.values()) {
|
|
@@ -109,6 +195,10 @@ export function initCron(config) {
|
|
|
109
195
|
}
|
|
110
196
|
}
|
|
111
197
|
function scheduleJob(jobDef, config) {
|
|
198
|
+
if (jobDef.enabled === false) {
|
|
199
|
+
console.log(`[cron] Skipping disabled job: ${jobDef.id}`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
112
202
|
if (jobDef.schedule.kind !== 'cron') {
|
|
113
203
|
console.warn(`[cron] Unsupported schedule kind: ${jobDef.schedule.kind}`);
|
|
114
204
|
return;
|
|
@@ -128,6 +218,13 @@ function scheduleJob(jobDef, config) {
|
|
|
128
218
|
console.log(`[cron] Scheduled: ${jobDef.name} (${jobDef.schedule.expr})`);
|
|
129
219
|
}
|
|
130
220
|
async function executeJobPayload(jobDef, config) {
|
|
221
|
+
if (activeExecutions.has(jobDef.id)) {
|
|
222
|
+
const message = `Skipping overlapping run for job "${jobDef.id}" (previous run still active)`;
|
|
223
|
+
console.warn(`[cron] ${message}`);
|
|
224
|
+
appendCronLogLine(jobDef.id, `=== SKIPPED: overlap guard (${jobDef.id}) ===`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
activeExecutions.add(jobDef.id);
|
|
131
228
|
const logEntry = {
|
|
132
229
|
jobId: jobDef.id,
|
|
133
230
|
jobName: jobDef.name,
|
|
@@ -135,13 +232,16 @@ async function executeJobPayload(jobDef, config) {
|
|
|
135
232
|
status: 'running',
|
|
136
233
|
};
|
|
137
234
|
runningJobs.set(jobDef.id, logEntry);
|
|
235
|
+
const discordThreadId = resolveDiscordThreadTarget(jobDef);
|
|
236
|
+
// Track synthesized voice for final notification
|
|
237
|
+
let synthesizedVoice = null;
|
|
138
238
|
// Log start immediately
|
|
139
239
|
appendCronLogLine(jobDef.id, `=== STARTED: ${jobDef.name} (${jobDef.id}) ===`);
|
|
140
240
|
appendCronLogLine(jobDef.id, `Model: ${jobDef.model || 'default'}`);
|
|
141
241
|
appendCronLogLine(jobDef.id, `Payload: ${jobDef.payload.kind}`);
|
|
142
242
|
// Notify channel at start
|
|
143
243
|
try {
|
|
144
|
-
await
|
|
244
|
+
await sendCronNotification(config, `🔄 Cron starting: ${jobDef.name}`, discordThreadId);
|
|
145
245
|
}
|
|
146
246
|
catch {
|
|
147
247
|
// Non-critical
|
|
@@ -157,14 +257,14 @@ async function executeJobPayload(jobDef, config) {
|
|
|
157
257
|
bashTimeout: 15000,
|
|
158
258
|
};
|
|
159
259
|
const tools = jobDef.payload.tools
|
|
160
|
-
? { ...jobDef.payload.tools, allowedPaths:
|
|
260
|
+
? { ...jobDef.payload.tools, allowedPaths: resolveAllowedPaths(config, jobDef.payload.tools.allowedPaths) }
|
|
161
261
|
: defaultTools;
|
|
162
|
-
const response = await runAgentTurn(config.agents.default, message, config, jobDef.model, tools, undefined, {
|
|
262
|
+
const response = await runCronAgentTurnWithRetry(jobDef.id, () => runAgentTurn(config.agents.default, message, config, jobDef.model, tools, undefined, {
|
|
163
263
|
channel: getActiveChannelId() || 'telegram',
|
|
164
264
|
trigger: 'cron',
|
|
165
265
|
sessionId: jobDef.id,
|
|
166
266
|
metadata: { jobName: jobDef.name, isCronJob: true },
|
|
167
|
-
});
|
|
267
|
+
}));
|
|
168
268
|
appendCronLogLine(jobDef.id, `Agent turn completed (${response.length} chars)`);
|
|
169
269
|
// Parse dual output (voice + text) if delimiters present
|
|
170
270
|
const { voice: voicePortion, text: textPortion } = parseDualOutput(response);
|
|
@@ -173,29 +273,39 @@ async function executeJobPayload(jobDef, config) {
|
|
|
173
273
|
}
|
|
174
274
|
// Use text portion for log output and notifications
|
|
175
275
|
logEntry.output = textPortion.slice(0, 5000);
|
|
176
|
-
// Post-run guard for pr-review job
|
|
177
|
-
if (jobDef.id === 'pr-review') {
|
|
178
|
-
const guardAlert = validatePrReviewOutput(textPortion);
|
|
179
|
-
if (guardAlert) {
|
|
180
|
-
appendCronLogLine(jobDef.id, guardAlert);
|
|
181
|
-
try {
|
|
182
|
-
await sendActiveChannelProactiveMessage(config, guardAlert);
|
|
183
|
-
}
|
|
184
|
-
catch {
|
|
185
|
-
// Non-critical
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
276
|
// Parse and save digest from the text portion
|
|
190
277
|
try {
|
|
191
|
-
parseAndSaveDigest(jobDef.id, jobDef.name, textPortion);
|
|
278
|
+
const digest = parseAndSaveDigest(jobDef.id, jobDef.name, textPortion);
|
|
192
279
|
appendCronLogLine(jobDef.id, 'Digest saved');
|
|
280
|
+
if (digest.articles.length > 0) {
|
|
281
|
+
const digestMessage = digest.summary ?? textPortion;
|
|
282
|
+
if (!digestMessage || !digestMessage.trim()) {
|
|
283
|
+
appendCronLogLine(jobDef.id, 'Digest message is empty, skipping send');
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
try {
|
|
287
|
+
const sent = await sendCronNotification(config, digestMessage, discordThreadId);
|
|
288
|
+
if (sent) {
|
|
289
|
+
appendCronLogLine(jobDef.id, `Digest sent to chat (${digestMessage.length} chars)`);
|
|
290
|
+
}
|
|
291
|
+
else if (discordThreadId) {
|
|
292
|
+
appendCronLogLine(jobDef.id, `Digest thread delivery failed (threadId=${discordThreadId}); no active-channel fallback`);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
appendCronLogLine(jobDef.id, 'Failed to send digest to chat');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
appendCronLogLine(jobDef.id, 'Failed to send digest to chat');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
193
303
|
}
|
|
194
304
|
catch (digestErr) {
|
|
195
305
|
const errMsg = digestErr instanceof Error ? digestErr.message : String(digestErr);
|
|
196
306
|
appendCronLogLine(jobDef.id, `Failed to save digest: ${errMsg}`);
|
|
197
307
|
}
|
|
198
|
-
// Synthesize
|
|
308
|
+
// Synthesize voice if configured (stored for final notification)
|
|
199
309
|
if (jobDef.payload.sendAsVoice && config.voice) {
|
|
200
310
|
try {
|
|
201
311
|
// Use voice portion if available, fall back to text
|
|
@@ -203,13 +313,9 @@ async function executeJobPayload(jobDef, config) {
|
|
|
203
313
|
appendCronLogLine(jobDef.id, `Synthesizing voice (${voicePortion ? 'voice portion' : 'full text fallback'})...`);
|
|
204
314
|
const speech = await synthesizeSpeech(voiceContent, config.voice);
|
|
205
315
|
appendCronLogLine(jobDef.id, `Voice synthesized (${speech.format}, ${speech.provider}, ${speech.buffer.length} bytes)`);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
appendCronLogLine(jobDef.id, 'No active channel for voice message');
|
|
212
|
-
}
|
|
316
|
+
// Store for final notification instead of sending immediately
|
|
317
|
+
synthesizedVoice = { buffer: speech.buffer, format: speech.format };
|
|
318
|
+
appendCronLogLine(jobDef.id, 'Voice stored for final notification');
|
|
213
319
|
}
|
|
214
320
|
catch (voiceErr) {
|
|
215
321
|
const errMsg = voiceErr instanceof Error ? voiceErr.message : String(voiceErr);
|
|
@@ -236,6 +342,15 @@ async function executeJobPayload(jobDef, config) {
|
|
|
236
342
|
durationMs: 0,
|
|
237
343
|
});
|
|
238
344
|
await endTrace(scriptTraceId, 'ok');
|
|
345
|
+
// Parse and save digest from script output (same as agentTurn path)
|
|
346
|
+
try {
|
|
347
|
+
const digest = parseAndSaveDigest(jobDef.id, jobDef.name, output);
|
|
348
|
+
appendCronLogLine(jobDef.id, `Digest saved: ${digest.articles.length} articles`);
|
|
349
|
+
}
|
|
350
|
+
catch (digestErr) {
|
|
351
|
+
const errMsg = digestErr instanceof Error ? digestErr.message : String(digestErr);
|
|
352
|
+
appendCronLogLine(jobDef.id, `Failed to save digest: ${errMsg}`);
|
|
353
|
+
}
|
|
239
354
|
}
|
|
240
355
|
catch (scriptErr) {
|
|
241
356
|
const scriptErrMsg = scriptErr instanceof Error ? scriptErr.message : String(scriptErr);
|
|
@@ -263,9 +378,10 @@ async function executeJobPayload(jobDef, config) {
|
|
|
263
378
|
logEntry.durationMs = new Date(logEntry.finishedAt).getTime() - new Date(logEntry.startedAt).getTime();
|
|
264
379
|
writeCronLog(logEntry);
|
|
265
380
|
runningJobs.delete(jobDef.id);
|
|
381
|
+
activeExecutions.delete(jobDef.id);
|
|
266
382
|
const elapsed = (logEntry.durationMs / 1000).toFixed(1);
|
|
267
383
|
console.log(`[cron] Job ${jobDef.id} finished: ${logEntry.status} (${elapsed}s)`);
|
|
268
|
-
// Notify active channel
|
|
384
|
+
// Notify active channel with optional voice attachment
|
|
269
385
|
try {
|
|
270
386
|
const icon = logEntry.status === 'success' ? '✅' : logEntry.status === 'timeout' ? '⏰' : '❌';
|
|
271
387
|
let notification = `${icon} Cron: ${jobDef.name} — ${logEntry.status} (${elapsed}s)`;
|
|
@@ -279,7 +395,20 @@ async function executeJobPayload(jobDef, config) {
|
|
|
279
395
|
: logEntry.output;
|
|
280
396
|
notification += `\n\n${output}`;
|
|
281
397
|
}
|
|
282
|
-
|
|
398
|
+
// Include synthesized voice if available
|
|
399
|
+
if (synthesizedVoice) {
|
|
400
|
+
appendCronLogLine(jobDef.id, `Sending notification with voice (${synthesizedVoice.format})`);
|
|
401
|
+
const sent = await sendCronNotification(config, notification, discordThreadId, synthesizedVoice.buffer, synthesizedVoice.format);
|
|
402
|
+
if (!sent && discordThreadId) {
|
|
403
|
+
appendCronLogLine(jobDef.id, `Final thread notification delivery failed (threadId=${discordThreadId}); no active-channel fallback`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
const sent = await sendCronNotification(config, notification, discordThreadId);
|
|
408
|
+
if (!sent && discordThreadId) {
|
|
409
|
+
appendCronLogLine(jobDef.id, `Final thread notification delivery failed (threadId=${discordThreadId}); no active-channel fallback`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
283
412
|
}
|
|
284
413
|
catch (notifyErr) {
|
|
285
414
|
console.error(`[cron] Failed to send notification: ${notifyErr}`);
|
|
@@ -296,19 +425,6 @@ async function executeScript(jobDef, config) {
|
|
|
296
425
|
throw new Error(`Working directory does not exist: ${cwd}`);
|
|
297
426
|
}
|
|
298
427
|
const timeoutMs = jobDef.payload.timeoutMs || 600000; // 10 min default
|
|
299
|
-
// Sandbox routing for script payloads
|
|
300
|
-
const sandboxCfg = config.sandbox;
|
|
301
|
-
if (sandboxCfg?.enabled) {
|
|
302
|
-
const merged = { ...SANDBOX_DEFAULTS, ...sandboxCfg };
|
|
303
|
-
const containerName = await ensureContainer(`cron-${jobDef.id}`, merged, jobDef.payload.tools?.allowedPaths || []);
|
|
304
|
-
console.log(`[cron:script] Running in sandbox container: ${containerName}`);
|
|
305
|
-
console.log(`[cron:script] Running: ${script.slice(0, 100)}${script.length > 100 ? '...' : ''}`);
|
|
306
|
-
if (cwd)
|
|
307
|
-
console.log(`[cron:script] cwd: ${cwd}`);
|
|
308
|
-
const output = await sandboxBash(containerName, script, cwd, timeoutMs);
|
|
309
|
-
console.log(`[cron:script] Sandbox completed (${output.length} chars)`);
|
|
310
|
-
return output;
|
|
311
|
-
}
|
|
312
428
|
return new Promise((resolve, reject) => {
|
|
313
429
|
const startTime = Date.now();
|
|
314
430
|
console.log(`[cron:script] Running: ${script.slice(0, 100)}${script.length > 100 ? '...' : ''}`);
|
|
@@ -317,7 +433,7 @@ async function executeScript(jobDef, config) {
|
|
|
317
433
|
const child = exec(script, {
|
|
318
434
|
cwd: cwd || undefined,
|
|
319
435
|
timeout: timeoutMs,
|
|
320
|
-
env:
|
|
436
|
+
env: sanitizeCronEnv(),
|
|
321
437
|
maxBuffer: 10 * 1024 * 1024, // 10MB output buffer
|
|
322
438
|
}, (error, stdout, stderr) => {
|
|
323
439
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
@@ -352,20 +468,21 @@ function resolveMessageSource(message) {
|
|
|
352
468
|
const trimmed = message.trim();
|
|
353
469
|
if (!trimmed.endsWith('.md'))
|
|
354
470
|
return message;
|
|
355
|
-
|
|
471
|
+
const promptsRoot = resolve(homedir(), '.skimpyclaw', 'prompts');
|
|
356
472
|
const resolved = trimmed.startsWith('~/')
|
|
357
|
-
?
|
|
358
|
-
: trimmed
|
|
473
|
+
? resolve(homedir(), trimmed.slice(2))
|
|
474
|
+
: trimmed.startsWith('/')
|
|
475
|
+
? resolve(trimmed)
|
|
476
|
+
: resolve(promptsRoot, trimmed);
|
|
477
|
+
const insidePromptsRoot = resolved === promptsRoot || resolved.startsWith(`${promptsRoot}/`);
|
|
478
|
+
if (!insidePromptsRoot) {
|
|
479
|
+
console.warn(`[cron] Rejected prompt path outside ~/.skimpyclaw/prompts: ${trimmed}`);
|
|
480
|
+
return message;
|
|
481
|
+
}
|
|
359
482
|
if (existsSync(resolved)) {
|
|
360
483
|
console.log(`[cron] Loading prompt from file: ${resolved}`);
|
|
361
484
|
return readFileSync(resolved, 'utf-8');
|
|
362
485
|
}
|
|
363
|
-
// Fallback: check ~/.skimpyclaw/prompts/ directory
|
|
364
|
-
const promptsDir = join(homedir(), '.skimpyclaw', 'prompts', trimmed);
|
|
365
|
-
if (existsSync(promptsDir)) {
|
|
366
|
-
console.log(`[cron] Loading prompt from prompts dir: ${promptsDir}`);
|
|
367
|
-
return readFileSync(promptsDir, 'utf-8');
|
|
368
|
-
}
|
|
369
486
|
console.warn(`[cron] Could not resolve prompt file: ${trimmed}`);
|
|
370
487
|
// Not a valid file path — treat as regular message text
|
|
371
488
|
return message;
|
|
@@ -393,35 +510,6 @@ export function parseDualOutput(response) {
|
|
|
393
510
|
text: text || response,
|
|
394
511
|
};
|
|
395
512
|
}
|
|
396
|
-
/**
|
|
397
|
-
* Post-run guard for the pr-review cron job.
|
|
398
|
-
* Validates that the agent actually used code_with_agent when PRs were found.
|
|
399
|
-
* Non-throwing — logs a warning and returns an alert message (or null if OK).
|
|
400
|
-
*/
|
|
401
|
-
export function validatePrReviewOutput(output) {
|
|
402
|
-
// Check for the machine-readable result line
|
|
403
|
-
const resultMatch = output.match(/\[PR_REVIEW_RESULT:\s*(.+?)\]/);
|
|
404
|
-
if (!resultMatch) {
|
|
405
|
-
return '⚠️ PR Pre-Review: Missing [PR_REVIEW_RESULT] line in output. The agent may not have followed the prompt correctly.';
|
|
406
|
-
}
|
|
407
|
-
const resultLine = resultMatch[1].trim();
|
|
408
|
-
// NO_CANDIDATES is fine — nothing to review
|
|
409
|
-
if (resultLine === 'NO_CANDIDATES') {
|
|
410
|
-
return null;
|
|
411
|
-
}
|
|
412
|
-
// Parse CANDIDATES=N CODE_AGENT_CALLS=M BLOCKED=B
|
|
413
|
-
const candidatesMatch = resultLine.match(/CANDIDATES=(\d+)/);
|
|
414
|
-
const callsMatch = resultLine.match(/CODE_AGENT_CALLS=(\d+)/);
|
|
415
|
-
const blockedMatch = resultLine.match(/BLOCKED=(\d+)/);
|
|
416
|
-
const candidates = candidatesMatch ? parseInt(candidatesMatch[1], 10) : 0;
|
|
417
|
-
const calls = callsMatch ? parseInt(callsMatch[1], 10) : 0;
|
|
418
|
-
const blocked = blockedMatch ? parseInt(blockedMatch[1], 10) : 0;
|
|
419
|
-
// If there were candidates but zero code_with_agent calls (and not all blocked), alert
|
|
420
|
-
if (candidates > 0 && calls === 0 && blocked < candidates) {
|
|
421
|
-
return `⚠️ PR Pre-Review: Found ${candidates} PR candidate(s) but code_with_agent was never called (blocked: ${blocked}). The agent likely wrote inline commentary instead of delegating.`;
|
|
422
|
-
}
|
|
423
|
-
return null;
|
|
424
|
-
}
|
|
425
513
|
function expandVariables(message) {
|
|
426
514
|
const now = new Date();
|
|
427
515
|
const date = now.toLocaleDateString('en-US', {
|
|
@@ -429,8 +517,13 @@ function expandVariables(message) {
|
|
|
429
517
|
day: 'numeric',
|
|
430
518
|
year: 'numeric',
|
|
431
519
|
});
|
|
520
|
+
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
|
521
|
+
const dd = String(now.getDate()).padStart(2, '0');
|
|
522
|
+
const yyyy = now.getFullYear();
|
|
523
|
+
const dateMmDdYyyy = `${mm}-${dd}-${yyyy}`;
|
|
432
524
|
return message
|
|
433
525
|
.replace(/\{\{date\}\}/g, date)
|
|
526
|
+
.replace(/\{\{date_mm-dd-yyyy\}\}/g, dateMmDdYyyy)
|
|
434
527
|
.replace(/\{\{time\}\}/g, now.toLocaleTimeString())
|
|
435
528
|
.replace(/\{\{iso\}\}/g, now.toISOString());
|
|
436
529
|
}
|