@awesomate/hosting-mcp 0.7.2 → 0.7.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomate/hosting-mcp",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
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",
@@ -97,6 +97,33 @@ fallback** — that's deliberate.
97
97
  `GET {apiBase}/api/hosting-access/context` (401 without a token still proves
98
98
  reachability).
99
99
 
100
+ ### When you're stuck: generate a support report
101
+
102
+ If a connect or tool failure survives the documented fixes above (wrong
103
+ account, proxy, resume, restart, legacy registrations), don't keep guessing —
104
+ hand Awesomate a diagnostic they can act on:
105
+
106
+ 1. Run `node ~/.claude/skills/awesomate-hosting/scripts/support-report.mjs
107
+ --note "<one line: what the user was doing and what happened>"`.
108
+ It writes a **fully redacted** report to
109
+ `~/.awesomate/support-report-<timestamp>.md` — tokens reduced to
110
+ prefix+last4, no key material, proxy credentials stripped. It includes
111
+ versions, profiles/pin/registration state, live connectivity probes, and
112
+ the last bootstrap log automatically.
113
+ 2. Show the user the file path and the headline findings (the ACTIVE line,
114
+ any ⚠ LEGACY registration flags, and the probe results).
115
+ 3. **Ask the user before submitting.** With their OK, re-run with `--submit`
116
+ — it POSTs the report to Awesomate and returns a reference like
117
+ `ASR-XXXXXXXX`. Tell the user to quote that reference to
118
+ support@awesomate.ai or their Awesomate contact; the report is already
119
+ attached to it server-side. Submission works even when the token is
120
+ broken (that's usually why you're here).
121
+ 4. If `--submit` fails too (fully offline), the user emails the file itself —
122
+ it's safe to send as-is.
123
+
124
+ Never edit the report to add raw tokens, codes, or keys, and never submit
125
+ without the user's explicit go-ahead.
126
+
100
127
  If this skill is loaded but **no `awesomate_*` tools exist in the session at
101
128
  all**, the MCP server was registered after Claude Code started (the bootstrap
102
129
  just ran). Don't investigate settings files or reinstall anything — and don't
@@ -222,6 +249,12 @@ Git skills drive the Git workflow; this skill just deploys the result.
222
249
  - `wp.sh` — run a WP-CLI command against a live site over that SSH.
223
250
  - `deploy.sh` — snapshot-first Studio→live deploy (files, optional DB).
224
251
  - `pull-live.sh` — clone a live site down to a local folder (read-only on live).
252
+ - `resolve-account.mjs` — shared account resolver (pin/env/profile precedence)
253
+ used by the shell scripts and the REST fallback; `--api` emits API/PAT/ACCT,
254
+ `--ssh` emits the ssh block.
255
+ - `support-report.mjs` — redacted diagnostic bundle for Awesomate support
256
+ (see "When you're stuck" above); `--submit` delivers it and returns a
257
+ reference ID.
225
258
 
226
259
  All scripts read `~/.awesomate/credentials.json`; none take secrets on the
227
260
  command line.
@@ -61,6 +61,37 @@ const credPath = join(dir, 'credentials.json');
61
61
  const keyPath = join(keysDir, 'id_ed25519');
62
62
  const PIN_FILENAME = '.awesomate.json';
63
63
 
64
+ // ---------------------------------------------------------------------------
65
+ // Connect log: every run's output (redacted) lands in ~/.awesomate/
66
+ // last-connect.log so a failure is diagnosable AFTER the fact — the
67
+ // support-report script attaches it automatically. Tokens/codes never reach
68
+ // the log: bootstrap output doesn't print them and the argv line is redacted.
69
+ // ---------------------------------------------------------------------------
70
+ const CONNECT_LOG = [`--- awesomate connect ${new Date().toISOString()} ---`];
71
+ function redactSecrets(s) {
72
+ return String(s)
73
+ .replace(/amt_pat_[A-Za-z0-9_-]{8,}/g, (t) => `${t.slice(0, 12)}…${t.slice(-4)}`)
74
+ .replace(/amt_bs_[A-Za-z0-9_-]{4,}/g, 'amt_bs_[redacted]');
75
+ }
76
+ CONNECT_LOG.push(`argv: ${process.argv.slice(2).map(redactSecrets).join(' ')}`);
77
+ for (const method of ['log', 'error']) {
78
+ const original = console[method].bind(console);
79
+ console[method] = (...args) => {
80
+ CONNECT_LOG.push(redactSecrets(args.join(' ')));
81
+ original(...args);
82
+ };
83
+ }
84
+ process.on('exit', (code) => {
85
+ try {
86
+ CONNECT_LOG.push(`exit: ${code}`);
87
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
88
+ writeFileSync(join(dir, 'last-connect.log'), `${CONNECT_LOG.join('\n')}\n`, { mode: 0o600 });
89
+ } catch { /* logging must never block exit */ }
90
+ });
91
+
92
+ const SUPPORT_HINT =
93
+ 'Stuck? Generate a support report: node ~/.claude/skills/awesomate-hosting/scripts/support-report.mjs --note "<what happened>" (add --submit to send it to Awesomate and get a reference ID).';
94
+
64
95
  // ---------------------------------------------------------------------------
65
96
  // Proxy-aware fetch. Node's built-in fetch does NOT honor HTTP(S)_PROXY —
66
97
  // in sandboxes that force egress through a proxy, requests bypass it and the
@@ -502,6 +533,7 @@ async function main() {
502
533
  console.log('Setup complete. Next: run awesomate_whoami to confirm the account, then ask me to read your plan and list your sites.');
503
534
  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.)');
504
535
  if (issues.length) {
536
+ console.log(SUPPORT_HINT);
505
537
  console.log(`AWESOMATE CONNECT: PARTIAL account=${slug} pin=${pinPath ?? 'none'} issues=${issues.join(',')}`);
506
538
  process.exit(1);
507
539
  }
@@ -513,6 +545,7 @@ main().catch((err) => {
513
545
  if (err.isApiError && err.status === 401) {
514
546
  console.error('Grab a fresh code from hub.awesomate.ai/sites — codes last 10 minutes (and stay re-runnable within that window).');
515
547
  }
548
+ console.error(SUPPORT_HINT);
516
549
  console.error('AWESOMATE CONNECT: FAILED reason=error');
517
550
  process.exit(1);
518
551
  });
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Awesomate support report — turn a broken setup into something support can
4
+ * actually act on.
5
+ *
6
+ * Collects a REDACTED diagnostic snapshot of this machine's Awesomate
7
+ * connection state (versions, profiles, pins, MCP registrations, proxy env,
8
+ * live connectivity probes, ssh tooling, the last bootstrap log) and writes
9
+ * it to ~/.awesomate/support-report-<timestamp>.md.
10
+ *
11
+ * node support-report.mjs [--note "what went wrong"] [--category connect|tools|ssh|deploy|n8n|other]
12
+ * node support-report.mjs --submit # also POST it to Awesomate (returns a reference ID)
13
+ *
14
+ * Redaction guarantees (safe to email or submit):
15
+ * - access tokens → first 12 chars + … + last 4 (identifies the row, useless as a credential)
16
+ * - setup codes → amt_bs_[redacted]
17
+ * - ssh → fingerprint + paths only, never key material
18
+ * - proxy URLs → user:pass@ stripped
19
+ *
20
+ * Node 18+ built-ins only.
21
+ */
22
+
23
+ import { execFileSync } from 'node:child_process';
24
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
25
+ import { homedir, hostname, platform, release, arch } from 'node:os';
26
+ import { join, dirname, resolve } from 'node:path';
27
+
28
+ function arg(name, fallback) {
29
+ const i = process.argv.indexOf(`--${name}`);
30
+ return i >= 0 && process.argv[i + 1] && !process.argv[i + 1].startsWith('--') ? process.argv[i + 1] : fallback;
31
+ }
32
+ const SUBMIT = process.argv.includes('--submit');
33
+ const NOTE = arg('note', null);
34
+ const CATEGORY = arg('category', 'connect');
35
+
36
+ const HOME = homedir();
37
+ const AWM_DIR = join(HOME, '.awesomate');
38
+ const CRED_PATH = join(AWM_DIR, 'credentials.json');
39
+ const PIN_FILENAME = '.awesomate.json';
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Redaction — applied to individual values AND as a final pass over the whole
43
+ // document, so nothing secret can leak through a path we forgot.
44
+ // ---------------------------------------------------------------------------
45
+ function redact(s) {
46
+ return String(s)
47
+ .replace(/amt_pat_[A-Za-z0-9_-]{8,}/g, (t) => `${t.slice(0, 12)}…${t.slice(-4)}`)
48
+ .replace(/amt_bs_[A-Za-z0-9_-]{4,}/g, 'amt_bs_[redacted]')
49
+ .replace(/(https?:\/\/)[^/@\s]+@/g, '$1[credentials-redacted]@')
50
+ .replace(/-----BEGIN[\s\S]*?-----END [A-Z ]*KEY-----/g, '[key-material-redacted]');
51
+ }
52
+
53
+ function readJson(path) {
54
+ try {
55
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
56
+ return parsed && typeof parsed === 'object' ? parsed : null;
57
+ } catch { return null; }
58
+ }
59
+
60
+ function tryExec(cmd, args) {
61
+ try {
62
+ return execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 8000 }).trim();
63
+ } catch { return null; }
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Account resolution — same precedence as the MCP server / resolve-account.mjs
68
+ // (duplicated minimally here so the report works even when those are broken).
69
+ // ---------------------------------------------------------------------------
70
+ function findPin() {
71
+ const home = resolve(HOME);
72
+ let dir = resolve(process.cwd());
73
+ for (;;) {
74
+ const candidate = join(dir, PIN_FILENAME);
75
+ if (existsSync(candidate)) return candidate;
76
+ if (dir === home) return null;
77
+ const parent = dirname(dir);
78
+ if (parent === dir) return null;
79
+ dir = parent;
80
+ }
81
+ }
82
+
83
+ function profileSummary(key, p) {
84
+ return {
85
+ profile: key,
86
+ slug: p.slug ?? null,
87
+ contactId: p.contactId ?? null,
88
+ email: p.email ?? null,
89
+ plan: p.plan ?? null,
90
+ apiBase: p.apiBase ?? null,
91
+ tokenRedacted: p.pat ? redact(p.pat) : null,
92
+ tokenExpiresAt: p.expiresAt ?? null,
93
+ ssh: p.ssh ? { host: p.ssh.host, user: p.ssh.user, port: p.ssh.port ?? 22, fingerprint: p.ssh.fingerprint ?? null, keyPath: p.ssh.keyPath ?? null } : null,
94
+ };
95
+ }
96
+
97
+ async function main() {
98
+ const lines = [];
99
+ const push = (s = '') => lines.push(s);
100
+
101
+ // ── Environment ──────────────────────────────────────────────────────────
102
+ push('# Awesomate support report');
103
+ push('');
104
+ push(`- generated: ${new Date().toISOString()}`);
105
+ push(`- host: ${hostname()} (${platform()} ${release()}, ${arch()})`);
106
+ push(`- node: ${process.version}`);
107
+ push(`- claude CLI: ${tryExec('claude', ['--version']) ?? 'not found on PATH'}`);
108
+ const installedVersion = (() => {
109
+ try { return readFileSync(join(HOME, '.claude', 'skills', 'awesomate-hosting', '.installed-version'), 'utf8').trim(); } catch { return null; }
110
+ })();
111
+ push(`- installed skill version: ${installedVersion ?? 'no marker (pre-0.6 install?)'}`);
112
+ push(`- cwd: ${process.cwd()}`);
113
+ if (NOTE) { push(''); push(`## What the user reports`); push(''); push(NOTE); }
114
+
115
+ // ── Proxy environment ────────────────────────────────────────────────────
116
+ push(''); push('## Proxy environment'); push('');
117
+ const proxyVars = ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'NO_PROXY', 'no_proxy', 'NODE_USE_ENV_PROXY'];
118
+ const setVars = proxyVars.filter((v) => process.env[v]);
119
+ if (setVars.length === 0) push('- none set');
120
+ for (const v of setVars) push(`- ${v}=${redact(process.env[v])}`);
121
+
122
+ // ── Credentials + account resolution ─────────────────────────────────────
123
+ push(''); push('## Credentials & account resolution'); push('');
124
+ const creds = readJson(CRED_PATH);
125
+ let profiles = {};
126
+ let activeApiBase = 'https://hub.awesomate.ai';
127
+ let activePat = null;
128
+ let activeSlug = null;
129
+ if (!creds) {
130
+ push(`- ${CRED_PATH}: ${existsSync(CRED_PATH) ? 'EXISTS BUT UNPARSEABLE' : 'missing (never connected on this machine?)'}`);
131
+ } else {
132
+ if (creds.profiles && typeof creds.profiles === 'object') profiles = creds.profiles;
133
+ else if (creds.pat) profiles = { [creds.slug || 'default']: creds };
134
+ push(`- credentials file: v${creds.version ?? 1}, defaultProfile: ${creds.defaultProfile ?? '(none)'}`);
135
+ for (const [key, p] of Object.entries(profiles)) {
136
+ push(`- profile ${JSON.stringify(profileSummary(key, p))}`);
137
+ }
138
+ }
139
+ const pinPath = findPin();
140
+ const pin = pinPath ? readJson(pinPath) : null;
141
+ push(`- folder pin: ${pinPath ? `${pinPath} → ${JSON.stringify(pin)}` : 'none found (walk-up from cwd)'}`);
142
+ const names = Object.keys(profiles);
143
+ if (process.env.AWESOMATE_PAT) { activePat = process.env.AWESOMATE_PAT; activeSlug = '(env PAT)'; push('- ACTIVE: env AWESOMATE_PAT override'); }
144
+ else if (process.env.AWESOMATE_ACCOUNT && profiles[process.env.AWESOMATE_ACCOUNT]) { const p = profiles[process.env.AWESOMATE_ACCOUNT]; activePat = p.pat; activeSlug = p.slug; activeApiBase = p.apiBase ?? activeApiBase; push(`- ACTIVE: env AWESOMATE_ACCOUNT=${process.env.AWESOMATE_ACCOUNT}`); }
145
+ else if (pin?.account && profiles[pin.account]) { const p = profiles[pin.account]; activePat = p.pat; activeSlug = p.slug; activeApiBase = p.apiBase ?? activeApiBase; push(`- ACTIVE: pin → ${pin.account}`); }
146
+ else if (pin?.account && !profiles[pin.account]) { push(`- ACTIVE: NONE — pin names "${pin.account}" but no such profile exists (this is a hard error by design)`); }
147
+ else if (names.length === 1) { const p = profiles[names[0]]; activePat = p.pat; activeSlug = p.slug; activeApiBase = p.apiBase ?? activeApiBase; push(`- ACTIVE: sole profile ${names[0]}`); }
148
+ else if (creds?.defaultProfile && profiles[creds.defaultProfile]) { const p = profiles[creds.defaultProfile]; activePat = p.pat; activeSlug = p.slug; activeApiBase = p.apiBase ?? activeApiBase; push(`- ACTIVE: defaultProfile ${creds.defaultProfile}`); }
149
+ else { push(`- ACTIVE: NONE (${names.length} profiles, no pin/default)`); }
150
+
151
+ // ── MCP registrations ────────────────────────────────────────────────────
152
+ push(''); push('## MCP registrations'); push('');
153
+ const claudeJson = readJson(join(HOME, '.claude.json'));
154
+ const userEntry = claudeJson?.mcpServers?.['awesomate-hosting'];
155
+ push(`- user scope (~/.claude.json): ${userEntry ? JSON.stringify({ command: userEntry.command, args: userEntry.args, envKeys: Object.keys(userEntry.env ?? {}) }) : 'none'}`);
156
+ if (userEntry?.env?.AWESOMATE_PAT) push(' ⚠ LEGACY: token baked into env — this pins the registration to one stale account. Re-run Connect to fix.');
157
+ for (const [projectDir, project] of Object.entries(claudeJson?.projects ?? {})) {
158
+ const e = project?.mcpServers?.['awesomate-hosting'];
159
+ if (e) push(`- local scope (${projectDir}): envKeys=${JSON.stringify(Object.keys(e.env ?? {}))}${e.env?.AWESOMATE_PAT ? ' ⚠ LEGACY env token' : ''}`);
160
+ }
161
+ const mcpJson = readJson(join(process.cwd(), '.mcp.json'));
162
+ const projEntry = mcpJson?.mcpServers?.['awesomate-hosting'];
163
+ if (projEntry) push(`- project scope (cwd/.mcp.json): envKeys=${JSON.stringify(Object.keys(projEntry.env ?? {}))}${projEntry.env?.AWESOMATE_PAT ? ' ⚠ LEGACY env token' : ''}`);
164
+
165
+ // ── Connectivity probes (proxy-aware) ────────────────────────────────────
166
+ push(''); push('## Connectivity probes'); push('');
167
+ let doFetch = fetch;
168
+ if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy) {
169
+ try {
170
+ const undici = await import('undici');
171
+ const dispatcher = new undici.EnvHttpProxyAgent();
172
+ doFetch = (url, opts = {}) => undici.fetch(url, { ...opts, dispatcher });
173
+ push('- probe transport: undici EnvHttpProxyAgent (proxy honored)');
174
+ } catch {
175
+ push('- probe transport: built-in fetch (undici unavailable — proxy NOT honored unless NODE_USE_ENV_PROXY=1)');
176
+ }
177
+ } else {
178
+ push('- probe transport: built-in fetch (no proxy configured)');
179
+ }
180
+ async function probe(label, url, headers) {
181
+ try {
182
+ const res = await doFetch(url, { headers, signal: AbortSignal.timeout(10_000) });
183
+ const body = await res.text();
184
+ const hasApiBody = (() => { try { const j = JSON.parse(body); return j && typeof j === 'object'; } catch { return false; } })();
185
+ push(`- ${label}: HTTP ${res.status} (${hasApiBody ? 'API response body' : 'NON-API body — likely a gateway/proxy in the way'})`);
186
+ } catch (err) {
187
+ push(`- ${label}: FAILED (${err?.cause?.code ?? err?.name ?? err?.message})`);
188
+ }
189
+ }
190
+ await probe('GET /api/hosting-access/context (no auth — 401 proves reachability)', `${activeApiBase}/api/hosting-access/context`);
191
+ if (activePat) {
192
+ await probe(`GET /api/hosting-access/context (as ${activeSlug})`, `${activeApiBase}/api/hosting-access/context`, { Authorization: `Bearer ${activePat}` });
193
+ } else {
194
+ push('- authenticated probe skipped: no active profile resolved');
195
+ }
196
+
197
+ // ── SSH tooling ──────────────────────────────────────────────────────────
198
+ push(''); push('## SSH tooling'); push('');
199
+ push(`- ssh: ${tryExec('ssh', ['-V']) ?? tryExec('sh', ['-c', 'ssh -V 2>&1']) ?? 'not found'}`);
200
+ push(`- ssh-keygen: ${tryExec('sh', ['-c', 'command -v ssh-keygen']) ?? 'not found'}`);
201
+ const keyPath = join(AWM_DIR, 'keys', 'id_ed25519');
202
+ push(`- key file: ${existsSync(keyPath) ? `present — ${tryExec('ssh-keygen', ['-lf', keyPath]) ?? 'fingerprint unavailable'}` : 'absent'}`);
203
+
204
+ // ── Last bootstrap log ───────────────────────────────────────────────────
205
+ push(''); push('## Last connect attempt (~/.awesomate/last-connect.log)'); push('');
206
+ const logPath = join(AWM_DIR, 'last-connect.log');
207
+ if (existsSync(logPath)) {
208
+ push('```');
209
+ push(redact(readFileSync(logPath, 'utf8')).trim().split('\n').slice(-60).join('\n'));
210
+ push('```');
211
+ } else {
212
+ push('- no log (bootstrap predates 0.7.3, or never ran on this machine)');
213
+ }
214
+
215
+ // ── Write + optionally submit ────────────────────────────────────────────
216
+ const report = redact(lines.join('\n')) + '\n';
217
+ mkdirSync(AWM_DIR, { recursive: true, mode: 0o700 });
218
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
219
+ const outPath = join(AWM_DIR, `support-report-${stamp}.md`);
220
+ writeFileSync(outPath, report);
221
+ chmodSync(outPath, 0o600);
222
+ console.log(`✓ Support report written to ${outPath} (all tokens redacted — safe to send).`);
223
+
224
+ if (!SUBMIT) {
225
+ console.log('Send it to support@awesomate.ai, or re-run with --submit to deliver it directly and get a reference ID.');
226
+ return;
227
+ }
228
+
229
+ const submitBody = {
230
+ report,
231
+ summary: NOTE ? NOTE.slice(0, 255) : `Diagnostic from ${activeSlug ?? 'unconnected machine'} (${hostname()})`,
232
+ slug: activeSlug && activeSlug !== '(env PAT)' ? activeSlug : null,
233
+ email: null,
234
+ category: CATEGORY,
235
+ clientVersion: installedVersion,
236
+ };
237
+ try {
238
+ const res = await doFetch(`${activeApiBase}/api/hosting-access/support-report`, {
239
+ method: 'POST',
240
+ headers: { 'Content-Type': 'application/json', ...(activePat ? { Authorization: `Bearer ${activePat}` } : {}) },
241
+ body: JSON.stringify(submitBody),
242
+ signal: AbortSignal.timeout(15_000),
243
+ });
244
+ const json = await res.json().catch(() => null);
245
+ if (res.ok && json?.reference) {
246
+ console.log(`✓ Submitted to Awesomate support. Reference: ${json.reference}`);
247
+ console.log(` ${json.next}`);
248
+ } else {
249
+ console.error(`Submit failed (HTTP ${res.status}${json?.error ? `: ${json.error}` : ''}) — email the file above to support@awesomate.ai instead.`);
250
+ process.exitCode = 1;
251
+ }
252
+ } catch (err) {
253
+ console.error(`Submit failed (${err?.cause?.code ?? err?.message}) — email the file above to support@awesomate.ai instead.`);
254
+ process.exitCode = 1;
255
+ }
256
+ }
257
+
258
+ main().catch((err) => {
259
+ console.error('support-report failed:', err?.stack ?? err?.message ?? String(err));
260
+ process.exit(1);
261
+ });