skimpyclaw 0.3.6 → 0.3.9
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 -0
- package/dist/__tests__/channels.test.js +1 -1
- package/dist/__tests__/code-agents-orchestrator.test.js +74 -7
- package/dist/__tests__/code-agents-preflight.test.d.ts +1 -0
- package/dist/__tests__/code-agents-preflight.test.js +88 -0
- package/dist/__tests__/code-agents-sandbox.test.d.ts +1 -0
- package/dist/__tests__/code-agents-sandbox.test.js +163 -0
- package/dist/__tests__/code-agents-utils.test.js +12 -1
- 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 +7 -5
- 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 -58
- 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 +151 -16
- 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 +30 -22
- package/dist/code-agents/orchestrator.d.ts +8 -2
- package/dist/code-agents/orchestrator.js +318 -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.d.ts +4 -0
- package/dist/code-agents/utils.js +38 -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-CkonC7Cd.js → index-BoTHPby4.js} +20 -20
- 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.js +122 -27
- 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 +2 -1
- 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 +26 -21
|
@@ -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.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',
|
|
@@ -532,7 +555,7 @@ export function buildSetupConfig(input) {
|
|
|
532
555
|
jobs: starterCronJobs,
|
|
533
556
|
},
|
|
534
557
|
heartbeat: {
|
|
535
|
-
intervalMs:
|
|
558
|
+
intervalMs: 3600000,
|
|
536
559
|
prompt: 'Read ~/.skimpyclaw/agents/main/HEARTBEAT.md. Follow it strictly. If nothing needs attention, reply HEARTBEAT_OK.',
|
|
537
560
|
tools: {
|
|
538
561
|
enabled: true,
|
|
@@ -640,6 +663,62 @@ When asked to search the web:
|
|
|
640
663
|
5. Close the browser when done.
|
|
641
664
|
|
|
642
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.
|
|
643
722
|
`,
|
|
644
723
|
};
|
|
645
724
|
function ensureCoreTemplates(agentDir) {
|
|
@@ -657,7 +736,7 @@ function ensureStarterSkills(starters) {
|
|
|
657
736
|
const created = [];
|
|
658
737
|
const skillsDir = join(CONFIG_DIR, 'skills');
|
|
659
738
|
mkdirSync(skillsDir, { recursive: true });
|
|
660
|
-
const requested = [];
|
|
739
|
+
const requested = ['duckduckgo-html-search']; // always installed
|
|
661
740
|
if (starters.skillDailyNotes)
|
|
662
741
|
requested.push('daily-notes');
|
|
663
742
|
if (starters.skillWeather)
|
|
@@ -690,7 +769,7 @@ async function validateTelegramToken(token) {
|
|
|
690
769
|
return { ok: true, detail: body.result?.username ? `@${body.result.username}` : 'valid token' };
|
|
691
770
|
}
|
|
692
771
|
catch (err) {
|
|
693
|
-
return { ok: false, detail:
|
|
772
|
+
return { ok: false, detail: toErrorMessage(err) };
|
|
694
773
|
}
|
|
695
774
|
}
|
|
696
775
|
async function validateProviderAuth(providers, secrets) {
|
|
@@ -709,7 +788,7 @@ async function validateProviderAuth(providers, secrets) {
|
|
|
709
788
|
checks.push({ name: 'Anthropic API', ok: res.ok, detail: res.ok ? 'auth ok' : `HTTP ${res.status}` });
|
|
710
789
|
}
|
|
711
790
|
catch (err) {
|
|
712
|
-
checks.push({ name: 'Anthropic API', ok: false, detail:
|
|
791
|
+
checks.push({ name: 'Anthropic API', ok: false, detail: toErrorMessage(err) });
|
|
713
792
|
}
|
|
714
793
|
}
|
|
715
794
|
if (providers.has('openai-api') && secrets.openaiKey) {
|
|
@@ -720,7 +799,7 @@ async function validateProviderAuth(providers, secrets) {
|
|
|
720
799
|
checks.push({ name: 'OpenAI API', ok: res.ok, detail: res.ok ? 'auth ok' : `HTTP ${res.status}` });
|
|
721
800
|
}
|
|
722
801
|
catch (err) {
|
|
723
|
-
checks.push({ name: 'OpenAI API', ok: false, detail:
|
|
802
|
+
checks.push({ name: 'OpenAI API', ok: false, detail: toErrorMessage(err) });
|
|
724
803
|
}
|
|
725
804
|
}
|
|
726
805
|
if (providers.has('minimax-api') && secrets.minimaxKey) {
|
|
@@ -737,7 +816,7 @@ async function validateProviderAuth(providers, secrets) {
|
|
|
737
816
|
checks.push({ name: 'MiniMax API', ok: res.ok, detail: res.ok ? 'auth ok' : `HTTP ${res.status}` });
|
|
738
817
|
}
|
|
739
818
|
catch (err) {
|
|
740
|
-
checks.push({ name: 'MiniMax API', ok: false, detail:
|
|
819
|
+
checks.push({ name: 'MiniMax API', ok: false, detail: toErrorMessage(err) });
|
|
741
820
|
}
|
|
742
821
|
}
|
|
743
822
|
if (providers.has('codex-oauth')) {
|
|
@@ -1006,28 +1085,27 @@ export async function runSetup(options = {}) {
|
|
|
1006
1085
|
sectionHeader('Starter Packs (optional)');
|
|
1007
1086
|
const localTz = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
1008
1087
|
const addTechNewsCron = /^y(es)?$/i.test(await ask(rl, ' Add starter cron: top 10 Hacker News daily? [y/N]: '));
|
|
1009
|
-
const addWeatherCron = /^y(es)?$/i.test(await ask(rl, ' Add starter cron: weather check daily at 7:00am? [y/N]: '));
|
|
1010
1088
|
let cronTimezone = localTz;
|
|
1011
|
-
|
|
1012
|
-
if (addTechNewsCron || addWeatherCron) {
|
|
1089
|
+
if (addTechNewsCron) {
|
|
1013
1090
|
const tzInput = await ask(rl, ` Timezone for starter cron jobs [${localTz}]: `);
|
|
1014
1091
|
cronTimezone = tzInput || localTz;
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1092
|
+
try {
|
|
1093
|
+
Intl.DateTimeFormat(undefined, { timeZone: cronTimezone });
|
|
1094
|
+
}
|
|
1095
|
+
catch {
|
|
1096
|
+
console.log(` ⚠ Invalid timezone "${cronTimezone}", using ${localTz}`);
|
|
1097
|
+
cronTimezone = localTz;
|
|
1098
|
+
}
|
|
1019
1099
|
}
|
|
1020
1100
|
const addDailyNotesSkill = /^y(es)?$/i.test(await ask(rl, ' Add starter skill: daily-notes? [y/N]: '));
|
|
1021
|
-
const addWeatherSkill = /^y(es)?$/i.test(await ask(rl, ' Add starter skill: weather? [y/N]: '));
|
|
1022
|
-
const addWebSearchSkill = /^y(es)?$/i.test(await ask(rl, ' Add starter skill: web-search (uses Browser tool)? [y/N]: '));
|
|
1023
1101
|
const starters = {
|
|
1024
1102
|
cronTechNews: addTechNewsCron,
|
|
1025
|
-
cronWeather:
|
|
1103
|
+
cronWeather: false,
|
|
1026
1104
|
timezone: cronTimezone,
|
|
1027
|
-
weatherLocation,
|
|
1105
|
+
weatherLocation: '',
|
|
1028
1106
|
skillDailyNotes: addDailyNotesSkill,
|
|
1029
|
-
skillWeather:
|
|
1030
|
-
skillWebSearch:
|
|
1107
|
+
skillWeather: false,
|
|
1108
|
+
skillWebSearch: false,
|
|
1031
1109
|
};
|
|
1032
1110
|
const { envContent, config: generatedConfig } = buildSetupArtifacts({
|
|
1033
1111
|
workspaceDir: extraAllowedPaths[0] || join(homedir(), '.skimpyclaw'),
|
|
@@ -1043,7 +1121,7 @@ export async function runSetup(options = {}) {
|
|
|
1043
1121
|
features,
|
|
1044
1122
|
starters,
|
|
1045
1123
|
});
|
|
1046
|
-
// On reconfigure, preserve dashboard token, cron jobs,
|
|
1124
|
+
// On reconfigure, preserve dashboard token, cron jobs, codeAgents config, security, langfuse
|
|
1047
1125
|
if (isReconfigure && existing.config) {
|
|
1048
1126
|
if (existing.config.dashboard?.token) {
|
|
1049
1127
|
generatedConfig.dashboard = existing.config.dashboard;
|
|
@@ -1062,8 +1140,8 @@ export async function runSetup(options = {}) {
|
|
|
1062
1140
|
}
|
|
1063
1141
|
generatedConfig.cron = { ...(existing.config.cron || {}), jobs: mergedCronJobs };
|
|
1064
1142
|
}
|
|
1065
|
-
if (existing.config.
|
|
1066
|
-
generatedConfig.
|
|
1143
|
+
if (existing.config.codeAgents) {
|
|
1144
|
+
generatedConfig.codeAgents = existing.config.codeAgents;
|
|
1067
1145
|
}
|
|
1068
1146
|
if (existing.config.security) {
|
|
1069
1147
|
generatedConfig.security = existing.config.security;
|
|
@@ -1215,13 +1293,30 @@ export async function runSetup(options = {}) {
|
|
|
1215
1293
|
console.log(` Token: ${c.cyan(dashboardToken)}`);
|
|
1216
1294
|
console.log(` ${c.dim('(also available via: skimpyclaw status)')}`);
|
|
1217
1295
|
console.log('\nNext steps:');
|
|
1218
|
-
|
|
1219
|
-
console.log(
|
|
1296
|
+
let step = 1;
|
|
1297
|
+
console.log(`${step++}. Review templates in ~/.skimpyclaw/agents/main/`);
|
|
1298
|
+
if (enableSandbox) {
|
|
1299
|
+
const runtimeHint = detectedSandboxRuntime === 'docker'
|
|
1300
|
+
? 'open -a Docker # or start Docker Desktop'
|
|
1301
|
+
: 'container system start';
|
|
1302
|
+
console.log(`${step++}. Start the container runtime (if not already running):`);
|
|
1303
|
+
console.log(` ${runtimeHint}`);
|
|
1304
|
+
console.log(`${step++}. Initialize the sandbox:`);
|
|
1305
|
+
console.log(' skimpyclaw sandbox init');
|
|
1306
|
+
console.log(`${step++}. Verify sandbox is working:`);
|
|
1307
|
+
console.log(' skimpyclaw sandbox doctor');
|
|
1308
|
+
}
|
|
1309
|
+
console.log(`${step++}. Start the daemon:`);
|
|
1220
1310
|
console.log(' skimpyclaw start --daemon');
|
|
1221
|
-
console.log(
|
|
1311
|
+
console.log(`${step++}. Check health:`);
|
|
1222
1312
|
console.log(' skimpyclaw status');
|
|
1223
|
-
console.log(
|
|
1224
|
-
console.log(
|
|
1313
|
+
console.log(`${step++}. Optional daemon controls: skimpyclaw stop | skimpyclaw restart`);
|
|
1314
|
+
console.log(`${step++}. Send /help in your ${useDiscord ? 'Discord bot DM/server' : 'Telegram bot'}`);
|
|
1315
|
+
console.log('');
|
|
1316
|
+
console.log(`${c.yellow('Note:')} The /team and /code tools require an external coding CLI on your PATH:`);
|
|
1317
|
+
console.log(' • Claude Code CLI → https://docs.anthropic.com/en/docs/claude-code');
|
|
1318
|
+
console.log(' • Codex CLI → https://github.com/openai/codex');
|
|
1319
|
+
console.log(' Install at least one to use code_with_agent / code_with_team.');
|
|
1225
1320
|
console.log('\n👙🦞 Enjoy!');
|
|
1226
1321
|
}
|
|
1227
1322
|
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
|
? {
|
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync } from 'fs';
|
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { isPathAllowed } from './path-utils.js';
|
|
5
|
+
import { toErrorMessage } from '../utils.js';
|
|
5
6
|
let playwrightModule = null;
|
|
6
7
|
let browserContext = null;
|
|
7
8
|
let browserPage = null;
|
|
@@ -101,7 +102,7 @@ async function ensureBrowser(config, overrides) {
|
|
|
101
102
|
});
|
|
102
103
|
}
|
|
103
104
|
catch (err) {
|
|
104
|
-
const msg =
|
|
105
|
+
const msg = toErrorMessage(err);
|
|
105
106
|
throw new Error(`Failed to launch browser (${options.type}): ${msg}. Ensure the browser is installed: npx playwright install ${options.type}`);
|
|
106
107
|
}
|
|
107
108
|
const pages = browserContext.pages();
|
|
@@ -305,33 +305,6 @@ export declare const TOOL_DEFINITIONS: ({
|
|
|
305
305
|
required: string[];
|
|
306
306
|
};
|
|
307
307
|
})[];
|
|
308
|
-
export declare const SPAWN_SUBAGENT_TOOL: {
|
|
309
|
-
name: string;
|
|
310
|
-
description: string;
|
|
311
|
-
input_schema: {
|
|
312
|
-
type: "object";
|
|
313
|
-
properties: {
|
|
314
|
-
task: {
|
|
315
|
-
type: string;
|
|
316
|
-
description: string;
|
|
317
|
-
};
|
|
318
|
-
type: {
|
|
319
|
-
type: string;
|
|
320
|
-
enum: string[];
|
|
321
|
-
description: string;
|
|
322
|
-
};
|
|
323
|
-
model: {
|
|
324
|
-
type: string;
|
|
325
|
-
description: string;
|
|
326
|
-
};
|
|
327
|
-
label: {
|
|
328
|
-
type: string;
|
|
329
|
-
description: string;
|
|
330
|
-
};
|
|
331
|
-
};
|
|
332
|
-
required: string[];
|
|
333
|
-
};
|
|
334
|
-
};
|
|
335
308
|
export declare const CODE_WITH_AGENT_TOOL: {
|
|
336
309
|
name: string;
|
|
337
310
|
description: string;
|
|
@@ -112,24 +112,6 @@ export const BROWSER_TOOL_DEFINITION = {
|
|
|
112
112
|
};
|
|
113
113
|
// Legacy export for backward compat — static list (built-ins + browser + no MCP)
|
|
114
114
|
export const TOOL_DEFINITIONS = [...BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION];
|
|
115
|
-
export const SPAWN_SUBAGENT_TOOL = {
|
|
116
|
-
name: 'spawn_subagent',
|
|
117
|
-
description: 'Spawn a background subagent to handle a task independently. Returns immediately with a run ID. Results are announced back to this chat when done. Use for tasks that benefit from parallel work or long-running operations.',
|
|
118
|
-
input_schema: {
|
|
119
|
-
type: 'object',
|
|
120
|
-
properties: {
|
|
121
|
-
task: { type: 'string', description: 'What the subagent should do — be specific and self-contained' },
|
|
122
|
-
type: {
|
|
123
|
-
type: 'string',
|
|
124
|
-
enum: ['coding', 'research'],
|
|
125
|
-
description: 'Agent type: coding (code/files/bash), research (investigation/reading)',
|
|
126
|
-
},
|
|
127
|
-
model: { type: 'string', description: 'Optional model override (e.g. claude-opus, claude-think)' },
|
|
128
|
-
label: { type: 'string', description: 'Short label for status display (e.g. "write tests", "check logs")' },
|
|
129
|
-
},
|
|
130
|
-
required: ['task', 'type'],
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
115
|
export const CODE_WITH_AGENT_TOOL = {
|
|
134
116
|
name: 'code_with_agent',
|
|
135
117
|
description: 'Delegate a coding task to a coding agent CLI (Claude Code or Codex). The agent will edit files, run commands, and return results. Always use this for code changes instead of writing code directly.',
|