scalemax 0.1.25 → 0.1.28
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 +12 -3
- package/dist/assets/claude-github-tools.mjs +319 -0
- package/dist/clients.js +3 -0
- package/dist/commands/claude-tools.d.ts +1 -0
- package/dist/commands/claude-tools.js +16 -0
- package/dist/commands/configure.js +16 -2
- package/dist/commands/helper-bot.d.ts +2 -2
- package/dist/commands/helper-bot.js +60 -9
- package/dist/commands/helper-docs.js +16 -4
- package/dist/configure/claude-tools.d.ts +34 -0
- package/dist/configure/claude-tools.js +283 -0
- package/dist/configure/claude.js +18 -3
- package/dist/configure/codex.js +2 -2
- package/dist/configure/continue.d.ts +5 -0
- package/dist/configure/continue.js +185 -0
- package/dist/configure/hermes.js +4 -4
- package/dist/configure/models.d.ts +7 -0
- package/dist/configure/models.js +19 -1
- package/dist/configure/openclaw.js +45 -12
- package/dist/configure/paths.js +2 -0
- package/dist/configure/roo-code.js +10 -4
- package/dist/configure/templates.js +4 -3
- package/dist/detect/common.js +1 -1
- package/dist/detect/continue.d.ts +2 -0
- package/dist/detect/continue.js +45 -0
- package/dist/index.js +6 -1
- package/dist/types/client.d.ts +2 -2
- package/dist/utils/api.d.ts +2 -1
- package/dist/utils/api.js +27 -3
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -77,6 +77,7 @@ scalemax logout Remove stored ScaleMax API key
|
|
|
77
77
|
scalemax doctor Run diagnostics
|
|
78
78
|
scalemax models List available ScaleMax models
|
|
79
79
|
scalemax helper-bot Open the ScaleMax-only support assistant
|
|
80
|
+
scalemax claude-tools Install bounded GitHub skill tools for Claude
|
|
80
81
|
scalemax update Reconfigure detected clients with the stored key
|
|
81
82
|
scalemax remove Remove ScaleMax-generated client configuration
|
|
82
83
|
scalemax --help Show help
|
|
@@ -121,7 +122,10 @@ npx scalemax@latest helper-bot --model fable-5 "Help me configure ScaleMax in Cu
|
|
|
121
122
|
|
|
122
123
|
Recognized setup, repair, and verification requests execute the CLI's allowlisted client operations directly. Requests such as `setup all providers`, `fix Hermes request failed`, `configure Claude Code`, `verify OpenCode`, and `setup vs code roo extension for claude` are handled by the controller before any model request. The controller detects the relevant client, runs a safe installer when the client has one, verifies existing ScaleMax configuration, repairs ScaleMax-managed files when verification fails, re-verifies, and reports each step.
|
|
123
124
|
|
|
124
|
-
For
|
|
125
|
+
For Claude tool access, run `scalemax claude-tools`, or ask helper-bot to `install GitHub skill tools for Claude Desktop and Claude Code`. Claude Code receives Anthropic's official `example-skills@anthropic-agent-skills` plugin plus public-GitHub read permissions. Claude Desktop/Cowork receives a local MCP server with only three tools: `github_fetch`, `github_tree`, and `github_install_skill`. The MCP server accepts only public GitHub HTTPS hosts, limits downloads, rejects unsafe archive paths and symlinks, refuses to replace an existing skill, and can write only beneath `~/Claude/skills`. It does not expose a general shell, arbitrary URLs, or unrestricted filesystem access. Restart Claude Desktop after installation and use Cowork or Code; a normal Chat tab does not expose local tools.
|
|
126
|
+
Only install skills from repositories you trust: a `SKILL.md` is instruction content that Claude may follow after installation, even though the bridge itself cannot execute arbitrary repository code.
|
|
127
|
+
|
|
128
|
+
For Roo Code, helper-bot can install VS Code on Windows with the fixed Microsoft winget package, install the official Roo Code extension, write OpenAI and Claude ScaleMax profiles when the key exposes those families, and verify the import handoff. For DeepSeek V4 Flash, helper-bot installs and configures Continue's Chat/Edit integration instead because the Azure deployment has no tool calling. For general troubleshooting that cannot be completed by a fixed client action, helper-bot sends a small `gpt-5.4` request grounded with bundled ScaleMax documentation about stored authentication, credit/token base URLs, provider scope, client setup, and common errors. The language model never generates or executes shell commands; only fixed ScaleMax client detection, installation, configuration, verification, and rollback operations are available to the controller.
|
|
125
129
|
|
|
126
130
|
The helper refuses unrelated requests and redacts ScaleMax-style secrets from prompts, provider output, errors, and progress notifications. Use `/status` for non-secret connection details, `/help` for commands, and `/quit` to exit.
|
|
127
131
|
|
|
@@ -132,6 +136,7 @@ The helper refuses unrelated requests and redacts ScaleMax-style secrets from pr
|
|
|
132
136
|
- Codex
|
|
133
137
|
- Cursor
|
|
134
138
|
- Roo Code (VS Code)
|
|
139
|
+
- Continue (VS Code)
|
|
135
140
|
- TRAE SOLO
|
|
136
141
|
- Claude Code
|
|
137
142
|
- OpenCode
|
|
@@ -156,7 +161,7 @@ ScaleMax merges a named `scalemax` provider into `~/.config/opencode/opencode.js
|
|
|
156
161
|
|
|
157
162
|
### OpenClaw
|
|
158
163
|
|
|
159
|
-
ScaleMax partitions OpenClaw models into native `openai-responses` (`scalemax-codex`, `/v1`) and `anthropic-messages` (`scalemax-claude`, origin) providers. The API key and compatibility User-Agent are OpenClaw file-backed SecretRefs stored in private `0600` files under `~/.openclaw/secrets/`; neither value is embedded in the main provider JSON. Migration removes the obsolete mixed provider, preserves the active model family, and scrubs plaintext credentials from generated ScaleMax agent registries even when they contain an older key. Output is capped at 4096 tokens, private-network routing is denied for the public gateway, and unrelated providers and agent settings are preserved.
|
|
164
|
+
ScaleMax partitions OpenClaw models into native `openai-responses` (`scalemax-codex`, `/v1`), OpenAI-compatible `openai-completions` (`scalemax-deepseek`, `/v1`), and `anthropic-messages` (`scalemax-claude`, origin) providers. `deepseek-v4-flash` is isolated in the Chat Completions provider with tools disabled; it is never advertised through Responses. The API key and compatibility User-Agent are OpenClaw file-backed SecretRefs stored in private `0600` files under `~/.openclaw/secrets/`; neither value is embedded in the main provider JSON. Migration removes the obsolete mixed provider, preserves the active model family, and scrubs plaintext credentials from generated ScaleMax agent registries even when they contain an older key. Output is capped at 4096 tokens, private-network routing is denied for the public gateway, and unrelated providers and agent settings are preserved.
|
|
160
165
|
|
|
161
166
|
### Hermes
|
|
162
167
|
|
|
@@ -164,7 +169,11 @@ ScaleMax creates family-specific Hermes providers in `~/.hermes/config.yaml` and
|
|
|
164
169
|
|
|
165
170
|
### Roo Code (VS Code)
|
|
166
171
|
|
|
167
|
-
Roo Code stores provider profiles and secrets in VS Code extension storage, so ScaleMax does not mutate its private database. Instead, the CLI writes a private `0600` Roo settings-import file at `~/.scalemax/roo-code-import.json` and points the supported `roo-cline.autoImportSettingsPath` VS Code setting at it. Reload VS Code once so Roo imports
|
|
172
|
+
Roo Code stores provider profiles and secrets in VS Code extension storage, so ScaleMax does not mutate its private database. Instead, the CLI writes a private `0600` Roo settings-import file at `~/.scalemax/roo-code-import.json` and points the supported `roo-cline.autoImportSettingsPath` VS Code setting at it. Codex and Claude profiles remain separate. DeepSeek V4 Flash is deliberately not offered in Roo because Roo's task-completion loop requires tool calling and this Azure deployment does not expose it. Reload VS Code once so Roo imports supported profiles into its SecretStorage. CLI verification intentionally reports the handoff as verified, not the extension runtime, until that reload occurs.
|
|
173
|
+
|
|
174
|
+
### Continue (VS Code)
|
|
175
|
+
|
|
176
|
+
Continue is the supported VS Code route for `deepseek-v4-flash`. The CLI installs the official `Continue.continue` extension when VS Code is available, merges a Chat/Edit-only OpenAI-compatible model into `~/.continue/config.yaml`, sets `useResponsesApi: false`, and keeps the authenticated ScaleMax key in private `~/.continue/.env` rather than the YAML. Existing Continue models and environment entries are preserved. Reload VS Code, select **ScaleMax DeepSeek V4 Flash**, and use Chat or Edit; Agent mode is unavailable because the deployment does not expose tool calling.
|
|
168
177
|
|
|
169
178
|
### Cursor BYOK routing on macOS
|
|
170
179
|
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Least-privilege GitHub MCP bridge installed by ScaleMax for Claude Desktop.
|
|
5
|
+
*
|
|
6
|
+
* It exposes no general shell, arbitrary URL, or general filesystem tool. It
|
|
7
|
+
* can read public GitHub content and install one repository subdirectory that
|
|
8
|
+
* contains SKILL.md under ~/Claude/skills. Existing skills are never replaced.
|
|
9
|
+
*/
|
|
10
|
+
import { createInterface } from 'node:readline';
|
|
11
|
+
import { promises as fs } from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { execFile } from 'node:child_process';
|
|
15
|
+
import { promisify } from 'node:util';
|
|
16
|
+
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
18
|
+
const MAX_FETCH_BYTES = 512 * 1024;
|
|
19
|
+
const MAX_TREE_BYTES = 2 * 1024 * 1024;
|
|
20
|
+
const MAX_ARCHIVE_BYTES = 32 * 1024 * 1024;
|
|
21
|
+
const MAX_INSTALLED_BYTES = 64 * 1024 * 1024;
|
|
22
|
+
const MAX_INSTALLED_FILES = 2_000;
|
|
23
|
+
const ALLOWED_HOSTS = new Set([
|
|
24
|
+
'github.com',
|
|
25
|
+
'raw.githubusercontent.com',
|
|
26
|
+
'api.github.com',
|
|
27
|
+
'codeload.github.com',
|
|
28
|
+
'objects.githubusercontent.com',
|
|
29
|
+
]);
|
|
30
|
+
const SKILLS_ROOT = path.join(os.homedir(), 'Claude', 'skills');
|
|
31
|
+
|
|
32
|
+
function send(message) {
|
|
33
|
+
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function result(id, value) {
|
|
37
|
+
send({ jsonrpc: '2.0', id, result: value });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function failure(id, code, message) {
|
|
41
|
+
send({ jsonrpc: '2.0', id, error: { code, message } });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isSafeRelative(value) {
|
|
45
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 500 &&
|
|
46
|
+
!path.posix.isAbsolute(value) && !value.split('/').includes('..') &&
|
|
47
|
+
!value.split('/').some(part => !part || part === '.') &&
|
|
48
|
+
!value.includes('\\') && !value.includes('\0');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function validateHost(rawUrl) {
|
|
52
|
+
let url;
|
|
53
|
+
try {
|
|
54
|
+
url = new URL(rawUrl);
|
|
55
|
+
} catch {
|
|
56
|
+
throw new Error('url must be a valid HTTPS URL');
|
|
57
|
+
}
|
|
58
|
+
if (url.protocol !== 'https:' || !ALLOWED_HOSTS.has(url.hostname.toLowerCase())) {
|
|
59
|
+
throw new Error('only public GitHub HTTPS hosts are allowed');
|
|
60
|
+
}
|
|
61
|
+
if (url.username || url.password) throw new Error('credentialed URLs are not allowed');
|
|
62
|
+
for (const key of url.searchParams.keys()) {
|
|
63
|
+
if (/(?:token|key|secret|signature|credential|auth)/i.test(key)) {
|
|
64
|
+
throw new Error('credential-like URL query parameters are not allowed');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return url;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function readLimited(response, limit = MAX_FETCH_BYTES) {
|
|
71
|
+
const length = Number(response.headers.get('content-length') || 0);
|
|
72
|
+
if (length > limit) throw new Error(`response exceeds ${limit} bytes`);
|
|
73
|
+
const reader = response.body?.getReader();
|
|
74
|
+
if (!reader) {
|
|
75
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
76
|
+
if (bytes.byteLength > limit) throw new Error(`response exceeds ${limit} bytes`);
|
|
77
|
+
return bytes;
|
|
78
|
+
}
|
|
79
|
+
const chunks = [];
|
|
80
|
+
let total = 0;
|
|
81
|
+
for (;;) {
|
|
82
|
+
const { done, value } = await reader.read();
|
|
83
|
+
if (done) break;
|
|
84
|
+
total += value.byteLength;
|
|
85
|
+
if (total > limit) {
|
|
86
|
+
await reader.cancel();
|
|
87
|
+
throw new Error(`response exceeds ${limit} bytes`);
|
|
88
|
+
}
|
|
89
|
+
chunks.push(Buffer.from(value));
|
|
90
|
+
}
|
|
91
|
+
return Buffer.concat(chunks);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function fetchBytes(rawUrl, limit = MAX_FETCH_BYTES) {
|
|
95
|
+
const url = validateHost(rawUrl);
|
|
96
|
+
const response = await fetch(url, {
|
|
97
|
+
method: 'GET',
|
|
98
|
+
redirect: 'error',
|
|
99
|
+
headers: { 'user-agent': 'ScaleMax-Claude-GitHub-Tools/1.1' },
|
|
100
|
+
});
|
|
101
|
+
if (!response.ok) throw new Error(`GitHub returned HTTP ${response.status}`);
|
|
102
|
+
return { url: url.toString(), response, bytes: await readLimited(response, limit) };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function repoSlug(repo) {
|
|
106
|
+
if (typeof repo !== 'string' || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo)) {
|
|
107
|
+
throw new Error('repo must be in owner/name form');
|
|
108
|
+
}
|
|
109
|
+
return repo;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function safeRef(ref = 'main') {
|
|
113
|
+
if (typeof ref !== 'string' || !/^[A-Za-z0-9._/-]{1,120}$/.test(ref) || ref.includes('..')) {
|
|
114
|
+
throw new Error('ref contains unsupported characters');
|
|
115
|
+
}
|
|
116
|
+
return ref;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function githubFetch(args) {
|
|
120
|
+
const maxBytes = Number.isInteger(args?.max_bytes)
|
|
121
|
+
? Math.min(Math.max(args.max_bytes, 1), MAX_FETCH_BYTES)
|
|
122
|
+
: MAX_FETCH_BYTES;
|
|
123
|
+
const { url, response, bytes } = await fetchBytes(args?.url, maxBytes);
|
|
124
|
+
const contentType = response.headers.get('content-type') || 'application/octet-stream';
|
|
125
|
+
const text = /(?:text|json|javascript|xml|yaml)/i.test(contentType)
|
|
126
|
+
? bytes.toString('utf8')
|
|
127
|
+
: `[binary response: ${bytes.byteLength} bytes, ${contentType}]`;
|
|
128
|
+
return { url, status: response.status, content_type: contentType, bytes: bytes.byteLength, content: text };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function githubTree(args) {
|
|
132
|
+
const repo = repoSlug(args?.repo);
|
|
133
|
+
const ref = safeRef(args?.ref);
|
|
134
|
+
const encodedRef = ref.split('/').map(encodeURIComponent).join('/');
|
|
135
|
+
const { bytes } = await fetchBytes(`https://api.github.com/repos/${repo}/git/trees/${encodedRef}?recursive=1`, MAX_TREE_BYTES);
|
|
136
|
+
const payload = JSON.parse(bytes.toString('utf8'));
|
|
137
|
+
if (!Array.isArray(payload.tree)) throw new Error('GitHub did not return a repository tree');
|
|
138
|
+
return {
|
|
139
|
+
repo,
|
|
140
|
+
ref,
|
|
141
|
+
truncated: payload.truncated === true,
|
|
142
|
+
paths: payload.tree.filter(entry => entry.type === 'blob').map(entry => entry.path).slice(0, 5_000),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function archiveRoot(entries) {
|
|
147
|
+
const roots = new Set();
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
const normalized = entry.replace(/^\.?\//, '');
|
|
150
|
+
if (normalized.startsWith('/') || normalized.split('/').includes('..') || normalized.includes('\\')) {
|
|
151
|
+
throw new Error('unsafe archive path rejected');
|
|
152
|
+
}
|
|
153
|
+
const root = normalized.split('/')[0];
|
|
154
|
+
if (!root) throw new Error('invalid archive entry');
|
|
155
|
+
roots.add(root);
|
|
156
|
+
}
|
|
157
|
+
if (roots.size !== 1) throw new Error('unexpected archive layout');
|
|
158
|
+
return [...roots][0];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function validateExtractedTree(root) {
|
|
162
|
+
const pending = [root];
|
|
163
|
+
let files = 0;
|
|
164
|
+
let bytes = 0;
|
|
165
|
+
while (pending.length) {
|
|
166
|
+
const current = pending.pop();
|
|
167
|
+
const stat = await fs.lstat(current);
|
|
168
|
+
if (stat.isSymbolicLink()) throw new Error('skill archives may not contain symbolic links');
|
|
169
|
+
if (stat.isDirectory()) {
|
|
170
|
+
for (const name of await fs.readdir(current)) pending.push(path.join(current, name));
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (!stat.isFile()) throw new Error('skill archives may contain only files and directories');
|
|
174
|
+
files += 1;
|
|
175
|
+
bytes += stat.size;
|
|
176
|
+
if (files > MAX_INSTALLED_FILES || bytes > MAX_INSTALLED_BYTES) throw new Error('extracted skill exceeds safety limits');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function tarCommand() {
|
|
181
|
+
const candidates = process.platform === 'win32'
|
|
182
|
+
? [path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'tar.exe')]
|
|
183
|
+
: ['/usr/bin/tar', '/bin/tar'];
|
|
184
|
+
for (const candidate of candidates) {
|
|
185
|
+
try { await fs.access(candidate); return candidate; } catch {}
|
|
186
|
+
}
|
|
187
|
+
throw new Error('a system tar utility is required to install a GitHub skill');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function safeSkillsRoot() {
|
|
191
|
+
await fs.mkdir(SKILLS_ROOT, { recursive: true, mode: 0o700 });
|
|
192
|
+
const resolved = await fs.realpath(SKILLS_ROOT);
|
|
193
|
+
const expected = path.resolve(SKILLS_ROOT);
|
|
194
|
+
const samePath = process.platform === 'win32'
|
|
195
|
+
? resolved.toLowerCase() === expected.toLowerCase()
|
|
196
|
+
: resolved === expected;
|
|
197
|
+
if (!samePath) throw new Error('refusing a symlinked Claude skills directory');
|
|
198
|
+
return resolved;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function githubInstallSkill(args) {
|
|
202
|
+
const repo = repoSlug(args?.repo);
|
|
203
|
+
const ref = safeRef(args?.ref);
|
|
204
|
+
const skillPath = args?.skill_path || '';
|
|
205
|
+
if (!isSafeRelative(skillPath)) throw new Error('skill_path must be a non-empty safe relative path');
|
|
206
|
+
const skillName = path.posix.basename(skillPath);
|
|
207
|
+
if (!/^[A-Za-z0-9._-]{1,80}$/.test(skillName)) throw new Error('skill directory name is not safe');
|
|
208
|
+
const skillsRoot = await safeSkillsRoot();
|
|
209
|
+
const destination = path.join(skillsRoot, skillName);
|
|
210
|
+
try {
|
|
211
|
+
await fs.access(destination);
|
|
212
|
+
throw new Error(`skill already exists at ${destination}; refusing to overwrite`);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (error?.message?.startsWith('skill already exists')) throw error;
|
|
215
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const encodedRef = ref.split('/').map(encodeURIComponent).join('/');
|
|
219
|
+
const { bytes } = await fetchBytes(`https://codeload.github.com/${repo}/tar.gz/${encodedRef}`, MAX_ARCHIVE_BYTES);
|
|
220
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'scalemax-skill-'));
|
|
221
|
+
const archive = path.join(tempRoot, 'repo.tar.gz');
|
|
222
|
+
const extracted = path.join(tempRoot, 'extracted');
|
|
223
|
+
try {
|
|
224
|
+
await fs.writeFile(archive, bytes, { mode: 0o600 });
|
|
225
|
+
await fs.mkdir(extracted, { mode: 0o700 });
|
|
226
|
+
const tar = await tarCommand();
|
|
227
|
+
const verbose = await execFileAsync(tar, ['-tvzf', archive], { maxBuffer: 4 * 1024 * 1024 });
|
|
228
|
+
for (const line of String(verbose.stdout).split('\n')) {
|
|
229
|
+
const marker = line.trimStart()[0];
|
|
230
|
+
if (marker === 'l' || marker === 'h') throw new Error('skill archives may not contain symbolic or hard links');
|
|
231
|
+
}
|
|
232
|
+
const listing = await execFileAsync(tar, ['-tzf', archive], { maxBuffer: 4 * 1024 * 1024 });
|
|
233
|
+
const entries = String(listing.stdout).split('\n').map(value => value.trim()).filter(Boolean);
|
|
234
|
+
const root = archiveRoot(entries);
|
|
235
|
+
const normalizedSkillPath = skillPath.replace(/^\/+|\/+$/g, '');
|
|
236
|
+
const prefix = `${root}/${normalizedSkillPath}`;
|
|
237
|
+
if (!entries.some(entry => entry.replace(/\/$/, '') === `${prefix}/SKILL.md`)) {
|
|
238
|
+
throw new Error('requested repository path does not contain SKILL.md');
|
|
239
|
+
}
|
|
240
|
+
await execFileAsync(tar, ['-xzf', archive, '-C', extracted, prefix], { maxBuffer: 4 * 1024 * 1024 });
|
|
241
|
+
const source = path.join(extracted, root, ...normalizedSkillPath.split('/'));
|
|
242
|
+
await validateExtractedTree(source);
|
|
243
|
+
const skillFile = path.join(source, 'SKILL.md');
|
|
244
|
+
if (!(await fs.lstat(skillFile)).isFile()) throw new Error('SKILL.md is not a regular file');
|
|
245
|
+
try { await fs.rename(source, destination); }
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (error?.code !== 'EXDEV') throw error;
|
|
248
|
+
await fs.cp(source, destination, { recursive: true, force: false, errorOnExist: true });
|
|
249
|
+
await fs.rm(source, { recursive: true, force: true });
|
|
250
|
+
}
|
|
251
|
+
return { repo, ref, skill_path: skillPath, installed_to: destination };
|
|
252
|
+
} finally {
|
|
253
|
+
await fs.rm(tempRoot, { recursive: true, force: true }).catch(() => {});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const tools = [
|
|
258
|
+
{
|
|
259
|
+
name: 'github_fetch',
|
|
260
|
+
description: 'Read public GitHub text or metadata over HTTPS. Only GitHub hosts are permitted; no writes or arbitrary URLs.',
|
|
261
|
+
inputSchema: { type: 'object', properties: { url: { type: 'string' }, max_bytes: { type: 'integer', minimum: 1, maximum: MAX_FETCH_BYTES } }, required: ['url'] },
|
|
262
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: 'github_tree',
|
|
266
|
+
description: 'List public files in a GitHub repository/ref so a skill directory can be selected.',
|
|
267
|
+
inputSchema: { type: 'object', properties: { repo: { type: 'string' }, ref: { type: 'string' } }, required: ['repo'] },
|
|
268
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'github_install_skill',
|
|
272
|
+
description: 'Download one public GitHub skill directory containing SKILL.md into ~/Claude/skills. Refuses overwrite and unsafe paths.',
|
|
273
|
+
inputSchema: { type: 'object', properties: { repo: { type: 'string' }, skill_path: { type: 'string' }, ref: { type: 'string' } }, required: ['repo', 'skill_path'] },
|
|
274
|
+
annotations: { readOnlyHint: false, openWorldHint: false },
|
|
275
|
+
},
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
async function handle(request) {
|
|
279
|
+
const id = request.id;
|
|
280
|
+
switch (request.method) {
|
|
281
|
+
case 'initialize':
|
|
282
|
+
return result(id, {
|
|
283
|
+
protocolVersion: request.params?.protocolVersion || '2024-11-05',
|
|
284
|
+
capabilities: { tools: {} },
|
|
285
|
+
serverInfo: { name: 'scalemax-github-tools', version: '1.1.0' },
|
|
286
|
+
});
|
|
287
|
+
case 'notifications/initialized':
|
|
288
|
+
case 'notifications/cancelled':
|
|
289
|
+
return;
|
|
290
|
+
case 'ping':
|
|
291
|
+
return result(id, {});
|
|
292
|
+
case 'tools/list':
|
|
293
|
+
return result(id, { tools });
|
|
294
|
+
case 'tools/call': {
|
|
295
|
+
try {
|
|
296
|
+
const name = request.params?.name;
|
|
297
|
+
const args = request.params?.arguments || {};
|
|
298
|
+
let value;
|
|
299
|
+
if (name === 'github_fetch') value = await githubFetch(args);
|
|
300
|
+
else if (name === 'github_tree') value = await githubTree(args);
|
|
301
|
+
else if (name === 'github_install_skill') value = await githubInstallSkill(args);
|
|
302
|
+
else throw new Error(`unknown tool: ${name}`);
|
|
303
|
+
return result(id, { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }], structuredContent: value });
|
|
304
|
+
} catch (error) {
|
|
305
|
+
return result(id, { content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }], isError: true });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
default:
|
|
309
|
+
return failure(id, -32601, `method not found: ${request.method}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
314
|
+
input.on('line', line => {
|
|
315
|
+
if (!line.trim()) return;
|
|
316
|
+
let request;
|
|
317
|
+
try { request = JSON.parse(line); } catch { return; }
|
|
318
|
+
Promise.resolve(handle(request)).catch(error => failure(request.id, -32603, error instanceof Error ? error.message : String(error)));
|
|
319
|
+
});
|
package/dist/clients.js
CHANGED
|
@@ -4,6 +4,8 @@ import * as cursorDetect from './detect/cursor.js';
|
|
|
4
4
|
import * as cursorConfig from './configure/cursor.js';
|
|
5
5
|
import * as rooCodeDetect from './detect/roo-code.js';
|
|
6
6
|
import * as rooCodeConfig from './configure/roo-code.js';
|
|
7
|
+
import * as continueDetect from './detect/continue.js';
|
|
8
|
+
import * as continueConfig from './configure/continue.js';
|
|
7
9
|
import * as traeSoloDetect from './detect/trae-solo.js';
|
|
8
10
|
import * as traeSoloConfig from './configure/trae-solo.js';
|
|
9
11
|
import * as claudeDetect from './detect/claude.js';
|
|
@@ -22,6 +24,7 @@ export const clients = [
|
|
|
22
24
|
{ id: 'codex', name: 'Codex', detect: codexDetect.detect, install: codexConfig.install, configure: codexConfig.configure, verify: codexConfig.verify, launch: codexConfig.launch, remove: codexConfig.remove },
|
|
23
25
|
{ id: 'cursor', name: 'Cursor', detect: cursorDetect.detect, configure: cursorConfig.configure, verify: cursorConfig.verify, remove: cursorConfig.remove },
|
|
24
26
|
{ id: 'roo-code', name: 'Roo Code (VS Code)', detect: rooCodeDetect.detect, configure: rooCodeConfig.configure, verify: rooCodeConfig.verify, remove: rooCodeConfig.remove },
|
|
27
|
+
{ id: 'continue', name: 'Continue (VS Code)', detect: continueDetect.detect, install: continueConfig.install, configure: continueConfig.configure, verify: continueConfig.verify, remove: continueConfig.remove },
|
|
25
28
|
{ id: 'trae-solo', name: 'TRAE SOLO', detect: traeSoloDetect.detect, configure: traeSoloConfig.configure, remove: traeSoloConfig.remove },
|
|
26
29
|
{ id: 'claude', name: 'Claude Code', detect: claudeDetect.detect, configure: claudeConfig.configure, verify: claudeConfig.verify, remove: claudeConfig.remove },
|
|
27
30
|
{ id: 'opencode', name: 'OpenCode', detect: opencodeDetect.detect, configure: opencodeConfig.configure, verify: opencodeConfig.verify, remove: opencodeConfig.remove },
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function claudeToolsCommand(home?: string): Promise<void>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { installClaudeGithubTools } from '../configure/claude-tools.js';
|
|
3
|
+
import { homeDir } from '../utils/paths.js';
|
|
4
|
+
export async function claudeToolsCommand(home = homeDir()) {
|
|
5
|
+
console.log(chalk.dim('Installing bounded public-GitHub tools for Claude Desktop/Cowork and Claude Code…'));
|
|
6
|
+
const result = await installClaudeGithubTools('both', home, process.platform, process.env);
|
|
7
|
+
if (result.desktop) {
|
|
8
|
+
console.log(chalk.green(`✓ Claude Desktop MCP verified (${result.desktop.configPaths.length} config${result.desktop.configPaths.length === 1 ? '' : 's'}).`));
|
|
9
|
+
console.log(chalk.dim(` Tools: github_fetch, github_tree, github_install_skill · destination: ~/Claude/skills`));
|
|
10
|
+
}
|
|
11
|
+
if (result.code?.installed)
|
|
12
|
+
console.log(chalk.green(`✓ Claude Code ${result.code.message}.`));
|
|
13
|
+
else if (result.code)
|
|
14
|
+
console.log(chalk.yellow(`! ${result.code.message}`));
|
|
15
|
+
console.log(chalk.dim('No general shell, arbitrary URL, or unrestricted filesystem tool was granted. Restart Claude Desktop before using the new MCP tools.'));
|
|
16
|
+
}
|
|
@@ -6,6 +6,7 @@ import { SCALEMAX_BASE_URL } from '../constants.js';
|
|
|
6
6
|
import { loadApiKey } from '../utils/keyStore.js';
|
|
7
7
|
import { validateApiKey, isAnthropicModel } from '../utils/api.js';
|
|
8
8
|
import { homeDir } from '../utils/paths.js';
|
|
9
|
+
import { isDeepSeekChatModelId } from '../configure/models.js';
|
|
9
10
|
export async function detectAll(home = homeDir()) {
|
|
10
11
|
const ctx = { home, platform: process.platform, env: process.env };
|
|
11
12
|
return Promise.all(clients.map(c => c.detect(ctx)));
|
|
@@ -44,13 +45,26 @@ export function clientAllowedForModels(clientId, models) {
|
|
|
44
45
|
return false;
|
|
45
46
|
// These clients use either a native family-specific adapter or the gateway's
|
|
46
47
|
// normalized OpenAI-compatible Chat Completions route, which supports both
|
|
47
|
-
// authenticated model families. Codex
|
|
48
|
-
|
|
48
|
+
// authenticated model families. Codex remains Responses-only; TRAE uses
|
|
49
|
+
// OpenAI-compatible Chat Completions.
|
|
50
|
+
const crossProtocolClients = new Set(['cursor', 'roo-code', 'opencode', 'openclaw', 'hermes', 'cherry-studio', 'api-code']);
|
|
51
|
+
if (clientId === 'continue')
|
|
52
|
+
return models.some(model => isDeepSeekChatModelId(model.id));
|
|
53
|
+
if (clientId === 'claude')
|
|
54
|
+
return models.some(isAnthropicModel);
|
|
55
|
+
if (clientId === 'codex')
|
|
56
|
+
return models.some(model => !isAnthropicModel(model) && !isDeepSeekChatModelId(model.id));
|
|
57
|
+
if (clientId === 'hermes' && models.every(model => isDeepSeekChatModelId(model.id)))
|
|
58
|
+
return false;
|
|
59
|
+
if (clientId === 'roo-code' && models.every(model => isDeepSeekChatModelId(model.id)))
|
|
60
|
+
return false;
|
|
49
61
|
if (crossProtocolClients.has(clientId))
|
|
50
62
|
return true;
|
|
51
63
|
return models.some(model => !isAnthropicModel(model));
|
|
52
64
|
}
|
|
53
65
|
function scopeHint(scope) {
|
|
66
|
+
if (scope === 'deepseek-v4-flash')
|
|
67
|
+
return 'DeepSeek V4 Flash only';
|
|
54
68
|
return scope === 'both' ? 'OpenAI + Claude' : scope === 'anthropic' ? 'Claude only' : 'OpenAI only';
|
|
55
69
|
}
|
|
56
70
|
export async function configureCommand(opts = {}) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ChatCompletionOptions, type ChatCompletionResult, type ChatMessage, type ModelInfo } from '../utils/api.js';
|
|
2
2
|
import { loadApiKey } from '../utils/keyStore.js';
|
|
3
3
|
import { validateApiKey } from '../utils/api.js';
|
|
4
|
-
import type { ClientDefinition } from '../types/client.js';
|
|
4
|
+
import type { ClientDefinition, ProviderScope } from '../types/client.js';
|
|
5
5
|
export declare const SCALEMAX_HELPER_REFUSAL: string;
|
|
6
6
|
export declare const SCALEMAX_HELPER_SYSTEM_PROMPT: string;
|
|
7
7
|
export declare const SCALEMAX_HELPER_INVALID_PROVIDER_RESPONSE: string;
|
|
@@ -14,7 +14,7 @@ export interface HelperConnection {
|
|
|
14
14
|
baseUrl: string;
|
|
15
15
|
model: string;
|
|
16
16
|
platform?: NodeJS.Platform;
|
|
17
|
-
providerScope?:
|
|
17
|
+
providerScope?: ProviderScope;
|
|
18
18
|
keyMode?: string;
|
|
19
19
|
availableModels?: string[];
|
|
20
20
|
modelCatalog?: ModelInfo[];
|
|
@@ -10,6 +10,7 @@ import { loadApiKey } from '../utils/keyStore.js';
|
|
|
10
10
|
import { homeDir } from '../utils/paths.js';
|
|
11
11
|
import { validateApiKey } from '../utils/api.js';
|
|
12
12
|
import { commandExists } from '../utils/exec.js';
|
|
13
|
+
import { installClaudeGithubTools } from '../configure/claude-tools.js';
|
|
13
14
|
export const SCALEMAX_HELPER_REFUSAL = `I am the official ScaleMax Helper.
|
|
14
15
|
|
|
15
16
|
I can only assist with installing, configuring, repairing, verifying, and troubleshooting ScaleMax.
|
|
@@ -542,6 +543,7 @@ const SCALEMAX_TOPIC_PATTERN = /\b(?:scalemax|api\s*key|stored\s+key|customer\s+
|
|
|
542
543
|
const TROUBLESHOOTING_PATTERN = /\b(?:error|failed?|failure|broken|not\s+working|doesn['’]?t\s+work|cannot|can['’]?t|timeout|invalid|unauthorized|forbidden|help)\b/i;
|
|
543
544
|
const GREETING_PATTERN = /^(?:hi|hello|hey|help|what can you do)[!.?\s]*$/i;
|
|
544
545
|
const CLAUDE_DESKTOP_PATTERN = /\bclaude\s+desktop\b/i;
|
|
546
|
+
const CLAUDE_GITHUB_TOOLS_PATTERN = /\b(?:github|skills?|mcp|tools?|download)\b/i;
|
|
545
547
|
const CLAUDE_DESKTOP_CONFIGURATION_PATTERN = /\b(?:configur(?:e|ation)|setup|set\s*up|gateway|base\s*url|api\s*key|connect(?:ion)?|third[- ]party|inference|field|install|verify)\b/i;
|
|
546
548
|
const CODEX_CLI_PATTERN = /\bcodex(?:\s+cli)?\b/i;
|
|
547
549
|
const CLIENT_CONFIGURATION_PATTERN = /\b(?:configur(?:e|ation)|setup|set\s*up|base\s*url|api\s*key|connect(?:ion)?|provider|install|repair|verify|update)\b/i;
|
|
@@ -758,7 +760,8 @@ For automatic configuration, exit with \`/quit\`, run \`scalemax update\`, then
|
|
|
758
760
|
return reply;
|
|
759
761
|
}
|
|
760
762
|
const HELPER_CLIENT_PATTERNS = [
|
|
761
|
-
{ id: '
|
|
763
|
+
{ id: 'continue', pattern: /\b(?:continue(?:\.dev)?|vscode\s+continue|vs\s+code\s+continue)\b/i },
|
|
764
|
+
{ id: 'roo-code', pattern: /\b(?:roo(?:\s+code)?|roo\s+(?:extension|plugin)|vscode\s+roo|vs\s+code\s+roo)\b/i },
|
|
762
765
|
{ id: 'openclaw', pattern: /\bopen\s*claw\b/i },
|
|
763
766
|
{ id: 'opencode', pattern: /\bopen\s*code\b/i },
|
|
764
767
|
{ id: 'cherry-studio', pattern: /\bcherry\s*studio\b/i },
|
|
@@ -770,6 +773,10 @@ const HELPER_CLIENT_PATTERNS = [
|
|
|
770
773
|
{ id: 'claude', pattern: /\bclaude(?:\s+code|\s+cli)?\b/i }
|
|
771
774
|
];
|
|
772
775
|
function helperActionKind(question) {
|
|
776
|
+
if (CLAUDE_GITHUB_TOOLS_PATTERN.test(question) && /\b(?:cannot|can['’]?t|unable|not\s+(?:able|being\s+able)|doesn['’]?t\s+have|no\s+tools?)\b/i.test(question))
|
|
777
|
+
return 'repair';
|
|
778
|
+
if (CLAUDE_GITHUB_TOOLS_PATTERN.test(question) && /\b(?:enable|grant|provide|add)\b/i.test(question))
|
|
779
|
+
return 'configure';
|
|
773
780
|
if (/\b(?:repair|fix|troubleshoot|diagnose|not\s+working|doesn['’]?t\s+work|broken|failed?|invalid|timeout|rate\s*limit(?:ed)?)\b/i.test(question))
|
|
774
781
|
return 'repair';
|
|
775
782
|
if (HELPER_MUTATION_PATTERN.test(question))
|
|
@@ -779,6 +786,8 @@ function helperActionKind(question) {
|
|
|
779
786
|
return undefined;
|
|
780
787
|
}
|
|
781
788
|
function helperActionTarget(question) {
|
|
789
|
+
if (/\bclaude\b/i.test(question) && CLAUDE_GITHUB_TOOLS_PATTERN.test(question))
|
|
790
|
+
return 'claude-tools';
|
|
782
791
|
if (CLAUDE_DESKTOP_PATTERN.test(question))
|
|
783
792
|
return undefined;
|
|
784
793
|
if (ALL_CLIENTS_PATTERN.test(question) || /\b(?:all|every)\b.{0,30}\bproviders?\b/i.test(question))
|
|
@@ -791,6 +800,8 @@ function helperActionTarget(question) {
|
|
|
791
800
|
return undefined;
|
|
792
801
|
}
|
|
793
802
|
function requestedProviderScope(question, target) {
|
|
803
|
+
if (/\bdeepseek(?:[-\s]+v4[-\s]+flash)?\b/i.test(question))
|
|
804
|
+
return 'deepseek-v4-flash';
|
|
794
805
|
if (/\b(?:both|openai\s*\+\s*claude|claude\s*\+\s*openai)\b/i.test(question))
|
|
795
806
|
return 'both';
|
|
796
807
|
if (/\b(?:claude|anthropic)\b/i.test(question))
|
|
@@ -803,17 +814,32 @@ function actionModels(connection, requestedScope) {
|
|
|
803
814
|
const catalog = connection.modelCatalog?.length
|
|
804
815
|
? connection.modelCatalog
|
|
805
816
|
: (connection.availableModels || []).map(id => ({ id }));
|
|
806
|
-
if (!requestedScope
|
|
817
|
+
if (!requestedScope)
|
|
807
818
|
return catalog;
|
|
808
|
-
|
|
819
|
+
if (requestedScope === 'deepseek-v4-flash') {
|
|
820
|
+
return catalog.filter(model => model.id.trim().toLowerCase() === 'deepseek-v4-flash');
|
|
821
|
+
}
|
|
822
|
+
if (requestedScope === 'both') {
|
|
823
|
+
return catalog.filter(model => model.id.trim().toLowerCase() !== 'deepseek-v4-flash');
|
|
824
|
+
}
|
|
825
|
+
return catalog.filter(model => requestedScope === 'anthropic'
|
|
826
|
+
? isAnthropicModel(model)
|
|
827
|
+
: !isAnthropicModel(model) && model.id.trim().toLowerCase() !== 'deepseek-v4-flash');
|
|
809
828
|
}
|
|
810
829
|
function helperClientAllowedForModels(clientId, models) {
|
|
811
830
|
if (!models.length)
|
|
812
831
|
return false;
|
|
813
|
-
const crossProtocolClients = new Set(['
|
|
832
|
+
const crossProtocolClients = new Set(['cursor', 'roo-code', 'opencode', 'openclaw', 'hermes', 'cherry-studio', 'api-code']);
|
|
833
|
+
const onlyDeepSeek = models.every(model => model.id.trim().toLowerCase() === 'deepseek-v4-flash');
|
|
834
|
+
if (clientId === 'continue')
|
|
835
|
+
return onlyDeepSeek;
|
|
836
|
+
if (clientId === 'claude')
|
|
837
|
+
return models.some(isAnthropicModel);
|
|
838
|
+
if ((clientId === 'codex' || clientId === 'hermes' || clientId === 'roo-code') && onlyDeepSeek)
|
|
839
|
+
return false;
|
|
814
840
|
if (crossProtocolClients.has(clientId))
|
|
815
841
|
return true;
|
|
816
|
-
return models.some(model => !isAnthropicModel(model));
|
|
842
|
+
return models.some(model => !isAnthropicModel(model) && model.id.trim().toLowerCase() !== 'deepseek-v4-flash');
|
|
817
843
|
}
|
|
818
844
|
function hasRooCodeExtension(paths) {
|
|
819
845
|
return paths.some(candidate => /^rooveterinaryinc\.roo-cline-/i.test(path.basename(candidate)));
|
|
@@ -897,9 +923,9 @@ async function installRooCodeExtension(ctx, client, write) {
|
|
|
897
923
|
async function runOneHelperClientAction(kind, client, connection, home, requestedScope, write) {
|
|
898
924
|
const models = actionModels(connection, requestedScope);
|
|
899
925
|
if (!models.length)
|
|
900
|
-
throw new Error(`The authenticated key does not expose ${requestedScope === 'anthropic' ? 'Claude' : requestedScope === 'openai' ? 'OpenAI' : 'compatible'} models for ${client.name}. No configuration was changed.`);
|
|
901
|
-
if (client.id === 'codex' && models.every(isAnthropicModel))
|
|
902
|
-
throw new Error('Codex requires
|
|
926
|
+
throw new Error(`The authenticated key does not expose ${requestedScope === 'anthropic' ? 'Claude' : requestedScope === 'openai' ? 'OpenAI' : requestedScope === 'deepseek-v4-flash' ? 'DeepSeek V4 Flash' : 'compatible'} models for ${client.name}. No configuration was changed.`);
|
|
927
|
+
if (client.id === 'codex' && models.every(model => isAnthropicModel(model) || model.id.trim().toLowerCase() === 'deepseek-v4-flash'))
|
|
928
|
+
throw new Error('Codex requires a Responses-capable model, but this key exposes only Claude or Chat-Completions-only models. No configuration was changed.');
|
|
903
929
|
if (client.id === 'cursor' && connection.platform !== 'darwin')
|
|
904
930
|
throw new Error(`Automatic Cursor configuration is currently verified on macOS only. ${helperPlatformName(connection.platform)} was not modified.`);
|
|
905
931
|
const ctx = {
|
|
@@ -970,7 +996,9 @@ async function runOneHelperClientAction(kind, client, connection, home, requeste
|
|
|
970
996
|
throw new Error(`${client.name} configuration was written but could not be detected afterward.`);
|
|
971
997
|
write(`✓ ${client.name} configuration detected after write.`);
|
|
972
998
|
}
|
|
973
|
-
const activation = client.id === 'roo-code'
|
|
999
|
+
const activation = client.id === 'roo-code'
|
|
1000
|
+
? ' Reload VS Code once so Roo Code imports the verified ScaleMax profile.'
|
|
1001
|
+
: client.id === 'continue' ? ' Reload VS Code once so Continue loads the verified ScaleMax profile.' : '';
|
|
974
1002
|
return `✅ Successfully completed: ${client.name} is configured and locally verified.${activation}`;
|
|
975
1003
|
}
|
|
976
1004
|
/** Execute only narrowly allowlisted ScaleMax client actions; never model-generated shell commands. */
|
|
@@ -982,6 +1010,29 @@ export async function executeHelperAction(question, connection, home, clientCata
|
|
|
982
1010
|
const target = helperActionTarget(cleanQuestion);
|
|
983
1011
|
if (!kind || !target)
|
|
984
1012
|
return undefined;
|
|
1013
|
+
if (target === 'claude-tools') {
|
|
1014
|
+
const toolTarget = CLAUDE_DESKTOP_PATTERN.test(cleanQuestion)
|
|
1015
|
+
? 'desktop'
|
|
1016
|
+
: /\bclaude\s+(?:code|cli|terminal)\b/i.test(cleanQuestion) ? 'code' : 'both';
|
|
1017
|
+
write(`✓ Stored ${helperBillingMode(connection)} key is authenticated; secrets remain hidden.`);
|
|
1018
|
+
write(`→ ${kind === 'verify' ? 'Verifying and reconciling' : 'Installing'} bounded public-GitHub tools for ${toolTarget === 'both' ? 'Claude Desktop/Cowork and Claude Code' : toolTarget === 'desktop' ? 'Claude Desktop/Cowork' : 'Claude Code'}…`);
|
|
1019
|
+
const tools = await installClaudeGithubTools(toolTarget, home, connection.platform || process.platform, process.env);
|
|
1020
|
+
const completed = [];
|
|
1021
|
+
if (tools.desktop) {
|
|
1022
|
+
completed.push(`Claude Desktop MCP exposes github_fetch, github_tree, and github_install_skill; installs are confined to ~/Claude/skills`);
|
|
1023
|
+
write('✓ Claude Desktop GitHub MCP process and tool catalog verified.');
|
|
1024
|
+
}
|
|
1025
|
+
if (tools.code?.installed) {
|
|
1026
|
+
completed.push(`Claude Code ${tools.code.message}`);
|
|
1027
|
+
write(`✓ Claude Code ${tools.code.message}.`);
|
|
1028
|
+
}
|
|
1029
|
+
else if (tools.code) {
|
|
1030
|
+
write(`! ${tools.code.message}`);
|
|
1031
|
+
}
|
|
1032
|
+
if (!completed.length)
|
|
1033
|
+
throw new Error('Claude Desktop and Claude Code were not detected, so no GitHub tools were installed.');
|
|
1034
|
+
return `✅ Successfully completed: ${completed.join('; ')}. Restart Claude Desktop before using the new MCP tools. No unrestricted shell or filesystem access was granted.`;
|
|
1035
|
+
}
|
|
985
1036
|
const scope = requestedProviderScope(cleanQuestion, target);
|
|
986
1037
|
const scopedModels = actionModels(connection, scope);
|
|
987
1038
|
const selected = target === 'all'
|