@nearform/fling 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +40 -15
- package/dist/cli.js +391 -133
- package/dist/cli.js.map +4 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -373,6 +373,7 @@ var CommentAuthorSchema = z3.discriminatedUnion("kind", [
|
|
|
373
373
|
z3.object({ kind: z3.literal("guest"), name: z3.string() })
|
|
374
374
|
]);
|
|
375
375
|
var ANCHOR_SELECTOR_MAX = 512;
|
|
376
|
+
var ANCHOR_QUOTE_MAX = 500;
|
|
376
377
|
var px = z3.number().finite().min(-1e7).max(1e7);
|
|
377
378
|
var percent = z3.number().min(0).max(100);
|
|
378
379
|
var CommentAnchorSchema = z3.object({
|
|
@@ -387,7 +388,12 @@ var CommentAnchorSchema = z3.object({
|
|
|
387
388
|
scrollY: px.optional(),
|
|
388
389
|
vw: z3.number().finite().min(0).max(1e5).optional(),
|
|
389
390
|
vh: z3.number().finite().min(0).max(1e5).optional(),
|
|
390
|
-
hash: z3.string().max(COMMENT_PATH_MAX).optional()
|
|
391
|
+
hash: z3.string().max(COMMENT_PATH_MAX).optional(),
|
|
392
|
+
ew: percent.optional(),
|
|
393
|
+
eh: percent.optional(),
|
|
394
|
+
docW: z3.number().finite().min(0).max(1e7).optional(),
|
|
395
|
+
docH: z3.number().finite().min(0).max(1e7).optional(),
|
|
396
|
+
quote: z3.string().min(1).max(ANCHOR_QUOTE_MAX).optional()
|
|
391
397
|
});
|
|
392
398
|
var CommentSchema = z3.object({
|
|
393
399
|
id: z3.string(),
|
|
@@ -982,7 +988,10 @@ var ShellMessageSchema = z7.discriminatedUnion("type", [
|
|
|
982
988
|
z7.object({ ...tag, type: z7.literal("ready") }),
|
|
983
989
|
/** the frame is on `path` (normalised, see normalizeCommentPath) with fragment `hash` ('' or '#…') */
|
|
984
990
|
z7.object({ ...tag, type: z7.literal("location"), path, hash, title: z7.string().max(300) }),
|
|
991
|
+
/** a pin placed while placing: a click (a spot) or a drag (a box) */
|
|
985
992
|
z7.object({ ...tag, type: z7.literal("pinPlaced"), anchor: CommentAnchorSchema }),
|
|
993
|
+
/** the reviewer selected text in the page: an anchor on it, with its quote */
|
|
994
|
+
z7.object({ ...tag, type: z7.literal("textSelected"), anchor: CommentAnchorSchema }),
|
|
986
995
|
z7.object({ ...tag, type: z7.literal("pinCancelled") }),
|
|
987
996
|
z7.object({ ...tag, type: z7.literal("pinClicked"), id: z7.string() }),
|
|
988
997
|
/** this reviewer's pointer on the page; null when it left the frame */
|
|
@@ -1088,7 +1097,7 @@ function exitCodeFor(error) {
|
|
|
1088
1097
|
// package.json
|
|
1089
1098
|
var package_default = {
|
|
1090
1099
|
name: "@nearform/fling",
|
|
1091
|
-
version: "0.
|
|
1100
|
+
version: "0.5.1",
|
|
1092
1101
|
license: "MIT",
|
|
1093
1102
|
description: "Fling a folder of static files at a Fling server and get a link back. CLI + MCP server.",
|
|
1094
1103
|
type: "module",
|
|
@@ -1308,7 +1317,9 @@ var ENV = {
|
|
|
1308
1317
|
configDir: "FLING_CONFIG_DIR",
|
|
1309
1318
|
debug: "FLING_DEBUG",
|
|
1310
1319
|
/** Where to look for files given by name (the .mcpb extension sets it from its settings). */
|
|
1311
|
-
downloadsDir: "FLING_DOWNLOADS_DIR"
|
|
1320
|
+
downloadsDir: "FLING_DOWNLOADS_DIR",
|
|
1321
|
+
/** A server the local MCP server adds to the remembered ones on start (the .mcpb bakes it in). */
|
|
1322
|
+
addServer: "FLING_ADD_SERVER"
|
|
1312
1323
|
};
|
|
1313
1324
|
var serverEntrySchema = z9.object({
|
|
1314
1325
|
/** What the server said about itself at login. A copy this CLI can't read is just refetched. */
|
|
@@ -1327,6 +1338,9 @@ function fromEnv(value) {
|
|
|
1327
1338
|
const v = value?.trim();
|
|
1328
1339
|
return v && !v.startsWith("${") ? v : void 0;
|
|
1329
1340
|
}
|
|
1341
|
+
function serverToAdd(env = process.env) {
|
|
1342
|
+
return fromEnv(env[ENV.addServer]);
|
|
1343
|
+
}
|
|
1330
1344
|
function resolveConfigDir(env = process.env) {
|
|
1331
1345
|
return env[ENV.configDir] ?? join(env.HOME ?? homedir(), CONFIG_DIR_NAME);
|
|
1332
1346
|
}
|
|
@@ -2207,6 +2221,7 @@ async function runComments(ctx, name, opts) {
|
|
|
2207
2221
|
}
|
|
2208
2222
|
const state = c.resolvedAt ? chalk6.green(" \u2713 resolved") : "";
|
|
2209
2223
|
lines.push(`${chalk6.dim(c.id)}${state} ${describe(c, list.site.currentVersionId)}`);
|
|
2224
|
+
if (c.anchor?.quote) lines.push(chalk6.dim(` > ${c.anchor.quote}`));
|
|
2210
2225
|
lines.push(...c.body.split("\n").map((l) => ` ${l}`));
|
|
2211
2226
|
for (const r of replies) {
|
|
2212
2227
|
lines.push(` ${chalk6.dim("\u21B3")} ${describe(r, list.site.currentVersionId)}`);
|
|
@@ -3841,11 +3856,132 @@ async function findFiles(opts, find = {}) {
|
|
|
3841
3856
|
return { files, searched: roots, complete };
|
|
3842
3857
|
}
|
|
3843
3858
|
|
|
3859
|
+
// src/mcp-servers.ts
|
|
3860
|
+
function serverLabel(url) {
|
|
3861
|
+
const u = new URL(url);
|
|
3862
|
+
return `${u.host}${u.pathname}`.replace(/\/+$/, "");
|
|
3863
|
+
}
|
|
3864
|
+
function describeServer(s) {
|
|
3865
|
+
const facts = [
|
|
3866
|
+
s.instance?.name,
|
|
3867
|
+
s.signedIn ? s.email ? `signed in as ${s.email}` : "signed in" : "signed out",
|
|
3868
|
+
s.current ? "current: the user's usual one" : void 0
|
|
3869
|
+
].filter(Boolean);
|
|
3870
|
+
return `${serverLabel(s.url)} (${facts.join("; ")})`;
|
|
3871
|
+
}
|
|
3872
|
+
function knownServersOf(config) {
|
|
3873
|
+
return Object.entries(config.servers).map(([url, entry]) => ({
|
|
3874
|
+
url,
|
|
3875
|
+
instance: entry.instance,
|
|
3876
|
+
current: url === config.current,
|
|
3877
|
+
signedIn: Boolean(entry.apiKey),
|
|
3878
|
+
...entry.apiKey && entry.user?.email ? { email: entry.user.email } : {}
|
|
3879
|
+
}));
|
|
3880
|
+
}
|
|
3881
|
+
var listing = (servers) => servers.map((s) => `- ${describeServer(s)}`).join("\n");
|
|
3882
|
+
var ServerNeededError = class extends CliError {
|
|
3883
|
+
servers;
|
|
3884
|
+
constructor(servers, tool2) {
|
|
3885
|
+
super(
|
|
3886
|
+
`Not run: several Flings are set up on this computer, so ${tool2} needs \`server\`.
|
|
3887
|
+
${listing(servers)}
|
|
3888
|
+
Unless the conversation already says which Fling to use, ask the user (suggest their usual one, marked current). Then call ${tool2} again with \`server\` (e.g. "${serverLabel(servers[0]?.url ?? "https://fling.example.com")}"), and pass the same \`server\` on every call for the rest of the conversation.`
|
|
3889
|
+
);
|
|
3890
|
+
this.name = "ServerNeededError";
|
|
3891
|
+
this.servers = servers;
|
|
3892
|
+
}
|
|
3893
|
+
};
|
|
3894
|
+
function rememberedServer(config, input) {
|
|
3895
|
+
const servers = knownServersOf(config);
|
|
3896
|
+
const known = servers.length ? `The Flings set up on this computer:
|
|
3897
|
+
${listing(servers)}` : "No Fling is set up on this computer yet.";
|
|
3898
|
+
let match;
|
|
3899
|
+
try {
|
|
3900
|
+
match = matchServer(Object.keys(config.servers), input, { loose: true });
|
|
3901
|
+
} catch (error) {
|
|
3902
|
+
throw new CliError(error.message, { hint: known });
|
|
3903
|
+
}
|
|
3904
|
+
if (!match.remembered)
|
|
3905
|
+
throw new CliError(`"${input}" is not a Fling set up on this computer`, {
|
|
3906
|
+
hint: `${known}
|
|
3907
|
+
To add one, call fling_login with its address as \`server\`.`
|
|
3908
|
+
});
|
|
3909
|
+
return match.url;
|
|
3910
|
+
}
|
|
3911
|
+
var isPinned = (config) => config.serverUrlSource === "flag" || config.serverUrlSource === "env";
|
|
3912
|
+
async function within(promise, ms, fallback) {
|
|
3913
|
+
let timer;
|
|
3914
|
+
const late = new Promise((r) => {
|
|
3915
|
+
timer = setTimeout(() => r(fallback), ms);
|
|
3916
|
+
});
|
|
3917
|
+
try {
|
|
3918
|
+
return await Promise.race([promise.catch(() => fallback), late]);
|
|
3919
|
+
} finally {
|
|
3920
|
+
clearTimeout(timer);
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
var START_FETCH_MS = 5e3;
|
|
3924
|
+
async function addServerOnStart(store, address, fetch2) {
|
|
3925
|
+
if (!address) return;
|
|
3926
|
+
let url;
|
|
3927
|
+
try {
|
|
3928
|
+
url = normalizeServerUrl(address);
|
|
3929
|
+
} catch {
|
|
3930
|
+
return;
|
|
3931
|
+
}
|
|
3932
|
+
if (url in (await store.load()).servers) return;
|
|
3933
|
+
const instance = await within(fetchInstance(url, fetch2), START_FETCH_MS, null);
|
|
3934
|
+
await store.putServer(url, instance ? { instance } : {});
|
|
3935
|
+
}
|
|
3936
|
+
async function knownServers(store, fetch2, pin) {
|
|
3937
|
+
const resolved = await store.resolve({ serverUrl: pin });
|
|
3938
|
+
const config = await store.load();
|
|
3939
|
+
const all = knownServersOf(config);
|
|
3940
|
+
const servers = isPinned(resolved) ? resolved.serverUrl ? [
|
|
3941
|
+
all.find((s) => s.url === resolved.serverUrl) ?? {
|
|
3942
|
+
url: resolved.serverUrl,
|
|
3943
|
+
current: false,
|
|
3944
|
+
signedIn: Boolean(resolved.apiKey)
|
|
3945
|
+
}
|
|
3946
|
+
] : [] : all;
|
|
3947
|
+
return Promise.all(
|
|
3948
|
+
servers.map(async (s) => {
|
|
3949
|
+
if (s.instance) return s;
|
|
3950
|
+
const instance = await within(fetchInstance(s.url, fetch2), START_FETCH_MS, null);
|
|
3951
|
+
if (instance && s.url in config.servers)
|
|
3952
|
+
await store.putServer(s.url, { instance }, { makeCurrent: false }).catch(() => {
|
|
3953
|
+
});
|
|
3954
|
+
return { ...s, instance };
|
|
3955
|
+
})
|
|
3956
|
+
);
|
|
3957
|
+
}
|
|
3958
|
+
function serverArgDescription(servers) {
|
|
3959
|
+
const [only] = servers;
|
|
3960
|
+
if (servers.length > 1)
|
|
3961
|
+
return `Which Fling: ${servers.map((s) => serverLabel(s.url)).join(", ")} (an address, host or unique part of it). Required while several Flings are set up: the one settled on for this conversation, on every call`;
|
|
3962
|
+
return `Which Fling, by address or host (any set up on this computer; see fling_servers). Omit it to use ${only ? serverLabel(only.url) : "the one this computer is signed in to"}`;
|
|
3963
|
+
}
|
|
3964
|
+
function serverToolNote(servers) {
|
|
3965
|
+
if (servers.length < 2) return "";
|
|
3966
|
+
return ` Pass \`server\`: several Flings are set up (${servers.map((s) => serverLabel(s.url)).join(", ")}), and every call names the one settled on for this conversation.`;
|
|
3967
|
+
}
|
|
3968
|
+
function serversInstructions(servers) {
|
|
3969
|
+
if (servers.length < 2) return void 0;
|
|
3970
|
+
const usual = servers.find((s) => s.current);
|
|
3971
|
+
return [
|
|
3972
|
+
`Several Flings are set up on this computer:
|
|
3973
|
+
${listing(servers)}`,
|
|
3974
|
+
`Settle which one this conversation uses before anything else: from the user's words ("on ${serverLabel(servers[0]?.url ?? "")}", "on the ${servers[0]?.instance?.name ?? "other"} one") or, if they don't say, by asking them${usual ? ` (suggest ${serverLabel(usual.url)}, their usual one)` : ""}. Say which Fling you are using, and pass it as \`server\` on every call for the rest of the conversation; switch only when the user asks. Calls without \`server\` do not run while several Flings are set up. Each Fling has its own sites, sign-in and sites domain: give the user the links that come back, which name the Fling.`,
|
|
3975
|
+
"This list was read when this server started and may have changed: call fling_servers to see the Flings set up now. To sign in to a Fling, or add one, call fling_login with its address as `server`."
|
|
3976
|
+
].join("\n\n");
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3844
3979
|
// src/mcp.ts
|
|
3845
3980
|
var LOGIN_HINT = `Not logged in to Fling. Call fling_login (it opens the browser to sign in), or ask the user to run \`${NPX_COMMAND} login\` in a terminal (in Claude Code: \`! ${NPX_COMMAND} login\`), then try again.`;
|
|
3846
|
-
var NO_SERVER_HINT = `No Fling server is set up on this computer. Ask the user for their Fling's address
|
|
3981
|
+
var NO_SERVER_HINT = `No Fling server is set up on this computer. Ask the user for their Fling's address, then call fling_login with it as \`server\` (or ask them to run \`${NPX_COMMAND} login <url>\` in a terminal; in Claude Code: \`! ${NPX_COMMAND} login <url>\`), then try again.`;
|
|
3847
3982
|
var mcpLocalInstructions = (w, opts = {}) => [
|
|
3848
3983
|
`${w.name} publishes static sites (HTML/CSS/JS, images, documents) at ${w.sitesPattern}. This server runs on the user's computer.`,
|
|
3984
|
+
...[serversInstructions(opts.servers ?? [])].filter((p) => p !== void 0),
|
|
3849
3985
|
"Content you created in this conversation (an HTML page, a report, a small app): call fling_publish with every file inline (text as utf8, binary as base64). Never pass file paths from your sandbox (e.g. /mnt/user-data/outputs/\u2026, /sessions/\u2026) \u2014 this computer cannot see them \u2014 and never curl or upload from the sandbox.",
|
|
3850
3986
|
'A file or folder the user has on this computer (a downloaded .html, a .zip, a build folder): call fling_deploy with its path, or just its file name (e.g. "Pitch Deck.html") \u2014 it is looked up in Downloads, Desktop, Documents/Claude and Claude Desktop output folders. Use fling_find_files to see recent candidates when unsure which file the user means.',
|
|
3851
3987
|
"Confirm the site name with the user before publishing (fling_check_name shows whether it is free and, for a path, the generated name). Names are global, first come first served; when one is taken, offer the suggestions.",
|
|
@@ -3855,20 +3991,26 @@ var mcpLocalInstructions = (w, opts = {}) => [
|
|
|
3855
3991
|
`Sites are private (only ${w.privateAudience}) by default; only make one public if asked. Use fling_share for a link people who cannot sign in can open. ${opts.comments ? "Always give the user the links that come back: the review link first, as the one to share for feedback, then the site URL." : "Always give the user the URL that comes back."}`,
|
|
3856
3992
|
...opts.comments ? [COMMENTS_NOTE(w)] : []
|
|
3857
3993
|
].join("\n\n");
|
|
3858
|
-
var
|
|
3859
|
-
|
|
3860
|
-
description: def.description,
|
|
3861
|
-
inputSchema: def.inputSchema,
|
|
3862
|
-
annotations: def.annotations
|
|
3863
|
-
});
|
|
3994
|
+
var onFling = (url, instance) => `
|
|
3995
|
+
Fling: ${serverLabel(url)}${instance ? ` (${instance.name})` : ""}`;
|
|
3864
3996
|
function createMcpServer(deps = {}) {
|
|
3865
3997
|
const store = deps.store ?? createConfigStore();
|
|
3866
3998
|
const fetchImpl = deps.fetch ?? ((input, init) => fetch(input, init));
|
|
3867
3999
|
const cwd = deps.cwd ?? process.cwd();
|
|
3868
4000
|
const locate = deps.locate ?? { ...systemLocateOptions(), cwd };
|
|
3869
4001
|
const waitReady = deps.waitReady ?? createReadyWaiter();
|
|
3870
|
-
const
|
|
4002
|
+
const servers = deps.servers ?? [];
|
|
4003
|
+
const several = servers.length > 1;
|
|
4004
|
+
const wording = mcpWording(several ? null : servers[0]?.instance);
|
|
3871
4005
|
const mcpTools2 = mcpToolsFor(wording);
|
|
4006
|
+
const serverArg = z11.string().min(1).max(500).optional().describe(serverArgDescription(servers));
|
|
4007
|
+
const note = serverToolNote(servers);
|
|
4008
|
+
const serverTool = (def, description = def.description) => ({
|
|
4009
|
+
title: def.title,
|
|
4010
|
+
description: `${description}${note}`,
|
|
4011
|
+
inputSchema: { ...def.inputSchema, server: serverArg },
|
|
4012
|
+
annotations: def.annotations
|
|
4013
|
+
});
|
|
3872
4014
|
async function edgeReady(url, paths) {
|
|
3873
4015
|
const r = await waitReady(url, paths).catch(() => ({ ready: false, waitedMs: 0 }));
|
|
3874
4016
|
return {
|
|
@@ -3877,27 +4019,44 @@ function createMcpServer(deps = {}) {
|
|
|
3877
4019
|
Not reachable yet at ${url}: ${NOT_READY_NOTE}.`
|
|
3878
4020
|
};
|
|
3879
4021
|
}
|
|
3880
|
-
async function
|
|
3881
|
-
|
|
4022
|
+
async function choose(server2, tool2) {
|
|
4023
|
+
if (server2 !== void 0)
|
|
4024
|
+
return store.resolve({ serverUrl: rememberedServer(await store.load(), server2) });
|
|
4025
|
+
const resolved = await store.resolve({ serverUrl: deps.server });
|
|
4026
|
+
if (isPinned(resolved)) return resolved;
|
|
4027
|
+
const config = await store.load();
|
|
4028
|
+
const known = knownServersOf(config);
|
|
4029
|
+
if (known.length > 1) throw new ServerNeededError(known, tool2);
|
|
4030
|
+
const only = known[0];
|
|
4031
|
+
return !resolved.serverUrl && only ? store.resolve({ serverUrl: only.url }) : resolved;
|
|
4032
|
+
}
|
|
4033
|
+
async function target(server2, tool2) {
|
|
4034
|
+
const config = await choose(server2, tool2);
|
|
3882
4035
|
if (!config.serverUrl) throw new NoServerError("fling");
|
|
3883
4036
|
if (!config.apiKey) throw new CliError("Not logged in", { exitCode: EXIT_NOT_LOGGED_IN });
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
4037
|
+
const url = config.serverUrl;
|
|
4038
|
+
let instance;
|
|
4039
|
+
return {
|
|
4040
|
+
client: createApiClient({ serverUrl: url, apiKey: config.apiKey, fetch: fetchImpl }),
|
|
4041
|
+
url,
|
|
4042
|
+
config,
|
|
4043
|
+
instance: () => {
|
|
4044
|
+
instance ??= loadInstance(store, config, fetchImpl);
|
|
4045
|
+
return instance;
|
|
4046
|
+
}
|
|
4047
|
+
};
|
|
3889
4048
|
}
|
|
3890
|
-
async function run(op) {
|
|
4049
|
+
async function run(tool2, server2, op) {
|
|
3891
4050
|
try {
|
|
3892
|
-
const { summary, data } = await op(await
|
|
3893
|
-
return
|
|
4051
|
+
const { summary, data } = await op(await target(server2, tool2));
|
|
4052
|
+
return textResult(`${summary}
|
|
3894
4053
|
|
|
3895
|
-
${JSON.stringify(data, null, 2)}`
|
|
4054
|
+
${JSON.stringify(data, null, 2)}`);
|
|
3896
4055
|
} catch (error) {
|
|
3897
|
-
return
|
|
4056
|
+
return failure(error);
|
|
3898
4057
|
}
|
|
3899
4058
|
}
|
|
3900
|
-
async function deployFolder(
|
|
4059
|
+
async function deployFolder(t2, dir, opts) {
|
|
3901
4060
|
let local = await buildManifest(dir);
|
|
3902
4061
|
local.skipped.dotfiles.unshift(...opts.skippedDotfiles ?? []);
|
|
3903
4062
|
const skippedNote = describeSkipped(local.skipped).map((l) => `
|
|
@@ -3924,14 +4083,14 @@ ${l}.`).join("");
|
|
|
3924
4083
|
}
|
|
3925
4084
|
}
|
|
3926
4085
|
const result = await deploy(
|
|
3927
|
-
{ client:
|
|
4086
|
+
{ client: t2.client, fetch: fetchImpl },
|
|
3928
4087
|
{ dir, name: opts.name, manifest: local, spa: opts.spa, public: opts.public }
|
|
3929
4088
|
).catch((error) => {
|
|
3930
4089
|
if (!(error instanceof NameTakenError)) throw error;
|
|
3931
4090
|
const how = opts.nameGiven ? "" : ` (generated from ${opts.nameFrom ?? "the folder"})`;
|
|
3932
4091
|
const pick = error.suggestions.length ? ` Free alternatives: ${error.suggestions.join(", ")}.` : "";
|
|
3933
4092
|
throw new CliError(
|
|
3934
|
-
`Not deployed: the site name "${error.siteName}"${how} is taken by someone else.${pick} Ask the user which name to use (one of these or their own), then call ${opts.retryWith} again with \`name\`.`
|
|
4093
|
+
`Not deployed: the site name "${error.siteName}"${how} is taken by someone else on ${serverLabel(t2.url)}.${pick} Ask the user which name to use (one of these or their own), then call ${opts.retryWith} again with \`name\`.`
|
|
3935
4094
|
);
|
|
3936
4095
|
});
|
|
3937
4096
|
const what = result.status === "unchanged" ? "Unchanged \u2014 already live" : result.status === "repointed" ? `Restored version ${result.versionId}` : `Deployed ${result.fileCount} files (uploaded ${result.uploaded})`;
|
|
@@ -3942,19 +4101,20 @@ Warnings:
|
|
|
3942
4101
|
result.url,
|
|
3943
4102
|
local.files.map((f) => f.path)
|
|
3944
4103
|
);
|
|
3945
|
-
const instance =
|
|
4104
|
+
const instance = await t2.instance();
|
|
3946
4105
|
const review = instance && commentsEnabled(instance) ? reviewUrl(instance.appOrigin, result.name) : void 0;
|
|
3947
|
-
const
|
|
3948
|
-
const head = review ? `${what}${
|
|
4106
|
+
const listing2 = generatedIndex ? " \u2014 with a generated index.html listing the HTML pages" : "";
|
|
4107
|
+
const head = review ? `${what}${listing2}.
|
|
3949
4108
|
${shareForFeedback(review, instance?.name ?? "Fling")}
|
|
3950
|
-
Site: ${result.url} (${result.site.visibility})` : `${what}: ${result.url} (${result.site.visibility})${
|
|
4109
|
+
Site: ${result.url} (${result.site.visibility})` : `${what}: ${result.url} (${result.site.visibility})${listing2}`;
|
|
3951
4110
|
return {
|
|
3952
4111
|
kind: "deployed",
|
|
3953
|
-
summary: `${head}${warnings}${skippedNote}${edge.note}
|
|
4112
|
+
summary: `${head}${warnings}${skippedNote}${edge.note}${onFling(t2.url, instance)}
|
|
3954
4113
|
To check exactly which files the site has, call fling_files with name "${result.name}".`,
|
|
3955
4114
|
data: {
|
|
3956
4115
|
status: result.status,
|
|
3957
4116
|
ready: edge.ready,
|
|
4117
|
+
server: t2.url,
|
|
3958
4118
|
name: result.name,
|
|
3959
4119
|
nameGenerated: !opts.nameGiven,
|
|
3960
4120
|
versionId: result.versionId,
|
|
@@ -3970,7 +4130,8 @@ To check exactly which files the site has, call fling_files with name "${result.
|
|
|
3970
4130
|
{ name: "fling", version: VERSION },
|
|
3971
4131
|
{
|
|
3972
4132
|
instructions: mcpLocalInstructions(wording, {
|
|
3973
|
-
comments: commentsEnabled(
|
|
4133
|
+
comments: servers.some((s) => commentsEnabled(s.instance)),
|
|
4134
|
+
servers
|
|
3974
4135
|
})
|
|
3975
4136
|
}
|
|
3976
4137
|
);
|
|
@@ -3979,7 +4140,7 @@ To check exactly which files the site has, call fling_files with name "${result.
|
|
|
3979
4140
|
"fling_deploy",
|
|
3980
4141
|
{
|
|
3981
4142
|
title: "Deploy a file or folder from this computer",
|
|
3982
|
-
description: `Fling something that is on the user's computer and return its stable URL (${wording.sitesPattern}): a folder of static files (e.g. a build output like dist/), a single .html file (served as the site's index.html), a .zip of a site (extracted; a single top-level folder is unwrapped), or any other single file (.pdf, .svg, .png\u2026, served next to a generated page that shows it). \`path\` can be absolute, relative to the working directory, or just a file name ("Pitch Deck.html"): names are looked up, newest first, in Downloads, Desktop, Documents/Claude and Claude Desktop output folders, and the result says where the file was found. Never pass paths from your own sandbox (/mnt/user-data/\u2026, /sessions/\u2026): for content you created, use fling_publish. Without \`name\`, a name is generated: from the file name for files ("Pitch Deck (1).html" \u2192 pitch-deck), from the project and folder for folders (~/work/rocketpod/docs \u2192 rocketpod-docs). Site names are one global namespace, first come first served \u2014 confirm the name with the user first (fling_check_name with \`path\` shows the generated one). If the name is taken it fails with free suggestions \u2014 ask the user to pick one, then retry with \`name\`. If there is no index.html but there are HTML pages, it does not deploy: ask the user whether to generate an index page listing them (\`listHtml: true\`) or deploy without one (\`listHtml: false\`), then retry. Re-deploying identical content is a no-op. Sites are private (only ${wording.privateAudience}) unless \`public\` is set.
|
|
4143
|
+
description: `Fling something that is on the user's computer and return its stable URL (${wording.sitesPattern}): a folder of static files (e.g. a build output like dist/), a single .html file (served as the site's index.html), a .zip of a site (extracted; a single top-level folder is unwrapped), or any other single file (.pdf, .svg, .png\u2026, served next to a generated page that shows it). \`path\` can be absolute, relative to the working directory, or just a file name ("Pitch Deck.html"): names are looked up, newest first, in Downloads, Desktop, Documents/Claude and Claude Desktop output folders, and the result says where the file was found. Never pass paths from your own sandbox (/mnt/user-data/\u2026, /sessions/\u2026): for content you created, use fling_publish. Without \`name\`, a name is generated: from the file name for files ("Pitch Deck (1).html" \u2192 pitch-deck), from the project and folder for folders (~/work/rocketpod/docs \u2192 rocketpod-docs). Site names are one global namespace, first come first served \u2014 confirm the name with the user first (fling_check_name with \`path\` shows the generated one). If the name is taken it fails with free suggestions \u2014 ask the user to pick one, then retry with \`name\`. If there is no index.html but there are HTML pages, it does not deploy: ask the user whether to generate an index page listing them (\`listHtml: true\`) or deploy without one (\`listHtml: false\`), then retry. Re-deploying identical content is a no-op. Sites are private (only ${wording.privateAudience}) unless \`public\` is set. ${NEVER_PUBLISHED}${note}`,
|
|
3983
4144
|
inputSchema: {
|
|
3984
4145
|
path: z11.string().min(1).describe(
|
|
3985
4146
|
"File or folder on this computer: absolute, relative to the working directory, or just its name (looked up in Downloads, Desktop and Claude output folders)"
|
|
@@ -3989,16 +4150,17 @@ To check exactly which files the site has, call fling_files with name "${result.
|
|
|
3989
4150
|
),
|
|
3990
4151
|
spa: spa2,
|
|
3991
4152
|
public: isPublic,
|
|
3992
|
-
listHtml: listHtml2
|
|
4153
|
+
listHtml: listHtml2,
|
|
4154
|
+
server: serverArg
|
|
3993
4155
|
},
|
|
3994
4156
|
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
3995
4157
|
},
|
|
3996
|
-
(args) => run(async (
|
|
4158
|
+
(args) => run("fling_deploy", args.server, async (t2) => {
|
|
3997
4159
|
const located = await resolveInput(args.path, locate);
|
|
3998
4160
|
const prepared = await prepareInput(located.path);
|
|
3999
4161
|
try {
|
|
4000
4162
|
const name = args.name !== void 0 ? resolveSiteName(prepared.dir, args.name) : prepared.defaultName;
|
|
4001
|
-
const outcome = await deployFolder(
|
|
4163
|
+
const outcome = await deployFolder(t2, prepared.dir, {
|
|
4002
4164
|
name,
|
|
4003
4165
|
nameGiven: args.name !== void 0,
|
|
4004
4166
|
nameFrom: prepared.kind === "folder" ? "the folder" : "the file name",
|
|
@@ -4052,33 +4214,31 @@ ${lines.join("\n")}` : `Nothing found from the last ${args.days ?? 14} days${arg
|
|
|
4052
4214
|
searched: searched.map((r) => ({ label: r.label, path: r.path })),
|
|
4053
4215
|
...complete ? {} : { truncated: true }
|
|
4054
4216
|
};
|
|
4055
|
-
return {
|
|
4056
|
-
content: [{ type: "text", text: `${summary}
|
|
4217
|
+
return textResult(`${summary}
|
|
4057
4218
|
|
|
4058
|
-
${JSON.stringify(data, null, 2)}`
|
|
4059
|
-
};
|
|
4219
|
+
${JSON.stringify(data, null, 2)}`);
|
|
4060
4220
|
} catch (error) {
|
|
4061
|
-
return
|
|
4221
|
+
return failure(error);
|
|
4062
4222
|
}
|
|
4063
4223
|
}
|
|
4064
4224
|
);
|
|
4065
4225
|
const publishDef = mcpTools2.publish;
|
|
4066
4226
|
server.registerTool(
|
|
4067
4227
|
publishDef.name,
|
|
4068
|
-
|
|
4069
|
-
|
|
4228
|
+
serverTool(
|
|
4229
|
+
publishDef,
|
|
4070
4230
|
// This server has no chunked upload tools; big sites go through fling_deploy instead.
|
|
4071
|
-
|
|
4231
|
+
publishDef.description.replace(
|
|
4072
4232
|
/\(whole site, up to [^;]*; bigger sites: fling_upload_begin \/ fling_upload_file \/ fling_upload_finish\)/,
|
|
4073
4233
|
"(whole site; for a big site that is already on this computer, use fling_deploy with its folder)"
|
|
4074
4234
|
)
|
|
4075
|
-
|
|
4076
|
-
(args) => run(async (
|
|
4235
|
+
),
|
|
4236
|
+
(args) => run(publishDef.name, args.server, async (t2) => {
|
|
4077
4237
|
const name = resolveSiteName(".", args.name);
|
|
4078
4238
|
const tmp = await mkdtemp3(join6(tmpdir3(), "fling-publish-"));
|
|
4079
4239
|
try {
|
|
4080
4240
|
const { skippedDotfiles } = await writeInlineFiles(tmp, args.files);
|
|
4081
|
-
|
|
4241
|
+
return await deployFolder(t2, tmp, {
|
|
4082
4242
|
skippedDotfiles,
|
|
4083
4243
|
name,
|
|
4084
4244
|
nameGiven: true,
|
|
@@ -4087,7 +4247,6 @@ ${JSON.stringify(data, null, 2)}` }]
|
|
|
4087
4247
|
listHtml: args.listHtml,
|
|
4088
4248
|
retryWith: "fling_publish"
|
|
4089
4249
|
});
|
|
4090
|
-
return outcome;
|
|
4091
4250
|
} finally {
|
|
4092
4251
|
await rm2(tmp, { recursive: true, force: true });
|
|
4093
4252
|
}
|
|
@@ -4096,14 +4255,15 @@ ${JSON.stringify(data, null, 2)}` }]
|
|
|
4096
4255
|
server.registerTool(
|
|
4097
4256
|
"fling_check_name",
|
|
4098
4257
|
{
|
|
4099
|
-
...
|
|
4100
|
-
description: `${mcpTools2.checkName.description} Pass \`path\` instead of \`name\` to see the name fling_deploy would generate for a file or folder on this computer (same lookup as fling_deploy), so you can confirm it with the user
|
|
4258
|
+
...serverTool(mcpTools2.checkName),
|
|
4259
|
+
description: `${mcpTools2.checkName.description} Pass \`path\` instead of \`name\` to see the name fling_deploy would generate for a file or folder on this computer (same lookup as fling_deploy), so you can confirm it with the user.${note}`,
|
|
4101
4260
|
inputSchema: {
|
|
4102
4261
|
name: z11.string().min(1).max(200).optional().describe("Site name to check"),
|
|
4103
|
-
path: z11.string().optional().describe("File or folder whose generated default name to check (path or file name)")
|
|
4262
|
+
path: z11.string().optional().describe("File or folder whose generated default name to check (path or file name)"),
|
|
4263
|
+
server: serverArg
|
|
4104
4264
|
}
|
|
4105
4265
|
},
|
|
4106
|
-
(args) => run(async (
|
|
4266
|
+
(args) => run("fling_check_name", args.server, async (t2) => {
|
|
4107
4267
|
if (args.name === void 0 && args.path === void 0)
|
|
4108
4268
|
throw new CliError("Pass `name` or `path`");
|
|
4109
4269
|
let name = args.name;
|
|
@@ -4112,8 +4272,8 @@ ${JSON.stringify(data, null, 2)}` }]
|
|
|
4112
4272
|
source = await resolveInput(args.path, locate);
|
|
4113
4273
|
name = defaultNameFor(source.path, source.isDirectory);
|
|
4114
4274
|
}
|
|
4115
|
-
const check2 = await checkName(
|
|
4116
|
-
const base = check2.status === "invalid" ? null : await
|
|
4275
|
+
const check2 = await checkName(t2.client, name);
|
|
4276
|
+
const base = check2.status === "invalid" ? null : sitesBaseOf(await t2.instance());
|
|
4117
4277
|
const url = base ? siteUrlFor(name, base) : null;
|
|
4118
4278
|
const suggestions = check2.suggestions.length ? ` Suggestions: ${check2.suggestions.join(", ")}.` : "";
|
|
4119
4279
|
const summary = {
|
|
@@ -4127,59 +4287,77 @@ ${JSON.stringify(data, null, 2)}` }]
|
|
|
4127
4287
|
data: {
|
|
4128
4288
|
...check2,
|
|
4129
4289
|
url,
|
|
4290
|
+
server: t2.url,
|
|
4130
4291
|
...source ? { generated: true, source: { path: source.path, foundIn: source.foundIn } } : {}
|
|
4131
4292
|
}
|
|
4132
4293
|
};
|
|
4133
4294
|
})
|
|
4134
4295
|
);
|
|
4135
|
-
|
|
4296
|
+
const pendingLogins = /* @__PURE__ */ new Map();
|
|
4136
4297
|
server.registerTool(
|
|
4137
4298
|
"fling_login",
|
|
4138
4299
|
{
|
|
4139
4300
|
title: "Sign in to Fling",
|
|
4140
|
-
description: `Sign the user in to ${wording.name}: opens their browser to sign in with ${wording.signIn} and stores the key on this computer (shared with the fling CLI). Waits up to ~30 seconds; if sign-in is not finished by then, it keeps waiting in the background for up to 5 minutes \u2014 tell the user to finish signing in in the browser, then retry what you were doing. Call it when another tool says the user is not logged in
|
|
4141
|
-
inputSchema: {
|
|
4301
|
+
description: `Sign the user in to ${several ? "a Fling" : wording.name}: opens their browser to sign in with ${wording.signIn} and stores the key on this computer (shared with the fling CLI). Waits up to ~30 seconds; if sign-in is not finished by then, it keeps waiting in the background for up to 5 minutes \u2014 tell the user to finish signing in in the browser, then retry what you were doing. Call it when another tool says the user is not logged in. Pass \`server\` (its address) to sign in to another Fling, including one not set up on this computer yet: it is remembered, and becomes the current one only when none is.` + note,
|
|
4302
|
+
inputSchema: {
|
|
4303
|
+
server: z11.string().min(1).max(500).optional().describe(
|
|
4304
|
+
`The Fling to sign in to: its address (e.g. "https://fling.example.com"), or the host of one set up on this computer. ${several ? `Required while several Flings are set up (${servers.map((s) => serverLabel(s.url)).join(", ")})` : `Omit it to use ${servers[0] ? serverLabel(servers[0].url) : "the one this computer knows"}`}`
|
|
4305
|
+
)
|
|
4306
|
+
},
|
|
4142
4307
|
annotations: { readOnlyHint: false, openWorldHint: true }
|
|
4143
4308
|
},
|
|
4144
|
-
async () => {
|
|
4309
|
+
async (args) => {
|
|
4145
4310
|
try {
|
|
4146
|
-
|
|
4147
|
-
|
|
4311
|
+
let serverUrl;
|
|
4312
|
+
if (args.server !== void 0) {
|
|
4313
|
+
const config2 = await store.load();
|
|
4314
|
+
try {
|
|
4315
|
+
serverUrl = matchServer(Object.keys(config2.servers), args.server, { loose: true }).url;
|
|
4316
|
+
} catch (error) {
|
|
4317
|
+
const known = knownServersOf(config2).map((s) => `- ${describeServer(s)}`);
|
|
4318
|
+
throw new CliError(error.message, {
|
|
4319
|
+
hint: known.length ? `The Flings set up on this computer:
|
|
4320
|
+
${known.join("\n")}` : ""
|
|
4321
|
+
});
|
|
4322
|
+
}
|
|
4323
|
+
} else {
|
|
4324
|
+
serverUrl = (await choose(void 0, "fling_login")).serverUrl;
|
|
4325
|
+
}
|
|
4148
4326
|
if (!serverUrl) throw new NoServerError("fling");
|
|
4149
|
-
|
|
4327
|
+
const config = await store.resolve({ serverUrl });
|
|
4328
|
+
let login = pendingLogins.get(serverUrl);
|
|
4329
|
+
if (config.apiKey && !login) {
|
|
4150
4330
|
const me = await whoami(
|
|
4151
|
-
createApiClient({
|
|
4152
|
-
serverUrl,
|
|
4153
|
-
apiKey: config.apiKey,
|
|
4154
|
-
fetch: fetchImpl
|
|
4155
|
-
})
|
|
4331
|
+
createApiClient({ serverUrl, apiKey: config.apiKey, fetch: fetchImpl })
|
|
4156
4332
|
).catch(() => null);
|
|
4157
|
-
if (me) return textResult(`Already signed in to ${
|
|
4333
|
+
if (me) return textResult(`Already signed in to ${serverUrl} as ${me.email}.`);
|
|
4158
4334
|
}
|
|
4159
|
-
if (!
|
|
4160
|
-
const
|
|
4335
|
+
if (!login) {
|
|
4336
|
+
const started = {
|
|
4161
4337
|
done: Promise.resolve()
|
|
4162
4338
|
};
|
|
4163
|
-
|
|
4339
|
+
started.done = loginFlow({
|
|
4164
4340
|
serverUrl,
|
|
4165
4341
|
store,
|
|
4166
4342
|
log: silentLogger,
|
|
4167
4343
|
open: deps.open ?? (async (url) => (await import("open")).default(url)),
|
|
4168
4344
|
fetch: fetchImpl,
|
|
4169
4345
|
timeoutMs: deps.loginTimeoutMs,
|
|
4170
|
-
// Signing in to
|
|
4346
|
+
// Signing in to another Fling leaves the CLI's current server as it is.
|
|
4171
4347
|
makeCurrent: !config.current || config.current === serverUrl,
|
|
4172
4348
|
onAuthUrl: (url) => {
|
|
4173
|
-
|
|
4349
|
+
started.url = url;
|
|
4174
4350
|
}
|
|
4175
4351
|
});
|
|
4176
|
-
|
|
4352
|
+
const key = serverUrl;
|
|
4353
|
+
started.done.catch(() => {
|
|
4177
4354
|
}).finally(() => {
|
|
4178
|
-
if (
|
|
4355
|
+
if (pendingLogins.get(key) === started) pendingLogins.delete(key);
|
|
4179
4356
|
});
|
|
4180
|
-
|
|
4357
|
+
pendingLogins.set(serverUrl, started);
|
|
4358
|
+
login = started;
|
|
4181
4359
|
}
|
|
4182
|
-
const current =
|
|
4360
|
+
const current = login;
|
|
4183
4361
|
const waited = await Promise.race([
|
|
4184
4362
|
current.done.then(
|
|
4185
4363
|
(r) => ({ ok: true, r }),
|
|
@@ -4192,34 +4370,82 @@ ${JSON.stringify(data, null, 2)}` }]
|
|
|
4192
4370
|
`Finish signing in in your browser, then try again. (The sign-in page is open; if it did not open, visit: ${current.url ?? "the Fling site"} \u2014 this stays valid for 5 minutes.)`
|
|
4193
4371
|
);
|
|
4194
4372
|
if (!waited.ok) throw waited.e;
|
|
4195
|
-
return textResult(
|
|
4373
|
+
return textResult(
|
|
4374
|
+
`Signed in as ${waited.r.user.email} (${waited.r.serverUrl}, ${waited.r.instance.name}).`
|
|
4375
|
+
);
|
|
4376
|
+
} catch (error) {
|
|
4377
|
+
return failure(error);
|
|
4378
|
+
}
|
|
4379
|
+
}
|
|
4380
|
+
);
|
|
4381
|
+
server.registerTool(
|
|
4382
|
+
"fling_servers",
|
|
4383
|
+
{
|
|
4384
|
+
title: "List the Flings on this computer",
|
|
4385
|
+
description: `List the Flings set up on this computer, read now: each one's address, name, who is signed in (or "signed out") and which is current, the user's usual one. Call it when unsure which Fling the user means or which exist; the list may have changed since this conversation started. Pass \`use\` to make one the current Fling, the same as running \`${NPX_COMMAND} use <server>\`: it changes the default of the fling CLI too, so only do it when the user asks. It does not replace passing \`server\` to the other tools while several Flings are set up.`,
|
|
4386
|
+
inputSchema: {
|
|
4387
|
+
use: z11.string().min(1).max(500).optional().describe(
|
|
4388
|
+
"Make this Fling current (address, host or unique part of it), like `fling use`"
|
|
4389
|
+
)
|
|
4390
|
+
},
|
|
4391
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false }
|
|
4392
|
+
},
|
|
4393
|
+
async (args) => {
|
|
4394
|
+
try {
|
|
4395
|
+
let switched = "";
|
|
4396
|
+
if (args.use !== void 0) {
|
|
4397
|
+
const url = rememberedServer(await store.load(), args.use);
|
|
4398
|
+
await store.setCurrent(url);
|
|
4399
|
+
switched = `Now using ${serverLabel(url)} as the current Fling (as \`fling use\` does).
|
|
4400
|
+
`;
|
|
4401
|
+
}
|
|
4402
|
+
const config = await store.load();
|
|
4403
|
+
const known = knownServersOf(config);
|
|
4404
|
+
const pinned = await store.resolve({ serverUrl: deps.server });
|
|
4405
|
+
const summary = known.length ? `${switched}${known.length} Fling(s) set up on this computer:
|
|
4406
|
+
${known.map((s) => `- ${describeServer(s)}`).join("\n")}` + (isPinned(pinned) && pinned.serverUrl ? `
|
|
4407
|
+
Calls without \`server\` go to ${serverLabel(pinned.serverUrl)} (set by this server's configuration).` : known.length > 1 ? "\nPass `server` on every call: the Fling settled on for this conversation." : "") : NO_SERVER_HINT;
|
|
4408
|
+
const data = {
|
|
4409
|
+
current: config.current ?? null,
|
|
4410
|
+
servers: known.map((s) => ({
|
|
4411
|
+
url: s.url,
|
|
4412
|
+
host: serverLabel(s.url),
|
|
4413
|
+
instance: s.instance?.name ?? null,
|
|
4414
|
+
signedIn: s.signedIn,
|
|
4415
|
+
email: s.email ?? null,
|
|
4416
|
+
current: s.current
|
|
4417
|
+
}))
|
|
4418
|
+
};
|
|
4419
|
+
return textResult(`${summary}
|
|
4420
|
+
|
|
4421
|
+
${JSON.stringify(data, null, 2)}`);
|
|
4196
4422
|
} catch (error) {
|
|
4197
|
-
return
|
|
4423
|
+
return failure(error);
|
|
4198
4424
|
}
|
|
4199
4425
|
}
|
|
4200
4426
|
);
|
|
4201
4427
|
const t = mcpTools2;
|
|
4202
4428
|
server.registerTool(
|
|
4203
4429
|
t.list.name,
|
|
4204
|
-
|
|
4205
|
-
(args) => run(async (
|
|
4206
|
-
const sites = await listSites(
|
|
4207
|
-
return { summary: `${sites.length} site(s)`, data: { sites } };
|
|
4430
|
+
serverTool(t.list),
|
|
4431
|
+
(args) => run(t.list.name, args.server, async ({ client, url }) => {
|
|
4432
|
+
const sites = await listSites(client, { archived: args.archived });
|
|
4433
|
+
return { summary: `${sites.length} site(s) on ${serverLabel(url)}`, data: { sites } };
|
|
4208
4434
|
})
|
|
4209
4435
|
);
|
|
4210
4436
|
server.registerTool(
|
|
4211
4437
|
t.info.name,
|
|
4212
|
-
|
|
4213
|
-
(args) => run(async (
|
|
4214
|
-
const site = await getSite(
|
|
4438
|
+
serverTool(t.info),
|
|
4439
|
+
(args) => run(t.info.name, args.server, async ({ client }) => {
|
|
4440
|
+
const site = await getSite(client, args.name);
|
|
4215
4441
|
return { summary: `${site.name}: ${site.url}`, data: site };
|
|
4216
4442
|
})
|
|
4217
4443
|
);
|
|
4218
4444
|
server.registerTool(
|
|
4219
4445
|
t.files.name,
|
|
4220
|
-
|
|
4221
|
-
(args) => run(async (
|
|
4222
|
-
const result = await versionFiles(
|
|
4446
|
+
serverTool(t.files),
|
|
4447
|
+
(args) => run(t.files.name, args.server, async ({ client }) => {
|
|
4448
|
+
const result = await versionFiles(client, args.name, args.versionId);
|
|
4223
4449
|
return {
|
|
4224
4450
|
summary: describeVersionFiles(result.name, result, {
|
|
4225
4451
|
current: result.current,
|
|
@@ -4231,15 +4457,15 @@ ${JSON.stringify(data, null, 2)}` }]
|
|
|
4231
4457
|
);
|
|
4232
4458
|
server.registerTool(
|
|
4233
4459
|
t.share.name,
|
|
4234
|
-
|
|
4235
|
-
(args) => run(async (
|
|
4460
|
+
serverTool(t.share),
|
|
4461
|
+
(args) => run(t.share.name, args.server, async ({ client }) => {
|
|
4236
4462
|
if (args.refresh && args.revoke) throw new CliError("Use either refresh or revoke, not both");
|
|
4237
|
-
const result = await share(
|
|
4463
|
+
const result = await share(client, args.name, args);
|
|
4238
4464
|
if (result.action === "revoked")
|
|
4239
4465
|
return { summary: `Share link for ${args.name} revoked`, data: result };
|
|
4240
4466
|
let edge = { ready: true, note: "" };
|
|
4241
4467
|
if (result.action !== "existing") {
|
|
4242
|
-
const files = await versionFiles(
|
|
4468
|
+
const files = await versionFiles(client, args.name).catch(() => null);
|
|
4243
4469
|
edge = await edgeReady(result.shareUrl, files?.files.map((f) => f.path) ?? []);
|
|
4244
4470
|
}
|
|
4245
4471
|
return {
|
|
@@ -4250,78 +4476,86 @@ ${JSON.stringify(data, null, 2)}` }]
|
|
|
4250
4476
|
);
|
|
4251
4477
|
server.registerTool(
|
|
4252
4478
|
t.setVisibility.name,
|
|
4253
|
-
|
|
4254
|
-
(args) => run(async (
|
|
4255
|
-
const site = await setVisibility(
|
|
4479
|
+
serverTool(t.setVisibility),
|
|
4480
|
+
(args) => run(t.setVisibility.name, args.server, async ({ client }) => {
|
|
4481
|
+
const site = await setVisibility(client, args.name, args.visibility);
|
|
4256
4482
|
return { summary: `${site.name} is now ${site.visibility}: ${site.url}`, data: site };
|
|
4257
4483
|
})
|
|
4258
4484
|
);
|
|
4259
4485
|
server.registerTool(
|
|
4260
4486
|
t.archive.name,
|
|
4261
|
-
|
|
4262
|
-
(args) => run(async (
|
|
4263
|
-
const site = await archiveSite(
|
|
4487
|
+
serverTool(t.archive),
|
|
4488
|
+
(args) => run(t.archive.name, args.server, async ({ client }) => {
|
|
4489
|
+
const site = await archiveSite(client, args.name, { unarchive: args.unarchive });
|
|
4264
4490
|
return { summary: `${site.name} ${args.unarchive ? "restored" : "archived"}`, data: site };
|
|
4265
4491
|
})
|
|
4266
4492
|
);
|
|
4267
4493
|
server.registerTool(
|
|
4268
4494
|
t.delete.name,
|
|
4269
|
-
|
|
4270
|
-
(args) => run(async (
|
|
4271
|
-
const result = await deleteSite(
|
|
4272
|
-
return { summary: `${args.name} deleted`, data: result };
|
|
4495
|
+
serverTool(t.delete),
|
|
4496
|
+
(args) => run(t.delete.name, args.server, async ({ client, url }) => {
|
|
4497
|
+
const result = await deleteSite(client, args.name);
|
|
4498
|
+
return { summary: `${args.name} deleted from ${serverLabel(url)}`, data: result };
|
|
4273
4499
|
})
|
|
4274
4500
|
);
|
|
4275
4501
|
server.registerTool(
|
|
4276
4502
|
t.rollback.name,
|
|
4277
|
-
|
|
4278
|
-
(args) => run(async (
|
|
4279
|
-
const site = await rollback(
|
|
4503
|
+
serverTool(t.rollback),
|
|
4504
|
+
(args) => run(t.rollback.name, args.server, async ({ client }) => {
|
|
4505
|
+
const site = await rollback(client, args.name, args.versionId);
|
|
4280
4506
|
return {
|
|
4281
4507
|
summary: `${site.name} now serves version ${site.currentVersionId}: ${site.url}`,
|
|
4282
4508
|
data: site
|
|
4283
4509
|
};
|
|
4284
4510
|
})
|
|
4285
4511
|
);
|
|
4286
|
-
if (
|
|
4287
|
-
|
|
4288
|
-
|
|
4512
|
+
if (servers.some((s) => commentsEnabled(s.instance))) {
|
|
4513
|
+
async function reviewOn(target2) {
|
|
4514
|
+
const instance = await target2.instance();
|
|
4515
|
+
if (instance?.capabilities.comments === false)
|
|
4516
|
+
throw new CliError(`${instance.name} (${serverLabel(target2.url)}) has comments turned off`);
|
|
4517
|
+
const appOrigin = instance?.appOrigin ?? target2.url;
|
|
4518
|
+
return {
|
|
4519
|
+
review: (name) => reviewUrl(appOrigin, name),
|
|
4520
|
+
fling: onFling(target2.url, instance)
|
|
4521
|
+
};
|
|
4522
|
+
}
|
|
4289
4523
|
server.registerTool(
|
|
4290
4524
|
t.commentsList.name,
|
|
4291
|
-
|
|
4292
|
-
(args) => run(async (
|
|
4293
|
-
const
|
|
4525
|
+
serverTool(t.commentsList),
|
|
4526
|
+
(args) => run(t.commentsList.name, args.server, async (target2) => {
|
|
4527
|
+
const { review, fling } = await reviewOn(target2);
|
|
4528
|
+
const list = await listComments(target2.client, args.name, { path: args.path });
|
|
4294
4529
|
const reviewPage2 = review(list.site.name);
|
|
4295
4530
|
return {
|
|
4296
|
-
summary: describeComments(list, {
|
|
4297
|
-
includeResolved: args.includeResolved,
|
|
4298
|
-
reviewUrl: reviewPage2
|
|
4299
|
-
}),
|
|
4531
|
+
summary: `${describeComments(list, { includeResolved: args.includeResolved, reviewUrl: reviewPage2 })}${fling}`,
|
|
4300
4532
|
data: { ...list, reviewUrl: reviewPage2 }
|
|
4301
4533
|
};
|
|
4302
4534
|
})
|
|
4303
4535
|
);
|
|
4304
4536
|
server.registerTool(
|
|
4305
4537
|
t.commentAdd.name,
|
|
4306
|
-
|
|
4307
|
-
(args) => run(async (
|
|
4308
|
-
const
|
|
4538
|
+
serverTool(t.commentAdd),
|
|
4539
|
+
(args) => run(t.commentAdd.name, args.server, async (target2) => {
|
|
4540
|
+
const { review, fling } = await reviewOn(target2);
|
|
4541
|
+
const comment = await addComment(target2.client, args.name, {
|
|
4309
4542
|
body: args.body,
|
|
4310
4543
|
path: args.path,
|
|
4311
4544
|
parentId: args.replyTo
|
|
4312
4545
|
});
|
|
4313
4546
|
const what = comment.parentId ? `Replied to thread ${comment.parentId}` : "Comment added";
|
|
4314
4547
|
return {
|
|
4315
|
-
summary: `${what} on ${args.name} (${comment.path}, id ${comment.id}). Review page: ${review(args.name)}`,
|
|
4548
|
+
summary: `${what} on ${args.name} (${comment.path}, id ${comment.id}). Review page: ${review(args.name)}${fling}`,
|
|
4316
4549
|
data: comment
|
|
4317
4550
|
};
|
|
4318
4551
|
})
|
|
4319
4552
|
);
|
|
4320
4553
|
server.registerTool(
|
|
4321
4554
|
t.commentResolve.name,
|
|
4322
|
-
|
|
4323
|
-
(args) => run(async (
|
|
4324
|
-
|
|
4555
|
+
serverTool(t.commentResolve),
|
|
4556
|
+
(args) => run(t.commentResolve.name, args.server, async (target2) => {
|
|
4557
|
+
await reviewOn(target2);
|
|
4558
|
+
const comment = await resolveComment(target2.client, args.name, args.id, !args.reopen);
|
|
4325
4559
|
return {
|
|
4326
4560
|
summary: `Thread ${comment.id} on ${args.name} ${args.reopen ? "reopened" : "resolved"}`,
|
|
4327
4561
|
data: comment
|
|
@@ -4330,22 +4564,24 @@ ${JSON.stringify(data, null, 2)}` }]
|
|
|
4330
4564
|
);
|
|
4331
4565
|
server.registerTool(
|
|
4332
4566
|
t.commentDelete.name,
|
|
4333
|
-
|
|
4334
|
-
(args) => run(async (
|
|
4335
|
-
|
|
4567
|
+
serverTool(t.commentDelete),
|
|
4568
|
+
(args) => run(t.commentDelete.name, args.server, async (target2) => {
|
|
4569
|
+
await reviewOn(target2);
|
|
4570
|
+
const result = await deleteComment(target2.client, args.name, args.id);
|
|
4336
4571
|
return { summary: `Comment ${args.id} on ${args.name} deleted`, data: result };
|
|
4337
4572
|
})
|
|
4338
4573
|
);
|
|
4339
4574
|
}
|
|
4340
4575
|
server.registerTool(
|
|
4341
4576
|
t.whoami.name,
|
|
4342
|
-
|
|
4343
|
-
() => run(async (
|
|
4344
|
-
const me = await whoami(
|
|
4345
|
-
const
|
|
4577
|
+
serverTool(t.whoami),
|
|
4578
|
+
(args) => run(t.whoami.name, args.server, async (target2) => {
|
|
4579
|
+
const me = await whoami(target2.client);
|
|
4580
|
+
const instance = await target2.instance();
|
|
4581
|
+
const base = sitesBaseOf(instance);
|
|
4346
4582
|
return {
|
|
4347
|
-
summary: `Signed in as ${me.email} (sites are served at ${sitesPattern(base)})`,
|
|
4348
|
-
data: me
|
|
4583
|
+
summary: `Signed in to ${serverLabel(target2.url)}${instance ? ` (${instance.name})` : ""} as ${me.email} (sites are served at ${sitesPattern(base)})`,
|
|
4584
|
+
data: { ...me, server: target2.url }
|
|
4349
4585
|
};
|
|
4350
4586
|
})
|
|
4351
4587
|
);
|
|
@@ -4382,6 +4618,25 @@ async function writeInlineFiles(dir, files) {
|
|
|
4382
4618
|
if (secretFiles.length > 0) throw new SecretFilesError(secretFiles);
|
|
4383
4619
|
return { skippedDotfiles };
|
|
4384
4620
|
}
|
|
4621
|
+
function failure(error) {
|
|
4622
|
+
if (error instanceof ServerNeededError) {
|
|
4623
|
+
const data = {
|
|
4624
|
+
status: "needs_server",
|
|
4625
|
+
servers: error.servers.map((s) => ({
|
|
4626
|
+
url: s.url,
|
|
4627
|
+
host: serverLabel(s.url),
|
|
4628
|
+
instance: s.instance?.name ?? null,
|
|
4629
|
+
signedIn: s.signedIn,
|
|
4630
|
+
email: s.email ?? null,
|
|
4631
|
+
current: s.current
|
|
4632
|
+
}))
|
|
4633
|
+
};
|
|
4634
|
+
return textResult(`${error.message}
|
|
4635
|
+
|
|
4636
|
+
${JSON.stringify(data, null, 2)}`);
|
|
4637
|
+
}
|
|
4638
|
+
return { isError: true, content: [{ type: "text", text: describeError(error) }] };
|
|
4639
|
+
}
|
|
4385
4640
|
function describeError(error) {
|
|
4386
4641
|
if (error instanceof NoServerError) return NO_SERVER_HINT;
|
|
4387
4642
|
if (error instanceof CliError && error.exitCode === EXIT_NOT_LOGGED_IN) return LOGIN_HINT;
|
|
@@ -4393,8 +4648,11 @@ ${error.hint}` : error.message;
|
|
|
4393
4648
|
}
|
|
4394
4649
|
async function runMcpServer(deps = {}) {
|
|
4395
4650
|
const store = deps.store ?? createConfigStore();
|
|
4396
|
-
const
|
|
4397
|
-
|
|
4651
|
+
const fetchImpl = deps.fetch ?? ((i, init) => fetch(i, init));
|
|
4652
|
+
await addServerOnStart(store, serverToAdd(), fetchImpl).catch(() => {
|
|
4653
|
+
});
|
|
4654
|
+
const servers = deps.servers ?? await knownServers(store, fetchImpl, deps.server).catch(() => []);
|
|
4655
|
+
const server = createMcpServer({ ...deps, store, servers });
|
|
4398
4656
|
await server.connect(new StdioServerTransport());
|
|
4399
4657
|
}
|
|
4400
4658
|
|