skimpyclaw 0.1.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 +230 -0
- package/dist/__tests__/agent.test.d.ts +1 -0
- package/dist/__tests__/agent.test.js +131 -0
- package/dist/__tests__/api.test.d.ts +1 -0
- package/dist/__tests__/api.test.js +1227 -0
- package/dist/__tests__/audit.test.d.ts +1 -0
- package/dist/__tests__/audit.test.js +122 -0
- package/dist/__tests__/cache.test.d.ts +1 -0
- package/dist/__tests__/cache.test.js +65 -0
- package/dist/__tests__/channels.test.d.ts +1 -0
- package/dist/__tests__/channels.test.js +85 -0
- package/dist/__tests__/cli.integration.test.d.ts +1 -0
- package/dist/__tests__/cli.integration.test.js +16 -0
- package/dist/__tests__/cli.test.d.ts +1 -0
- package/dist/__tests__/cli.test.js +230 -0
- package/dist/__tests__/code-agents-executor.test.d.ts +1 -0
- package/dist/__tests__/code-agents-executor.test.js +75 -0
- package/dist/__tests__/code-agents-orchestrator.test.d.ts +1 -0
- package/dist/__tests__/code-agents-orchestrator.test.js +149 -0
- package/dist/__tests__/code-agents-parser.test.d.ts +1 -0
- package/dist/__tests__/code-agents-parser.test.js +39 -0
- package/dist/__tests__/code-agents-utils.test.d.ts +1 -0
- package/dist/__tests__/code-agents-utils.test.js +41 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +46 -0
- package/dist/__tests__/cron.test.d.ts +1 -0
- package/dist/__tests__/cron.test.js +66 -0
- package/dist/__tests__/dashboard-mode.test.d.ts +1 -0
- package/dist/__tests__/dashboard-mode.test.js +145 -0
- package/dist/__tests__/dashboard.test.d.ts +1 -0
- package/dist/__tests__/dashboard.test.js +43 -0
- package/dist/__tests__/doctor.formatters.test.d.ts +1 -0
- package/dist/__tests__/doctor.formatters.test.js +65 -0
- package/dist/__tests__/doctor.index.test.d.ts +1 -0
- package/dist/__tests__/doctor.index.test.js +48 -0
- package/dist/__tests__/doctor.runner.test.d.ts +1 -0
- package/dist/__tests__/doctor.runner.test.js +204 -0
- package/dist/__tests__/exec-approval.test.d.ts +1 -0
- package/dist/__tests__/exec-approval.test.js +323 -0
- package/dist/__tests__/file-lock.test.d.ts +1 -0
- package/dist/__tests__/file-lock.test.js +92 -0
- package/dist/__tests__/langfuse.test.d.ts +1 -0
- package/dist/__tests__/langfuse.test.js +40 -0
- package/dist/__tests__/model-selection.test.d.ts +1 -0
- package/dist/__tests__/model-selection.test.js +62 -0
- package/dist/__tests__/orchestrator.test.d.ts +1 -0
- package/dist/__tests__/orchestrator.test.js +425 -0
- package/dist/__tests__/providers-init.test.d.ts +1 -0
- package/dist/__tests__/providers-init.test.js +32 -0
- package/dist/__tests__/providers-routing.test.d.ts +1 -0
- package/dist/__tests__/providers-routing.test.js +25 -0
- package/dist/__tests__/providers-utils.test.d.ts +1 -0
- package/dist/__tests__/providers-utils.test.js +54 -0
- package/dist/__tests__/security.test.d.ts +1 -0
- package/dist/__tests__/security.test.js +22 -0
- package/dist/__tests__/sessions.test.d.ts +1 -0
- package/dist/__tests__/sessions.test.js +147 -0
- package/dist/__tests__/setup.test.d.ts +1 -0
- package/dist/__tests__/setup.test.js +114 -0
- package/dist/__tests__/skills.test.d.ts +1 -0
- package/dist/__tests__/skills.test.js +333 -0
- package/dist/__tests__/subagent.test.d.ts +1 -0
- package/dist/__tests__/subagent.test.js +240 -0
- package/dist/__tests__/telegram-utils.test.d.ts +1 -0
- package/dist/__tests__/telegram-utils.test.js +22 -0
- package/dist/__tests__/telegram.test.d.ts +1 -0
- package/dist/__tests__/telegram.test.js +42 -0
- package/dist/__tests__/token-efficiency.test.d.ts +1 -0
- package/dist/__tests__/token-efficiency.test.js +38 -0
- package/dist/__tests__/tool-guard.test.d.ts +1 -0
- package/dist/__tests__/tool-guard.test.js +105 -0
- package/dist/__tests__/tools.test.d.ts +1 -0
- package/dist/__tests__/tools.test.js +589 -0
- package/dist/__tests__/usage.test.d.ts +1 -0
- package/dist/__tests__/usage.test.js +197 -0
- package/dist/__tests__/voice.test.d.ts +1 -0
- package/dist/__tests__/voice.test.js +214 -0
- package/dist/agent.d.ts +24 -0
- package/dist/agent.js +269 -0
- package/dist/api.d.ts +3 -0
- package/dist/api.js +943 -0
- package/dist/audit.d.ts +26 -0
- package/dist/audit.js +121 -0
- package/dist/cache.d.ts +8 -0
- package/dist/cache.js +24 -0
- package/dist/channels/telegram/handlers.d.ts +41 -0
- package/dist/channels/telegram/handlers.js +498 -0
- package/dist/channels/telegram/index.d.ts +14 -0
- package/dist/channels/telegram/index.js +326 -0
- package/dist/channels/telegram/types.d.ts +26 -0
- package/dist/channels/telegram/types.js +31 -0
- package/dist/channels/telegram/utils.d.ts +25 -0
- package/dist/channels/telegram/utils.js +256 -0
- package/dist/channels.d.ts +11 -0
- package/dist/channels.js +118 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +768 -0
- package/dist/code-agents/executor.d.ts +5 -0
- package/dist/code-agents/executor.js +463 -0
- package/dist/code-agents/index.d.ts +22 -0
- package/dist/code-agents/index.js +199 -0
- package/dist/code-agents/orchestrator.d.ts +23 -0
- package/dist/code-agents/orchestrator.js +403 -0
- package/dist/code-agents/parser.d.ts +21 -0
- package/dist/code-agents/parser.js +197 -0
- package/dist/code-agents/registry.d.ts +27 -0
- package/dist/code-agents/registry.js +147 -0
- package/dist/code-agents/types.d.ts +66 -0
- package/dist/code-agents/types.js +4 -0
- package/dist/code-agents/utils.d.ts +36 -0
- package/dist/code-agents/utils.js +236 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +123 -0
- package/dist/cron.d.ts +49 -0
- package/dist/cron.js +400 -0
- package/dist/dashboard/assets/index-CZJCvMSN.js +65 -0
- package/dist/dashboard/assets/index-EAg6lqF5.css +1 -0
- package/dist/dashboard/favicon.svg +3 -0
- package/dist/dashboard/index.html +21 -0
- package/dist/dashboard-frontend.d.ts +7 -0
- package/dist/dashboard-frontend.js +86 -0
- package/dist/dashboard.d.ts +8 -0
- package/dist/dashboard.js +4071 -0
- package/dist/digests.d.ts +36 -0
- package/dist/digests.js +338 -0
- package/dist/discord.d.ts +8 -0
- package/dist/discord.js +828 -0
- package/dist/doctor/checks.d.ts +18 -0
- package/dist/doctor/checks.js +368 -0
- package/dist/doctor/formatters.d.ts +3 -0
- package/dist/doctor/formatters.js +44 -0
- package/dist/doctor/index.d.ts +8 -0
- package/dist/doctor/index.js +7 -0
- package/dist/doctor/runner.d.ts +3 -0
- package/dist/doctor/runner.js +109 -0
- package/dist/doctor/types.d.ts +20 -0
- package/dist/doctor/types.js +1 -0
- package/dist/exec-approval.d.ts +101 -0
- package/dist/exec-approval.js +432 -0
- package/dist/file-lock.d.ts +34 -0
- package/dist/file-lock.js +81 -0
- package/dist/gateway.d.ts +8 -0
- package/dist/gateway.js +114 -0
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.js +101 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +75 -0
- package/dist/langfuse.d.ts +34 -0
- package/dist/langfuse.js +145 -0
- package/dist/mcp-context-a8c.d.ts +13 -0
- package/dist/mcp-context-a8c.js +34 -0
- package/dist/model-selection.d.ts +18 -0
- package/dist/model-selection.js +50 -0
- package/dist/orchestrator.d.ts +15 -0
- package/dist/orchestrator.js +676 -0
- package/dist/providers/anthropic.d.ts +7 -0
- package/dist/providers/anthropic.js +319 -0
- package/dist/providers/codex.d.ts +17 -0
- package/dist/providers/codex.js +508 -0
- package/dist/providers/content.d.ts +21 -0
- package/dist/providers/content.js +55 -0
- package/dist/providers/index.d.ts +13 -0
- package/dist/providers/index.js +138 -0
- package/dist/providers/observability.d.ts +19 -0
- package/dist/providers/observability.js +94 -0
- package/dist/providers/openai.d.ts +10 -0
- package/dist/providers/openai.js +310 -0
- package/dist/providers/tool-guard.d.ts +30 -0
- package/dist/providers/tool-guard.js +89 -0
- package/dist/providers/types.d.ts +34 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/utils.d.ts +65 -0
- package/dist/providers/utils.js +199 -0
- package/dist/security.d.ts +8 -0
- package/dist/security.js +113 -0
- package/dist/service.d.ts +8 -0
- package/dist/service.js +38 -0
- package/dist/sessions.d.ts +35 -0
- package/dist/sessions.js +142 -0
- package/dist/setup.d.ts +36 -0
- package/dist/setup.js +821 -0
- package/dist/skills-types.d.ts +65 -0
- package/dist/skills-types.js +2 -0
- package/dist/skills.d.ts +32 -0
- package/dist/skills.js +260 -0
- package/dist/subagent.d.ts +19 -0
- package/dist/subagent.js +376 -0
- package/dist/telegram.d.ts +2 -0
- package/dist/telegram.js +11 -0
- package/dist/tools/bash-tool.d.ts +3 -0
- package/dist/tools/bash-tool.js +59 -0
- package/dist/tools/browser-tool.d.ts +3 -0
- package/dist/tools/browser-tool.js +265 -0
- package/dist/tools/definitions.d.ts +432 -0
- package/dist/tools/definitions.js +181 -0
- package/dist/tools/execute-context.d.ts +26 -0
- package/dist/tools/execute-context.js +1 -0
- package/dist/tools/file-tools.d.ts +8 -0
- package/dist/tools/file-tools.js +67 -0
- package/dist/tools/path-utils.d.ts +1 -0
- package/dist/tools/path-utils.js +8 -0
- package/dist/tools.d.ts +24 -0
- package/dist/tools.js +281 -0
- package/dist/types.d.ts +259 -0
- package/dist/types.js +2 -0
- package/dist/usage.d.ts +76 -0
- package/dist/usage.js +150 -0
- package/dist/voice.d.ts +37 -0
- package/dist/voice.js +461 -0
- package/package.json +70 -0
- package/templates/AGENTS.md +38 -0
- package/templates/BOOT.md +23 -0
- package/templates/BOOTSTRAP.md +26 -0
- package/templates/HEARTBEAT.md +5 -0
- package/templates/IDENTITY.md +5 -0
- package/templates/MEMORY.md +24 -0
- package/templates/SOUL.md +92 -0
- package/templates/TOOLS.md +30 -0
- package/templates/USER.md +31 -0
package/dist/subagent.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
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
|
+
},
|
|
23
|
+
description: 'Code tasks with broad file + bash access'
|
|
24
|
+
},
|
|
25
|
+
research: {
|
|
26
|
+
agentId: 'research',
|
|
27
|
+
defaultModel: 'claude-think',
|
|
28
|
+
toolConfig: {
|
|
29
|
+
enabled: true,
|
|
30
|
+
allowedPaths: [join(homedir(), '.skimpyclaw')],
|
|
31
|
+
maxIterations: 30,
|
|
32
|
+
bashTimeout: 15000
|
|
33
|
+
},
|
|
34
|
+
description: 'Research tasks with configurable file access'
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
// --- Starter Templates ---
|
|
38
|
+
const STARTER_TEMPLATES = {
|
|
39
|
+
coding: {
|
|
40
|
+
identity: `# IDENTITY.md - Coding Subagent
|
|
41
|
+
|
|
42
|
+
Name: Coding Agent
|
|
43
|
+
Emoji: 🔧
|
|
44
|
+
|
|
45
|
+
You are a coding subagent dispatched for a specific task. You have file and bash access
|
|
46
|
+
within your allowed paths (provided at runtime).
|
|
47
|
+
|
|
48
|
+
## Your Role
|
|
49
|
+
- Execute coding tasks: write code, fix bugs, refactor, run commands
|
|
50
|
+
- Act on instructions directly — don't narrate what you're going to do
|
|
51
|
+
- Return concise results when done
|
|
52
|
+
|
|
53
|
+
## Your Limitations
|
|
54
|
+
- You are short-lived: complete the task and return the result
|
|
55
|
+
- You have ONLY 4 tools: Read, Write, Glob, Bash
|
|
56
|
+
- Do NOT invent or hallucinate tools that don't exist
|
|
57
|
+
`,
|
|
58
|
+
tools: `# TOOLS.md - Coding Subagent Tools
|
|
59
|
+
|
|
60
|
+
## CRITICAL: Act, Don't Narrate
|
|
61
|
+
|
|
62
|
+
**NEVER say "let me write the code" or "now I'll update the file" — just call the tool.**
|
|
63
|
+
You have a LIMITED number of tool iterations. Every message you spend talking about what
|
|
64
|
+
you're going to do is one less chance to actually do it.
|
|
65
|
+
|
|
66
|
+
## Your 4 Tools
|
|
67
|
+
|
|
68
|
+
### Read
|
|
69
|
+
Read the contents of a file. Parameter: \`file_path\` (string, required)
|
|
70
|
+
|
|
71
|
+
### Write
|
|
72
|
+
Write content to a file. Parameters: \`file_path\` (string, required), \`content\` (string, required)
|
|
73
|
+
|
|
74
|
+
### Glob
|
|
75
|
+
List files and directories at a path. Parameter: \`path\` (string, required)
|
|
76
|
+
|
|
77
|
+
### Bash
|
|
78
|
+
Execute a shell command. Parameters: \`command\` (string, required), \`cwd\` (string, optional)
|
|
79
|
+
|
|
80
|
+
## Key Paths
|
|
81
|
+
- Config: ~/.skimpyclaw/config.json
|
|
82
|
+
- Agent templates: ~/.skimpyclaw/agents/
|
|
83
|
+
`
|
|
84
|
+
},
|
|
85
|
+
research: {
|
|
86
|
+
identity: `# IDENTITY.md - Research Subagent
|
|
87
|
+
|
|
88
|
+
Name: Research Agent
|
|
89
|
+
Emoji: 🔍
|
|
90
|
+
|
|
91
|
+
You are a research subagent dispatched for a specific task.
|
|
92
|
+
|
|
93
|
+
## Your Role
|
|
94
|
+
- Research questions using vault notes and files
|
|
95
|
+
- Summarize findings concisely
|
|
96
|
+
- Act on instructions directly — don't narrate what you're going to do
|
|
97
|
+
|
|
98
|
+
## Your Limitations
|
|
99
|
+
- You are short-lived: complete the task and return the result
|
|
100
|
+
- You have ONLY 4 tools: Read, Write, Glob, Bash
|
|
101
|
+
- Do NOT invent or hallucinate tools that don't exist
|
|
102
|
+
`,
|
|
103
|
+
tools: `# TOOLS.md - Research Subagent Tools
|
|
104
|
+
|
|
105
|
+
## CRITICAL: Act, Don't Narrate
|
|
106
|
+
|
|
107
|
+
**NEVER say "let me check" or "I'll look into that" — just call the tool.**
|
|
108
|
+
|
|
109
|
+
## Your 4 Tools
|
|
110
|
+
|
|
111
|
+
### Read
|
|
112
|
+
Read the contents of a file. Parameter: \`file_path\` (string, required)
|
|
113
|
+
|
|
114
|
+
### Write
|
|
115
|
+
Write content to a file. Parameters: \`file_path\` (string, required), \`content\` (string, required)
|
|
116
|
+
|
|
117
|
+
### Glob
|
|
118
|
+
List files and directories at a path. Parameter: \`path\` (string, required)
|
|
119
|
+
|
|
120
|
+
### Bash
|
|
121
|
+
Execute a shell command. Parameters: \`command\` (string, required), \`cwd\` (string, optional)
|
|
122
|
+
|
|
123
|
+
## Key Paths
|
|
124
|
+
- Config: ~/.skimpyclaw/config.json
|
|
125
|
+
- Agent templates: ~/.skimpyclaw/agents/
|
|
126
|
+
- Add your notes/vault paths to allowedPaths in config before using them
|
|
127
|
+
`
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
// Agent identity metadata for in-memory registration
|
|
131
|
+
const AGENT_IDENTITIES = {
|
|
132
|
+
coding: { name: 'Coding Agent', emoji: '🔧' },
|
|
133
|
+
research: { name: 'Research Agent', emoji: '🔍' }
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Ensure agent directory and templates exist for a subagent type.
|
|
137
|
+
* Creates the dir + starter IDENTITY.md + TOOLS.md if missing.
|
|
138
|
+
* Registers the agent in config.agents.list (in-memory only).
|
|
139
|
+
*/
|
|
140
|
+
export function ensureAgentSetup(type, config) {
|
|
141
|
+
const preset = PRESETS[type];
|
|
142
|
+
const agentDir = getAgentDir(preset.agentId);
|
|
143
|
+
// Create dir + starter templates if missing
|
|
144
|
+
if (!existsSync(agentDir)) {
|
|
145
|
+
mkdirSync(agentDir, { recursive: true });
|
|
146
|
+
const templates = STARTER_TEMPLATES[type];
|
|
147
|
+
writeFileSync(join(agentDir, 'IDENTITY.md'), templates.identity, 'utf-8');
|
|
148
|
+
writeFileSync(join(agentDir, 'TOOLS.md'), templates.tools, 'utf-8');
|
|
149
|
+
console.log(`[subagent] Created agent dir with templates: ${agentDir}`);
|
|
150
|
+
}
|
|
151
|
+
// Register in config.agents.list if not already there (in-memory only)
|
|
152
|
+
if (!config.agents.list[preset.agentId]) {
|
|
153
|
+
const identity = AGENT_IDENTITIES[type];
|
|
154
|
+
config.agents.list[preset.agentId] = {
|
|
155
|
+
identity,
|
|
156
|
+
model: resolveModel(preset.defaultModel || 'anthropic/claude-sonnet-4-5', config),
|
|
157
|
+
thinking: 'medium'
|
|
158
|
+
};
|
|
159
|
+
console.log(`[subagent] Registered agent in config: ${preset.agentId}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Task tracking
|
|
163
|
+
let taskCounter = 0;
|
|
164
|
+
const tasks = new Map();
|
|
165
|
+
let deliverMessage = null;
|
|
166
|
+
export function initSubagentSystem(deliverFn) {
|
|
167
|
+
deliverMessage = deliverFn;
|
|
168
|
+
console.log('[subagent] System initialized');
|
|
169
|
+
}
|
|
170
|
+
export function getPresetDescriptions() {
|
|
171
|
+
return Object.entries(PRESETS)
|
|
172
|
+
.map(([type, preset]) => ` ${type} — ${preset.description} (default: ${preset.defaultModel || 'current model'})`)
|
|
173
|
+
.join('\n');
|
|
174
|
+
}
|
|
175
|
+
// --- Disk Registry ---
|
|
176
|
+
function ensureRegistryDir() {
|
|
177
|
+
const dir = join(homedir(), '.skimpyclaw', 'logs');
|
|
178
|
+
if (!existsSync(dir)) {
|
|
179
|
+
mkdirSync(dir, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function appendRegistryEvent(event) {
|
|
183
|
+
try {
|
|
184
|
+
ensureRegistryDir();
|
|
185
|
+
const line = JSON.stringify({ ...event, timestamp: new Date().toISOString() }) + '\n';
|
|
186
|
+
appendFileSync(REGISTRY_PATH, line, 'utf-8');
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
console.warn('[subagent] Failed to write registry event:', err instanceof Error ? err.message : err);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// --- Dispatch ---
|
|
193
|
+
export function dispatchSubagent(type, prompt, chatId, config, modelOverride, history, options) {
|
|
194
|
+
const maxConcurrent = config.subagents?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
|
|
195
|
+
const active = [...tasks.values()].filter((t) => t.status === 'running' || t.status === 'pending');
|
|
196
|
+
if (active.length >= maxConcurrent) {
|
|
197
|
+
throw new Error(`Max concurrent agents reached (${maxConcurrent}). Use /tasks to see running agents or /cancel to stop one.`);
|
|
198
|
+
}
|
|
199
|
+
const preset = PRESETS[type];
|
|
200
|
+
if (!preset) {
|
|
201
|
+
throw new Error(`Unknown agent type: ${type}. Use: ${Object.keys(PRESETS).join(', ')}`);
|
|
202
|
+
}
|
|
203
|
+
taskCounter++;
|
|
204
|
+
const id = `t${taskCounter}`;
|
|
205
|
+
const model = resolveModel(modelOverride || preset.defaultModel || getCurrentModel(), config);
|
|
206
|
+
const maxRetries = options?.maxRetries ?? config.subagents?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
207
|
+
const task = {
|
|
208
|
+
id,
|
|
209
|
+
type,
|
|
210
|
+
prompt,
|
|
211
|
+
status: 'pending',
|
|
212
|
+
chatId,
|
|
213
|
+
model,
|
|
214
|
+
label: options?.label,
|
|
215
|
+
createdAt: new Date(),
|
|
216
|
+
retryCount: 0,
|
|
217
|
+
maxRetries,
|
|
218
|
+
abortController: new AbortController()
|
|
219
|
+
};
|
|
220
|
+
tasks.set(id, task);
|
|
221
|
+
// Log to disk registry
|
|
222
|
+
appendRegistryEvent({
|
|
223
|
+
type: 'task_created',
|
|
224
|
+
taskId: id,
|
|
225
|
+
agentType: type,
|
|
226
|
+
model,
|
|
227
|
+
label: options?.label,
|
|
228
|
+
prompt: prompt.slice(0, 500),
|
|
229
|
+
});
|
|
230
|
+
// Build tool config with merged paths
|
|
231
|
+
const toolConfig = {
|
|
232
|
+
...preset.toolConfig,
|
|
233
|
+
allowedPaths: [
|
|
234
|
+
...preset.toolConfig.allowedPaths,
|
|
235
|
+
...(options?.allowedPaths || []),
|
|
236
|
+
],
|
|
237
|
+
};
|
|
238
|
+
// Fire and forget — don't await
|
|
239
|
+
executeTask(task, config, toolConfig, history).catch((err) => {
|
|
240
|
+
console.error(`[subagent] Unhandled error in task ${id}:`, err);
|
|
241
|
+
});
|
|
242
|
+
return task;
|
|
243
|
+
}
|
|
244
|
+
async function executeTask(task, config, toolConfig, history) {
|
|
245
|
+
task.status = 'running';
|
|
246
|
+
task.startedAt = new Date();
|
|
247
|
+
const label = task.label ? ` "${task.label}"` : '';
|
|
248
|
+
console.log(`[subagent] Starting ${task.id}${label} (${task.type}, model: ${task.model})`);
|
|
249
|
+
try {
|
|
250
|
+
// Check cancellation before starting
|
|
251
|
+
if (task.abortController.signal.aborted) {
|
|
252
|
+
task.status = 'cancelled';
|
|
253
|
+
task.completedAt = new Date();
|
|
254
|
+
appendRegistryEvent({ type: 'task_cancelled', taskId: task.id });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// Ensure agent dir + templates exist, register in config
|
|
258
|
+
ensureAgentSetup(task.type, config);
|
|
259
|
+
const response = await runAgentTurn(task.type, // agentId = preset.agentId = type name
|
|
260
|
+
task.prompt, config, task.model, toolConfig, history, {
|
|
261
|
+
channel: 'subagent',
|
|
262
|
+
sessionId: task.id,
|
|
263
|
+
metadata: {
|
|
264
|
+
type: task.type,
|
|
265
|
+
chatId: task.chatId,
|
|
266
|
+
label: task.label,
|
|
267
|
+
},
|
|
268
|
+
abortSignal: task.abortController.signal,
|
|
269
|
+
});
|
|
270
|
+
// Check cancellation after completion
|
|
271
|
+
if (task.abortController.signal.aborted) {
|
|
272
|
+
task.status = 'cancelled';
|
|
273
|
+
task.completedAt = new Date();
|
|
274
|
+
releaseAllLocks(task.id);
|
|
275
|
+
appendRegistryEvent({ type: 'task_cancelled', taskId: task.id });
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
task.status = 'completed';
|
|
279
|
+
task.result = response;
|
|
280
|
+
task.completedAt = new Date();
|
|
281
|
+
// Release any file locks held by this task
|
|
282
|
+
releaseAllLocks(task.id);
|
|
283
|
+
const elapsed = Math.round((task.completedAt.getTime() - task.startedAt.getTime()) / 1000);
|
|
284
|
+
console.log(`[subagent] Completed ${task.id}${label} in ${elapsed}s`);
|
|
285
|
+
appendRegistryEvent({
|
|
286
|
+
type: 'task_completed',
|
|
287
|
+
taskId: task.id,
|
|
288
|
+
elapsed,
|
|
289
|
+
resultLength: response.length,
|
|
290
|
+
});
|
|
291
|
+
// Deliver result
|
|
292
|
+
if (deliverMessage) {
|
|
293
|
+
const labelStr = task.label ? ` (${task.label})` : '';
|
|
294
|
+
const header = `✅ Agent ${task.id}${labelStr} completed in ${elapsed}s:`;
|
|
295
|
+
await deliverMessage(task.chatId, `${header}\n\n${response}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
// Release any file locks on error
|
|
300
|
+
releaseAllLocks(task.id);
|
|
301
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
302
|
+
const retryCount = task.retryCount ?? 0;
|
|
303
|
+
const maxRetries = task.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
304
|
+
// Retry if under limit and not cancelled
|
|
305
|
+
if (retryCount < maxRetries && !task.abortController.signal.aborted) {
|
|
306
|
+
task.retryCount = retryCount + 1;
|
|
307
|
+
const elapsed = Math.round((Date.now() - (task.startedAt?.getTime() || task.createdAt.getTime())) / 1000);
|
|
308
|
+
console.log(`[subagent] Task ${task.id} failed after ${elapsed}s (attempt ${retryCount + 1}/${maxRetries + 1}), retrying: ${errorMsg}`);
|
|
309
|
+
appendRegistryEvent({
|
|
310
|
+
type: 'task_retry',
|
|
311
|
+
taskId: task.id,
|
|
312
|
+
attempt: task.retryCount,
|
|
313
|
+
error: errorMsg,
|
|
314
|
+
});
|
|
315
|
+
if (deliverMessage) {
|
|
316
|
+
await deliverMessage(task.chatId, `⚠️ Agent ${task.id} failed (attempt ${retryCount + 1}/${maxRetries + 1}), retrying...\n\nError: ${errorMsg}`);
|
|
317
|
+
}
|
|
318
|
+
// Modify prompt to include error context for retry (compressed to save tokens)
|
|
319
|
+
const taskSummary = task.prompt.length > 500 ? task.prompt.slice(0, 500) + '...' : task.prompt;
|
|
320
|
+
const retryPrompt = `Retry task (attempt ${(task.retryCount ?? 0) + 1}).\n\nTask: ${taskSummary}\n\nPrevious error: ${errorMsg}\n\nTry a different approach.`;
|
|
321
|
+
task.prompt = retryPrompt;
|
|
322
|
+
task.startedAt = new Date();
|
|
323
|
+
// Retry
|
|
324
|
+
return executeTask(task, config, {
|
|
325
|
+
...PRESETS[task.type].toolConfig,
|
|
326
|
+
}, history);
|
|
327
|
+
}
|
|
328
|
+
task.status = 'failed';
|
|
329
|
+
task.error = errorMsg;
|
|
330
|
+
task.completedAt = new Date();
|
|
331
|
+
const elapsed = Math.round((task.completedAt.getTime() -
|
|
332
|
+
(task.startedAt?.getTime() || task.createdAt.getTime())) /
|
|
333
|
+
1000);
|
|
334
|
+
console.error(`[subagent] Failed ${task.id} after ${elapsed}s (all retries exhausted):`, task.error);
|
|
335
|
+
appendRegistryEvent({
|
|
336
|
+
type: 'task_failed',
|
|
337
|
+
taskId: task.id,
|
|
338
|
+
elapsed,
|
|
339
|
+
error: task.error,
|
|
340
|
+
});
|
|
341
|
+
if (deliverMessage) {
|
|
342
|
+
await deliverMessage(task.chatId, `❌ Agent ${task.id} (${task.type}) failed after ${elapsed}s:\n\n${task.error}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
export function cancelTask(id) {
|
|
347
|
+
const task = tasks.get(id);
|
|
348
|
+
if (!task)
|
|
349
|
+
return null;
|
|
350
|
+
if (task.status !== 'pending' && task.status !== 'running')
|
|
351
|
+
return task;
|
|
352
|
+
task.abortController.abort();
|
|
353
|
+
task.status = 'cancelled';
|
|
354
|
+
task.completedAt = new Date();
|
|
355
|
+
releaseAllLocks(task.id);
|
|
356
|
+
appendRegistryEvent({ type: 'task_cancelled', taskId: task.id });
|
|
357
|
+
console.log(`[subagent] Cancelled ${task.id}`);
|
|
358
|
+
return task;
|
|
359
|
+
}
|
|
360
|
+
export function getActiveTasks() {
|
|
361
|
+
return [...tasks.values()].filter((t) => t.status === 'pending' || t.status === 'running');
|
|
362
|
+
}
|
|
363
|
+
export function getRecentTasks(n = 10) {
|
|
364
|
+
return [...tasks.values()]
|
|
365
|
+
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
|
366
|
+
.slice(0, n);
|
|
367
|
+
}
|
|
368
|
+
export function getTask(id) {
|
|
369
|
+
return tasks.get(id) || null;
|
|
370
|
+
}
|
|
371
|
+
// For testing
|
|
372
|
+
export function resetForTesting() {
|
|
373
|
+
taskCounter = 0;
|
|
374
|
+
tasks.clear();
|
|
375
|
+
deliverMessage = null;
|
|
376
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
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';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { isBashCommandSafe } from '../security.js';
|
|
3
|
+
import { classifyCommandRisk, requiresApproval, createApprovalRequest, waitForApproval, } from '../exec-approval.js';
|
|
4
|
+
import { isPathAllowed } from './path-utils.js';
|
|
5
|
+
export async function executeBash(command, cwd, config, context) {
|
|
6
|
+
// Hard block: existing safety filter (always enforced)
|
|
7
|
+
if (!isBashCommandSafe(command)) {
|
|
8
|
+
return Promise.resolve('Error: Command blocked by safety filter.');
|
|
9
|
+
}
|
|
10
|
+
if (cwd && !isPathAllowed(cwd, config.allowedPaths)) {
|
|
11
|
+
return Promise.resolve('Error: Working directory not in allowed paths.');
|
|
12
|
+
}
|
|
13
|
+
// Exec approval gate: classify risk and check if approval is needed
|
|
14
|
+
const approvalConfig = config.execApproval;
|
|
15
|
+
if (approvalConfig?.enabled !== false) {
|
|
16
|
+
const classification = classifyCommandRisk(command);
|
|
17
|
+
if (requiresApproval(classification, approvalConfig)) {
|
|
18
|
+
// Build channel metadata from context for notification routing
|
|
19
|
+
const channelMeta = context?.channel
|
|
20
|
+
? {
|
|
21
|
+
channel: context.channel,
|
|
22
|
+
chatId: context.channelTargetId ?? context.chatId,
|
|
23
|
+
userId: context.approverUserId,
|
|
24
|
+
username: context.approverUsername,
|
|
25
|
+
}
|
|
26
|
+
: context?.chatId
|
|
27
|
+
? {
|
|
28
|
+
channel: 'telegram',
|
|
29
|
+
chatId: context.chatId,
|
|
30
|
+
}
|
|
31
|
+
: undefined;
|
|
32
|
+
// Create a pending approval request and wait for resolution
|
|
33
|
+
const ttlMs = approvalConfig?.ttlMs ?? 5 * 60 * 1000;
|
|
34
|
+
const request = createApprovalRequest(command, cwd, classification, approvalConfig, channelMeta);
|
|
35
|
+
const resolved = await waitForApproval(request.id, ttlMs);
|
|
36
|
+
if (resolved.status !== 'approved') {
|
|
37
|
+
return `⛔ Command not executed — approval ${resolved.status} (tier ${classification.tier}: ${classification.reason}).`;
|
|
38
|
+
}
|
|
39
|
+
// Approved — fall through to execution below
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const timeout = config.bashTimeout || 30_000;
|
|
43
|
+
return new Promise((res) => {
|
|
44
|
+
exec(command, {
|
|
45
|
+
cwd: cwd || undefined,
|
|
46
|
+
timeout,
|
|
47
|
+
env: { ...process.env },
|
|
48
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
49
|
+
}, (error, stdout, stderr) => {
|
|
50
|
+
if (error) {
|
|
51
|
+
const parts = [stdout, stderr, `Exit code: ${error.code ?? 'unknown'}`].filter(Boolean);
|
|
52
|
+
res(parts.join('\n').slice(0, 50_000));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const output = [stdout, stderr].filter(Boolean).join('\n');
|
|
56
|
+
res(output.slice(0, 50_000) || '(no output)');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|