@nearform/fling 0.4.0 → 0.5.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/CHANGELOG.md +26 -0
- package/README.md +40 -15
- package/dist/cli.js +380 -132
- package/dist/cli.js.map +4 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1088,7 +1088,7 @@ function exitCodeFor(error) {
|
|
|
1088
1088
|
// package.json
|
|
1089
1089
|
var package_default = {
|
|
1090
1090
|
name: "@nearform/fling",
|
|
1091
|
-
version: "0.
|
|
1091
|
+
version: "0.5.0",
|
|
1092
1092
|
license: "MIT",
|
|
1093
1093
|
description: "Fling a folder of static files at a Fling server and get a link back. CLI + MCP server.",
|
|
1094
1094
|
type: "module",
|
|
@@ -1308,7 +1308,9 @@ var ENV = {
|
|
|
1308
1308
|
configDir: "FLING_CONFIG_DIR",
|
|
1309
1309
|
debug: "FLING_DEBUG",
|
|
1310
1310
|
/** Where to look for files given by name (the .mcpb extension sets it from its settings). */
|
|
1311
|
-
downloadsDir: "FLING_DOWNLOADS_DIR"
|
|
1311
|
+
downloadsDir: "FLING_DOWNLOADS_DIR",
|
|
1312
|
+
/** A server the local MCP server adds to the remembered ones on start (the .mcpb bakes it in). */
|
|
1313
|
+
addServer: "FLING_ADD_SERVER"
|
|
1312
1314
|
};
|
|
1313
1315
|
var serverEntrySchema = z9.object({
|
|
1314
1316
|
/** What the server said about itself at login. A copy this CLI can't read is just refetched. */
|
|
@@ -1327,6 +1329,9 @@ function fromEnv(value) {
|
|
|
1327
1329
|
const v = value?.trim();
|
|
1328
1330
|
return v && !v.startsWith("${") ? v : void 0;
|
|
1329
1331
|
}
|
|
1332
|
+
function serverToAdd(env = process.env) {
|
|
1333
|
+
return fromEnv(env[ENV.addServer]);
|
|
1334
|
+
}
|
|
1330
1335
|
function resolveConfigDir(env = process.env) {
|
|
1331
1336
|
return env[ENV.configDir] ?? join(env.HOME ?? homedir(), CONFIG_DIR_NAME);
|
|
1332
1337
|
}
|
|
@@ -3841,11 +3846,132 @@ async function findFiles(opts, find = {}) {
|
|
|
3841
3846
|
return { files, searched: roots, complete };
|
|
3842
3847
|
}
|
|
3843
3848
|
|
|
3849
|
+
// src/mcp-servers.ts
|
|
3850
|
+
function serverLabel(url) {
|
|
3851
|
+
const u = new URL(url);
|
|
3852
|
+
return `${u.host}${u.pathname}`.replace(/\/+$/, "");
|
|
3853
|
+
}
|
|
3854
|
+
function describeServer(s) {
|
|
3855
|
+
const facts = [
|
|
3856
|
+
s.instance?.name,
|
|
3857
|
+
s.signedIn ? s.email ? `signed in as ${s.email}` : "signed in" : "signed out",
|
|
3858
|
+
s.current ? "current: the user's usual one" : void 0
|
|
3859
|
+
].filter(Boolean);
|
|
3860
|
+
return `${serverLabel(s.url)} (${facts.join("; ")})`;
|
|
3861
|
+
}
|
|
3862
|
+
function knownServersOf(config) {
|
|
3863
|
+
return Object.entries(config.servers).map(([url, entry]) => ({
|
|
3864
|
+
url,
|
|
3865
|
+
instance: entry.instance,
|
|
3866
|
+
current: url === config.current,
|
|
3867
|
+
signedIn: Boolean(entry.apiKey),
|
|
3868
|
+
...entry.apiKey && entry.user?.email ? { email: entry.user.email } : {}
|
|
3869
|
+
}));
|
|
3870
|
+
}
|
|
3871
|
+
var listing = (servers) => servers.map((s) => `- ${describeServer(s)}`).join("\n");
|
|
3872
|
+
var ServerNeededError = class extends CliError {
|
|
3873
|
+
servers;
|
|
3874
|
+
constructor(servers, tool2) {
|
|
3875
|
+
super(
|
|
3876
|
+
`Not run: several Flings are set up on this computer, so ${tool2} needs \`server\`.
|
|
3877
|
+
${listing(servers)}
|
|
3878
|
+
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.`
|
|
3879
|
+
);
|
|
3880
|
+
this.name = "ServerNeededError";
|
|
3881
|
+
this.servers = servers;
|
|
3882
|
+
}
|
|
3883
|
+
};
|
|
3884
|
+
function rememberedServer(config, input) {
|
|
3885
|
+
const servers = knownServersOf(config);
|
|
3886
|
+
const known = servers.length ? `The Flings set up on this computer:
|
|
3887
|
+
${listing(servers)}` : "No Fling is set up on this computer yet.";
|
|
3888
|
+
let match;
|
|
3889
|
+
try {
|
|
3890
|
+
match = matchServer(Object.keys(config.servers), input, { loose: true });
|
|
3891
|
+
} catch (error) {
|
|
3892
|
+
throw new CliError(error.message, { hint: known });
|
|
3893
|
+
}
|
|
3894
|
+
if (!match.remembered)
|
|
3895
|
+
throw new CliError(`"${input}" is not a Fling set up on this computer`, {
|
|
3896
|
+
hint: `${known}
|
|
3897
|
+
To add one, call fling_login with its address as \`server\`.`
|
|
3898
|
+
});
|
|
3899
|
+
return match.url;
|
|
3900
|
+
}
|
|
3901
|
+
var isPinned = (config) => config.serverUrlSource === "flag" || config.serverUrlSource === "env";
|
|
3902
|
+
async function within(promise, ms, fallback) {
|
|
3903
|
+
let timer;
|
|
3904
|
+
const late = new Promise((r) => {
|
|
3905
|
+
timer = setTimeout(() => r(fallback), ms);
|
|
3906
|
+
});
|
|
3907
|
+
try {
|
|
3908
|
+
return await Promise.race([promise.catch(() => fallback), late]);
|
|
3909
|
+
} finally {
|
|
3910
|
+
clearTimeout(timer);
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
var START_FETCH_MS = 5e3;
|
|
3914
|
+
async function addServerOnStart(store, address, fetch2) {
|
|
3915
|
+
if (!address) return;
|
|
3916
|
+
let url;
|
|
3917
|
+
try {
|
|
3918
|
+
url = normalizeServerUrl(address);
|
|
3919
|
+
} catch {
|
|
3920
|
+
return;
|
|
3921
|
+
}
|
|
3922
|
+
if (url in (await store.load()).servers) return;
|
|
3923
|
+
const instance = await within(fetchInstance(url, fetch2), START_FETCH_MS, null);
|
|
3924
|
+
await store.putServer(url, instance ? { instance } : {});
|
|
3925
|
+
}
|
|
3926
|
+
async function knownServers(store, fetch2, pin) {
|
|
3927
|
+
const resolved = await store.resolve({ serverUrl: pin });
|
|
3928
|
+
const config = await store.load();
|
|
3929
|
+
const all = knownServersOf(config);
|
|
3930
|
+
const servers = isPinned(resolved) ? resolved.serverUrl ? [
|
|
3931
|
+
all.find((s) => s.url === resolved.serverUrl) ?? {
|
|
3932
|
+
url: resolved.serverUrl,
|
|
3933
|
+
current: false,
|
|
3934
|
+
signedIn: Boolean(resolved.apiKey)
|
|
3935
|
+
}
|
|
3936
|
+
] : [] : all;
|
|
3937
|
+
return Promise.all(
|
|
3938
|
+
servers.map(async (s) => {
|
|
3939
|
+
if (s.instance) return s;
|
|
3940
|
+
const instance = await within(fetchInstance(s.url, fetch2), START_FETCH_MS, null);
|
|
3941
|
+
if (instance && s.url in config.servers)
|
|
3942
|
+
await store.putServer(s.url, { instance }, { makeCurrent: false }).catch(() => {
|
|
3943
|
+
});
|
|
3944
|
+
return { ...s, instance };
|
|
3945
|
+
})
|
|
3946
|
+
);
|
|
3947
|
+
}
|
|
3948
|
+
function serverArgDescription(servers) {
|
|
3949
|
+
const [only] = servers;
|
|
3950
|
+
if (servers.length > 1)
|
|
3951
|
+
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`;
|
|
3952
|
+
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"}`;
|
|
3953
|
+
}
|
|
3954
|
+
function serverToolNote(servers) {
|
|
3955
|
+
if (servers.length < 2) return "";
|
|
3956
|
+
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.`;
|
|
3957
|
+
}
|
|
3958
|
+
function serversInstructions(servers) {
|
|
3959
|
+
if (servers.length < 2) return void 0;
|
|
3960
|
+
const usual = servers.find((s) => s.current);
|
|
3961
|
+
return [
|
|
3962
|
+
`Several Flings are set up on this computer:
|
|
3963
|
+
${listing(servers)}`,
|
|
3964
|
+
`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.`,
|
|
3965
|
+
"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`."
|
|
3966
|
+
].join("\n\n");
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3844
3969
|
// src/mcp.ts
|
|
3845
3970
|
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
|
|
3971
|
+
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
3972
|
var mcpLocalInstructions = (w, opts = {}) => [
|
|
3848
3973
|
`${w.name} publishes static sites (HTML/CSS/JS, images, documents) at ${w.sitesPattern}. This server runs on the user's computer.`,
|
|
3974
|
+
...[serversInstructions(opts.servers ?? [])].filter((p) => p !== void 0),
|
|
3849
3975
|
"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
3976
|
'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
3977
|
"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 +3981,26 @@ var mcpLocalInstructions = (w, opts = {}) => [
|
|
|
3855
3981
|
`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
3982
|
...opts.comments ? [COMMENTS_NOTE(w)] : []
|
|
3857
3983
|
].join("\n\n");
|
|
3858
|
-
var
|
|
3859
|
-
|
|
3860
|
-
description: def.description,
|
|
3861
|
-
inputSchema: def.inputSchema,
|
|
3862
|
-
annotations: def.annotations
|
|
3863
|
-
});
|
|
3984
|
+
var onFling = (url, instance) => `
|
|
3985
|
+
Fling: ${serverLabel(url)}${instance ? ` (${instance.name})` : ""}`;
|
|
3864
3986
|
function createMcpServer(deps = {}) {
|
|
3865
3987
|
const store = deps.store ?? createConfigStore();
|
|
3866
3988
|
const fetchImpl = deps.fetch ?? ((input, init) => fetch(input, init));
|
|
3867
3989
|
const cwd = deps.cwd ?? process.cwd();
|
|
3868
3990
|
const locate = deps.locate ?? { ...systemLocateOptions(), cwd };
|
|
3869
3991
|
const waitReady = deps.waitReady ?? createReadyWaiter();
|
|
3870
|
-
const
|
|
3992
|
+
const servers = deps.servers ?? [];
|
|
3993
|
+
const several = servers.length > 1;
|
|
3994
|
+
const wording = mcpWording(several ? null : servers[0]?.instance);
|
|
3871
3995
|
const mcpTools2 = mcpToolsFor(wording);
|
|
3996
|
+
const serverArg = z11.string().min(1).max(500).optional().describe(serverArgDescription(servers));
|
|
3997
|
+
const note = serverToolNote(servers);
|
|
3998
|
+
const serverTool = (def, description = def.description) => ({
|
|
3999
|
+
title: def.title,
|
|
4000
|
+
description: `${description}${note}`,
|
|
4001
|
+
inputSchema: { ...def.inputSchema, server: serverArg },
|
|
4002
|
+
annotations: def.annotations
|
|
4003
|
+
});
|
|
3872
4004
|
async function edgeReady(url, paths) {
|
|
3873
4005
|
const r = await waitReady(url, paths).catch(() => ({ ready: false, waitedMs: 0 }));
|
|
3874
4006
|
return {
|
|
@@ -3877,27 +4009,44 @@ function createMcpServer(deps = {}) {
|
|
|
3877
4009
|
Not reachable yet at ${url}: ${NOT_READY_NOTE}.`
|
|
3878
4010
|
};
|
|
3879
4011
|
}
|
|
3880
|
-
async function
|
|
3881
|
-
|
|
4012
|
+
async function choose(server2, tool2) {
|
|
4013
|
+
if (server2 !== void 0)
|
|
4014
|
+
return store.resolve({ serverUrl: rememberedServer(await store.load(), server2) });
|
|
4015
|
+
const resolved = await store.resolve({ serverUrl: deps.server });
|
|
4016
|
+
if (isPinned(resolved)) return resolved;
|
|
4017
|
+
const config = await store.load();
|
|
4018
|
+
const known = knownServersOf(config);
|
|
4019
|
+
if (known.length > 1) throw new ServerNeededError(known, tool2);
|
|
4020
|
+
const only = known[0];
|
|
4021
|
+
return !resolved.serverUrl && only ? store.resolve({ serverUrl: only.url }) : resolved;
|
|
4022
|
+
}
|
|
4023
|
+
async function target(server2, tool2) {
|
|
4024
|
+
const config = await choose(server2, tool2);
|
|
3882
4025
|
if (!config.serverUrl) throw new NoServerError("fling");
|
|
3883
4026
|
if (!config.apiKey) throw new CliError("Not logged in", { exitCode: EXIT_NOT_LOGGED_IN });
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
4027
|
+
const url = config.serverUrl;
|
|
4028
|
+
let instance;
|
|
4029
|
+
return {
|
|
4030
|
+
client: createApiClient({ serverUrl: url, apiKey: config.apiKey, fetch: fetchImpl }),
|
|
4031
|
+
url,
|
|
4032
|
+
config,
|
|
4033
|
+
instance: () => {
|
|
4034
|
+
instance ??= loadInstance(store, config, fetchImpl);
|
|
4035
|
+
return instance;
|
|
4036
|
+
}
|
|
4037
|
+
};
|
|
3889
4038
|
}
|
|
3890
|
-
async function run(op) {
|
|
4039
|
+
async function run(tool2, server2, op) {
|
|
3891
4040
|
try {
|
|
3892
|
-
const { summary, data } = await op(await
|
|
3893
|
-
return
|
|
4041
|
+
const { summary, data } = await op(await target(server2, tool2));
|
|
4042
|
+
return textResult(`${summary}
|
|
3894
4043
|
|
|
3895
|
-
${JSON.stringify(data, null, 2)}`
|
|
4044
|
+
${JSON.stringify(data, null, 2)}`);
|
|
3896
4045
|
} catch (error) {
|
|
3897
|
-
return
|
|
4046
|
+
return failure(error);
|
|
3898
4047
|
}
|
|
3899
4048
|
}
|
|
3900
|
-
async function deployFolder(
|
|
4049
|
+
async function deployFolder(t2, dir, opts) {
|
|
3901
4050
|
let local = await buildManifest(dir);
|
|
3902
4051
|
local.skipped.dotfiles.unshift(...opts.skippedDotfiles ?? []);
|
|
3903
4052
|
const skippedNote = describeSkipped(local.skipped).map((l) => `
|
|
@@ -3924,14 +4073,14 @@ ${l}.`).join("");
|
|
|
3924
4073
|
}
|
|
3925
4074
|
}
|
|
3926
4075
|
const result = await deploy(
|
|
3927
|
-
{ client:
|
|
4076
|
+
{ client: t2.client, fetch: fetchImpl },
|
|
3928
4077
|
{ dir, name: opts.name, manifest: local, spa: opts.spa, public: opts.public }
|
|
3929
4078
|
).catch((error) => {
|
|
3930
4079
|
if (!(error instanceof NameTakenError)) throw error;
|
|
3931
4080
|
const how = opts.nameGiven ? "" : ` (generated from ${opts.nameFrom ?? "the folder"})`;
|
|
3932
4081
|
const pick = error.suggestions.length ? ` Free alternatives: ${error.suggestions.join(", ")}.` : "";
|
|
3933
4082
|
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\`.`
|
|
4083
|
+
`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
4084
|
);
|
|
3936
4085
|
});
|
|
3937
4086
|
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 +4091,20 @@ Warnings:
|
|
|
3942
4091
|
result.url,
|
|
3943
4092
|
local.files.map((f) => f.path)
|
|
3944
4093
|
);
|
|
3945
|
-
const instance =
|
|
4094
|
+
const instance = await t2.instance();
|
|
3946
4095
|
const review = instance && commentsEnabled(instance) ? reviewUrl(instance.appOrigin, result.name) : void 0;
|
|
3947
|
-
const
|
|
3948
|
-
const head = review ? `${what}${
|
|
4096
|
+
const listing2 = generatedIndex ? " \u2014 with a generated index.html listing the HTML pages" : "";
|
|
4097
|
+
const head = review ? `${what}${listing2}.
|
|
3949
4098
|
${shareForFeedback(review, instance?.name ?? "Fling")}
|
|
3950
|
-
Site: ${result.url} (${result.site.visibility})` : `${what}: ${result.url} (${result.site.visibility})${
|
|
4099
|
+
Site: ${result.url} (${result.site.visibility})` : `${what}: ${result.url} (${result.site.visibility})${listing2}`;
|
|
3951
4100
|
return {
|
|
3952
4101
|
kind: "deployed",
|
|
3953
|
-
summary: `${head}${warnings}${skippedNote}${edge.note}
|
|
4102
|
+
summary: `${head}${warnings}${skippedNote}${edge.note}${onFling(t2.url, instance)}
|
|
3954
4103
|
To check exactly which files the site has, call fling_files with name "${result.name}".`,
|
|
3955
4104
|
data: {
|
|
3956
4105
|
status: result.status,
|
|
3957
4106
|
ready: edge.ready,
|
|
4107
|
+
server: t2.url,
|
|
3958
4108
|
name: result.name,
|
|
3959
4109
|
nameGenerated: !opts.nameGiven,
|
|
3960
4110
|
versionId: result.versionId,
|
|
@@ -3970,7 +4120,8 @@ To check exactly which files the site has, call fling_files with name "${result.
|
|
|
3970
4120
|
{ name: "fling", version: VERSION },
|
|
3971
4121
|
{
|
|
3972
4122
|
instructions: mcpLocalInstructions(wording, {
|
|
3973
|
-
comments: commentsEnabled(
|
|
4123
|
+
comments: servers.some((s) => commentsEnabled(s.instance)),
|
|
4124
|
+
servers
|
|
3974
4125
|
})
|
|
3975
4126
|
}
|
|
3976
4127
|
);
|
|
@@ -3979,7 +4130,7 @@ To check exactly which files the site has, call fling_files with name "${result.
|
|
|
3979
4130
|
"fling_deploy",
|
|
3980
4131
|
{
|
|
3981
4132
|
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.
|
|
4133
|
+
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
4134
|
inputSchema: {
|
|
3984
4135
|
path: z11.string().min(1).describe(
|
|
3985
4136
|
"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 +4140,17 @@ To check exactly which files the site has, call fling_files with name "${result.
|
|
|
3989
4140
|
),
|
|
3990
4141
|
spa: spa2,
|
|
3991
4142
|
public: isPublic,
|
|
3992
|
-
listHtml: listHtml2
|
|
4143
|
+
listHtml: listHtml2,
|
|
4144
|
+
server: serverArg
|
|
3993
4145
|
},
|
|
3994
4146
|
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
3995
4147
|
},
|
|
3996
|
-
(args) => run(async (
|
|
4148
|
+
(args) => run("fling_deploy", args.server, async (t2) => {
|
|
3997
4149
|
const located = await resolveInput(args.path, locate);
|
|
3998
4150
|
const prepared = await prepareInput(located.path);
|
|
3999
4151
|
try {
|
|
4000
4152
|
const name = args.name !== void 0 ? resolveSiteName(prepared.dir, args.name) : prepared.defaultName;
|
|
4001
|
-
const outcome = await deployFolder(
|
|
4153
|
+
const outcome = await deployFolder(t2, prepared.dir, {
|
|
4002
4154
|
name,
|
|
4003
4155
|
nameGiven: args.name !== void 0,
|
|
4004
4156
|
nameFrom: prepared.kind === "folder" ? "the folder" : "the file name",
|
|
@@ -4052,33 +4204,31 @@ ${lines.join("\n")}` : `Nothing found from the last ${args.days ?? 14} days${arg
|
|
|
4052
4204
|
searched: searched.map((r) => ({ label: r.label, path: r.path })),
|
|
4053
4205
|
...complete ? {} : { truncated: true }
|
|
4054
4206
|
};
|
|
4055
|
-
return {
|
|
4056
|
-
content: [{ type: "text", text: `${summary}
|
|
4207
|
+
return textResult(`${summary}
|
|
4057
4208
|
|
|
4058
|
-
${JSON.stringify(data, null, 2)}`
|
|
4059
|
-
};
|
|
4209
|
+
${JSON.stringify(data, null, 2)}`);
|
|
4060
4210
|
} catch (error) {
|
|
4061
|
-
return
|
|
4211
|
+
return failure(error);
|
|
4062
4212
|
}
|
|
4063
4213
|
}
|
|
4064
4214
|
);
|
|
4065
4215
|
const publishDef = mcpTools2.publish;
|
|
4066
4216
|
server.registerTool(
|
|
4067
4217
|
publishDef.name,
|
|
4068
|
-
|
|
4069
|
-
|
|
4218
|
+
serverTool(
|
|
4219
|
+
publishDef,
|
|
4070
4220
|
// This server has no chunked upload tools; big sites go through fling_deploy instead.
|
|
4071
|
-
|
|
4221
|
+
publishDef.description.replace(
|
|
4072
4222
|
/\(whole site, up to [^;]*; bigger sites: fling_upload_begin \/ fling_upload_file \/ fling_upload_finish\)/,
|
|
4073
4223
|
"(whole site; for a big site that is already on this computer, use fling_deploy with its folder)"
|
|
4074
4224
|
)
|
|
4075
|
-
|
|
4076
|
-
(args) => run(async (
|
|
4225
|
+
),
|
|
4226
|
+
(args) => run(publishDef.name, args.server, async (t2) => {
|
|
4077
4227
|
const name = resolveSiteName(".", args.name);
|
|
4078
4228
|
const tmp = await mkdtemp3(join6(tmpdir3(), "fling-publish-"));
|
|
4079
4229
|
try {
|
|
4080
4230
|
const { skippedDotfiles } = await writeInlineFiles(tmp, args.files);
|
|
4081
|
-
|
|
4231
|
+
return await deployFolder(t2, tmp, {
|
|
4082
4232
|
skippedDotfiles,
|
|
4083
4233
|
name,
|
|
4084
4234
|
nameGiven: true,
|
|
@@ -4087,7 +4237,6 @@ ${JSON.stringify(data, null, 2)}` }]
|
|
|
4087
4237
|
listHtml: args.listHtml,
|
|
4088
4238
|
retryWith: "fling_publish"
|
|
4089
4239
|
});
|
|
4090
|
-
return outcome;
|
|
4091
4240
|
} finally {
|
|
4092
4241
|
await rm2(tmp, { recursive: true, force: true });
|
|
4093
4242
|
}
|
|
@@ -4096,14 +4245,15 @@ ${JSON.stringify(data, null, 2)}` }]
|
|
|
4096
4245
|
server.registerTool(
|
|
4097
4246
|
"fling_check_name",
|
|
4098
4247
|
{
|
|
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
|
|
4248
|
+
...serverTool(mcpTools2.checkName),
|
|
4249
|
+
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
4250
|
inputSchema: {
|
|
4102
4251
|
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)")
|
|
4252
|
+
path: z11.string().optional().describe("File or folder whose generated default name to check (path or file name)"),
|
|
4253
|
+
server: serverArg
|
|
4104
4254
|
}
|
|
4105
4255
|
},
|
|
4106
|
-
(args) => run(async (
|
|
4256
|
+
(args) => run("fling_check_name", args.server, async (t2) => {
|
|
4107
4257
|
if (args.name === void 0 && args.path === void 0)
|
|
4108
4258
|
throw new CliError("Pass `name` or `path`");
|
|
4109
4259
|
let name = args.name;
|
|
@@ -4112,8 +4262,8 @@ ${JSON.stringify(data, null, 2)}` }]
|
|
|
4112
4262
|
source = await resolveInput(args.path, locate);
|
|
4113
4263
|
name = defaultNameFor(source.path, source.isDirectory);
|
|
4114
4264
|
}
|
|
4115
|
-
const check2 = await checkName(
|
|
4116
|
-
const base = check2.status === "invalid" ? null : await
|
|
4265
|
+
const check2 = await checkName(t2.client, name);
|
|
4266
|
+
const base = check2.status === "invalid" ? null : sitesBaseOf(await t2.instance());
|
|
4117
4267
|
const url = base ? siteUrlFor(name, base) : null;
|
|
4118
4268
|
const suggestions = check2.suggestions.length ? ` Suggestions: ${check2.suggestions.join(", ")}.` : "";
|
|
4119
4269
|
const summary = {
|
|
@@ -4127,59 +4277,77 @@ ${JSON.stringify(data, null, 2)}` }]
|
|
|
4127
4277
|
data: {
|
|
4128
4278
|
...check2,
|
|
4129
4279
|
url,
|
|
4280
|
+
server: t2.url,
|
|
4130
4281
|
...source ? { generated: true, source: { path: source.path, foundIn: source.foundIn } } : {}
|
|
4131
4282
|
}
|
|
4132
4283
|
};
|
|
4133
4284
|
})
|
|
4134
4285
|
);
|
|
4135
|
-
|
|
4286
|
+
const pendingLogins = /* @__PURE__ */ new Map();
|
|
4136
4287
|
server.registerTool(
|
|
4137
4288
|
"fling_login",
|
|
4138
4289
|
{
|
|
4139
4290
|
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: {
|
|
4291
|
+
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,
|
|
4292
|
+
inputSchema: {
|
|
4293
|
+
server: z11.string().min(1).max(500).optional().describe(
|
|
4294
|
+
`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"}`}`
|
|
4295
|
+
)
|
|
4296
|
+
},
|
|
4142
4297
|
annotations: { readOnlyHint: false, openWorldHint: true }
|
|
4143
4298
|
},
|
|
4144
|
-
async () => {
|
|
4299
|
+
async (args) => {
|
|
4145
4300
|
try {
|
|
4146
|
-
|
|
4147
|
-
|
|
4301
|
+
let serverUrl;
|
|
4302
|
+
if (args.server !== void 0) {
|
|
4303
|
+
const config2 = await store.load();
|
|
4304
|
+
try {
|
|
4305
|
+
serverUrl = matchServer(Object.keys(config2.servers), args.server, { loose: true }).url;
|
|
4306
|
+
} catch (error) {
|
|
4307
|
+
const known = knownServersOf(config2).map((s) => `- ${describeServer(s)}`);
|
|
4308
|
+
throw new CliError(error.message, {
|
|
4309
|
+
hint: known.length ? `The Flings set up on this computer:
|
|
4310
|
+
${known.join("\n")}` : ""
|
|
4311
|
+
});
|
|
4312
|
+
}
|
|
4313
|
+
} else {
|
|
4314
|
+
serverUrl = (await choose(void 0, "fling_login")).serverUrl;
|
|
4315
|
+
}
|
|
4148
4316
|
if (!serverUrl) throw new NoServerError("fling");
|
|
4149
|
-
|
|
4317
|
+
const config = await store.resolve({ serverUrl });
|
|
4318
|
+
let login = pendingLogins.get(serverUrl);
|
|
4319
|
+
if (config.apiKey && !login) {
|
|
4150
4320
|
const me = await whoami(
|
|
4151
|
-
createApiClient({
|
|
4152
|
-
serverUrl,
|
|
4153
|
-
apiKey: config.apiKey,
|
|
4154
|
-
fetch: fetchImpl
|
|
4155
|
-
})
|
|
4321
|
+
createApiClient({ serverUrl, apiKey: config.apiKey, fetch: fetchImpl })
|
|
4156
4322
|
).catch(() => null);
|
|
4157
|
-
if (me) return textResult(`Already signed in to ${
|
|
4323
|
+
if (me) return textResult(`Already signed in to ${serverUrl} as ${me.email}.`);
|
|
4158
4324
|
}
|
|
4159
|
-
if (!
|
|
4160
|
-
const
|
|
4325
|
+
if (!login) {
|
|
4326
|
+
const started = {
|
|
4161
4327
|
done: Promise.resolve()
|
|
4162
4328
|
};
|
|
4163
|
-
|
|
4329
|
+
started.done = loginFlow({
|
|
4164
4330
|
serverUrl,
|
|
4165
4331
|
store,
|
|
4166
4332
|
log: silentLogger,
|
|
4167
4333
|
open: deps.open ?? (async (url) => (await import("open")).default(url)),
|
|
4168
4334
|
fetch: fetchImpl,
|
|
4169
4335
|
timeoutMs: deps.loginTimeoutMs,
|
|
4170
|
-
// Signing in to
|
|
4336
|
+
// Signing in to another Fling leaves the CLI's current server as it is.
|
|
4171
4337
|
makeCurrent: !config.current || config.current === serverUrl,
|
|
4172
4338
|
onAuthUrl: (url) => {
|
|
4173
|
-
|
|
4339
|
+
started.url = url;
|
|
4174
4340
|
}
|
|
4175
4341
|
});
|
|
4176
|
-
|
|
4342
|
+
const key = serverUrl;
|
|
4343
|
+
started.done.catch(() => {
|
|
4177
4344
|
}).finally(() => {
|
|
4178
|
-
if (
|
|
4345
|
+
if (pendingLogins.get(key) === started) pendingLogins.delete(key);
|
|
4179
4346
|
});
|
|
4180
|
-
|
|
4347
|
+
pendingLogins.set(serverUrl, started);
|
|
4348
|
+
login = started;
|
|
4181
4349
|
}
|
|
4182
|
-
const current =
|
|
4350
|
+
const current = login;
|
|
4183
4351
|
const waited = await Promise.race([
|
|
4184
4352
|
current.done.then(
|
|
4185
4353
|
(r) => ({ ok: true, r }),
|
|
@@ -4192,34 +4360,82 @@ ${JSON.stringify(data, null, 2)}` }]
|
|
|
4192
4360
|
`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
4361
|
);
|
|
4194
4362
|
if (!waited.ok) throw waited.e;
|
|
4195
|
-
return textResult(
|
|
4363
|
+
return textResult(
|
|
4364
|
+
`Signed in as ${waited.r.user.email} (${waited.r.serverUrl}, ${waited.r.instance.name}).`
|
|
4365
|
+
);
|
|
4366
|
+
} catch (error) {
|
|
4367
|
+
return failure(error);
|
|
4368
|
+
}
|
|
4369
|
+
}
|
|
4370
|
+
);
|
|
4371
|
+
server.registerTool(
|
|
4372
|
+
"fling_servers",
|
|
4373
|
+
{
|
|
4374
|
+
title: "List the Flings on this computer",
|
|
4375
|
+
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.`,
|
|
4376
|
+
inputSchema: {
|
|
4377
|
+
use: z11.string().min(1).max(500).optional().describe(
|
|
4378
|
+
"Make this Fling current (address, host or unique part of it), like `fling use`"
|
|
4379
|
+
)
|
|
4380
|
+
},
|
|
4381
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false }
|
|
4382
|
+
},
|
|
4383
|
+
async (args) => {
|
|
4384
|
+
try {
|
|
4385
|
+
let switched = "";
|
|
4386
|
+
if (args.use !== void 0) {
|
|
4387
|
+
const url = rememberedServer(await store.load(), args.use);
|
|
4388
|
+
await store.setCurrent(url);
|
|
4389
|
+
switched = `Now using ${serverLabel(url)} as the current Fling (as \`fling use\` does).
|
|
4390
|
+
`;
|
|
4391
|
+
}
|
|
4392
|
+
const config = await store.load();
|
|
4393
|
+
const known = knownServersOf(config);
|
|
4394
|
+
const pinned = await store.resolve({ serverUrl: deps.server });
|
|
4395
|
+
const summary = known.length ? `${switched}${known.length} Fling(s) set up on this computer:
|
|
4396
|
+
${known.map((s) => `- ${describeServer(s)}`).join("\n")}` + (isPinned(pinned) && pinned.serverUrl ? `
|
|
4397
|
+
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;
|
|
4398
|
+
const data = {
|
|
4399
|
+
current: config.current ?? null,
|
|
4400
|
+
servers: known.map((s) => ({
|
|
4401
|
+
url: s.url,
|
|
4402
|
+
host: serverLabel(s.url),
|
|
4403
|
+
instance: s.instance?.name ?? null,
|
|
4404
|
+
signedIn: s.signedIn,
|
|
4405
|
+
email: s.email ?? null,
|
|
4406
|
+
current: s.current
|
|
4407
|
+
}))
|
|
4408
|
+
};
|
|
4409
|
+
return textResult(`${summary}
|
|
4410
|
+
|
|
4411
|
+
${JSON.stringify(data, null, 2)}`);
|
|
4196
4412
|
} catch (error) {
|
|
4197
|
-
return
|
|
4413
|
+
return failure(error);
|
|
4198
4414
|
}
|
|
4199
4415
|
}
|
|
4200
4416
|
);
|
|
4201
4417
|
const t = mcpTools2;
|
|
4202
4418
|
server.registerTool(
|
|
4203
4419
|
t.list.name,
|
|
4204
|
-
|
|
4205
|
-
(args) => run(async (
|
|
4206
|
-
const sites = await listSites(
|
|
4207
|
-
return { summary: `${sites.length} site(s)`, data: { sites } };
|
|
4420
|
+
serverTool(t.list),
|
|
4421
|
+
(args) => run(t.list.name, args.server, async ({ client, url }) => {
|
|
4422
|
+
const sites = await listSites(client, { archived: args.archived });
|
|
4423
|
+
return { summary: `${sites.length} site(s) on ${serverLabel(url)}`, data: { sites } };
|
|
4208
4424
|
})
|
|
4209
4425
|
);
|
|
4210
4426
|
server.registerTool(
|
|
4211
4427
|
t.info.name,
|
|
4212
|
-
|
|
4213
|
-
(args) => run(async (
|
|
4214
|
-
const site = await getSite(
|
|
4428
|
+
serverTool(t.info),
|
|
4429
|
+
(args) => run(t.info.name, args.server, async ({ client }) => {
|
|
4430
|
+
const site = await getSite(client, args.name);
|
|
4215
4431
|
return { summary: `${site.name}: ${site.url}`, data: site };
|
|
4216
4432
|
})
|
|
4217
4433
|
);
|
|
4218
4434
|
server.registerTool(
|
|
4219
4435
|
t.files.name,
|
|
4220
|
-
|
|
4221
|
-
(args) => run(async (
|
|
4222
|
-
const result = await versionFiles(
|
|
4436
|
+
serverTool(t.files),
|
|
4437
|
+
(args) => run(t.files.name, args.server, async ({ client }) => {
|
|
4438
|
+
const result = await versionFiles(client, args.name, args.versionId);
|
|
4223
4439
|
return {
|
|
4224
4440
|
summary: describeVersionFiles(result.name, result, {
|
|
4225
4441
|
current: result.current,
|
|
@@ -4231,15 +4447,15 @@ ${JSON.stringify(data, null, 2)}` }]
|
|
|
4231
4447
|
);
|
|
4232
4448
|
server.registerTool(
|
|
4233
4449
|
t.share.name,
|
|
4234
|
-
|
|
4235
|
-
(args) => run(async (
|
|
4450
|
+
serverTool(t.share),
|
|
4451
|
+
(args) => run(t.share.name, args.server, async ({ client }) => {
|
|
4236
4452
|
if (args.refresh && args.revoke) throw new CliError("Use either refresh or revoke, not both");
|
|
4237
|
-
const result = await share(
|
|
4453
|
+
const result = await share(client, args.name, args);
|
|
4238
4454
|
if (result.action === "revoked")
|
|
4239
4455
|
return { summary: `Share link for ${args.name} revoked`, data: result };
|
|
4240
4456
|
let edge = { ready: true, note: "" };
|
|
4241
4457
|
if (result.action !== "existing") {
|
|
4242
|
-
const files = await versionFiles(
|
|
4458
|
+
const files = await versionFiles(client, args.name).catch(() => null);
|
|
4243
4459
|
edge = await edgeReady(result.shareUrl, files?.files.map((f) => f.path) ?? []);
|
|
4244
4460
|
}
|
|
4245
4461
|
return {
|
|
@@ -4250,78 +4466,86 @@ ${JSON.stringify(data, null, 2)}` }]
|
|
|
4250
4466
|
);
|
|
4251
4467
|
server.registerTool(
|
|
4252
4468
|
t.setVisibility.name,
|
|
4253
|
-
|
|
4254
|
-
(args) => run(async (
|
|
4255
|
-
const site = await setVisibility(
|
|
4469
|
+
serverTool(t.setVisibility),
|
|
4470
|
+
(args) => run(t.setVisibility.name, args.server, async ({ client }) => {
|
|
4471
|
+
const site = await setVisibility(client, args.name, args.visibility);
|
|
4256
4472
|
return { summary: `${site.name} is now ${site.visibility}: ${site.url}`, data: site };
|
|
4257
4473
|
})
|
|
4258
4474
|
);
|
|
4259
4475
|
server.registerTool(
|
|
4260
4476
|
t.archive.name,
|
|
4261
|
-
|
|
4262
|
-
(args) => run(async (
|
|
4263
|
-
const site = await archiveSite(
|
|
4477
|
+
serverTool(t.archive),
|
|
4478
|
+
(args) => run(t.archive.name, args.server, async ({ client }) => {
|
|
4479
|
+
const site = await archiveSite(client, args.name, { unarchive: args.unarchive });
|
|
4264
4480
|
return { summary: `${site.name} ${args.unarchive ? "restored" : "archived"}`, data: site };
|
|
4265
4481
|
})
|
|
4266
4482
|
);
|
|
4267
4483
|
server.registerTool(
|
|
4268
4484
|
t.delete.name,
|
|
4269
|
-
|
|
4270
|
-
(args) => run(async (
|
|
4271
|
-
const result = await deleteSite(
|
|
4272
|
-
return { summary: `${args.name} deleted`, data: result };
|
|
4485
|
+
serverTool(t.delete),
|
|
4486
|
+
(args) => run(t.delete.name, args.server, async ({ client, url }) => {
|
|
4487
|
+
const result = await deleteSite(client, args.name);
|
|
4488
|
+
return { summary: `${args.name} deleted from ${serverLabel(url)}`, data: result };
|
|
4273
4489
|
})
|
|
4274
4490
|
);
|
|
4275
4491
|
server.registerTool(
|
|
4276
4492
|
t.rollback.name,
|
|
4277
|
-
|
|
4278
|
-
(args) => run(async (
|
|
4279
|
-
const site = await rollback(
|
|
4493
|
+
serverTool(t.rollback),
|
|
4494
|
+
(args) => run(t.rollback.name, args.server, async ({ client }) => {
|
|
4495
|
+
const site = await rollback(client, args.name, args.versionId);
|
|
4280
4496
|
return {
|
|
4281
4497
|
summary: `${site.name} now serves version ${site.currentVersionId}: ${site.url}`,
|
|
4282
4498
|
data: site
|
|
4283
4499
|
};
|
|
4284
4500
|
})
|
|
4285
4501
|
);
|
|
4286
|
-
if (
|
|
4287
|
-
|
|
4288
|
-
|
|
4502
|
+
if (servers.some((s) => commentsEnabled(s.instance))) {
|
|
4503
|
+
async function reviewOn(target2) {
|
|
4504
|
+
const instance = await target2.instance();
|
|
4505
|
+
if (instance?.capabilities.comments === false)
|
|
4506
|
+
throw new CliError(`${instance.name} (${serverLabel(target2.url)}) has comments turned off`);
|
|
4507
|
+
const appOrigin = instance?.appOrigin ?? target2.url;
|
|
4508
|
+
return {
|
|
4509
|
+
review: (name) => reviewUrl(appOrigin, name),
|
|
4510
|
+
fling: onFling(target2.url, instance)
|
|
4511
|
+
};
|
|
4512
|
+
}
|
|
4289
4513
|
server.registerTool(
|
|
4290
4514
|
t.commentsList.name,
|
|
4291
|
-
|
|
4292
|
-
(args) => run(async (
|
|
4293
|
-
const
|
|
4515
|
+
serverTool(t.commentsList),
|
|
4516
|
+
(args) => run(t.commentsList.name, args.server, async (target2) => {
|
|
4517
|
+
const { review, fling } = await reviewOn(target2);
|
|
4518
|
+
const list = await listComments(target2.client, args.name, { path: args.path });
|
|
4294
4519
|
const reviewPage2 = review(list.site.name);
|
|
4295
4520
|
return {
|
|
4296
|
-
summary: describeComments(list, {
|
|
4297
|
-
includeResolved: args.includeResolved,
|
|
4298
|
-
reviewUrl: reviewPage2
|
|
4299
|
-
}),
|
|
4521
|
+
summary: `${describeComments(list, { includeResolved: args.includeResolved, reviewUrl: reviewPage2 })}${fling}`,
|
|
4300
4522
|
data: { ...list, reviewUrl: reviewPage2 }
|
|
4301
4523
|
};
|
|
4302
4524
|
})
|
|
4303
4525
|
);
|
|
4304
4526
|
server.registerTool(
|
|
4305
4527
|
t.commentAdd.name,
|
|
4306
|
-
|
|
4307
|
-
(args) => run(async (
|
|
4308
|
-
const
|
|
4528
|
+
serverTool(t.commentAdd),
|
|
4529
|
+
(args) => run(t.commentAdd.name, args.server, async (target2) => {
|
|
4530
|
+
const { review, fling } = await reviewOn(target2);
|
|
4531
|
+
const comment = await addComment(target2.client, args.name, {
|
|
4309
4532
|
body: args.body,
|
|
4310
4533
|
path: args.path,
|
|
4311
4534
|
parentId: args.replyTo
|
|
4312
4535
|
});
|
|
4313
4536
|
const what = comment.parentId ? `Replied to thread ${comment.parentId}` : "Comment added";
|
|
4314
4537
|
return {
|
|
4315
|
-
summary: `${what} on ${args.name} (${comment.path}, id ${comment.id}). Review page: ${review(args.name)}`,
|
|
4538
|
+
summary: `${what} on ${args.name} (${comment.path}, id ${comment.id}). Review page: ${review(args.name)}${fling}`,
|
|
4316
4539
|
data: comment
|
|
4317
4540
|
};
|
|
4318
4541
|
})
|
|
4319
4542
|
);
|
|
4320
4543
|
server.registerTool(
|
|
4321
4544
|
t.commentResolve.name,
|
|
4322
|
-
|
|
4323
|
-
(args) => run(async (
|
|
4324
|
-
|
|
4545
|
+
serverTool(t.commentResolve),
|
|
4546
|
+
(args) => run(t.commentResolve.name, args.server, async (target2) => {
|
|
4547
|
+
await reviewOn(target2);
|
|
4548
|
+
const comment = await resolveComment(target2.client, args.name, args.id, !args.reopen);
|
|
4325
4549
|
return {
|
|
4326
4550
|
summary: `Thread ${comment.id} on ${args.name} ${args.reopen ? "reopened" : "resolved"}`,
|
|
4327
4551
|
data: comment
|
|
@@ -4330,22 +4554,24 @@ ${JSON.stringify(data, null, 2)}` }]
|
|
|
4330
4554
|
);
|
|
4331
4555
|
server.registerTool(
|
|
4332
4556
|
t.commentDelete.name,
|
|
4333
|
-
|
|
4334
|
-
(args) => run(async (
|
|
4335
|
-
|
|
4557
|
+
serverTool(t.commentDelete),
|
|
4558
|
+
(args) => run(t.commentDelete.name, args.server, async (target2) => {
|
|
4559
|
+
await reviewOn(target2);
|
|
4560
|
+
const result = await deleteComment(target2.client, args.name, args.id);
|
|
4336
4561
|
return { summary: `Comment ${args.id} on ${args.name} deleted`, data: result };
|
|
4337
4562
|
})
|
|
4338
4563
|
);
|
|
4339
4564
|
}
|
|
4340
4565
|
server.registerTool(
|
|
4341
4566
|
t.whoami.name,
|
|
4342
|
-
|
|
4343
|
-
() => run(async (
|
|
4344
|
-
const me = await whoami(
|
|
4345
|
-
const
|
|
4567
|
+
serverTool(t.whoami),
|
|
4568
|
+
(args) => run(t.whoami.name, args.server, async (target2) => {
|
|
4569
|
+
const me = await whoami(target2.client);
|
|
4570
|
+
const instance = await target2.instance();
|
|
4571
|
+
const base = sitesBaseOf(instance);
|
|
4346
4572
|
return {
|
|
4347
|
-
summary: `Signed in as ${me.email} (sites are served at ${sitesPattern(base)})`,
|
|
4348
|
-
data: me
|
|
4573
|
+
summary: `Signed in to ${serverLabel(target2.url)}${instance ? ` (${instance.name})` : ""} as ${me.email} (sites are served at ${sitesPattern(base)})`,
|
|
4574
|
+
data: { ...me, server: target2.url }
|
|
4349
4575
|
};
|
|
4350
4576
|
})
|
|
4351
4577
|
);
|
|
@@ -4382,6 +4608,25 @@ async function writeInlineFiles(dir, files) {
|
|
|
4382
4608
|
if (secretFiles.length > 0) throw new SecretFilesError(secretFiles);
|
|
4383
4609
|
return { skippedDotfiles };
|
|
4384
4610
|
}
|
|
4611
|
+
function failure(error) {
|
|
4612
|
+
if (error instanceof ServerNeededError) {
|
|
4613
|
+
const data = {
|
|
4614
|
+
status: "needs_server",
|
|
4615
|
+
servers: error.servers.map((s) => ({
|
|
4616
|
+
url: s.url,
|
|
4617
|
+
host: serverLabel(s.url),
|
|
4618
|
+
instance: s.instance?.name ?? null,
|
|
4619
|
+
signedIn: s.signedIn,
|
|
4620
|
+
email: s.email ?? null,
|
|
4621
|
+
current: s.current
|
|
4622
|
+
}))
|
|
4623
|
+
};
|
|
4624
|
+
return textResult(`${error.message}
|
|
4625
|
+
|
|
4626
|
+
${JSON.stringify(data, null, 2)}`);
|
|
4627
|
+
}
|
|
4628
|
+
return { isError: true, content: [{ type: "text", text: describeError(error) }] };
|
|
4629
|
+
}
|
|
4385
4630
|
function describeError(error) {
|
|
4386
4631
|
if (error instanceof NoServerError) return NO_SERVER_HINT;
|
|
4387
4632
|
if (error instanceof CliError && error.exitCode === EXIT_NOT_LOGGED_IN) return LOGIN_HINT;
|
|
@@ -4393,8 +4638,11 @@ ${error.hint}` : error.message;
|
|
|
4393
4638
|
}
|
|
4394
4639
|
async function runMcpServer(deps = {}) {
|
|
4395
4640
|
const store = deps.store ?? createConfigStore();
|
|
4396
|
-
const
|
|
4397
|
-
|
|
4641
|
+
const fetchImpl = deps.fetch ?? ((i, init) => fetch(i, init));
|
|
4642
|
+
await addServerOnStart(store, serverToAdd(), fetchImpl).catch(() => {
|
|
4643
|
+
});
|
|
4644
|
+
const servers = deps.servers ?? await knownServers(store, fetchImpl, deps.server).catch(() => []);
|
|
4645
|
+
const server = createMcpServer({ ...deps, store, servers });
|
|
4398
4646
|
await server.connect(new StdioServerTransport());
|
|
4399
4647
|
}
|
|
4400
4648
|
|