skimpyclaw 0.3.14 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -37
- package/dist/__tests__/adapter-types.test.d.ts +4 -0
- package/dist/__tests__/adapter-types.test.js +63 -0
- package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
- package/dist/__tests__/anthropic-adapter.test.js +264 -0
- package/dist/__tests__/api.test.js +0 -1
- package/dist/__tests__/cli.integration.test.js +2 -4
- package/dist/__tests__/cli.test.js +0 -1
- package/dist/__tests__/code-agents-notifications.test.js +137 -0
- package/dist/__tests__/code-agents-parser.test.js +19 -1
- package/dist/__tests__/code-agents-preflight.test.js +3 -28
- package/dist/__tests__/code-agents-utils.test.js +34 -9
- package/dist/__tests__/code-agents-worktrees.test.js +116 -0
- package/dist/__tests__/codex-adapter.test.js +184 -0
- package/dist/__tests__/codex-auth.test.js +66 -0
- package/dist/__tests__/codex-provider-gating.test.js +35 -0
- package/dist/__tests__/codex-unified-loop.test.js +111 -0
- package/dist/__tests__/config-security.test.js +127 -0
- package/dist/__tests__/config.test.js +23 -0
- package/dist/__tests__/context-manager.test.js +243 -164
- package/dist/__tests__/cron-run.test.js +250 -0
- package/dist/__tests__/cron.test.js +12 -38
- package/dist/__tests__/digests.test.js +67 -0
- package/dist/__tests__/discord-attachments.test.js +211 -0
- package/dist/__tests__/discord-docs.test.d.ts +1 -0
- package/dist/__tests__/discord-docs.test.js +27 -0
- package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-agents.test.js +115 -0
- package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-context.test.js +42 -0
- package/dist/__tests__/doctor.formatters.test.js +4 -4
- package/dist/__tests__/doctor.index.test.js +1 -1
- package/dist/__tests__/doctor.runner.test.js +3 -15
- package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
- package/dist/__tests__/env-sanitizer.test.js +45 -0
- package/dist/__tests__/exec-approval.test.js +61 -0
- package/dist/__tests__/fetch-tool.test.d.ts +1 -0
- package/dist/__tests__/fetch-tool.test.js +85 -0
- package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
- package/dist/__tests__/gateway-status-auth.test.js +72 -0
- package/dist/__tests__/heartbeat.test.js +3 -3
- package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
- package/dist/__tests__/interactive-sessions.test.js +96 -0
- package/dist/__tests__/langfuse.test.js +6 -18
- package/dist/__tests__/model-selection.test.js +3 -4
- package/dist/__tests__/providers-init.test.js +2 -8
- package/dist/__tests__/providers-routing.test.js +1 -1
- package/dist/__tests__/providers-utils.test.js +13 -3
- package/dist/__tests__/sessions.test.js +14 -10
- package/dist/__tests__/setup.test.js +12 -29
- package/dist/__tests__/skills.test.js +10 -7
- package/dist/__tests__/stream-formatter.test.d.ts +1 -0
- package/dist/__tests__/stream-formatter.test.js +114 -0
- package/dist/__tests__/token-efficiency.test.js +131 -15
- package/dist/__tests__/tool-loop.test.d.ts +4 -0
- package/dist/__tests__/tool-loop.test.js +505 -0
- package/dist/__tests__/tools.test.js +101 -276
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +14 -0
- package/dist/__tests__/voice.test.js +21 -0
- package/dist/agent.js +35 -4
- package/dist/api.js +113 -37
- package/dist/channels/discord/attachments.d.ts +50 -0
- package/dist/channels/discord/attachments.js +137 -0
- package/dist/channels/discord/delegation.d.ts +5 -0
- package/dist/channels/discord/delegation.js +136 -0
- package/dist/channels/discord/handlers.js +694 -7
- package/dist/channels/discord/index.d.ts +16 -1
- package/dist/channels/discord/index.js +64 -1
- package/dist/channels/discord/thread-agents.d.ts +54 -0
- package/dist/channels/discord/thread-agents.js +323 -0
- package/dist/channels/discord/threads.d.ts +58 -0
- package/dist/channels/discord/threads.js +192 -0
- package/dist/channels/discord/types.js +4 -2
- package/dist/channels/discord/utils.d.ts +16 -0
- package/dist/channels/discord/utils.js +86 -6
- package/dist/channels/telegram/index.d.ts +1 -1
- package/dist/channels/telegram/types.js +1 -1
- package/dist/channels/telegram/utils.js +9 -3
- package/dist/channels.d.ts +1 -1
- package/dist/cli.js +20 -400
- package/dist/code-agents/executor.d.ts +1 -1
- package/dist/code-agents/executor.js +101 -45
- package/dist/code-agents/index.d.ts +2 -7
- package/dist/code-agents/index.js +111 -80
- package/dist/code-agents/interactive-resume.d.ts +6 -0
- package/dist/code-agents/interactive-resume.js +98 -0
- package/dist/code-agents/interactive-sessions.d.ts +20 -0
- package/dist/code-agents/interactive-sessions.js +132 -0
- package/dist/code-agents/parser.js +5 -1
- package/dist/code-agents/registry.d.ts +7 -1
- package/dist/code-agents/registry.js +11 -23
- package/dist/code-agents/stream-formatter.d.ts +8 -0
- package/dist/code-agents/stream-formatter.js +92 -0
- package/dist/code-agents/types.d.ts +16 -24
- package/dist/code-agents/utils.d.ts +35 -11
- package/dist/code-agents/utils.js +349 -95
- package/dist/code-agents/worktrees.d.ts +37 -0
- package/dist/code-agents/worktrees.js +116 -0
- package/dist/config.d.ts +2 -4
- package/dist/config.js +123 -23
- package/dist/cron.d.ts +1 -6
- package/dist/cron.js +175 -82
- package/dist/dashboard/assets/index-B345aOO-.js +65 -0
- package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/digests.d.ts +1 -0
- package/dist/digests.js +132 -42
- package/dist/doctor/checks.d.ts +0 -3
- package/dist/doctor/checks.js +1 -108
- package/dist/doctor/runner.js +1 -4
- package/dist/env-sanitizer.d.ts +2 -0
- package/dist/env-sanitizer.js +61 -0
- package/dist/exec-approval.d.ts +11 -1
- package/dist/exec-approval.js +17 -4
- package/dist/gateway.d.ts +3 -1
- package/dist/gateway.js +17 -7
- package/dist/heartbeat.js +1 -6
- package/dist/langfuse.js +3 -29
- package/dist/model-selection.js +3 -1
- package/dist/providers/adapter.d.ts +118 -0
- package/dist/providers/adapter.js +6 -0
- package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
- package/dist/providers/adapters/anthropic-adapter.js +204 -0
- package/dist/providers/adapters/codex-adapter.d.ts +26 -0
- package/dist/providers/adapters/codex-adapter.js +203 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +10 -272
- package/dist/providers/codex.d.ts +21 -0
- package/dist/providers/codex.js +149 -330
- package/dist/providers/content.d.ts +1 -1
- package/dist/providers/content.js +2 -2
- package/dist/providers/context-manager.d.ts +18 -6
- package/dist/providers/context-manager.js +199 -223
- package/dist/providers/index.d.ts +9 -1
- package/dist/providers/index.js +73 -64
- package/dist/providers/loop-utils.d.ts +20 -0
- package/dist/providers/loop-utils.js +30 -0
- package/dist/providers/tool-loop.d.ts +12 -0
- package/dist/providers/tool-loop.js +251 -0
- package/dist/providers/utils.d.ts +19 -3
- package/dist/providers/utils.js +100 -29
- package/dist/secure-store.d.ts +8 -0
- package/dist/secure-store.js +80 -0
- package/dist/service.js +3 -28
- package/dist/sessions.d.ts +3 -0
- package/dist/sessions.js +147 -18
- package/dist/setup-templates.js +13 -25
- package/dist/setup.d.ts +10 -6
- package/dist/setup.js +84 -292
- package/dist/skills.js +3 -11
- package/dist/tools/agent-delegation.d.ts +19 -0
- package/dist/tools/agent-delegation.js +49 -0
- package/dist/tools/bash-tool.js +89 -34
- package/dist/tools/definitions.d.ts +199 -302
- package/dist/tools/definitions.js +70 -123
- package/dist/tools/execute-context.d.ts +13 -4
- package/dist/tools/fetch-tool.js +109 -13
- package/dist/tools/file-tools.js +7 -1
- package/dist/tools.d.ts +7 -7
- package/dist/tools.js +133 -151
- package/dist/types.d.ts +37 -30
- package/dist/utils.js +4 -6
- package/dist/voice.d.ts +1 -1
- package/dist/voice.js +17 -4
- package/package.json +33 -23
- package/templates/TOOLS.md +0 -27
- package/dist/__tests__/audit.test.js +0 -122
- package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
- package/dist/__tests__/code-agents-sandbox.test.js +0 -163
- package/dist/__tests__/orchestrator.test.js +0 -425
- package/dist/__tests__/sandbox-bridge.test.js +0 -116
- package/dist/__tests__/sandbox-manager.test.js +0 -144
- package/dist/__tests__/sandbox-mount-security.test.js +0 -139
- package/dist/__tests__/sandbox-runtime.test.js +0 -176
- package/dist/__tests__/subagent.test.js +0 -240
- package/dist/__tests__/telegram.test.js +0 -42
- package/dist/code-agents/orchestrator.d.ts +0 -29
- package/dist/code-agents/orchestrator.js +0 -694
- package/dist/code-agents/worktree.d.ts +0 -40
- package/dist/code-agents/worktree.js +0 -215
- package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
- package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
- package/dist/dashboard.d.ts +0 -8
- package/dist/dashboard.js +0 -4071
- package/dist/discord.d.ts +0 -8
- package/dist/discord.js +0 -792
- package/dist/mcp-context-a8c.d.ts +0 -13
- package/dist/mcp-context-a8c.js +0 -34
- package/dist/orchestrator.d.ts +0 -15
- package/dist/orchestrator.js +0 -676
- package/dist/providers/openai.d.ts +0 -10
- package/dist/providers/openai.js +0 -355
- package/dist/sandbox/bridge.d.ts +0 -5
- package/dist/sandbox/bridge.js +0 -63
- package/dist/sandbox/index.d.ts +0 -5
- package/dist/sandbox/index.js +0 -4
- package/dist/sandbox/manager.d.ts +0 -7
- package/dist/sandbox/manager.js +0 -100
- package/dist/sandbox/mount-security.d.ts +0 -12
- package/dist/sandbox/mount-security.js +0 -122
- package/dist/sandbox/runtime.d.ts +0 -39
- package/dist/sandbox/runtime.js +0 -192
- package/dist/sandbox-utils.d.ts +0 -6
- package/dist/sandbox-utils.js +0 -36
- package/dist/subagent.d.ts +0 -19
- package/dist/subagent.js +0 -407
- package/dist/telegram.d.ts +0 -2
- package/dist/telegram.js +0 -11
- package/dist/tools/browser-tool.d.ts +0 -3
- package/dist/tools/browser-tool.js +0 -266
- package/sandbox/Dockerfile +0 -40
- /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
- /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
- /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
- /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
package/dist/providers/utils.js
CHANGED
|
@@ -5,17 +5,7 @@ import { homedir } from 'os';
|
|
|
5
5
|
// Anti-hallucination instructions injected between the Claude Code identity
|
|
6
6
|
// block and the actual system prompt. Prevents the model from roleplaying
|
|
7
7
|
// Claude Code's full behavior (XML tool calls, fabricated output, etc.)
|
|
8
|
-
export const TOOL_GUARD = `You are
|
|
9
|
-
You are NOT the full Claude Code CLI. Do NOT roleplay as Claude Code.
|
|
10
|
-
|
|
11
|
-
## Tool Rules
|
|
12
|
-
- You have ONLY the tools provided via the API tool_use mechanism.
|
|
13
|
-
- Tool names are case-sensitive. Call tools exactly as listed.
|
|
14
|
-
- NEVER output tool calls as text/XML/JSON. Use the API tool_use mechanism only.
|
|
15
|
-
- NEVER fabricate tool results or file contents. If you haven't read a file, say so.
|
|
16
|
-
- NEVER invent tools that are not in your tool list (no str_replace_editor, no view, etc.)
|
|
17
|
-
- If a Browser tool is available, you DO have web-browsing access via that tool. Use it instead of claiming you can't browse.
|
|
18
|
-
- If you need information, use a tool to get it. Do not guess.`;
|
|
8
|
+
export const TOOL_GUARD = `You are SkimpyClaw (NOT Claude Code CLI). Use only API tool_use — never text/XML tools. Never fabricate results.`;
|
|
19
9
|
// Track if using OAuth (requires Claude Code identity)
|
|
20
10
|
let usingOAuth = false;
|
|
21
11
|
export function setUsingOAuth(value) {
|
|
@@ -86,7 +76,7 @@ export function toOpenAITools(toolDefs) {
|
|
|
86
76
|
function: {
|
|
87
77
|
name: t.name,
|
|
88
78
|
description: t.description,
|
|
89
|
-
parameters: t.input_schema,
|
|
79
|
+
parameters: t.input_schema && t.input_schema.type ? t.input_schema : { type: 'object', properties: {} },
|
|
90
80
|
},
|
|
91
81
|
}));
|
|
92
82
|
}
|
|
@@ -110,9 +100,12 @@ function migrateDeprecatedModelSpec(modelSpec) {
|
|
|
110
100
|
else if (/^claude[-.]3[-.]5[-.]haiku(?:[-_.].*)?$/i.test(bare) || bare === 'claude-haiku') {
|
|
111
101
|
migratedBare = 'claude-haiku-4-5';
|
|
112
102
|
}
|
|
113
|
-
else if (/^claude[-.]opus[-.]4
|
|
103
|
+
else if (/^claude[-.]opus[-.]4[-_.]6$/i.test(bare)) {
|
|
114
104
|
migratedBare = 'claude-opus-4-6';
|
|
115
105
|
}
|
|
106
|
+
else if (/^claude[-.]opus[-.]4$/i.test(bare)) {
|
|
107
|
+
migratedBare = 'claude-opus-4-7';
|
|
108
|
+
}
|
|
116
109
|
if (migratedBare === bare)
|
|
117
110
|
return modelSpec;
|
|
118
111
|
return hasProvider ? `${provider}/${migratedBare}` : migratedBare;
|
|
@@ -142,22 +135,21 @@ export function shouldUseCodexAliasProvider(provider, isCodexModel, codexProvide
|
|
|
142
135
|
* Get provider name from model spec.
|
|
143
136
|
*/
|
|
144
137
|
export function getProvider(model) {
|
|
145
|
-
// Explicit prefix: "openrouter/google/gemini-2.0-flash" → "openrouter"
|
|
146
138
|
const slashIdx = model.indexOf('/');
|
|
147
139
|
if (slashIdx > 0) {
|
|
148
140
|
const prefix = model.slice(0, slashIdx);
|
|
149
|
-
// Known Anthropic prefix
|
|
150
141
|
if (prefix === 'anthropic')
|
|
151
142
|
return 'anthropic';
|
|
152
|
-
|
|
143
|
+
if (prefix === 'codex')
|
|
144
|
+
return 'codex';
|
|
145
|
+
if (prefix === 'openai' && /\bcodex\b/i.test(model.slice(slashIdx + 1)))
|
|
146
|
+
return 'openai';
|
|
153
147
|
return prefix;
|
|
154
148
|
}
|
|
155
|
-
// No prefix: infer from model name
|
|
156
149
|
if (model.includes('claude'))
|
|
157
150
|
return 'anthropic';
|
|
158
|
-
if (model.includes('gpt'))
|
|
159
|
-
return '
|
|
160
|
-
// Default to anthropic
|
|
151
|
+
if (model.includes('gpt') || model.includes('codex'))
|
|
152
|
+
return 'codex';
|
|
161
153
|
return 'anthropic';
|
|
162
154
|
}
|
|
163
155
|
/**
|
|
@@ -186,7 +178,7 @@ export function stripProvider(model, openaiClients, responsesApiProviders) {
|
|
|
186
178
|
* a scratch file and replaced with a compact summary + file path.
|
|
187
179
|
* Outputs below this are returned inline (no file I/O overhead).
|
|
188
180
|
*/
|
|
189
|
-
const MASK_THRESHOLD =
|
|
181
|
+
const MASK_THRESHOLD = 4000; // ~1000 tokens — must be high enough for typical file reads
|
|
190
182
|
/**
|
|
191
183
|
* Mask large tool outputs by writing to scratch files.
|
|
192
184
|
* Returns the original result if small enough, or a summary + file path if large.
|
|
@@ -196,18 +188,16 @@ export function truncateToolResult(result, _maxBytes = 10_240) {
|
|
|
196
188
|
if (result.length <= MASK_THRESHOLD)
|
|
197
189
|
return result;
|
|
198
190
|
try {
|
|
199
|
-
const scratchDir = join(homedir(), '.skimpyclaw', '
|
|
191
|
+
const scratchDir = join(homedir(), '.skimpyclaw', 's');
|
|
200
192
|
if (!existsSync(scratchDir))
|
|
201
193
|
mkdirSync(scratchDir, { recursive: true });
|
|
202
|
-
const id =
|
|
203
|
-
const filePath = join(scratchDir,
|
|
194
|
+
const id = Math.random().toString(36).slice(2, 5);
|
|
195
|
+
const filePath = join(scratchDir, id);
|
|
204
196
|
writeFileSync(filePath, result);
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
const tail = result.slice(-500);
|
|
208
|
-
const summary = head + (result.length > 1000 ? '\n...\n' + tail : '');
|
|
197
|
+
const home = homedir().replace(/\/+$/, '');
|
|
198
|
+
const shortFP = filePath.startsWith(home) ? '~' + filePath.slice(home.length) : filePath;
|
|
209
199
|
console.log(`[context-manager] Masked ${result.length} chars → ${filePath}`);
|
|
210
|
-
return
|
|
200
|
+
return `→${shortFP}`;
|
|
211
201
|
}
|
|
212
202
|
catch (err) {
|
|
213
203
|
// Fallback: simple truncation
|
|
@@ -215,6 +205,86 @@ export function truncateToolResult(result, _maxBytes = 10_240) {
|
|
|
215
205
|
return result.slice(0, MASK_THRESHOLD) + `\n\n[Truncated: ${result.length} chars total]`;
|
|
216
206
|
}
|
|
217
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Write full output to a scratch file. Returns the file path, or null on failure.
|
|
210
|
+
*/
|
|
211
|
+
function writeScratchFile(result) {
|
|
212
|
+
try {
|
|
213
|
+
const scratchDir = join(homedir(), '.skimpyclaw', 's');
|
|
214
|
+
if (!existsSync(scratchDir))
|
|
215
|
+
mkdirSync(scratchDir, { recursive: true });
|
|
216
|
+
const id = Math.random().toString(36).slice(2, 5);
|
|
217
|
+
const filePath = join(scratchDir, id);
|
|
218
|
+
writeFileSync(filePath, result);
|
|
219
|
+
return filePath;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Structured split tool results: generates a semantic summary based on tool type.
|
|
227
|
+
* For small results (<= MASK_THRESHOLD), returns unchanged.
|
|
228
|
+
* For large results, writes full output to scratch file and returns a compact,
|
|
229
|
+
* tool-aware summary with the scratch file path.
|
|
230
|
+
*/
|
|
231
|
+
export function splitToolResult(toolName, toolInput, result) {
|
|
232
|
+
// Never split scratch file reads — these are already split results being retrieved
|
|
233
|
+
const nameLower0 = toolName.toLowerCase();
|
|
234
|
+
if (nameLower0 === 'read' || nameLower0 === 'read_file') {
|
|
235
|
+
const filePath = toolInput.file_path || toolInput.path || '';
|
|
236
|
+
const scratchPrefix = join(homedir(), '.skimpyclaw', 's');
|
|
237
|
+
if (typeof filePath === 'string' && (filePath.startsWith(scratchPrefix) || filePath.startsWith('~/.skimpyclaw/s/') || filePath.startsWith('~/.skimpyclaw/scratch/'))) {
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (result.length <= MASK_THRESHOLD)
|
|
242
|
+
return result;
|
|
243
|
+
const scratchPath = writeScratchFile(result);
|
|
244
|
+
if (!scratchPath) {
|
|
245
|
+
// Fallback to legacy truncation
|
|
246
|
+
return truncateToolResult(result);
|
|
247
|
+
}
|
|
248
|
+
// Use tilde-prefixed path in output to save tokens
|
|
249
|
+
const home = homedir().replace(/\/+$/, '');
|
|
250
|
+
const shortPath = scratchPath.startsWith(home) ? '~' + scratchPath.slice(home.length) : scratchPath;
|
|
251
|
+
console.log(`[context-manager] Split ${result.length} chars (${toolName}) → ${scratchPath}`);
|
|
252
|
+
const nameLower = toolName.toLowerCase();
|
|
253
|
+
// Bash: include exit code, errors, and preview
|
|
254
|
+
if (nameLower === 'bash') {
|
|
255
|
+
const exitMatch = result.match(/exit code[:\s]+(\d+)/i);
|
|
256
|
+
const exitInfo = exitMatch && exitMatch[1] !== '0' ? `exit=${exitMatch[1]}\n` : '';
|
|
257
|
+
const lines = result.split('\n');
|
|
258
|
+
const errLines = lines.filter(l => /^(error|fatal|ERR!)/i.test(l.trim()));
|
|
259
|
+
const errNote = errLines.length > 0 ? errLines.slice(0, 3).join('\n') + '\n' : '';
|
|
260
|
+
const preview = result.slice(0, 600);
|
|
261
|
+
const truncNote = result.length > 600 ? `\n... (${result.length} chars total)` : '';
|
|
262
|
+
return `${exitInfo}${errNote}${preview}${truncNote}\nFull output saved to ${scratchPath} — use Read tool to retrieve.`;
|
|
263
|
+
}
|
|
264
|
+
// All other tools: include preview so the model has usable data
|
|
265
|
+
const preview = result.slice(0, 800);
|
|
266
|
+
const truncNote = result.length > 800 ? `\n... (${result.length} chars total)` : '';
|
|
267
|
+
return `${preview}${truncNote}\nFull output saved to ${scratchPath} — use Read tool to retrieve.`;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Compact old tool results in Anthropic-format messages.
|
|
271
|
+
* Replaces tool_result content with '✓' for all results except the last
|
|
272
|
+
* `keepRecent` messages. The model has already processed these results,
|
|
273
|
+
* so we only need to preserve the structure (tool_use_id matching).
|
|
274
|
+
*
|
|
275
|
+
* Mutates the messages array in place for efficiency.
|
|
276
|
+
*/
|
|
277
|
+
export function compactOldResults(messages, keepRecent = 2) {
|
|
278
|
+
if (messages.length <= keepRecent)
|
|
279
|
+
return;
|
|
280
|
+
const cutoff = messages.length - keepRecent;
|
|
281
|
+
// Remove old messages entirely — keep only recent
|
|
282
|
+
messages.splice(0, cutoff);
|
|
283
|
+
// Ensure first message is user role (API requirement)
|
|
284
|
+
if (messages.length > 0 && messages[0].role !== 'user') {
|
|
285
|
+
messages.unshift({ role: 'user', content: '·' });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
218
288
|
/**
|
|
219
289
|
* Build thinking config based on thinking level.
|
|
220
290
|
*/
|
|
@@ -225,6 +295,7 @@ export function buildThinkingConfig(thinking) {
|
|
|
225
295
|
low: 2048,
|
|
226
296
|
medium: 8192,
|
|
227
297
|
high: 16384,
|
|
298
|
+
xhigh: 32768,
|
|
228
299
|
};
|
|
229
300
|
const budget = budgetTokens[thinking] || 2048;
|
|
230
301
|
return {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function secureStoreAvailable(): {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
detail?: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function clearSecureStoreAvailabilityCacheForTests(): void;
|
|
6
|
+
export declare function requireSecureStore(purpose: string): void;
|
|
7
|
+
export declare function getSecureValue(service: string, account: string): string | null;
|
|
8
|
+
export declare function setSecureValue(service: string, account: string, value: string): void;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
function runSecurityCommand(args) {
|
|
3
|
+
const result = spawnSync('security', args, {
|
|
4
|
+
encoding: 'utf-8',
|
|
5
|
+
timeout: 5000,
|
|
6
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
7
|
+
});
|
|
8
|
+
if (result.error) {
|
|
9
|
+
return { ok: false, detail: result.error.message };
|
|
10
|
+
}
|
|
11
|
+
if (result.status !== 0) {
|
|
12
|
+
const detail = (result.stderr || result.stdout || '').trim() || `exit ${result.status}`;
|
|
13
|
+
return { ok: false, detail };
|
|
14
|
+
}
|
|
15
|
+
return { ok: true, detail: (result.stdout || '').trim() };
|
|
16
|
+
}
|
|
17
|
+
let availabilityCache = {
|
|
18
|
+
checked: false,
|
|
19
|
+
ok: false,
|
|
20
|
+
};
|
|
21
|
+
export function secureStoreAvailable() {
|
|
22
|
+
if (availabilityCache.checked) {
|
|
23
|
+
return { ok: availabilityCache.ok, detail: availabilityCache.detail };
|
|
24
|
+
}
|
|
25
|
+
if (process.platform !== 'darwin') {
|
|
26
|
+
availabilityCache = {
|
|
27
|
+
checked: true,
|
|
28
|
+
ok: false,
|
|
29
|
+
detail: 'macOS Keychain is only available on darwin',
|
|
30
|
+
};
|
|
31
|
+
return { ok: false, detail: availabilityCache.detail };
|
|
32
|
+
}
|
|
33
|
+
const probe = runSecurityCommand(['list-keychains']);
|
|
34
|
+
availabilityCache = {
|
|
35
|
+
checked: true,
|
|
36
|
+
ok: probe.ok,
|
|
37
|
+
detail: probe.ok ? undefined : probe.detail,
|
|
38
|
+
};
|
|
39
|
+
return { ok: availabilityCache.ok, detail: availabilityCache.detail };
|
|
40
|
+
}
|
|
41
|
+
export function clearSecureStoreAvailabilityCacheForTests() {
|
|
42
|
+
availabilityCache = { checked: false, ok: false };
|
|
43
|
+
}
|
|
44
|
+
export function requireSecureStore(purpose) {
|
|
45
|
+
const availability = secureStoreAvailable();
|
|
46
|
+
if (!availability.ok) {
|
|
47
|
+
throw new Error(`[secure-store] ${purpose} requires macOS Keychain, but it is unavailable${availability.detail ? `: ${availability.detail}` : ''}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function getSecureValue(service, account) {
|
|
51
|
+
requireSecureStore('Secret retrieval');
|
|
52
|
+
const result = runSecurityCommand([
|
|
53
|
+
'find-generic-password',
|
|
54
|
+
'-s',
|
|
55
|
+
service,
|
|
56
|
+
'-a',
|
|
57
|
+
account,
|
|
58
|
+
'-w',
|
|
59
|
+
]);
|
|
60
|
+
if (!result.ok) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return (result.detail || '').trim();
|
|
64
|
+
}
|
|
65
|
+
export function setSecureValue(service, account, value) {
|
|
66
|
+
requireSecureStore('Secret storage');
|
|
67
|
+
const result = runSecurityCommand([
|
|
68
|
+
'add-generic-password',
|
|
69
|
+
'-U',
|
|
70
|
+
'-s',
|
|
71
|
+
service,
|
|
72
|
+
'-a',
|
|
73
|
+
account,
|
|
74
|
+
'-w',
|
|
75
|
+
value,
|
|
76
|
+
]);
|
|
77
|
+
if (!result.ok) {
|
|
78
|
+
throw new Error(`[secure-store] Failed writing ${service}/${account}: ${result.detail || 'unknown error'}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
package/dist/service.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
1
4
|
import { createGateway } from './gateway.js';
|
|
2
5
|
import { initCron, stopCron } from './cron.js';
|
|
3
6
|
import { initHeartbeat, stopHeartbeat } from './heartbeat.js';
|
|
@@ -5,13 +8,9 @@ import { initActiveChannel, startActiveChannel, stopActiveChannel } from './chan
|
|
|
5
8
|
import { initProviders } from './agent.js';
|
|
6
9
|
import { initLangfuse, shutdownLangfuse } from './langfuse.js';
|
|
7
10
|
import { restoreCodeAgentTasks, setCodeAgentConfig } from './tools.js';
|
|
8
|
-
import { releaseAll, cleanupOrphans, setRuntime, probeRuntime } from './sandbox/index.js';
|
|
9
11
|
/** Clean up old scratch files (observation masking). Keeps files < 24h. */
|
|
10
12
|
function cleanupScratch() {
|
|
11
13
|
try {
|
|
12
|
-
const { readdirSync, statSync, unlinkSync } = require('fs');
|
|
13
|
-
const { join } = require('path');
|
|
14
|
-
const { homedir } = require('os');
|
|
15
14
|
const dir = join(homedir(), '.skimpyclaw', 'scratch');
|
|
16
15
|
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
17
16
|
let count = 0;
|
|
@@ -37,29 +36,6 @@ export async function startRuntime(config) {
|
|
|
37
36
|
restoreCodeAgentTasks();
|
|
38
37
|
setCodeAgentConfig(config);
|
|
39
38
|
cleanupScratch();
|
|
40
|
-
// Initialize sandbox runtime if configured — auto-disable if no runtime available
|
|
41
|
-
if (config.sandbox?.enabled) {
|
|
42
|
-
const detected = probeRuntime(config.sandbox.runtime);
|
|
43
|
-
if (detected) {
|
|
44
|
-
setRuntime(detected);
|
|
45
|
-
if (detected !== config.sandbox.runtime) {
|
|
46
|
-
console.log(`[sandbox] Configured runtime "${config.sandbox.runtime}" not found, using "${detected}" instead`);
|
|
47
|
-
}
|
|
48
|
-
// Clean up orphaned sandbox containers from previous runs
|
|
49
|
-
try {
|
|
50
|
-
const count = await cleanupOrphans();
|
|
51
|
-
if (count > 0)
|
|
52
|
-
console.log(`[sandbox] Cleaned up ${count} orphaned container(s)`);
|
|
53
|
-
}
|
|
54
|
-
catch (err) {
|
|
55
|
-
console.warn('[sandbox] Failed to clean up orphaned containers:', err instanceof Error ? err.message : err);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
console.warn('[sandbox] No container runtime found (docker/container not installed). Sandbox disabled.');
|
|
60
|
-
config.sandbox.enabled = false;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
39
|
const port = smokeTest ? (parseInt(process.env.SKIMPYCLAW_SMOKE_PORT || '19999', 10)) : config.gateway.port;
|
|
64
40
|
const gateway = await createGateway(config);
|
|
65
41
|
const host = config.gateway.host ?? '127.0.0.1';
|
|
@@ -77,7 +53,6 @@ export async function startRuntime(config) {
|
|
|
77
53
|
config,
|
|
78
54
|
gateway,
|
|
79
55
|
stop: async () => {
|
|
80
|
-
await releaseAll();
|
|
81
56
|
stopCron();
|
|
82
57
|
stopHeartbeat();
|
|
83
58
|
await stopActiveChannel();
|
package/dist/sessions.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ChatMessage } from './types.js';
|
|
2
2
|
export declare const MAX_HISTORY_PAIRS = 5;
|
|
3
|
+
/** Reset session key cache between tests to prevent cross-test leakage. */
|
|
4
|
+
export declare function clearSessionKeyCacheForTests(): void;
|
|
3
5
|
/** Override sessions directory. Used in tests. */
|
|
4
6
|
export declare function setSessionsDir(dir: string): void;
|
|
5
7
|
export interface SessionEntry {
|
|
@@ -9,6 +11,7 @@ export interface SessionEntry {
|
|
|
9
11
|
summary?: true;
|
|
10
12
|
proactive?: true;
|
|
11
13
|
}
|
|
14
|
+
export declare function readSessionEntriesFromFile(filePath: string): SessionEntry[];
|
|
12
15
|
/**
|
|
13
16
|
* Load last MAX_HISTORY_PAIRS exchanges from disk.
|
|
14
17
|
* Returns [] if file doesn't exist.
|
package/dist/sessions.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
// Persistent conversation history —
|
|
1
|
+
// Persistent conversation history — encrypted JSONL per chat
|
|
2
2
|
// Storage: ~/.skimpyclaw/sessions/{platform}-{chatId}.jsonl
|
|
3
|
-
// Each line: {"ts":"...","user":"...","assistant":"..."}
|
|
4
3
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, } from 'fs';
|
|
5
4
|
import { join } from 'path';
|
|
6
5
|
import { homedir } from 'os';
|
|
6
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto';
|
|
7
|
+
import { getSecureValue, setSecureValue } from './secure-store.js';
|
|
7
8
|
let SESSIONS_DIR = join(homedir(), '.skimpyclaw', 'sessions');
|
|
8
9
|
export const MAX_HISTORY_PAIRS = 5;
|
|
10
|
+
const ENCRYPTED_PREFIX = 'ENCv1:';
|
|
11
|
+
const SESSION_KEY_SERVICE = 'skimpyclaw-history';
|
|
12
|
+
const SESSION_KEY_ACCOUNT = 'session-key-v1';
|
|
13
|
+
const SESSION_KEY_ENV = 'SKIMPYCLAW_HISTORY_KEY';
|
|
14
|
+
let sessionKeyCache = null;
|
|
15
|
+
let warnedUnavailable = false;
|
|
16
|
+
/** Reset session key cache between tests to prevent cross-test leakage. */
|
|
17
|
+
export function clearSessionKeyCacheForTests() {
|
|
18
|
+
sessionKeyCache = null;
|
|
19
|
+
warnedUnavailable = false;
|
|
20
|
+
}
|
|
9
21
|
/** Override sessions directory. Used in tests. */
|
|
10
22
|
export function setSessionsDir(dir) {
|
|
11
23
|
SESSIONS_DIR = dir;
|
|
@@ -18,28 +30,142 @@ function ensureDir() {
|
|
|
18
30
|
mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
19
31
|
}
|
|
20
32
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
33
|
+
function deriveKeyFromEnv(raw) {
|
|
34
|
+
const trimmed = raw.trim();
|
|
35
|
+
if (/^[a-f0-9]{64}$/i.test(trimmed)) {
|
|
36
|
+
return Buffer.from(trimmed, 'hex');
|
|
37
|
+
}
|
|
38
|
+
const base64 = Buffer.from(trimmed, 'base64');
|
|
39
|
+
if (base64.length === 32) {
|
|
40
|
+
return base64;
|
|
41
|
+
}
|
|
42
|
+
return createHash('sha256').update(trimmed).digest();
|
|
43
|
+
}
|
|
44
|
+
function getSessionKey() {
|
|
45
|
+
if (sessionKeyCache) {
|
|
46
|
+
return sessionKeyCache;
|
|
47
|
+
}
|
|
48
|
+
const envKey = process.env[SESSION_KEY_ENV];
|
|
49
|
+
if (envKey) {
|
|
50
|
+
sessionKeyCache = deriveKeyFromEnv(envKey);
|
|
51
|
+
return sessionKeyCache;
|
|
52
|
+
}
|
|
53
|
+
if (process.platform !== 'darwin') {
|
|
54
|
+
throw new Error(`[sessions] Encrypted history requires macOS Keychain or ${SESSION_KEY_ENV} on non-macOS environments.`);
|
|
55
|
+
}
|
|
56
|
+
const existing = getSecureValue(SESSION_KEY_SERVICE, SESSION_KEY_ACCOUNT);
|
|
57
|
+
if (existing) {
|
|
58
|
+
sessionKeyCache = deriveKeyFromEnv(existing);
|
|
59
|
+
return sessionKeyCache;
|
|
60
|
+
}
|
|
61
|
+
const generated = randomBytes(32).toString('base64');
|
|
62
|
+
setSecureValue(SESSION_KEY_SERVICE, SESSION_KEY_ACCOUNT, generated);
|
|
63
|
+
sessionKeyCache = Buffer.from(generated, 'base64');
|
|
64
|
+
return sessionKeyCache;
|
|
65
|
+
}
|
|
66
|
+
function encryptEntry(entry) {
|
|
67
|
+
const key = getSessionKey();
|
|
68
|
+
const iv = randomBytes(12);
|
|
69
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
70
|
+
const plaintext = Buffer.from(JSON.stringify(entry), 'utf-8');
|
|
71
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
72
|
+
const tag = cipher.getAuthTag();
|
|
73
|
+
const payload = Buffer.concat([iv, tag, ciphertext]).toString('base64');
|
|
74
|
+
return `${ENCRYPTED_PREFIX}${payload}`;
|
|
75
|
+
}
|
|
76
|
+
function decryptEntry(line) {
|
|
77
|
+
const key = getSessionKey();
|
|
78
|
+
const payload = Buffer.from(line.slice(ENCRYPTED_PREFIX.length), 'base64');
|
|
79
|
+
if (payload.length < 29) {
|
|
80
|
+
throw new Error('Encrypted session entry payload is invalid');
|
|
81
|
+
}
|
|
82
|
+
const iv = payload.subarray(0, 12);
|
|
83
|
+
const tag = payload.subarray(12, 28);
|
|
84
|
+
const ciphertext = payload.subarray(28);
|
|
85
|
+
const decipher = createDecipheriv('aes-256-gcm', key, iv);
|
|
86
|
+
decipher.setAuthTag(tag);
|
|
87
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf-8');
|
|
88
|
+
return JSON.parse(plaintext);
|
|
89
|
+
}
|
|
90
|
+
function parseLine(line) {
|
|
91
|
+
if (!line)
|
|
92
|
+
return { entry: null, wasPlaintext: false };
|
|
93
|
+
try {
|
|
94
|
+
if (line.startsWith(ENCRYPTED_PREFIX)) {
|
|
95
|
+
return { entry: decryptEntry(line), wasPlaintext: false };
|
|
96
|
+
}
|
|
97
|
+
return { entry: JSON.parse(line), wasPlaintext: true };
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return { entry: null, wasPlaintext: false };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function canEncrypt() {
|
|
104
|
+
try {
|
|
105
|
+
getSessionKey();
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
if (!warnedUnavailable) {
|
|
110
|
+
warnedUnavailable = true;
|
|
111
|
+
console.error('[sessions] Encryption unavailable:', err instanceof Error ? err.message : String(err));
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function migratePlaintextFile(filePath, entries) {
|
|
117
|
+
if (!entries.length || !canEncrypt())
|
|
118
|
+
return;
|
|
119
|
+
try {
|
|
120
|
+
const encryptedLines = entries.map(entry => encryptEntry(entry));
|
|
121
|
+
writeFileSync(filePath, encryptedLines.join('\n') + '\n', 'utf-8');
|
|
122
|
+
console.log(`[sessions] Migrated plaintext history to encrypted format: ${filePath}`);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
console.error('[sessions] Failed to migrate session file:', err);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function appendEncryptedEntry(filePath, entry) {
|
|
129
|
+
if (!canEncrypt()) {
|
|
130
|
+
throw new Error('Encrypted session storage is unavailable. Configure macOS Keychain access or SKIMPYCLAW_HISTORY_KEY.');
|
|
131
|
+
}
|
|
132
|
+
appendFileSync(filePath, encryptEntry(entry) + '\n', 'utf-8');
|
|
133
|
+
}
|
|
134
|
+
export function readSessionEntriesFromFile(filePath) {
|
|
27
135
|
if (!existsSync(filePath))
|
|
28
136
|
return [];
|
|
29
137
|
try {
|
|
30
138
|
const content = readFileSync(filePath, 'utf-8');
|
|
31
139
|
const lines = content.trim().split('\n').filter(Boolean);
|
|
32
|
-
// Parse all valid lines
|
|
33
140
|
const entries = [];
|
|
141
|
+
let foundPlaintext = false;
|
|
34
142
|
for (const line of lines) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
entries.push(entry);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// Skip malformed lines
|
|
143
|
+
const parsed = parseLine(line);
|
|
144
|
+
if (parsed.entry) {
|
|
145
|
+
entries.push(parsed.entry);
|
|
146
|
+
if (parsed.wasPlaintext)
|
|
147
|
+
foundPlaintext = true;
|
|
41
148
|
}
|
|
42
149
|
}
|
|
150
|
+
if (foundPlaintext) {
|
|
151
|
+
migratePlaintextFile(filePath, entries);
|
|
152
|
+
}
|
|
153
|
+
return entries;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Load last MAX_HISTORY_PAIRS exchanges from disk.
|
|
161
|
+
* Returns [] if file doesn't exist.
|
|
162
|
+
*/
|
|
163
|
+
export async function loadHistory(platform, chatId) {
|
|
164
|
+
const filePath = sessionPath(platform, chatId);
|
|
165
|
+
if (!existsSync(filePath))
|
|
166
|
+
return [];
|
|
167
|
+
try {
|
|
168
|
+
const entries = readSessionEntriesFromFile(filePath);
|
|
43
169
|
// Take last MAX_HISTORY_PAIRS entries
|
|
44
170
|
const usable = entries.filter((entry) => {
|
|
45
171
|
if (entry.summary)
|
|
@@ -81,7 +207,7 @@ export async function saveExchange(platform, chatId, userMsg, assistantMsg) {
|
|
|
81
207
|
user: userMsg,
|
|
82
208
|
assistant: assistantMsg,
|
|
83
209
|
};
|
|
84
|
-
|
|
210
|
+
appendEncryptedEntry(filePath, entry);
|
|
85
211
|
}
|
|
86
212
|
catch (err) {
|
|
87
213
|
console.error('[sessions] Failed to save exchange:', err);
|
|
@@ -100,7 +226,7 @@ export async function saveProactiveMessage(platform, chatId, message) {
|
|
|
100
226
|
user: message,
|
|
101
227
|
proactive: true,
|
|
102
228
|
};
|
|
103
|
-
|
|
229
|
+
appendEncryptedEntry(filePath, entry);
|
|
104
230
|
}
|
|
105
231
|
catch (err) {
|
|
106
232
|
console.error('[sessions] Failed to save proactive message:', err);
|
|
@@ -120,7 +246,10 @@ export async function replaceWithSummary(platform, chatId, summary) {
|
|
|
120
246
|
assistant: summary,
|
|
121
247
|
summary: true,
|
|
122
248
|
};
|
|
123
|
-
|
|
249
|
+
if (!canEncrypt()) {
|
|
250
|
+
throw new Error('Encrypted session storage is unavailable. Configure macOS Keychain access or SKIMPYCLAW_HISTORY_KEY.');
|
|
251
|
+
}
|
|
252
|
+
writeFileSync(filePath, encryptEntry(entry) + '\n', 'utf-8');
|
|
124
253
|
}
|
|
125
254
|
catch (err) {
|
|
126
255
|
console.error('[sessions] Failed to replace with summary:', err);
|
package/dist/setup-templates.js
CHANGED
|
@@ -39,25 +39,24 @@ When asked about weather or generating a daily briefing:
|
|
|
39
39
|
`,
|
|
40
40
|
'web-search': `---
|
|
41
41
|
name: web-search
|
|
42
|
-
description: Search the web using
|
|
42
|
+
description: Search the web using Fetch against DuckDuckGo HTML results.
|
|
43
43
|
triggers: ["search", "look up", "google", "find online", "web search"]
|
|
44
44
|
priority: 50
|
|
45
45
|
---
|
|
46
46
|
|
|
47
47
|
When asked to search the web:
|
|
48
|
-
1. Use
|
|
49
|
-
2.
|
|
50
|
-
3. If a specific result looks promising,
|
|
48
|
+
1. Use Fetch on https://html.duckduckgo.com/html/?q=<URL-encoded query>
|
|
49
|
+
2. Read the returned text for relevant result links and snippets.
|
|
50
|
+
3. If a specific result looks promising, Fetch that URL and extract the relevant content.
|
|
51
51
|
4. Summarize findings concisely — include source URLs.
|
|
52
|
-
5. Close the browser when done.
|
|
53
52
|
|
|
54
53
|
Do NOT fabricate results. If the search returns nothing useful, say so.
|
|
55
54
|
`,
|
|
56
55
|
'duckduckgo-html-search': `---
|
|
57
56
|
name: duckduckgo-html-search
|
|
58
|
-
description: Search the web via DuckDuckGo HTML results using
|
|
57
|
+
description: Search the web via DuckDuckGo HTML results using Fetch
|
|
59
58
|
emoji: 🦆
|
|
60
|
-
tags: [search, web,
|
|
59
|
+
tags: [search, web, fetch]
|
|
61
60
|
priority: 45
|
|
62
61
|
enabled: true
|
|
63
62
|
---
|
|
@@ -67,15 +66,13 @@ enabled: true
|
|
|
67
66
|
Use this skill when the user asks for web search, source gathering, or lightweight browsing.
|
|
68
67
|
|
|
69
68
|
## Priority rule
|
|
70
|
-
DuckDuckGo HTML via
|
|
71
|
-
- Prefer DuckDuckGo first
|
|
72
|
-
- Use \\\`$web_search\\\` only when the user explicitly asks for it, DuckDuckGo is blocked, or Browser is unavailable.
|
|
69
|
+
DuckDuckGo HTML via Fetch is the default search path.
|
|
70
|
+
- Prefer DuckDuckGo first.
|
|
73
71
|
|
|
74
72
|
## Default workflow
|
|
75
73
|
1. Build query URL: \\\`https://duckduckgo.com/html/?q=<urlencoded query>\\\`
|
|
76
|
-
2.
|
|
77
|
-
3.
|
|
78
|
-
4. Extract results using one Browser \\\`evaluate\\\` call when possible.
|
|
74
|
+
2. Fetch the URL.
|
|
75
|
+
3. Extract result titles, URLs, and snippets from the returned HTML/text when possible.
|
|
79
76
|
5. Return only actually extracted items (never pad count).
|
|
80
77
|
|
|
81
78
|
## Extraction requirements
|
|
@@ -92,13 +89,6 @@ If a field is missing, set it to \\\`UNAVAILABLE\\\`.
|
|
|
92
89
|
- Never mix real and invented entries.
|
|
93
90
|
- Include source URLs in output.
|
|
94
91
|
|
|
95
|
-
## Browser strategy
|
|
96
|
-
- Prefer one-page extraction via \\\`evaluate\\\`:
|
|
97
|
-
- Collect \\\`a.result__a\\\` for title + href
|
|
98
|
-
- Collect nearby snippet nodes (\\\`.result__snippet\\\`) when present
|
|
99
|
-
- Use minimal actions: open → waitFor → evaluate → optional screenshot.
|
|
100
|
-
- If selectors change, fallback to visible text extraction and clearly mark reduced confidence.
|
|
101
|
-
|
|
102
92
|
## Output format (concise)
|
|
103
93
|
- Query used
|
|
104
94
|
- Result count actually extracted
|
|
@@ -148,7 +138,7 @@ export function buildStarterCronJobs(starters) {
|
|
|
148
138
|
jobs.push({
|
|
149
139
|
id: 'memory-trim',
|
|
150
140
|
name: 'Memory Trim',
|
|
151
|
-
model: 'claude-
|
|
141
|
+
model: 'anthropic/claude-haiku-4-5',
|
|
152
142
|
schedule: {
|
|
153
143
|
kind: 'cron',
|
|
154
144
|
expr: '0 0,12 * * *',
|
|
@@ -177,13 +167,12 @@ export function buildStarterCronJobs(starters) {
|
|
|
177
167
|
},
|
|
178
168
|
payload: {
|
|
179
169
|
kind: 'agentTurn',
|
|
180
|
-
message: 'Use
|
|
170
|
+
message: 'Use Fetch to read https://news.ycombinator.com and fetch today\'s top 10 stories. Reply with title, URL, and 1-line summary for each item.',
|
|
181
171
|
tools: {
|
|
182
172
|
enabled: true,
|
|
183
173
|
allowedPaths: [`${homedir()}/.skimpyclaw`],
|
|
184
174
|
maxIterations: 30,
|
|
185
175
|
bashTimeout: 30000,
|
|
186
|
-
browser: { enabled: true, headless: true },
|
|
187
176
|
},
|
|
188
177
|
},
|
|
189
178
|
});
|
|
@@ -199,13 +188,12 @@ export function buildStarterCronJobs(starters) {
|
|
|
199
188
|
},
|
|
200
189
|
payload: {
|
|
201
190
|
kind: 'agentTurn',
|
|
202
|
-
message: `Use
|
|
191
|
+
message: `Use Fetch to check current weather and today's forecast for ${starters.weatherLocation}. Keep it concise: current temp/conditions, highs/lows, precipitation chance, and 1 recommendation.`,
|
|
203
192
|
tools: {
|
|
204
193
|
enabled: true,
|
|
205
194
|
allowedPaths: [`${homedir()}/.skimpyclaw`],
|
|
206
195
|
maxIterations: 30,
|
|
207
196
|
bashTimeout: 30000,
|
|
208
|
-
browser: { enabled: true, headless: true },
|
|
209
197
|
},
|
|
210
198
|
},
|
|
211
199
|
});
|