@awesomate/hosting-mcp 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +221 -53
- package/package.json +3 -2
- package/skill/awesomate-hosting/SKILL.md +37 -5
- package/skill/awesomate-hosting/scripts/bootstrap.mjs +204 -29
- package/skill/awesomate-hosting/scripts/deploy.sh +8 -9
- package/skill/awesomate-hosting/scripts/pull-live.sh +4 -6
- package/skill/awesomate-hosting/scripts/resolve-account.mjs +100 -0
- package/skill/awesomate-hosting/scripts/ssh-connect.sh +7 -7
- package/skill/awesomate-n8n/SKILL.md +12 -3
package/dist/index.js
CHANGED
|
@@ -2980,7 +2980,7 @@ var require_compile = __commonJS({
|
|
|
2980
2980
|
const schOrFunc = root.refs[ref];
|
|
2981
2981
|
if (schOrFunc)
|
|
2982
2982
|
return schOrFunc;
|
|
2983
|
-
let _sch =
|
|
2983
|
+
let _sch = resolve2.call(this, root, ref);
|
|
2984
2984
|
if (_sch === void 0) {
|
|
2985
2985
|
const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
|
|
2986
2986
|
const { schemaId } = this.opts;
|
|
@@ -3007,7 +3007,7 @@ var require_compile = __commonJS({
|
|
|
3007
3007
|
function sameSchemaEnv(s1, s2) {
|
|
3008
3008
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
3009
3009
|
}
|
|
3010
|
-
function
|
|
3010
|
+
function resolve2(root, ref) {
|
|
3011
3011
|
let sch;
|
|
3012
3012
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
3013
3013
|
ref = sch;
|
|
@@ -3638,7 +3638,7 @@ var require_fast_uri = __commonJS({
|
|
|
3638
3638
|
}
|
|
3639
3639
|
return uri;
|
|
3640
3640
|
}
|
|
3641
|
-
function
|
|
3641
|
+
function resolve2(baseURI, relativeURI, options) {
|
|
3642
3642
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
3643
3643
|
const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
3644
3644
|
schemelessOptions.skipEscape = true;
|
|
@@ -3902,7 +3902,7 @@ var require_fast_uri = __commonJS({
|
|
|
3902
3902
|
var fastUri = {
|
|
3903
3903
|
SCHEMES,
|
|
3904
3904
|
normalize,
|
|
3905
|
-
resolve,
|
|
3905
|
+
resolve: resolve2,
|
|
3906
3906
|
resolveComponent,
|
|
3907
3907
|
equal,
|
|
3908
3908
|
serialize,
|
|
@@ -18985,7 +18985,7 @@ var Protocol = class {
|
|
|
18985
18985
|
return;
|
|
18986
18986
|
}
|
|
18987
18987
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
|
|
18988
|
-
await new Promise((
|
|
18988
|
+
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
18989
18989
|
options?.signal?.throwIfAborted();
|
|
18990
18990
|
}
|
|
18991
18991
|
} catch (error2) {
|
|
@@ -19002,7 +19002,7 @@ var Protocol = class {
|
|
|
19002
19002
|
*/
|
|
19003
19003
|
request(request, resultSchema, options) {
|
|
19004
19004
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
19005
|
-
return new Promise((
|
|
19005
|
+
return new Promise((resolve2, reject) => {
|
|
19006
19006
|
const earlyReject = (error2) => {
|
|
19007
19007
|
reject(error2);
|
|
19008
19008
|
};
|
|
@@ -19080,7 +19080,7 @@ var Protocol = class {
|
|
|
19080
19080
|
if (!parseResult.success) {
|
|
19081
19081
|
reject(parseResult.error);
|
|
19082
19082
|
} else {
|
|
19083
|
-
|
|
19083
|
+
resolve2(parseResult.data);
|
|
19084
19084
|
}
|
|
19085
19085
|
} catch (error2) {
|
|
19086
19086
|
reject(error2);
|
|
@@ -19341,12 +19341,12 @@ var Protocol = class {
|
|
|
19341
19341
|
}
|
|
19342
19342
|
} catch {
|
|
19343
19343
|
}
|
|
19344
|
-
return new Promise((
|
|
19344
|
+
return new Promise((resolve2, reject) => {
|
|
19345
19345
|
if (signal.aborted) {
|
|
19346
19346
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
19347
19347
|
return;
|
|
19348
19348
|
}
|
|
19349
|
-
const timeoutId = setTimeout(
|
|
19349
|
+
const timeoutId = setTimeout(resolve2, interval);
|
|
19350
19350
|
signal.addEventListener("abort", () => {
|
|
19351
19351
|
clearTimeout(timeoutId);
|
|
19352
19352
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -20446,7 +20446,7 @@ var McpServer = class {
|
|
|
20446
20446
|
let task = createTaskResult.task;
|
|
20447
20447
|
const pollInterval = task.pollInterval ?? 5e3;
|
|
20448
20448
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
20449
|
-
await new Promise((
|
|
20449
|
+
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
20450
20450
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
20451
20451
|
if (!updatedTask) {
|
|
20452
20452
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -21095,12 +21095,12 @@ var StdioServerTransport = class {
|
|
|
21095
21095
|
this.onclose?.();
|
|
21096
21096
|
}
|
|
21097
21097
|
send(message) {
|
|
21098
|
-
return new Promise((
|
|
21098
|
+
return new Promise((resolve2) => {
|
|
21099
21099
|
const json = serializeMessage(message);
|
|
21100
21100
|
if (this._stdout.write(json)) {
|
|
21101
|
-
|
|
21101
|
+
resolve2();
|
|
21102
21102
|
} else {
|
|
21103
|
-
this._stdout.once("drain",
|
|
21103
|
+
this._stdout.once("drain", resolve2);
|
|
21104
21104
|
}
|
|
21105
21105
|
});
|
|
21106
21106
|
}
|
|
@@ -21109,43 +21109,168 @@ var StdioServerTransport = class {
|
|
|
21109
21109
|
// src/index.ts
|
|
21110
21110
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
21111
21111
|
import { homedir as homedir2 } from "node:os";
|
|
21112
|
-
import { join as join2, dirname } from "node:path";
|
|
21112
|
+
import { join as join2, dirname as dirname2 } from "node:path";
|
|
21113
21113
|
import { fileURLToPath } from "node:url";
|
|
21114
21114
|
|
|
21115
21115
|
// src/config.ts
|
|
21116
|
-
import { readFileSync } from "node:fs";
|
|
21116
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
21117
21117
|
import { homedir } from "node:os";
|
|
21118
|
-
import { join } from "node:path";
|
|
21118
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
21119
21119
|
var DEFAULT_API_BASE = "https://hub.awesomate.ai";
|
|
21120
|
-
|
|
21121
|
-
|
|
21122
|
-
|
|
21120
|
+
var PIN_FILENAME = ".awesomate.json";
|
|
21121
|
+
function credentialsPath() {
|
|
21122
|
+
return join(homedir(), ".awesomate", "credentials.json");
|
|
21123
|
+
}
|
|
21124
|
+
function readJsonObject(path, label) {
|
|
21125
|
+
let raw;
|
|
21126
|
+
try {
|
|
21127
|
+
raw = readFileSync(path, "utf8");
|
|
21128
|
+
} catch (err) {
|
|
21129
|
+
if (err?.code !== "ENOENT") {
|
|
21130
|
+
console.error(`[awesomate-hosting-mcp] could not read ${label} at ${path}: ${err.message}`);
|
|
21131
|
+
}
|
|
21132
|
+
return null;
|
|
21133
|
+
}
|
|
21123
21134
|
try {
|
|
21124
|
-
const raw = readFileSync(credPath, "utf8");
|
|
21125
21135
|
const parsed = JSON.parse(raw);
|
|
21126
|
-
if (parsed && typeof parsed === "object") {
|
|
21127
|
-
|
|
21128
|
-
} else {
|
|
21129
|
-
console.error(`[awesomate-hosting-mcp] ${credPath} is not a JSON object \u2014 ignoring it.`);
|
|
21136
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
21137
|
+
return parsed;
|
|
21130
21138
|
}
|
|
21139
|
+
console.error(`[awesomate-hosting-mcp] ${path} is not a JSON object \u2014 ignoring it.`);
|
|
21131
21140
|
} catch (err) {
|
|
21132
|
-
|
|
21133
|
-
|
|
21134
|
-
|
|
21141
|
+
console.error(`[awesomate-hosting-mcp] ${path} is not valid JSON (${err.message}) \u2014 ignoring it.`);
|
|
21142
|
+
}
|
|
21143
|
+
return null;
|
|
21144
|
+
}
|
|
21145
|
+
function readProfiles(credPath = credentialsPath()) {
|
|
21146
|
+
const file = readJsonObject(credPath, "credentials") ?? {};
|
|
21147
|
+
const profiles = {};
|
|
21148
|
+
if (file.profiles && typeof file.profiles === "object") {
|
|
21149
|
+
for (const [key, profile] of Object.entries(file.profiles)) {
|
|
21150
|
+
if (profile && typeof profile === "object" && typeof profile.pat === "string") {
|
|
21151
|
+
profiles[key] = profile;
|
|
21152
|
+
}
|
|
21153
|
+
}
|
|
21154
|
+
} else if (typeof file.pat === "string" && file.pat) {
|
|
21155
|
+
profiles[file.slug || "default"] = {
|
|
21156
|
+
apiBase: file.apiBase,
|
|
21157
|
+
pat: file.pat,
|
|
21158
|
+
contactId: file.contactId,
|
|
21159
|
+
slug: file.slug,
|
|
21160
|
+
email: file.email,
|
|
21161
|
+
plan: file.plan,
|
|
21162
|
+
expiresAt: file.expiresAt,
|
|
21163
|
+
cpanel: file.cpanel,
|
|
21164
|
+
ssh: file.ssh
|
|
21165
|
+
};
|
|
21166
|
+
}
|
|
21167
|
+
const defaultProfile = typeof file.defaultProfile === "string" && profiles[file.defaultProfile] ? file.defaultProfile : null;
|
|
21168
|
+
return { profiles, defaultProfile };
|
|
21169
|
+
}
|
|
21170
|
+
function findPinFile(startDir = process.cwd()) {
|
|
21171
|
+
const explicit = process.env.AWESOMATE_PIN_FILE;
|
|
21172
|
+
if (explicit) {
|
|
21173
|
+
const path = isAbsolute(explicit) ? explicit : resolve(startDir, explicit);
|
|
21174
|
+
return existsSync(path) ? path : null;
|
|
21175
|
+
}
|
|
21176
|
+
const home = resolve(homedir());
|
|
21177
|
+
let dir = resolve(startDir);
|
|
21178
|
+
for (; ; ) {
|
|
21179
|
+
const candidate = join(dir, PIN_FILENAME);
|
|
21180
|
+
if (existsSync(candidate)) return candidate;
|
|
21181
|
+
if (dir === home) return null;
|
|
21182
|
+
const parent = dirname(dir);
|
|
21183
|
+
if (parent === dir) return null;
|
|
21184
|
+
dir = parent;
|
|
21185
|
+
}
|
|
21186
|
+
}
|
|
21187
|
+
function profileList(profiles) {
|
|
21188
|
+
const names = Object.keys(profiles);
|
|
21189
|
+
return names.length ? names.join(", ") : "(none \u2014 run Connect Claude Code from hub.awesomate.ai/sites)";
|
|
21190
|
+
}
|
|
21191
|
+
function fromProfile(key, profile, source, credPath, pinPath, availableProfiles) {
|
|
21192
|
+
const pat = profile.pat ?? "";
|
|
21193
|
+
if (!pat.startsWith("amt_pat_")) {
|
|
21194
|
+
throw new Error(
|
|
21195
|
+
`Profile "${key}" in ${credPath} has a malformed access token \u2014 re-run Connect Claude Code from hub.awesomate.ai/sites for this account.`
|
|
21196
|
+
);
|
|
21197
|
+
}
|
|
21198
|
+
return {
|
|
21199
|
+
apiBase: (process.env.AWESOMATE_API_BASE || profile.apiBase || DEFAULT_API_BASE).replace(/\/$/, ""),
|
|
21200
|
+
pat,
|
|
21201
|
+
account: profile.slug ?? key,
|
|
21202
|
+
email: profile.email ?? null,
|
|
21203
|
+
plan: profile.plan ?? null,
|
|
21204
|
+
expiresAt: profile.expiresAt ?? null,
|
|
21205
|
+
source,
|
|
21206
|
+
credPath,
|
|
21207
|
+
pinPath,
|
|
21208
|
+
availableProfiles
|
|
21209
|
+
};
|
|
21210
|
+
}
|
|
21211
|
+
function loadConfig() {
|
|
21212
|
+
const credPath = credentialsPath();
|
|
21213
|
+
const { profiles, defaultProfile } = readProfiles(credPath);
|
|
21214
|
+
const available = Object.keys(profiles);
|
|
21215
|
+
const envPat = process.env.AWESOMATE_PAT;
|
|
21216
|
+
if (envPat) {
|
|
21217
|
+
if (!envPat.startsWith("amt_pat_")) {
|
|
21218
|
+
throw new Error("AWESOMATE_PAT does not look like an Awesomate access token (expected amt_pat_ prefix).");
|
|
21219
|
+
}
|
|
21220
|
+
const matched = Object.entries(profiles).find(([, p]) => p.pat === envPat);
|
|
21221
|
+
return {
|
|
21222
|
+
apiBase: (process.env.AWESOMATE_API_BASE || matched?.[1].apiBase || DEFAULT_API_BASE).replace(/\/$/, ""),
|
|
21223
|
+
pat: envPat,
|
|
21224
|
+
account: matched ? matched[1].slug ?? matched[0] : null,
|
|
21225
|
+
email: matched?.[1].email ?? null,
|
|
21226
|
+
plan: matched?.[1].plan ?? null,
|
|
21227
|
+
expiresAt: matched?.[1].expiresAt ?? null,
|
|
21228
|
+
source: "env-pat",
|
|
21229
|
+
credPath,
|
|
21230
|
+
pinPath: null,
|
|
21231
|
+
availableProfiles: available
|
|
21232
|
+
};
|
|
21233
|
+
}
|
|
21234
|
+
const envAccount = process.env.AWESOMATE_ACCOUNT;
|
|
21235
|
+
if (envAccount) {
|
|
21236
|
+
const profile = profiles[envAccount];
|
|
21237
|
+
if (!profile) {
|
|
21238
|
+
throw new Error(
|
|
21239
|
+
`AWESOMATE_ACCOUNT="${envAccount}" has no matching profile in ${credPath}. Available profiles: ${profileList(profiles)}. Connect that account from hub.awesomate.ai/sites (logged in as it) to add its profile.`
|
|
21135
21240
|
);
|
|
21136
21241
|
}
|
|
21242
|
+
return fromProfile(envAccount, profile, "env-account", credPath, null, available);
|
|
21137
21243
|
}
|
|
21138
|
-
const
|
|
21139
|
-
|
|
21140
|
-
|
|
21244
|
+
const pinPath = findPinFile();
|
|
21245
|
+
if (pinPath) {
|
|
21246
|
+
const pin = readJsonObject(pinPath, "project pin");
|
|
21247
|
+
const account = pin && typeof pin.account === "string" ? pin.account : null;
|
|
21248
|
+
if (!account) {
|
|
21249
|
+
throw new Error(`${pinPath} exists but has no "account" field. Expected: {"account": "<slug>"}.`);
|
|
21250
|
+
}
|
|
21251
|
+
const profile = profiles[account];
|
|
21252
|
+
if (!profile) {
|
|
21253
|
+
throw new Error(
|
|
21254
|
+
`This folder is pinned to account "${account}" (${pinPath}) but no matching profile exists in ${credPath}. Available profiles: ${profileList(profiles)}. Fix the pin, or connect "${account}" from hub.awesomate.ai/sites while logged into that account.`
|
|
21255
|
+
);
|
|
21256
|
+
}
|
|
21257
|
+
return fromProfile(account, profile, "pin", credPath, pinPath, available);
|
|
21258
|
+
}
|
|
21259
|
+
if (available.length === 1) {
|
|
21260
|
+
const key = available[0];
|
|
21261
|
+
return fromProfile(key, profiles[key], "sole-profile", credPath, null, available);
|
|
21262
|
+
}
|
|
21263
|
+
if (defaultProfile) {
|
|
21264
|
+
return fromProfile(defaultProfile, profiles[defaultProfile], "default-profile", credPath, null, available);
|
|
21265
|
+
}
|
|
21266
|
+
if (available.length === 0) {
|
|
21141
21267
|
throw new Error(
|
|
21142
|
-
|
|
21268
|
+
`No Awesomate access token found. Set AWESOMATE_PAT or run Connect Claude Code from hub.awesomate.ai/sites (it writes ${credPath}).`
|
|
21143
21269
|
);
|
|
21144
21270
|
}
|
|
21145
|
-
|
|
21146
|
-
|
|
21147
|
-
|
|
21148
|
-
return { apiBase, pat };
|
|
21271
|
+
throw new Error(
|
|
21272
|
+
`Multiple Awesomate accounts are connected (${profileList(profiles)}) but this folder isn't pinned to one. Create a ${PIN_FILENAME} in the project root with {"account": "<slug>"}, set AWESOMATE_ACCOUNT, or set defaultProfile in credentials.json.`
|
|
21273
|
+
);
|
|
21149
21274
|
}
|
|
21150
21275
|
|
|
21151
21276
|
// src/http.ts
|
|
@@ -21219,11 +21344,22 @@ function hubDelete(config3, path) {
|
|
|
21219
21344
|
}
|
|
21220
21345
|
|
|
21221
21346
|
// src/index.ts
|
|
21222
|
-
var config2;
|
|
21347
|
+
var config2 = null;
|
|
21348
|
+
var configError = null;
|
|
21349
|
+
function requireConfig() {
|
|
21350
|
+
if (!config2) {
|
|
21351
|
+
throw new Error(configError ?? "Awesomate account not configured.");
|
|
21352
|
+
}
|
|
21353
|
+
return config2;
|
|
21354
|
+
}
|
|
21355
|
+
function accountStamp() {
|
|
21356
|
+
if (!config2) return `account: NOT CONNECTED \u2014 ${configError ?? "unknown error"}`;
|
|
21357
|
+
return `account: ${config2.account ?? "unknown (env PAT not matched to a profile)"} (source: ${config2.source})`;
|
|
21358
|
+
}
|
|
21223
21359
|
var SERVER_VERSION = (() => {
|
|
21224
21360
|
try {
|
|
21225
21361
|
const pkg = JSON.parse(
|
|
21226
|
-
readFileSync2(join2(
|
|
21362
|
+
readFileSync2(join2(dirname2(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")
|
|
21227
21363
|
);
|
|
21228
21364
|
return pkg.version ?? "0.0.0";
|
|
21229
21365
|
} catch {
|
|
@@ -21250,21 +21386,48 @@ var server = new McpServer({
|
|
|
21250
21386
|
version: SERVER_VERSION
|
|
21251
21387
|
});
|
|
21252
21388
|
function textResult(data) {
|
|
21253
|
-
return {
|
|
21389
|
+
return {
|
|
21390
|
+
content: [{ type: "text", text: `${accountStamp()}
|
|
21391
|
+
${JSON.stringify(data, null, 2)}` }]
|
|
21392
|
+
};
|
|
21254
21393
|
}
|
|
21255
21394
|
function errorResult(err) {
|
|
21256
21395
|
const message = err instanceof Error ? err.message : String(err);
|
|
21257
|
-
return { content: [{ type: "text", text:
|
|
21396
|
+
return { content: [{ type: "text", text: `${accountStamp()}
|
|
21397
|
+
${message}` }], isError: true };
|
|
21258
21398
|
}
|
|
21259
21399
|
function readTool(name, description, path) {
|
|
21260
21400
|
server.registerTool(name, { description, inputSchema: {} }, async () => {
|
|
21261
21401
|
try {
|
|
21262
|
-
return textResult(await hubGet(
|
|
21402
|
+
return textResult(await hubGet(requireConfig(), path));
|
|
21263
21403
|
} catch (err) {
|
|
21264
21404
|
return errorResult(err);
|
|
21265
21405
|
}
|
|
21266
21406
|
});
|
|
21267
21407
|
}
|
|
21408
|
+
server.registerTool(
|
|
21409
|
+
"awesomate_whoami",
|
|
21410
|
+
{
|
|
21411
|
+
description: "Zero-network identity check: which Awesomate account this session is connected to (slug, plan, token expiry) and WHY (env var, project pin file, sole profile, or default). Call it after connecting, after switching folders, and any time the account in play matters \u2014 if the slug is not the account the user expects, STOP and fix the pin/connection before doing any work.",
|
|
21412
|
+
inputSchema: {}
|
|
21413
|
+
},
|
|
21414
|
+
async () => {
|
|
21415
|
+
if (!config2) {
|
|
21416
|
+
return errorResult(new Error(configError ?? "Awesomate account not configured."));
|
|
21417
|
+
}
|
|
21418
|
+
return textResult({
|
|
21419
|
+
account: config2.account,
|
|
21420
|
+
email: config2.email,
|
|
21421
|
+
plan: config2.plan,
|
|
21422
|
+
tokenExpiresAt: config2.expiresAt,
|
|
21423
|
+
apiBase: config2.apiBase,
|
|
21424
|
+
source: config2.source,
|
|
21425
|
+
credentialsFile: config2.credPath,
|
|
21426
|
+
pinFile: config2.pinPath,
|
|
21427
|
+
availableProfiles: config2.availableProfiles
|
|
21428
|
+
});
|
|
21429
|
+
}
|
|
21430
|
+
);
|
|
21268
21431
|
server.registerTool(
|
|
21269
21432
|
"awesomate_get_context",
|
|
21270
21433
|
{
|
|
@@ -21273,7 +21436,7 @@ server.registerTool(
|
|
|
21273
21436
|
},
|
|
21274
21437
|
async () => {
|
|
21275
21438
|
try {
|
|
21276
|
-
const ctx = await hubGet(
|
|
21439
|
+
const ctx = await hubGet(requireConfig(), "/api/hosting-access/context");
|
|
21277
21440
|
return textResult({ ...ctx, skill: skillUpdateInfo() });
|
|
21278
21441
|
} catch (err) {
|
|
21279
21442
|
return errorResult(err);
|
|
@@ -21300,21 +21463,21 @@ server.registerTool(
|
|
|
21300
21463
|
async ({ action, workflowJson, workflowId, draftId, operationId }) => {
|
|
21301
21464
|
try {
|
|
21302
21465
|
if (action === "validate") {
|
|
21303
|
-
return textResult(await hubPost(
|
|
21466
|
+
return textResult(await hubPost(requireConfig(), "/api/my-n8n/machine/workflows/validate", { workflow: workflowJson }));
|
|
21304
21467
|
}
|
|
21305
21468
|
if (action === "create_draft") {
|
|
21306
|
-
return textResult(await hubPost(
|
|
21469
|
+
return textResult(await hubPost(requireConfig(), "/api/my-n8n/machine/workflows/draft", workflowJson ?? {}));
|
|
21307
21470
|
}
|
|
21308
21471
|
if (action === "rollback") {
|
|
21309
21472
|
if (!operationId) return errorResult(new Error("operationId is required for rollback"));
|
|
21310
21473
|
return textResult(
|
|
21311
|
-
await hubPost(
|
|
21474
|
+
await hubPost(requireConfig(), `/api/my-n8n/machine/operations/${encodeURIComponent(operationId)}/rollback`, {})
|
|
21312
21475
|
);
|
|
21313
21476
|
}
|
|
21314
21477
|
if (!workflowId) return errorResult(new Error(`workflowId is required for ${action}`));
|
|
21315
21478
|
if (action === "activate" || action === "deactivate") {
|
|
21316
21479
|
return textResult(
|
|
21317
|
-
await hubPost(
|
|
21480
|
+
await hubPost(requireConfig(), `/api/my-n8n/machine/workflows/${encodeURIComponent(workflowId)}/activate`, {
|
|
21318
21481
|
active: action === "activate"
|
|
21319
21482
|
})
|
|
21320
21483
|
);
|
|
@@ -21322,11 +21485,11 @@ server.registerTool(
|
|
|
21322
21485
|
if (action === "promote") {
|
|
21323
21486
|
if (!draftId) return errorResult(new Error("draftId is required for promote"));
|
|
21324
21487
|
return textResult(
|
|
21325
|
-
await hubPost(
|
|
21488
|
+
await hubPost(requireConfig(), `/api/my-n8n/machine/workflows/${encodeURIComponent(workflowId)}/promote`, { draftId })
|
|
21326
21489
|
);
|
|
21327
21490
|
}
|
|
21328
21491
|
return textResult(
|
|
21329
|
-
await hubDelete(
|
|
21492
|
+
await hubDelete(requireConfig(), `/api/my-n8n/machine/workflows/${encodeURIComponent(workflowId)}`)
|
|
21330
21493
|
);
|
|
21331
21494
|
} catch (err) {
|
|
21332
21495
|
return errorResult(err);
|
|
@@ -21345,7 +21508,7 @@ server.registerTool(
|
|
|
21345
21508
|
async ({ workflowId, payload }) => {
|
|
21346
21509
|
try {
|
|
21347
21510
|
return textResult(
|
|
21348
|
-
await hubPost(
|
|
21511
|
+
await hubPost(requireConfig(), `/api/my-n8n/machine/workflows/${encodeURIComponent(workflowId)}/test-fire`, { payload })
|
|
21349
21512
|
);
|
|
21350
21513
|
} catch (err) {
|
|
21351
21514
|
return errorResult(err);
|
|
@@ -21406,7 +21569,7 @@ server.registerTool(
|
|
|
21406
21569
|
async ({ domain, reason }) => {
|
|
21407
21570
|
try {
|
|
21408
21571
|
return textResult(
|
|
21409
|
-
await hubPost(
|
|
21572
|
+
await hubPost(requireConfig(), `/api/hosting-access/sites/${encodeURIComponent(domain)}/snapshot`, { reason })
|
|
21410
21573
|
);
|
|
21411
21574
|
} catch (err) {
|
|
21412
21575
|
return errorResult(err);
|
|
@@ -21421,7 +21584,7 @@ server.registerTool(
|
|
|
21421
21584
|
},
|
|
21422
21585
|
async ({ domain }) => {
|
|
21423
21586
|
try {
|
|
21424
|
-
return textResult(await hubGet(
|
|
21587
|
+
return textResult(await hubGet(requireConfig(), `/api/hosting-access/sites/${encodeURIComponent(domain)}/snapshots`));
|
|
21425
21588
|
} catch (err) {
|
|
21426
21589
|
return errorResult(err);
|
|
21427
21590
|
}
|
|
@@ -21439,7 +21602,7 @@ server.registerTool(
|
|
|
21439
21602
|
async ({ domain, snapshotId }) => {
|
|
21440
21603
|
try {
|
|
21441
21604
|
return textResult(
|
|
21442
|
-
await hubPost(
|
|
21605
|
+
await hubPost(requireConfig(), `/api/hosting-access/sites/${encodeURIComponent(domain)}/rollback`, { snapshotId })
|
|
21443
21606
|
);
|
|
21444
21607
|
} catch (err) {
|
|
21445
21608
|
return errorResult(err);
|
|
@@ -21447,10 +21610,15 @@ server.registerTool(
|
|
|
21447
21610
|
}
|
|
21448
21611
|
);
|
|
21449
21612
|
async function main() {
|
|
21450
|
-
|
|
21613
|
+
try {
|
|
21614
|
+
config2 = loadConfig();
|
|
21615
|
+
} catch (err) {
|
|
21616
|
+
configError = err instanceof Error ? err.message : String(err);
|
|
21617
|
+
console.error(`[awesomate-hosting-mcp] account not resolved: ${configError}`);
|
|
21618
|
+
}
|
|
21451
21619
|
const transport = new StdioServerTransport();
|
|
21452
21620
|
await server.connect(transport);
|
|
21453
|
-
console.error(
|
|
21621
|
+
console.error(`[awesomate-hosting-mcp] connected (stdio) \u2014 ${accountStamp()}`);
|
|
21454
21622
|
}
|
|
21455
21623
|
main().catch((err) => {
|
|
21456
21624
|
console.error(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awesomate/hosting-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/index.js --banner:js='#!/usr/bin/env node' --external:node:*",
|
|
21
21
|
"typecheck": "tsc --noEmit",
|
|
22
|
-
"
|
|
22
|
+
"test": "esbuild src/config.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/config.mjs --external:node:* && node --test test/",
|
|
23
|
+
"prepublishOnly": "npm run typecheck && npm run test && npm run build"
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
25
26
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -24,8 +24,14 @@ generation, defer to WordPress Studio and any installed WordPress skills
|
|
|
24
24
|
|
|
25
25
|
## 0. First run — orient before acting
|
|
26
26
|
|
|
27
|
-
On the first hosting request in a session, call **`
|
|
28
|
-
|
|
27
|
+
On the first hosting request in a session, call **`awesomate_whoami`** — it's
|
|
28
|
+
zero-network and tells you WHICH account this folder is connected to and why
|
|
29
|
+
(`source`: a `.awesomate.json` folder pin, `AWESOMATE_ACCOUNT`, the sole
|
|
30
|
+
profile, or the default profile). **If the slug isn't the account the user is
|
|
31
|
+
talking about, stop and fix the connection before doing any work** — every
|
|
32
|
+
subsequent tool response is stamped with `account: <slug> (source: …)`; treat a
|
|
33
|
+
mismatch there the same way. Then call **`awesomate_get_context`** once (then
|
|
34
|
+
`awesomate_get_limits` if you're about to create/change anything). From the
|
|
29
35
|
context note three things and cache them for the session:
|
|
30
36
|
|
|
31
37
|
- **`plan`** and **`capabilities.shell`** — this decides which branch below you're on.
|
|
@@ -41,13 +47,39 @@ context note three things and cache them for the session:
|
|
|
41
47
|
If `awesomate_get_context` fails with a connectivity error, it's the user's
|
|
42
48
|
network or the API base — not an auth problem; say so.
|
|
43
49
|
|
|
50
|
+
### Multiple accounts on one machine
|
|
51
|
+
|
|
52
|
+
`~/.awesomate/credentials.json` can hold several account **profiles** (one per
|
|
53
|
+
slug), and each project folder is bound to one via a committable
|
|
54
|
+
`.awesomate.json` pin (`{"account": "<slug>"}`) written by the bootstrap in the
|
|
55
|
+
folder it ran in. Resolution order: `AWESOMATE_PAT` env → `AWESOMATE_ACCOUNT`
|
|
56
|
+
env → nearest folder pin (walking up, stopping at `$HOME`) → sole profile →
|
|
57
|
+
`defaultProfile`. A pin naming a missing profile is a **hard error, never a
|
|
58
|
+
fallback** — that's deliberate.
|
|
59
|
+
|
|
60
|
+
- **Wrong account connected?** Compare `awesomate_whoami` against the folder's
|
|
61
|
+
`.awesomate.json`. Fix by editing the pin (if the profile exists) or by
|
|
62
|
+
running Connect Claude Code from the hub **while logged into the right hub
|
|
63
|
+
account, from the right folder** — the connect command carries
|
|
64
|
+
`--account <slug>` and the bootstrap aborts on a mismatch instead of storing
|
|
65
|
+
anything.
|
|
66
|
+
- **"Multiple accounts … folder isn't pinned" error?** Create the pin file or
|
|
67
|
+
set `AWESOMATE_ACCOUNT`. Don't guess an account for the user.
|
|
68
|
+
- Connecting a second account never disconnects the first; re-pairing an
|
|
69
|
+
account just refreshes its profile.
|
|
70
|
+
- The bootstrap prints a final `AWESOMATE CONNECT: SUCCESS account=<slug>` (or
|
|
71
|
+
`FAILED reason=…`) sentinel — when a connect ran as a `!` bang command, check
|
|
72
|
+
for that line instead of assuming success from silence.
|
|
73
|
+
|
|
44
74
|
If this skill is loaded but **no `awesomate_*` tools exist in the session at
|
|
45
75
|
all**, the MCP server was registered after Claude Code started (the bootstrap
|
|
46
76
|
just ran). Don't investigate settings files or reinstall anything — and don't
|
|
47
77
|
make a restart the precondition for showing the user their own data. The
|
|
48
|
-
skill works right now over REST
|
|
49
|
-
|
|
50
|
-
|
|
78
|
+
skill works right now over REST — resolve the ACTIVE profile first (run
|
|
79
|
+
`node ~/.claude/skills/awesomate-hosting/scripts/resolve-account.mjs --api` to
|
|
80
|
+
get `API`/`PAT`/`ACCT`; never read top-level `pat` directly when a `profiles`
|
|
81
|
+
map exists, and never echo the pat into the conversation) and GET with
|
|
82
|
+
`Authorization: Bearer <pat>`:
|
|
51
83
|
|
|
52
84
|
| MCP tool | REST equivalent |
|
|
53
85
|
|---|---|
|
|
@@ -3,23 +3,28 @@
|
|
|
3
3
|
* Awesomate Hosting — one-time bootstrap.
|
|
4
4
|
*
|
|
5
5
|
* Redeems the single-use setup code from the hub's "Connect Claude Code" prompt
|
|
6
|
-
* for a durable, scoped access token; stores it
|
|
6
|
+
* for a durable, scoped access token; stores it as a PROFILE (multi-account
|
|
7
|
+
* safe); pins the current folder to that account; and (on Support Plus+)
|
|
7
8
|
* generates an SSH keypair and authorizes the PUBLIC key on the client's own
|
|
8
9
|
* cPanel account. The private key never leaves this machine; the setup code is
|
|
9
10
|
* the only secret that ever crossed the clipboard, and it's single-use + short-
|
|
10
|
-
* lived.
|
|
11
|
+
* lived. The access token itself is never printed.
|
|
11
12
|
*
|
|
12
13
|
* Usage (Claude runs this — the user doesn't touch a terminal):
|
|
13
|
-
* node bootstrap.mjs --code amt_bs_XXXX [--api https://hub.awesomate.ai]
|
|
14
|
+
* node bootstrap.mjs --code amt_bs_XXXX [--account <slug>] [--api https://hub.awesomate.ai]
|
|
15
|
+
*
|
|
16
|
+
* --account asserts WHICH account the code is expected to connect. If the
|
|
17
|
+
* redeemed identity doesn't match, the bootstrap aborts before writing
|
|
18
|
+
* anything — connecting the wrong account loudly beats connecting it silently.
|
|
14
19
|
*
|
|
15
20
|
* Dependency-free: Node 18+ built-ins only (global fetch, node:fs/os/path,
|
|
16
|
-
* child_process for ssh-keygen).
|
|
21
|
+
* child_process for ssh-keygen + the claude CLI).
|
|
17
22
|
*/
|
|
18
23
|
|
|
19
24
|
import { execFileSync } from 'node:child_process';
|
|
20
25
|
import { existsSync, mkdirSync, writeFileSync, chmodSync, readFileSync, cpSync, readdirSync } from 'node:fs';
|
|
21
26
|
import { homedir, hostname } from 'node:os';
|
|
22
|
-
import { join, dirname } from 'node:path';
|
|
27
|
+
import { join, dirname, resolve } from 'node:path';
|
|
23
28
|
import { fileURLToPath } from 'node:url';
|
|
24
29
|
|
|
25
30
|
function arg(name, fallback) {
|
|
@@ -28,9 +33,11 @@ function arg(name, fallback) {
|
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
const code = arg('code');
|
|
36
|
+
const expectedAccount = arg('account', null);
|
|
31
37
|
const apiBase = (arg('api', 'https://hub.awesomate.ai')).replace(/\/$/, '');
|
|
32
38
|
if (!code || !code.startsWith('amt_bs_')) {
|
|
33
39
|
console.error('Missing/invalid --code. Copy a fresh setup prompt from hub.awesomate.ai/sites.');
|
|
40
|
+
console.error('AWESOMATE CONNECT: FAILED reason=bad-code');
|
|
34
41
|
process.exit(1);
|
|
35
42
|
}
|
|
36
43
|
|
|
@@ -38,11 +45,16 @@ const dir = join(homedir(), '.awesomate');
|
|
|
38
45
|
const keysDir = join(dir, 'keys');
|
|
39
46
|
const credPath = join(dir, 'credentials.json');
|
|
40
47
|
const keyPath = join(keysDir, 'id_ed25519');
|
|
48
|
+
const PIN_FILENAME = '.awesomate.json';
|
|
41
49
|
|
|
42
|
-
async function
|
|
50
|
+
async function request(method, path, { body, token } = {}) {
|
|
43
51
|
const headers = { 'Content-Type': 'application/json' };
|
|
44
52
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
45
|
-
const res = await fetch(`${apiBase}${path}`, {
|
|
53
|
+
const res = await fetch(`${apiBase}${path}`, {
|
|
54
|
+
method,
|
|
55
|
+
headers,
|
|
56
|
+
body: method === 'GET' ? undefined : JSON.stringify(body ?? {}),
|
|
57
|
+
});
|
|
46
58
|
const text = await res.text();
|
|
47
59
|
let json;
|
|
48
60
|
try { json = text ? JSON.parse(text) : null; } catch { json = text; }
|
|
@@ -52,12 +64,84 @@ async function post(path, { body, token } = {}) {
|
|
|
52
64
|
}
|
|
53
65
|
return json;
|
|
54
66
|
}
|
|
67
|
+
const post = (path, opts) => request('POST', path, opts);
|
|
68
|
+
const get = (path, opts) => request('GET', path, opts);
|
|
69
|
+
|
|
70
|
+
function readJsonFile(path) {
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
73
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
74
|
+
} catch { return null; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Merge one account profile into credentials.json (v2 multi-profile shape).
|
|
79
|
+
* Existing profiles for OTHER accounts are preserved — connecting a second
|
|
80
|
+
* account must never disconnect the first. Top-level v1 fields are kept as a
|
|
81
|
+
* mirror of the default profile because already-installed skill scripts
|
|
82
|
+
* (ssh-connect.sh / deploy.sh / pull-live.sh copies from older versions) read
|
|
83
|
+
* them directly until the skill is refreshed.
|
|
84
|
+
*/
|
|
85
|
+
function mergeProfile(profile) {
|
|
86
|
+
const existing = readJsonFile(credPath) ?? {};
|
|
87
|
+
const profiles = { ...(existing.profiles ?? {}) };
|
|
88
|
+
|
|
89
|
+
// v1 file: lift the old top-level account into the profiles map first so it
|
|
90
|
+
// survives this connect.
|
|
91
|
+
if (!existing.profiles && typeof existing.pat === 'string' && existing.pat) {
|
|
92
|
+
const legacyKey = existing.slug || 'default';
|
|
93
|
+
profiles[legacyKey] = {
|
|
94
|
+
apiBase: existing.apiBase, pat: existing.pat, contactId: existing.contactId,
|
|
95
|
+
slug: existing.slug, email: existing.email, plan: existing.plan,
|
|
96
|
+
expiresAt: existing.expiresAt, cpanel: existing.cpanel, ssh: existing.ssh,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
profiles[profile.slug] = { ...(profiles[profile.slug] ?? {}), ...profile };
|
|
101
|
+
|
|
102
|
+
let defaultProfile = existing.defaultProfile;
|
|
103
|
+
if (defaultProfile && !profiles[defaultProfile]) defaultProfile = undefined;
|
|
104
|
+
if (!defaultProfile && Object.keys(profiles).length === 1) defaultProfile = profile.slug;
|
|
105
|
+
if (existing.defaultProfile && existing.defaultProfile !== profile.slug && profiles[existing.defaultProfile]) {
|
|
106
|
+
console.log(`• defaultProfile stays "${existing.defaultProfile}" — this folder is pinned to "${profile.slug}" instead.`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// The v1 mirror follows the default profile when one is set; otherwise the
|
|
110
|
+
// account just connected (matching the old last-connect-wins behavior for
|
|
111
|
+
// legacy readers, without losing the other profiles).
|
|
112
|
+
const mirrorSource = (defaultProfile && profiles[defaultProfile]) || profile;
|
|
113
|
+
const creds = {
|
|
114
|
+
version: 2,
|
|
115
|
+
...(defaultProfile ? { defaultProfile } : {}),
|
|
116
|
+
profiles,
|
|
117
|
+
apiBase: mirrorSource.apiBase, pat: mirrorSource.pat, contactId: mirrorSource.contactId,
|
|
118
|
+
slug: mirrorSource.slug, email: mirrorSource.email ?? null, plan: mirrorSource.plan,
|
|
119
|
+
cpanel: mirrorSource.cpanel ?? null, expiresAt: mirrorSource.expiresAt ?? null,
|
|
120
|
+
...(mirrorSource.ssh ? { ssh: mirrorSource.ssh } : {}),
|
|
121
|
+
};
|
|
55
122
|
|
|
56
|
-
function writeCreds(creds) {
|
|
57
123
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
58
124
|
chmodSync(dir, 0o700);
|
|
59
125
|
writeFileSync(credPath, JSON.stringify(creds, null, 2));
|
|
60
126
|
chmodSync(credPath, 0o600);
|
|
127
|
+
return creds;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Pin the folder bootstrap ran in to this account. Never pins $HOME. */
|
|
131
|
+
function writePin(slug) {
|
|
132
|
+
const cwd = resolve(process.cwd());
|
|
133
|
+
if (cwd === resolve(homedir())) {
|
|
134
|
+
console.log(`• Running from the home directory — not writing a ${PIN_FILENAME} pin. Re-run Connect from the project folder to pin it.`);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const pinPath = join(cwd, PIN_FILENAME);
|
|
138
|
+
const existing = readJsonFile(pinPath);
|
|
139
|
+
if (existing?.account && existing.account !== slug) {
|
|
140
|
+
console.log(`• Re-pinning this folder: ${existing.account} → ${slug}.`);
|
|
141
|
+
}
|
|
142
|
+
writeFileSync(pinPath, `${JSON.stringify({ version: 1, account: slug }, null, 2)}\n`);
|
|
143
|
+
console.log(`✓ Pinned ${cwd} to account "${slug}" (${PIN_FILENAME}).`);
|
|
144
|
+
return pinPath;
|
|
61
145
|
}
|
|
62
146
|
|
|
63
147
|
/**
|
|
@@ -119,23 +203,116 @@ function installSkill() {
|
|
|
119
203
|
return true;
|
|
120
204
|
}
|
|
121
205
|
|
|
206
|
+
function claudeCli(args) {
|
|
207
|
+
return execFileSync('claude', args, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const MANUAL_ADD =
|
|
211
|
+
'claude mcp add --scope user awesomate-hosting -- npx -y -p @awesomate/hosting-mcp awesomate-hosting-mcp';
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Converge on exactly one env-free, user-scope MCP registration.
|
|
215
|
+
*
|
|
216
|
+
* Legacy bootstraps baked AWESOMATE_PAT into the registration env — env beats
|
|
217
|
+
* credentials.json inside the server, so those registrations keep serving a
|
|
218
|
+
* STALE account forever after a re-pair. Remove every registration that
|
|
219
|
+
* carries the env var (user scope, plus this project's .mcp.json / local
|
|
220
|
+
* scope), then add the clean one. Registrations in OTHER projects' local
|
|
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.
|
|
225
|
+
*/
|
|
226
|
+
function ensureMcpRegistration() {
|
|
227
|
+
const cwd = resolve(process.cwd());
|
|
228
|
+
const claudeConfig = readJsonFile(join(homedir(), '.claude.json')) ?? {};
|
|
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
|
+
}
|
|
239
|
+
|
|
240
|
+
for (const scope of scopesToClean) {
|
|
241
|
+
claudeCli(['mcp', 'remove', 'awesomate-hosting', '-s', scope]);
|
|
242
|
+
console.log(`✓ Removed legacy ${scope}-scope registration (had a token baked into its env).`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const userEntry = claudeConfig.mcpServers?.['awesomate-hosting'];
|
|
246
|
+
const userEntryIsClean =
|
|
247
|
+
userEntry && !userEntry.env?.AWESOMATE_PAT && !scopesToClean.includes('user') &&
|
|
248
|
+
JSON.stringify(userEntry.args ?? []).includes('awesomate-hosting-mcp');
|
|
249
|
+
if (userEntryIsClean) {
|
|
250
|
+
console.log('✓ MCP server already registered (user scope, env-free) — account selection comes from the folder pin.');
|
|
251
|
+
} else {
|
|
252
|
+
if (userEntry && !scopesToClean.includes('user')) {
|
|
253
|
+
// Exists but is odd (wrong command, partial env) — replace it.
|
|
254
|
+
try { claudeCli(['mcp', 'remove', 'awesomate-hosting', '-s', 'user']); } catch { /* may not exist at this scope */ }
|
|
255
|
+
}
|
|
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
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const w of warnings) {
|
|
262
|
+
console.log(`• Another project still has a legacy registration with a baked-in token. From that project run:\n ${w}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
122
266
|
async function main() {
|
|
123
267
|
console.log(`Redeeming setup code against ${apiBase} …`);
|
|
124
268
|
const redeemed = await post('/api/hosting-access/redeem', {
|
|
125
269
|
body: { code, machineLabel: `Claude Code on ${hostname()}` },
|
|
126
270
|
});
|
|
127
271
|
|
|
128
|
-
const
|
|
272
|
+
const slug = redeemed.slug ?? null;
|
|
273
|
+
if (!slug) {
|
|
274
|
+
console.error('The hub did not return an account slug for this code — cannot safely store a profile. Contact Awesomate support.');
|
|
275
|
+
console.error('AWESOMATE CONNECT: FAILED reason=no-slug');
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
// Wrong-account guard: abort BEFORE writing anything.
|
|
279
|
+
if (expectedAccount && expectedAccount !== slug) {
|
|
280
|
+
console.error(`This setup code belongs to account "${slug}" but this connect was for "${expectedAccount}".`);
|
|
281
|
+
console.error(`You are probably logged into the wrong hub account — log into ${expectedAccount}'s hub account and copy a fresh Connect prompt.`);
|
|
282
|
+
console.error(`AWESOMATE CONNECT: FAILED reason=account-mismatch expected=${expectedAccount} got=${slug}`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Server-side identity check with the NEW token before trusting it.
|
|
287
|
+
let context = null;
|
|
288
|
+
try {
|
|
289
|
+
context = await get('/api/hosting-access/context', { token: redeemed.pat });
|
|
290
|
+
} catch (err) {
|
|
291
|
+
console.error(`Token verification failed (${err.message}) — not storing it. Re-run with a fresh code from hub.awesomate.ai/sites.`);
|
|
292
|
+
console.error('AWESOMATE CONNECT: FAILED reason=verify-failed');
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
const ctxSlug = context?.slug ?? context?.account?.slug ?? null;
|
|
296
|
+
if (ctxSlug && ctxSlug !== slug) {
|
|
297
|
+
console.error(`Identity mismatch: redeem said "${slug}" but the token resolves to "${ctxSlug}". Not storing it — contact Awesomate support.`);
|
|
298
|
+
console.error('AWESOMATE CONNECT: FAILED reason=identity-mismatch');
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const profile = {
|
|
129
303
|
apiBase,
|
|
130
304
|
pat: redeemed.pat,
|
|
131
305
|
contactId: redeemed.contactId,
|
|
132
|
-
slug
|
|
306
|
+
slug,
|
|
307
|
+
email: redeemed.email ?? context?.email ?? context?.account?.email ?? null,
|
|
133
308
|
plan: redeemed.plan,
|
|
134
309
|
cpanel: redeemed.cpanel ?? null,
|
|
135
310
|
expiresAt: redeemed.expiresAt ?? null,
|
|
136
311
|
};
|
|
137
|
-
|
|
138
|
-
console.log(`✓ Access token stored
|
|
312
|
+
mergeProfile(profile);
|
|
313
|
+
console.log(`✓ Access token stored as profile "${slug}" in ${credPath} (chmod 600). Plan: ${redeemed.plan}.`);
|
|
314
|
+
|
|
315
|
+
const pinPath = writePin(slug);
|
|
139
316
|
|
|
140
317
|
// Install the skill + register the MCP server FIRST — these always work and
|
|
141
318
|
// don't depend on hosting being fully provisioned. SSH comes last so that a
|
|
@@ -144,19 +321,13 @@ async function main() {
|
|
|
144
321
|
console.log(`• Could not auto-install the skill (${err.message}) — continuing; you can copy skill/ manually.`);
|
|
145
322
|
}
|
|
146
323
|
|
|
147
|
-
// Register the MCP server. Prefer the Claude CLI if present; otherwise print
|
|
148
|
-
// the config for Claude to apply.
|
|
149
|
-
const cmd = `claude mcp add awesomate-hosting --env AWESOMATE_PAT=${redeemed.pat} --env AWESOMATE_API_BASE=${apiBase} -- npx -y -p @awesomate/hosting-mcp awesomate-hosting-mcp`;
|
|
150
|
-
let registered = false;
|
|
151
324
|
try {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
console.
|
|
155
|
-
|
|
156
|
-
console.
|
|
157
|
-
|
|
158
|
-
console.log('…or add to .mcp.json:');
|
|
159
|
-
console.log(JSON.stringify({ mcpServers: { 'awesomate-hosting': { command: 'npx', args: ['-y', '-p', '@awesomate/hosting-mcp', 'awesomate-hosting-mcp'], env: { AWESOMATE_PAT: redeemed.pat, AWESOMATE_API_BASE: apiBase } } } }, null, 2));
|
|
325
|
+
ensureMcpRegistration();
|
|
326
|
+
} catch (err) {
|
|
327
|
+
console.error(`MCP registration failed: ${err.message}`);
|
|
328
|
+
console.error(`Register it manually (no token goes in the command — the folder pin picks the account):\n ${MANUAL_ADD}`);
|
|
329
|
+
console.error(`AWESOMATE CONNECT: FAILED reason=mcp-registration account=${slug}`);
|
|
330
|
+
process.exit(1);
|
|
160
331
|
}
|
|
161
332
|
|
|
162
333
|
// SSH key (Support Plus+): best-effort. If it fails (e.g. hosting not fully
|
|
@@ -166,7 +337,7 @@ async function main() {
|
|
|
166
337
|
try {
|
|
167
338
|
if (!existsSync(keyPath)) {
|
|
168
339
|
mkdirSync(keysDir, { recursive: true, mode: 0o700 });
|
|
169
|
-
execFileSync('ssh-keygen', ['-t', 'ed25519', '-f', keyPath, '-N', '', '-C', `awesomate:${
|
|
340
|
+
execFileSync('ssh-keygen', ['-t', 'ed25519', '-f', keyPath, '-N', '', '-C', `awesomate:${slug}`], { stdio: 'pipe' });
|
|
170
341
|
chmodSync(keyPath, 0o600);
|
|
171
342
|
}
|
|
172
343
|
const publicKey = readFileSync(`${keyPath}.pub`, 'utf8').trim();
|
|
@@ -174,8 +345,8 @@ async function main() {
|
|
|
174
345
|
token: redeemed.pat,
|
|
175
346
|
body: { publicKey, label: `Claude Code on ${hostname()}` },
|
|
176
347
|
});
|
|
177
|
-
|
|
178
|
-
|
|
348
|
+
profile.ssh = { host: reg.sshHost, user: reg.sshUser, port: reg.sshPort ?? 22, keyPath, fingerprint: reg.fingerprint };
|
|
349
|
+
mergeProfile(profile);
|
|
179
350
|
console.log(`✓ SSH key authorized on ${reg.sshUser}@${reg.sshHost} (jailed to your account).`);
|
|
180
351
|
} catch (err) {
|
|
181
352
|
console.log(`• SSH key not registered yet: ${err.message}`);
|
|
@@ -185,12 +356,16 @@ async function main() {
|
|
|
185
356
|
console.log('• Plan has no shell access — read/status mode. Upgrade to Support Plus for SSH + deploy.');
|
|
186
357
|
}
|
|
187
358
|
|
|
188
|
-
|
|
189
|
-
|
|
359
|
+
const who = profile.email ? `${slug} (${profile.email}, plan ${redeemed.plan})` : `${slug} (plan ${redeemed.plan})`;
|
|
360
|
+
console.log(`\n✓ Connected: ${who}${pinPath ? ` — pinned ${dirname(pinPath)}` : ''}`);
|
|
361
|
+
console.log('Setup complete. Next: run awesomate_whoami to confirm the account, then ask me to read your plan and list your sites.');
|
|
362
|
+
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.)');
|
|
363
|
+
console.log(`AWESOMATE CONNECT: SUCCESS account=${slug} pin=${pinPath ?? 'none'}`);
|
|
190
364
|
}
|
|
191
365
|
|
|
192
366
|
main().catch((err) => {
|
|
193
367
|
console.error('Bootstrap failed:', err.message);
|
|
194
368
|
console.error('If the code expired or was already used, grab a fresh one from hub.awesomate.ai/sites.');
|
|
369
|
+
console.error('AWESOMATE CONNECT: FAILED reason=error');
|
|
195
370
|
process.exit(1);
|
|
196
371
|
});
|
|
@@ -52,18 +52,17 @@ done
|
|
|
52
52
|
echo "--from wp-content looks empty (no themes/ or plugins/). Refusing to --delete against live."; exit 1; }
|
|
53
53
|
case "$DOMAIN" in *[!a-z0-9.-]*|"") echo "Invalid --domain."; exit 1;; esac
|
|
54
54
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
console.log(`H=${q(c.ssh.host)}; U=${q(c.ssh.user)}; P=${q(c.ssh.port||22)}; K=${q(c.ssh.keyPath)}; API=${q(c.apiBase)}; PAT=${q(c.pat)}`);
|
|
63
|
-
')"; then
|
|
55
|
+
# Resolve the account (folder pin / AWESOMATE_ACCOUNT / sole profile) and its
|
|
56
|
+
# connection details + PAT via the shared resolver (node is present wherever
|
|
57
|
+
# the MCP runs). Capture first, THEN eval — so a resolver failure (no-shell
|
|
58
|
+
# plan exits 2, unpinned multi-account exits 1) aborts here with its own
|
|
59
|
+
# message instead of leaving $U/$P/… unset for `set -u` to trip on later.
|
|
60
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
61
|
+
if ! CREDS_EVAL="$(node "$SCRIPT_DIR/resolve-account.mjs" --ssh --api)"; then
|
|
64
62
|
exit 1
|
|
65
63
|
fi
|
|
66
64
|
eval "$CREDS_EVAL"
|
|
65
|
+
echo "→ account: $ACCT"
|
|
67
66
|
|
|
68
67
|
# Harden every credentials-derived field BEFORE it reaches ssh/rsync/scp. rsync
|
|
69
68
|
# re-tokenizes its -e remote-shell string on whitespace (ignoring shell quotes),
|
|
@@ -25,15 +25,13 @@ done
|
|
|
25
25
|
[ -n "$DOMAIN" ] && [ -n "$TO" ] || { echo "Usage: pull-live.sh --domain <live-domain> --to <local-dir> [--docroot PATH]"; exit 1; }
|
|
26
26
|
case "$DOMAIN" in *[!a-z0-9.-]*|"") echo "Invalid --domain."; exit 1;; esac
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if
|
|
31
|
-
const q = (s) => "'"'"'" + String(s == null ? "" : s).replace(/'"'"'/g, "") + "'"'"'";
|
|
32
|
-
console.log(`H=${q(c.ssh.host)}; U=${q(c.ssh.user)}; P=${q(c.ssh.port||22)}; K=${q(c.ssh.keyPath)}`);
|
|
33
|
-
')"; then
|
|
28
|
+
# Shared resolver: folder pin / AWESOMATE_ACCOUNT / sole profile → ssh block.
|
|
29
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
30
|
+
if ! CREDS_EVAL="$(node "$SCRIPT_DIR/resolve-account.mjs" --ssh)"; then
|
|
34
31
|
exit 1
|
|
35
32
|
fi
|
|
36
33
|
eval "$CREDS_EVAL"
|
|
34
|
+
echo "→ account: $ACCT"
|
|
37
35
|
|
|
38
36
|
# Harden credentials-derived fields before they reach rsync/scp/ssh — rsync
|
|
39
37
|
# word-splits its -e remote-shell string, so whitespace here could inject ssh
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Shared account resolver for the awesomate-hosting skill scripts.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the MCP server's precedence exactly (mcp/src/config.ts):
|
|
6
|
+
* AWESOMATE_ACCOUNT env → .awesomate.json pin (walked up from cwd, stopping
|
|
7
|
+
* at $HOME) → sole profile → defaultProfile → legacy v1 top-level fields.
|
|
8
|
+
* A pin naming a missing profile is a hard error — never a silent fallback.
|
|
9
|
+
*
|
|
10
|
+
* Prints shell-eval assignments (single-quoted, quote-stripped values — same
|
|
11
|
+
* hardening contract the callers' charclass gates assume):
|
|
12
|
+
* node resolve-account.mjs --ssh # ACCT + H/U/P/K
|
|
13
|
+
* node resolve-account.mjs --api # ACCT + API/PAT
|
|
14
|
+
* Flags combine. Exit 1 = no resolvable account, exit 2 = no shell access.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { join, dirname, resolve } from 'node:path';
|
|
20
|
+
|
|
21
|
+
function readJson(path) {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
24
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
25
|
+
} catch { return null; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fail(msg, code = 1) { console.error(msg); process.exit(code); }
|
|
29
|
+
|
|
30
|
+
const credPath = join(homedir(), '.awesomate', 'credentials.json');
|
|
31
|
+
const file = readJson(credPath) ?? {};
|
|
32
|
+
|
|
33
|
+
const profiles = {};
|
|
34
|
+
if (file.profiles && typeof file.profiles === 'object') {
|
|
35
|
+
for (const [key, profile] of Object.entries(file.profiles)) {
|
|
36
|
+
if (profile && typeof profile.pat === 'string') profiles[key] = profile;
|
|
37
|
+
}
|
|
38
|
+
} else if (typeof file.pat === 'string' && file.pat) {
|
|
39
|
+
profiles[file.slug || 'default'] = file; // legacy v1 single-account file
|
|
40
|
+
}
|
|
41
|
+
const names = Object.keys(profiles);
|
|
42
|
+
|
|
43
|
+
function findPin() {
|
|
44
|
+
const explicit = process.env.AWESOMATE_PIN_FILE;
|
|
45
|
+
if (explicit) return existsSync(explicit) ? explicit : null;
|
|
46
|
+
const home = resolve(homedir());
|
|
47
|
+
let dir = resolve(process.cwd());
|
|
48
|
+
for (;;) {
|
|
49
|
+
const candidate = join(dir, '.awesomate.json');
|
|
50
|
+
if (existsSync(candidate)) return candidate;
|
|
51
|
+
if (dir === home) return null;
|
|
52
|
+
const parent = dirname(dir);
|
|
53
|
+
if (parent === dir) return null;
|
|
54
|
+
dir = parent;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let key = null;
|
|
59
|
+
if (process.env.AWESOMATE_ACCOUNT) {
|
|
60
|
+
key = process.env.AWESOMATE_ACCOUNT;
|
|
61
|
+
if (!profiles[key]) {
|
|
62
|
+
fail(`AWESOMATE_ACCOUNT="${key}" has no matching profile in ${credPath}. Available: ${names.join(', ') || '(none)'}.`);
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
const pinPath = findPin();
|
|
66
|
+
if (pinPath) {
|
|
67
|
+
const pin = readJson(pinPath);
|
|
68
|
+
key = pin && typeof pin.account === 'string' ? pin.account : null;
|
|
69
|
+
if (!key) fail(`${pinPath} exists but has no "account" field. Expected: {"account": "<slug>"}.`);
|
|
70
|
+
if (!profiles[key]) {
|
|
71
|
+
fail(
|
|
72
|
+
`This folder is pinned to account "${key}" (${pinPath}) but no matching profile exists in ${credPath}. ` +
|
|
73
|
+
`Available: ${names.join(', ') || '(none)'}. Connect "${key}" from hub.awesomate.ai/sites (logged in as it) or fix the pin.`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
} else if (names.length === 1) {
|
|
77
|
+
key = names[0];
|
|
78
|
+
} else if (file.defaultProfile && profiles[file.defaultProfile]) {
|
|
79
|
+
key = file.defaultProfile;
|
|
80
|
+
} else if (names.length === 0) {
|
|
81
|
+
fail('Not connected. Run the Connect Claude Code setup from hub.awesomate.ai/sites.');
|
|
82
|
+
} else {
|
|
83
|
+
fail(
|
|
84
|
+
`Multiple accounts are connected (${names.join(', ')}) and this folder isn't pinned to one. ` +
|
|
85
|
+
'Create .awesomate.json {"account": "<slug>"} in the project root or set AWESOMATE_ACCOUNT.',
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const profile = profiles[key];
|
|
91
|
+
const q = (s) => "'" + String(s == null ? '' : s).replace(/'/g, '') + "'";
|
|
92
|
+
const out = [`ACCT=${q(profile.slug ?? key)}`];
|
|
93
|
+
if (process.argv.includes('--ssh')) {
|
|
94
|
+
if (!profile.ssh) fail('Your plan does not include shell access (Support Plus+ required).', 2);
|
|
95
|
+
out.push(`H=${q(profile.ssh.host)}`, `U=${q(profile.ssh.user)}`, `P=${q(profile.ssh.port || 22)}`, `K=${q(profile.ssh.keyPath)}`);
|
|
96
|
+
}
|
|
97
|
+
if (process.argv.includes('--api')) {
|
|
98
|
+
out.push(`API=${q(profile.apiBase || 'https://hub.awesomate.ai')}`, `PAT=${q(profile.pat)}`);
|
|
99
|
+
}
|
|
100
|
+
console.log(out.join('; '));
|
|
@@ -8,12 +8,12 @@ set -euo pipefail
|
|
|
8
8
|
CRED="$HOME/.awesomate/credentials.json"
|
|
9
9
|
[ -f "$CRED" ] || { echo "Not connected. Run bootstrap first (Connect Claude Code on hub.awesomate.ai/sites)."; exit 1; }
|
|
10
10
|
|
|
11
|
-
#
|
|
12
|
-
eval
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
# Resolve WHICH account (folder pin / AWESOMATE_ACCOUNT / sole profile) and its
|
|
12
|
+
# ssh block via the shared resolver. Capture then eval so a resolver failure
|
|
13
|
+
# aborts with its own message instead of tripping set -u later.
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
if ! CREDS_EVAL="$(node "$SCRIPT_DIR/resolve-account.mjs" --ssh)"; then exit 1; fi
|
|
16
|
+
eval "$CREDS_EVAL"
|
|
17
|
+
echo "→ account: $ACCT" >&2
|
|
18
18
|
|
|
19
19
|
exec ssh -i "$K" -p "$P" -o StrictHostKeyChecking=accept-new "$U@$H" "$@"
|
|
@@ -20,7 +20,13 @@ promote back, and delete drafts — via the `awesomate_n8n_deploy` and
|
|
|
20
20
|
|
|
21
21
|
## 0. First run (every session)
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Run `awesomate_whoami` first — n8n tools act on whichever ACCOUNT this folder
|
|
24
|
+
resolves to (folder `.awesomate.json` pin / `AWESOMATE_ACCOUNT` / sole
|
|
25
|
+
profile; see the awesomate-hosting skill's multi-account section). If the slug
|
|
26
|
+
isn't the instance the user means, stop and fix the pin/connection before any
|
|
27
|
+
n8n work. Every tool response is stamped `account: <slug>` — watch it.
|
|
28
|
+
|
|
29
|
+
Then call `awesomate_n8n_context` once before any n8n work and cache the result:
|
|
24
30
|
|
|
25
31
|
- `consented: false` → give the user the `settingsUrl` link (Settings →
|
|
26
32
|
Privacy → "Allow Claude Code to Build n8n Workflows"), wait for them to
|
|
@@ -40,8 +46,11 @@ $env, `$json.body`, task-runner limits, activation semantics).
|
|
|
40
46
|
|
|
41
47
|
## 1. Reads
|
|
42
48
|
|
|
43
|
-
PAT as `Authorization: Bearer <pat>`
|
|
44
|
-
|
|
49
|
+
PAT as `Authorization: Bearer <pat>` against `apiBase`. Resolve the ACTIVE
|
|
50
|
+
profile's pat — `node ~/.claude/skills/awesomate-hosting/scripts/resolve-account.mjs --api`
|
|
51
|
+
— never read `credentials.json`'s top-level `pat` directly when a `profiles`
|
|
52
|
+
map exists (it mirrors the default profile, not necessarily this folder's
|
|
53
|
+
account). Never echo the PAT into the conversation.
|
|
45
54
|
|
|
46
55
|
| What | Endpoint |
|
|
47
56
|
|---|---|
|