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
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Config } from '../types.js';
|
|
2
|
+
import type { DoctorCheckResult } from './types.js';
|
|
3
|
+
export declare function checkNodeVersion(): Promise<DoctorCheckResult>;
|
|
4
|
+
export declare function checkPackageManagerAvailable(): Promise<DoctorCheckResult>;
|
|
5
|
+
export declare function checkTypeScriptCompile(): Promise<DoctorCheckResult>;
|
|
6
|
+
export declare function checkConfigExistsAndValidJson(): Promise<DoctorCheckResult>;
|
|
7
|
+
export declare function checkRequiredEnvVars(config: Config): Promise<DoctorCheckResult>;
|
|
8
|
+
export declare function checkEnvVarPatterns(config: Config): Promise<DoctorCheckResult>;
|
|
9
|
+
export declare function checkAllowedPathsWritable(config: Config): Promise<DoctorCheckResult>;
|
|
10
|
+
export declare function checkProviderAuth(providerName: string, providerConfig: NonNullable<Config['models']['providers'][string]>): Promise<DoctorCheckResult>;
|
|
11
|
+
export declare function checkTelegramToken(token: string): Promise<DoctorCheckResult>;
|
|
12
|
+
export declare function checkDiscordToken(token: string): Promise<DoctorCheckResult>;
|
|
13
|
+
export declare function checkBrowserBinaryIfEnabled(config: Config): Promise<DoctorCheckResult>;
|
|
14
|
+
export declare function checkVoiceDependencies(config: Config): Promise<DoctorCheckResult>;
|
|
15
|
+
export declare function checkMcpConfig(config: Config): Promise<DoctorCheckResult>;
|
|
16
|
+
export declare function checkGatewayHostBindable(host: string): Promise<DoctorCheckResult>;
|
|
17
|
+
export declare function checkSkimpyclawDirWritable(): Promise<DoctorCheckResult>;
|
|
18
|
+
export declare function checkPortAvailability(port: number): Promise<DoctorCheckResult>;
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { accessSync, constants, existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
import net from 'net';
|
|
6
|
+
import { getConfigPath } from '../config.js';
|
|
7
|
+
function ok(name, category, detail) {
|
|
8
|
+
return { name, category, ok: true, detail };
|
|
9
|
+
}
|
|
10
|
+
function fail(name, category, detail, remedy, fatal = false) {
|
|
11
|
+
return { name, category, ok: false, detail, remedy, fatal };
|
|
12
|
+
}
|
|
13
|
+
export async function checkNodeVersion() {
|
|
14
|
+
const name = 'node_version';
|
|
15
|
+
const category = 'environment';
|
|
16
|
+
const version = process.versions.node;
|
|
17
|
+
const major = Number.parseInt(version.split('.')[0], 10);
|
|
18
|
+
if (Number.isNaN(major)) {
|
|
19
|
+
return fail(name, category, `Unable to parse Node.js version: ${version}`, 'Install Node.js 18 or newer.');
|
|
20
|
+
}
|
|
21
|
+
if (major < 18) {
|
|
22
|
+
return fail(name, category, `Node.js ${version} detected`, 'Upgrade to Node.js 18 or newer.');
|
|
23
|
+
}
|
|
24
|
+
return ok(name, category, `v${version}`);
|
|
25
|
+
}
|
|
26
|
+
export async function checkPackageManagerAvailable() {
|
|
27
|
+
const name = 'package_manager_available';
|
|
28
|
+
const category = 'environment';
|
|
29
|
+
const pnpm = spawnSync('pnpm', ['--version'], { encoding: 'utf-8' });
|
|
30
|
+
if (pnpm.status === 0) {
|
|
31
|
+
return ok(name, category, `pnpm ${pnpm.stdout.trim()}`);
|
|
32
|
+
}
|
|
33
|
+
const npm = spawnSync('npm', ['--version'], { encoding: 'utf-8' });
|
|
34
|
+
if (npm.status === 0) {
|
|
35
|
+
return ok(name, category, `npm ${npm.stdout.trim()}`);
|
|
36
|
+
}
|
|
37
|
+
return fail(name, category, 'Neither pnpm nor npm is available', 'Install pnpm (preferred) or npm and ensure it is on PATH.');
|
|
38
|
+
}
|
|
39
|
+
export async function checkTypeScriptCompile() {
|
|
40
|
+
const name = 'typescript_compile';
|
|
41
|
+
const category = 'environment';
|
|
42
|
+
const run = spawnSync('pnpm', ['exec', 'tsc', '--noEmit', '--pretty', 'false'], {
|
|
43
|
+
encoding: 'utf-8',
|
|
44
|
+
cwd: process.cwd(),
|
|
45
|
+
});
|
|
46
|
+
if (run.status === 0) {
|
|
47
|
+
return ok(name, category, 'tsc --noEmit passed');
|
|
48
|
+
}
|
|
49
|
+
const detail = (run.stderr || run.stdout || 'TypeScript compile check failed').trim().split('\n')[0];
|
|
50
|
+
return fail(name, category, detail, 'Run "pnpm exec tsc --noEmit" locally and fix TypeScript errors.');
|
|
51
|
+
}
|
|
52
|
+
export async function checkConfigExistsAndValidJson() {
|
|
53
|
+
const name = 'config_json_valid';
|
|
54
|
+
const category = 'configuration';
|
|
55
|
+
const configPath = getConfigPath();
|
|
56
|
+
if (!existsSync(configPath)) {
|
|
57
|
+
return fail(name, category, `Config file not found: ${configPath}`, 'Run "skimpyclaw onboard" to create ~/.skimpyclaw/config.json.', true);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
61
|
+
JSON.parse(raw);
|
|
62
|
+
return ok(name, category, configPath);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
66
|
+
return fail(name, category, detail, 'Fix ~/.skimpyclaw/config.json to valid JSON and rerun doctor.', true);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function checkRequiredEnvVars(config) {
|
|
70
|
+
const name = 'required_env_vars';
|
|
71
|
+
const category = 'configuration';
|
|
72
|
+
const missing = [];
|
|
73
|
+
if (config.channels.telegram?.enabled && !config.channels.telegram.token?.trim()) {
|
|
74
|
+
missing.push('TELEGRAM_BOT_TOKEN');
|
|
75
|
+
}
|
|
76
|
+
if (config.channels.discord?.enabled && !config.channels.discord.token?.trim()) {
|
|
77
|
+
missing.push('DISCORD_BOT_TOKEN');
|
|
78
|
+
}
|
|
79
|
+
for (const [providerName, providerCfg] of Object.entries(config.models.providers || {})) {
|
|
80
|
+
if (!providerCfg)
|
|
81
|
+
continue;
|
|
82
|
+
const key = providerCfg.apiKey || providerCfg.authToken;
|
|
83
|
+
if (!key?.trim()) {
|
|
84
|
+
missing.push(`${providerName.toUpperCase()}_API_KEY`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (missing.length > 0) {
|
|
88
|
+
return fail(name, category, `Missing values: ${missing.join(', ')}`, 'Set missing variables in ~/.skimpyclaw/.env and rerun doctor.');
|
|
89
|
+
}
|
|
90
|
+
return ok(name, category, 'All required env-backed values are present');
|
|
91
|
+
}
|
|
92
|
+
export async function checkEnvVarPatterns(config) {
|
|
93
|
+
const name = 'env_var_patterns';
|
|
94
|
+
const category = 'configuration';
|
|
95
|
+
const issues = [];
|
|
96
|
+
const telegramToken = config.channels.telegram?.token || '';
|
|
97
|
+
if (config.channels.telegram?.enabled && !/^\d+:[A-Za-z0-9_-]{20,}$/.test(telegramToken)) {
|
|
98
|
+
issues.push('TELEGRAM_BOT_TOKEN format looks invalid');
|
|
99
|
+
}
|
|
100
|
+
for (const [providerName, providerCfg] of Object.entries(config.models.providers || {})) {
|
|
101
|
+
const key = providerCfg?.apiKey || providerCfg?.authToken || '';
|
|
102
|
+
if (!key)
|
|
103
|
+
continue;
|
|
104
|
+
if (/openai|anthropic|minimax/i.test(providerName) && !key.startsWith('sk-')) {
|
|
105
|
+
issues.push(`${providerName} key should usually start with "sk-"`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (issues.length > 0) {
|
|
109
|
+
return fail(name, category, issues.join('; '), 'Verify API key/token formats in ~/.skimpyclaw/.env.');
|
|
110
|
+
}
|
|
111
|
+
return ok(name, category, 'Env value patterns look valid');
|
|
112
|
+
}
|
|
113
|
+
export async function checkAllowedPathsWritable(config) {
|
|
114
|
+
const name = 'allowed_paths_writable';
|
|
115
|
+
const category = 'configuration';
|
|
116
|
+
const paths = new Set();
|
|
117
|
+
for (const channel of [config.channels.telegram, config.channels.discord]) {
|
|
118
|
+
if (!channel)
|
|
119
|
+
continue;
|
|
120
|
+
if (channel.tools?.allowedPaths) {
|
|
121
|
+
channel.tools.allowedPaths.forEach((p) => paths.add(String(p)));
|
|
122
|
+
}
|
|
123
|
+
if (channel.defaultAllowedPaths) {
|
|
124
|
+
channel.defaultAllowedPaths.forEach((p) => paths.add(String(p)));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const failures = [];
|
|
128
|
+
for (const path of paths) {
|
|
129
|
+
try {
|
|
130
|
+
accessSync(path, constants.W_OK);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
failures.push(path);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (failures.length > 0) {
|
|
137
|
+
return fail(name, category, `Unwritable paths: ${failures.join(', ')}`, 'Fix filesystem permissions for tool allowlist paths.');
|
|
138
|
+
}
|
|
139
|
+
return ok(name, category, `${paths.size} paths writable`);
|
|
140
|
+
}
|
|
141
|
+
export async function checkProviderAuth(providerName, providerConfig) {
|
|
142
|
+
const name = `provider_${providerName}_auth`;
|
|
143
|
+
const category = 'provider_auth';
|
|
144
|
+
const token = providerConfig.apiKey || providerConfig.authToken || '';
|
|
145
|
+
if (!token) {
|
|
146
|
+
return fail(name, category, 'Missing provider token', `Set ${providerName.toUpperCase()}_API_KEY in ~/.skimpyclaw/.env.`);
|
|
147
|
+
}
|
|
148
|
+
const normalized = providerName.toLowerCase();
|
|
149
|
+
try {
|
|
150
|
+
if (normalized === 'openai') {
|
|
151
|
+
const res = await fetch(`${providerConfig.baseURL || 'https://api.openai.com/v1'}/models`, {
|
|
152
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
153
|
+
signal: AbortSignal.timeout(7000),
|
|
154
|
+
});
|
|
155
|
+
if (!res.ok) {
|
|
156
|
+
return fail(name, category, `${res.status} ${res.statusText}`, `Check ${providerName.toUpperCase()}_API_KEY and provider base URL.`);
|
|
157
|
+
}
|
|
158
|
+
return ok(name, category, `${res.status} ${res.statusText}`);
|
|
159
|
+
}
|
|
160
|
+
if (normalized === 'anthropic') {
|
|
161
|
+
// OAuth tokens can't be validated via API (they use Claude Code's internal refresh flow)
|
|
162
|
+
if (providerConfig.authToken) {
|
|
163
|
+
return ok(name, category, 'OAuth token configured (refresh handled at runtime)');
|
|
164
|
+
}
|
|
165
|
+
const res = await fetch(`${providerConfig.baseURL || 'https://api.anthropic.com'}/v1/models`, {
|
|
166
|
+
headers: {
|
|
167
|
+
'x-api-key': token,
|
|
168
|
+
'anthropic-version': '2023-06-01',
|
|
169
|
+
},
|
|
170
|
+
signal: AbortSignal.timeout(7000),
|
|
171
|
+
});
|
|
172
|
+
if (!res.ok) {
|
|
173
|
+
return fail(name, category, `${res.status} ${res.statusText}`, `Check ANTHROPIC_API_KEY and provider base URL.`);
|
|
174
|
+
}
|
|
175
|
+
return ok(name, category, `${res.status} ${res.statusText}`);
|
|
176
|
+
}
|
|
177
|
+
return ok(name, category, 'Skipped network auth check (unsupported provider type)');
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
181
|
+
return fail(name, category, detail, `Check ${providerName.toUpperCase()}_API_KEY and network connectivity.`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
export async function checkTelegramToken(token) {
|
|
185
|
+
const name = 'telegram_token_valid';
|
|
186
|
+
const category = 'channels';
|
|
187
|
+
if (!token?.trim()) {
|
|
188
|
+
return fail(name, category, 'Missing Telegram token', 'Set TELEGRAM_BOT_TOKEN in ~/.skimpyclaw/.env.');
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const res = await fetch(`https://api.telegram.org/bot${token}/getMe`, {
|
|
192
|
+
signal: AbortSignal.timeout(7000),
|
|
193
|
+
});
|
|
194
|
+
const body = await res.json();
|
|
195
|
+
if (!res.ok || !body.ok) {
|
|
196
|
+
const detail = body.description || `${res.status} ${res.statusText}`;
|
|
197
|
+
return fail(name, category, detail, 'Check TELEGRAM_BOT_TOKEN in ~/.skimpyclaw/.env.');
|
|
198
|
+
}
|
|
199
|
+
return ok(name, category, `Connected as @${body.result?.username || 'unknown'}`);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
203
|
+
return fail(name, category, detail, 'Check network and TELEGRAM_BOT_TOKEN.');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
export async function checkDiscordToken(token) {
|
|
207
|
+
const name = 'discord_token_valid';
|
|
208
|
+
const category = 'channels';
|
|
209
|
+
if (!token?.trim()) {
|
|
210
|
+
return fail(name, category, 'Missing Discord token', 'Set DISCORD_BOT_TOKEN in ~/.skimpyclaw/.env.');
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const res = await fetch('https://discord.com/api/v10/users/@me', {
|
|
214
|
+
headers: { Authorization: `Bot ${token}` },
|
|
215
|
+
signal: AbortSignal.timeout(7000),
|
|
216
|
+
});
|
|
217
|
+
if (!res.ok) {
|
|
218
|
+
return fail(name, category, `${res.status} ${res.statusText}`, 'Check DISCORD_BOT_TOKEN in ~/.skimpyclaw/.env.');
|
|
219
|
+
}
|
|
220
|
+
const body = await res.json();
|
|
221
|
+
return ok(name, category, `Connected as ${body.username || 'unknown'}`);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
225
|
+
return fail(name, category, detail, 'Check network and DISCORD_BOT_TOKEN.');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function isAnyBrowserEnabled(config) {
|
|
229
|
+
return Boolean(config.channels.telegram?.tools?.browser?.enabled
|
|
230
|
+
|| config.channels.discord?.tools?.browser?.enabled
|
|
231
|
+
|| config.heartbeat?.tools?.browser?.enabled);
|
|
232
|
+
}
|
|
233
|
+
export async function checkBrowserBinaryIfEnabled(config) {
|
|
234
|
+
const name = 'browser_binary_available';
|
|
235
|
+
const category = 'runtime';
|
|
236
|
+
if (!isAnyBrowserEnabled(config)) {
|
|
237
|
+
return ok(name, category, 'Browser tools disabled');
|
|
238
|
+
}
|
|
239
|
+
const explicitPath = config.channels.telegram?.tools?.browser?.executablePath
|
|
240
|
+
|| config.channels.discord?.tools?.browser?.executablePath
|
|
241
|
+
|| config.heartbeat?.tools?.browser?.executablePath;
|
|
242
|
+
if (explicitPath) {
|
|
243
|
+
if (existsSync(explicitPath)) {
|
|
244
|
+
return ok(name, category, explicitPath);
|
|
245
|
+
}
|
|
246
|
+
return fail(name, category, `Browser executable not found at ${explicitPath}`, 'Fix tools.browser.executablePath or install a supported browser.');
|
|
247
|
+
}
|
|
248
|
+
const probes = [
|
|
249
|
+
['which', ['google-chrome']],
|
|
250
|
+
['which', ['chromium']],
|
|
251
|
+
['which', ['chromium-browser']],
|
|
252
|
+
['which', ['firefox']],
|
|
253
|
+
];
|
|
254
|
+
for (const [cmd, args] of probes) {
|
|
255
|
+
const probe = spawnSync(cmd, args, { encoding: 'utf-8' });
|
|
256
|
+
if (probe.status === 0 && probe.stdout.trim()) {
|
|
257
|
+
return ok(name, category, probe.stdout.trim());
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const macChrome = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
|
261
|
+
if (existsSync(macChrome)) {
|
|
262
|
+
return ok(name, category, macChrome);
|
|
263
|
+
}
|
|
264
|
+
return fail(name, category, 'No supported browser binary found', 'Install Chrome/Chromium/Firefox or set tools.browser.executablePath.');
|
|
265
|
+
}
|
|
266
|
+
export async function checkVoiceDependencies(config) {
|
|
267
|
+
const name = 'voice_dependencies';
|
|
268
|
+
const category = 'runtime';
|
|
269
|
+
if (!config.voice?.enabled) {
|
|
270
|
+
return ok(name, category, 'Voice disabled');
|
|
271
|
+
}
|
|
272
|
+
const issues = [];
|
|
273
|
+
const ffmpeg = spawnSync('which', ['ffmpeg'], { encoding: 'utf-8' });
|
|
274
|
+
if (ffmpeg.status !== 0) {
|
|
275
|
+
issues.push('ffmpeg not found');
|
|
276
|
+
}
|
|
277
|
+
// Check for whisper-cli (C++) or whisper (Python)
|
|
278
|
+
const whisperCli = spawnSync('which', ['whisper-cli'], { encoding: 'utf-8' });
|
|
279
|
+
const whisperPy = spawnSync('which', ['whisper'], { encoding: 'utf-8' });
|
|
280
|
+
if (whisperCli.status !== 0 && whisperPy.status !== 0) {
|
|
281
|
+
issues.push('whisper not found (neither whisper-cli nor whisper)');
|
|
282
|
+
}
|
|
283
|
+
if (issues.length > 0) {
|
|
284
|
+
return fail(name, category, issues.join('; '), 'Install ffmpeg and whisper-cli (or whisper) for voice features.');
|
|
285
|
+
}
|
|
286
|
+
return ok(name, category, 'ffmpeg and whisper available');
|
|
287
|
+
}
|
|
288
|
+
export async function checkMcpConfig(config) {
|
|
289
|
+
const name = 'mcp_config';
|
|
290
|
+
const category = 'runtime';
|
|
291
|
+
// Check if any tool definitions reference MCP
|
|
292
|
+
const rawConfigPath = join(homedir(), '.skimpyclaw', 'config.json');
|
|
293
|
+
let hasMcpRef = false;
|
|
294
|
+
try {
|
|
295
|
+
const raw = readFileSync(rawConfigPath, 'utf-8');
|
|
296
|
+
hasMcpRef = raw.includes('mcp__') || raw.includes('mcporter');
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// Config not readable — other checks handle that
|
|
300
|
+
}
|
|
301
|
+
if (!hasMcpRef) {
|
|
302
|
+
return ok(name, category, 'MCP tools not configured');
|
|
303
|
+
}
|
|
304
|
+
const mcporterConfig = join(homedir(), '.mcporter', 'mcporter.json');
|
|
305
|
+
if (!existsSync(mcporterConfig)) {
|
|
306
|
+
return fail(name, category, `mcporter config not found at ${mcporterConfig}`, 'Run mcporter setup or create ~/.mcporter/mcporter.json.');
|
|
307
|
+
}
|
|
308
|
+
return ok(name, category, mcporterConfig);
|
|
309
|
+
}
|
|
310
|
+
export async function checkGatewayHostBindable(host) {
|
|
311
|
+
const name = 'gateway_host_bindable';
|
|
312
|
+
const category = 'runtime';
|
|
313
|
+
if (host === '127.0.0.1' || host === '0.0.0.0' || host === 'localhost' || host === '::') {
|
|
314
|
+
return ok(name, category, `${host} (always available)`);
|
|
315
|
+
}
|
|
316
|
+
// Try to bind briefly to verify the IP exists on a local interface
|
|
317
|
+
const canBind = await new Promise((resolve) => {
|
|
318
|
+
const server = net.createServer();
|
|
319
|
+
server.once('error', () => resolve(false));
|
|
320
|
+
server.listen(0, host, () => {
|
|
321
|
+
server.close(() => resolve(true));
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
if (!canBind) {
|
|
325
|
+
return fail(name, category, `Cannot bind to ${host}`, `Verify ${host} exists on a local network interface or set gateway.host to 127.0.0.1.`);
|
|
326
|
+
}
|
|
327
|
+
return ok(name, category, `${host} bindable`);
|
|
328
|
+
}
|
|
329
|
+
export async function checkSkimpyclawDirWritable() {
|
|
330
|
+
const name = 'skimpyclaw_dirs_writable';
|
|
331
|
+
const category = 'runtime';
|
|
332
|
+
const base = join(homedir(), '.skimpyclaw');
|
|
333
|
+
if (!existsSync(base)) {
|
|
334
|
+
return fail(name, category, `Directory not found: ${base}`, 'Run "skimpyclaw onboard" to initialize ~/.skimpyclaw.', true);
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
accessSync(base, constants.W_OK);
|
|
338
|
+
return ok(name, category, base);
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return fail(name, category, `Directory not writable: ${base}`, 'Fix permissions for ~/.skimpyclaw.');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
export async function checkPortAvailability(port) {
|
|
345
|
+
const name = 'gateway_port_available';
|
|
346
|
+
const category = 'runtime';
|
|
347
|
+
const canListen = await new Promise((resolve) => {
|
|
348
|
+
const server = net.createServer();
|
|
349
|
+
server.once('error', () => {
|
|
350
|
+
resolve(false);
|
|
351
|
+
});
|
|
352
|
+
server.listen(port, '127.0.0.1', () => {
|
|
353
|
+
server.close(() => resolve(true));
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
if (!canListen) {
|
|
357
|
+
// Check if SkimpyClaw itself is using the port
|
|
358
|
+
try {
|
|
359
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(3000) });
|
|
360
|
+
if (res.ok) {
|
|
361
|
+
return ok(name, category, `Gateway already running on port ${port}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch { /* not our gateway */ }
|
|
365
|
+
return fail(name, category, `Port ${port} already in use by another process`, `Free port ${port} or set a different gateway.port in config.`);
|
|
366
|
+
}
|
|
367
|
+
return ok(name, category, `Port ${port} is available`);
|
|
368
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const CATEGORY_ORDER = [
|
|
2
|
+
'environment',
|
|
3
|
+
'configuration',
|
|
4
|
+
'provider_auth',
|
|
5
|
+
'channels',
|
|
6
|
+
'runtime',
|
|
7
|
+
];
|
|
8
|
+
function groupChecks(report) {
|
|
9
|
+
const groups = new Map();
|
|
10
|
+
for (const category of CATEGORY_ORDER) {
|
|
11
|
+
groups.set(category, []);
|
|
12
|
+
}
|
|
13
|
+
for (const check of report.checks) {
|
|
14
|
+
const group = groups.get(check.category) || [];
|
|
15
|
+
group.push(check);
|
|
16
|
+
groups.set(check.category, group);
|
|
17
|
+
}
|
|
18
|
+
return groups;
|
|
19
|
+
}
|
|
20
|
+
export function formatDoctorHuman(report) {
|
|
21
|
+
const lines = [];
|
|
22
|
+
const groups = groupChecks(report);
|
|
23
|
+
for (const category of CATEGORY_ORDER) {
|
|
24
|
+
const checks = groups.get(category) || [];
|
|
25
|
+
if (checks.length === 0)
|
|
26
|
+
continue;
|
|
27
|
+
lines.push(category);
|
|
28
|
+
for (const check of checks) {
|
|
29
|
+
const symbol = check.ok ? '✓' : '✗';
|
|
30
|
+
lines.push(`${symbol} ${check.name}: ${check.detail}`);
|
|
31
|
+
if (!check.ok && check.remedy) {
|
|
32
|
+
lines.push(` → ${check.remedy}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
lines.push('');
|
|
36
|
+
}
|
|
37
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
38
|
+
lines.pop();
|
|
39
|
+
}
|
|
40
|
+
return lines.join('\n');
|
|
41
|
+
}
|
|
42
|
+
export function formatDoctorJson(report) {
|
|
43
|
+
return JSON.stringify(report, null, 2);
|
|
44
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { formatDoctorHuman, formatDoctorJson } from './formatters.js';
|
|
2
|
+
import { runDoctor as runDoctorChecks } from './runner.js';
|
|
3
|
+
export async function runDoctor(options = {}) {
|
|
4
|
+
const { report, exitCode } = await runDoctorChecks();
|
|
5
|
+
const output = options.json ? formatDoctorJson(report) : formatDoctorHuman(report);
|
|
6
|
+
return { output, exitCode };
|
|
7
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { loadConfig } from '../config.js';
|
|
2
|
+
import { checkNodeVersion, checkPackageManagerAvailable, checkTypeScriptCompile, checkConfigExistsAndValidJson, checkRequiredEnvVars, checkEnvVarPatterns, checkAllowedPathsWritable, checkProviderAuth, checkTelegramToken, checkDiscordToken, checkBrowserBinaryIfEnabled, checkVoiceDependencies, checkMcpConfig, checkGatewayHostBindable, checkSkimpyclawDirWritable, checkPortAvailability, } from './checks.js';
|
|
3
|
+
export function computeExitCode(report) {
|
|
4
|
+
if (report.checks.some((check) => !check.ok && check.fatal)) {
|
|
5
|
+
return 2;
|
|
6
|
+
}
|
|
7
|
+
if (report.checks.some((check) => !check.ok)) {
|
|
8
|
+
return 1;
|
|
9
|
+
}
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
function asErrorMessage(error) {
|
|
13
|
+
return error instanceof Error ? error.message : String(error);
|
|
14
|
+
}
|
|
15
|
+
async function runSafe(name, category, fn) {
|
|
16
|
+
try {
|
|
17
|
+
const result = await fn();
|
|
18
|
+
return {
|
|
19
|
+
...result,
|
|
20
|
+
name: result.name || name,
|
|
21
|
+
category: result.category || category,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return {
|
|
26
|
+
name,
|
|
27
|
+
category,
|
|
28
|
+
ok: false,
|
|
29
|
+
detail: asErrorMessage(error),
|
|
30
|
+
remedy: 'Fix the underlying error and rerun skimpyclaw doctor.',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function buildReport(startedAt, checks) {
|
|
35
|
+
const finishedAt = new Date().toISOString();
|
|
36
|
+
const temp = {
|
|
37
|
+
ok: checks.every((check) => check.ok),
|
|
38
|
+
exitCode: 0,
|
|
39
|
+
startedAt,
|
|
40
|
+
finishedAt,
|
|
41
|
+
checks,
|
|
42
|
+
};
|
|
43
|
+
const exitCode = computeExitCode(temp);
|
|
44
|
+
temp.ok = exitCode === 0;
|
|
45
|
+
temp.exitCode = exitCode;
|
|
46
|
+
return temp;
|
|
47
|
+
}
|
|
48
|
+
function providerEntries(config) {
|
|
49
|
+
const entries = [];
|
|
50
|
+
for (const [name, provider] of Object.entries(config.models.providers || {})) {
|
|
51
|
+
if (!provider)
|
|
52
|
+
continue;
|
|
53
|
+
entries.push([name, provider]);
|
|
54
|
+
}
|
|
55
|
+
return entries;
|
|
56
|
+
}
|
|
57
|
+
export async function runDoctor() {
|
|
58
|
+
const startedAt = new Date().toISOString();
|
|
59
|
+
const checks = [];
|
|
60
|
+
checks.push(await runSafe('node_version', 'environment', () => checkNodeVersion()));
|
|
61
|
+
checks.push(await runSafe('package_manager_available', 'environment', () => checkPackageManagerAvailable()));
|
|
62
|
+
checks.push(await runSafe('typescript_compile', 'environment', () => checkTypeScriptCompile()));
|
|
63
|
+
const configJson = await runSafe('config_json_valid', 'configuration', () => checkConfigExistsAndValidJson());
|
|
64
|
+
checks.push(configJson);
|
|
65
|
+
if (!configJson.ok && configJson.fatal) {
|
|
66
|
+
const report = buildReport(startedAt, checks);
|
|
67
|
+
return { report, exitCode: report.exitCode };
|
|
68
|
+
}
|
|
69
|
+
let config;
|
|
70
|
+
try {
|
|
71
|
+
config = loadConfig();
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
checks.push({
|
|
75
|
+
name: 'config_load',
|
|
76
|
+
category: 'configuration',
|
|
77
|
+
ok: false,
|
|
78
|
+
detail: asErrorMessage(error),
|
|
79
|
+
remedy: 'Fix config or environment values and rerun skimpyclaw doctor.',
|
|
80
|
+
fatal: true,
|
|
81
|
+
});
|
|
82
|
+
const report = buildReport(startedAt, checks);
|
|
83
|
+
return { report, exitCode: report.exitCode };
|
|
84
|
+
}
|
|
85
|
+
checks.push(await runSafe('required_env_vars', 'configuration', () => checkRequiredEnvVars(config)));
|
|
86
|
+
checks.push(await runSafe('env_var_patterns', 'configuration', () => checkEnvVarPatterns(config)));
|
|
87
|
+
checks.push(await runSafe('allowed_paths_writable', 'configuration', () => checkAllowedPathsWritable(config)));
|
|
88
|
+
const hasFatalConfigFailure = checks.some((check) => check.category === 'configuration' && !check.ok && check.fatal);
|
|
89
|
+
if (!hasFatalConfigFailure) {
|
|
90
|
+
for (const [providerName, providerCfg] of providerEntries(config)) {
|
|
91
|
+
checks.push(await runSafe(`provider_${providerName}_auth`, 'provider_auth', () => checkProviderAuth(providerName, providerCfg)));
|
|
92
|
+
}
|
|
93
|
+
if (config.channels.telegram?.enabled) {
|
|
94
|
+
checks.push(await runSafe('telegram_token_valid', 'channels', () => checkTelegramToken(config.channels.telegram.token)));
|
|
95
|
+
}
|
|
96
|
+
const discord = config.channels.discord;
|
|
97
|
+
if (discord?.enabled) {
|
|
98
|
+
checks.push(await runSafe('discord_token_valid', 'channels', () => checkDiscordToken(discord.token)));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
checks.push(await runSafe('browser_binary_available', 'runtime', () => checkBrowserBinaryIfEnabled(config)));
|
|
102
|
+
checks.push(await runSafe('voice_dependencies', 'runtime', () => checkVoiceDependencies(config)));
|
|
103
|
+
checks.push(await runSafe('mcp_config', 'runtime', () => checkMcpConfig(config)));
|
|
104
|
+
checks.push(await runSafe('gateway_host_bindable', 'runtime', () => checkGatewayHostBindable(config.gateway.host ?? '127.0.0.1')));
|
|
105
|
+
checks.push(await runSafe('skimpyclaw_dirs_writable', 'runtime', () => checkSkimpyclawDirWritable()));
|
|
106
|
+
checks.push(await runSafe('gateway_port_available', 'runtime', () => checkPortAvailability(config.gateway.port)));
|
|
107
|
+
const report = buildReport(startedAt, checks);
|
|
108
|
+
return { report, exitCode: report.exitCode };
|
|
109
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type DoctorCategory = 'environment' | 'configuration' | 'provider_auth' | 'channels' | 'runtime';
|
|
2
|
+
export interface DoctorCheckResult {
|
|
3
|
+
name: string;
|
|
4
|
+
category: DoctorCategory;
|
|
5
|
+
ok: boolean;
|
|
6
|
+
detail: string;
|
|
7
|
+
remedy?: string;
|
|
8
|
+
fatal?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface DoctorReport {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
exitCode: 0 | 1 | 2;
|
|
13
|
+
startedAt: string;
|
|
14
|
+
finishedAt: string;
|
|
15
|
+
checks: DoctorCheckResult[];
|
|
16
|
+
}
|
|
17
|
+
export interface DoctorRunResult {
|
|
18
|
+
report: DoctorReport;
|
|
19
|
+
exitCode: 0 | 1 | 2;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|