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/subagent.js
DELETED
|
@@ -1,407 +0,0 @@
|
|
|
1
|
-
// Subagent system: background task dispatch with concurrency control
|
|
2
|
-
import { homedir } from 'os';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import { existsSync, mkdirSync, writeFileSync, appendFileSync } from 'fs';
|
|
5
|
-
import { runAgentTurn } from './agent.js';
|
|
6
|
-
import { getCurrentModel } from './gateway.js';
|
|
7
|
-
import { getAgentDir } from './config.js';
|
|
8
|
-
import { releaseAllLocks } from './file-lock.js';
|
|
9
|
-
import { resolveModel } from './providers/utils.js';
|
|
10
|
-
const DEFAULT_MAX_CONCURRENT = 5;
|
|
11
|
-
const DEFAULT_MAX_RETRIES = 2;
|
|
12
|
-
const REGISTRY_PATH = join(homedir(), '.skimpyclaw', 'logs', 'subagent-runs.jsonl');
|
|
13
|
-
const PRESETS = {
|
|
14
|
-
coding: {
|
|
15
|
-
agentId: 'coding',
|
|
16
|
-
defaultModel: 'claude-opus',
|
|
17
|
-
toolConfig: {
|
|
18
|
-
enabled: true,
|
|
19
|
-
allowedPaths: [join(homedir(), '.skimpyclaw')],
|
|
20
|
-
maxIterations: 50,
|
|
21
|
-
bashTimeout: 30000,
|
|
22
|
-
toolProfile: 'minimal',
|
|
23
|
-
},
|
|
24
|
-
description: 'Code tasks with broad file + bash access'
|
|
25
|
-
},
|
|
26
|
-
research: {
|
|
27
|
-
agentId: 'research',
|
|
28
|
-
defaultModel: 'claude-think',
|
|
29
|
-
toolConfig: {
|
|
30
|
-
enabled: true,
|
|
31
|
-
allowedPaths: [join(homedir(), '.skimpyclaw')],
|
|
32
|
-
maxIterations: 30,
|
|
33
|
-
bashTimeout: 15000,
|
|
34
|
-
toolProfile: 'minimal',
|
|
35
|
-
},
|
|
36
|
-
description: 'Research tasks with configurable file access'
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
// --- Starter Templates ---
|
|
40
|
-
const STARTER_TEMPLATES = {
|
|
41
|
-
coding: {
|
|
42
|
-
identity: `# IDENTITY.md - Coding Subagent
|
|
43
|
-
|
|
44
|
-
Name: Coding Agent
|
|
45
|
-
Emoji: 🔧
|
|
46
|
-
|
|
47
|
-
You are a coding subagent dispatched for a specific task. You have file and bash access
|
|
48
|
-
within your allowed paths (provided at runtime).
|
|
49
|
-
|
|
50
|
-
## Your Role
|
|
51
|
-
- Execute coding tasks: write code, fix bugs, refactor, run commands
|
|
52
|
-
- Act on instructions directly — don't narrate what you're going to do
|
|
53
|
-
- Return concise results when done
|
|
54
|
-
|
|
55
|
-
## Your Limitations
|
|
56
|
-
- You are short-lived: complete the task and return the result
|
|
57
|
-
- You have ONLY 4 tools: Read, Write, Glob, Bash
|
|
58
|
-
- Do NOT invent or hallucinate tools that don't exist
|
|
59
|
-
`,
|
|
60
|
-
tools: `# TOOLS.md - Coding Subagent Tools
|
|
61
|
-
|
|
62
|
-
## CRITICAL: Act, Don't Narrate
|
|
63
|
-
|
|
64
|
-
**NEVER say "let me write the code" or "now I'll update the file" — just call the tool.**
|
|
65
|
-
You have a LIMITED number of tool iterations. Every message you spend talking about what
|
|
66
|
-
you're going to do is one less chance to actually do it.
|
|
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
|
-
|
|
83
|
-
## Your 4 Tools
|
|
84
|
-
|
|
85
|
-
### Read
|
|
86
|
-
Read the contents of a file. Parameter: \`file_path\` (string, required)
|
|
87
|
-
|
|
88
|
-
### Write
|
|
89
|
-
Write content to a file. Parameters: \`file_path\` (string, required), \`content\` (string, required)
|
|
90
|
-
|
|
91
|
-
### Glob
|
|
92
|
-
List files and directories at a path. Parameter: \`path\` (string, required)
|
|
93
|
-
|
|
94
|
-
### Bash
|
|
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.
|
|
97
|
-
|
|
98
|
-
## Key Paths
|
|
99
|
-
- Config: ~/.skimpyclaw/config.json
|
|
100
|
-
- Agent templates: ~/.skimpyclaw/agents/
|
|
101
|
-
`
|
|
102
|
-
},
|
|
103
|
-
research: {
|
|
104
|
-
identity: `# IDENTITY.md - Research Subagent
|
|
105
|
-
|
|
106
|
-
Name: Research Agent
|
|
107
|
-
Emoji: 🔍
|
|
108
|
-
|
|
109
|
-
You are a research subagent dispatched for a specific task.
|
|
110
|
-
|
|
111
|
-
## Your Role
|
|
112
|
-
- Research questions using vault notes and files
|
|
113
|
-
- Summarize findings concisely
|
|
114
|
-
- Act on instructions directly — don't narrate what you're going to do
|
|
115
|
-
|
|
116
|
-
## Your Limitations
|
|
117
|
-
- You are short-lived: complete the task and return the result
|
|
118
|
-
- You have ONLY 4 tools: Read, Write, Glob, Bash
|
|
119
|
-
- Do NOT invent or hallucinate tools that don't exist
|
|
120
|
-
`,
|
|
121
|
-
tools: `# TOOLS.md - Research Subagent Tools
|
|
122
|
-
|
|
123
|
-
## CRITICAL: Act, Don't Narrate
|
|
124
|
-
|
|
125
|
-
**NEVER say "let me check" or "I'll look into that" — just call the tool.**
|
|
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
|
-
|
|
139
|
-
## Your 4 Tools
|
|
140
|
-
|
|
141
|
-
### Read
|
|
142
|
-
Read the contents of a file. Parameter: \`file_path\` (string, required)
|
|
143
|
-
|
|
144
|
-
### Write
|
|
145
|
-
Write content to a file. Parameters: \`file_path\` (string, required), \`content\` (string, required)
|
|
146
|
-
|
|
147
|
-
### Glob
|
|
148
|
-
List files and directories at a path. Parameter: \`path\` (string, required)
|
|
149
|
-
|
|
150
|
-
### Bash
|
|
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.
|
|
153
|
-
|
|
154
|
-
## Key Paths
|
|
155
|
-
- Config: ~/.skimpyclaw/config.json
|
|
156
|
-
- Agent templates: ~/.skimpyclaw/agents/
|
|
157
|
-
- Add your notes/vault paths to allowedPaths in config before using them
|
|
158
|
-
`
|
|
159
|
-
},
|
|
160
|
-
};
|
|
161
|
-
// Agent identity metadata for in-memory registration
|
|
162
|
-
const AGENT_IDENTITIES = {
|
|
163
|
-
coding: { name: 'Coding Agent', emoji: '🔧' },
|
|
164
|
-
research: { name: 'Research Agent', emoji: '🔍' }
|
|
165
|
-
};
|
|
166
|
-
/**
|
|
167
|
-
* Ensure agent directory and templates exist for a subagent type.
|
|
168
|
-
* Creates the dir + starter IDENTITY.md + TOOLS.md if missing.
|
|
169
|
-
* Registers the agent in config.agents.list (in-memory only).
|
|
170
|
-
*/
|
|
171
|
-
export function ensureAgentSetup(type, config) {
|
|
172
|
-
const preset = PRESETS[type];
|
|
173
|
-
const agentDir = getAgentDir(preset.agentId);
|
|
174
|
-
// Create dir + starter templates if missing
|
|
175
|
-
if (!existsSync(agentDir)) {
|
|
176
|
-
mkdirSync(agentDir, { recursive: true });
|
|
177
|
-
const templates = STARTER_TEMPLATES[type];
|
|
178
|
-
writeFileSync(join(agentDir, 'IDENTITY.md'), templates.identity, 'utf-8');
|
|
179
|
-
writeFileSync(join(agentDir, 'TOOLS.md'), templates.tools, 'utf-8');
|
|
180
|
-
console.log(`[subagent] Created agent dir with templates: ${agentDir}`);
|
|
181
|
-
}
|
|
182
|
-
// Register in config.agents.list if not already there (in-memory only)
|
|
183
|
-
if (!config.agents.list[preset.agentId]) {
|
|
184
|
-
const identity = AGENT_IDENTITIES[type];
|
|
185
|
-
config.agents.list[preset.agentId] = {
|
|
186
|
-
identity,
|
|
187
|
-
model: resolveModel(preset.defaultModel || 'anthropic/claude-sonnet-4-5', config),
|
|
188
|
-
thinking: 'medium'
|
|
189
|
-
};
|
|
190
|
-
console.log(`[subagent] Registered agent in config: ${preset.agentId}`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
// Task tracking
|
|
194
|
-
let taskCounter = 0;
|
|
195
|
-
const tasks = new Map();
|
|
196
|
-
let deliverMessage = null;
|
|
197
|
-
export function initSubagentSystem(deliverFn) {
|
|
198
|
-
deliverMessage = deliverFn;
|
|
199
|
-
console.log('[subagent] System initialized');
|
|
200
|
-
}
|
|
201
|
-
export function getPresetDescriptions() {
|
|
202
|
-
return Object.entries(PRESETS)
|
|
203
|
-
.map(([type, preset]) => ` ${type} — ${preset.description} (default: ${preset.defaultModel || 'current model'})`)
|
|
204
|
-
.join('\n');
|
|
205
|
-
}
|
|
206
|
-
// --- Disk Registry ---
|
|
207
|
-
function ensureRegistryDir() {
|
|
208
|
-
const dir = join(homedir(), '.skimpyclaw', 'logs');
|
|
209
|
-
if (!existsSync(dir)) {
|
|
210
|
-
mkdirSync(dir, { recursive: true });
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
function appendRegistryEvent(event) {
|
|
214
|
-
try {
|
|
215
|
-
ensureRegistryDir();
|
|
216
|
-
const line = JSON.stringify({ ...event, timestamp: new Date().toISOString() }) + '\n';
|
|
217
|
-
appendFileSync(REGISTRY_PATH, line, 'utf-8');
|
|
218
|
-
}
|
|
219
|
-
catch (err) {
|
|
220
|
-
console.warn('[subagent] Failed to write registry event:', err instanceof Error ? err.message : err);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
// --- Dispatch ---
|
|
224
|
-
export function dispatchSubagent(type, prompt, chatId, config, modelOverride, history, options) {
|
|
225
|
-
const maxConcurrent = config.subagents?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
|
|
226
|
-
const active = [...tasks.values()].filter((t) => t.status === 'running' || t.status === 'pending');
|
|
227
|
-
if (active.length >= maxConcurrent) {
|
|
228
|
-
throw new Error(`Max concurrent agents reached (${maxConcurrent}). Use /tasks to see running agents or /cancel to stop one.`);
|
|
229
|
-
}
|
|
230
|
-
const preset = PRESETS[type];
|
|
231
|
-
if (!preset) {
|
|
232
|
-
throw new Error(`Unknown agent type: ${type}. Use: ${Object.keys(PRESETS).join(', ')}`);
|
|
233
|
-
}
|
|
234
|
-
taskCounter++;
|
|
235
|
-
const id = `t${taskCounter}`;
|
|
236
|
-
const model = resolveModel(modelOverride || preset.defaultModel || getCurrentModel(), config);
|
|
237
|
-
const maxRetries = options?.maxRetries ?? config.subagents?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
238
|
-
const task = {
|
|
239
|
-
id,
|
|
240
|
-
type,
|
|
241
|
-
prompt,
|
|
242
|
-
status: 'pending',
|
|
243
|
-
chatId,
|
|
244
|
-
model,
|
|
245
|
-
label: options?.label,
|
|
246
|
-
createdAt: new Date(),
|
|
247
|
-
retryCount: 0,
|
|
248
|
-
maxRetries,
|
|
249
|
-
abortController: new AbortController()
|
|
250
|
-
};
|
|
251
|
-
tasks.set(id, task);
|
|
252
|
-
// Log to disk registry
|
|
253
|
-
appendRegistryEvent({
|
|
254
|
-
type: 'task_created',
|
|
255
|
-
taskId: id,
|
|
256
|
-
agentType: type,
|
|
257
|
-
model,
|
|
258
|
-
label: options?.label,
|
|
259
|
-
prompt: prompt.slice(0, 500),
|
|
260
|
-
});
|
|
261
|
-
// Build tool config with merged paths
|
|
262
|
-
const toolConfig = {
|
|
263
|
-
...preset.toolConfig,
|
|
264
|
-
allowedPaths: [
|
|
265
|
-
...preset.toolConfig.allowedPaths,
|
|
266
|
-
...(options?.allowedPaths || []),
|
|
267
|
-
],
|
|
268
|
-
};
|
|
269
|
-
// Fire and forget — don't await
|
|
270
|
-
executeTask(task, config, toolConfig, history).catch((err) => {
|
|
271
|
-
console.error(`[subagent] Unhandled error in task ${id}:`, err);
|
|
272
|
-
});
|
|
273
|
-
return task;
|
|
274
|
-
}
|
|
275
|
-
async function executeTask(task, config, toolConfig, history) {
|
|
276
|
-
task.status = 'running';
|
|
277
|
-
task.startedAt = new Date();
|
|
278
|
-
const label = task.label ? ` "${task.label}"` : '';
|
|
279
|
-
console.log(`[subagent] Starting ${task.id}${label} (${task.type}, model: ${task.model})`);
|
|
280
|
-
try {
|
|
281
|
-
// Check cancellation before starting
|
|
282
|
-
if (task.abortController.signal.aborted) {
|
|
283
|
-
task.status = 'cancelled';
|
|
284
|
-
task.completedAt = new Date();
|
|
285
|
-
appendRegistryEvent({ type: 'task_cancelled', taskId: task.id });
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
// Ensure agent dir + templates exist, register in config
|
|
289
|
-
ensureAgentSetup(task.type, config);
|
|
290
|
-
const response = await runAgentTurn(task.type, // agentId = preset.agentId = type name
|
|
291
|
-
task.prompt, config, task.model, toolConfig, history, {
|
|
292
|
-
channel: 'subagent',
|
|
293
|
-
sessionId: task.id,
|
|
294
|
-
metadata: {
|
|
295
|
-
type: task.type,
|
|
296
|
-
chatId: task.chatId,
|
|
297
|
-
label: task.label,
|
|
298
|
-
},
|
|
299
|
-
abortSignal: task.abortController.signal,
|
|
300
|
-
});
|
|
301
|
-
// Check cancellation after completion
|
|
302
|
-
if (task.abortController.signal.aborted) {
|
|
303
|
-
task.status = 'cancelled';
|
|
304
|
-
task.completedAt = new Date();
|
|
305
|
-
releaseAllLocks(task.id);
|
|
306
|
-
appendRegistryEvent({ type: 'task_cancelled', taskId: task.id });
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
task.status = 'completed';
|
|
310
|
-
task.result = response;
|
|
311
|
-
task.completedAt = new Date();
|
|
312
|
-
// Release any file locks held by this task
|
|
313
|
-
releaseAllLocks(task.id);
|
|
314
|
-
const elapsed = Math.round((task.completedAt.getTime() - task.startedAt.getTime()) / 1000);
|
|
315
|
-
console.log(`[subagent] Completed ${task.id}${label} in ${elapsed}s`);
|
|
316
|
-
appendRegistryEvent({
|
|
317
|
-
type: 'task_completed',
|
|
318
|
-
taskId: task.id,
|
|
319
|
-
elapsed,
|
|
320
|
-
resultLength: response.length,
|
|
321
|
-
});
|
|
322
|
-
// Deliver result
|
|
323
|
-
if (deliverMessage) {
|
|
324
|
-
const labelStr = task.label ? ` (${task.label})` : '';
|
|
325
|
-
const header = `✅ Agent ${task.id}${labelStr} completed in ${elapsed}s:`;
|
|
326
|
-
await deliverMessage(task.chatId, `${header}\n\n${response}`);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
catch (error) {
|
|
330
|
-
// Release any file locks on error
|
|
331
|
-
releaseAllLocks(task.id);
|
|
332
|
-
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
333
|
-
const retryCount = task.retryCount ?? 0;
|
|
334
|
-
const maxRetries = task.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
335
|
-
// Retry if under limit and not cancelled
|
|
336
|
-
if (retryCount < maxRetries && !task.abortController.signal.aborted) {
|
|
337
|
-
task.retryCount = retryCount + 1;
|
|
338
|
-
const elapsed = Math.round((Date.now() - (task.startedAt?.getTime() || task.createdAt.getTime())) / 1000);
|
|
339
|
-
console.log(`[subagent] Task ${task.id} failed after ${elapsed}s (attempt ${retryCount + 1}/${maxRetries + 1}), retrying: ${errorMsg}`);
|
|
340
|
-
appendRegistryEvent({
|
|
341
|
-
type: 'task_retry',
|
|
342
|
-
taskId: task.id,
|
|
343
|
-
attempt: task.retryCount,
|
|
344
|
-
error: errorMsg,
|
|
345
|
-
});
|
|
346
|
-
if (deliverMessage) {
|
|
347
|
-
await deliverMessage(task.chatId, `⚠️ Agent ${task.id} failed (attempt ${retryCount + 1}/${maxRetries + 1}), retrying...\n\nError: ${errorMsg}`);
|
|
348
|
-
}
|
|
349
|
-
// Modify prompt to include error context for retry (compressed to save tokens)
|
|
350
|
-
const taskSummary = task.prompt.length > 500 ? task.prompt.slice(0, 500) + '...' : task.prompt;
|
|
351
|
-
const retryPrompt = `Retry task (attempt ${(task.retryCount ?? 0) + 1}).\n\nTask: ${taskSummary}\n\nPrevious error: ${errorMsg}\n\nTry a different approach.`;
|
|
352
|
-
task.prompt = retryPrompt;
|
|
353
|
-
task.startedAt = new Date();
|
|
354
|
-
// Retry
|
|
355
|
-
return executeTask(task, config, {
|
|
356
|
-
...PRESETS[task.type].toolConfig,
|
|
357
|
-
}, history);
|
|
358
|
-
}
|
|
359
|
-
task.status = 'failed';
|
|
360
|
-
task.error = errorMsg;
|
|
361
|
-
task.completedAt = new Date();
|
|
362
|
-
const elapsed = Math.round((task.completedAt.getTime() -
|
|
363
|
-
(task.startedAt?.getTime() || task.createdAt.getTime())) /
|
|
364
|
-
1000);
|
|
365
|
-
console.error(`[subagent] Failed ${task.id} after ${elapsed}s (all retries exhausted):`, task.error);
|
|
366
|
-
appendRegistryEvent({
|
|
367
|
-
type: 'task_failed',
|
|
368
|
-
taskId: task.id,
|
|
369
|
-
elapsed,
|
|
370
|
-
error: task.error,
|
|
371
|
-
});
|
|
372
|
-
if (deliverMessage) {
|
|
373
|
-
await deliverMessage(task.chatId, `❌ Agent ${task.id} (${task.type}) failed after ${elapsed}s:\n\n${task.error}`);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
export function cancelTask(id) {
|
|
378
|
-
const task = tasks.get(id);
|
|
379
|
-
if (!task)
|
|
380
|
-
return null;
|
|
381
|
-
if (task.status !== 'pending' && task.status !== 'running')
|
|
382
|
-
return task;
|
|
383
|
-
task.abortController.abort();
|
|
384
|
-
task.status = 'cancelled';
|
|
385
|
-
task.completedAt = new Date();
|
|
386
|
-
releaseAllLocks(task.id);
|
|
387
|
-
appendRegistryEvent({ type: 'task_cancelled', taskId: task.id });
|
|
388
|
-
console.log(`[subagent] Cancelled ${task.id}`);
|
|
389
|
-
return task;
|
|
390
|
-
}
|
|
391
|
-
export function getActiveTasks() {
|
|
392
|
-
return [...tasks.values()].filter((t) => t.status === 'pending' || t.status === 'running');
|
|
393
|
-
}
|
|
394
|
-
export function getRecentTasks(n = 10) {
|
|
395
|
-
return [...tasks.values()]
|
|
396
|
-
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
|
397
|
-
.slice(0, n);
|
|
398
|
-
}
|
|
399
|
-
export function getTask(id) {
|
|
400
|
-
return tasks.get(id) || null;
|
|
401
|
-
}
|
|
402
|
-
// For testing
|
|
403
|
-
export function resetForTesting() {
|
|
404
|
-
taskCounter = 0;
|
|
405
|
-
tasks.clear();
|
|
406
|
-
deliverMessage = null;
|
|
407
|
-
}
|
package/dist/telegram.d.ts
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
export { initTelegram, startTelegram, stopTelegram, getBot, setSilenceUntil, getSilenceUntil, isSilenced, sendProactiveMessage, sendProactiveVoice, getHistory, addToHistory, clearHistory, getRunContext, getDefaultTelegramToolConfig, buildHelpText, getTelegramDefaultChatId, startTypingIndicator, sendLongMessage, sendLongMessageHtml, escapeHtml, markdownToTelegramHtml, state, BOT_COMMANDS, LAUNCHD_LABEL, commandHandlers, subscribeToApprovalEvents, } from './channels/telegram/index.js';
|
|
2
|
-
export type { MemoryFileInfo, TelegramContext } from './channels/telegram/types.js';
|
package/dist/telegram.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// Telegram bot - Re-export from new location for backward compatibility
|
|
2
|
-
// @deprecated Import from './channels/telegram/index.js' instead
|
|
3
|
-
export {
|
|
4
|
-
// Core functions
|
|
5
|
-
initTelegram, startTelegram, stopTelegram, getBot, setSilenceUntil, getSilenceUntil, isSilenced, sendProactiveMessage, sendProactiveVoice,
|
|
6
|
-
// Utilities
|
|
7
|
-
getHistory, addToHistory, clearHistory, getRunContext, getDefaultTelegramToolConfig, buildHelpText, getTelegramDefaultChatId, startTypingIndicator, sendLongMessage, sendLongMessageHtml, escapeHtml, markdownToTelegramHtml,
|
|
8
|
-
// Constants
|
|
9
|
-
state, BOT_COMMANDS, LAUNCHD_LABEL,
|
|
10
|
-
// Handlers
|
|
11
|
-
commandHandlers, subscribeToApprovalEvents, } from './channels/telegram/index.js';
|
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync } from 'fs';
|
|
2
|
-
import { join, dirname } from 'path';
|
|
3
|
-
import { homedir } from 'os';
|
|
4
|
-
import { isPathAllowed } from './path-utils.js';
|
|
5
|
-
import { toErrorMessage } from '../utils.js';
|
|
6
|
-
let playwrightModule = null;
|
|
7
|
-
let browserContext = null;
|
|
8
|
-
let browserPage = null;
|
|
9
|
-
let browserOptionsKey = null;
|
|
10
|
-
async function getPlaywright() {
|
|
11
|
-
if (!playwrightModule) {
|
|
12
|
-
try {
|
|
13
|
-
playwrightModule = await import('playwright');
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
throw new Error('Playwright not installed. Run: npx playwright install');
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return playwrightModule;
|
|
20
|
-
}
|
|
21
|
-
function resolveScreenshotPath(filePath, config) {
|
|
22
|
-
if (filePath) {
|
|
23
|
-
if (!isPathAllowed(filePath, config.allowedPaths)) {
|
|
24
|
-
throw new Error(`Path not allowed. Permitted: ${config.allowedPaths.join(', ')}`);
|
|
25
|
-
}
|
|
26
|
-
const dir = dirname(filePath);
|
|
27
|
-
if (!existsSync(dir)) {
|
|
28
|
-
mkdirSync(dir, { recursive: true });
|
|
29
|
-
}
|
|
30
|
-
return filePath;
|
|
31
|
-
}
|
|
32
|
-
const dir = join(homedir(), '.skimpyclaw', 'screenshots');
|
|
33
|
-
if (!existsSync(dir)) {
|
|
34
|
-
mkdirSync(dir, { recursive: true });
|
|
35
|
-
}
|
|
36
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
37
|
-
return join(dir, `shot-${stamp}.png`);
|
|
38
|
-
}
|
|
39
|
-
function isFileUrlAllowed(url, config) {
|
|
40
|
-
if (!url.startsWith('file://'))
|
|
41
|
-
return true;
|
|
42
|
-
if (!config.browser?.allowFile)
|
|
43
|
-
return false;
|
|
44
|
-
const filePath = new URL(url).pathname;
|
|
45
|
-
return isPathAllowed(filePath, config.allowedPaths);
|
|
46
|
-
}
|
|
47
|
-
/** Pick override only if it's a meaningful value (not empty string, not 0 for non-numeric fields). */
|
|
48
|
-
function pick(override, configVal, fallback) {
|
|
49
|
-
if (override !== undefined && override !== null && override !== '')
|
|
50
|
-
return override;
|
|
51
|
-
if (configVal !== undefined && configVal !== null && configVal !== '')
|
|
52
|
-
return configVal;
|
|
53
|
-
return fallback;
|
|
54
|
-
}
|
|
55
|
-
function buildBrowserOptions(config, overrides) {
|
|
56
|
-
// Config values take priority for security/environment settings.
|
|
57
|
-
// Overrides (from model tool calls) only apply when config doesn't specify a value.
|
|
58
|
-
const type = pick(overrides?.type, config.browser?.type, 'chromium');
|
|
59
|
-
const headless = config.browser?.headless ?? overrides?.headless ?? true;
|
|
60
|
-
const slowMo = config.browser?.slowMoMs ?? ((typeof overrides?.slowMoMs === 'number' && overrides.slowMoMs > 0) ? overrides.slowMoMs : undefined);
|
|
61
|
-
const userAgent = pick(config.browser?.userAgent, overrides?.userAgent);
|
|
62
|
-
const viewport = config.browser?.viewport ?? (overrides?.viewport?.width ? overrides.viewport : undefined);
|
|
63
|
-
// Security: profileDir and executablePath are config-only — never allow model overrides
|
|
64
|
-
const profileDir = config.browser?.profileDir || join(homedir(), '.skimpyclaw', 'browser-profile');
|
|
65
|
-
const executablePath = config.browser?.executablePath;
|
|
66
|
-
return { type, headless, slowMo, userAgent, viewport, profileDir, executablePath };
|
|
67
|
-
}
|
|
68
|
-
async function ensureBrowser(config, overrides) {
|
|
69
|
-
const options = buildBrowserOptions(config, overrides);
|
|
70
|
-
const optionsKey = JSON.stringify(options);
|
|
71
|
-
if (browserContext && browserPage && browserOptionsKey === optionsKey)
|
|
72
|
-
return;
|
|
73
|
-
if (browserPage) {
|
|
74
|
-
await browserPage.close().catch(() => { });
|
|
75
|
-
browserPage = null;
|
|
76
|
-
}
|
|
77
|
-
if (browserContext) {
|
|
78
|
-
await browserContext.close().catch(() => { });
|
|
79
|
-
browserContext = null;
|
|
80
|
-
}
|
|
81
|
-
if (!isPathAllowed(options.profileDir, config.allowedPaths)) {
|
|
82
|
-
throw new Error(`Profile dir not allowed. Add to allowedPaths: ${options.profileDir}`);
|
|
83
|
-
}
|
|
84
|
-
if (!existsSync(options.profileDir)) {
|
|
85
|
-
mkdirSync(options.profileDir, { recursive: true });
|
|
86
|
-
}
|
|
87
|
-
const pw = await getPlaywright();
|
|
88
|
-
const browserLauncher = pw[options.type] || pw.chromium;
|
|
89
|
-
try {
|
|
90
|
-
browserContext = await browserLauncher.launchPersistentContext(options.profileDir, {
|
|
91
|
-
headless: options.headless,
|
|
92
|
-
slowMo: options.slowMo,
|
|
93
|
-
userAgent: options.userAgent,
|
|
94
|
-
viewport: options.viewport,
|
|
95
|
-
executablePath: options.executablePath,
|
|
96
|
-
args: [
|
|
97
|
-
'--disable-blink-features=AutomationControlled',
|
|
98
|
-
'--no-first-run',
|
|
99
|
-
'--no-default-browser-check',
|
|
100
|
-
],
|
|
101
|
-
ignoreDefaultArgs: ['--enable-automation', '--no-sandbox'],
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
const msg = toErrorMessage(err);
|
|
106
|
-
throw new Error(`Failed to launch browser (${options.type}): ${msg}. Ensure the browser is installed: npx playwright install ${options.type}`);
|
|
107
|
-
}
|
|
108
|
-
const pages = browserContext.pages();
|
|
109
|
-
browserPage = pages.length > 0 ? pages[0] : await browserContext.newPage();
|
|
110
|
-
// Remove navigator.webdriver flag that sites use to detect automation
|
|
111
|
-
await browserPage.addInitScript(`Object.defineProperty(navigator, 'webdriver', { get: () => false })`);
|
|
112
|
-
browserOptionsKey = optionsKey;
|
|
113
|
-
}
|
|
114
|
-
export async function executeBrowser(input, config) {
|
|
115
|
-
if (!config.browser?.enabled) {
|
|
116
|
-
return 'Error: Browser tool is disabled. Enable it in config (tools.browser.enabled).';
|
|
117
|
-
}
|
|
118
|
-
const action = String(input.action || '').toLowerCase();
|
|
119
|
-
const timeoutMs = typeof input.timeoutMs === 'number' ? input.timeoutMs : 30_000;
|
|
120
|
-
switch (action) {
|
|
121
|
-
case 'open': {
|
|
122
|
-
const url = input.url;
|
|
123
|
-
if (!url)
|
|
124
|
-
return 'Error: url is required for open.';
|
|
125
|
-
if (!isFileUrlAllowed(url, config)) {
|
|
126
|
-
return 'Error: file:// URLs are blocked. Enable tools.browser.allowFile to allow.';
|
|
127
|
-
}
|
|
128
|
-
await ensureBrowser(config, input);
|
|
129
|
-
await browserPage.goto(url, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
130
|
-
return `Opened: ${url}`;
|
|
131
|
-
}
|
|
132
|
-
case 'click': {
|
|
133
|
-
if (!browserPage)
|
|
134
|
-
return 'Error: Browser not open. Call open(url) first.';
|
|
135
|
-
const selector = input.selector;
|
|
136
|
-
if (!selector)
|
|
137
|
-
return 'Error: selector is required for click.';
|
|
138
|
-
await browserPage.click(selector, { timeout: timeoutMs });
|
|
139
|
-
return `Clicked: ${selector}`;
|
|
140
|
-
}
|
|
141
|
-
case 'type': {
|
|
142
|
-
if (!browserPage)
|
|
143
|
-
return 'Error: Browser not open. Call open(url) first.';
|
|
144
|
-
const selector = input.selector;
|
|
145
|
-
const text = input.text;
|
|
146
|
-
if (!selector || text === undefined)
|
|
147
|
-
return 'Error: selector and text are required for type.';
|
|
148
|
-
await browserPage.fill(selector, text, { timeout: timeoutMs });
|
|
149
|
-
return `Typed into: ${selector}`;
|
|
150
|
-
}
|
|
151
|
-
case 'waitfor': {
|
|
152
|
-
if (!browserPage)
|
|
153
|
-
return 'Error: Browser not open. Call open(url) first.';
|
|
154
|
-
const selector = input.selector;
|
|
155
|
-
const text = input.text;
|
|
156
|
-
if (!selector && !text)
|
|
157
|
-
return 'Error: selector or text is required for waitFor.';
|
|
158
|
-
if (selector) {
|
|
159
|
-
await browserPage.waitForSelector(selector, { timeout: timeoutMs });
|
|
160
|
-
return `Waited for selector: ${selector}`;
|
|
161
|
-
}
|
|
162
|
-
await browserPage.waitForSelector(`text=${text}`, { timeout: timeoutMs });
|
|
163
|
-
return `Waited for text: ${text}`;
|
|
164
|
-
}
|
|
165
|
-
case 'select': {
|
|
166
|
-
if (!browserPage)
|
|
167
|
-
return 'Error: Browser not open. Call open(url) first.';
|
|
168
|
-
const selector = input.selector;
|
|
169
|
-
const value = input.text;
|
|
170
|
-
if (!selector || value === undefined)
|
|
171
|
-
return 'Error: selector and text (value) are required for select.';
|
|
172
|
-
await browserPage.selectOption(selector, value, { timeout: timeoutMs });
|
|
173
|
-
return `Selected "${value}" in: ${selector}`;
|
|
174
|
-
}
|
|
175
|
-
case 'hover': {
|
|
176
|
-
if (!browserPage)
|
|
177
|
-
return 'Error: Browser not open. Call open(url) first.';
|
|
178
|
-
const selector = input.selector;
|
|
179
|
-
if (!selector)
|
|
180
|
-
return 'Error: selector is required for hover.';
|
|
181
|
-
await browserPage.hover(selector, { timeout: timeoutMs });
|
|
182
|
-
return `Hovered: ${selector}`;
|
|
183
|
-
}
|
|
184
|
-
case 'scroll': {
|
|
185
|
-
if (!browserPage)
|
|
186
|
-
return 'Error: Browser not open. Call open(url) first.';
|
|
187
|
-
const selector = input.selector;
|
|
188
|
-
if (selector) {
|
|
189
|
-
await browserPage.evaluate(`{
|
|
190
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
191
|
-
if (el) el.scrollIntoView({ behavior: 'smooth' });
|
|
192
|
-
}`);
|
|
193
|
-
return `Scrolled into view: ${selector}`;
|
|
194
|
-
}
|
|
195
|
-
const direction = (input.direction || 'down').toLowerCase();
|
|
196
|
-
const amount = typeof input.amount === 'number' ? input.amount : undefined;
|
|
197
|
-
const dir = direction === 'up' ? -1 : 1;
|
|
198
|
-
const scrollExpr = amount != null
|
|
199
|
-
? `window.scrollBy(0, ${dir * amount})`
|
|
200
|
-
: `window.scrollBy(0, ${dir} * window.innerHeight)`;
|
|
201
|
-
await browserPage.evaluate(scrollExpr);
|
|
202
|
-
return `Scrolled ${direction}${amount ? ` ${amount}px` : ' one viewport'}`;
|
|
203
|
-
}
|
|
204
|
-
case 'evaluate': {
|
|
205
|
-
if (!browserPage)
|
|
206
|
-
return 'Error: Browser not open. Call open(url) first.';
|
|
207
|
-
const script = input.script;
|
|
208
|
-
if (!script)
|
|
209
|
-
return 'Error: script is required for evaluate.';
|
|
210
|
-
const result = await browserPage.evaluate(script);
|
|
211
|
-
return JSON.stringify(result);
|
|
212
|
-
}
|
|
213
|
-
case 'gettext': {
|
|
214
|
-
if (!browserPage)
|
|
215
|
-
return 'Error: Browser not open. Call open(url) first.';
|
|
216
|
-
const selector = input.selector;
|
|
217
|
-
if (selector) {
|
|
218
|
-
const text = await browserPage.textContent(selector, { timeout: timeoutMs });
|
|
219
|
-
return text ?? '(no text content)';
|
|
220
|
-
}
|
|
221
|
-
const bodyText = await browserPage.evaluate('document.body.innerText');
|
|
222
|
-
return bodyText || '(empty page)';
|
|
223
|
-
}
|
|
224
|
-
case 'screenshot': {
|
|
225
|
-
if (!browserPage)
|
|
226
|
-
return 'Error: Browser not open. Call open(url) first.';
|
|
227
|
-
const filePath = resolveScreenshotPath(input.file_path, config);
|
|
228
|
-
await browserPage.screenshot({ path: filePath, fullPage: true });
|
|
229
|
-
return `Saved screenshot: ${filePath}`;
|
|
230
|
-
}
|
|
231
|
-
case 'wait': {
|
|
232
|
-
const waitMs = typeof input.timeMs === 'number' ? input.timeMs : timeoutMs;
|
|
233
|
-
await new Promise((r) => setTimeout(r, waitMs));
|
|
234
|
-
return `Waited ${waitMs}ms`;
|
|
235
|
-
}
|
|
236
|
-
case 'close': {
|
|
237
|
-
await cleanupBrowser();
|
|
238
|
-
return 'Browser closed.';
|
|
239
|
-
}
|
|
240
|
-
default:
|
|
241
|
-
return `Error: Unknown browser action "${action}"`;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
export async function cleanupBrowser() {
|
|
245
|
-
if (browserPage) {
|
|
246
|
-
await browserPage.close().catch(() => { });
|
|
247
|
-
browserPage = null;
|
|
248
|
-
}
|
|
249
|
-
if (browserContext) {
|
|
250
|
-
await browserContext.close().catch(() => { });
|
|
251
|
-
browserContext = null;
|
|
252
|
-
}
|
|
253
|
-
browserOptionsKey = null;
|
|
254
|
-
}
|
|
255
|
-
// Prevent orphaned browser processes on exit
|
|
256
|
-
const handleExit = () => {
|
|
257
|
-
if (browserContext) {
|
|
258
|
-
browserContext.close().catch(() => { });
|
|
259
|
-
browserContext = null;
|
|
260
|
-
browserPage = null;
|
|
261
|
-
browserOptionsKey = null;
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
process.on('SIGTERM', handleExit);
|
|
265
|
-
process.on('SIGINT', handleExit);
|
|
266
|
-
process.on('exit', handleExit);
|