skimpyclaw 0.3.5 → 0.3.8
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 +14 -6
- package/dist/__tests__/api.test.js +1 -19
- package/dist/__tests__/channels.test.js +1 -1
- package/dist/__tests__/code-agents-orchestrator.test.js +74 -7
- package/dist/__tests__/code-agents-sandbox.test.d.ts +1 -0
- package/dist/__tests__/code-agents-sandbox.test.js +163 -0
- package/dist/__tests__/context-manager.test.d.ts +1 -0
- package/dist/__tests__/context-manager.test.js +236 -0
- package/dist/__tests__/package-manager-detection.test.js +5 -5
- package/dist/__tests__/setup.test.js +10 -7
- package/dist/__tests__/skills.test.js +2 -2
- package/dist/__tests__/structured-context.test.d.ts +1 -0
- package/dist/__tests__/structured-context.test.js +100 -0
- package/dist/__tests__/tools.test.js +65 -3
- package/dist/agent.js +4 -5
- package/dist/api.js +10 -85
- package/dist/audit.js +5 -51
- package/dist/channels/telegram/handlers.js +2 -60
- package/dist/channels/telegram/index.js +0 -7
- package/dist/channels.js +1 -1
- package/dist/cli.js +186 -17
- package/dist/code-agents/executor.d.ts +9 -4
- package/dist/code-agents/executor.js +187 -13
- package/dist/code-agents/index.d.ts +1 -1
- package/dist/code-agents/index.js +23 -21
- package/dist/code-agents/orchestrator.d.ts +8 -2
- package/dist/code-agents/orchestrator.js +297 -27
- package/dist/code-agents/structured-context.d.ts +7 -0
- package/dist/code-agents/structured-context.js +54 -0
- package/dist/code-agents/types.d.ts +2 -0
- package/dist/code-agents/utils.js +12 -2
- package/dist/code-agents/worktree.d.ts +40 -0
- package/dist/code-agents/worktree.js +215 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +5 -3
- package/dist/cron.js +18 -4
- package/dist/dashboard/assets/index-BoTHPby4.js +65 -0
- package/dist/dashboard/assets/{index-EAg6lqF5.css → index-D4mufvBg.css} +1 -1
- package/dist/dashboard/index.html +2 -2
- package/dist/discord.js +4 -40
- package/dist/exec-approval.js +1 -1
- package/dist/file-lock.js +1 -1
- package/dist/gateway.js +3 -10
- package/dist/providers/anthropic.js +9 -5
- package/dist/providers/codex.js +10 -6
- package/dist/providers/context-manager.d.ts +22 -0
- package/dist/providers/context-manager.js +100 -0
- package/dist/providers/openai.js +9 -5
- package/dist/providers/types.d.ts +1 -0
- package/dist/security.js +9 -0
- package/dist/setup.d.ts +2 -1
- package/dist/setup.js +156 -34
- package/dist/skills.js +9 -2
- package/dist/subagent.js +33 -2
- package/dist/tools/bash-tool.js +8 -0
- package/dist/tools/browser-tool.js +3 -2
- package/dist/tools/definitions.d.ts +0 -27
- package/dist/tools/definitions.js +0 -18
- package/dist/tools/execute-context.d.ts +4 -4
- package/dist/tools/file-tools.d.ts +1 -1
- package/dist/tools/file-tools.js +1 -1
- package/dist/tools.d.ts +5 -5
- package/dist/tools.js +87 -98
- package/dist/types.d.ts +14 -22
- package/dist/usage.d.ts +1 -0
- package/dist/usage.js +30 -46
- package/dist/utils.d.ts +18 -0
- package/dist/utils.js +71 -0
- package/dist/voice.js +9 -7
- package/package.json +1 -1
- package/dist/dashboard/assets/index-UVAjSXCG.js +0 -107
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Context manager for agentic tool loops.
|
|
2
|
+
// When accumulated messages exceed the token threshold, compacts old tool results
|
|
3
|
+
// to keep context size bounded without breaking message structure.
|
|
4
|
+
//
|
|
5
|
+
// Key constraint: tool_use/tool_result pairs (Anthropic) and
|
|
6
|
+
// function_call/function_call_output pairs (Codex) must stay structurally intact.
|
|
7
|
+
// We truncate the CONTENT of old results — never remove blocks entirely.
|
|
8
|
+
const DEFAULT_MAX_CONTEXT_TOKENS = 200_000;
|
|
9
|
+
const KEEP_TAIL = 8; // always keep last N messages/items untouched
|
|
10
|
+
const RESULT_MAX_CHARS = 500; // compact old results to this length
|
|
11
|
+
/** Rough token estimate: 1 token ≈ 4 chars of JSON. */
|
|
12
|
+
export function estimateTokens(data) {
|
|
13
|
+
return Math.ceil(JSON.stringify(data).length / 4);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Compact Anthropic-format apiMessages when over threshold.
|
|
17
|
+
* Truncates content of old tool_result blocks; leaves last KEEP_TAIL messages intact.
|
|
18
|
+
* Does NOT mutate the input array — returns a new array.
|
|
19
|
+
*/
|
|
20
|
+
export function compactAnthropicMessages(messages, config, iteration = 0) {
|
|
21
|
+
if (config?.enabled === false)
|
|
22
|
+
return messages;
|
|
23
|
+
const maxTokens = config?.maxContextTokens ?? DEFAULT_MAX_CONTEXT_TOKENS;
|
|
24
|
+
const estimated = estimateTokens(messages);
|
|
25
|
+
if (estimated <= maxTokens)
|
|
26
|
+
return messages;
|
|
27
|
+
console.log(`[context-manager] Compacting at iteration ${iteration} (~${Math.round(estimated / 1000)}k tokens > ${Math.round(maxTokens / 1000)}k threshold)`);
|
|
28
|
+
const tail = messages.slice(-KEEP_TAIL);
|
|
29
|
+
const head = messages.slice(0, -KEEP_TAIL);
|
|
30
|
+
const compacted = head.map(msg => {
|
|
31
|
+
if (!Array.isArray(msg.content))
|
|
32
|
+
return msg;
|
|
33
|
+
let changed = false;
|
|
34
|
+
const newContent = msg.content.map((block) => {
|
|
35
|
+
if (block.type !== 'tool_result')
|
|
36
|
+
return block;
|
|
37
|
+
const raw = typeof block.content === 'string'
|
|
38
|
+
? block.content
|
|
39
|
+
: JSON.stringify(block.content);
|
|
40
|
+
if (raw.length <= RESULT_MAX_CHARS)
|
|
41
|
+
return block;
|
|
42
|
+
changed = true;
|
|
43
|
+
return { ...block, content: raw.slice(0, RESULT_MAX_CHARS) + ' [truncated]' };
|
|
44
|
+
});
|
|
45
|
+
return changed ? { ...msg, content: newContent } : msg;
|
|
46
|
+
});
|
|
47
|
+
return [...compacted, ...tail];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Compact OpenAI-format apiMessages when over threshold.
|
|
51
|
+
* Truncates content of old `role: 'tool'` messages; leaves last KEEP_TAIL messages intact.
|
|
52
|
+
* Does NOT mutate the input array — returns a new array.
|
|
53
|
+
*/
|
|
54
|
+
export function compactOpenAIMessages(messages, config, iteration = 0) {
|
|
55
|
+
if (config?.enabled === false)
|
|
56
|
+
return messages;
|
|
57
|
+
const maxTokens = config?.maxContextTokens ?? DEFAULT_MAX_CONTEXT_TOKENS;
|
|
58
|
+
const estimated = estimateTokens(messages);
|
|
59
|
+
if (estimated <= maxTokens)
|
|
60
|
+
return messages;
|
|
61
|
+
console.log(`[context-manager] Compacting OpenAI messages at iteration ${iteration} (~${Math.round(estimated / 1000)}k tokens > ${Math.round(maxTokens / 1000)}k threshold)`);
|
|
62
|
+
const tail = messages.slice(-KEEP_TAIL);
|
|
63
|
+
const head = messages.slice(0, -KEEP_TAIL);
|
|
64
|
+
const compacted = head.map(msg => {
|
|
65
|
+
if (msg.role !== 'tool')
|
|
66
|
+
return msg;
|
|
67
|
+
if (typeof msg.content !== 'string')
|
|
68
|
+
return msg;
|
|
69
|
+
if (msg.content.length <= RESULT_MAX_CHARS)
|
|
70
|
+
return msg;
|
|
71
|
+
return { ...msg, content: msg.content.slice(0, RESULT_MAX_CHARS) + ' [truncated]' };
|
|
72
|
+
});
|
|
73
|
+
return [...compacted, ...tail];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Compact Codex-format input items when over threshold.
|
|
77
|
+
* Truncates output of old function_call_output items; leaves last KEEP_TAIL items intact.
|
|
78
|
+
* Does NOT mutate the input array — returns a new array.
|
|
79
|
+
*/
|
|
80
|
+
export function compactCodexMessages(input, config, iteration = 0) {
|
|
81
|
+
if (config?.enabled === false)
|
|
82
|
+
return input;
|
|
83
|
+
const maxTokens = config?.maxContextTokens ?? DEFAULT_MAX_CONTEXT_TOKENS;
|
|
84
|
+
const estimated = estimateTokens(input);
|
|
85
|
+
if (estimated <= maxTokens)
|
|
86
|
+
return input;
|
|
87
|
+
console.log(`[context-manager] Compacting Codex input at iteration ${iteration} (~${Math.round(estimated / 1000)}k tokens > ${Math.round(maxTokens / 1000)}k threshold)`);
|
|
88
|
+
const tail = input.slice(-KEEP_TAIL);
|
|
89
|
+
const head = input.slice(0, -KEEP_TAIL);
|
|
90
|
+
const compacted = head.map(item => {
|
|
91
|
+
if (item.type !== 'function_call_output')
|
|
92
|
+
return item;
|
|
93
|
+
if (typeof item.output !== 'string')
|
|
94
|
+
return item;
|
|
95
|
+
if (item.output.length <= RESULT_MAX_CHARS)
|
|
96
|
+
return item;
|
|
97
|
+
return { ...item, output: item.output.slice(0, RESULT_MAX_CHARS) + ' [truncated]' };
|
|
98
|
+
});
|
|
99
|
+
return [...compacted, ...tail];
|
|
100
|
+
}
|
package/dist/providers/openai.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// OpenAI-Compatible Provider (OpenAI, OpenRouter, Groq, etc.)
|
|
2
2
|
import { startObservation } from '@langfuse/tracing';
|
|
3
3
|
import { stripProvider, toOpenAITools, truncateToolResult } from './utils.js';
|
|
4
|
+
import { compactOpenAIMessages } from './context-manager.js';
|
|
4
5
|
import { toOpenAIContent } from './content.js';
|
|
5
6
|
import { toUsageDetails, toCostDetails } from './observability.js';
|
|
6
7
|
import { getToolDefinitions, executeTool } from '../tools.js';
|
|
8
|
+
import { toErrorMessage } from '../utils.js';
|
|
7
9
|
import { ToolCallGuard } from './tool-guard.js';
|
|
8
10
|
import { addEvent } from '../audit.js';
|
|
9
11
|
import { buildUsageRecord, recordUsage } from '../usage.js';
|
|
@@ -106,7 +108,7 @@ export async function chatOpenAI(params, provider) {
|
|
|
106
108
|
return content;
|
|
107
109
|
}
|
|
108
110
|
catch (err) {
|
|
109
|
-
const errorMessage =
|
|
111
|
+
const errorMessage = toErrorMessage(err);
|
|
110
112
|
genObs?.update({ level: 'ERROR', statusMessage: errorMessage, output: { error: errorMessage } });
|
|
111
113
|
genObs?.end();
|
|
112
114
|
throw err;
|
|
@@ -123,7 +125,7 @@ export async function chatWithToolsOpenAI(params, provider) {
|
|
|
123
125
|
// Resolve tools once at start
|
|
124
126
|
const includeSpawn = !!(toolContext?.fullConfig && (toolContext?.chatId || toolContext?.isCronJob));
|
|
125
127
|
const toolDefs = await getToolDefinitions(toolConfig, {
|
|
126
|
-
|
|
128
|
+
includeAgentTools: includeSpawn,
|
|
127
129
|
includeMcp: false,
|
|
128
130
|
projects: toolContext?.fullConfig?.projects
|
|
129
131
|
});
|
|
@@ -150,6 +152,8 @@ export async function chatWithToolsOpenAI(params, provider) {
|
|
|
150
152
|
toolCalls: toolLog,
|
|
151
153
|
};
|
|
152
154
|
}
|
|
155
|
+
// Compact old tool results if context is growing large
|
|
156
|
+
const messagesForApi = compactOpenAIMessages(apiMessages, toolConfig.contextManagement, i + 1);
|
|
153
157
|
console.log(`[agent:openai-tools] Iteration ${i + 1}/${maxIterations} (provider: ${provider}, model: ${modelId})`);
|
|
154
158
|
const genObs = await startGenerationObservation(`${provider}:${modelId}`, {
|
|
155
159
|
input: { messages: apiMessages },
|
|
@@ -164,7 +168,7 @@ export async function chatWithToolsOpenAI(params, provider) {
|
|
|
164
168
|
try {
|
|
165
169
|
completion = await client.chat.completions.create({
|
|
166
170
|
model: modelId,
|
|
167
|
-
messages:
|
|
171
|
+
messages: messagesForApi,
|
|
168
172
|
tools: openaiTools,
|
|
169
173
|
max_tokens: options.maxTokens || 4096,
|
|
170
174
|
temperature: options.temperature,
|
|
@@ -187,7 +191,7 @@ export async function chatWithToolsOpenAI(params, provider) {
|
|
|
187
191
|
guard.recordTokens(completion.usage?.prompt_tokens ?? 0, completion.usage?.completion_tokens ?? 0);
|
|
188
192
|
}
|
|
189
193
|
catch (err) {
|
|
190
|
-
const errorMessage =
|
|
194
|
+
const errorMessage = toErrorMessage(err);
|
|
191
195
|
genObs?.update({ level: 'ERROR', statusMessage: errorMessage, output: { error: errorMessage } });
|
|
192
196
|
genObs?.end();
|
|
193
197
|
throw err;
|
|
@@ -315,7 +319,7 @@ export async function chatWithToolsOpenAI(params, provider) {
|
|
|
315
319
|
}
|
|
316
320
|
}
|
|
317
321
|
catch (err) {
|
|
318
|
-
const errorMessage =
|
|
322
|
+
const errorMessage = toErrorMessage(err);
|
|
319
323
|
toolObs?.update({ level: 'ERROR', statusMessage: errorMessage, output: { error: errorMessage } });
|
|
320
324
|
toolObs?.end();
|
|
321
325
|
if (toolContext?.auditTraceId) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Config, ChatMessage, ChatOptions, ToolConfig } from '../types.js';
|
|
2
2
|
import type { ExecuteToolContext } from '../tools/execute-context.js';
|
|
3
|
+
export type ContextManagementConfig = NonNullable<ToolConfig['contextManagement']>;
|
|
3
4
|
export interface ToolChatResult {
|
|
4
5
|
response: string;
|
|
5
6
|
toolCalls: string[];
|
package/dist/security.js
CHANGED
|
@@ -77,10 +77,19 @@ export function isRateLimited(userId) {
|
|
|
77
77
|
const timestamps = rateLimiter.get(key) || [];
|
|
78
78
|
const recent = timestamps.filter(t => now - t < WINDOW_MS);
|
|
79
79
|
if (recent.length >= RATE_LIMIT) {
|
|
80
|
+
rateLimiter.set(key, recent);
|
|
80
81
|
return true;
|
|
81
82
|
}
|
|
82
83
|
recent.push(now);
|
|
83
84
|
rateLimiter.set(key, recent);
|
|
85
|
+
// Prune stale entries periodically (every 100th call)
|
|
86
|
+
if (rateLimiter.size > 50) {
|
|
87
|
+
for (const [k, ts] of rateLimiter) {
|
|
88
|
+
if (ts.every(t => now - t >= WINDOW_MS)) {
|
|
89
|
+
rateLimiter.delete(k);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
84
93
|
return false;
|
|
85
94
|
}
|
|
86
95
|
export function clearRateLimiter() {
|
package/dist/setup.d.ts
CHANGED
|
@@ -21,8 +21,9 @@ interface SetupStarters {
|
|
|
21
21
|
cronWeather: boolean;
|
|
22
22
|
timezone: string;
|
|
23
23
|
weatherLocation: string;
|
|
24
|
-
skillCodeReview: boolean;
|
|
25
24
|
skillDailyNotes: boolean;
|
|
25
|
+
skillWeather: boolean;
|
|
26
|
+
skillWebSearch: boolean;
|
|
26
27
|
}
|
|
27
28
|
interface SetupBuildInput {
|
|
28
29
|
workspaceDir: string;
|
package/dist/setup.js
CHANGED
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from 'url';
|
|
|
7
7
|
import { spawnSync } from 'child_process';
|
|
8
8
|
import { randomUUID } from 'crypto';
|
|
9
9
|
import { runDoctor as runDoctorChecks } from './doctor/runner.js';
|
|
10
|
+
import { toErrorMessage } from './utils.js';
|
|
10
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
12
|
const __dirname = dirname(__filename);
|
|
12
13
|
// ANSI color helpers (no chalk dependency)
|
|
@@ -224,6 +225,28 @@ async function askProviders(rl, existingProviders) {
|
|
|
224
225
|
}
|
|
225
226
|
function buildStarterCronJobs(starters) {
|
|
226
227
|
const jobs = [];
|
|
228
|
+
// Memory trim is always included — runs 2x/day on a cheap model
|
|
229
|
+
jobs.push({
|
|
230
|
+
id: 'memory-trim',
|
|
231
|
+
name: 'Memory Trim',
|
|
232
|
+
model: 'claude-haiku',
|
|
233
|
+
schedule: {
|
|
234
|
+
kind: 'cron',
|
|
235
|
+
expr: '0 0,12 * * *',
|
|
236
|
+
tz: starters.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
237
|
+
},
|
|
238
|
+
payload: {
|
|
239
|
+
kind: 'agentTurn',
|
|
240
|
+
message: '~/.skimpyclaw/prompts/memory-trim.md',
|
|
241
|
+
tools: {
|
|
242
|
+
enabled: true,
|
|
243
|
+
allowedPaths: [`${homedir()}/.skimpyclaw`],
|
|
244
|
+
maxIterations: 30,
|
|
245
|
+
bashTimeout: 10000,
|
|
246
|
+
toolProfile: 'minimal',
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
});
|
|
227
250
|
if (starters.cronTechNews) {
|
|
228
251
|
jobs.push({
|
|
229
252
|
id: 'tech-digest',
|
|
@@ -460,17 +483,20 @@ export function buildSetupConfig(input) {
|
|
|
460
483
|
cronWeather: false,
|
|
461
484
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
|
|
462
485
|
weatherLocation: 'New York, NY',
|
|
463
|
-
skillCodeReview: false,
|
|
464
486
|
skillDailyNotes: false,
|
|
487
|
+
skillWeather: false,
|
|
488
|
+
skillWebSearch: false,
|
|
465
489
|
};
|
|
466
490
|
const basePaths = ['${HOME}/.skimpyclaw'];
|
|
467
491
|
const allPaths = [...basePaths, ...(input.extraAllowedPaths || [])];
|
|
468
492
|
const starterCronJobs = buildStarterCronJobs(starters);
|
|
469
493
|
const starterSkillEntries = {};
|
|
470
|
-
if (starters.skillCodeReview)
|
|
471
|
-
starterSkillEntries['code-review'] = true;
|
|
472
494
|
if (starters.skillDailyNotes)
|
|
473
495
|
starterSkillEntries['daily-notes'] = true;
|
|
496
|
+
if (starters.skillWeather)
|
|
497
|
+
starterSkillEntries['weather'] = true;
|
|
498
|
+
if (starters.skillWebSearch)
|
|
499
|
+
starterSkillEntries['web-search'] = true;
|
|
474
500
|
return {
|
|
475
501
|
gateway: {
|
|
476
502
|
port: 18790,
|
|
@@ -529,7 +555,7 @@ export function buildSetupConfig(input) {
|
|
|
529
555
|
jobs: starterCronJobs,
|
|
530
556
|
},
|
|
531
557
|
heartbeat: {
|
|
532
|
-
intervalMs:
|
|
558
|
+
intervalMs: 3600000,
|
|
533
559
|
prompt: 'Read ~/.skimpyclaw/agents/main/HEARTBEAT.md. Follow it strictly. If nothing needs attention, reply HEARTBEAT_OK.',
|
|
534
560
|
tools: {
|
|
535
561
|
enabled: true,
|
|
@@ -595,19 +621,6 @@ const REQUIRED_TEMPLATE_DEFAULTS = {
|
|
|
595
621
|
'HEARTBEAT.md': '# HEARTBEAT\n\nIf nothing needs attention, reply HEARTBEAT_OK.\n',
|
|
596
622
|
};
|
|
597
623
|
const STARTER_SKILL_TEMPLATES = {
|
|
598
|
-
'code-review': `---
|
|
599
|
-
name: code-review
|
|
600
|
-
description: Structured code review checklist for bugs, regressions, and missing tests.
|
|
601
|
-
triggers: ["review", "pr", "regression", "tests"]
|
|
602
|
-
priority: 80
|
|
603
|
-
---
|
|
604
|
-
|
|
605
|
-
When asked to review code:
|
|
606
|
-
1. Focus on correctness and regressions first.
|
|
607
|
-
2. Call out missing or weak test coverage.
|
|
608
|
-
3. Prefer concrete file-level findings.
|
|
609
|
-
4. End with risk summary and recommended fixes.
|
|
610
|
-
`,
|
|
611
624
|
'daily-notes': `---
|
|
612
625
|
name: daily-notes
|
|
613
626
|
description: Keep daily notes organized under the configured daily notes directory.
|
|
@@ -620,6 +633,92 @@ When writing daily notes:
|
|
|
620
633
|
2. Include sections: Priorities, Schedule, Notes, Follow-ups.
|
|
621
634
|
3. Keep entries concise and actionable.
|
|
622
635
|
4. Avoid creating files outside the configured daily notes directory.
|
|
636
|
+
`,
|
|
637
|
+
'weather': `---
|
|
638
|
+
name: weather
|
|
639
|
+
description: Fetch and format weather data for daily briefings and quick checks.
|
|
640
|
+
triggers: ["weather", "forecast", "temperature", "rain"]
|
|
641
|
+
priority: 45
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
When asked about weather or generating a daily briefing:
|
|
645
|
+
1. Use web search to find current weather for the user's location.
|
|
646
|
+
2. Format as: conditions, high/low temps, precipitation chance.
|
|
647
|
+
3. Keep it to 2-3 sentences max.
|
|
648
|
+
4. Include any weather alerts if present.
|
|
649
|
+
5. For daily briefings: mention if rain is expected (affects outdoor plans).
|
|
650
|
+
`,
|
|
651
|
+
'web-search': `---
|
|
652
|
+
name: web-search
|
|
653
|
+
description: Search the web using the Browser tool. Opens DuckDuckGo, reads results, and returns findings.
|
|
654
|
+
triggers: ["search", "look up", "google", "find online", "web search"]
|
|
655
|
+
priority: 50
|
|
656
|
+
---
|
|
657
|
+
|
|
658
|
+
When asked to search the web:
|
|
659
|
+
1. Use the Browser tool to open https://html.duckduckgo.com/html/?q=<URL-encoded query>
|
|
660
|
+
2. Use getText to read the search results page.
|
|
661
|
+
3. If a specific result looks promising, open that URL and extract the relevant content.
|
|
662
|
+
4. Summarize findings concisely — include source URLs.
|
|
663
|
+
5. Close the browser when done.
|
|
664
|
+
|
|
665
|
+
Do NOT fabricate results. If the search returns nothing useful, say so.
|
|
666
|
+
`,
|
|
667
|
+
'duckduckgo-html-search': `---
|
|
668
|
+
name: duckduckgo-html-search
|
|
669
|
+
description: Search the web via DuckDuckGo HTML results using the Browser tool
|
|
670
|
+
emoji: 🦆
|
|
671
|
+
tags: [search, web, browser]
|
|
672
|
+
priority: 45
|
|
673
|
+
enabled: true
|
|
674
|
+
---
|
|
675
|
+
|
|
676
|
+
# DuckDuckGo HTML Search Skill
|
|
677
|
+
|
|
678
|
+
Use this skill when the user asks for web search, source gathering, or lightweight browsing.
|
|
679
|
+
|
|
680
|
+
## Priority rule
|
|
681
|
+
DuckDuckGo HTML via Browser is the default search path.
|
|
682
|
+
- Prefer DuckDuckGo first, even if \\\`$web_search\\\` is available.
|
|
683
|
+
- Use \\\`$web_search\\\` only when the user explicitly asks for it, DuckDuckGo is blocked, or Browser is unavailable.
|
|
684
|
+
|
|
685
|
+
## Default workflow
|
|
686
|
+
1. Build query URL: \\\`https://duckduckgo.com/html/?q=<urlencoded query>\\\`
|
|
687
|
+
2. Open the URL with Browser.
|
|
688
|
+
3. Wait for result anchors (\\\`a.result__a\\\`) or fallback body text.
|
|
689
|
+
4. Extract results using one Browser \\\`evaluate\\\` call when possible.
|
|
690
|
+
5. Return only actually extracted items (never pad count).
|
|
691
|
+
|
|
692
|
+
## Extraction requirements
|
|
693
|
+
For each result, capture when available:
|
|
694
|
+
- title
|
|
695
|
+
- url
|
|
696
|
+
- snippet
|
|
697
|
+
|
|
698
|
+
If a field is missing, set it to \\\`UNAVAILABLE\\\`.
|
|
699
|
+
|
|
700
|
+
## Integrity rules
|
|
701
|
+
- Never fabricate results.
|
|
702
|
+
- If the page blocks, fails, or no results render, return \\\`UNAVAILABLE\\\` and state why.
|
|
703
|
+
- Never mix real and invented entries.
|
|
704
|
+
- Include source URLs in output.
|
|
705
|
+
|
|
706
|
+
## Browser strategy
|
|
707
|
+
- Prefer one-page extraction via \\\`evaluate\\\`:
|
|
708
|
+
- Collect \\\`a.result__a\\\` for title + href
|
|
709
|
+
- Collect nearby snippet nodes (\\\`.result__snippet\\\`) when present
|
|
710
|
+
- Use minimal actions: open → waitFor → evaluate → optional screenshot.
|
|
711
|
+
- If selectors change, fallback to visible text extraction and clearly mark reduced confidence.
|
|
712
|
+
|
|
713
|
+
## Output format (concise)
|
|
714
|
+
- Query used
|
|
715
|
+
- Result count actually extracted
|
|
716
|
+
- Bulleted results with title + URL + snippet
|
|
717
|
+
- Notes section for failures/limits
|
|
718
|
+
|
|
719
|
+
## Safe defaults
|
|
720
|
+
- Default top results target: 5 (or user-specified)
|
|
721
|
+
- If user asks for deep research, gather multiple queries but keep each query's extraction explicit and separated.
|
|
623
722
|
`,
|
|
624
723
|
};
|
|
625
724
|
function ensureCoreTemplates(agentDir) {
|
|
@@ -637,11 +736,13 @@ function ensureStarterSkills(starters) {
|
|
|
637
736
|
const created = [];
|
|
638
737
|
const skillsDir = join(CONFIG_DIR, 'skills');
|
|
639
738
|
mkdirSync(skillsDir, { recursive: true });
|
|
640
|
-
const requested = [];
|
|
641
|
-
if (starters.skillCodeReview)
|
|
642
|
-
requested.push('code-review');
|
|
739
|
+
const requested = ['duckduckgo-html-search']; // always installed
|
|
643
740
|
if (starters.skillDailyNotes)
|
|
644
741
|
requested.push('daily-notes');
|
|
742
|
+
if (starters.skillWeather)
|
|
743
|
+
requested.push('weather');
|
|
744
|
+
if (starters.skillWebSearch)
|
|
745
|
+
requested.push('web-search');
|
|
645
746
|
for (const skillName of requested) {
|
|
646
747
|
const dir = join(skillsDir, skillName);
|
|
647
748
|
const skillPath = join(dir, 'SKILL.md');
|
|
@@ -668,7 +769,7 @@ async function validateTelegramToken(token) {
|
|
|
668
769
|
return { ok: true, detail: body.result?.username ? `@${body.result.username}` : 'valid token' };
|
|
669
770
|
}
|
|
670
771
|
catch (err) {
|
|
671
|
-
return { ok: false, detail:
|
|
772
|
+
return { ok: false, detail: toErrorMessage(err) };
|
|
672
773
|
}
|
|
673
774
|
}
|
|
674
775
|
async function validateProviderAuth(providers, secrets) {
|
|
@@ -687,7 +788,7 @@ async function validateProviderAuth(providers, secrets) {
|
|
|
687
788
|
checks.push({ name: 'Anthropic API', ok: res.ok, detail: res.ok ? 'auth ok' : `HTTP ${res.status}` });
|
|
688
789
|
}
|
|
689
790
|
catch (err) {
|
|
690
|
-
checks.push({ name: 'Anthropic API', ok: false, detail:
|
|
791
|
+
checks.push({ name: 'Anthropic API', ok: false, detail: toErrorMessage(err) });
|
|
691
792
|
}
|
|
692
793
|
}
|
|
693
794
|
if (providers.has('openai-api') && secrets.openaiKey) {
|
|
@@ -698,7 +799,7 @@ async function validateProviderAuth(providers, secrets) {
|
|
|
698
799
|
checks.push({ name: 'OpenAI API', ok: res.ok, detail: res.ok ? 'auth ok' : `HTTP ${res.status}` });
|
|
699
800
|
}
|
|
700
801
|
catch (err) {
|
|
701
|
-
checks.push({ name: 'OpenAI API', ok: false, detail:
|
|
802
|
+
checks.push({ name: 'OpenAI API', ok: false, detail: toErrorMessage(err) });
|
|
702
803
|
}
|
|
703
804
|
}
|
|
704
805
|
if (providers.has('minimax-api') && secrets.minimaxKey) {
|
|
@@ -715,7 +816,7 @@ async function validateProviderAuth(providers, secrets) {
|
|
|
715
816
|
checks.push({ name: 'MiniMax API', ok: res.ok, detail: res.ok ? 'auth ok' : `HTTP ${res.status}` });
|
|
716
817
|
}
|
|
717
818
|
catch (err) {
|
|
718
|
-
checks.push({ name: 'MiniMax API', ok: false, detail:
|
|
819
|
+
checks.push({ name: 'MiniMax API', ok: false, detail: toErrorMessage(err) });
|
|
719
820
|
}
|
|
720
821
|
}
|
|
721
822
|
if (providers.has('codex-oauth')) {
|
|
@@ -990,20 +1091,29 @@ export async function runSetup(options = {}) {
|
|
|
990
1091
|
if (addTechNewsCron || addWeatherCron) {
|
|
991
1092
|
const tzInput = await ask(rl, ` Timezone for starter cron jobs [${localTz}]: `);
|
|
992
1093
|
cronTimezone = tzInput || localTz;
|
|
1094
|
+
try {
|
|
1095
|
+
Intl.DateTimeFormat(undefined, { timeZone: cronTimezone });
|
|
1096
|
+
}
|
|
1097
|
+
catch {
|
|
1098
|
+
console.log(` ⚠ Invalid timezone "${cronTimezone}", using ${localTz}`);
|
|
1099
|
+
cronTimezone = localTz;
|
|
1100
|
+
}
|
|
993
1101
|
}
|
|
994
1102
|
if (addWeatherCron) {
|
|
995
1103
|
const locationInput = await ask(rl, ' Weather location (city, state/country) [New York, NY]: ');
|
|
996
1104
|
weatherLocation = locationInput || 'New York, NY';
|
|
997
1105
|
}
|
|
998
|
-
const addCodeReviewSkill = /^y(es)?$/i.test(await ask(rl, ' Add starter skill: code-review? [y/N]: '));
|
|
999
1106
|
const addDailyNotesSkill = /^y(es)?$/i.test(await ask(rl, ' Add starter skill: daily-notes? [y/N]: '));
|
|
1107
|
+
const addWeatherSkill = /^y(es)?$/i.test(await ask(rl, ' Add starter skill: weather? [y/N]: '));
|
|
1108
|
+
const addWebSearchSkill = /^y(es)?$/i.test(await ask(rl, ' Add starter skill: web-search (uses Browser tool)? [y/N]: '));
|
|
1000
1109
|
const starters = {
|
|
1001
1110
|
cronTechNews: addTechNewsCron,
|
|
1002
1111
|
cronWeather: addWeatherCron,
|
|
1003
1112
|
timezone: cronTimezone,
|
|
1004
1113
|
weatherLocation,
|
|
1005
|
-
skillCodeReview: addCodeReviewSkill,
|
|
1006
1114
|
skillDailyNotes: addDailyNotesSkill,
|
|
1115
|
+
skillWeather: addWeatherSkill,
|
|
1116
|
+
skillWebSearch: addWebSearchSkill,
|
|
1007
1117
|
};
|
|
1008
1118
|
const { envContent, config: generatedConfig } = buildSetupArtifacts({
|
|
1009
1119
|
workspaceDir: extraAllowedPaths[0] || join(homedir(), '.skimpyclaw'),
|
|
@@ -1019,7 +1129,7 @@ export async function runSetup(options = {}) {
|
|
|
1019
1129
|
features,
|
|
1020
1130
|
starters,
|
|
1021
1131
|
});
|
|
1022
|
-
// On reconfigure, preserve dashboard token, cron jobs,
|
|
1132
|
+
// On reconfigure, preserve dashboard token, cron jobs, codeAgents config, security, langfuse
|
|
1023
1133
|
if (isReconfigure && existing.config) {
|
|
1024
1134
|
if (existing.config.dashboard?.token) {
|
|
1025
1135
|
generatedConfig.dashboard = existing.config.dashboard;
|
|
@@ -1038,8 +1148,8 @@ export async function runSetup(options = {}) {
|
|
|
1038
1148
|
}
|
|
1039
1149
|
generatedConfig.cron = { ...(existing.config.cron || {}), jobs: mergedCronJobs };
|
|
1040
1150
|
}
|
|
1041
|
-
if (existing.config.
|
|
1042
|
-
generatedConfig.
|
|
1151
|
+
if (existing.config.codeAgents) {
|
|
1152
|
+
generatedConfig.codeAgents = existing.config.codeAgents;
|
|
1043
1153
|
}
|
|
1044
1154
|
if (existing.config.security) {
|
|
1045
1155
|
generatedConfig.security = existing.config.security;
|
|
@@ -1191,13 +1301,25 @@ export async function runSetup(options = {}) {
|
|
|
1191
1301
|
console.log(` Token: ${c.cyan(dashboardToken)}`);
|
|
1192
1302
|
console.log(` ${c.dim('(also available via: skimpyclaw status)')}`);
|
|
1193
1303
|
console.log('\nNext steps:');
|
|
1194
|
-
|
|
1195
|
-
console.log(
|
|
1304
|
+
let step = 1;
|
|
1305
|
+
console.log(`${step++}. Review templates in ~/.skimpyclaw/agents/main/`);
|
|
1306
|
+
if (enableSandbox) {
|
|
1307
|
+
const runtimeHint = detectedSandboxRuntime === 'docker'
|
|
1308
|
+
? 'open -a Docker # or start Docker Desktop'
|
|
1309
|
+
: 'container system start';
|
|
1310
|
+
console.log(`${step++}. Start the container runtime (if not already running):`);
|
|
1311
|
+
console.log(` ${runtimeHint}`);
|
|
1312
|
+
console.log(`${step++}. Initialize the sandbox:`);
|
|
1313
|
+
console.log(' skimpyclaw sandbox init');
|
|
1314
|
+
console.log(`${step++}. Verify sandbox is working:`);
|
|
1315
|
+
console.log(' skimpyclaw sandbox doctor');
|
|
1316
|
+
}
|
|
1317
|
+
console.log(`${step++}. Start the daemon:`);
|
|
1196
1318
|
console.log(' skimpyclaw start --daemon');
|
|
1197
|
-
console.log(
|
|
1319
|
+
console.log(`${step++}. Check health:`);
|
|
1198
1320
|
console.log(' skimpyclaw status');
|
|
1199
|
-
console.log(
|
|
1200
|
-
console.log(
|
|
1321
|
+
console.log(`${step++}. Optional daemon controls: skimpyclaw stop | skimpyclaw restart`);
|
|
1322
|
+
console.log(`${step++}. Send /help in your ${useDiscord ? 'Discord bot DM/server' : 'Telegram bot'}`);
|
|
1201
1323
|
console.log('\n👙🦞 Enjoy!');
|
|
1202
1324
|
}
|
|
1203
1325
|
finally {
|
package/dist/skills.js
CHANGED
|
@@ -5,18 +5,25 @@ import { homedir } from 'os';
|
|
|
5
5
|
import { execSync } from 'child_process';
|
|
6
6
|
import matter from 'gray-matter';
|
|
7
7
|
import { TTLCache } from './cache.js';
|
|
8
|
+
import { toErrorMessage } from './utils.js';
|
|
8
9
|
const DEFAULT_SKILLS_DIR = join(homedir(), '.skimpyclaw', 'skills');
|
|
9
10
|
const DEFAULT_PRIORITY = 100;
|
|
10
11
|
/**
|
|
11
12
|
* Check if a binary exists on PATH.
|
|
12
13
|
* Returns true if found, false otherwise.
|
|
13
14
|
*/
|
|
15
|
+
const binExistsCache = new Map();
|
|
14
16
|
function binExists(name) {
|
|
17
|
+
const cached = binExistsCache.get(name);
|
|
18
|
+
if (cached !== undefined)
|
|
19
|
+
return cached;
|
|
15
20
|
try {
|
|
16
21
|
execSync(`which ${name}`, { stdio: 'ignore' });
|
|
22
|
+
binExistsCache.set(name, true);
|
|
17
23
|
return true;
|
|
18
24
|
}
|
|
19
25
|
catch {
|
|
26
|
+
binExistsCache.set(name, false);
|
|
20
27
|
return false;
|
|
21
28
|
}
|
|
22
29
|
}
|
|
@@ -64,7 +71,7 @@ export function checkEligibility(skill, toolConfig) {
|
|
|
64
71
|
if (!toolConfig.browser?.enabled)
|
|
65
72
|
missing.push(tool);
|
|
66
73
|
}
|
|
67
|
-
//
|
|
74
|
+
// built-in tools are available whenever tools.enabled is true
|
|
68
75
|
}
|
|
69
76
|
if (missing.length > 0) {
|
|
70
77
|
return { eligible: false, reason: `Tools not enabled (needs: ${missing.join(', ')})` };
|
|
@@ -128,7 +135,7 @@ function parseSkillFile(dirPath, dirName, skillConfig, toolConfig) {
|
|
|
128
135
|
};
|
|
129
136
|
}
|
|
130
137
|
catch (err) {
|
|
131
|
-
const msg =
|
|
138
|
+
const msg = toErrorMessage(err);
|
|
132
139
|
console.warn(`[skills] Failed to parse ${skillPath}: ${msg}`);
|
|
133
140
|
return null;
|
|
134
141
|
}
|
package/dist/subagent.js
CHANGED
|
@@ -18,7 +18,8 @@ const PRESETS = {
|
|
|
18
18
|
enabled: true,
|
|
19
19
|
allowedPaths: [join(homedir(), '.skimpyclaw')],
|
|
20
20
|
maxIterations: 50,
|
|
21
|
-
bashTimeout: 30000
|
|
21
|
+
bashTimeout: 30000,
|
|
22
|
+
toolProfile: 'minimal',
|
|
22
23
|
},
|
|
23
24
|
description: 'Code tasks with broad file + bash access'
|
|
24
25
|
},
|
|
@@ -29,7 +30,8 @@ const PRESETS = {
|
|
|
29
30
|
enabled: true,
|
|
30
31
|
allowedPaths: [join(homedir(), '.skimpyclaw')],
|
|
31
32
|
maxIterations: 30,
|
|
32
|
-
bashTimeout: 15000
|
|
33
|
+
bashTimeout: 15000,
|
|
34
|
+
toolProfile: 'minimal',
|
|
33
35
|
},
|
|
34
36
|
description: 'Research tasks with configurable file access'
|
|
35
37
|
},
|
|
@@ -63,6 +65,21 @@ within your allowed paths (provided at runtime).
|
|
|
63
65
|
You have a LIMITED number of tool iterations. Every message you spend talking about what
|
|
64
66
|
you're going to do is one less chance to actually do it.
|
|
65
67
|
|
|
68
|
+
## Tool Priority — Use Native Tools First
|
|
69
|
+
|
|
70
|
+
**For file operations, always prefer native tools over bash scripts. They are faster, safer, and use fewer iterations.**
|
|
71
|
+
|
|
72
|
+
| Task | Use this | NOT this |
|
|
73
|
+
|------|----------|----------|
|
|
74
|
+
| List files/dirs | \`Glob\` | \`ls\`, \`find\`, \`python3\` scripts |
|
|
75
|
+
| Read a file | \`Read\` | \`cat\`, \`python3\` scripts |
|
|
76
|
+
| Write a file | \`Write\` | \`echo >\`, \`tee\`, \`python3\` scripts |
|
|
77
|
+
| Run builds/tests | \`Bash\` | — |
|
|
78
|
+
| Git operations | \`Bash\` | — |
|
|
79
|
+
| Install packages | \`Bash\` | — |
|
|
80
|
+
|
|
81
|
+
**NEVER use \`python3 -\`, \`perl -e\`, \`ruby -e\`, or other inline interpreter scripts to explore the filesystem. Use Glob and Read instead.**
|
|
82
|
+
|
|
66
83
|
## Your 4 Tools
|
|
67
84
|
|
|
68
85
|
### Read
|
|
@@ -76,6 +93,7 @@ List files and directories at a path. Parameter: \`path\` (string, required)
|
|
|
76
93
|
|
|
77
94
|
### Bash
|
|
78
95
|
Execute a shell command. Parameters: \`command\` (string, required), \`cwd\` (string, optional)
|
|
96
|
+
Reserved for: builds, tests, git, package managers, and commands that have no native tool equivalent.
|
|
79
97
|
|
|
80
98
|
## Key Paths
|
|
81
99
|
- Config: ~/.skimpyclaw/config.json
|
|
@@ -106,6 +124,18 @@ You are a research subagent dispatched for a specific task.
|
|
|
106
124
|
|
|
107
125
|
**NEVER say "let me check" or "I'll look into that" — just call the tool.**
|
|
108
126
|
|
|
127
|
+
## Tool Priority — Use Native Tools First
|
|
128
|
+
|
|
129
|
+
**For file operations, always prefer native tools over bash scripts.**
|
|
130
|
+
|
|
131
|
+
| Task | Use this | NOT this |
|
|
132
|
+
|------|----------|----------|
|
|
133
|
+
| List files/dirs | \`Glob\` | \`ls\`, \`find\`, \`python3\` scripts |
|
|
134
|
+
| Read a file | \`Read\` | \`cat\`, \`python3\` scripts |
|
|
135
|
+
| Write a file | \`Write\` | \`echo >\`, \`tee\` |
|
|
136
|
+
|
|
137
|
+
**NEVER use \`python3 -\`, \`perl -e\`, or other inline interpreter scripts to explore the filesystem.**
|
|
138
|
+
|
|
109
139
|
## Your 4 Tools
|
|
110
140
|
|
|
111
141
|
### Read
|
|
@@ -119,6 +149,7 @@ List files and directories at a path. Parameter: \`path\` (string, required)
|
|
|
119
149
|
|
|
120
150
|
### Bash
|
|
121
151
|
Execute a shell command. Parameters: \`command\` (string, required), \`cwd\` (string, optional)
|
|
152
|
+
Reserved for: git, package managers, and commands with no native tool equivalent.
|
|
122
153
|
|
|
123
154
|
## Key Paths
|
|
124
155
|
- Config: ~/.skimpyclaw/config.json
|
package/dist/tools/bash-tool.js
CHANGED
|
@@ -37,6 +37,14 @@ export async function executeBash(command, cwd, config, context) {
|
|
|
37
37
|
if (approvalConfig?.enabled !== false) {
|
|
38
38
|
const classification = classifyCommandRisk(command);
|
|
39
39
|
if (requiresApproval(classification, approvalConfig)) {
|
|
40
|
+
// Unattended contexts (cron, no approver) have no human available to approve.
|
|
41
|
+
// Fast-deny instead of blocking for the full TTL.
|
|
42
|
+
const isUnattended = context?.channel === 'subagent' ||
|
|
43
|
+
context?.isCronJob === true ||
|
|
44
|
+
(!context?.approverUserId && !context?.channelTargetId && !context?.chatId);
|
|
45
|
+
if (isUnattended) {
|
|
46
|
+
return `⛔ Command blocked — tier ${classification.tier} commands require approval but no approver is available in this context (${classification.reason}). Use safer alternatives or request approval via an interactive channel.`;
|
|
47
|
+
}
|
|
40
48
|
// Build channel metadata from context for notification routing
|
|
41
49
|
const channelMeta = context?.channel
|
|
42
50
|
? {
|