skimpyclaw 0.3.4 → 0.3.6
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/dist/__tests__/doctor.runner.test.js +5 -1
- package/dist/__tests__/package-manager-detection.test.d.ts +1 -0
- package/dist/__tests__/package-manager-detection.test.js +172 -0
- package/dist/__tests__/setup.test.js +3 -2
- package/dist/cli.js +43 -1
- package/dist/code-agents/executor.d.ts +16 -0
- package/dist/code-agents/executor.js +77 -3
- package/dist/code-agents/utils.js +7 -3
- package/dist/doctor/checks.d.ts +1 -0
- package/dist/doctor/checks.js +21 -5
- package/dist/doctor/runner.js +2 -1
- package/dist/setup.d.ts +2 -1
- package/dist/setup.js +86 -20
- package/dist/tools/browser-tool.js +1 -1
- package/dist/tools/definitions.js +2 -2
- package/package.json +22 -26
- package/sandbox/Dockerfile +40 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
const { mockLoadConfig, mockCheckNodeVersion, mockCheckPackageManagerAvailable, mockCheckTypeScriptCompile, mockCheckConfigExistsAndValidJson, mockCheckRequiredEnvVars, mockCheckEnvVarPatterns, mockCheckAllowedPathsWritable, mockCheckProviderAuth, mockCheckTelegramToken, mockCheckDiscordToken, mockCheckBrowserBinaryIfEnabled, mockCheckVoiceDependencies, mockCheckMcpConfig, mockCheckGatewayHostBindable, mockCheckSkimpyclawDirWritable, mockCheckPortAvailability, mockCheckSandboxAvailable, } = vi.hoisted(() => ({
|
|
2
|
+
const { mockLoadConfig, mockCheckNodeVersion, mockCheckPackageManagerAvailable, mockCheckTypeScriptCompile, mockCheckConfigExistsAndValidJson, mockCheckRequiredEnvVars, mockCheckEnvVarPatterns, mockCheckAllowedPathsWritable, mockCheckProviderAuth, mockCheckTelegramToken, mockCheckDiscordToken, mockCheckBrowserBinaryIfEnabled, mockCheckPlaywrightIfBrowserEnabled, mockCheckVoiceDependencies, mockCheckMcpConfig, mockCheckGatewayHostBindable, mockCheckSkimpyclawDirWritable, mockCheckPortAvailability, mockCheckSandboxAvailable, } = vi.hoisted(() => ({
|
|
3
3
|
mockLoadConfig: vi.fn(),
|
|
4
4
|
mockCheckNodeVersion: vi.fn(),
|
|
5
5
|
mockCheckPackageManagerAvailable: vi.fn(),
|
|
@@ -12,6 +12,7 @@ const { mockLoadConfig, mockCheckNodeVersion, mockCheckPackageManagerAvailable,
|
|
|
12
12
|
mockCheckTelegramToken: vi.fn(),
|
|
13
13
|
mockCheckDiscordToken: vi.fn(),
|
|
14
14
|
mockCheckBrowserBinaryIfEnabled: vi.fn(),
|
|
15
|
+
mockCheckPlaywrightIfBrowserEnabled: vi.fn(),
|
|
15
16
|
mockCheckVoiceDependencies: vi.fn(),
|
|
16
17
|
mockCheckMcpConfig: vi.fn(),
|
|
17
18
|
mockCheckGatewayHostBindable: vi.fn(),
|
|
@@ -34,6 +35,7 @@ vi.mock('../doctor/checks.js', () => ({
|
|
|
34
35
|
checkTelegramToken: mockCheckTelegramToken,
|
|
35
36
|
checkDiscordToken: mockCheckDiscordToken,
|
|
36
37
|
checkBrowserBinaryIfEnabled: mockCheckBrowserBinaryIfEnabled,
|
|
38
|
+
checkPlaywrightIfBrowserEnabled: mockCheckPlaywrightIfBrowserEnabled,
|
|
37
39
|
checkVoiceDependencies: mockCheckVoiceDependencies,
|
|
38
40
|
checkMcpConfig: mockCheckMcpConfig,
|
|
39
41
|
checkGatewayHostBindable: mockCheckGatewayHostBindable,
|
|
@@ -75,6 +77,7 @@ describe('doctor runner', () => {
|
|
|
75
77
|
mockCheckTelegramToken.mockReset();
|
|
76
78
|
mockCheckDiscordToken.mockReset();
|
|
77
79
|
mockCheckBrowserBinaryIfEnabled.mockReset();
|
|
80
|
+
mockCheckPlaywrightIfBrowserEnabled.mockReset();
|
|
78
81
|
mockCheckVoiceDependencies.mockReset();
|
|
79
82
|
mockCheckMcpConfig.mockReset();
|
|
80
83
|
mockCheckGatewayHostBindable.mockReset();
|
|
@@ -92,6 +95,7 @@ describe('doctor runner', () => {
|
|
|
92
95
|
mockCheckTelegramToken.mockResolvedValue(okCheck('telegram_token_valid', 'channels'));
|
|
93
96
|
mockCheckDiscordToken.mockResolvedValue(okCheck('discord_token_valid', 'channels'));
|
|
94
97
|
mockCheckBrowserBinaryIfEnabled.mockResolvedValue(okCheck('browser_binary_available', 'runtime'));
|
|
98
|
+
mockCheckPlaywrightIfBrowserEnabled.mockResolvedValue(okCheck('playwright_installed', 'runtime', 'Browser tools disabled'));
|
|
95
99
|
mockCheckVoiceDependencies.mockResolvedValue(okCheck('voice_dependencies', 'runtime', 'Voice disabled'));
|
|
96
100
|
mockCheckMcpConfig.mockResolvedValue(okCheck('mcp_config', 'runtime', 'MCP tools not configured'));
|
|
97
101
|
mockCheckGatewayHostBindable.mockResolvedValue(okCheck('gateway_host_bindable', 'runtime', '127.0.0.1 (always available)'));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'fs';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
// Import the real functions (no mocks needed — these are pure fs reads)
|
|
6
|
+
import { detectPackageManager, buildValidationCommand } from '../code-agents/executor.js';
|
|
7
|
+
describe('detectPackageManager', () => {
|
|
8
|
+
let tempDir;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
tempDir = mkdtempSync(join(tmpdir(), 'pm-detect-'));
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
14
|
+
});
|
|
15
|
+
it('returns pnpm as fallback when no indicators present', () => {
|
|
16
|
+
expect(detectPackageManager(tempDir)).toBe('pnpm');
|
|
17
|
+
});
|
|
18
|
+
it('detects yarn from packageManager field', () => {
|
|
19
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
|
|
20
|
+
name: 'test',
|
|
21
|
+
packageManager: 'yarn@4.1.0',
|
|
22
|
+
}));
|
|
23
|
+
expect(detectPackageManager(tempDir)).toBe('yarn');
|
|
24
|
+
});
|
|
25
|
+
it('detects pnpm from packageManager field', () => {
|
|
26
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
|
|
27
|
+
name: 'test',
|
|
28
|
+
packageManager: 'pnpm@9.0.0',
|
|
29
|
+
}));
|
|
30
|
+
expect(detectPackageManager(tempDir)).toBe('pnpm');
|
|
31
|
+
});
|
|
32
|
+
it('detects npm from packageManager field', () => {
|
|
33
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
|
|
34
|
+
name: 'test',
|
|
35
|
+
packageManager: 'npm@10.0.0',
|
|
36
|
+
}));
|
|
37
|
+
expect(detectPackageManager(tempDir)).toBe('npm');
|
|
38
|
+
});
|
|
39
|
+
it('detects bun from packageManager field', () => {
|
|
40
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
|
|
41
|
+
name: 'test',
|
|
42
|
+
packageManager: 'bun@1.0.0',
|
|
43
|
+
}));
|
|
44
|
+
expect(detectPackageManager(tempDir)).toBe('bun');
|
|
45
|
+
});
|
|
46
|
+
it('detects yarn from yarn.lock', () => {
|
|
47
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({ name: 'test' }));
|
|
48
|
+
writeFileSync(join(tempDir, 'yarn.lock'), '');
|
|
49
|
+
expect(detectPackageManager(tempDir)).toBe('yarn');
|
|
50
|
+
});
|
|
51
|
+
it('detects pnpm from pnpm-lock.yaml', () => {
|
|
52
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({ name: 'test' }));
|
|
53
|
+
writeFileSync(join(tempDir, 'pnpm-lock.yaml'), '');
|
|
54
|
+
expect(detectPackageManager(tempDir)).toBe('pnpm');
|
|
55
|
+
});
|
|
56
|
+
it('detects npm from package-lock.json', () => {
|
|
57
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({ name: 'test' }));
|
|
58
|
+
writeFileSync(join(tempDir, 'package-lock.json'), '{}');
|
|
59
|
+
expect(detectPackageManager(tempDir)).toBe('npm');
|
|
60
|
+
});
|
|
61
|
+
it('detects bun from bun.lockb', () => {
|
|
62
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({ name: 'test' }));
|
|
63
|
+
writeFileSync(join(tempDir, 'bun.lockb'), '');
|
|
64
|
+
expect(detectPackageManager(tempDir)).toBe('bun');
|
|
65
|
+
});
|
|
66
|
+
it('detects bun from bun.lock', () => {
|
|
67
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({ name: 'test' }));
|
|
68
|
+
writeFileSync(join(tempDir, 'bun.lock'), '');
|
|
69
|
+
expect(detectPackageManager(tempDir)).toBe('bun');
|
|
70
|
+
});
|
|
71
|
+
it('packageManager field takes priority over lockfile', () => {
|
|
72
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
|
|
73
|
+
name: 'test',
|
|
74
|
+
packageManager: 'yarn@4.1.0',
|
|
75
|
+
}));
|
|
76
|
+
// Also has a pnpm lockfile — packageManager should win
|
|
77
|
+
writeFileSync(join(tempDir, 'pnpm-lock.yaml'), '');
|
|
78
|
+
expect(detectPackageManager(tempDir)).toBe('yarn');
|
|
79
|
+
});
|
|
80
|
+
it('handles malformed package.json gracefully', () => {
|
|
81
|
+
writeFileSync(join(tempDir, 'package.json'), 'not valid json{{{');
|
|
82
|
+
expect(detectPackageManager(tempDir)).toBe('pnpm');
|
|
83
|
+
});
|
|
84
|
+
// wp-calypso case: yarn project with yarn.lock
|
|
85
|
+
it('detects yarn for wp-calypso-like project', () => {
|
|
86
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
|
|
87
|
+
name: 'wp-calypso',
|
|
88
|
+
scripts: { build: 'yarn build:client', test: 'jest' },
|
|
89
|
+
}));
|
|
90
|
+
writeFileSync(join(tempDir, 'yarn.lock'), '# yarn lockfile v1\n');
|
|
91
|
+
expect(detectPackageManager(tempDir)).toBe('yarn');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe('buildValidationCommand', () => {
|
|
95
|
+
let tempDir;
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
tempDir = mkdtempSync(join(tmpdir(), 'pm-cmd-'));
|
|
98
|
+
});
|
|
99
|
+
afterEach(() => {
|
|
100
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
101
|
+
});
|
|
102
|
+
it('returns pnpm build && pnpm test for pnpm project with both scripts', () => {
|
|
103
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
|
|
104
|
+
name: 'test',
|
|
105
|
+
scripts: { build: 'tsc', test: 'vitest' },
|
|
106
|
+
}));
|
|
107
|
+
writeFileSync(join(tempDir, 'pnpm-lock.yaml'), '');
|
|
108
|
+
expect(buildValidationCommand(tempDir)).toBe('pnpm build && pnpm test');
|
|
109
|
+
});
|
|
110
|
+
it('returns yarn build && yarn test for yarn project with both scripts', () => {
|
|
111
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
|
|
112
|
+
name: 'test',
|
|
113
|
+
scripts: { build: 'tsc', test: 'jest' },
|
|
114
|
+
}));
|
|
115
|
+
writeFileSync(join(tempDir, 'yarn.lock'), '');
|
|
116
|
+
expect(buildValidationCommand(tempDir)).toBe('yarn build && yarn test');
|
|
117
|
+
});
|
|
118
|
+
it('returns npm run build && npm run test for npm project with both scripts', () => {
|
|
119
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
|
|
120
|
+
name: 'test',
|
|
121
|
+
scripts: { build: 'tsc', test: 'jest' },
|
|
122
|
+
}));
|
|
123
|
+
writeFileSync(join(tempDir, 'package-lock.json'), '{}');
|
|
124
|
+
expect(buildValidationCommand(tempDir)).toBe('npm run build && npm run test');
|
|
125
|
+
});
|
|
126
|
+
it('returns bun build && bun test for bun project with both scripts', () => {
|
|
127
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
|
|
128
|
+
name: 'test',
|
|
129
|
+
scripts: { build: 'bun build', test: 'bun test' },
|
|
130
|
+
}));
|
|
131
|
+
writeFileSync(join(tempDir, 'bun.lockb'), '');
|
|
132
|
+
expect(buildValidationCommand(tempDir)).toBe('bun build && bun test');
|
|
133
|
+
});
|
|
134
|
+
it('returns only test command when build script is missing', () => {
|
|
135
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
|
|
136
|
+
name: 'test',
|
|
137
|
+
scripts: { test: 'jest' },
|
|
138
|
+
}));
|
|
139
|
+
writeFileSync(join(tempDir, 'yarn.lock'), '');
|
|
140
|
+
expect(buildValidationCommand(tempDir)).toBe('yarn test');
|
|
141
|
+
});
|
|
142
|
+
it('returns only build command when test script is missing', () => {
|
|
143
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
|
|
144
|
+
name: 'test',
|
|
145
|
+
scripts: { build: 'tsc' },
|
|
146
|
+
}));
|
|
147
|
+
writeFileSync(join(tempDir, 'yarn.lock'), '');
|
|
148
|
+
expect(buildValidationCommand(tempDir)).toBe('yarn build');
|
|
149
|
+
});
|
|
150
|
+
it('falls back to both commands when no package.json exists', () => {
|
|
151
|
+
// No package.json, no lockfile → pnpm fallback
|
|
152
|
+
expect(buildValidationCommand(tempDir)).toBe('pnpm build && pnpm test');
|
|
153
|
+
});
|
|
154
|
+
it('falls back to both commands when scripts object is empty', () => {
|
|
155
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
|
|
156
|
+
name: 'test',
|
|
157
|
+
scripts: {},
|
|
158
|
+
}));
|
|
159
|
+
writeFileSync(join(tempDir, 'yarn.lock'), '');
|
|
160
|
+
expect(buildValidationCommand(tempDir)).toBe('yarn build && yarn test');
|
|
161
|
+
});
|
|
162
|
+
// wp-calypso scenario
|
|
163
|
+
it('generates yarn commands for wp-calypso-like project', () => {
|
|
164
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
|
|
165
|
+
name: 'wp-calypso',
|
|
166
|
+
packageManager: 'yarn@4.5.0',
|
|
167
|
+
scripts: { build: 'yarn workspaces foreach build', test: 'jest' },
|
|
168
|
+
}));
|
|
169
|
+
writeFileSync(join(tempDir, 'yarn.lock'), '# yarn lockfile v1\n');
|
|
170
|
+
expect(buildValidationCommand(tempDir)).toBe('yarn build && yarn test');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -128,8 +128,9 @@ describe('setup config generation', () => {
|
|
|
128
128
|
cronWeather: true,
|
|
129
129
|
timezone: 'America/New_York',
|
|
130
130
|
weatherLocation: 'Austin, TX',
|
|
131
|
-
skillCodeReview: true,
|
|
132
131
|
skillDailyNotes: true,
|
|
132
|
+
skillWeather: true,
|
|
133
|
+
skillWebSearch: false,
|
|
133
134
|
},
|
|
134
135
|
});
|
|
135
136
|
expect(config.cron.jobs).toHaveLength(2);
|
|
@@ -138,7 +139,7 @@ describe('setup config generation', () => {
|
|
|
138
139
|
expect(config.cron.jobs[1].schedule.tz).toBe('America/New_York');
|
|
139
140
|
expect(config.cron.jobs[1].payload.message).toContain('Austin, TX');
|
|
140
141
|
expect(config.skills.enabled).toBe(true);
|
|
141
|
-
expect(config.skills.entries['code-review']).toBe(true);
|
|
142
142
|
expect(config.skills.entries['daily-notes']).toBe(true);
|
|
143
|
+
expect(config.skills.entries['weather']).toBe(true);
|
|
143
144
|
});
|
|
144
145
|
});
|
package/dist/cli.js
CHANGED
|
@@ -780,10 +780,18 @@ function sandboxImageExists(runtime, image) {
|
|
|
780
780
|
return spawnSync(runtime, ['image', 'inspect', image], { encoding: 'utf-8' }).status === 0;
|
|
781
781
|
}
|
|
782
782
|
function resolveSandboxDir() {
|
|
783
|
+
// 1. Check CWD (user is in repo root)
|
|
783
784
|
const cwdSandbox = join(process.cwd(), 'sandbox');
|
|
784
785
|
if (existsSync(join(cwdSandbox, 'Dockerfile'))) {
|
|
785
786
|
return cwdSandbox;
|
|
786
787
|
}
|
|
788
|
+
// 2. Check relative to package root (global/npm install)
|
|
789
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
790
|
+
const pkgRoot = join(thisFile, '..', '..'); // dist/src/cli.js -> repo root
|
|
791
|
+
const pkgSandbox = join(pkgRoot, 'sandbox');
|
|
792
|
+
if (existsSync(join(pkgSandbox, 'Dockerfile'))) {
|
|
793
|
+
return pkgSandbox;
|
|
794
|
+
}
|
|
787
795
|
return null;
|
|
788
796
|
}
|
|
789
797
|
function parseSandboxOption(args, flag) {
|
|
@@ -942,7 +950,30 @@ async function commandSandbox(args) {
|
|
|
942
950
|
}
|
|
943
951
|
return failed ? 1 : 0;
|
|
944
952
|
}
|
|
945
|
-
console.log(
|
|
953
|
+
console.log(`Usage: skimpyclaw sandbox <command>
|
|
954
|
+
|
|
955
|
+
Commands:
|
|
956
|
+
init Build sandbox image and enable in config
|
|
957
|
+
status List active sandbox containers
|
|
958
|
+
prune Remove orphaned sandbox containers
|
|
959
|
+
doctor Run targeted sandbox diagnostics
|
|
960
|
+
|
|
961
|
+
Init options:
|
|
962
|
+
--runtime <container|docker> Container runtime (default: auto-detect)
|
|
963
|
+
--profile <minimal|dev|full> Package set (default: minimal)
|
|
964
|
+
--image <name> Image name (default: skimpyclaw-sandbox:latest)
|
|
965
|
+
--network <name> Network name (default: auto per runtime)
|
|
966
|
+
|
|
967
|
+
Profiles:
|
|
968
|
+
minimal bash, curl, git, gh, jq, python3, ripgrep, pnpm
|
|
969
|
+
dev minimal + gcc, g++, make
|
|
970
|
+
full dev + pip3, sqlite3, unzip, less
|
|
971
|
+
|
|
972
|
+
Which runtime?
|
|
973
|
+
Apple Containers (macOS 26+) — lighter, faster startup, no daemon.
|
|
974
|
+
Docker — cross-platform, use if you already run Docker.
|
|
975
|
+
Auto-detect prefers Apple Containers, falls back to Docker.
|
|
976
|
+
`);
|
|
946
977
|
return 1;
|
|
947
978
|
}
|
|
948
979
|
export async function runCli(argv = process.argv.slice(2)) {
|
|
@@ -951,6 +982,17 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
951
982
|
printHelp();
|
|
952
983
|
return 0;
|
|
953
984
|
}
|
|
985
|
+
if (command === '--version' || command === '-v' || command === 'version') {
|
|
986
|
+
const pkgPath = join(fileURLToPath(import.meta.url), '..', '..', 'package.json');
|
|
987
|
+
try {
|
|
988
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
989
|
+
console.log(`skimpyclaw v${pkg.version}`);
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
console.log('skimpyclaw (version unknown)');
|
|
993
|
+
}
|
|
994
|
+
return 0;
|
|
995
|
+
}
|
|
954
996
|
try {
|
|
955
997
|
if (command === 'start') {
|
|
956
998
|
if (args.includes('--daemon')) {
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import type { CodeAgentBackgroundOptions, ValidationResult } from './types.js';
|
|
2
|
+
/** Supported JS package managers. */
|
|
3
|
+
export type PackageManager = 'pnpm' | 'yarn' | 'npm' | 'bun';
|
|
4
|
+
/**
|
|
5
|
+
* Detect the package manager for a project directory.
|
|
6
|
+
* Detection order (first match wins):
|
|
7
|
+
* 1. package.json `packageManager` field (e.g. "yarn@4.1.0")
|
|
8
|
+
* 2. Lockfile presence: yarn.lock → yarn, pnpm-lock.yaml → pnpm, bun.lockb / bun.lock → bun, package-lock.json → npm
|
|
9
|
+
* 3. Fallback: 'pnpm' (SkimpyClaw default)
|
|
10
|
+
*/
|
|
11
|
+
export declare function detectPackageManager(workdir: string): PackageManager;
|
|
12
|
+
/**
|
|
13
|
+
* Build the validation command for a project directory.
|
|
14
|
+
* Checks for `build` and `test` scripts in package.json, then runs them
|
|
15
|
+
* with the detected package manager. Falls back to `<pm> build && <pm> test`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildValidationCommand(workdir: string): string;
|
|
2
18
|
/** Run build/test validation. Shared by solo agents and team orchestrator. */
|
|
3
19
|
export declare function runValidation(workdir: string): Promise<ValidationResult>;
|
|
4
20
|
/** Background execution of a coding agent. Updates task status throughout. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Code Agent Executor - Background execution logic
|
|
2
2
|
import { spawn, exec } from 'child_process';
|
|
3
|
-
import { createWriteStream } from 'fs';
|
|
3
|
+
import { createWriteStream, existsSync, readFileSync } from 'fs';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
// SKIMPYCLAW_ROOT for log paths
|
|
6
6
|
const SKIMPYCLAW_ROOT = join(import.meta.dirname || process.cwd(), '..', '..');
|
|
@@ -12,10 +12,83 @@ import { startTrace, addEvent, endTrace } from '../audit.js';
|
|
|
12
12
|
import { buildUsageRecord, recordUsage } from '../usage.js';
|
|
13
13
|
import { ensureContainer, SANDBOX_DEFAULTS, getRuntime } from '../sandbox/index.js';
|
|
14
14
|
const CANCELLED_MESSAGE = 'Cancelled by user';
|
|
15
|
+
/**
|
|
16
|
+
* Detect the package manager for a project directory.
|
|
17
|
+
* Detection order (first match wins):
|
|
18
|
+
* 1. package.json `packageManager` field (e.g. "yarn@4.1.0")
|
|
19
|
+
* 2. Lockfile presence: yarn.lock → yarn, pnpm-lock.yaml → pnpm, bun.lockb / bun.lock → bun, package-lock.json → npm
|
|
20
|
+
* 3. Fallback: 'pnpm' (SkimpyClaw default)
|
|
21
|
+
*/
|
|
22
|
+
export function detectPackageManager(workdir) {
|
|
23
|
+
// 1. Check package.json packageManager field
|
|
24
|
+
try {
|
|
25
|
+
const pkgPath = join(workdir, 'package.json');
|
|
26
|
+
if (existsSync(pkgPath)) {
|
|
27
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
28
|
+
if (typeof pkg.packageManager === 'string') {
|
|
29
|
+
const name = pkg.packageManager.split('@')[0].toLowerCase();
|
|
30
|
+
if (name === 'yarn')
|
|
31
|
+
return 'yarn';
|
|
32
|
+
if (name === 'pnpm')
|
|
33
|
+
return 'pnpm';
|
|
34
|
+
if (name === 'npm')
|
|
35
|
+
return 'npm';
|
|
36
|
+
if (name === 'bun')
|
|
37
|
+
return 'bun';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch { /* ignore parse errors, fall through */ }
|
|
42
|
+
// 2. Check lockfiles
|
|
43
|
+
if (existsSync(join(workdir, 'yarn.lock')))
|
|
44
|
+
return 'yarn';
|
|
45
|
+
if (existsSync(join(workdir, 'pnpm-lock.yaml')))
|
|
46
|
+
return 'pnpm';
|
|
47
|
+
if (existsSync(join(workdir, 'bun.lockb')) || existsSync(join(workdir, 'bun.lock')))
|
|
48
|
+
return 'bun';
|
|
49
|
+
if (existsSync(join(workdir, 'package-lock.json')))
|
|
50
|
+
return 'npm';
|
|
51
|
+
// 3. Fallback
|
|
52
|
+
return 'pnpm';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Build the validation command for a project directory.
|
|
56
|
+
* Checks for `build` and `test` scripts in package.json, then runs them
|
|
57
|
+
* with the detected package manager. Falls back to `<pm> build && <pm> test`.
|
|
58
|
+
*/
|
|
59
|
+
export function buildValidationCommand(workdir) {
|
|
60
|
+
const pm = detectPackageManager(workdir);
|
|
61
|
+
const run = pm === 'npm' ? 'npm run' : pm;
|
|
62
|
+
// Check which scripts exist in package.json
|
|
63
|
+
let hasBuild = false;
|
|
64
|
+
let hasTest = false;
|
|
65
|
+
try {
|
|
66
|
+
const pkgPath = join(workdir, 'package.json');
|
|
67
|
+
if (existsSync(pkgPath)) {
|
|
68
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
69
|
+
const scripts = pkg.scripts || {};
|
|
70
|
+
hasBuild = !!scripts.build;
|
|
71
|
+
hasTest = !!scripts.test;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch { /* ignore */ }
|
|
75
|
+
const parts = [];
|
|
76
|
+
if (hasBuild)
|
|
77
|
+
parts.push(`${run} build`);
|
|
78
|
+
if (hasTest)
|
|
79
|
+
parts.push(`${run} test`);
|
|
80
|
+
// If neither build nor test scripts exist, still try — the scripts
|
|
81
|
+
// might be defined in a workspace root or the commands may work anyway
|
|
82
|
+
if (parts.length === 0) {
|
|
83
|
+
parts.push(`${run} build`, `${run} test`);
|
|
84
|
+
}
|
|
85
|
+
return parts.join(' && ');
|
|
86
|
+
}
|
|
15
87
|
/** Run build/test validation. Shared by solo agents and team orchestrator. */
|
|
16
88
|
export function runValidation(workdir) {
|
|
89
|
+
const cmd = buildValidationCommand(workdir);
|
|
17
90
|
return new Promise((resolve) => {
|
|
18
|
-
exec(
|
|
91
|
+
exec(cmd, {
|
|
19
92
|
cwd: workdir,
|
|
20
93
|
timeout: VALIDATE_TIMEOUT_MS,
|
|
21
94
|
maxBuffer: 5 * 1024 * 1024,
|
|
@@ -242,8 +315,9 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
242
315
|
caTask.outputPreview = agentOutput.slice(0, 500);
|
|
243
316
|
caTask.liveOutput = undefined;
|
|
244
317
|
writeCodeAgentTask(caTask);
|
|
318
|
+
const validationCmd = buildValidationCommand(workdir);
|
|
245
319
|
const runValidationPromise = () => new Promise((res) => {
|
|
246
|
-
const validationProc = exec(
|
|
320
|
+
const validationProc = exec(validationCmd, {
|
|
247
321
|
cwd: workdir,
|
|
248
322
|
timeout: VALIDATE_TIMEOUT_MS,
|
|
249
323
|
maxBuffer: 5 * 1024 * 1024,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { execSync } from 'child_process';
|
|
3
3
|
import { resolve, join } from 'path';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
|
+
import { buildValidationCommand } from './executor.js';
|
|
5
6
|
// Resolve CLI paths once at import time so spawn doesn't get ENOENT
|
|
6
7
|
function resolveCliPath(name) {
|
|
7
8
|
try {
|
|
@@ -113,7 +114,7 @@ export function buildCodeAgentArgs(input) {
|
|
|
113
114
|
'--mcp-config', playwrightMcp,
|
|
114
115
|
...toolArgs,
|
|
115
116
|
'--max-turns', maxTurns,
|
|
116
|
-
'--append-system-prompt',
|
|
117
|
+
'--append-system-prompt', `Output text only. Never use say or TTS. Focus on the coding task. Run ${buildValidationCommand(input.workdir || process.cwd())} to verify changes.`,
|
|
117
118
|
];
|
|
118
119
|
// Only pass model to Claude CLI if it's not a known non-Claude model.
|
|
119
120
|
// GPT/Codex/Kimi/o-series models would be rejected by the Claude CLI.
|
|
@@ -152,6 +153,10 @@ export function buildTeamNotification(task, getChildTask) {
|
|
|
152
153
|
lines.push(` ${childIcon} ${child.id} (${childDur}): ${subtask}`);
|
|
153
154
|
}
|
|
154
155
|
}
|
|
156
|
+
// Synthesis result
|
|
157
|
+
if (task.outputPreview) {
|
|
158
|
+
lines.push(`\nResult: ${task.outputPreview}`);
|
|
159
|
+
}
|
|
155
160
|
// Errors
|
|
156
161
|
if (task.error && task.error !== 'Validation failed') {
|
|
157
162
|
lines.push(`\nError: ${task.error}`);
|
|
@@ -169,8 +174,7 @@ export function buildSoloNotification(task) {
|
|
|
169
174
|
const validation = task.validationPassed ? ' Build/tests pass.' : '';
|
|
170
175
|
let message = `✅ Coding agent ${task.id} completed (${dur}).${validation}\n\nTask: ${taskPreview}`;
|
|
171
176
|
if (task.outputPreview) {
|
|
172
|
-
|
|
173
|
-
message += `\n\nResult: ${preview}`;
|
|
177
|
+
message += `\n\nResult: ${task.outputPreview}`;
|
|
174
178
|
}
|
|
175
179
|
return message;
|
|
176
180
|
}
|
package/dist/doctor/checks.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare function checkTelegramToken(token: string): Promise<DoctorCheckRe
|
|
|
12
12
|
export declare function checkDiscordToken(token: string): Promise<DoctorCheckResult>;
|
|
13
13
|
export declare function checkBrowserBinaryIfEnabled(config: Config): Promise<DoctorCheckResult>;
|
|
14
14
|
export declare function checkVoiceDependencies(config: Config): Promise<DoctorCheckResult>;
|
|
15
|
+
export declare function checkPlaywrightIfBrowserEnabled(config: Config): Promise<DoctorCheckResult>;
|
|
15
16
|
export declare function checkMcpConfig(config: Config): Promise<DoctorCheckResult>;
|
|
16
17
|
export declare function checkGatewayHostBindable(host: string): Promise<DoctorCheckResult>;
|
|
17
18
|
export declare function checkSkimpyclawDirWritable(): Promise<DoctorCheckResult>;
|
package/dist/doctor/checks.js
CHANGED
|
@@ -274,16 +274,32 @@ export async function checkVoiceDependencies(config) {
|
|
|
274
274
|
if (ffmpeg.status !== 0) {
|
|
275
275
|
issues.push('ffmpeg not found');
|
|
276
276
|
}
|
|
277
|
-
// Check for whisper
|
|
277
|
+
// Check for STT: local whisper OR API provider
|
|
278
278
|
const whisperCli = spawnSync('which', ['whisper-cli'], { encoding: 'utf-8' });
|
|
279
279
|
const whisperPy = spawnSync('which', ['whisper'], { encoding: 'utf-8' });
|
|
280
|
-
|
|
281
|
-
|
|
280
|
+
const hasLocalWhisper = whisperCli.status === 0 || whisperPy.status === 0;
|
|
281
|
+
// Check if any API STT provider is configured
|
|
282
|
+
const hasApiStt = Object.values(config.voice?.providers || {}).some((p) => p && typeof p === 'object' && 'stt' in p);
|
|
283
|
+
if (!hasLocalWhisper && !hasApiStt) {
|
|
284
|
+
issues.push('No STT available — install whisper-cli (brew install whisper-cpp) or configure an API STT provider (e.g. openai.stt)');
|
|
282
285
|
}
|
|
283
286
|
if (issues.length > 0) {
|
|
284
|
-
return fail(name, category, issues.join('; '), 'Install ffmpeg and whisper-cli (
|
|
287
|
+
return fail(name, category, issues.join('; '), 'Install ffmpeg, and either whisper-cli (brew install whisper-cpp) or add openai.stt to voice providers.');
|
|
285
288
|
}
|
|
286
|
-
|
|
289
|
+
const sttMethod = hasLocalWhisper ? (whisperCli.status === 0 ? 'whisper-cli' : 'whisper') : 'API STT';
|
|
290
|
+
return ok(name, category, `ffmpeg and ${sttMethod} available`);
|
|
291
|
+
}
|
|
292
|
+
export async function checkPlaywrightIfBrowserEnabled(config) {
|
|
293
|
+
const name = 'playwright_installed';
|
|
294
|
+
const category = 'runtime';
|
|
295
|
+
if (!isAnyBrowserEnabled(config)) {
|
|
296
|
+
return ok(name, category, 'Browser tools disabled');
|
|
297
|
+
}
|
|
298
|
+
const pw = spawnSync('npx', ['playwright', '--version'], { encoding: 'utf-8', timeout: 10000 });
|
|
299
|
+
if (pw.status === 0) {
|
|
300
|
+
return ok(name, category, `Playwright ${(pw.stdout || '').trim()}`);
|
|
301
|
+
}
|
|
302
|
+
return fail(name, category, 'Playwright not installed', 'Run: npx playwright install chromium');
|
|
287
303
|
}
|
|
288
304
|
export async function checkMcpConfig(config) {
|
|
289
305
|
const name = 'mcp_config';
|
package/dist/doctor/runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadConfig } from '../config.js';
|
|
2
|
-
import { checkNodeVersion, checkPackageManagerAvailable, checkTypeScriptCompile, checkConfigExistsAndValidJson, checkRequiredEnvVars, checkEnvVarPatterns, checkAllowedPathsWritable, checkProviderAuth, checkTelegramToken, checkDiscordToken, checkBrowserBinaryIfEnabled, checkVoiceDependencies, checkMcpConfig, checkGatewayHostBindable, checkSkimpyclawDirWritable, checkPortAvailability, checkSandboxAvailable, } from './checks.js';
|
|
2
|
+
import { checkNodeVersion, checkPackageManagerAvailable, checkTypeScriptCompile, checkConfigExistsAndValidJson, checkRequiredEnvVars, checkEnvVarPatterns, checkAllowedPathsWritable, checkProviderAuth, checkTelegramToken, checkDiscordToken, checkBrowserBinaryIfEnabled, checkPlaywrightIfBrowserEnabled, checkVoiceDependencies, checkMcpConfig, checkGatewayHostBindable, checkSkimpyclawDirWritable, checkPortAvailability, checkSandboxAvailable, } from './checks.js';
|
|
3
3
|
export function computeExitCode(report) {
|
|
4
4
|
if (report.checks.some((check) => !check.ok && check.fatal)) {
|
|
5
5
|
return 2;
|
|
@@ -99,6 +99,7 @@ export async function runDoctor() {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
checks.push(await runSafe('browser_binary_available', 'runtime', () => checkBrowserBinaryIfEnabled(config)));
|
|
102
|
+
checks.push(await runSafe('playwright_installed', 'runtime', () => checkPlaywrightIfBrowserEnabled(config)));
|
|
102
103
|
checks.push(await runSafe('voice_dependencies', 'runtime', () => checkVoiceDependencies(config)));
|
|
103
104
|
checks.push(await runSafe('mcp_config', 'runtime', () => checkMcpConfig(config)));
|
|
104
105
|
checks.push(await runSafe('gateway_host_bindable', 'runtime', () => checkGatewayHostBindable(config.gateway.host ?? '127.0.0.1')));
|
package/dist/setup.d.ts
CHANGED
|
@@ -21,8 +21,9 @@ interface SetupStarters {
|
|
|
21
21
|
cronWeather: boolean;
|
|
22
22
|
timezone: string;
|
|
23
23
|
weatherLocation: string;
|
|
24
|
-
skillCodeReview: boolean;
|
|
25
24
|
skillDailyNotes: boolean;
|
|
25
|
+
skillWeather: boolean;
|
|
26
|
+
skillWebSearch: boolean;
|
|
26
27
|
}
|
|
27
28
|
interface SetupBuildInput {
|
|
28
29
|
workspaceDir: string;
|
package/dist/setup.js
CHANGED
|
@@ -460,17 +460,20 @@ export function buildSetupConfig(input) {
|
|
|
460
460
|
cronWeather: false,
|
|
461
461
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
|
|
462
462
|
weatherLocation: 'New York, NY',
|
|
463
|
-
skillCodeReview: false,
|
|
464
463
|
skillDailyNotes: false,
|
|
464
|
+
skillWeather: false,
|
|
465
|
+
skillWebSearch: false,
|
|
465
466
|
};
|
|
466
467
|
const basePaths = ['${HOME}/.skimpyclaw'];
|
|
467
468
|
const allPaths = [...basePaths, ...(input.extraAllowedPaths || [])];
|
|
468
469
|
const starterCronJobs = buildStarterCronJobs(starters);
|
|
469
470
|
const starterSkillEntries = {};
|
|
470
|
-
if (starters.skillCodeReview)
|
|
471
|
-
starterSkillEntries['code-review'] = true;
|
|
472
471
|
if (starters.skillDailyNotes)
|
|
473
472
|
starterSkillEntries['daily-notes'] = true;
|
|
473
|
+
if (starters.skillWeather)
|
|
474
|
+
starterSkillEntries['weather'] = true;
|
|
475
|
+
if (starters.skillWebSearch)
|
|
476
|
+
starterSkillEntries['web-search'] = true;
|
|
474
477
|
return {
|
|
475
478
|
gateway: {
|
|
476
479
|
port: 18790,
|
|
@@ -595,19 +598,6 @@ const REQUIRED_TEMPLATE_DEFAULTS = {
|
|
|
595
598
|
'HEARTBEAT.md': '# HEARTBEAT\n\nIf nothing needs attention, reply HEARTBEAT_OK.\n',
|
|
596
599
|
};
|
|
597
600
|
const STARTER_SKILL_TEMPLATES = {
|
|
598
|
-
'code-review': `---
|
|
599
|
-
name: code-review
|
|
600
|
-
description: Structured code review checklist for bugs, regressions, and missing tests.
|
|
601
|
-
triggers: ["review", "pr", "regression", "tests"]
|
|
602
|
-
priority: 80
|
|
603
|
-
---
|
|
604
|
-
|
|
605
|
-
When asked to review code:
|
|
606
|
-
1. Focus on correctness and regressions first.
|
|
607
|
-
2. Call out missing or weak test coverage.
|
|
608
|
-
3. Prefer concrete file-level findings.
|
|
609
|
-
4. End with risk summary and recommended fixes.
|
|
610
|
-
`,
|
|
611
601
|
'daily-notes': `---
|
|
612
602
|
name: daily-notes
|
|
613
603
|
description: Keep daily notes organized under the configured daily notes directory.
|
|
@@ -620,6 +610,36 @@ When writing daily notes:
|
|
|
620
610
|
2. Include sections: Priorities, Schedule, Notes, Follow-ups.
|
|
621
611
|
3. Keep entries concise and actionable.
|
|
622
612
|
4. Avoid creating files outside the configured daily notes directory.
|
|
613
|
+
`,
|
|
614
|
+
'weather': `---
|
|
615
|
+
name: weather
|
|
616
|
+
description: Fetch and format weather data for daily briefings and quick checks.
|
|
617
|
+
triggers: ["weather", "forecast", "temperature", "rain"]
|
|
618
|
+
priority: 45
|
|
619
|
+
---
|
|
620
|
+
|
|
621
|
+
When asked about weather or generating a daily briefing:
|
|
622
|
+
1. Use web search to find current weather for the user's location.
|
|
623
|
+
2. Format as: conditions, high/low temps, precipitation chance.
|
|
624
|
+
3. Keep it to 2-3 sentences max.
|
|
625
|
+
4. Include any weather alerts if present.
|
|
626
|
+
5. For daily briefings: mention if rain is expected (affects outdoor plans).
|
|
627
|
+
`,
|
|
628
|
+
'web-search': `---
|
|
629
|
+
name: web-search
|
|
630
|
+
description: Search the web using the Browser tool. Opens DuckDuckGo, reads results, and returns findings.
|
|
631
|
+
triggers: ["search", "look up", "google", "find online", "web search"]
|
|
632
|
+
priority: 50
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
When asked to search the web:
|
|
636
|
+
1. Use the Browser tool to open https://html.duckduckgo.com/html/?q=<URL-encoded query>
|
|
637
|
+
2. Use getText to read the search results page.
|
|
638
|
+
3. If a specific result looks promising, open that URL and extract the relevant content.
|
|
639
|
+
4. Summarize findings concisely — include source URLs.
|
|
640
|
+
5. Close the browser when done.
|
|
641
|
+
|
|
642
|
+
Do NOT fabricate results. If the search returns nothing useful, say so.
|
|
623
643
|
`,
|
|
624
644
|
};
|
|
625
645
|
function ensureCoreTemplates(agentDir) {
|
|
@@ -638,10 +658,12 @@ function ensureStarterSkills(starters) {
|
|
|
638
658
|
const skillsDir = join(CONFIG_DIR, 'skills');
|
|
639
659
|
mkdirSync(skillsDir, { recursive: true });
|
|
640
660
|
const requested = [];
|
|
641
|
-
if (starters.skillCodeReview)
|
|
642
|
-
requested.push('code-review');
|
|
643
661
|
if (starters.skillDailyNotes)
|
|
644
662
|
requested.push('daily-notes');
|
|
663
|
+
if (starters.skillWeather)
|
|
664
|
+
requested.push('weather');
|
|
665
|
+
if (starters.skillWebSearch)
|
|
666
|
+
requested.push('web-search');
|
|
645
667
|
for (const skillName of requested) {
|
|
646
668
|
const dir = join(skillsDir, skillName);
|
|
647
669
|
const skillPath = join(dir, 'SKILL.md');
|
|
@@ -876,6 +898,22 @@ export async function runSetup(options = {}) {
|
|
|
876
898
|
else {
|
|
877
899
|
statusWarn('Chrome not found — browser tool may not work until Chrome is installed');
|
|
878
900
|
}
|
|
901
|
+
// Check for Playwright
|
|
902
|
+
const pw = spawnSync('npx', ['playwright', '--version'], { encoding: 'utf-8', timeout: 10000 });
|
|
903
|
+
if (pw.status === 0) {
|
|
904
|
+
statusOk('Playwright detected');
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
console.log('');
|
|
908
|
+
console.log(' ┌─────────────────────────────────────────────────────────┐');
|
|
909
|
+
console.log(' │ Browser tool requires Playwright. Install it: │');
|
|
910
|
+
console.log(' │ │');
|
|
911
|
+
console.log(' │ npx playwright install chromium │');
|
|
912
|
+
console.log(' │ │');
|
|
913
|
+
console.log(' │ Without this, the browser tool will fail at runtime. │');
|
|
914
|
+
console.log(' └─────────────────────────────────────────────────────────┘');
|
|
915
|
+
console.log('');
|
|
916
|
+
}
|
|
879
917
|
}
|
|
880
918
|
else {
|
|
881
919
|
statusOk('browser disabled');
|
|
@@ -891,6 +929,32 @@ export async function runSetup(options = {}) {
|
|
|
891
929
|
else {
|
|
892
930
|
statusWarn('ffmpeg not found — voice features may not work until ffmpeg is installed');
|
|
893
931
|
}
|
|
932
|
+
// Check for STT (speech-to-text) capability
|
|
933
|
+
const whisperCli = spawnSync('which', ['whisper-cli'], { encoding: 'utf-8' });
|
|
934
|
+
const whisperPy = spawnSync('which', ['whisper'], { encoding: 'utf-8' });
|
|
935
|
+
if (whisperCli.status === 0) {
|
|
936
|
+
statusOk('whisper-cli detected (whisper.cpp)');
|
|
937
|
+
}
|
|
938
|
+
else if (whisperPy.status === 0) {
|
|
939
|
+
statusOk('whisper detected (Python)');
|
|
940
|
+
}
|
|
941
|
+
else {
|
|
942
|
+
console.log('');
|
|
943
|
+
console.log(' ┌─────────────────────────────────────────────────────────┐');
|
|
944
|
+
console.log(' │ Voice transcription (STT) requires one of: │');
|
|
945
|
+
console.log(' │ │');
|
|
946
|
+
console.log(' │ Option A: Local whisper.cpp (free, recommended) │');
|
|
947
|
+
console.log(' │ brew install whisper-cpp │');
|
|
948
|
+
console.log(' │ whisper-cpp-download-ggml-model small │');
|
|
949
|
+
console.log(' │ │');
|
|
950
|
+
console.log(' │ Option B: OpenAI Whisper API ($0.006/min) │');
|
|
951
|
+
console.log(' │ Add OPENAI_API_KEY to ~/.skimpyclaw/.env │');
|
|
952
|
+
console.log(' │ Config auto-includes openai.stt if provider selected │');
|
|
953
|
+
console.log(' │ │');
|
|
954
|
+
console.log(' │ Without either, voice messages cannot be transcribed. │');
|
|
955
|
+
console.log(' └─────────────────────────────────────────────────────────┘');
|
|
956
|
+
console.log('');
|
|
957
|
+
}
|
|
894
958
|
}
|
|
895
959
|
else {
|
|
896
960
|
statusOk('voice disabled');
|
|
@@ -953,15 +1017,17 @@ export async function runSetup(options = {}) {
|
|
|
953
1017
|
const locationInput = await ask(rl, ' Weather location (city, state/country) [New York, NY]: ');
|
|
954
1018
|
weatherLocation = locationInput || 'New York, NY';
|
|
955
1019
|
}
|
|
956
|
-
const addCodeReviewSkill = /^y(es)?$/i.test(await ask(rl, ' Add starter skill: code-review? [y/N]: '));
|
|
957
1020
|
const addDailyNotesSkill = /^y(es)?$/i.test(await ask(rl, ' Add starter skill: daily-notes? [y/N]: '));
|
|
1021
|
+
const addWeatherSkill = /^y(es)?$/i.test(await ask(rl, ' Add starter skill: weather? [y/N]: '));
|
|
1022
|
+
const addWebSearchSkill = /^y(es)?$/i.test(await ask(rl, ' Add starter skill: web-search (uses Browser tool)? [y/N]: '));
|
|
958
1023
|
const starters = {
|
|
959
1024
|
cronTechNews: addTechNewsCron,
|
|
960
1025
|
cronWeather: addWeatherCron,
|
|
961
1026
|
timezone: cronTimezone,
|
|
962
1027
|
weatherLocation,
|
|
963
|
-
skillCodeReview: addCodeReviewSkill,
|
|
964
1028
|
skillDailyNotes: addDailyNotesSkill,
|
|
1029
|
+
skillWeather: addWeatherSkill,
|
|
1030
|
+
skillWebSearch: addWebSearchSkill,
|
|
965
1031
|
};
|
|
966
1032
|
const { envContent, config: generatedConfig } = buildSetupArtifacts({
|
|
967
1033
|
workspaceDir: extraAllowedPaths[0] || join(homedir(), '.skimpyclaw'),
|
|
@@ -142,7 +142,7 @@ export const CODE_WITH_AGENT_TOOL = {
|
|
|
142
142
|
model: { type: 'string', description: 'Model override (e.g. opus, gpt-5.3-codex)' },
|
|
143
143
|
max_turns: { type: 'number', description: 'Max agentic turns, Claude only (default: 30)' },
|
|
144
144
|
timeout_minutes: { type: 'number', description: 'Timeout in minutes (default: 10, max: 30)' },
|
|
145
|
-
validate: { type: 'boolean', description: 'Run
|
|
145
|
+
validate: { type: 'boolean', description: 'Run build && test after completion using the auto-detected package manager (default: true)' },
|
|
146
146
|
},
|
|
147
147
|
required: ['task'],
|
|
148
148
|
},
|
|
@@ -159,7 +159,7 @@ export const CODE_WITH_TEAM_TOOL = {
|
|
|
159
159
|
agent: { type: 'string', enum: ['claude', 'codex', 'kimi'], description: 'Which coding CLI to use for all team workers. Omit to use configured default.' },
|
|
160
160
|
model: { type: 'string', description: 'Model override (e.g. claude-sonnet-4-5, gpt-5.3-codex)' },
|
|
161
161
|
timeout_minutes: { type: 'number', description: 'Total timeout in minutes (default: 20, max: 60)' },
|
|
162
|
-
validate: { type: 'boolean', description: 'Run
|
|
162
|
+
validate: { type: 'boolean', description: 'Run build && test after all agents complete using the auto-detected package manager (default: true)' },
|
|
163
163
|
},
|
|
164
164
|
required: ['task'],
|
|
165
165
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skimpyclaw",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Lightweight personal AI assistant with Telegram and Discord integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -10,30 +10,12 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"dist",
|
|
13
|
+
"sandbox",
|
|
13
14
|
"templates",
|
|
14
15
|
"com.skimpyclaw.gateway.plist.example",
|
|
15
16
|
"README.md",
|
|
16
17
|
"LICENSE"
|
|
17
18
|
],
|
|
18
|
-
"scripts": {
|
|
19
|
-
"cli": "tsx src/cli.ts",
|
|
20
|
-
"start": "tsx src/index.ts",
|
|
21
|
-
"dev": "tsx watch src/index.ts",
|
|
22
|
-
"dashboard:dev": "pnpm --dir web/dashboard dev",
|
|
23
|
-
"dashboard:build": "pnpm --dir web/dashboard install --frozen-lockfile && pnpm --dir web/dashboard build",
|
|
24
|
-
"docs:dev": "pnpm --dir docs dev",
|
|
25
|
-
"docs:build": "pnpm --dir docs install --frozen-lockfile && pnpm --dir docs build",
|
|
26
|
-
"docs:preview": "pnpm --dir docs preview",
|
|
27
|
-
"setup": "tsx src/setup.ts",
|
|
28
|
-
"onboard": "tsx src/cli.ts onboard",
|
|
29
|
-
"build": "tsc && pnpm dashboard:build",
|
|
30
|
-
"release:check": "pnpm build && pnpm test",
|
|
31
|
-
"release:local": "bash ./scripts/release.sh",
|
|
32
|
-
"lint": "eslint \"src/**/*.ts\"",
|
|
33
|
-
"typecheck": "tsc --noEmit",
|
|
34
|
-
"test": "vitest run",
|
|
35
|
-
"ci": "pnpm run lint && pnpm run typecheck && pnpm run test"
|
|
36
|
-
},
|
|
37
19
|
"dependencies": {
|
|
38
20
|
"@anthropic-ai/sdk": "^0.52.0",
|
|
39
21
|
"@grammyjs/runner": "^2.0.3",
|
|
@@ -55,11 +37,6 @@
|
|
|
55
37
|
"openai": "^4.47.0",
|
|
56
38
|
"playwright": "^1.49.0"
|
|
57
39
|
},
|
|
58
|
-
"pnpm": {
|
|
59
|
-
"onlyBuiltDependencies": [
|
|
60
|
-
"esbuild"
|
|
61
|
-
]
|
|
62
|
-
},
|
|
63
40
|
"devDependencies": {
|
|
64
41
|
"@eslint/js": "^9.39.2",
|
|
65
42
|
"@types/node": "^20.11.0",
|
|
@@ -69,5 +46,24 @@
|
|
|
69
46
|
"typescript": "^5.4.0",
|
|
70
47
|
"typescript-eslint": "^8.54.0",
|
|
71
48
|
"vitest": "^4.0.18"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"cli": "tsx src/cli.ts",
|
|
52
|
+
"start": "tsx src/index.ts",
|
|
53
|
+
"dev": "tsx watch src/index.ts",
|
|
54
|
+
"dashboard:dev": "pnpm --dir web/dashboard dev",
|
|
55
|
+
"dashboard:build": "pnpm --dir web/dashboard install --frozen-lockfile && pnpm --dir web/dashboard build",
|
|
56
|
+
"docs:dev": "pnpm --dir docs dev",
|
|
57
|
+
"docs:build": "pnpm --dir docs install --frozen-lockfile && pnpm --dir docs build",
|
|
58
|
+
"docs:preview": "pnpm --dir docs preview",
|
|
59
|
+
"setup": "tsx src/setup.ts",
|
|
60
|
+
"onboard": "tsx src/cli.ts onboard",
|
|
61
|
+
"build": "tsc && pnpm dashboard:build",
|
|
62
|
+
"release:check": "pnpm build && pnpm test",
|
|
63
|
+
"release:local": "bash ./scripts/release.sh",
|
|
64
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
65
|
+
"typecheck": "tsc --noEmit",
|
|
66
|
+
"test": "vitest run",
|
|
67
|
+
"ci": "pnpm run lint && pnpm run typecheck && pnpm run test"
|
|
72
68
|
}
|
|
73
|
-
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
FROM node:22-slim
|
|
2
|
+
|
|
3
|
+
ARG SKIMPY_PROFILE=minimal
|
|
4
|
+
|
|
5
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
6
|
+
bash \
|
|
7
|
+
ca-certificates \
|
|
8
|
+
curl \
|
|
9
|
+
git \
|
|
10
|
+
gnupg \
|
|
11
|
+
jq \
|
|
12
|
+
python3 \
|
|
13
|
+
ripgrep \
|
|
14
|
+
&& mkdir -p /etc/apt/keyrings \
|
|
15
|
+
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
|
16
|
+
| gpg --dearmor -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
|
17
|
+
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
|
18
|
+
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
|
|
19
|
+
> /etc/apt/sources.list.d/github-cli.list \
|
|
20
|
+
&& apt-get update \
|
|
21
|
+
&& apt-get install -y --no-install-recommends gh \
|
|
22
|
+
&& if [ "$SKIMPY_PROFILE" = "dev" ] || [ "$SKIMPY_PROFILE" = "full" ]; then \
|
|
23
|
+
apt-get install -y --no-install-recommends build-essential make gcc g++ pkg-config; \
|
|
24
|
+
fi \
|
|
25
|
+
&& if [ "$SKIMPY_PROFILE" = "full" ]; then \
|
|
26
|
+
apt-get install -y --no-install-recommends python3-pip sqlite3 unzip less; \
|
|
27
|
+
fi \
|
|
28
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
29
|
+
|
|
30
|
+
# Install pnpm globally (Claude Code not needed inside sandbox)
|
|
31
|
+
RUN npm install -g pnpm
|
|
32
|
+
|
|
33
|
+
# Create non-root user matching typical macOS uid/gid
|
|
34
|
+
RUN groupadd -g 20 sandboxgrp || true \
|
|
35
|
+
&& useradd -m -s /bin/bash -u 501 -g 20 sandbox || true
|
|
36
|
+
|
|
37
|
+
USER sandbox
|
|
38
|
+
WORKDIR /workspace
|
|
39
|
+
|
|
40
|
+
CMD ["sleep", "infinity"]
|