skimpyclaw 0.3.10 → 0.3.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.
- package/dist/__tests__/channels.test.js +1 -1
- package/dist/__tests__/context-manager.test.js +219 -76
- package/dist/__tests__/providers-utils.test.js +2 -0
- package/dist/__tests__/sandbox-manager.test.js +25 -0
- package/dist/__tests__/sandbox-mount-security.test.js +8 -0
- package/dist/__tests__/setup.test.js +1 -1
- package/dist/__tests__/tools.test.js +11 -9
- package/dist/agent.js +1 -1
- package/dist/api.js +5 -0
- package/dist/channels/discord/handlers.d.ts +7 -0
- package/dist/channels/discord/handlers.js +479 -0
- package/dist/channels/discord/index.d.ts +8 -0
- package/dist/channels/discord/index.js +149 -0
- package/dist/channels/discord/types.d.ts +6 -0
- package/dist/channels/discord/types.js +17 -0
- package/dist/channels/discord/utils.d.ts +14 -0
- package/dist/channels/discord/utils.js +161 -0
- package/dist/channels/telegram/utils.d.ts +1 -1
- package/dist/channels/telegram/utils.js +7 -9
- package/dist/channels.js +1 -1
- package/dist/cli.js +8 -43
- package/dist/code-agents/parser.js +5 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +13 -0
- package/dist/cron.js +6 -3
- package/dist/heartbeat.js +11 -15
- package/dist/providers/anthropic.js +7 -1
- package/dist/providers/codex.js +8 -2
- package/dist/providers/context-manager.d.ts +37 -6
- package/dist/providers/context-manager.js +303 -47
- package/dist/providers/openai.js +8 -2
- package/dist/providers/utils.js +1 -1
- package/dist/sandbox/manager.js +11 -0
- package/dist/sandbox/mount-security.js +5 -1
- package/dist/sandbox/runtime.d.ts +1 -0
- package/dist/sandbox/runtime.js +5 -0
- package/dist/sandbox-utils.d.ts +6 -0
- package/dist/sandbox-utils.js +36 -0
- package/dist/security.js +4 -3
- package/dist/setup-templates.d.ts +14 -0
- package/dist/setup-templates.js +214 -0
- package/dist/setup.d.ts +1 -9
- package/dist/setup.js +3 -244
- package/dist/tools/bash-tool.js +11 -1
- package/dist/tools/definitions.d.ts +57 -0
- package/dist/tools/definitions.js +19 -1
- package/dist/tools/fetch-tool.d.ts +8 -0
- package/dist/tools/fetch-tool.js +80 -0
- package/dist/tools.d.ts +4 -2
- package/dist/tools.js +110 -62
- package/dist/types.d.ts +5 -0
- package/package.json +3 -4
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { resolveAllowedPaths } from '../../config.js';
|
|
2
|
+
import * as sessions from '../../sessions.js';
|
|
3
|
+
import { BOT_COMMANDS, MAX_HISTORY_PAIRS } from './types.js';
|
|
4
|
+
// ── State ───────────────────────────────────────────────────────────
|
|
5
|
+
const chatHistory = new Map();
|
|
6
|
+
const loadedFromDisk = new Set();
|
|
7
|
+
// ── History ─────────────────────────────────────────────────────────
|
|
8
|
+
export async function getHistory(key) {
|
|
9
|
+
if (!loadedFromDisk.has(key)) {
|
|
10
|
+
loadedFromDisk.add(key);
|
|
11
|
+
const diskHistory = await sessions.loadHistory('discord', key).catch(() => []);
|
|
12
|
+
if (diskHistory.length > 0 && !chatHistory.has(key)) {
|
|
13
|
+
chatHistory.set(key, diskHistory);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return chatHistory.get(key) || [];
|
|
17
|
+
}
|
|
18
|
+
export async function addToHistory(key, userMsg, assistantMsg) {
|
|
19
|
+
const history = await getHistory(key);
|
|
20
|
+
history.push({ role: 'user', content: userMsg });
|
|
21
|
+
history.push({ role: 'assistant', content: assistantMsg });
|
|
22
|
+
while (history.length > MAX_HISTORY_PAIRS * 2) {
|
|
23
|
+
history.shift();
|
|
24
|
+
history.shift();
|
|
25
|
+
}
|
|
26
|
+
chatHistory.set(key, history);
|
|
27
|
+
sessions.saveExchange('discord', key, userMsg, assistantMsg).catch(() => { });
|
|
28
|
+
}
|
|
29
|
+
export async function clearHistory(key) {
|
|
30
|
+
chatHistory.delete(key);
|
|
31
|
+
loadedFromDisk.delete(key);
|
|
32
|
+
await sessions.clearHistory('discord', key).catch(() => { });
|
|
33
|
+
}
|
|
34
|
+
/** Replace history with a compact summary (used by /compact). */
|
|
35
|
+
export function replaceHistory(key, summary) {
|
|
36
|
+
chatHistory.set(key, [
|
|
37
|
+
{ role: 'user', content: 'Summary of our previous conversation:' },
|
|
38
|
+
{ role: 'assistant', content: summary },
|
|
39
|
+
]);
|
|
40
|
+
loadedFromDisk.add(key);
|
|
41
|
+
}
|
|
42
|
+
// ── Tool config ─────────────────────────────────────────────────────
|
|
43
|
+
export function getDiscordToolConfig(cfg) {
|
|
44
|
+
const discord = cfg.channels.discord;
|
|
45
|
+
if (discord?.tools) {
|
|
46
|
+
return {
|
|
47
|
+
...discord.tools,
|
|
48
|
+
allowedPaths: discord.tools.allowedPaths?.length
|
|
49
|
+
? discord.tools.allowedPaths
|
|
50
|
+
: resolveAllowedPaths(cfg),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
enabled: true,
|
|
55
|
+
allowedPaths: resolveAllowedPaths(cfg),
|
|
56
|
+
maxIterations: 100,
|
|
57
|
+
bashTimeout: 15000,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
61
|
+
export function conversationKey(message) {
|
|
62
|
+
if (message.channel.isDMBased()) {
|
|
63
|
+
return `dm:${message.author.id}`;
|
|
64
|
+
}
|
|
65
|
+
return `channel:${message.channelId}`;
|
|
66
|
+
}
|
|
67
|
+
export function getDiscordRunContext(message) {
|
|
68
|
+
return {
|
|
69
|
+
userId: message.author.id,
|
|
70
|
+
sessionId: message.channel.id,
|
|
71
|
+
channel: 'discord',
|
|
72
|
+
trigger: 'discord',
|
|
73
|
+
metadata: {
|
|
74
|
+
username: message.author.username,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function buildHelpText(cfg) {
|
|
79
|
+
const agentConfig = cfg.agents.list[cfg.agents.default];
|
|
80
|
+
const emoji = agentConfig?.identity?.emoji || '🦞';
|
|
81
|
+
const name = agentConfig?.identity?.name || 'SkimpyClaw';
|
|
82
|
+
const commandList = BOT_COMMANDS.map(c => `/${c.command} - ${c.description}`).join('\n');
|
|
83
|
+
return `${emoji} ${name} online.\n\nSend a message to chat, or use a command:\n\n${commandList}`;
|
|
84
|
+
}
|
|
85
|
+
export function splitToChunks(text, maxLength) {
|
|
86
|
+
if (text.length <= maxLength)
|
|
87
|
+
return [text];
|
|
88
|
+
const chunks = [];
|
|
89
|
+
let current = '';
|
|
90
|
+
for (const paragraph of text.split('\n\n')) {
|
|
91
|
+
if (current.length + paragraph.length + 2 > maxLength) {
|
|
92
|
+
if (current)
|
|
93
|
+
chunks.push(current.trim());
|
|
94
|
+
if (paragraph.length > maxLength) {
|
|
95
|
+
const lines = paragraph.split('\n');
|
|
96
|
+
let lineBuf = '';
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
if (lineBuf.length + line.length + 1 > maxLength) {
|
|
99
|
+
if (lineBuf)
|
|
100
|
+
chunks.push(lineBuf.trim());
|
|
101
|
+
if (line.length > maxLength) {
|
|
102
|
+
for (let i = 0; i < line.length; i += maxLength) {
|
|
103
|
+
chunks.push(line.slice(i, i + maxLength));
|
|
104
|
+
}
|
|
105
|
+
lineBuf = '';
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
lineBuf = line;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
lineBuf += (lineBuf ? '\n' : '') + line;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
current = lineBuf;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
current = paragraph;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
current += (current ? '\n\n' : '') + paragraph;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (current)
|
|
126
|
+
chunks.push(current.trim());
|
|
127
|
+
return chunks.filter(c => c.length > 0);
|
|
128
|
+
}
|
|
129
|
+
export async function sendLongText(message, text) {
|
|
130
|
+
const chunks = splitToChunks(text, 1900);
|
|
131
|
+
for (const chunk of chunks) {
|
|
132
|
+
await message.reply(chunk);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export function startTypingIndicator(message) {
|
|
136
|
+
const maxDurationMs = 90_000;
|
|
137
|
+
let stopped = false;
|
|
138
|
+
const stop = () => {
|
|
139
|
+
if (stopped)
|
|
140
|
+
return;
|
|
141
|
+
stopped = true;
|
|
142
|
+
clearInterval(interval);
|
|
143
|
+
clearTimeout(watchdog);
|
|
144
|
+
};
|
|
145
|
+
const channel = message.channel;
|
|
146
|
+
if (typeof channel.sendTyping === 'function') {
|
|
147
|
+
void channel.sendTyping().catch(() => { });
|
|
148
|
+
}
|
|
149
|
+
const interval = setInterval(() => {
|
|
150
|
+
if (stopped)
|
|
151
|
+
return;
|
|
152
|
+
if (typeof channel.sendTyping === 'function') {
|
|
153
|
+
void channel.sendTyping().catch(() => { });
|
|
154
|
+
}
|
|
155
|
+
}, 4000);
|
|
156
|
+
const watchdog = setTimeout(() => {
|
|
157
|
+
console.warn('[discord] Typing indicator watchdog reached; auto-stopping.');
|
|
158
|
+
stop();
|
|
159
|
+
}, maxDurationMs);
|
|
160
|
+
return stop;
|
|
161
|
+
}
|
|
@@ -12,7 +12,7 @@ export declare function getHistory(chatId: number): Promise<ChatMessage[]>;
|
|
|
12
12
|
export declare function addToHistory(chatId: number, userMsg: string, assistantMsg: string): Promise<void>;
|
|
13
13
|
export declare function clearHistory(chatId: number): Promise<void>;
|
|
14
14
|
export declare function getRunContext(ctx: Context): AgentRunContext;
|
|
15
|
-
export declare function getDefaultTelegramToolConfig(cfg: Config): ToolConfig
|
|
15
|
+
export declare function getDefaultTelegramToolConfig(cfg: Config): ToolConfig;
|
|
16
16
|
/** Get Telegram default chat ID from config */
|
|
17
17
|
export declare function getTelegramDefaultChatId(cfg: Config): number | null;
|
|
18
18
|
/** Send a long message by splitting it into chunks */
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { existsSync, readdirSync, statSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
|
+
import { resolveAllowedPaths } from '../../config.js';
|
|
5
6
|
import { state, MAX_HISTORY_PAIRS, BOT_COMMANDS } from './types.js';
|
|
6
7
|
import * as sessions from '../../sessions.js';
|
|
7
8
|
/** Keep sending "typing..." every 4s until the returned stop function is called. */
|
|
@@ -102,20 +103,17 @@ export function getRunContext(ctx) {
|
|
|
102
103
|
// Default tool config for Telegram — gives the agent file/bash access
|
|
103
104
|
export function getDefaultTelegramToolConfig(cfg) {
|
|
104
105
|
if (cfg.channels.telegram.tools) {
|
|
105
|
-
return cfg.channels.telegram.tools;
|
|
106
|
-
}
|
|
107
|
-
if (cfg.channels.telegram.defaultAllowedPaths?.length) {
|
|
108
106
|
return {
|
|
109
|
-
|
|
110
|
-
allowedPaths: cfg.channels.telegram.
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
...cfg.channels.telegram.tools,
|
|
108
|
+
allowedPaths: cfg.channels.telegram.tools.allowedPaths?.length
|
|
109
|
+
? cfg.channels.telegram.tools.allowedPaths
|
|
110
|
+
: resolveAllowedPaths(cfg),
|
|
113
111
|
};
|
|
114
112
|
}
|
|
115
113
|
return {
|
|
116
114
|
enabled: true,
|
|
117
|
-
allowedPaths:
|
|
118
|
-
maxIterations:
|
|
115
|
+
allowedPaths: resolveAllowedPaths(cfg),
|
|
116
|
+
maxIterations: 100,
|
|
119
117
|
bashTimeout: 15000,
|
|
120
118
|
};
|
|
121
119
|
}
|
package/dist/channels.js
CHANGED
|
@@ -38,7 +38,7 @@ async function loadAdapter(channel) {
|
|
|
38
38
|
resolveDefaultTarget: telegram.getTelegramDefaultChatId,
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
-
const discord = await import('./discord.js');
|
|
41
|
+
const discord = await import('./channels/discord/index.js');
|
|
42
42
|
return {
|
|
43
43
|
init: discord.initDiscord,
|
|
44
44
|
start: discord.startDiscord,
|
package/dist/cli.js
CHANGED
|
@@ -4,12 +4,13 @@ import { join } from 'path';
|
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { spawn, spawnSync } from 'child_process';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
|
-
import { loadConfig, loadRawConfig, getConfigPath, saveConfig } from './config.js';
|
|
7
|
+
import { loadConfig, loadRawConfig, getConfigPath, saveConfig, resolveAllowedPaths } from './config.js';
|
|
8
8
|
import { startRuntime } from './service.js';
|
|
9
9
|
import { runSetup, renderGatewayPlist } from './setup.js';
|
|
10
10
|
import { runDoctor as runDoctorCommand } from './doctor/index.js';
|
|
11
11
|
import { executeTool, getToolDefinitions, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION } from './tools.js';
|
|
12
12
|
import { formatModelSelectionError, getModelSelectionUsage, resolveModelSelection } from './model-selection.js';
|
|
13
|
+
import { detectSandboxRuntime, isSandboxRuntimeRunning, sandboxNetworkExists, defaultSandboxNetwork, sandboxImageExists, } from './sandbox-utils.js';
|
|
13
14
|
const APP_NAME = 'skimpyclaw';
|
|
14
15
|
const DEFAULT_PORT = 18790;
|
|
15
16
|
const LAUNCHD_LABEL = 'com.skimpyclaw.gateway';
|
|
@@ -102,19 +103,17 @@ function hasFlag(args, flag) {
|
|
|
102
103
|
return args.includes(flag);
|
|
103
104
|
}
|
|
104
105
|
function getCliToolConfig(config) {
|
|
105
|
-
if (config.channels.telegram.tools)
|
|
106
|
-
return config.channels.telegram.tools;
|
|
107
|
-
if (config.channels.telegram.defaultAllowedPaths?.length) {
|
|
106
|
+
if (config.channels.telegram.tools) {
|
|
108
107
|
return {
|
|
109
|
-
|
|
110
|
-
allowedPaths: config.channels.telegram.
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
...config.channels.telegram.tools,
|
|
109
|
+
allowedPaths: config.channels.telegram.tools.allowedPaths?.length
|
|
110
|
+
? config.channels.telegram.tools.allowedPaths
|
|
111
|
+
: resolveAllowedPaths(config),
|
|
113
112
|
};
|
|
114
113
|
}
|
|
115
114
|
return {
|
|
116
115
|
enabled: true,
|
|
117
|
-
allowedPaths:
|
|
116
|
+
allowedPaths: resolveAllowedPaths(config),
|
|
118
117
|
maxIterations: 100,
|
|
119
118
|
bashTimeout: 15000,
|
|
120
119
|
};
|
|
@@ -771,40 +770,6 @@ const SANDBOX_CLI_BY_PROFILE = {
|
|
|
771
770
|
dev: ['bash', 'curl', 'git', 'gh', 'jq', 'python3', 'rg', 'pnpm', 'gcc', 'g++', 'make'],
|
|
772
771
|
full: ['bash', 'curl', 'git', 'gh', 'jq', 'python3', 'rg', 'pnpm', 'gcc', 'g++', 'make', 'pip3', 'sqlite3'],
|
|
773
772
|
};
|
|
774
|
-
function defaultSandboxNetwork(runtime) {
|
|
775
|
-
return runtime === 'container' ? 'default' : 'bridge';
|
|
776
|
-
}
|
|
777
|
-
function detectSandboxRuntime(preferred) {
|
|
778
|
-
if (preferred === 'container' || preferred === 'docker') {
|
|
779
|
-
return spawnSync(preferred, ['--version'], { encoding: 'utf-8' }).status === 0 ? preferred : null;
|
|
780
|
-
}
|
|
781
|
-
if (spawnSync('container', ['--version'], { encoding: 'utf-8' }).status === 0) {
|
|
782
|
-
return 'container';
|
|
783
|
-
}
|
|
784
|
-
if (spawnSync('docker', ['--version'], { encoding: 'utf-8' }).status === 0) {
|
|
785
|
-
return 'docker';
|
|
786
|
-
}
|
|
787
|
-
return null;
|
|
788
|
-
}
|
|
789
|
-
function isSandboxRuntimeRunning(runtime) {
|
|
790
|
-
if (runtime === 'container') {
|
|
791
|
-
return spawnSync('container', ['system', 'status'], { encoding: 'utf-8' }).status === 0;
|
|
792
|
-
}
|
|
793
|
-
return spawnSync('docker', ['info'], { encoding: 'utf-8' }).status === 0;
|
|
794
|
-
}
|
|
795
|
-
function sandboxNetworkExists(runtime, network) {
|
|
796
|
-
if (runtime === 'container') {
|
|
797
|
-
const result = spawnSync('container', ['network', 'ls'], { encoding: 'utf-8' });
|
|
798
|
-
if (result.status !== 0)
|
|
799
|
-
return false;
|
|
800
|
-
return result.stdout.split('\n').some((line) => line.trim().split(/\s+/)[0] === network);
|
|
801
|
-
}
|
|
802
|
-
const result = spawnSync('docker', ['network', 'inspect', network], { encoding: 'utf-8' });
|
|
803
|
-
return result.status === 0;
|
|
804
|
-
}
|
|
805
|
-
function sandboxImageExists(runtime, image) {
|
|
806
|
-
return spawnSync(runtime, ['image', 'inspect', image], { encoding: 'utf-8' }).status === 0;
|
|
807
|
-
}
|
|
808
773
|
function resolveSandboxDir() {
|
|
809
774
|
// 1. Check CWD (user is in repo root)
|
|
810
775
|
const cwdSandbox = join(process.cwd(), 'sandbox');
|
|
@@ -184,9 +184,14 @@ export function parseCodexOutput(stdout) {
|
|
|
184
184
|
for (const line of lines) {
|
|
185
185
|
try {
|
|
186
186
|
const obj = JSON.parse(line);
|
|
187
|
+
// Standard output_text events
|
|
187
188
|
if (obj.type === 'output_text' || obj.output_text) {
|
|
188
189
|
outputs.push(obj.output_text || obj.text || '');
|
|
189
190
|
}
|
|
191
|
+
// Codex stream-json: item.completed with agent_message
|
|
192
|
+
else if (obj.type === 'item.completed' && obj.item?.type === 'agent_message' && obj.item?.text) {
|
|
193
|
+
outputs.push(obj.item.text);
|
|
194
|
+
}
|
|
190
195
|
}
|
|
191
196
|
catch {
|
|
192
197
|
if (line.trim())
|
package/dist/config.d.ts
CHANGED
|
@@ -17,4 +17,11 @@ export declare function listMemoryFiles(agentId: string): {
|
|
|
17
17
|
date: string;
|
|
18
18
|
size: number;
|
|
19
19
|
}[];
|
|
20
|
+
/**
|
|
21
|
+
* Resolve allowed paths for a given context. Priority:
|
|
22
|
+
* 1. Explicit toolConfig.allowedPaths (if provided)
|
|
23
|
+
* 2. Config top-level allowedPaths
|
|
24
|
+
* 3. Fallback: ~/.skimpyclaw only
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveAllowedPaths(config: Config, overridePaths?: string[]): string[];
|
|
20
27
|
export declare function readMemoryFile(agentId: string, filename: string): string;
|
package/dist/config.js
CHANGED
|
@@ -109,6 +109,19 @@ export function listMemoryFiles(agentId) {
|
|
|
109
109
|
};
|
|
110
110
|
}).sort((a, b) => b.date.localeCompare(a.date));
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Resolve allowed paths for a given context. Priority:
|
|
114
|
+
* 1. Explicit toolConfig.allowedPaths (if provided)
|
|
115
|
+
* 2. Config top-level allowedPaths
|
|
116
|
+
* 3. Fallback: ~/.skimpyclaw only
|
|
117
|
+
*/
|
|
118
|
+
export function resolveAllowedPaths(config, overridePaths) {
|
|
119
|
+
if (overridePaths?.length)
|
|
120
|
+
return overridePaths;
|
|
121
|
+
if (config.allowedPaths?.length)
|
|
122
|
+
return config.allowedPaths;
|
|
123
|
+
return [join(homedir(), '.skimpyclaw')];
|
|
124
|
+
}
|
|
112
125
|
export function readMemoryFile(agentId, filename) {
|
|
113
126
|
if (!isValidAgentId(agentId)) {
|
|
114
127
|
throw new Error('Invalid agent ID');
|
package/dist/cron.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Cron } from 'croner';
|
|
|
3
3
|
import { exec } from 'child_process';
|
|
4
4
|
import { existsSync, mkdirSync, appendFileSync, readFileSync, watch } from 'fs';
|
|
5
5
|
import { join } from 'path';
|
|
6
|
-
import { getLogsDir, getConfigPath, loadConfig } from './config.js';
|
|
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';
|
|
@@ -152,11 +152,14 @@ async function executeJobPayload(jobDef, config) {
|
|
|
152
152
|
appendCronLogLine(jobDef.id, `Agent turn started (prompt: ${message.slice(0, 100)}...)`);
|
|
153
153
|
const defaultTools = {
|
|
154
154
|
enabled: true,
|
|
155
|
-
allowedPaths:
|
|
155
|
+
allowedPaths: resolveAllowedPaths(config),
|
|
156
156
|
maxIterations: 30,
|
|
157
157
|
bashTimeout: 15000,
|
|
158
158
|
};
|
|
159
|
-
const
|
|
159
|
+
const tools = jobDef.payload.tools
|
|
160
|
+
? { ...jobDef.payload.tools, allowedPaths: jobDef.payload.tools.allowedPaths?.length ? jobDef.payload.tools.allowedPaths : resolveAllowedPaths(config) }
|
|
161
|
+
: defaultTools;
|
|
162
|
+
const response = await runAgentTurn(config.agents.default, message, config, jobDef.model, tools, undefined, {
|
|
160
163
|
channel: getActiveChannelId() || 'telegram',
|
|
161
164
|
trigger: 'cron',
|
|
162
165
|
sessionId: jobDef.id,
|
package/dist/heartbeat.js
CHANGED
|
@@ -6,30 +6,26 @@
|
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import { homedir } from 'os';
|
|
8
8
|
import { runAgentTurn } from './agent.js';
|
|
9
|
+
import { resolveAllowedPaths } from './config.js';
|
|
9
10
|
import { pruneIdle, SANDBOX_DEFAULTS } from './sandbox/index.js';
|
|
10
11
|
import { getActiveChannelId, isActiveChannelSilenced, sendActiveChannelProactiveMessage, } from './channels.js';
|
|
11
12
|
let heartbeatTimer = null;
|
|
12
13
|
let running = false;
|
|
13
|
-
const DEFAULT_HEARTBEAT_TOOLS = {
|
|
14
|
-
enabled: true,
|
|
15
|
-
allowedPaths: [join(homedir(), '.skimpyclaw')],
|
|
16
|
-
maxIterations: 100,
|
|
17
|
-
bashTimeout: 15000,
|
|
18
|
-
};
|
|
19
14
|
function getHeartbeatTools(config) {
|
|
20
15
|
if (config.heartbeat.tools) {
|
|
21
|
-
return config.heartbeat.tools;
|
|
22
|
-
}
|
|
23
|
-
const defaultAllowedPaths = config.channels.active === 'discord'
|
|
24
|
-
? config.channels.discord?.defaultAllowedPaths
|
|
25
|
-
: config.channels.telegram.defaultAllowedPaths || config.channels.discord?.defaultAllowedPaths;
|
|
26
|
-
if (defaultAllowedPaths?.length) {
|
|
27
16
|
return {
|
|
28
|
-
...
|
|
29
|
-
allowedPaths:
|
|
17
|
+
...config.heartbeat.tools,
|
|
18
|
+
allowedPaths: config.heartbeat.tools.allowedPaths?.length
|
|
19
|
+
? config.heartbeat.tools.allowedPaths
|
|
20
|
+
: resolveAllowedPaths(config),
|
|
30
21
|
};
|
|
31
22
|
}
|
|
32
|
-
return
|
|
23
|
+
return {
|
|
24
|
+
enabled: true,
|
|
25
|
+
allowedPaths: resolveAllowedPaths(config),
|
|
26
|
+
maxIterations: 100,
|
|
27
|
+
bashTimeout: 15000,
|
|
28
|
+
};
|
|
33
29
|
}
|
|
34
30
|
function getHeartbeatFilePath(config) {
|
|
35
31
|
return join(homedir(), '.skimpyclaw', 'agents', config.agents.default, 'HEARTBEAT.md');
|
|
@@ -152,7 +152,13 @@ export async function chatWithToolsAnthropic(params) {
|
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
154
|
// Compact old tool results if context is growing large
|
|
155
|
-
const
|
|
155
|
+
const compactionResult = await compactAnthropicMessages(apiMessages, toolConfig.contextManagement, i + 1, config);
|
|
156
|
+
const messagesForApi = compactionResult.messages;
|
|
157
|
+
if (compactionResult.compacted) {
|
|
158
|
+
const method = compactionResult.method === 'llm' ? 'LLM summary' : 'truncation';
|
|
159
|
+
const detail = `~${Math.round((compactionResult.tokensBefore || 0) / 1000)}k → ~${Math.round((compactionResult.tokensAfter || 0) / 1000)}k tokens`;
|
|
160
|
+
toolLog.push(`[context compacted via ${method}: ${detail}]`);
|
|
161
|
+
}
|
|
156
162
|
const anthropicParams = {
|
|
157
163
|
model: modelId,
|
|
158
164
|
max_tokens: options.maxTokens || 16384,
|
package/dist/providers/codex.js
CHANGED
|
@@ -265,7 +265,7 @@ export async function chatCodex(params) {
|
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
export async function chatWithToolsCodex(params) {
|
|
268
|
-
const { messages, options, toolConfig, toolContext } = params;
|
|
268
|
+
const { messages, options, config, toolConfig, toolContext } = params;
|
|
269
269
|
const modelId = stripProvider(options.model);
|
|
270
270
|
const maxIterations = toolConfig.maxIterations || 100;
|
|
271
271
|
// Build input — system messages go to `instructions`, rest to `input`
|
|
@@ -308,7 +308,13 @@ export async function chatWithToolsCodex(params) {
|
|
|
308
308
|
};
|
|
309
309
|
}
|
|
310
310
|
// Compact old tool results if context is growing large
|
|
311
|
-
const
|
|
311
|
+
const compactionResult = await compactCodexMessages(input, toolConfig.contextManagement, i + 1, config);
|
|
312
|
+
const inputForApi = compactionResult.messages;
|
|
313
|
+
if (compactionResult.compacted) {
|
|
314
|
+
const method = compactionResult.method === 'llm' ? 'LLM summary' : 'truncation';
|
|
315
|
+
const detail = `~${Math.round((compactionResult.tokensBefore || 0) / 1000)}k → ~${Math.round((compactionResult.tokensAfter || 0) / 1000)}k tokens`;
|
|
316
|
+
toolLog.push(`[context compacted via ${method}: ${detail}]`);
|
|
317
|
+
}
|
|
312
318
|
const body = {
|
|
313
319
|
model: modelId,
|
|
314
320
|
instructions,
|
|
@@ -1,22 +1,53 @@
|
|
|
1
1
|
import type { ContextManagementConfig } from './types.js';
|
|
2
|
+
import type { Config } from '../types.js';
|
|
2
3
|
export type { ContextManagementConfig };
|
|
4
|
+
/** Result of a compaction attempt, including metadata about what happened. */
|
|
5
|
+
export interface CompactionResult<T> {
|
|
6
|
+
messages: T[];
|
|
7
|
+
/** Whether any compaction was performed */
|
|
8
|
+
compacted: boolean;
|
|
9
|
+
/** 'llm' if LLM summarized, 'truncation' if mechanically truncated, undefined if no compaction */
|
|
10
|
+
method?: 'llm' | 'truncation';
|
|
11
|
+
/** The summary text (only when method === 'llm') */
|
|
12
|
+
summary?: string;
|
|
13
|
+
/** Estimated tokens before compaction */
|
|
14
|
+
tokensBefore?: number;
|
|
15
|
+
/** Estimated tokens after compaction */
|
|
16
|
+
tokensAfter?: number;
|
|
17
|
+
}
|
|
3
18
|
/** Rough token estimate: 1 token ≈ 4 chars of JSON. */
|
|
4
19
|
export declare function estimateTokens(data: any[]): number;
|
|
20
|
+
/**
|
|
21
|
+
* Serialize Anthropic-format messages into a human-readable conversation transcript
|
|
22
|
+
* suitable for LLM summarization.
|
|
23
|
+
*/
|
|
24
|
+
declare function serializeAnthropicMessages(messages: any[]): string;
|
|
25
|
+
/**
|
|
26
|
+
* Serialize OpenAI-format messages into a human-readable transcript.
|
|
27
|
+
*/
|
|
28
|
+
declare function serializeOpenAIMessages(messages: any[]): string;
|
|
29
|
+
/**
|
|
30
|
+
* Serialize Codex-format input items into a human-readable transcript.
|
|
31
|
+
*/
|
|
32
|
+
declare function serializeCodexMessages(items: any[]): string;
|
|
5
33
|
/**
|
|
6
34
|
* Compact Anthropic-format apiMessages when over threshold.
|
|
7
|
-
*
|
|
35
|
+
* Uses LLM summarization for old messages; falls back to truncation on failure.
|
|
8
36
|
* Does NOT mutate the input array — returns a new array.
|
|
9
37
|
*/
|
|
10
|
-
export declare function compactAnthropicMessages(messages: any[], config?: ContextManagementConfig, iteration?: number): any
|
|
38
|
+
export declare function compactAnthropicMessages(messages: any[], config?: ContextManagementConfig, iteration?: number, fullConfig?: Config): Promise<CompactionResult<any>>;
|
|
11
39
|
/**
|
|
12
40
|
* Compact OpenAI-format apiMessages when over threshold.
|
|
13
|
-
*
|
|
41
|
+
* Uses LLM summarization for old messages; falls back to truncation on failure.
|
|
14
42
|
* Does NOT mutate the input array — returns a new array.
|
|
15
43
|
*/
|
|
16
|
-
export declare function compactOpenAIMessages(messages: any[], config?: ContextManagementConfig, iteration?: number): any
|
|
44
|
+
export declare function compactOpenAIMessages(messages: any[], config?: ContextManagementConfig, iteration?: number, fullConfig?: Config): Promise<CompactionResult<any>>;
|
|
17
45
|
/**
|
|
18
46
|
* Compact Codex-format input items when over threshold.
|
|
19
|
-
*
|
|
47
|
+
* Uses LLM summarization for old items; falls back to truncation on failure.
|
|
20
48
|
* Does NOT mutate the input array — returns a new array.
|
|
21
49
|
*/
|
|
22
|
-
export declare function compactCodexMessages(input: any[], config?: ContextManagementConfig, iteration?: number): any
|
|
50
|
+
export declare function compactCodexMessages(input: any[], config?: ContextManagementConfig, iteration?: number, fullConfig?: Config): Promise<CompactionResult<any>>;
|
|
51
|
+
export { serializeAnthropicMessages, serializeOpenAIMessages, serializeCodexMessages };
|
|
52
|
+
/** Reset compaction markers (for testing). */
|
|
53
|
+
export declare function resetCompactionState(): void;
|