@awesomate/hosting-mcp 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +18685 -107
- package/package.json +2 -1
- package/skill/awesomate-hosting/SKILL.md +28 -2
- package/skill/awesomate-hosting/scripts/bootstrap.mjs +245 -113
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awesomate/hosting-mcp",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Awesomate MCP server — lets Claude manage your Awesomate WordPress hosting, plan, limits, and n8n automations",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
27
|
+
"undici": "^6.21.0",
|
|
27
28
|
"zod": "^3.24.1"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
@@ -68,8 +68,34 @@ fallback** — that's deliberate.
|
|
|
68
68
|
- Connecting a second account never disconnects the first; re-pairing an
|
|
69
69
|
account just refreshes its profile.
|
|
70
70
|
- The bootstrap prints a final `AWESOMATE CONNECT: SUCCESS account=<slug>` (or
|
|
71
|
-
`FAILED reason=…`) sentinel — when a connect ran as a
|
|
72
|
-
for that line instead of assuming success from
|
|
71
|
+
`PARTIAL … issues=…` / `FAILED reason=…`) sentinel — when a connect ran as a
|
|
72
|
+
`!` bang command, check for that line instead of assuming success from
|
|
73
|
+
silence. `PARTIAL` means the token/pin are stored but a step (listed in
|
|
74
|
+
`issues=`) needs the printed manual fix.
|
|
75
|
+
|
|
76
|
+
### Sandboxed / ephemeral environments (cloud agent containers)
|
|
77
|
+
|
|
78
|
+
- **Egress proxy**: if `HTTPS_PROXY`/`HTTP_PROXY` is set, the environment
|
|
79
|
+
forces traffic through a proxy that Node's built-in fetch ignores — requests
|
|
80
|
+
bypass it and the gateway answers 403, which looks like a bad code/token but
|
|
81
|
+
is a transport failure. The bootstrap and MCP server handle this themselves
|
|
82
|
+
(undici env-proxy agent); if an older version is in play, prefix commands
|
|
83
|
+
with `NODE_USE_ENV_PROXY=1`. Diagnostic: `curl` works but `node -e "fetch(…)"`
|
|
84
|
+
403s → proxy bypass, not credentials.
|
|
85
|
+
- **Interrupted connect?** Setup codes are re-runnable for their whole
|
|
86
|
+
10-minute life (each run mints a fresh token), and `--pat amt_pat_…` resumes
|
|
87
|
+
with an already-redeemed token — never tell the user their code is spent
|
|
88
|
+
unless the API literally said so.
|
|
89
|
+
- **Nothing persists**: `~/.awesomate`, the skill copy, and the MCP
|
|
90
|
+
registration all vanish with the container. Treat a container connect as a
|
|
91
|
+
diagnosis/test, and tell the user a real workstation is needed for a lasting
|
|
92
|
+
install.
|
|
93
|
+
- **No ssh client**: containers usually lack `ssh-keygen`/`ssh`, so deploy and
|
|
94
|
+
shell are unavailable — reads and management calls still work. The bootstrap
|
|
95
|
+
skips SSH with a note rather than failing.
|
|
96
|
+
- Never probe connectivity with a POST to `/redeem` — it's state-changing. Use
|
|
97
|
+
`GET {apiBase}/api/hosting-access/context` (401 without a token still proves
|
|
98
|
+
reachability).
|
|
73
99
|
|
|
74
100
|
If this skill is loaded but **no `awesomate_*` tools exist in the session at
|
|
75
101
|
all**, the MCP server was registered after Claude Code started (the bootstrap
|
|
@@ -2,27 +2,33 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Awesomate Hosting — one-time bootstrap.
|
|
4
4
|
*
|
|
5
|
-
* Redeems the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* Redeems the setup code from the hub's "Connect Claude Code" prompt for a
|
|
6
|
+
* durable, scoped access token; stores it as a PROFILE (multi-account safe);
|
|
7
|
+
* pins the current folder to that account; and (on Support Plus+) generates an
|
|
8
|
+
* SSH keypair and authorizes the PUBLIC key on the client's own cPanel
|
|
9
|
+
* account. The private key never leaves this machine; the setup code is the
|
|
10
|
+
* only secret that ever crossed the clipboard, and it's short-lived. The
|
|
11
|
+
* access token itself is never printed.
|
|
12
12
|
*
|
|
13
13
|
* Usage (Claude runs this — the user doesn't touch a terminal):
|
|
14
|
-
* node bootstrap.mjs --code amt_bs_XXXX [--account <slug>] [--
|
|
14
|
+
* node bootstrap.mjs --code amt_bs_XXXX [--account <slug>] [--pin-dir <dir>]
|
|
15
|
+
* node bootstrap.mjs --pat amt_pat_XXXX [--account <slug>] # resume: skip redeem
|
|
15
16
|
*
|
|
16
|
-
* --account asserts WHICH account
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* --account asserts WHICH account this connect is for — mismatch aborts before
|
|
18
|
+
* anything is written. --pat resumes a half-finished bootstrap with an
|
|
19
|
+
* already-redeemed token (no fresh code needed). Setup codes are replayable
|
|
20
|
+
* for their whole 10-minute life, so re-running the same command is also safe.
|
|
19
21
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
+
* Proxied environments (cloud agent sandboxes): Node's built-in fetch ignores
|
|
23
|
+
* HTTP(S)_PROXY, so this script routes through undici's EnvHttpProxyAgent when
|
|
24
|
+
* a proxy is configured (falling back to a one-shot re-exec with
|
|
25
|
+
* NODE_USE_ENV_PROXY=1 when undici isn't resolvable).
|
|
26
|
+
*
|
|
27
|
+
* Node 18+ built-ins only, plus undici when running from the npm package.
|
|
22
28
|
*/
|
|
23
29
|
|
|
24
|
-
import { execFileSync } from 'node:child_process';
|
|
25
|
-
import { existsSync, mkdirSync, writeFileSync, chmodSync, readFileSync, cpSync, readdirSync } from 'node:fs';
|
|
30
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
31
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync, readFileSync, cpSync, readdirSync, renameSync, statSync } from 'node:fs';
|
|
26
32
|
import { homedir, hostname } from 'node:os';
|
|
27
33
|
import { join, dirname, resolve } from 'node:path';
|
|
28
34
|
import { fileURLToPath } from 'node:url';
|
|
@@ -33,13 +39,21 @@ function arg(name, fallback) {
|
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
const code = arg('code');
|
|
42
|
+
const resumePat = arg('pat');
|
|
36
43
|
const expectedAccount = arg('account', null);
|
|
44
|
+
const pinDirArg = arg('pin-dir', null);
|
|
37
45
|
const apiBase = (arg('api', 'https://hub.awesomate.ai')).replace(/\/$/, '');
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
|
|
47
|
+
if (!resumePat && (!code || !code.startsWith('amt_bs_'))) {
|
|
48
|
+
console.error('Missing/invalid --code. Copy a fresh setup prompt from hub.awesomate.ai/sites (or resume with --pat if you already hold a token).');
|
|
40
49
|
console.error('AWESOMATE CONNECT: FAILED reason=bad-code');
|
|
41
50
|
process.exit(1);
|
|
42
51
|
}
|
|
52
|
+
if (resumePat && !resumePat.startsWith('amt_pat_')) {
|
|
53
|
+
console.error('--pat does not look like an Awesomate access token (expected amt_pat_ prefix).');
|
|
54
|
+
console.error('AWESOMATE CONNECT: FAILED reason=bad-pat');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
43
57
|
|
|
44
58
|
const dir = join(homedir(), '.awesomate');
|
|
45
59
|
const keysDir = join(dir, 'keys');
|
|
@@ -47,20 +61,81 @@ const credPath = join(dir, 'credentials.json');
|
|
|
47
61
|
const keyPath = join(keysDir, 'id_ed25519');
|
|
48
62
|
const PIN_FILENAME = '.awesomate.json';
|
|
49
63
|
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Proxy-aware fetch. Node's built-in fetch does NOT honor HTTP(S)_PROXY —
|
|
66
|
+
// in sandboxes that force egress through a proxy, requests bypass it and the
|
|
67
|
+
// gateway answers 403, which looks exactly like a credential failure. Route
|
|
68
|
+
// through undici's EnvHttpProxyAgent when a proxy env var is set.
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
const PROXY_URL =
|
|
71
|
+
process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || '';
|
|
72
|
+
let doFetch = fetch;
|
|
73
|
+
|
|
74
|
+
async function initProxySupport() {
|
|
75
|
+
if (!PROXY_URL) return;
|
|
76
|
+
// undici tags EnvHttpProxyAgent experimental and warns on construction —
|
|
77
|
+
// noise in the middle of connect output. Keep every other warning.
|
|
78
|
+
process.removeAllListeners('warning');
|
|
79
|
+
process.on('warning', (w) => {
|
|
80
|
+
if (!/EnvHttpProxyAgent/.test(String(w.message))) console.error(w.stack ?? String(w.message));
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
// Resolves when running from the npm package (undici is a dependency).
|
|
84
|
+
const undici = await import('undici');
|
|
85
|
+
const dispatcher = new undici.EnvHttpProxyAgent();
|
|
86
|
+
doFetch = (url, opts = {}) => undici.fetch(url, { ...opts, dispatcher });
|
|
87
|
+
console.log(`• Egress proxy detected — routing hub requests through ${PROXY_URL}.`);
|
|
88
|
+
} catch {
|
|
89
|
+
// Running from an installed skill copy with no node_modules. Re-exec once
|
|
90
|
+
// with NODE_USE_ENV_PROXY=1 (built-in fetch honors proxies on newer Node;
|
|
91
|
+
// the env var is ignored where unsupported, so this is at worst a no-op).
|
|
92
|
+
if (process.env.AWESOMATE_PROXY_REEXEC !== '1') {
|
|
93
|
+
const r = spawnSync(process.execPath, process.argv.slice(1), {
|
|
94
|
+
stdio: 'inherit',
|
|
95
|
+
env: { ...process.env, NODE_USE_ENV_PROXY: '1', AWESOMATE_PROXY_REEXEC: '1' },
|
|
96
|
+
});
|
|
97
|
+
process.exit(r.status ?? 1);
|
|
98
|
+
}
|
|
99
|
+
console.log(`• Egress proxy detected (${PROXY_URL}) but proxy support is unavailable in this runtime — requests may be blocked by the gateway.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const proxyAdvice = () =>
|
|
104
|
+
PROXY_URL
|
|
105
|
+
? ` An egress proxy is configured (${PROXY_URL}); if this persists, re-run the command prefixed with NODE_USE_ENV_PROXY=1.`
|
|
106
|
+
: '';
|
|
107
|
+
|
|
50
108
|
async function request(method, path, { body, token } = {}) {
|
|
51
109
|
const headers = { 'Content-Type': 'application/json' };
|
|
52
110
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
111
|
+
let res;
|
|
112
|
+
try {
|
|
113
|
+
res = await doFetch(`${apiBase}${path}`, {
|
|
114
|
+
method,
|
|
115
|
+
headers,
|
|
116
|
+
body: method === 'GET' ? undefined : JSON.stringify(body ?? {}),
|
|
117
|
+
});
|
|
118
|
+
} catch (err) {
|
|
119
|
+
const detail = err?.cause?.code ?? err?.code ?? err?.message ?? 'network error';
|
|
120
|
+
throw new Error(`Could not reach ${apiBase} (${detail}). This is a NETWORK problem, not a code/token problem.${proxyAdvice()}`);
|
|
121
|
+
}
|
|
58
122
|
const text = await res.text();
|
|
59
123
|
let json;
|
|
60
124
|
try { json = text ? JSON.parse(text) : null; } catch { json = text; }
|
|
125
|
+
const apiError = json && typeof json === 'object' && typeof json.error === 'string' ? json.error : null;
|
|
61
126
|
if (!res.ok) {
|
|
62
|
-
|
|
63
|
-
|
|
127
|
+
if (apiError) {
|
|
128
|
+
const e = new Error(`${path} failed: ${apiError}`);
|
|
129
|
+
e.isApiError = true;
|
|
130
|
+
e.status = res.status;
|
|
131
|
+
throw e;
|
|
132
|
+
}
|
|
133
|
+
// No structured error body → the response almost certainly came from an
|
|
134
|
+
// egress gateway / firewall, not from the hub. Say so instead of blaming
|
|
135
|
+
// the code — that misdirection is expensive.
|
|
136
|
+
throw new Error(
|
|
137
|
+
`${path} returned HTTP ${res.status} without an API error body — the request likely never reached hub.awesomate.ai (egress proxy or firewall in the way).${proxyAdvice()}`,
|
|
138
|
+
);
|
|
64
139
|
}
|
|
65
140
|
return json;
|
|
66
141
|
}
|
|
@@ -74,6 +149,13 @@ function readJsonFile(path) {
|
|
|
74
149
|
} catch { return null; }
|
|
75
150
|
}
|
|
76
151
|
|
|
152
|
+
/** Atomic JSON write (tmp + rename) so a crash can't leave a torn file. */
|
|
153
|
+
function writeJsonFile(path, value) {
|
|
154
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
155
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
|
|
156
|
+
renameSync(tmp, path);
|
|
157
|
+
}
|
|
158
|
+
|
|
77
159
|
/**
|
|
78
160
|
* Merge one account profile into credentials.json (v2 multi-profile shape).
|
|
79
161
|
* Existing profiles for OTHER accounts are preserved — connecting a second
|
|
@@ -122,25 +204,33 @@ function mergeProfile(profile) {
|
|
|
122
204
|
|
|
123
205
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
124
206
|
chmodSync(dir, 0o700);
|
|
125
|
-
|
|
207
|
+
writeJsonFile(credPath, creds);
|
|
126
208
|
chmodSync(credPath, 0o600);
|
|
127
209
|
return creds;
|
|
128
210
|
}
|
|
129
211
|
|
|
130
|
-
/**
|
|
212
|
+
/**
|
|
213
|
+
* Pin a folder to this account. --pin-dir overrides the implicit cwd (useful
|
|
214
|
+
* when the bootstrap is run from somewhere other than the project root).
|
|
215
|
+
* Never pins $HOME.
|
|
216
|
+
*/
|
|
131
217
|
function writePin(slug) {
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
console.log(`•
|
|
218
|
+
const target = resolve(pinDirArg || process.cwd());
|
|
219
|
+
if (pinDirArg && !(existsSync(target) && statSync(target).isDirectory())) {
|
|
220
|
+
console.log(`• --pin-dir ${pinDirArg} is not an existing directory — skipping the pin. Create it and add ${PIN_FILENAME} with {"account": "${slug}"}.`);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
if (target === resolve(homedir())) {
|
|
224
|
+
console.log(`• Target is the home directory — not writing a ${PIN_FILENAME} pin. Re-run from the project folder (or pass --pin-dir) to pin it.`);
|
|
135
225
|
return null;
|
|
136
226
|
}
|
|
137
|
-
const pinPath = join(
|
|
227
|
+
const pinPath = join(target, PIN_FILENAME);
|
|
138
228
|
const existing = readJsonFile(pinPath);
|
|
139
229
|
if (existing?.account && existing.account !== slug) {
|
|
140
|
-
console.log(`• Re-pinning
|
|
230
|
+
console.log(`• Re-pinning ${target}: ${existing.account} → ${slug}.`);
|
|
141
231
|
}
|
|
142
|
-
|
|
143
|
-
console.log(`✓ Pinned ${
|
|
232
|
+
writeJsonFile(pinPath, { version: 1, account: slug });
|
|
233
|
+
console.log(`✓ Pinned ${target} to account "${slug}" (${PIN_FILENAME}).`);
|
|
144
234
|
return pinPath;
|
|
145
235
|
}
|
|
146
236
|
|
|
@@ -152,12 +242,6 @@ function writePin(slug) {
|
|
|
152
242
|
* failure here never aborts the rest of setup.
|
|
153
243
|
*/
|
|
154
244
|
function installSkill() {
|
|
155
|
-
// bootstrap.mjs lives at <skill>/scripts/bootstrap.mjs → the skill dir is
|
|
156
|
-
// `..` and every sibling of it under the package's skill/ dir is another
|
|
157
|
-
// bundled skill (e.g. awesomate-n8n). Install them all. When bootstrap is
|
|
158
|
-
// re-run from an already-installed copy (~/.claude/skills/...), the parent
|
|
159
|
-
// is the user's whole skills folder — in that case only reinstall this one
|
|
160
|
-
// skill, never its unrelated siblings.
|
|
161
245
|
const hostingSkill = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
162
246
|
const skillRoot = dirname(hostingSkill);
|
|
163
247
|
// Only the package layout (skill/<name>/) has bundled siblings; from an
|
|
@@ -203,100 +287,138 @@ function installSkill() {
|
|
|
203
287
|
return true;
|
|
204
288
|
}
|
|
205
289
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
'claude mcp add --scope user awesomate-hosting -- npx -y -p @awesomate/hosting-mcp awesomate-hosting-mcp';
|
|
290
|
+
const CLEAN_ENTRY = {
|
|
291
|
+
command: 'npx',
|
|
292
|
+
args: ['-y', '-p', '@awesomate/hosting-mcp', 'awesomate-hosting-mcp'],
|
|
293
|
+
};
|
|
294
|
+
const MANUAL_ADD_JSON = JSON.stringify({ mcpServers: { 'awesomate-hosting': CLEAN_ENTRY } }, null, 2);
|
|
212
295
|
|
|
213
296
|
/**
|
|
214
|
-
* Converge on exactly one env-free, user-scope MCP registration
|
|
297
|
+
* Converge on exactly one env-free, user-scope MCP registration by editing the
|
|
298
|
+
* config files DIRECTLY — `claude mcp add` flag syntax has drifted between CLI
|
|
299
|
+
* versions (--scope/-s rejected on some), and a registration is too important
|
|
300
|
+
* to hang off a moving target.
|
|
215
301
|
*
|
|
216
302
|
* Legacy bootstraps baked AWESOMATE_PAT into the registration env — env beats
|
|
217
303
|
* credentials.json inside the server, so those registrations keep serving a
|
|
218
|
-
* STALE account forever after a re-pair. Remove every
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
* scopes can't be removed from here — those are reported for manual cleanup.
|
|
222
|
-
*
|
|
223
|
-
* Throws on failure: a wrong registration is exactly the silent-wrong-account
|
|
224
|
-
* bug this bootstrap exists to prevent.
|
|
304
|
+
* STALE account forever after a re-pair. Remove every entry that carries the
|
|
305
|
+
* env var (user scope, this project's local scope + .mcp.json); entries in
|
|
306
|
+
* OTHER projects are reported for manual cleanup, not touched.
|
|
225
307
|
*/
|
|
226
308
|
function ensureMcpRegistration() {
|
|
227
309
|
const cwd = resolve(process.cwd());
|
|
228
|
-
const
|
|
229
|
-
const warnings = [];
|
|
230
|
-
|
|
231
|
-
const scopesToClean = [];
|
|
232
|
-
if (claudeConfig.mcpServers?.['awesomate-hosting']?.env?.AWESOMATE_PAT) scopesToClean.push('user');
|
|
233
|
-
if (readJsonFile(join(cwd, '.mcp.json'))?.mcpServers?.['awesomate-hosting']?.env?.AWESOMATE_PAT) scopesToClean.push('project');
|
|
234
|
-
for (const [projectDir, project] of Object.entries(claudeConfig.projects ?? {})) {
|
|
235
|
-
if (!project?.mcpServers?.['awesomate-hosting']?.env?.AWESOMATE_PAT) continue;
|
|
236
|
-
if (resolve(projectDir) === cwd) scopesToClean.push('local');
|
|
237
|
-
else warnings.push(`cd ${projectDir} && claude mcp remove awesomate-hosting -s local`);
|
|
238
|
-
}
|
|
310
|
+
const claudeJsonPath = join(homedir(), '.claude.json');
|
|
239
311
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
312
|
+
let cfg = {};
|
|
313
|
+
if (existsSync(claudeJsonPath)) {
|
|
314
|
+
cfg = readJsonFile(claudeJsonPath);
|
|
315
|
+
if (!cfg) {
|
|
316
|
+
throw new Error(`${claudeJsonPath} exists but is not valid JSON — refusing to rewrite it. Fix the file, then add:\n${MANUAL_ADD_JSON}`);
|
|
317
|
+
}
|
|
243
318
|
}
|
|
244
319
|
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
320
|
+
const entry = cfg.mcpServers?.['awesomate-hosting'];
|
|
321
|
+
const entryIsClean =
|
|
322
|
+
entry && !entry.env?.AWESOMATE_PAT && JSON.stringify(entry.args ?? []).includes('awesomate-hosting-mcp');
|
|
323
|
+
|
|
324
|
+
let changed = false;
|
|
325
|
+
if (!entryIsClean) {
|
|
326
|
+
cfg.mcpServers = { ...(cfg.mcpServers ?? {}), 'awesomate-hosting': CLEAN_ENTRY };
|
|
327
|
+
changed = true;
|
|
328
|
+
console.log(
|
|
329
|
+
entry?.env?.AWESOMATE_PAT
|
|
330
|
+
? '✓ Replaced the legacy user-scope registration (had a token baked into its env) with an env-free one.'
|
|
331
|
+
: '✓ Registered the Awesomate Hosting MCP server (user scope, env-free — the folder pin picks the account).',
|
|
332
|
+
);
|
|
251
333
|
} else {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
334
|
+
console.log('✓ MCP server already registered (user scope, env-free) — account selection comes from the folder pin.');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// This project's local-scope entry (lives under projects[cwd] in ~/.claude.json).
|
|
338
|
+
const localEntry = cfg.projects?.[cwd]?.mcpServers?.['awesomate-hosting'];
|
|
339
|
+
if (localEntry?.env?.AWESOMATE_PAT) {
|
|
340
|
+
delete cfg.projects[cwd].mcpServers['awesomate-hosting'];
|
|
341
|
+
changed = true;
|
|
342
|
+
console.log("✓ Removed this project's legacy local-scope registration (token baked into env).");
|
|
343
|
+
}
|
|
344
|
+
// Other projects: report, don't touch.
|
|
345
|
+
for (const [projectDir, project] of Object.entries(cfg.projects ?? {})) {
|
|
346
|
+
if (resolve(projectDir) === cwd) continue;
|
|
347
|
+
if (project?.mcpServers?.['awesomate-hosting']?.env?.AWESOMATE_PAT) {
|
|
348
|
+
console.log(`• ${projectDir} still has a legacy registration with a baked-in token — from that folder run the Connect command again, or remove the awesomate-hosting entry from its local MCP config.`);
|
|
255
349
|
}
|
|
256
|
-
claudeCli(['mcp', 'add', '--scope', 'user', 'awesomate-hosting', '--',
|
|
257
|
-
'npx', '-y', '-p', '@awesomate/hosting-mcp', 'awesomate-hosting-mcp']);
|
|
258
|
-
console.log('✓ Registered the Awesomate Hosting MCP server (user scope, env-free — the folder pin picks the account).');
|
|
259
350
|
}
|
|
260
351
|
|
|
261
|
-
|
|
262
|
-
|
|
352
|
+
if (changed) writeJsonFile(claudeJsonPath, cfg);
|
|
353
|
+
|
|
354
|
+
// Project-scope .mcp.json in cwd.
|
|
355
|
+
const mcpJsonPath = join(cwd, '.mcp.json');
|
|
356
|
+
const mcpJson = readJsonFile(mcpJsonPath);
|
|
357
|
+
if (mcpJson?.mcpServers?.['awesomate-hosting']?.env?.AWESOMATE_PAT) {
|
|
358
|
+
delete mcpJson.mcpServers['awesomate-hosting'];
|
|
359
|
+
writeJsonFile(mcpJsonPath, mcpJson);
|
|
360
|
+
console.log('✓ Removed the legacy project-scope (.mcp.json) registration (token baked into env).');
|
|
263
361
|
}
|
|
264
362
|
}
|
|
265
363
|
|
|
266
364
|
async function main() {
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
365
|
+
await initProxySupport();
|
|
366
|
+
const issues = [];
|
|
367
|
+
|
|
368
|
+
// ── Identity: redeem a code, or verify a resumed PAT ─────────────────────
|
|
369
|
+
let redeemed;
|
|
370
|
+
if (resumePat) {
|
|
371
|
+
console.log(`Resuming with an existing access token — verifying against ${apiBase} …`);
|
|
372
|
+
const ctx = await get('/api/hosting-access/context', { token: resumePat });
|
|
373
|
+
redeemed = {
|
|
374
|
+
pat: resumePat,
|
|
375
|
+
contactId: ctx.contactId,
|
|
376
|
+
slug: ctx.slug ?? null,
|
|
377
|
+
email: ctx.email ?? null,
|
|
378
|
+
plan: ctx.plan,
|
|
379
|
+
shellAccess: Boolean(ctx.capabilities?.shell),
|
|
380
|
+
cpanel: ctx.cpanel ?? null,
|
|
381
|
+
expiresAt: ctx.patExpiresAt ?? null,
|
|
382
|
+
};
|
|
383
|
+
} else {
|
|
384
|
+
console.log(`Redeeming setup code against ${apiBase} …`);
|
|
385
|
+
redeemed = await post('/api/hosting-access/redeem', {
|
|
386
|
+
body: { code, machineLabel: `Claude Code on ${hostname()}` },
|
|
387
|
+
});
|
|
388
|
+
}
|
|
271
389
|
|
|
272
390
|
const slug = redeemed.slug ?? null;
|
|
273
391
|
if (!slug) {
|
|
274
|
-
console.error('The hub did not return an account slug
|
|
392
|
+
console.error('The hub did not return an account slug — cannot safely store a profile. Contact Awesomate support.');
|
|
275
393
|
console.error('AWESOMATE CONNECT: FAILED reason=no-slug');
|
|
276
394
|
process.exit(1);
|
|
277
395
|
}
|
|
278
396
|
// Wrong-account guard: abort BEFORE writing anything.
|
|
279
397
|
if (expectedAccount && expectedAccount !== slug) {
|
|
280
|
-
console.error(`This setup code belongs to account "${slug}" but this connect was for "${expectedAccount}".`);
|
|
398
|
+
console.error(`This ${resumePat ? 'token' : 'setup code'} belongs to account "${slug}" but this connect was for "${expectedAccount}".`);
|
|
281
399
|
console.error(`You are probably logged into the wrong hub account — log into ${expectedAccount}'s hub account and copy a fresh Connect prompt.`);
|
|
282
400
|
console.error(`AWESOMATE CONNECT: FAILED reason=account-mismatch expected=${expectedAccount} got=${slug}`);
|
|
283
401
|
process.exit(1);
|
|
284
402
|
}
|
|
285
403
|
|
|
286
|
-
// Server-side identity check with the NEW token before trusting it
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
context =
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
404
|
+
// Server-side identity check with the NEW token before trusting it (the
|
|
405
|
+
// resume path already did exactly this to build `redeemed`).
|
|
406
|
+
if (!resumePat) {
|
|
407
|
+
let context = null;
|
|
408
|
+
try {
|
|
409
|
+
context = await get('/api/hosting-access/context', { token: redeemed.pat });
|
|
410
|
+
} catch (err) {
|
|
411
|
+
console.error(`Token verification failed (${err.message}) — not storing it. Re-run the same command (setup codes stay valid for 10 minutes).`);
|
|
412
|
+
console.error('AWESOMATE CONNECT: FAILED reason=verify-failed');
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
const ctxSlug = context?.slug ?? null;
|
|
416
|
+
if (ctxSlug && ctxSlug !== slug) {
|
|
417
|
+
console.error(`Identity mismatch: redeem said "${slug}" but the token resolves to "${ctxSlug}". Not storing it — contact Awesomate support.`);
|
|
418
|
+
console.error('AWESOMATE CONNECT: FAILED reason=identity-mismatch');
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
redeemed.email = redeemed.email ?? context?.email ?? null;
|
|
300
422
|
}
|
|
301
423
|
|
|
302
424
|
const profile = {
|
|
@@ -304,7 +426,7 @@ async function main() {
|
|
|
304
426
|
pat: redeemed.pat,
|
|
305
427
|
contactId: redeemed.contactId,
|
|
306
428
|
slug,
|
|
307
|
-
email: redeemed.email ??
|
|
429
|
+
email: redeemed.email ?? null,
|
|
308
430
|
plan: redeemed.plan,
|
|
309
431
|
cpanel: redeemed.cpanel ?? null,
|
|
310
432
|
expiresAt: redeemed.expiresAt ?? null,
|
|
@@ -315,8 +437,8 @@ async function main() {
|
|
|
315
437
|
const pinPath = writePin(slug);
|
|
316
438
|
|
|
317
439
|
// Install the skill + register the MCP server FIRST — these always work and
|
|
318
|
-
// don't depend on hosting being fully provisioned.
|
|
319
|
-
//
|
|
440
|
+
// don't depend on hosting being fully provisioned. Neither is allowed to
|
|
441
|
+
// strand the other or SSH: every step from here is collected, not fatal.
|
|
320
442
|
try { installSkill(); } catch (err) {
|
|
321
443
|
console.log(`• Could not auto-install the skill (${err.message}) — continuing; you can copy skill/ manually.`);
|
|
322
444
|
}
|
|
@@ -324,15 +446,15 @@ async function main() {
|
|
|
324
446
|
try {
|
|
325
447
|
ensureMcpRegistration();
|
|
326
448
|
} catch (err) {
|
|
327
|
-
|
|
328
|
-
console.error(
|
|
329
|
-
console.error(`
|
|
330
|
-
process.exit(1);
|
|
449
|
+
issues.push('mcp-registration');
|
|
450
|
+
console.error(`• MCP registration failed: ${err.message}`);
|
|
451
|
+
console.error(` Add it manually (no token goes in the config — the folder pin picks the account):\n${MANUAL_ADD_JSON}`);
|
|
331
452
|
}
|
|
332
453
|
|
|
333
454
|
// SSH key (Support Plus+): best-effort. If it fails (e.g. hosting not fully
|
|
334
|
-
// provisioned yet
|
|
335
|
-
// ready until it's registered, which
|
|
455
|
+
// provisioned yet, or no ssh tooling in this environment), keep the token +
|
|
456
|
+
// skill + MCP — deploy/shell just aren't ready until it's registered, which
|
|
457
|
+
// the skill can retry later.
|
|
336
458
|
if (redeemed.shellAccess) {
|
|
337
459
|
try {
|
|
338
460
|
if (!existsSync(keyPath)) {
|
|
@@ -349,8 +471,12 @@ async function main() {
|
|
|
349
471
|
mergeProfile(profile);
|
|
350
472
|
console.log(`✓ SSH key authorized on ${reg.sshUser}@${reg.sshHost} (jailed to your account).`);
|
|
351
473
|
} catch (err) {
|
|
352
|
-
|
|
353
|
-
|
|
474
|
+
if (err?.code === 'ENOENT') {
|
|
475
|
+
console.log('• No ssh-keygen in this environment — skipping SSH setup. Deploy and shell need a real workstation; reads and management calls work fine here.');
|
|
476
|
+
} else {
|
|
477
|
+
console.log(`• SSH key not registered yet: ${err.message}`);
|
|
478
|
+
console.log(' Everything else is set up. This usually means hosting isn\'t fully provisioned — contact Awesomate support, then ask me to "retry SSH setup".');
|
|
479
|
+
}
|
|
354
480
|
}
|
|
355
481
|
} else {
|
|
356
482
|
console.log('• Plan has no shell access — read/status mode. Upgrade to Support Plus for SSH + deploy.');
|
|
@@ -360,12 +486,18 @@ async function main() {
|
|
|
360
486
|
console.log(`\n✓ Connected: ${who}${pinPath ? ` — pinned ${dirname(pinPath)}` : ''}`);
|
|
361
487
|
console.log('Setup complete. Next: run awesomate_whoami to confirm the account, then ask me to read your plan and list your sites.');
|
|
362
488
|
console.log('(The skill works in your current Claude session right away via its REST fallback — restart Claude Code when convenient to load the MCP tools.)');
|
|
489
|
+
if (issues.length) {
|
|
490
|
+
console.log(`AWESOMATE CONNECT: PARTIAL account=${slug} pin=${pinPath ?? 'none'} issues=${issues.join(',')}`);
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
363
493
|
console.log(`AWESOMATE CONNECT: SUCCESS account=${slug} pin=${pinPath ?? 'none'}`);
|
|
364
494
|
}
|
|
365
495
|
|
|
366
496
|
main().catch((err) => {
|
|
367
497
|
console.error('Bootstrap failed:', err.message);
|
|
368
|
-
|
|
498
|
+
if (err.isApiError && err.status === 401) {
|
|
499
|
+
console.error('Grab a fresh code from hub.awesomate.ai/sites — codes last 10 minutes (and stay re-runnable within that window).');
|
|
500
|
+
}
|
|
369
501
|
console.error('AWESOMATE CONNECT: FAILED reason=error');
|
|
370
502
|
process.exit(1);
|
|
371
503
|
});
|