clawmoat 0.7.0 → 1.0.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/.dockerignore +9 -0
- package/CHANGELOG.md +18 -0
- package/CONTRIBUTING.md +4 -2
- package/DEMO.md +87 -0
- package/Dockerfile +5 -18
- package/README.md +294 -8
- package/SECURITY.md +58 -10
- package/THREAT_MODEL.md +129 -0
- package/agent/README.md +131 -0
- package/agent/index.js +471 -0
- package/agent/install-service.sh +94 -0
- package/agent/openclaw-hook.js +453 -0
- package/agent/provider-setup.js +649 -0
- package/agent/setup.js +274 -0
- package/assets/BADGE-USAGE.md +20 -0
- package/assets/clawmoat-badge.svg +21 -0
- package/bin/clawmoat.js +468 -111
- package/docs/affiliates/dashboard.html +124 -0
- package/docs/affiliates/index.html +236 -0
- package/docs/agent-install.html +183 -0
- package/docs/ai-agent-security-scanner.html +10 -6
- package/docs/badge/index.html +149 -0
- package/docs/badge/scanning.svg +23 -0
- package/docs/blog/386-malicious-skills.html +262 -0
- package/docs/blog/40000-exposed-openclaw-instances.html +201 -0
- package/docs/blog/agent-trust-protocol.html +198 -0
- package/docs/blog/ai-agent-earns-commissions.html +230 -0
- package/docs/blog/bugmageddon-agent-firewall.html +174 -0
- package/docs/blog/calculator-math.html +180 -0
- package/docs/blog/clawmoat-vs-llamafirewall-nemo-guardrails.html +229 -0
- package/docs/blog/host-guardian-launch.html +18 -8
- package/docs/blog/ibm-experts-agent-runtime-protection.html +247 -0
- package/docs/blog/index.html +211 -9
- package/docs/blog/langchain-security-tutorial.html +18 -8
- package/docs/blog/mcp-30-cves-security-crisis.html +286 -0
- package/docs/blog/meta-researcher-rogue-agent.html +201 -0
- package/docs/blog/microsoft-openclaw-workstation-security.html +235 -0
- package/docs/blog/nist-ai-agent-standards-clawmoat.html +377 -0
- package/docs/blog/oasis-websocket-hijack.html +212 -0
- package/docs/blog/ollama-openclaw-security.html +160 -0
- package/docs/blog/openclaw-enterprise-readiness-claw10.html +199 -0
- package/docs/blog/openclaw-security-reckoning-2026.html +368 -0
- package/docs/blog/owasp-agentic-ai-top10.html +18 -8
- package/docs/blog/securing-ai-agents.html +18 -8
- package/docs/blog/supply-chain-agents.html +18 -8
- package/docs/business/index.html +525 -0
- package/docs/business/install.html +261 -0
- package/docs/checklist.html +174 -0
- package/docs/compare/index.html +122 -0
- package/docs/compare/lakera/index.html +62 -0
- package/docs/compare/llm-guard/index.html +49 -0
- package/docs/compare/snyk-agent-scan/index.html +63 -0
- package/docs/compare.html +10 -6
- package/docs/dashboard/index.html +520 -0
- package/docs/finance/index.html +220 -0
- package/docs/guides/business-deployment.html +770 -0
- package/docs/hall-of-fame.html +174 -0
- package/docs/index.html +447 -154
- package/docs/install.sh +557 -0
- package/docs/integrations/langchain.html +14 -6
- package/docs/integrations/openai.html +14 -6
- package/docs/integrations/openclaw.html +55 -7
- package/docs/plans/2026-03-26-threat-intel-api.md +255 -0
- package/docs/plans/2026-04-14-bugmageddon-marketing-pack.md +329 -0
- package/docs/plans/2026-04-14-clawmoat-v1-bugmageddon.md +248 -0
- package/docs/plans/2026-04-14-v1-release-update.md +91 -0
- package/docs/plans/2026-04-19-supabase-audit.md +68 -0
- package/docs/plans/2026-05-12-sales-push.md +303 -0
- package/docs/playground/index.html +893 -0
- package/docs/playground.html +4 -7
- package/docs/privacy-policy/index.html +122 -0
- package/docs/rfcs/defense-in-depth.md +467 -0
- package/docs/scan/index.html +358 -0
- package/docs/services/case-study.html +255 -0
- package/docs/services/downloads/install-openclaw.bat +45 -0
- package/docs/services/downloads/install-openclaw.command +38 -0
- package/docs/services/downloads/install-openclaw.sh +38 -0
- package/docs/services/get-started.html +165 -0
- package/docs/services/index.html +598 -0
- package/docs/services/multi-agent-security.html +284 -0
- package/docs/services/one-pager.html +99 -0
- package/docs/services/pitch-deck.html +229 -0
- package/docs/services/roi-calculator.html +258 -0
- package/docs/sitemap.xml +192 -2
- package/docs/support/index.html +135 -0
- package/docs/templates/customer-service/HEARTBEAT.md +61 -0
- package/docs/templates/customer-service/MEMORY.md +89 -0
- package/docs/templates/customer-service/SOUL.md +41 -0
- package/docs/templates/customer-service/USER.md +56 -0
- package/docs/templates/executive/HEARTBEAT.md +86 -0
- package/docs/templates/executive/MEMORY.md +92 -0
- package/docs/templates/executive/SOUL.md +44 -0
- package/docs/templates/executive/USER.md +62 -0
- package/docs/templates/finance/HEARTBEAT.md +58 -0
- package/docs/templates/finance/MEMORY.md +87 -0
- package/docs/templates/finance/SOUL.md +38 -0
- package/docs/templates/finance/USER.md +53 -0
- package/docs/templates/index.html +115 -0
- package/docs/templates/operations/HEARTBEAT.md +63 -0
- package/docs/templates/operations/MEMORY.md +68 -0
- package/docs/templates/operations/SOUL.md +38 -0
- package/docs/templates/operations/USER.md +49 -0
- package/docs/templates/sales/HEARTBEAT.md +55 -0
- package/docs/templates/sales/MEMORY.md +89 -0
- package/docs/templates/sales/SOUL.md +34 -0
- package/docs/templates/sales/USER.md +54 -0
- package/docs/terms-of-service/index.html +122 -0
- package/eslint.config.js +32 -0
- package/evals/README.md +29 -0
- package/evals/cases.json +390 -0
- package/evals/results.md +68 -0
- package/evals/run.js +180 -0
- package/examples/basic-usage.js +38 -0
- package/examples/demo-attack/demo.js +186 -0
- package/examples/python-quickstart/README.md +54 -0
- package/examples/python-quickstart/clawmoat_client.py +167 -0
- package/examples/video-demo/README.md +14 -0
- package/examples/video-demo/scene-a-normal.js +29 -0
- package/examples/video-demo/scene-b-attack-arrives.js +31 -0
- package/examples/video-demo/scene-c-hijack.js +44 -0
- package/examples/video-demo/scene-d-clawmoat.js +46 -0
- package/integrations/crewai/README.md +32 -0
- package/integrations/crewai/clawmoat_crewai/__init__.py +17 -0
- package/integrations/crewai/clawmoat_crewai/guard.py +103 -0
- package/integrations/crewai/pyproject.toml +21 -0
- package/integrations/langchain/README.md +91 -0
- package/integrations/langchain/clawmoat_langchain/__init__.py +17 -0
- package/integrations/langchain/clawmoat_langchain/callback.py +489 -0
- package/integrations/langchain/pyproject.toml +32 -0
- package/integrations/litellm/README.md +324 -0
- package/integrations/litellm/clawmoat_litellm/__init__.py +21 -0
- package/integrations/litellm/clawmoat_litellm/callback.py +329 -0
- package/integrations/litellm/clawmoat_litellm/proxy_middleware.py +224 -0
- package/integrations/litellm/pyproject.toml +74 -0
- package/integrations/openai-agents/README.md +392 -0
- package/integrations/openai-agents/clawmoat_openai_agents/__init__.py +20 -0
- package/integrations/openai-agents/clawmoat_openai_agents/guardrail.py +431 -0
- package/integrations/openai-agents/clawmoat_openai_agents/middleware.py +311 -0
- package/integrations/openai-agents/pyproject.toml +76 -0
- package/package.json +6 -5
- package/plugins/openclaw-adapter/PHASE1.md +439 -0
- package/plugins/openclaw-adapter/README.md +103 -0
- package/plugins/openclaw-adapter/SPEC.md +1644 -0
- package/plugins/openclaw-adapter/package.json +31 -0
- package/plugins/openclaw-adapter/src/index.test.ts +226 -0
- package/plugins/openclaw-adapter/src/index.ts +140 -0
- package/plugins/openclaw-adapter/tsconfig.json +14 -0
- package/server/data/threats.json +290 -0
- package/server/index.js +224 -10
- package/src/adapters/express.js +161 -0
- package/src/adapters/index.js +92 -0
- package/src/adapters/langchain.js +185 -0
- package/src/approval/index.js +456 -0
- package/src/ban-scanner.js +200 -0
- package/src/boundary-scanner.js +296 -0
- package/src/ci-scanner.js +279 -0
- package/src/code-scanner.js +245 -0
- package/src/enforce.js +166 -0
- package/src/finance/index.js +585 -0
- package/src/finance/mcp-firewall.js +486 -0
- package/src/formatters/json.js +80 -0
- package/src/formatters/sarif.js +388 -0
- package/src/guardian/alerts.js +34 -3
- package/src/guardian/gateway-monitor.js +590 -0
- package/src/guardian/index.js +41 -2
- package/src/index.js +105 -0
- package/src/integrations/agentmesh.js +501 -0
- package/src/language-detector.js +201 -0
- package/src/mcp-scanner.js +253 -0
- package/src/multimodal/index.js +579 -0
- package/src/obfuscation-scanner.js +457 -0
- package/src/policy-engine.js +402 -0
- package/src/scanners/dependency-attacks.js +128 -0
- package/src/scanners/prompt-injection.js +18 -0
- package/src/scanners/supply-chain.js +14 -0
- package/src/templates/default-config.yml +90 -0
- package/src/vuln-ops/exploitability.js +46 -0
- package/src/watch/live-monitor.js +720 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ClawMoat Provider Setup
|
|
4
|
+
*
|
|
5
|
+
* Interactive setup for AI provider connections.
|
|
6
|
+
* Supports Claude (Anthropic), ChatGPT/Codex (OpenAI), and Kimi (Moonshot).
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node agent/provider-setup.js Interactive setup
|
|
10
|
+
* node agent/provider-setup.js --list Show configured providers
|
|
11
|
+
* node agent/provider-setup.js --test Test all connections
|
|
12
|
+
* node agent/provider-setup.js --openclaw Generate OpenClaw config snippet
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const https = require('https');
|
|
21
|
+
const http = require('http');
|
|
22
|
+
const crypto = require('crypto');
|
|
23
|
+
const readline = require('readline');
|
|
24
|
+
|
|
25
|
+
const CONFIG_DIR = path.join(os.homedir(), '.clawmoat');
|
|
26
|
+
const PROVIDERS_PATH = path.join(CONFIG_DIR, 'providers.json');
|
|
27
|
+
|
|
28
|
+
// ─── Colors ───────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const BOLD = '\x1b[1m';
|
|
31
|
+
const DIM = '\x1b[2m';
|
|
32
|
+
const RESET = '\x1b[0m';
|
|
33
|
+
const RED = '\x1b[31m';
|
|
34
|
+
const GREEN = '\x1b[32m';
|
|
35
|
+
const YELLOW = '\x1b[33m';
|
|
36
|
+
const CYAN = '\x1b[36m';
|
|
37
|
+
const MAGENTA = '\x1b[35m';
|
|
38
|
+
|
|
39
|
+
// ─── Providers ────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const PROVIDERS = {
|
|
42
|
+
anthropic: {
|
|
43
|
+
name: 'Claude (Anthropic)',
|
|
44
|
+
emoji: '🟣',
|
|
45
|
+
description: 'Claude Max subscription or API key',
|
|
46
|
+
models: ['claude-opus-4-6', 'claude-sonnet-4-6'],
|
|
47
|
+
methods: ['api-key', 'setup-token'],
|
|
48
|
+
},
|
|
49
|
+
'openai-codex': {
|
|
50
|
+
name: 'ChatGPT / Codex (OpenAI)',
|
|
51
|
+
emoji: '🟢',
|
|
52
|
+
description: 'GPT Max subscription (OAuth) or API key',
|
|
53
|
+
models: ['gpt-5.4', 'gpt-5.4-pro'],
|
|
54
|
+
methods: ['oauth', 'api-key'],
|
|
55
|
+
},
|
|
56
|
+
'kimi-coding': {
|
|
57
|
+
name: 'Kimi (Moonshot AI)',
|
|
58
|
+
emoji: '🔵',
|
|
59
|
+
description: 'Kimi annual subscription or API key',
|
|
60
|
+
models: ['kimi-k2.5', 'kimi-k2-thinking'],
|
|
61
|
+
methods: ['kimi-code-key', 'moonshot-api-key'],
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ─── Kimi Header Generation ──────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
function generateKimiHeaders(deviceName) {
|
|
68
|
+
const hostname = deviceName || os.hostname();
|
|
69
|
+
const deviceId = crypto.randomBytes(16).toString('hex');
|
|
70
|
+
const platform = `${os.type()} ${os.release()} ${os.arch()}`;
|
|
71
|
+
const kimiVersion = '1.22.0';
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
'User-Agent': `KimiCLI/${kimiVersion}`,
|
|
75
|
+
'X-Msh-Platform': 'kimi_cli',
|
|
76
|
+
'X-Msh-Version': kimiVersion,
|
|
77
|
+
'X-Msh-Device-Name': hostname,
|
|
78
|
+
'X-Msh-Device-Model': platform,
|
|
79
|
+
'X-Msh-Device-Id': deviceId,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── HTTP Helper ──────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
function httpRequest(url, options = {}) {
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
const parsed = new URL(url);
|
|
88
|
+
const mod = parsed.protocol === 'https:' ? https : http;
|
|
89
|
+
const body = options.body ? JSON.stringify(options.body) : null;
|
|
90
|
+
|
|
91
|
+
const req = mod.request({
|
|
92
|
+
hostname: parsed.hostname,
|
|
93
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
94
|
+
path: parsed.pathname + parsed.search,
|
|
95
|
+
method: options.method || 'POST',
|
|
96
|
+
headers: {
|
|
97
|
+
'Content-Type': 'application/json',
|
|
98
|
+
...(body ? { 'Content-Length': Buffer.byteLength(body) } : {}),
|
|
99
|
+
...(options.headers || {}),
|
|
100
|
+
},
|
|
101
|
+
timeout: 15000,
|
|
102
|
+
}, res => {
|
|
103
|
+
let data = '';
|
|
104
|
+
res.on('data', c => data += c);
|
|
105
|
+
res.on('end', () => {
|
|
106
|
+
try {
|
|
107
|
+
resolve({ status: res.statusCode, data: JSON.parse(data) });
|
|
108
|
+
} catch {
|
|
109
|
+
resolve({ status: res.statusCode, data, raw: true });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
req.on('error', e => resolve({ status: 0, error: e.message }));
|
|
115
|
+
req.on('timeout', () => { req.destroy(); resolve({ status: 0, error: 'Timeout' }); });
|
|
116
|
+
if (body) req.write(body);
|
|
117
|
+
req.end();
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── Provider Testers ─────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
async function testAnthropic(config) {
|
|
124
|
+
if (config.method === 'setup-token') {
|
|
125
|
+
return { ok: true, note: 'Setup token configured. Run "openclaw models auth paste-token" to activate.' };
|
|
126
|
+
}
|
|
127
|
+
const res = await httpRequest('https://api.anthropic.com/v1/messages', {
|
|
128
|
+
headers: {
|
|
129
|
+
'x-api-key': config.apiKey,
|
|
130
|
+
'anthropic-version': '2023-06-01',
|
|
131
|
+
},
|
|
132
|
+
body: {
|
|
133
|
+
model: 'claude-sonnet-4-6',
|
|
134
|
+
max_tokens: 10,
|
|
135
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
if (res.status === 200) return { ok: true };
|
|
139
|
+
if (res.status === 401) return { ok: false, error: 'Invalid API key' };
|
|
140
|
+
if (res.status === 429) return { ok: true, note: 'Rate limited but key is valid' };
|
|
141
|
+
return { ok: false, error: `HTTP ${res.status}: ${JSON.stringify(res.data).slice(0, 100)}` };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function testOpenAICodex(config) {
|
|
145
|
+
if (config.method === 'oauth') {
|
|
146
|
+
return { ok: true, note: 'OAuth configured. Run "openclaw models auth login --provider openai-codex" to authenticate.' };
|
|
147
|
+
}
|
|
148
|
+
const res = await httpRequest('https://api.openai.com/v1/chat/completions', {
|
|
149
|
+
headers: { 'Authorization': `Bearer ${config.apiKey}` },
|
|
150
|
+
body: {
|
|
151
|
+
model: 'gpt-4o-mini',
|
|
152
|
+
max_tokens: 10,
|
|
153
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
if (res.status === 200) return { ok: true };
|
|
157
|
+
if (res.status === 401) return { ok: false, error: 'Invalid API key' };
|
|
158
|
+
if (res.status === 429) return { ok: true, note: 'Rate limited but key is valid' };
|
|
159
|
+
return { ok: false, error: `HTTP ${res.status}: ${JSON.stringify(res.data).slice(0, 100)}` };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function testKimiCoding(config) {
|
|
163
|
+
const headers = {
|
|
164
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
165
|
+
...config.headers,
|
|
166
|
+
};
|
|
167
|
+
const res = await httpRequest('https://api.kimi.com/coding/v1/chat/completions', {
|
|
168
|
+
headers,
|
|
169
|
+
body: {
|
|
170
|
+
model: 'kimi-k2.5',
|
|
171
|
+
max_tokens: 10,
|
|
172
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
if (res.status === 200) return { ok: true };
|
|
176
|
+
if (res.data && res.data.error) {
|
|
177
|
+
const msg = res.data.error.message || '';
|
|
178
|
+
if (msg.includes('only available for Coding Agents')) {
|
|
179
|
+
return { ok: false, error: 'Missing Kimi CLI headers. This is a bug — please report it.' };
|
|
180
|
+
}
|
|
181
|
+
if (msg.includes('insufficient balance')) {
|
|
182
|
+
return { ok: false, error: 'Insufficient balance on Kimi account' };
|
|
183
|
+
}
|
|
184
|
+
if (msg.includes('Invalid Authentication')) {
|
|
185
|
+
return { ok: false, error: 'Invalid API key' };
|
|
186
|
+
}
|
|
187
|
+
return { ok: false, error: msg.slice(0, 100) };
|
|
188
|
+
}
|
|
189
|
+
if (res.status === 429) return { ok: true, note: 'Rate limited but key is valid' };
|
|
190
|
+
return { ok: false, error: `HTTP ${res.status}` };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function testMoonshotAPI(config) {
|
|
194
|
+
const res = await httpRequest('https://api.moonshot.ai/v1/chat/completions', {
|
|
195
|
+
headers: { 'Authorization': `Bearer ${config.apiKey}` },
|
|
196
|
+
body: {
|
|
197
|
+
model: 'kimi-k2.5',
|
|
198
|
+
max_tokens: 10,
|
|
199
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
if (res.status === 200) return { ok: true };
|
|
203
|
+
if (res.data && res.data.error) {
|
|
204
|
+
const msg = res.data.error.message || '';
|
|
205
|
+
if (msg.includes('insufficient balance')) return { ok: false, error: 'Zero balance. Top up at platform.moonshot.ai' };
|
|
206
|
+
if (msg.includes('Invalid Authentication')) return { ok: false, error: 'Invalid API key' };
|
|
207
|
+
return { ok: false, error: msg.slice(0, 100) };
|
|
208
|
+
}
|
|
209
|
+
return { ok: false, error: `HTTP ${res.status}` };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const TESTERS = {
|
|
213
|
+
anthropic: testAnthropic,
|
|
214
|
+
'openai-codex': testOpenAICodex,
|
|
215
|
+
'kimi-coding': testKimiCoding,
|
|
216
|
+
'moonshot': testMoonshotAPI,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// ─── Config I/O ───────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
function loadProviders() {
|
|
222
|
+
try {
|
|
223
|
+
return JSON.parse(fs.readFileSync(PROVIDERS_PATH, 'utf8'));
|
|
224
|
+
} catch {
|
|
225
|
+
return {};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function saveProviders(providers) {
|
|
230
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
231
|
+
fs.writeFileSync(PROVIDERS_PATH, JSON.stringify(providers, null, 2));
|
|
232
|
+
try { fs.chmodSync(PROVIDERS_PATH, 0o600); } catch {}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── OpenClaw Config Generator ────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
function generateOpenClawConfig(providers) {
|
|
238
|
+
const config = {
|
|
239
|
+
env: {},
|
|
240
|
+
agents: { defaults: { models: {} } },
|
|
241
|
+
models: { mode: 'merge', providers: {} },
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
if (providers.anthropic) {
|
|
245
|
+
if (providers.anthropic.method === 'api-key') {
|
|
246
|
+
config.env.ANTHROPIC_API_KEY = providers.anthropic.apiKey;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (providers['openai-codex']) {
|
|
251
|
+
const p = providers['openai-codex'];
|
|
252
|
+
if (p.method === 'api-key') {
|
|
253
|
+
config.env.OPENAI_API_KEY = p.apiKey;
|
|
254
|
+
}
|
|
255
|
+
// OAuth needs: openclaw models auth login --provider openai-codex
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (providers['kimi-coding']) {
|
|
259
|
+
const p = providers['kimi-coding'];
|
|
260
|
+
if (p.method === 'kimi-code-key') {
|
|
261
|
+
config.env.KIMI_API_KEY = p.apiKey;
|
|
262
|
+
config.models.providers['kimi-coding'] = {
|
|
263
|
+
baseUrl: 'https://api.kimi.com/coding/v1',
|
|
264
|
+
apiKey: '${KIMI_API_KEY}',
|
|
265
|
+
api: 'openai-completions',
|
|
266
|
+
headers: p.headers,
|
|
267
|
+
models: [
|
|
268
|
+
{ id: 'kimi-k2.5', name: 'Kimi K2.5', contextWindow: 262144, maxTokens: 8192 },
|
|
269
|
+
{ id: 'kimi-k2-thinking', name: 'Kimi K2 Thinking', contextWindow: 262144, maxTokens: 8192 },
|
|
270
|
+
],
|
|
271
|
+
};
|
|
272
|
+
} else if (p.method === 'moonshot-api-key') {
|
|
273
|
+
config.env.MOONSHOT_API_KEY = p.apiKey;
|
|
274
|
+
config.models.providers.moonshot = {
|
|
275
|
+
baseUrl: 'https://api.moonshot.ai/v1',
|
|
276
|
+
apiKey: '${MOONSHOT_API_KEY}',
|
|
277
|
+
api: 'openai-completions',
|
|
278
|
+
models: [
|
|
279
|
+
{ id: 'kimi-k2.5', name: 'Kimi K2.5', contextWindow: 262144, maxTokens: 8192 },
|
|
280
|
+
],
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Clean up empty sections
|
|
286
|
+
if (!Object.keys(config.env).length) delete config.env;
|
|
287
|
+
if (!Object.keys(config.models.providers).length) delete config.models;
|
|
288
|
+
if (!Object.keys(config.agents.defaults.models).length) delete config.agents;
|
|
289
|
+
|
|
290
|
+
return config;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ─── Interactive Setup ────────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
function createRL() {
|
|
296
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function ask(rl, question, defaultVal) {
|
|
300
|
+
return new Promise(resolve => {
|
|
301
|
+
const suffix = defaultVal ? ` [${defaultVal}]` : '';
|
|
302
|
+
rl.question(`${question}${suffix}: `, answer => {
|
|
303
|
+
resolve(answer.trim() || defaultVal || '');
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function askYN(rl, question, defaultYes = true) {
|
|
309
|
+
const suffix = defaultYes ? '[Y/n]' : '[y/N]';
|
|
310
|
+
return new Promise(resolve => {
|
|
311
|
+
rl.question(`${question} ${suffix}: `, answer => {
|
|
312
|
+
const a = answer.trim().toLowerCase();
|
|
313
|
+
if (!a) resolve(defaultYes);
|
|
314
|
+
else resolve(a === 'y' || a === 'yes');
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function askChoice(rl, question, choices) {
|
|
320
|
+
return new Promise(resolve => {
|
|
321
|
+
console.log(`\n${question}`);
|
|
322
|
+
choices.forEach((c, i) => console.log(` ${CYAN}${i + 1}${RESET}) ${c.label}`));
|
|
323
|
+
rl.question(`\nChoice [1-${choices.length}]: `, answer => {
|
|
324
|
+
const idx = parseInt(answer.trim(), 10) - 1;
|
|
325
|
+
if (idx >= 0 && idx < choices.length) resolve(choices[idx].value);
|
|
326
|
+
else resolve(choices[0].value);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function setupAnthropic(rl, existing) {
|
|
332
|
+
console.log(`\n${BOLD}${MAGENTA}🟣 Claude (Anthropic)${RESET}`);
|
|
333
|
+
console.log(`${DIM}Use your Claude Max subscription or an Anthropic API key.${RESET}\n`);
|
|
334
|
+
|
|
335
|
+
const method = await askChoice(rl, 'How do you want to connect?', [
|
|
336
|
+
{ label: 'API key (from console.anthropic.com)', value: 'api-key' },
|
|
337
|
+
{ label: 'Claude Max (setup-token from Claude CLI)', value: 'setup-token' },
|
|
338
|
+
{ label: 'Skip — not using Claude', value: 'skip' },
|
|
339
|
+
]);
|
|
340
|
+
|
|
341
|
+
if (method === 'skip') return null;
|
|
342
|
+
|
|
343
|
+
if (method === 'api-key') {
|
|
344
|
+
const apiKey = await ask(rl, 'Anthropic API key (sk-ant-...)');
|
|
345
|
+
if (!apiKey) return null;
|
|
346
|
+
|
|
347
|
+
process.stdout.write('Testing connection... ');
|
|
348
|
+
const result = await testAnthropic({ method, apiKey });
|
|
349
|
+
if (result.ok) {
|
|
350
|
+
console.log(`${GREEN}✓ Connected${RESET}${result.note ? ` (${result.note})` : ''}`);
|
|
351
|
+
} else {
|
|
352
|
+
console.log(`${RED}✗ ${result.error}${RESET}`);
|
|
353
|
+
const cont = await askYN(rl, 'Save anyway?', false);
|
|
354
|
+
if (!cont) return null;
|
|
355
|
+
}
|
|
356
|
+
return { method, apiKey, model: 'claude-sonnet-4-6', configuredAt: new Date().toISOString() };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (method === 'setup-token') {
|
|
360
|
+
console.log(`\n${YELLOW}To get a setup token:${RESET}`);
|
|
361
|
+
console.log(` 1. Install Claude CLI: ${CYAN}npm install -g @anthropic-ai/claude-code${RESET}`);
|
|
362
|
+
console.log(` 2. Run: ${CYAN}claude setup-token${RESET}`);
|
|
363
|
+
console.log(` 3. Paste the token below\n`);
|
|
364
|
+
const token = await ask(rl, 'Setup token');
|
|
365
|
+
if (!token) return null;
|
|
366
|
+
return { method, setupToken: token, model: 'claude-sonnet-4-6', configuredAt: new Date().toISOString() };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function setupOpenAICodex(rl, existing) {
|
|
371
|
+
console.log(`\n${BOLD}${GREEN}🟢 ChatGPT / Codex (OpenAI)${RESET}`);
|
|
372
|
+
console.log(`${DIM}Use your GPT Max subscription (OAuth) or an OpenAI API key.${RESET}\n`);
|
|
373
|
+
|
|
374
|
+
const method = await askChoice(rl, 'How do you want to connect?', [
|
|
375
|
+
{ label: 'GPT Max subscription (OAuth — recommended)', value: 'oauth' },
|
|
376
|
+
{ label: 'API key (from platform.openai.com)', value: 'api-key' },
|
|
377
|
+
{ label: 'Skip — not using ChatGPT', value: 'skip' },
|
|
378
|
+
]);
|
|
379
|
+
|
|
380
|
+
if (method === 'skip') return null;
|
|
381
|
+
|
|
382
|
+
if (method === 'oauth') {
|
|
383
|
+
console.log(`\n${YELLOW}To complete OAuth setup:${RESET}`);
|
|
384
|
+
console.log(` 1. Install Codex CLI: ${CYAN}npm install -g @openai/codex${RESET}`);
|
|
385
|
+
console.log(` 2. Run: ${CYAN}openclaw models auth login --provider openai-codex${RESET}`);
|
|
386
|
+
console.log(` 3. Approve in browser when prompted\n`);
|
|
387
|
+
console.log(`${DIM}This uses your GPT Max subscription — no API charges.${RESET}`);
|
|
388
|
+
return { method, model: 'gpt-5.4', configuredAt: new Date().toISOString() };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (method === 'api-key') {
|
|
392
|
+
const apiKey = await ask(rl, 'OpenAI API key (sk-...)');
|
|
393
|
+
if (!apiKey) return null;
|
|
394
|
+
|
|
395
|
+
process.stdout.write('Testing connection... ');
|
|
396
|
+
const result = await testOpenAICodex({ method, apiKey });
|
|
397
|
+
if (result.ok) {
|
|
398
|
+
console.log(`${GREEN}✓ Connected${RESET}${result.note ? ` (${result.note})` : ''}`);
|
|
399
|
+
} else {
|
|
400
|
+
console.log(`${RED}✗ ${result.error}${RESET}`);
|
|
401
|
+
const cont = await askYN(rl, 'Save anyway?', false);
|
|
402
|
+
if (!cont) return null;
|
|
403
|
+
}
|
|
404
|
+
return { method, apiKey, model: 'gpt-5.4', configuredAt: new Date().toISOString() };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function setupKimiCoding(rl, existing) {
|
|
409
|
+
console.log(`\n${BOLD}${CYAN}🔵 Kimi (Moonshot AI)${RESET}`);
|
|
410
|
+
console.log(`${DIM}Use your Kimi annual subscription or a Moonshot API key.${RESET}\n`);
|
|
411
|
+
|
|
412
|
+
const method = await askChoice(rl, 'How do you want to connect?', [
|
|
413
|
+
{ label: 'Kimi subscription key (sk-kimi-... from kimi.com/code/console)', value: 'kimi-code-key' },
|
|
414
|
+
{ label: 'Moonshot API key (sk-... from platform.moonshot.ai)', value: 'moonshot-api-key' },
|
|
415
|
+
{ label: 'Skip — not using Kimi', value: 'skip' },
|
|
416
|
+
]);
|
|
417
|
+
|
|
418
|
+
if (method === 'skip') return null;
|
|
419
|
+
|
|
420
|
+
if (method === 'kimi-code-key') {
|
|
421
|
+
console.log(`\n${YELLOW}Get your Kimi Code key:${RESET}`);
|
|
422
|
+
console.log(` 1. Go to ${CYAN}https://kimi.com/code/console${RESET}`);
|
|
423
|
+
console.log(` 2. Click "Create new API Key"`);
|
|
424
|
+
console.log(` 3. Copy the key (starts with sk-kimi-...)\n`);
|
|
425
|
+
|
|
426
|
+
const apiKey = await ask(rl, 'Kimi Code API key (sk-kimi-...)');
|
|
427
|
+
if (!apiKey) return null;
|
|
428
|
+
|
|
429
|
+
const deviceName = os.hostname();
|
|
430
|
+
const headers = generateKimiHeaders(deviceName);
|
|
431
|
+
|
|
432
|
+
process.stdout.write('Testing connection... ');
|
|
433
|
+
const result = await testKimiCoding({ method, apiKey, headers });
|
|
434
|
+
if (result.ok) {
|
|
435
|
+
console.log(`${GREEN}✓ Connected to Kimi K2.5${RESET}`);
|
|
436
|
+
} else {
|
|
437
|
+
console.log(`${RED}✗ ${result.error}${RESET}`);
|
|
438
|
+
const cont = await askYN(rl, 'Save anyway?', false);
|
|
439
|
+
if (!cont) return null;
|
|
440
|
+
}
|
|
441
|
+
return { method, apiKey, headers, model: 'kimi-k2.5', configuredAt: new Date().toISOString() };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (method === 'moonshot-api-key') {
|
|
445
|
+
console.log(`\n${YELLOW}Get your Moonshot API key:${RESET}`);
|
|
446
|
+
console.log(` 1. Go to ${CYAN}https://platform.moonshot.ai${RESET}`);
|
|
447
|
+
console.log(` 2. Create an API key (starts with sk-...)`);
|
|
448
|
+
console.log(` 3. ${DIM}Note: this is pay-per-token, separate from Kimi subscription${RESET}\n`);
|
|
449
|
+
|
|
450
|
+
const apiKey = await ask(rl, 'Moonshot API key (sk-...)');
|
|
451
|
+
if (!apiKey) return null;
|
|
452
|
+
|
|
453
|
+
process.stdout.write('Testing connection... ');
|
|
454
|
+
const result = await testMoonshotAPI({ apiKey });
|
|
455
|
+
if (result.ok) {
|
|
456
|
+
console.log(`${GREEN}✓ Connected${RESET}`);
|
|
457
|
+
} else {
|
|
458
|
+
console.log(`${RED}✗ ${result.error}${RESET}`);
|
|
459
|
+
const cont = await askYN(rl, 'Save anyway?', false);
|
|
460
|
+
if (!cont) return null;
|
|
461
|
+
}
|
|
462
|
+
return { method, apiKey, model: 'kimi-k2.5', configuredAt: new Date().toISOString() };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
467
|
+
|
|
468
|
+
async function cmdSetup() {
|
|
469
|
+
const rl = createRL();
|
|
470
|
+
|
|
471
|
+
console.log(`\n${BOLD}╔════════════════════════════════════════╗${RESET}`);
|
|
472
|
+
console.log(`${BOLD}║ ClawMoat — AI Provider Setup ║${RESET}`);
|
|
473
|
+
console.log(`${BOLD}╚════════════════════════════════════════╝${RESET}`);
|
|
474
|
+
console.log(`\nConfigure your AI subscriptions for use with OpenClaw.`);
|
|
475
|
+
console.log(`${DIM}Use your existing paid plans — no extra API costs.${RESET}\n`);
|
|
476
|
+
|
|
477
|
+
const existing = loadProviders();
|
|
478
|
+
|
|
479
|
+
// Show existing if any
|
|
480
|
+
if (Object.keys(existing).length) {
|
|
481
|
+
console.log(`${DIM}Currently configured:${RESET}`);
|
|
482
|
+
for (const [id, p] of Object.entries(existing)) {
|
|
483
|
+
const info = PROVIDERS[id] || {};
|
|
484
|
+
console.log(` ${info.emoji || '•'} ${info.name || id} (${p.method}, ${p.model || '?'})`);
|
|
485
|
+
}
|
|
486
|
+
console.log('');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const providers = { ...existing };
|
|
490
|
+
|
|
491
|
+
// Claude
|
|
492
|
+
const wantClaude = await askYN(rl, `Configure ${PROVIDERS.anthropic.emoji} Claude (Anthropic)?`, !existing.anthropic);
|
|
493
|
+
if (wantClaude) {
|
|
494
|
+
const result = await setupAnthropic(rl, existing.anthropic);
|
|
495
|
+
if (result) providers.anthropic = result;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// OpenAI
|
|
499
|
+
const wantOpenAI = await askYN(rl, `\nConfigure ${PROVIDERS['openai-codex'].emoji} ChatGPT / Codex (OpenAI)?`, !existing['openai-codex']);
|
|
500
|
+
if (wantOpenAI) {
|
|
501
|
+
const result = await setupOpenAICodex(rl, existing['openai-codex']);
|
|
502
|
+
if (result) providers['openai-codex'] = result;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Kimi
|
|
506
|
+
const wantKimi = await askYN(rl, `\nConfigure ${PROVIDERS['kimi-coding'].emoji} Kimi (Moonshot AI)?`, !existing['kimi-coding']);
|
|
507
|
+
if (wantKimi) {
|
|
508
|
+
const result = await setupKimiCoding(rl, existing['kimi-coding']);
|
|
509
|
+
if (result) providers['kimi-coding'] = result;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Save
|
|
513
|
+
const configured = Object.keys(providers).filter(k => providers[k]);
|
|
514
|
+
if (configured.length) {
|
|
515
|
+
saveProviders(providers);
|
|
516
|
+
console.log(`\n${GREEN}✓ ${configured.length} provider(s) saved to ${PROVIDERS_PATH}${RESET}`);
|
|
517
|
+
|
|
518
|
+
// Offer OpenClaw config
|
|
519
|
+
const wantSnippet = await askYN(rl, '\nGenerate OpenClaw config snippet?', true);
|
|
520
|
+
if (wantSnippet) {
|
|
521
|
+
const snippet = generateOpenClawConfig(providers);
|
|
522
|
+
console.log(`\n${BOLD}Add this to your ~/.openclaw/openclaw.json:${RESET}\n`);
|
|
523
|
+
console.log(JSON.stringify(snippet, null, 2));
|
|
524
|
+
|
|
525
|
+
const wantSave = await askYN(rl, '\nSave snippet to ~/.clawmoat/openclaw-snippet.json?', true);
|
|
526
|
+
if (wantSave) {
|
|
527
|
+
fs.writeFileSync(path.join(CONFIG_DIR, 'openclaw-snippet.json'), JSON.stringify(snippet, null, 2));
|
|
528
|
+
console.log(`${GREEN}✓ Saved${RESET}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Next steps
|
|
533
|
+
console.log(`\n${BOLD}Next steps:${RESET}`);
|
|
534
|
+
if (providers['openai-codex'] && providers['openai-codex'].method === 'oauth') {
|
|
535
|
+
console.log(` ${CYAN}openclaw models auth login --provider openai-codex${RESET} (one-time browser auth)`);
|
|
536
|
+
}
|
|
537
|
+
if (providers.anthropic && providers.anthropic.method === 'setup-token') {
|
|
538
|
+
console.log(` ${CYAN}openclaw models auth paste-token --provider anthropic${RESET}`);
|
|
539
|
+
}
|
|
540
|
+
console.log(` ${CYAN}openclaw gateway restart${RESET} (apply changes)`);
|
|
541
|
+
} else {
|
|
542
|
+
console.log(`\n${YELLOW}No providers configured.${RESET}`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
rl.close();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function cmdList() {
|
|
549
|
+
const providers = loadProviders();
|
|
550
|
+
if (!Object.keys(providers).length) {
|
|
551
|
+
console.log(`${YELLOW}No providers configured.${RESET} Run: ${CYAN}clawmoat providers setup${RESET}`);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
console.log(`\n${BOLD}Configured AI Providers${RESET}\n`);
|
|
556
|
+
for (const [id, p] of Object.entries(providers)) {
|
|
557
|
+
const info = PROVIDERS[id] || {};
|
|
558
|
+
const keyHint = p.apiKey ? `${p.apiKey.slice(0, 12)}...${p.apiKey.slice(-4)}` : '(OAuth/token)';
|
|
559
|
+
console.log(`${info.emoji || '•'} ${BOLD}${info.name || id}${RESET}`);
|
|
560
|
+
console.log(` Method: ${p.method}`);
|
|
561
|
+
console.log(` Model: ${p.model || '?'}`);
|
|
562
|
+
console.log(` Auth: ${keyHint}`);
|
|
563
|
+
console.log(` Added: ${p.configuredAt || '?'}`);
|
|
564
|
+
console.log('');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function cmdTest() {
|
|
569
|
+
const providers = loadProviders();
|
|
570
|
+
if (!Object.keys(providers).length) {
|
|
571
|
+
console.log(`${YELLOW}No providers configured.${RESET} Run: ${CYAN}clawmoat providers setup${RESET}`);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
console.log(`\n${BOLD}Testing AI Provider Connections${RESET}\n`);
|
|
576
|
+
let allOk = true;
|
|
577
|
+
|
|
578
|
+
for (const [id, p] of Object.entries(providers)) {
|
|
579
|
+
const info = PROVIDERS[id] || {};
|
|
580
|
+
process.stdout.write(`${info.emoji || '•'} ${info.name || id}... `);
|
|
581
|
+
|
|
582
|
+
let tester;
|
|
583
|
+
if (id === 'kimi-coding' && p.method === 'moonshot-api-key') {
|
|
584
|
+
tester = TESTERS.moonshot;
|
|
585
|
+
} else {
|
|
586
|
+
tester = TESTERS[id];
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (!tester) {
|
|
590
|
+
console.log(`${YELLOW}? No tester${RESET}`);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const result = await tester(p);
|
|
595
|
+
if (result.ok) {
|
|
596
|
+
console.log(`${GREEN}✓ OK${RESET}${result.note ? ` ${DIM}(${result.note})${RESET}` : ''}`);
|
|
597
|
+
} else {
|
|
598
|
+
console.log(`${RED}✗ ${result.error}${RESET}`);
|
|
599
|
+
allOk = false;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
console.log(allOk ? `\n${GREEN}All providers connected.${RESET}` : `\n${YELLOW}Some providers need attention.${RESET}`);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function cmdOpenClaw() {
|
|
607
|
+
const providers = loadProviders();
|
|
608
|
+
if (!Object.keys(providers).length) {
|
|
609
|
+
console.log(`${YELLOW}No providers configured.${RESET} Run: ${CYAN}clawmoat providers setup${RESET}`);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const snippet = generateOpenClawConfig(providers);
|
|
613
|
+
console.log(JSON.stringify(snippet, null, 2));
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ─── Main (only when run directly) ────────────────────────────────────────────
|
|
617
|
+
|
|
618
|
+
if (require.main === module) {
|
|
619
|
+
const args = process.argv.slice(2);
|
|
620
|
+
const command = args[0] || 'setup';
|
|
621
|
+
|
|
622
|
+
switch (command) {
|
|
623
|
+
case 'setup':
|
|
624
|
+
case '--setup':
|
|
625
|
+
cmdSetup().catch(e => { console.error(e); process.exit(1); });
|
|
626
|
+
break;
|
|
627
|
+
case 'list':
|
|
628
|
+
case '--list':
|
|
629
|
+
cmdList();
|
|
630
|
+
break;
|
|
631
|
+
case 'test':
|
|
632
|
+
case '--test':
|
|
633
|
+
cmdTest().catch(e => { console.error(e); process.exit(1); });
|
|
634
|
+
break;
|
|
635
|
+
case 'openclaw':
|
|
636
|
+
case '--openclaw':
|
|
637
|
+
cmdOpenClaw();
|
|
638
|
+
break;
|
|
639
|
+
default:
|
|
640
|
+
console.log(`Usage: clawmoat providers [setup|list|test|openclaw]`);
|
|
641
|
+
console.log(` setup Interactive provider configuration`);
|
|
642
|
+
console.log(` list Show configured providers`);
|
|
643
|
+
console.log(` test Test all provider connections`);
|
|
644
|
+
console.log(` openclaw Generate OpenClaw config snippet`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Export for use by setup.js
|
|
649
|
+
module.exports = { cmdSetup, cmdList, cmdTest, cmdOpenClaw, generateKimiHeaders, loadProviders, PROVIDERS };
|