@awesomate/hosting-mcp 0.7.1 → 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/README.md
CHANGED
|
@@ -14,14 +14,24 @@ which redeems a setup code for a scoped access token (stored at
|
|
|
14
14
|
(Support Plus+), and registers this server. The **`awesomate-hosting` skill**
|
|
15
15
|
(bundled under `skill/`) makes Claude plan-aware and enforces snapshot-before-change.
|
|
16
16
|
|
|
17
|
-
Manual registration
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
Manual registration — add to `~/.claude.json` under `mcpServers` (the
|
|
18
|
+
bootstrap does this automatically; never bake `AWESOMATE_PAT` into the env,
|
|
19
|
+
that pins the registration to one stale account forever):
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
"awesomate-hosting": {
|
|
23
|
+
"command": "npx",
|
|
24
|
+
"args": ["-y", "--package=@awesomate/hosting-mcp", "awesomate-hosting-mcp"]
|
|
25
|
+
}
|
|
21
26
|
```
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
If you use `claude mcp add` instead, keep the `--package=` form — the claude
|
|
29
|
+
CLI pre-scans the whole argv for its own global `-p`/`--print` flag (even
|
|
30
|
+
after `--`), so a bare `-p <pkg>` breaks its option parsing.
|
|
31
|
+
|
|
32
|
+
Account resolution: `AWESOMATE_PAT` env → `AWESOMATE_ACCOUNT` env →
|
|
33
|
+
`.awesomate.json` folder pin → sole/default profile in
|
|
34
|
+
`~/.awesomate/credentials.json` (multi-account since 0.7.0).
|
|
25
35
|
|
|
26
36
|
## Tools (v0.1 — read-only)
|
|
27
37
|
|
package/package.json
CHANGED
|
@@ -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
|
|
@@ -215,11 +242,19 @@ Git skills drive the Git workflow; this skill just deploys the result.
|
|
|
215
242
|
effort) generates an SSH keypair and registers the public key. If the SSH
|
|
216
243
|
step fails (e.g. hosting not fully provisioned), the token + skill + MCP are
|
|
217
244
|
still set up. The Connect-Claude-Code prompt from the hub runs this via
|
|
218
|
-
`npx -
|
|
245
|
+
`npx -y --package=@awesomate/hosting-mcp awesomate-hosting-bootstrap`.
|
|
246
|
+
(Always the `--package=` form — a bare `-p` breaks the claude CLI's argv
|
|
247
|
+
pre-scan if the command is ever wrapped in `claude mcp add`.)
|
|
219
248
|
- `ssh-connect.sh` — open a jailed SSH session (or run one remote command).
|
|
220
249
|
- `wp.sh` — run a WP-CLI command against a live site over that SSH.
|
|
221
250
|
- `deploy.sh` — snapshot-first Studio→live deploy (files, optional DB).
|
|
222
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.
|
|
223
258
|
|
|
224
259
|
All scripts read `~/.awesomate/credentials.json`; none take secrets on the
|
|
225
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
|
|
@@ -287,9 +318,16 @@ function installSkill() {
|
|
|
287
318
|
return true;
|
|
288
319
|
}
|
|
289
320
|
|
|
321
|
+
// --package= (with the =) instead of -p: the claude CLI pre-scans the FULL
|
|
322
|
+
// argv for its own global -p/--print flag — even after the `--` separator —
|
|
323
|
+
// so any `claude mcp add … -- npx -y -p pkg bin` flips it into print mode and
|
|
324
|
+
// it rejects the scope flag as "unknown option" (marlins bug report,
|
|
325
|
+
// 2026-07-28). npx treats both forms identically at runtime; the = form keeps
|
|
326
|
+
// the value glued to the token so these args are safe to copy anywhere.
|
|
327
|
+
// The explicit package must stay: this package ships two bins.
|
|
290
328
|
const CLEAN_ENTRY = {
|
|
291
329
|
command: 'npx',
|
|
292
|
-
args: ['-y', '
|
|
330
|
+
args: ['-y', '--package=@awesomate/hosting-mcp', 'awesomate-hosting-mcp'],
|
|
293
331
|
};
|
|
294
332
|
const MANUAL_ADD_JSON = JSON.stringify({ mcpServers: { 'awesomate-hosting': CLEAN_ENTRY } }, null, 2);
|
|
295
333
|
|
|
@@ -349,7 +387,15 @@ function ensureMcpRegistration() {
|
|
|
349
387
|
}
|
|
350
388
|
}
|
|
351
389
|
|
|
352
|
-
if (changed)
|
|
390
|
+
if (changed) {
|
|
391
|
+
writeJsonFile(claudeJsonPath, cfg);
|
|
392
|
+
// Read back and verify — never trust a write (or a CLI exit code) alone.
|
|
393
|
+
const verify = readJsonFile(claudeJsonPath);
|
|
394
|
+
const written = verify?.mcpServers?.['awesomate-hosting'];
|
|
395
|
+
if (!written || written.command !== CLEAN_ENTRY.command || !JSON.stringify(written.args ?? []).includes('awesomate-hosting-mcp')) {
|
|
396
|
+
throw new Error(`wrote ${claudeJsonPath} but the awesomate-hosting entry did not verify on read-back`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
353
399
|
|
|
354
400
|
// Project-scope .mcp.json in cwd.
|
|
355
401
|
const mcpJsonPath = join(cwd, '.mcp.json');
|
|
@@ -487,6 +533,7 @@ async function main() {
|
|
|
487
533
|
console.log('Setup complete. Next: run awesomate_whoami to confirm the account, then ask me to read your plan and list your sites.');
|
|
488
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.)');
|
|
489
535
|
if (issues.length) {
|
|
536
|
+
console.log(SUPPORT_HINT);
|
|
490
537
|
console.log(`AWESOMATE CONNECT: PARTIAL account=${slug} pin=${pinPath ?? 'none'} issues=${issues.join(',')}`);
|
|
491
538
|
process.exit(1);
|
|
492
539
|
}
|
|
@@ -498,6 +545,7 @@ main().catch((err) => {
|
|
|
498
545
|
if (err.isApiError && err.status === 401) {
|
|
499
546
|
console.error('Grab a fresh code from hub.awesomate.ai/sites — codes last 10 minutes (and stay re-runnable within that window).');
|
|
500
547
|
}
|
|
548
|
+
console.error(SUPPORT_HINT);
|
|
501
549
|
console.error('AWESOMATE CONNECT: FAILED reason=error');
|
|
502
550
|
process.exit(1);
|
|
503
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
|
+
});
|