skimpyclaw 0.3.6 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -6
- package/dist/__tests__/api.test.js +1 -0
- package/dist/__tests__/channels.test.js +1 -1
- package/dist/__tests__/code-agents-orchestrator.test.js +74 -7
- package/dist/__tests__/code-agents-preflight.test.d.ts +1 -0
- package/dist/__tests__/code-agents-preflight.test.js +88 -0
- package/dist/__tests__/code-agents-sandbox.test.d.ts +1 -0
- package/dist/__tests__/code-agents-sandbox.test.js +163 -0
- package/dist/__tests__/code-agents-utils.test.js +12 -1
- package/dist/__tests__/context-manager.test.d.ts +1 -0
- package/dist/__tests__/context-manager.test.js +236 -0
- package/dist/__tests__/package-manager-detection.test.js +5 -5
- package/dist/__tests__/setup.test.js +7 -5
- package/dist/__tests__/skills.test.js +2 -2
- package/dist/__tests__/structured-context.test.d.ts +1 -0
- package/dist/__tests__/structured-context.test.js +100 -0
- package/dist/__tests__/tools.test.js +65 -3
- package/dist/agent.js +4 -5
- package/dist/api.js +10 -58
- package/dist/audit.js +5 -51
- package/dist/channels/telegram/handlers.js +2 -60
- package/dist/channels/telegram/index.js +0 -7
- package/dist/channels.js +1 -1
- package/dist/cli.js +151 -16
- package/dist/code-agents/executor.d.ts +9 -4
- package/dist/code-agents/executor.js +187 -13
- package/dist/code-agents/index.d.ts +1 -1
- package/dist/code-agents/index.js +30 -22
- package/dist/code-agents/orchestrator.d.ts +8 -2
- package/dist/code-agents/orchestrator.js +318 -27
- package/dist/code-agents/structured-context.d.ts +7 -0
- package/dist/code-agents/structured-context.js +54 -0
- package/dist/code-agents/types.d.ts +2 -0
- package/dist/code-agents/utils.d.ts +4 -0
- package/dist/code-agents/utils.js +38 -2
- package/dist/code-agents/worktree.d.ts +40 -0
- package/dist/code-agents/worktree.js +215 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +5 -3
- package/dist/cron.js +18 -4
- package/dist/dashboard/assets/{index-CkonC7Cd.js → index-BoTHPby4.js} +20 -20
- package/dist/dashboard/assets/{index-EAg6lqF5.css → index-D4mufvBg.css} +1 -1
- package/dist/dashboard/index.html +2 -2
- package/dist/discord.js +4 -40
- package/dist/exec-approval.js +1 -1
- package/dist/file-lock.js +1 -1
- package/dist/gateway.js +3 -10
- package/dist/providers/anthropic.js +9 -5
- package/dist/providers/codex.js +10 -6
- package/dist/providers/context-manager.d.ts +22 -0
- package/dist/providers/context-manager.js +100 -0
- package/dist/providers/openai.js +9 -5
- package/dist/providers/types.d.ts +1 -0
- package/dist/security.js +9 -0
- package/dist/setup.js +122 -27
- package/dist/skills.js +9 -2
- package/dist/subagent.js +33 -2
- package/dist/tools/bash-tool.js +8 -0
- package/dist/tools/browser-tool.js +2 -1
- package/dist/tools/definitions.d.ts +0 -27
- package/dist/tools/definitions.js +0 -18
- package/dist/tools/execute-context.d.ts +4 -4
- package/dist/tools/file-tools.d.ts +1 -1
- package/dist/tools/file-tools.js +1 -1
- package/dist/tools.d.ts +5 -5
- package/dist/tools.js +87 -98
- package/dist/types.d.ts +14 -22
- package/dist/usage.d.ts +1 -0
- package/dist/usage.js +30 -46
- package/dist/utils.d.ts +18 -0
- package/dist/utils.js +71 -0
- package/dist/voice.js +9 -7
- package/package.json +26 -21
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// Git worktree isolation for parallel coding agents.
|
|
2
|
+
// Each parallel child gets its own worktree so they can't overwrite each other.
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { existsSync, rmSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
/**
|
|
7
|
+
* Check if a directory is inside a git repo.
|
|
8
|
+
*/
|
|
9
|
+
export function isGitRepo(workdir) {
|
|
10
|
+
try {
|
|
11
|
+
execSync('git rev-parse --is-inside-work-tree', {
|
|
12
|
+
cwd: workdir,
|
|
13
|
+
timeout: 5000,
|
|
14
|
+
encoding: 'utf-8',
|
|
15
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
16
|
+
});
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get the git repo root directory.
|
|
25
|
+
*/
|
|
26
|
+
export function getGitRoot(workdir) {
|
|
27
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
28
|
+
cwd: workdir,
|
|
29
|
+
timeout: 5000,
|
|
30
|
+
encoding: 'utf-8',
|
|
31
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
32
|
+
}).trim();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Stage and commit all current changes so worktrees branch from a clean state.
|
|
36
|
+
* Returns the commit hash, or null if nothing to commit.
|
|
37
|
+
*/
|
|
38
|
+
export function commitPendingChanges(workdir, message) {
|
|
39
|
+
try {
|
|
40
|
+
// Check for any changes (staged or unstaged)
|
|
41
|
+
const status = execSync('git status --porcelain', {
|
|
42
|
+
cwd: workdir,
|
|
43
|
+
timeout: 5000,
|
|
44
|
+
encoding: 'utf-8',
|
|
45
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
46
|
+
}).trim();
|
|
47
|
+
if (!status)
|
|
48
|
+
return null;
|
|
49
|
+
execSync('git add -A && git commit -m ' + JSON.stringify(message), {
|
|
50
|
+
cwd: workdir,
|
|
51
|
+
timeout: 10000,
|
|
52
|
+
encoding: 'utf-8',
|
|
53
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
54
|
+
});
|
|
55
|
+
return execSync('git rev-parse HEAD', {
|
|
56
|
+
cwd: workdir,
|
|
57
|
+
timeout: 5000,
|
|
58
|
+
encoding: 'utf-8',
|
|
59
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
60
|
+
}).trim();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Create a git worktree for a child agent.
|
|
68
|
+
* Creates a new branch and worktree directory under .skimpyclaw-worktrees/.
|
|
69
|
+
*/
|
|
70
|
+
export function createWorktree(workdir, childId) {
|
|
71
|
+
const gitRoot = getGitRoot(workdir);
|
|
72
|
+
const worktreeDir = join(gitRoot, '.skimpyclaw-worktrees', childId);
|
|
73
|
+
const branch = `skimpyclaw-team/${childId}`;
|
|
74
|
+
// Clean up stale worktree/branch if they exist
|
|
75
|
+
try {
|
|
76
|
+
execSync(`git worktree remove --force ${JSON.stringify(worktreeDir)}`, {
|
|
77
|
+
cwd: gitRoot,
|
|
78
|
+
timeout: 10000,
|
|
79
|
+
encoding: 'utf-8',
|
|
80
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch { /* didn't exist */ }
|
|
84
|
+
try {
|
|
85
|
+
execSync(`git branch -D ${JSON.stringify(branch)}`, {
|
|
86
|
+
cwd: gitRoot,
|
|
87
|
+
timeout: 5000,
|
|
88
|
+
encoding: 'utf-8',
|
|
89
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch { /* didn't exist */ }
|
|
93
|
+
// Create worktree on a new branch from HEAD
|
|
94
|
+
execSync(`git worktree add -b ${JSON.stringify(branch)} ${JSON.stringify(worktreeDir)} HEAD`, {
|
|
95
|
+
cwd: gitRoot,
|
|
96
|
+
timeout: 15000,
|
|
97
|
+
encoding: 'utf-8',
|
|
98
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
99
|
+
});
|
|
100
|
+
return { path: worktreeDir, branch };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Merge a child's worktree branch back into the current branch.
|
|
104
|
+
* Returns { merged: true } on success, { merged: false, conflict: string } on conflict.
|
|
105
|
+
*/
|
|
106
|
+
export function mergeWorktree(workdir, branch, childId) {
|
|
107
|
+
const gitRoot = getGitRoot(workdir);
|
|
108
|
+
try {
|
|
109
|
+
// First check if the child branch has any changes compared to where it branched
|
|
110
|
+
const diff = execSync(`git diff HEAD...${JSON.stringify(branch)} --stat`, {
|
|
111
|
+
cwd: gitRoot,
|
|
112
|
+
timeout: 10000,
|
|
113
|
+
encoding: 'utf-8',
|
|
114
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
115
|
+
}).trim();
|
|
116
|
+
if (!diff) {
|
|
117
|
+
// No changes on child branch — nothing to merge
|
|
118
|
+
return { merged: true };
|
|
119
|
+
}
|
|
120
|
+
execSync(`git merge --no-edit ${JSON.stringify(branch)}`, {
|
|
121
|
+
cwd: gitRoot,
|
|
122
|
+
timeout: 15000,
|
|
123
|
+
encoding: 'utf-8',
|
|
124
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
125
|
+
});
|
|
126
|
+
return { merged: true };
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
// Merge conflict — abort and report
|
|
130
|
+
try {
|
|
131
|
+
execSync('git merge --abort', {
|
|
132
|
+
cwd: gitRoot,
|
|
133
|
+
timeout: 5000,
|
|
134
|
+
encoding: 'utf-8',
|
|
135
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch { /* no merge in progress */ }
|
|
139
|
+
const conflict = err instanceof Error ? err.message : String(err);
|
|
140
|
+
return { merged: false, conflict: conflict.slice(0, 2000) };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Remove a worktree and its branch.
|
|
145
|
+
*/
|
|
146
|
+
export function removeWorktree(workdir, childId, branch) {
|
|
147
|
+
const gitRoot = getGitRoot(workdir);
|
|
148
|
+
const worktreeDir = join(gitRoot, '.skimpyclaw-worktrees', childId);
|
|
149
|
+
try {
|
|
150
|
+
execSync(`git worktree remove --force ${JSON.stringify(worktreeDir)}`, {
|
|
151
|
+
cwd: gitRoot,
|
|
152
|
+
timeout: 10000,
|
|
153
|
+
encoding: 'utf-8',
|
|
154
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch { /* already removed */ }
|
|
158
|
+
// Clean up directory if git worktree remove didn't
|
|
159
|
+
if (existsSync(worktreeDir)) {
|
|
160
|
+
rmSync(worktreeDir, { recursive: true, force: true });
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
execSync(`git branch -D ${JSON.stringify(branch)}`, {
|
|
164
|
+
cwd: gitRoot,
|
|
165
|
+
timeout: 5000,
|
|
166
|
+
encoding: 'utf-8',
|
|
167
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch { /* already deleted */ }
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Clean up all skimpyclaw worktrees in a repo (e.g. on crash recovery).
|
|
174
|
+
*/
|
|
175
|
+
export function cleanupAllWorktrees(workdir) {
|
|
176
|
+
const gitRoot = getGitRoot(workdir);
|
|
177
|
+
const worktreeBaseDir = join(gitRoot, '.skimpyclaw-worktrees');
|
|
178
|
+
// Prune stale worktree references
|
|
179
|
+
try {
|
|
180
|
+
execSync('git worktree prune', {
|
|
181
|
+
cwd: gitRoot,
|
|
182
|
+
timeout: 10000,
|
|
183
|
+
encoding: 'utf-8',
|
|
184
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
catch { /* ignore */ }
|
|
188
|
+
// Remove the directory
|
|
189
|
+
if (existsSync(worktreeBaseDir)) {
|
|
190
|
+
rmSync(worktreeBaseDir, { recursive: true, force: true });
|
|
191
|
+
}
|
|
192
|
+
// Delete all skimpyclaw-team/* branches
|
|
193
|
+
try {
|
|
194
|
+
const branches = execSync('git branch --list "skimpyclaw-team/*"', {
|
|
195
|
+
cwd: gitRoot,
|
|
196
|
+
timeout: 5000,
|
|
197
|
+
encoding: 'utf-8',
|
|
198
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
199
|
+
}).trim();
|
|
200
|
+
if (branches) {
|
|
201
|
+
for (const b of branches.split('\n').map(s => s.trim()).filter(Boolean)) {
|
|
202
|
+
try {
|
|
203
|
+
execSync(`git branch -D ${JSON.stringify(b)}`, {
|
|
204
|
+
cwd: gitRoot,
|
|
205
|
+
timeout: 5000,
|
|
206
|
+
encoding: 'utf-8',
|
|
207
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch { /* ignore */ }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch { /* ignore */ }
|
|
215
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Config } from './types.js';
|
|
|
2
2
|
export declare function loadConfig(): Config;
|
|
3
3
|
export declare function loadRawConfig(): Record<string, any>;
|
|
4
4
|
export declare function getConfigPath(): string;
|
|
5
|
+
export declare function isValidAgentId(agentId: string): boolean;
|
|
5
6
|
export declare function getAgentDir(agentId: string): string;
|
|
6
7
|
export declare function getLogsDir(): string;
|
|
7
8
|
export declare function getSessionsDir(): string;
|
package/dist/config.js
CHANGED
|
@@ -54,8 +54,11 @@ export function loadRawConfig() {
|
|
|
54
54
|
export function getConfigPath() {
|
|
55
55
|
return CONFIG_PATH;
|
|
56
56
|
}
|
|
57
|
+
export function isValidAgentId(agentId) {
|
|
58
|
+
return /^[a-zA-Z0-9_-]+$/.test(agentId);
|
|
59
|
+
}
|
|
57
60
|
export function getAgentDir(agentId) {
|
|
58
|
-
if (
|
|
61
|
+
if (!isValidAgentId(agentId)) {
|
|
59
62
|
throw new Error('Invalid agent ID');
|
|
60
63
|
}
|
|
61
64
|
return join(homedir(), '.skimpyclaw', 'agents', agentId);
|
|
@@ -107,8 +110,7 @@ export function listMemoryFiles(agentId) {
|
|
|
107
110
|
}).sort((a, b) => b.date.localeCompare(a.date));
|
|
108
111
|
}
|
|
109
112
|
export function readMemoryFile(agentId, filename) {
|
|
110
|
-
|
|
111
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) {
|
|
113
|
+
if (!isValidAgentId(agentId)) {
|
|
112
114
|
throw new Error('Invalid agent ID');
|
|
113
115
|
}
|
|
114
116
|
// Validate no path traversal
|
package/dist/cron.js
CHANGED
|
@@ -10,6 +10,20 @@ import { startTrace, addEvent, endTrace } from './audit.js';
|
|
|
10
10
|
import { sendActiveChannelProactiveMessage, sendActiveChannelProactiveVoice, getActiveChannelId } from './channels.js';
|
|
11
11
|
import { parseAndSaveDigest } from './digests.js';
|
|
12
12
|
import { synthesizeSpeech } from './voice.js';
|
|
13
|
+
import { toErrorMessage } from './utils.js';
|
|
14
|
+
function safeTimezone(tz) {
|
|
15
|
+
const fallback = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
16
|
+
if (!tz)
|
|
17
|
+
return fallback;
|
|
18
|
+
try {
|
|
19
|
+
Intl.DateTimeFormat(undefined, { timeZone: tz });
|
|
20
|
+
return tz;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
console.warn(`[cron] Invalid timezone "${tz}", falling back to ${fallback}`);
|
|
24
|
+
return fallback;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
13
27
|
import { ensureContainer, SANDBOX_DEFAULTS, sandboxBash } from './sandbox/index.js';
|
|
14
28
|
const scheduledJobs = new Map();
|
|
15
29
|
let configWatcher = null;
|
|
@@ -100,7 +114,7 @@ function scheduleJob(jobDef, config) {
|
|
|
100
114
|
return;
|
|
101
115
|
}
|
|
102
116
|
const cronJob = new Cron(jobDef.schedule.expr, {
|
|
103
|
-
timezone: jobDef.schedule.tz
|
|
117
|
+
timezone: safeTimezone(jobDef.schedule.tz),
|
|
104
118
|
}, async () => {
|
|
105
119
|
console.log(`[cron] Running job: ${jobDef.name}`);
|
|
106
120
|
await executeJobPayload(jobDef, config);
|
|
@@ -235,7 +249,7 @@ async function executeJobPayload(jobDef, config) {
|
|
|
235
249
|
appendCronLogLine(jobDef.id, `=== COMPLETED: success ===`);
|
|
236
250
|
}
|
|
237
251
|
catch (err) {
|
|
238
|
-
const msg =
|
|
252
|
+
const msg = toErrorMessage(err);
|
|
239
253
|
logEntry.status = msg.includes('TIMEOUT') || msg.includes('timed out') ? 'timeout' : 'error';
|
|
240
254
|
logEntry.error = msg;
|
|
241
255
|
appendCronLogLine(jobDef.id, `=== FAILED: ${logEntry.status} — ${msg.slice(0, 200)} ===`);
|
|
@@ -337,14 +351,14 @@ function resolveMessageSource(message) {
|
|
|
337
351
|
return message;
|
|
338
352
|
// Expand ~ to home directory
|
|
339
353
|
const resolved = trimmed.startsWith('~/')
|
|
340
|
-
? join(
|
|
354
|
+
? join(homedir(), trimmed.slice(2))
|
|
341
355
|
: trimmed;
|
|
342
356
|
if (existsSync(resolved)) {
|
|
343
357
|
console.log(`[cron] Loading prompt from file: ${resolved}`);
|
|
344
358
|
return readFileSync(resolved, 'utf-8');
|
|
345
359
|
}
|
|
346
360
|
// Fallback: check ~/.skimpyclaw/prompts/ directory
|
|
347
|
-
const promptsDir = join(
|
|
361
|
+
const promptsDir = join(homedir(), '.skimpyclaw', 'prompts', trimmed);
|
|
348
362
|
if (existsSync(promptsDir)) {
|
|
349
363
|
console.log(`[cron] Loading prompt from prompts dir: ${promptsDir}`);
|
|
350
364
|
return readFileSync(promptsDir, 'utf-8');
|