premanmcp 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/desktop.js +29 -4
- package/bin/integrations.js +202 -102
- package/bin/shared.js +42 -0
- package/bin/verify.js +6 -2
- package/package.json +1 -1
- package/dist/auth_flow_ui.d.ts +0 -38
- package/dist/auth_flow_ui.js +0 -170
- package/dist/mcp-preview-panel.d.ts +0 -30
- package/dist/mcp-preview-panel.js +0 -166
- package/dist/user_auth_flow.d.ts +0 -11
- package/dist/user_auth_flow.js +0 -279
package/bin/desktop.js
CHANGED
|
@@ -133,9 +133,16 @@ export function desktopAppRunning(destination = "/Applications") {
|
|
|
133
133
|
* installed. A copy that ignores the request keeps running and the caller says
|
|
134
134
|
* so rather than escalating.
|
|
135
135
|
*/
|
|
136
|
+
// Longer than the app allows its own shutdown, which is fifteen seconds of
|
|
137
|
+
// waiting for the runner's last calls to PreMan. Giving up first is not a
|
|
138
|
+
// neutral act: what follows is a launch, and a launch that lands while the old
|
|
139
|
+
// instance still holds the single-instance lock is delivered to that instance
|
|
140
|
+
// rather than starting a new one.
|
|
141
|
+
const QUIT_TIMEOUT_MS = 20_000;
|
|
142
|
+
|
|
136
143
|
export async function quitDesktopApp({
|
|
137
144
|
destination = "/Applications",
|
|
138
|
-
timeoutMs =
|
|
145
|
+
timeoutMs = QUIT_TIMEOUT_MS,
|
|
139
146
|
sleep = defaultSleep,
|
|
140
147
|
} = {}) {
|
|
141
148
|
if (!desktopAppRunning(destination)) return "not-running";
|
|
@@ -230,6 +237,7 @@ export async function openDesktopSignedIn(
|
|
|
230
237
|
// exercised without a real app on the machine running the tests.
|
|
231
238
|
isRunning = desktopAppRunning,
|
|
232
239
|
quit = quitDesktopApp,
|
|
240
|
+
launch = defaultLaunch,
|
|
233
241
|
} = {}
|
|
234
242
|
) {
|
|
235
243
|
if (!desktopAppInstalled(destination)) {
|
|
@@ -241,9 +249,18 @@ export async function openDesktopSignedIn(
|
|
|
241
249
|
restarted = (await quit({ destination, sleep })) === "quit";
|
|
242
250
|
}
|
|
243
251
|
const handoff = writeDesktopSession(creds);
|
|
244
|
-
|
|
245
|
-
//
|
|
246
|
-
|
|
252
|
+
|
|
253
|
+
// Asked to go and still here. Launching now does not start anything: the old
|
|
254
|
+
// instance holds the single-instance lock until it actually exits, so the
|
|
255
|
+
// launch reaches it as a second-instance and it re-shows the window it was in
|
|
256
|
+
// the middle of dismantling -- which is how onboarding ended on a black
|
|
257
|
+
// rectangle. Nothing here can hurry it, and the session on disk is what the
|
|
258
|
+
// next launch adopts, so saying so beats making it worse.
|
|
259
|
+
if (wasRunning && restartIfRunning && !restarted) {
|
|
260
|
+
return { state: "opened-already-running", handoff: handoff.state };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
launch(destination);
|
|
247
264
|
if (handoff.state !== "written") return { state: "opened", handoff: handoff.state };
|
|
248
265
|
|
|
249
266
|
if (wasRunning && !restarted) {
|
|
@@ -266,6 +283,14 @@ export async function openDesktopSignedIn(
|
|
|
266
283
|
|
|
267
284
|
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
268
285
|
|
|
286
|
+
// The bundle path rather than the name: `open -a PreMan` asks LaunchServices,
|
|
287
|
+
// which may well pick a different copy than the one just installed.
|
|
288
|
+
const defaultLaunch = (destination) =>
|
|
289
|
+
spawn("open", ["-a", installedAppPath(destination)], {
|
|
290
|
+
stdio: "ignore",
|
|
291
|
+
detached: true,
|
|
292
|
+
}).unref();
|
|
293
|
+
|
|
269
294
|
/**
|
|
270
295
|
* Open a Playground session URL in PreMan.app on macOS when it is installed.
|
|
271
296
|
* Windows/Linux (and Mac without the app) get the website URL printed instead.
|
package/bin/integrations.js
CHANGED
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { describeCheckout, explainNoCheckout, resolveCheckout } from "./repo.js";
|
|
18
|
+
import { desktopAppInstalled, writeDesktopSession } from "./desktop.js";
|
|
18
19
|
import {
|
|
19
20
|
DEFAULT_BACKEND,
|
|
21
|
+
askWhileWaiting,
|
|
20
22
|
assertOk,
|
|
21
23
|
backendUrl,
|
|
22
24
|
callBackendJson,
|
|
@@ -115,37 +117,58 @@ export class Unrecoverable extends Error {}
|
|
|
115
117
|
* Returns the truthy value from ``check``, or null on timeout. Ordinary
|
|
116
118
|
* exceptions are swallowed and retried; :class:`Unrecoverable` stops the wait.
|
|
117
119
|
*/
|
|
120
|
+
const NOT_ANSWERED = Symbol("not answered");
|
|
121
|
+
|
|
118
122
|
async function waitFor(
|
|
119
123
|
label,
|
|
120
124
|
check,
|
|
121
|
-
{ hint = "", hintAfterMs = 60000, timeoutMs = POLL_TIMEOUT_MS } = {}
|
|
125
|
+
{ hint = "", hintAfterMs = 60000, timeoutMs = POLL_TIMEOUT_MS, escape = null } = {}
|
|
122
126
|
) {
|
|
123
127
|
const startedAt = Date.now();
|
|
124
128
|
const deadline = startedAt + timeoutMs;
|
|
125
129
|
let hinted = false;
|
|
130
|
+
let asked = null;
|
|
126
131
|
process.stdout.write(`Waiting for ${label}`);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
try {
|
|
133
|
+
while (Date.now() < deadline) {
|
|
134
|
+
try {
|
|
135
|
+
const done = await check();
|
|
136
|
+
if (done) {
|
|
137
|
+
process.stdout.write(asked ? "\n" : " done.\n");
|
|
138
|
+
return done;
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (err instanceof Unrecoverable) {
|
|
142
|
+
process.stdout.write("\n");
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
/* keep waiting; the customer is elsewhere */
|
|
133
146
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
147
|
+
// A minute of dots is indistinguishable from a hang, and the reason this
|
|
148
|
+
// waits forever is usually something the customer can act on.
|
|
149
|
+
if (hint && !hinted && Date.now() - startedAt > hintAfterMs) {
|
|
150
|
+
hinted = true;
|
|
151
|
+
process.stdout.write(`\n${hint}\n`);
|
|
152
|
+
// Dots and a question cannot share a line. Once there is something to
|
|
153
|
+
// answer, the answering is the whole of what this is doing.
|
|
154
|
+
if (escape) asked = askWhileWaiting(escape.question);
|
|
155
|
+
else process.stdout.write("Still waiting");
|
|
156
|
+
}
|
|
157
|
+
if (!asked) process.stdout.write(".");
|
|
158
|
+
// Polling carries on underneath the question, so an install that lands
|
|
159
|
+
// while it is on the screen still ends this the way it should.
|
|
160
|
+
const answer = await Promise.race([
|
|
161
|
+
new Promise((resolve) => setTimeout(() => resolve(NOT_ANSWERED), POLL_INTERVAL_MS)),
|
|
162
|
+
asked ? asked.reply : new Promise(() => {}),
|
|
163
|
+
]);
|
|
164
|
+
if (answer !== NOT_ANSWERED) {
|
|
165
|
+
escape.answer = typeof answer === "string" ? answer : "";
|
|
166
|
+
return null;
|
|
138
167
|
}
|
|
139
|
-
/* keep waiting; the customer is elsewhere */
|
|
140
|
-
}
|
|
141
|
-
// A minute of dots is indistinguishable from a hang, and the reason this
|
|
142
|
-
// waits forever is usually something the customer can act on.
|
|
143
|
-
if (hint && !hinted && Date.now() - startedAt > hintAfterMs) {
|
|
144
|
-
hinted = true;
|
|
145
|
-
process.stdout.write(`\n${hint}\nStill waiting`);
|
|
146
168
|
}
|
|
147
|
-
|
|
148
|
-
|
|
169
|
+
} finally {
|
|
170
|
+
// Whichever side won, the question goes when the wait does.
|
|
171
|
+
asked?.cancel();
|
|
149
172
|
}
|
|
150
173
|
process.stdout.write("\n");
|
|
151
174
|
return null;
|
|
@@ -254,7 +277,7 @@ export async function awsCommand(args) {
|
|
|
254
277
|
// GitHub
|
|
255
278
|
// ---------------------------------------------------------------------------
|
|
256
279
|
|
|
257
|
-
export async function githubCommand(args) {
|
|
280
|
+
export async function githubCommand(args, { ensureDesktopApp = null } = {}) {
|
|
258
281
|
const token = requireKey(args);
|
|
259
282
|
|
|
260
283
|
// This route answers with a bare array; callBackendJson exposes it as `list`.
|
|
@@ -265,15 +288,24 @@ export async function githubCommand(args) {
|
|
|
265
288
|
|
|
266
289
|
const seen = new Set((await listRepos()).map((r) => r.id));
|
|
267
290
|
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
// tab into the hosted dashboard
|
|
271
|
-
// environment owns the App's setup URL.
|
|
272
|
-
//
|
|
273
|
-
//
|
|
291
|
+
// The desktop return is the right ending for a run started in a terminal: the
|
|
292
|
+
// CLI is already polling for the installation, and the web return 302s the
|
|
293
|
+
// tab into the hosted dashboard — an app they did not ask for, on whichever
|
|
294
|
+
// environment owns the App's setup URL.
|
|
295
|
+
//
|
|
296
|
+
// What it must never be is unconditional. That page's only action is a
|
|
297
|
+
// preman:// link, and a machine with no app answers one with a system dialog
|
|
298
|
+
// reading "There is no application set to open the URL preman://open". That is
|
|
299
|
+
// where a first run actually ended, every time: the app installs at the end of
|
|
300
|
+
// this walk and GitHub is asked about in the middle, so by the time this link
|
|
301
|
+
// was minted there had never been an app. A caller may put one there first;
|
|
302
|
+
// without one, the web return is the only ending that leads anywhere.
|
|
303
|
+
const hasDesktopApp = ensureDesktopApp
|
|
304
|
+
? await ensureDesktopApp()
|
|
305
|
+
: desktopAppInstalled(args.value("--dest", "/Applications"));
|
|
274
306
|
const started = await callBackendJson(args, "POST", "/integrations/github/app/install", {
|
|
275
307
|
token,
|
|
276
|
-
json: { return_to: "desktop" },
|
|
308
|
+
json: { return_to: hasDesktopApp ? "desktop" : "web" },
|
|
277
309
|
});
|
|
278
310
|
assertOk(started, "start GitHub install");
|
|
279
311
|
|
|
@@ -290,37 +322,64 @@ export async function githubCommand(args) {
|
|
|
290
322
|
// a success carrying no repositories means the App is installed and sharing
|
|
291
323
|
// nothing — two dead ends that are indistinguishable from the repository list.
|
|
292
324
|
let refresh = null;
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
325
|
+
const lookForRepos = async () => {
|
|
326
|
+
// Installing the App and having repositories appear are two events: the
|
|
327
|
+
// callback records the installation, and a refresh materialises the repos.
|
|
328
|
+
// Polling the repo list alone waits for something that may never arrive on
|
|
329
|
+
// its own.
|
|
330
|
+
refresh = await callBackendJson(args, "POST", "/integrations/github/app/refresh", {
|
|
331
|
+
token,
|
|
332
|
+
json: {},
|
|
333
|
+
});
|
|
334
|
+
// Compare against what existed before, so a user who already had repos
|
|
335
|
+
// connected is not told they are done the moment polling starts.
|
|
336
|
+
const fresh = (await listRepos()).filter((r) => !seen.has(r.id));
|
|
337
|
+
return fresh.length ? fresh : null;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// Waiting is the right default, because the install usually lands inside it.
|
|
341
|
+
// What it cannot be is the only thing on offer. Somebody who picked no
|
|
342
|
+
// repository, or installed onto an organisation where an owner has to approve
|
|
343
|
+
// it first, is watching dots for an event that is not coming, and the only
|
|
344
|
+
// way out of that was to guess that Ctrl-C here was safe.
|
|
345
|
+
const escape = {
|
|
346
|
+
question:
|
|
347
|
+
"Have you saved the repository selection on GitHub? [y/n, or Enter to keep waiting]: ",
|
|
348
|
+
answer: null,
|
|
349
|
+
};
|
|
350
|
+
let done = await waitFor("the installation", lookForRepos, {
|
|
351
|
+
timeoutMs: Number(process.env.PREMAN_GITHUB_POLL_MS) || GITHUB_POLL_TIMEOUT_MS,
|
|
352
|
+
hintAfterMs: Number(process.env.PREMAN_GITHUB_HINT_MS) || 20000,
|
|
353
|
+
hint: "Still nothing from GitHub — confirm a repository to come back here.",
|
|
354
|
+
escape,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// A yes buys one more look, not one more minute. The callback lands while the
|
|
358
|
+
// question is still on the screen often enough to be worth asking again, and
|
|
359
|
+
// if it has not by now, more dots will not change that.
|
|
360
|
+
if (!done && affirmative(escape.answer)) {
|
|
361
|
+
done = await lookForRepos().catch(() => null);
|
|
362
|
+
}
|
|
315
363
|
|
|
316
364
|
if (!done) {
|
|
317
|
-
process.stdout.write(githubHandOff(args, refresh));
|
|
365
|
+
process.stdout.write(githubHandOff(args, refresh, escape.answer));
|
|
318
366
|
return;
|
|
319
367
|
}
|
|
320
368
|
connected(`GitHub connected: ${done.length} repository(ies).`);
|
|
321
369
|
for (const repo of done.slice(0, 5)) process.stdout.write(` - ${repo.repo_url}\n`);
|
|
322
370
|
}
|
|
323
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Whether a free-text reply means yes.
|
|
374
|
+
*
|
|
375
|
+
* Silence is not consent anywhere else in this walk and it is not here either:
|
|
376
|
+
* only an explicit yes counts, and everything else is left for later.
|
|
377
|
+
*/
|
|
378
|
+
function affirmative(answer) {
|
|
379
|
+
const said = (answer || "").trim().toLowerCase();
|
|
380
|
+
return said === "y" || said === "yes";
|
|
381
|
+
}
|
|
382
|
+
|
|
324
383
|
/**
|
|
325
384
|
* Stop waiting, and name the dead end instead of the timeout.
|
|
326
385
|
*
|
|
@@ -328,7 +387,16 @@ export async function githubCommand(args) {
|
|
|
328
387
|
* finishes in the browser whether this process is watching or not, and the two
|
|
329
388
|
* ways it can complete and still leave PreMan with nothing are both actionable.
|
|
330
389
|
*/
|
|
331
|
-
function githubHandOff(args, refresh) {
|
|
390
|
+
function githubHandOff(args, refresh, answer = null) {
|
|
391
|
+
// A no is a decision, not a dead end. Reporting it as one would be telling
|
|
392
|
+
// somebody that something went wrong immediately after they said they had
|
|
393
|
+
// simply not done it yet.
|
|
394
|
+
if (answer !== null && !affirmative(answer)) {
|
|
395
|
+
return (
|
|
396
|
+
`Left for later, and nothing here is broken.\n` +
|
|
397
|
+
` Connect it whenever you like with '${cliInvocation()} github'.\n`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
332
400
|
const installed = Boolean(refresh?.ok) && Number(refresh.installations_refreshed || 0) > 0;
|
|
333
401
|
if (installed) {
|
|
334
402
|
return (
|
|
@@ -590,6 +658,12 @@ async function askStep(question, { assumeYes, canGoBack, defaultYes = true }) {
|
|
|
590
658
|
* only step that asks nothing: by then every decision is made, and a download is
|
|
591
659
|
* something to leave running, not something to make someone watch first.
|
|
592
660
|
*
|
|
661
|
+
* With one exception, and it is not a preference. Connecting a repository ends
|
|
662
|
+
* with GitHub handing the browser back through a preman:// link, and a machine
|
|
663
|
+
* with no app answers that with a system dialog rather than a window. So saying
|
|
664
|
+
* yes to the repository pulls the download forward to just before it; saying no
|
|
665
|
+
* leaves the order exactly as described above.
|
|
666
|
+
*
|
|
593
667
|
* The steps themselves live in connect/guide.js, which is also what
|
|
594
668
|
* `preman connect --guide` runs. Two walks that asked the same questions in
|
|
595
669
|
* different words with different defaults was the thing worth deleting.
|
|
@@ -631,6 +705,43 @@ export async function onboardCommand(
|
|
|
631
705
|
// is said in one place rather than before PreMan has looked at the code.
|
|
632
706
|
const checkout = await resolveCheckout(args, resolveApiKey(args));
|
|
633
707
|
|
|
708
|
+
// What the app install did, once, shared by the step that needs an app to
|
|
709
|
+
// exist and the step that owns installing one.
|
|
710
|
+
//
|
|
711
|
+
// GitHub's callback ends on a page whose only action is a preman:// link, and
|
|
712
|
+
// the app is the last step of this walk — so a first run always handed GitHub
|
|
713
|
+
// back to a machine with nothing to open it with, and macOS said so in a
|
|
714
|
+
// dialog. The download moves in front of that step when it is needed and stays
|
|
715
|
+
// where it was when it is not: someone who declines the repository never
|
|
716
|
+
// reaches this, and still gets the app at the end.
|
|
717
|
+
//
|
|
718
|
+
// Memoised because it is a hundred megabytes and both callers want the same
|
|
719
|
+
// copy of it. Answers whether there is now an app on this machine, which is
|
|
720
|
+
// the only part either caller can act on.
|
|
721
|
+
let desktopInstall = null;
|
|
722
|
+
const installedNow = (result) => result?.state === "installed" || result?.state === "current";
|
|
723
|
+
const ensureDesktopApp = async ({ onInstalled } = {}) => {
|
|
724
|
+
const already = desktopPrecheck(args);
|
|
725
|
+
if (already?.state === "installed") return true;
|
|
726
|
+
// --no-desktop, or a platform with no build. Nothing to install here, and so
|
|
727
|
+
// nothing that could ever answer a preman:// link either.
|
|
728
|
+
if (already) return false;
|
|
729
|
+
if (!desktopInstall) {
|
|
730
|
+
process.stdout.write(
|
|
731
|
+
"Getting the PreMan app first, so GitHub has somewhere to hand you back to.\n"
|
|
732
|
+
);
|
|
733
|
+
desktopInstall = await runDesktopInstall(args, {
|
|
734
|
+
install: () => installDesktop([...commandArgs], onInstalled ? { onInstalled } : undefined),
|
|
735
|
+
});
|
|
736
|
+
// Handed the session the moment it lands, before anything can launch it.
|
|
737
|
+
// The app reads one only when it starts, and the callback's deep link is a
|
|
738
|
+
// start — without this it opens on a login screen for an account signed in
|
|
739
|
+
// ninety seconds ago.
|
|
740
|
+
if (installedNow(desktopInstall)) writeDesktopSession(creds);
|
|
741
|
+
}
|
|
742
|
+
return installedNow(desktopInstall);
|
|
743
|
+
};
|
|
744
|
+
|
|
634
745
|
const steps = [
|
|
635
746
|
{
|
|
636
747
|
// No question. Everything below is a decision about code PreMan has not
|
|
@@ -672,7 +783,7 @@ export async function onboardCommand(
|
|
|
672
783
|
return `Connect ${checkout.slug} so PreMan can open pull requests for the ${found} endpoint${found === 1 ? "" : "s"} it just mapped?`;
|
|
673
784
|
},
|
|
674
785
|
run: async () => {
|
|
675
|
-
await githubCommand(args);
|
|
786
|
+
await githubCommand(args, { ensureDesktopApp });
|
|
676
787
|
// Finishing the App install is not the same as this repository being
|
|
677
788
|
// connected: the install shares whichever repositories the customer
|
|
678
789
|
// picked, which may not include this one. Re-asking is the difference
|
|
@@ -783,66 +894,55 @@ export async function onboardCommand(
|
|
|
783
894
|
// and someone who already has the app does not watch it download again — both
|
|
784
895
|
// of which happened when the check came after.
|
|
785
896
|
const desktop = desktopPrecheck(args);
|
|
786
|
-
if (desktop?.state === "installed") {
|
|
897
|
+
if (desktop?.state === "installed" || !desktop) {
|
|
787
898
|
steps.push({
|
|
788
899
|
name: "Desktop app",
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
//
|
|
792
|
-
//
|
|
793
|
-
//
|
|
900
|
+
// No question. See offerDesktop in connect/guide.js for why the ask went
|
|
901
|
+
// away; `--no-desktop` is the way past it.
|
|
902
|
+
//
|
|
903
|
+
// Installing and launching used to be one thing here, which is why the one
|
|
904
|
+
// run with nothing to install was also the only run that opened nothing: it
|
|
905
|
+
// stopped at the sentence and sent someone to a website instead of the app
|
|
906
|
+
// already sitting on their machine. They are two things now, and the
|
|
907
|
+
// install may have happened further up the walk, so this step is the launch
|
|
908
|
+
// and ensureDesktopApp decides whether anything is left to download.
|
|
794
909
|
run: async () => {
|
|
795
|
-
|
|
796
|
-
|
|
910
|
+
// Said only when this walk did not just download it. After an install of
|
|
911
|
+
// our own, announcing that it is already installed is the walk telling
|
|
912
|
+
// somebody about their own last thirty seconds.
|
|
913
|
+
if (!desktopInstall && desktopPrecheck(args)?.state === "installed") {
|
|
914
|
+
process.stdout.write(`${MARK.ok()} PreMan desktop app already installed.\n`);
|
|
915
|
+
}
|
|
916
|
+
let stopOpening = null;
|
|
797
917
|
try {
|
|
918
|
+
const ready = await ensureDesktopApp({
|
|
919
|
+
onInstalled: () => {
|
|
920
|
+
stopOpening ??= showOpeningDesktop();
|
|
921
|
+
},
|
|
922
|
+
});
|
|
923
|
+
if (!ready) {
|
|
924
|
+
if (desktopInstall?.state === "unsupported") {
|
|
925
|
+
// Not a failure: the download link has already been printed, and
|
|
926
|
+
// the account this step exists to create is finished either way.
|
|
927
|
+
process.stdout.write("Sign in there with the account you just used.\n");
|
|
928
|
+
}
|
|
929
|
+
// A failed install already said so through runDesktopInstall, and its
|
|
930
|
+
// state is what keeps this out of the summary as a skip.
|
|
931
|
+
return desktopInstall || { state: "skipped" };
|
|
932
|
+
}
|
|
933
|
+
stopOpening ??= showOpeningDesktop();
|
|
798
934
|
return {
|
|
935
|
+
...(desktopInstall || {}),
|
|
936
|
+
// Onboarding installs *and* hands the fresh session over, so the app
|
|
937
|
+
// opens already signed in rather than on a login screen for the
|
|
938
|
+
// account created ninety seconds ago.
|
|
799
939
|
appOpened: await showDesktopSignedIn(openDesktopSignedIn, args, creds, stopOpening),
|
|
800
940
|
};
|
|
801
941
|
} finally {
|
|
802
|
-
stopOpening();
|
|
942
|
+
stopOpening?.();
|
|
803
943
|
}
|
|
804
944
|
},
|
|
805
945
|
});
|
|
806
|
-
} else if (!desktop) {
|
|
807
|
-
steps.push({
|
|
808
|
-
name: "Desktop app",
|
|
809
|
-
// No question. See offerDesktop in connect/guide.js for why the ask went
|
|
810
|
-
// away; `--no-desktop` is the way past it.
|
|
811
|
-
run: () =>
|
|
812
|
-
runDesktopInstall(args, {
|
|
813
|
-
// Onboarding installs *and* hands the fresh session over, so the app
|
|
814
|
-
// opens already signed in rather than on a login screen for the
|
|
815
|
-
// account created ninety seconds ago.
|
|
816
|
-
install: async () => {
|
|
817
|
-
let stopOpening = null;
|
|
818
|
-
try {
|
|
819
|
-
const installed = await installDesktop([...commandArgs], {
|
|
820
|
-
onInstalled: () => {
|
|
821
|
-
stopOpening ??= showOpeningDesktop();
|
|
822
|
-
},
|
|
823
|
-
});
|
|
824
|
-
if (installed?.state === "unsupported") {
|
|
825
|
-
// Not a failure: the download link has already been printed, and
|
|
826
|
-
// the account this step exists to create is finished either way.
|
|
827
|
-
process.stdout.write("Sign in there with the account you just used.\n");
|
|
828
|
-
return installed;
|
|
829
|
-
}
|
|
830
|
-
stopOpening ??= showOpeningDesktop();
|
|
831
|
-
return {
|
|
832
|
-
...installed,
|
|
833
|
-
appOpened: await showDesktopSignedIn(
|
|
834
|
-
openDesktopSignedIn,
|
|
835
|
-
args,
|
|
836
|
-
creds,
|
|
837
|
-
stopOpening
|
|
838
|
-
),
|
|
839
|
-
};
|
|
840
|
-
} finally {
|
|
841
|
-
stopOpening?.();
|
|
842
|
-
}
|
|
843
|
-
},
|
|
844
|
-
}),
|
|
845
|
-
});
|
|
846
946
|
}
|
|
847
947
|
|
|
848
948
|
// Outcome per step rather than three lists, so revisiting a step replaces its
|
package/bin/shared.js
CHANGED
|
@@ -321,6 +321,48 @@ export async function promptText(question) {
|
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Ask a question that whatever we are waiting for may answer first.
|
|
326
|
+
*
|
|
327
|
+
* Returns the reply and a `cancel` for when it arrives too late to matter. A
|
|
328
|
+
* plain promptText cannot be raced: abandoning its promise leaves the readline
|
|
329
|
+
* interface open, an open interface holds stdin, and stdin holds the process
|
|
330
|
+
* open with it -- so a wait that ended on its own would sit there afterwards on
|
|
331
|
+
* a question nobody needed answering.
|
|
332
|
+
*
|
|
333
|
+
* Where there is no terminal there is nobody to ask, so the reply is a promise
|
|
334
|
+
* that never settles. A race is then decided entirely by the other side, which
|
|
335
|
+
* is how an unattended run goes on behaving like one.
|
|
336
|
+
*/
|
|
337
|
+
export function askWhileWaiting(question) {
|
|
338
|
+
const nobodyThere =
|
|
339
|
+
!process.stdin.isTTY || process.stdin.readableEnded || process.stdin.destroyed;
|
|
340
|
+
if (nobodyThere) return { reply: new Promise(() => {}), cancel: () => {} };
|
|
341
|
+
|
|
342
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
343
|
+
let cancelled = false;
|
|
344
|
+
let settle = null;
|
|
345
|
+
const reply = new Promise((resolve) => {
|
|
346
|
+
settle = (value) => {
|
|
347
|
+
if (settle.done) return;
|
|
348
|
+
settle.done = true;
|
|
349
|
+
resolve(value);
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
rl.question(question).then(
|
|
353
|
+
(value) => settle(value.trim()),
|
|
354
|
+
() => settle("")
|
|
355
|
+
);
|
|
356
|
+
rl.once("close", () => settle(cancelled ? null : ""));
|
|
357
|
+
return {
|
|
358
|
+
reply,
|
|
359
|
+
cancel: () => {
|
|
360
|
+
cancelled = true;
|
|
361
|
+
rl.close();
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
324
366
|
export async function promptSecret(question) {
|
|
325
367
|
if (!process.stdin.isTTY || !process.stdin.setRawMode) {
|
|
326
368
|
return promptText(question);
|
package/bin/verify.js
CHANGED
|
@@ -27,7 +27,7 @@ import { changedFilesForPush, readChangedFiles, readPushRefsFromStdin, repoRoot
|
|
|
27
27
|
import { missingMountPrefix, resolveLocalTarget } from "./detect.js";
|
|
28
28
|
import { formatDashboardLink, formatPushLink } from "./link.js";
|
|
29
29
|
import { createReporter } from "./progress.js";
|
|
30
|
-
import { backendUrl, callBackendJson, cliInvocation, frontendUrl, makeArgs, nowMs, resolveApiKey } from "./shared.js";
|
|
30
|
+
import { backendUrl, callBackendJson, cliInvocation, describeFailure, frontendUrl, makeArgs, nowMs, resolveApiKey } from "./shared.js";
|
|
31
31
|
|
|
32
32
|
export const VERIFY_HELP = `
|
|
33
33
|
Verify options:
|
|
@@ -250,7 +250,11 @@ async function fetchInventory(args, token) {
|
|
|
250
250
|
if (!result.ok) {
|
|
251
251
|
return {
|
|
252
252
|
endpoints,
|
|
253
|
-
|
|
253
|
+
// FastAPI answers a rejected request with an object, and a validation
|
|
254
|
+
// error with an array of them. Interpolated straight into the skip
|
|
255
|
+
// line, both read as "[object Object]" -- which is what every push
|
|
256
|
+
// against a backend that said no has printed.
|
|
257
|
+
error: describeFailure(result, `status ${result.status_code}`),
|
|
254
258
|
};
|
|
255
259
|
}
|
|
256
260
|
|
package/package.json
CHANGED
package/dist/auth_flow_ui.d.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auth flow endpoints (signup, verify OTP, login, resend OTP) with JSON schemas
|
|
3
|
-
* for share_endpoints_with_ui / agent-sessions push.
|
|
4
|
-
*/
|
|
5
|
-
export type AuthFlowEndpoint = {
|
|
6
|
-
method: string;
|
|
7
|
-
path_template: string;
|
|
8
|
-
description: string;
|
|
9
|
-
tags: string[];
|
|
10
|
-
source_file: string;
|
|
11
|
-
request_body_schema: Record<string, unknown>;
|
|
12
|
-
response_schema: Record<string, unknown>;
|
|
13
|
-
mcp_tool?: string;
|
|
14
|
-
};
|
|
15
|
-
/** Core auth endpoints aligned with routes/auth/routes.py and user_auth_* MCP tools. */
|
|
16
|
-
export declare function buildAuthFlowEndpoints(): AuthFlowEndpoint[];
|
|
17
|
-
export type ShareAuthFlowResult = {
|
|
18
|
-
session_id: string;
|
|
19
|
-
url: string;
|
|
20
|
-
endpoint_count: number;
|
|
21
|
-
user_id: number | null;
|
|
22
|
-
auto_discoverable: boolean;
|
|
23
|
-
upstream_base_url: string;
|
|
24
|
-
endpoints: AuthFlowEndpoint[];
|
|
25
|
-
ui: {
|
|
26
|
-
url: string;
|
|
27
|
-
note: string;
|
|
28
|
-
};
|
|
29
|
-
related_tools: string[];
|
|
30
|
-
};
|
|
31
|
-
export declare function shareAuthFlowToUi(opts: {
|
|
32
|
-
backendUrl: string;
|
|
33
|
-
frontendUrl: string;
|
|
34
|
-
upstreamBaseUrl?: string;
|
|
35
|
-
sessionId?: string;
|
|
36
|
-
intent?: string;
|
|
37
|
-
apiKey?: string;
|
|
38
|
-
}): Promise<ShareAuthFlowResult>;
|
package/dist/auth_flow_ui.js
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auth flow endpoints (signup, verify OTP, login, resend OTP) with JSON schemas
|
|
3
|
-
* for share_endpoints_with_ui / agent-sessions push.
|
|
4
|
-
*/
|
|
5
|
-
const EMAIL_PROP = { type: "string", format: "email", description: "User email" };
|
|
6
|
-
const PASSWORD_PROP = {
|
|
7
|
-
type: "string",
|
|
8
|
-
minLength: 6,
|
|
9
|
-
description: "Password (min 6 characters on the server)",
|
|
10
|
-
};
|
|
11
|
-
const OTP_PROP = { type: "string", description: "6-digit code from email" };
|
|
12
|
-
const TOKEN_RESPONSE = {
|
|
13
|
-
type: "object",
|
|
14
|
-
properties: {
|
|
15
|
-
access_token: { type: "string", description: "JWT bearer token" },
|
|
16
|
-
token_type: { type: "string", enum: ["bearer"] },
|
|
17
|
-
user: {
|
|
18
|
-
type: "object",
|
|
19
|
-
properties: {
|
|
20
|
-
id: { type: "string" },
|
|
21
|
-
email: { type: "string", format: "email" },
|
|
22
|
-
},
|
|
23
|
-
required: ["id", "email"],
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
required: ["access_token", "token_type", "user"],
|
|
27
|
-
};
|
|
28
|
-
const OTP_SENT_RESPONSE = {
|
|
29
|
-
type: "object",
|
|
30
|
-
properties: {
|
|
31
|
-
message: { type: "string" },
|
|
32
|
-
email_sent: { type: "boolean" },
|
|
33
|
-
},
|
|
34
|
-
required: ["message", "email_sent"],
|
|
35
|
-
};
|
|
36
|
-
/** Core auth endpoints aligned with routes/auth/routes.py and user_auth_* MCP tools. */
|
|
37
|
-
export function buildAuthFlowEndpoints() {
|
|
38
|
-
return [
|
|
39
|
-
{
|
|
40
|
-
method: "POST",
|
|
41
|
-
path_template: "/auth/signup",
|
|
42
|
-
description: "Register with email and password. Sends OTP to email; next: verify-otp.",
|
|
43
|
-
tags: ["auth", "signup"],
|
|
44
|
-
source_file: "routes/auth/routes.py",
|
|
45
|
-
mcp_tool: "user_auth_signup",
|
|
46
|
-
request_body_schema: {
|
|
47
|
-
type: "object",
|
|
48
|
-
properties: { email: EMAIL_PROP, password: PASSWORD_PROP },
|
|
49
|
-
required: ["email", "password"],
|
|
50
|
-
additionalProperties: false,
|
|
51
|
-
},
|
|
52
|
-
response_schema: {
|
|
53
|
-
type: "object",
|
|
54
|
-
properties: {
|
|
55
|
-
message: { type: "string" },
|
|
56
|
-
user_id: { type: "string" },
|
|
57
|
-
email: { type: "string", format: "email" },
|
|
58
|
-
email_sent: { type: "boolean" },
|
|
59
|
-
},
|
|
60
|
-
required: ["message", "user_id", "email", "email_sent"],
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
method: "POST",
|
|
65
|
-
path_template: "/auth/verify-otp",
|
|
66
|
-
description: "Verify email OTP after signup; returns JWT access_token.",
|
|
67
|
-
tags: ["auth", "otp"],
|
|
68
|
-
source_file: "routes/auth/routes.py",
|
|
69
|
-
mcp_tool: "user_auth_verify_otp",
|
|
70
|
-
request_body_schema: {
|
|
71
|
-
type: "object",
|
|
72
|
-
properties: { email: EMAIL_PROP, otp: OTP_PROP },
|
|
73
|
-
required: ["email", "otp"],
|
|
74
|
-
additionalProperties: false,
|
|
75
|
-
},
|
|
76
|
-
response_schema: TOKEN_RESPONSE,
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
method: "POST",
|
|
80
|
-
path_template: "/auth/login",
|
|
81
|
-
description: "Login with email and password. Returns access_token if email is verified.",
|
|
82
|
-
tags: ["auth", "login"],
|
|
83
|
-
source_file: "routes/auth/routes.py",
|
|
84
|
-
mcp_tool: "user_auth_login",
|
|
85
|
-
request_body_schema: {
|
|
86
|
-
type: "object",
|
|
87
|
-
properties: { email: EMAIL_PROP, password: PASSWORD_PROP },
|
|
88
|
-
required: ["email", "password"],
|
|
89
|
-
additionalProperties: false,
|
|
90
|
-
},
|
|
91
|
-
response_schema: TOKEN_RESPONSE,
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
method: "POST",
|
|
95
|
-
path_template: "/auth/resend-otp",
|
|
96
|
-
description: "Send (resend) verification OTP to email.",
|
|
97
|
-
tags: ["auth", "otp"],
|
|
98
|
-
source_file: "routes/auth/routes.py",
|
|
99
|
-
mcp_tool: "user_auth_resend_otp",
|
|
100
|
-
request_body_schema: {
|
|
101
|
-
type: "object",
|
|
102
|
-
properties: { email: EMAIL_PROP },
|
|
103
|
-
required: ["email"],
|
|
104
|
-
additionalProperties: false,
|
|
105
|
-
},
|
|
106
|
-
response_schema: OTP_SENT_RESPONSE,
|
|
107
|
-
},
|
|
108
|
-
];
|
|
109
|
-
}
|
|
110
|
-
export async function shareAuthFlowToUi(opts) {
|
|
111
|
-
const backend = opts.backendUrl.replace(/\/+$/, "");
|
|
112
|
-
const frontend = opts.frontendUrl.replace(/\/+$/, "");
|
|
113
|
-
const apiKey = opts.apiKey?.trim();
|
|
114
|
-
if (!apiKey) {
|
|
115
|
-
throw new Error("PreMan authentication required. Run preman_login first so auth-flow sessions can stream to your dashboard.");
|
|
116
|
-
}
|
|
117
|
-
const upstream = (opts.upstreamBaseUrl || backend).replace(/\/+$/, "") || backend;
|
|
118
|
-
const sessionId = opts.sessionId?.trim() || crypto.randomUUID();
|
|
119
|
-
const endpoints = buildAuthFlowEndpoints().map((ep) => ({
|
|
120
|
-
...ep,
|
|
121
|
-
base_url: upstream,
|
|
122
|
-
}));
|
|
123
|
-
const resp = await fetch(`${backend}/agent-sessions/${encodeURIComponent(sessionId)}/endpoints`, {
|
|
124
|
-
method: "POST",
|
|
125
|
-
headers: {
|
|
126
|
-
"Content-Type": "application/json",
|
|
127
|
-
Accept: "application/json",
|
|
128
|
-
Authorization: `Bearer ${apiKey}`,
|
|
129
|
-
},
|
|
130
|
-
body: JSON.stringify({
|
|
131
|
-
endpoints,
|
|
132
|
-
upstream_base_url: upstream,
|
|
133
|
-
intent: opts.intent || "Auth flow: signup, verify OTP, login, resend OTP",
|
|
134
|
-
client_label: "premanmcp",
|
|
135
|
-
}),
|
|
136
|
-
});
|
|
137
|
-
const text = await resp.text();
|
|
138
|
-
let body = {};
|
|
139
|
-
try {
|
|
140
|
-
body = text ? JSON.parse(text) : {};
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
throw new Error(`Agent session push failed: ${resp.status} ${text.slice(0, 500)}`);
|
|
144
|
-
}
|
|
145
|
-
if (!resp.ok) {
|
|
146
|
-
throw new Error(String(body.detail ?? body.error ?? `Agent session push failed: ${resp.status}`));
|
|
147
|
-
}
|
|
148
|
-
const sid = String(body.id ?? sessionId);
|
|
149
|
-
const url = `${frontend}/try?session=${encodeURIComponent(sid)}`;
|
|
150
|
-
return {
|
|
151
|
-
session_id: sid,
|
|
152
|
-
url,
|
|
153
|
-
endpoint_count: Number(body.endpoint_count ?? endpoints.length),
|
|
154
|
-
user_id: typeof body.user_id === "number" ? body.user_id : null,
|
|
155
|
-
auto_discoverable: Boolean(body.auto_discoverable),
|
|
156
|
-
upstream_base_url: upstream,
|
|
157
|
-
endpoints: buildAuthFlowEndpoints(),
|
|
158
|
-
ui: {
|
|
159
|
-
url,
|
|
160
|
-
note: "Open in Cursor Agent Browser or the Playground session list. Test signup → verify-otp → login, or resend-otp. " +
|
|
161
|
-
"Schemas are prefilled from routes/auth Pydantic models.",
|
|
162
|
-
},
|
|
163
|
-
related_tools: [
|
|
164
|
-
"user_auth_signup",
|
|
165
|
-
"user_auth_verify_otp",
|
|
166
|
-
"user_auth_login",
|
|
167
|
-
"user_auth_resend_otp",
|
|
168
|
-
],
|
|
169
|
-
};
|
|
170
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
export declare const MCP_PREVIEW_RESOURCE_URI = "ui://preman/mcp-preview";
|
|
2
|
-
export declare const RESOURCE_URI_META_KEY = "ui/resourceUri";
|
|
3
|
-
export declare function escapeHtmlAttr(s: string): string;
|
|
4
|
-
export declare function escapeHtmlText(s: string): string;
|
|
5
|
-
export interface PreviewTool {
|
|
6
|
-
name?: string;
|
|
7
|
-
description?: string;
|
|
8
|
-
inputSchema?: unknown;
|
|
9
|
-
_endpoint_ref?: {
|
|
10
|
-
method?: string;
|
|
11
|
-
path_template?: string;
|
|
12
|
-
tags?: string[];
|
|
13
|
-
source?: string;
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
export interface PreviewPayload {
|
|
17
|
-
intent?: string | string[];
|
|
18
|
-
selection_method?: string | string[];
|
|
19
|
-
rationale?: string | string[] | Record<string, string>;
|
|
20
|
-
selected_count?: number;
|
|
21
|
-
spec_preview?: {
|
|
22
|
-
upstream_base_url?: string | string[];
|
|
23
|
-
tools?: PreviewTool[];
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
export declare function buildConversionPanelHtml(data: PreviewPayload): string;
|
|
27
|
-
export declare function writeMcpPreviewFile(panelHtml: string): Promise<{
|
|
28
|
-
absolutePath: string;
|
|
29
|
-
fileUrl: string;
|
|
30
|
-
}>;
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Two-pane HTML for mcp_preview — shared by the MCP stdio server and
|
|
3
|
-
* scripts/emit-mcp-preview.mjs (browser tab fallback when Cursor does not render mcp-app).
|
|
4
|
-
*/
|
|
5
|
-
import fs from "node:fs/promises";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { pathToFileURL } from "node:url";
|
|
8
|
-
export const MCP_PREVIEW_RESOURCE_URI = "ui://preman/mcp-preview";
|
|
9
|
-
export const RESOURCE_URI_META_KEY = "ui/resourceUri";
|
|
10
|
-
export function escapeHtmlAttr(s) {
|
|
11
|
-
return s.replace(/&/g, "&").replace(/"/g, """);
|
|
12
|
-
}
|
|
13
|
-
export function escapeHtmlText(s) {
|
|
14
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
15
|
-
}
|
|
16
|
-
export function buildConversionPanelHtml(data) {
|
|
17
|
-
const toStr = (v) => {
|
|
18
|
-
if (v == null)
|
|
19
|
-
return "";
|
|
20
|
-
if (Array.isArray(v))
|
|
21
|
-
return v.filter((x) => x != null).map((x) => String(x)).join("; ");
|
|
22
|
-
if (typeof v === "object") {
|
|
23
|
-
try {
|
|
24
|
-
return Object.entries(v)
|
|
25
|
-
.map(([k, val]) => `${k}: ${val == null ? "" : String(val)}`)
|
|
26
|
-
.join(" · ");
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
try {
|
|
30
|
-
return JSON.stringify(v);
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return String(v);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return String(v);
|
|
38
|
-
};
|
|
39
|
-
const intent = toStr(data.intent).trim() || "(unspecified)";
|
|
40
|
-
const method = toStr(data.selection_method).trim() || "unknown";
|
|
41
|
-
const rationale = toStr(data.rationale).trim();
|
|
42
|
-
const upstream = toStr(data.spec_preview?.upstream_base_url).trim();
|
|
43
|
-
const tools = Array.isArray(data.spec_preview?.tools) ? data.spec_preview.tools : [];
|
|
44
|
-
const selectedCount = typeof data.selected_count === "number" ? data.selected_count : tools.length;
|
|
45
|
-
const linkKey = (m, p) => `${(m || "").toUpperCase()} ${p || ""}`.trim();
|
|
46
|
-
const endpointRows = tools
|
|
47
|
-
.map((t) => {
|
|
48
|
-
const ref = t._endpoint_ref ?? {};
|
|
49
|
-
const m = (ref.method ?? "").toUpperCase();
|
|
50
|
-
const p = ref.path_template ?? "";
|
|
51
|
-
const key = linkKey(m, p);
|
|
52
|
-
const tags = Array.isArray(ref.tags) && ref.tags.length > 0 ? ref.tags.join(", ") : "";
|
|
53
|
-
return `
|
|
54
|
-
<div class="row" data-link="${escapeHtmlAttr(key)}">
|
|
55
|
-
<div class="row-head">
|
|
56
|
-
<span class="method ${escapeHtmlAttr(m)}">${escapeHtmlText(m)}</span>
|
|
57
|
-
<span class="path">${escapeHtmlText(p)}</span>
|
|
58
|
-
</div>
|
|
59
|
-
${tags ? `<div class="row-meta">tags: ${escapeHtmlText(tags)}</div>` : ""}
|
|
60
|
-
</div>`;
|
|
61
|
-
})
|
|
62
|
-
.join("");
|
|
63
|
-
const toolRows = tools
|
|
64
|
-
.map((t) => {
|
|
65
|
-
const ref = t._endpoint_ref ?? {};
|
|
66
|
-
const key = linkKey(ref.method, ref.path_template);
|
|
67
|
-
const schemaJson = (() => {
|
|
68
|
-
try {
|
|
69
|
-
return JSON.stringify(t.inputSchema ?? {}, null, 2);
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
return "{}";
|
|
73
|
-
}
|
|
74
|
-
})();
|
|
75
|
-
return `
|
|
76
|
-
<div class="row" data-link="${escapeHtmlAttr(key)}">
|
|
77
|
-
<div class="tool-name">${escapeHtmlText(t.name ?? "(unnamed)")}</div>
|
|
78
|
-
${t.description ? `<div class="tool-desc">${escapeHtmlText(t.description)}</div>` : ""}
|
|
79
|
-
<pre class="tool-schema">${escapeHtmlText(schemaJson)}</pre>
|
|
80
|
-
</div>`;
|
|
81
|
-
})
|
|
82
|
-
.join("");
|
|
83
|
-
const empty = tools.length === 0
|
|
84
|
-
? `<div class="empty">No matching endpoints. Try a different intent or run <code>verify_endpoints_live</code> first.</div>`
|
|
85
|
-
: "";
|
|
86
|
-
return `<!DOCTYPE html>
|
|
87
|
-
<html lang="en">
|
|
88
|
-
<head>
|
|
89
|
-
<meta charset="UTF-8" />
|
|
90
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
91
|
-
<title>PreMan · MCP Preview</title>
|
|
92
|
-
<style>
|
|
93
|
-
:root { color-scheme: light dark; --bg:#0d1117; --bg2:#161b22; --border:#30363d; --fg:#e6edf3; --muted:#8b949e; --accent:#58a6ff; --green:#2ea043; --orange:#bf8700; --red:#cf222e; --purple:#8957e5; }
|
|
94
|
-
* { margin:0; padding:0; box-sizing:border-box; }
|
|
95
|
-
html, body { height:100%; background:var(--bg); color:var(--fg); font-family:-apple-system,BlinkMacSystemFont,sans-serif; font-size:13px; }
|
|
96
|
-
body { display:flex; flex-direction:column; overflow:hidden; }
|
|
97
|
-
header { padding:10px 14px; border-bottom:1px solid var(--border); background:var(--bg2); flex-shrink:0; }
|
|
98
|
-
header h1 { font-size:13px; font-weight:600; color:var(--accent); }
|
|
99
|
-
header .meta { font-size:11px; color:var(--muted); margin-top:3px; }
|
|
100
|
-
header .meta strong { color:var(--fg); font-weight:600; }
|
|
101
|
-
header .rationale { font-size:11px; color:var(--muted); margin-top:5px; font-style:italic; max-width:900px; }
|
|
102
|
-
.columns { flex:1; display:grid; grid-template-columns:1fr 1fr; gap:1px; background:var(--border); overflow:hidden; min-height:0; }
|
|
103
|
-
.col { background:var(--bg); overflow-y:auto; }
|
|
104
|
-
.col-header { padding:7px 12px; font-size:10px; font-weight:700; letter-spacing:0.06em; text-transform:uppercase; color:var(--muted); border-bottom:1px solid var(--border); position:sticky; top:0; background:var(--bg2); z-index:1; }
|
|
105
|
-
.row { padding:9px 12px; border-bottom:1px solid var(--border); transition:background 0.1s; }
|
|
106
|
-
.row:last-child { border-bottom:none; }
|
|
107
|
-
.row:hover, .row.linked { background:rgba(88,166,255,0.08); }
|
|
108
|
-
.row-head { display:flex; align-items:center; gap:8px; }
|
|
109
|
-
.row-meta { font-size:10px; color:var(--muted); margin-top:3px; padding-left:56px; }
|
|
110
|
-
.method { display:inline-block; min-width:48px; text-align:center; padding:2px 6px; border-radius:3px; font-weight:700; font-size:10px; color:white; font-family:SFMono-Regular,Consolas,monospace; }
|
|
111
|
-
.method.GET{background:var(--accent);} .method.POST{background:var(--green);} .method.PATCH{background:var(--orange);} .method.PUT{background:var(--purple);} .method.DELETE{background:var(--red);}
|
|
112
|
-
.path { font-family:SFMono-Regular,Consolas,monospace; font-size:12px; word-break:break-all; }
|
|
113
|
-
.tool-name { font-family:SFMono-Regular,Consolas,monospace; font-size:12px; font-weight:600; color:var(--accent); }
|
|
114
|
-
.tool-desc { font-size:11px; color:var(--muted); margin-top:3px; line-height:1.4; }
|
|
115
|
-
.tool-schema { font-family:SFMono-Regular,Consolas,monospace; font-size:10px; background:var(--bg2); padding:6px 8px; border-radius:3px; margin-top:6px; white-space:pre-wrap; word-break:break-all; max-height:120px; overflow-y:auto; line-height:1.4; color:var(--muted); }
|
|
116
|
-
.empty { padding:24px; text-align:center; color:var(--muted); font-style:italic; }
|
|
117
|
-
.empty code { font-family:SFMono-Regular,Consolas,monospace; color:var(--accent); background:var(--bg2); padding:1px 5px; border-radius:3px; font-style:normal; }
|
|
118
|
-
footer { padding:8px 14px; border-top:1px solid var(--border); background:var(--bg2); display:flex; gap:10px; align-items:center; flex-shrink:0; font-size:11px; color:var(--muted); }
|
|
119
|
-
footer .upstream { font-family:SFMono-Regular,Consolas,monospace; color:var(--accent); }
|
|
120
|
-
footer .deploy-hint { margin-left:auto; }
|
|
121
|
-
footer .deploy-hint code { font-family:SFMono-Regular,Consolas,monospace; color:var(--fg); background:var(--bg); padding:2px 6px; border-radius:3px; }
|
|
122
|
-
</style>
|
|
123
|
-
</head>
|
|
124
|
-
<body>
|
|
125
|
-
<header>
|
|
126
|
-
<h1>API → MCP Conversion preview</h1>
|
|
127
|
-
<div class="meta">
|
|
128
|
-
Intent: <strong>${escapeHtmlText(intent)}</strong> · Selected <strong>${selectedCount}</strong> endpoint${selectedCount === 1 ? "" : "s"} · Method: ${escapeHtmlText(method)}
|
|
129
|
-
</div>
|
|
130
|
-
${rationale ? `<div class="rationale">${escapeHtmlText(rationale)}</div>` : ""}
|
|
131
|
-
</header>
|
|
132
|
-
<main class="columns">
|
|
133
|
-
<div class="col">
|
|
134
|
-
<div class="col-header">Your API endpoints (${tools.length})</div>
|
|
135
|
-
${endpointRows}${tools.length === 0 ? empty : ""}
|
|
136
|
-
</div>
|
|
137
|
-
<div class="col">
|
|
138
|
-
<div class="col-header">Generated MCP tools (${tools.length})</div>
|
|
139
|
-
${toolRows}${tools.length === 0 ? empty : ""}
|
|
140
|
-
</div>
|
|
141
|
-
</main>
|
|
142
|
-
<footer>
|
|
143
|
-
<span>Upstream: <span class="upstream">${escapeHtmlText(upstream || "(not set)")}</span></span>
|
|
144
|
-
<span class="deploy-hint">Next: ask the agent to call <code>mcp_deploy</code></span>
|
|
145
|
-
</footer>
|
|
146
|
-
<script>
|
|
147
|
-
document.querySelectorAll('.row[data-link]').forEach(row => {
|
|
148
|
-
const key = row.getAttribute('data-link');
|
|
149
|
-
if (!key) return;
|
|
150
|
-
const matches = () => document.querySelectorAll('[data-link="' + CSS.escape(key) + '"]');
|
|
151
|
-
row.addEventListener('mouseenter', () => matches().forEach(r => r.classList.add('linked')));
|
|
152
|
-
row.addEventListener('mouseleave', () => matches().forEach(r => r.classList.remove('linked')));
|
|
153
|
-
});
|
|
154
|
-
</script>
|
|
155
|
-
</body>
|
|
156
|
-
</html>`;
|
|
157
|
-
}
|
|
158
|
-
export async function writeMcpPreviewFile(panelHtml) {
|
|
159
|
-
const outPath = path.join(process.cwd(), "preman-mcp", "mcp-preview-last.html");
|
|
160
|
-
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
161
|
-
await fs.writeFile(outPath, panelHtml, "utf8");
|
|
162
|
-
return {
|
|
163
|
-
absolutePath: outPath,
|
|
164
|
-
fileUrl: pathToFileURL(outPath).href,
|
|
165
|
-
};
|
|
166
|
-
}
|
package/dist/user_auth_flow.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* App user auth (JWT) — tools that call the FastAPI backend at PREMAN_BACKEND /auth/*.
|
|
3
|
-
* Separate from preman_login (device flow + pm_live_ API key). No API key is required
|
|
4
|
-
* for these tools; for JWT-protected routes, pass the access_token from login/verify.
|
|
5
|
-
*/
|
|
6
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
-
/**
|
|
8
|
-
* Register MCP tools for routes/auth (email, OTP, password, JWT).
|
|
9
|
-
* Proxies to the same process as preman-local (PREMAN_BACKEND, e.g. http://127.0.0.1:8000).
|
|
10
|
-
*/
|
|
11
|
-
export declare function registerUserAuthFlowTools(server: McpServer, backendUrl: string): void;
|
package/dist/user_auth_flow.js
DELETED
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
function toolError(message, code = "backend_error", hints) {
|
|
3
|
-
const payload = { error: message, error_code: code };
|
|
4
|
-
if (hints)
|
|
5
|
-
payload._agent_hints = hints;
|
|
6
|
-
return {
|
|
7
|
-
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
8
|
-
isError: true,
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
function jsonOk(obj) {
|
|
12
|
-
return { content: [{ type: "text", text: JSON.stringify(obj) }] };
|
|
13
|
-
}
|
|
14
|
-
function normalizeBase(backendUrl) {
|
|
15
|
-
return backendUrl.replace(/\/+$/, "");
|
|
16
|
-
}
|
|
17
|
-
async function callAuthJson(base, method, path, opts) {
|
|
18
|
-
const url = new URL(path.startsWith("/") ? path.slice(1) : path, `${base}/`);
|
|
19
|
-
if (opts?.query) {
|
|
20
|
-
for (const [k, v] of Object.entries(opts.query)) {
|
|
21
|
-
if (v != null && v !== "")
|
|
22
|
-
url.searchParams.set(k, v);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
const headers = { Accept: "application/json" };
|
|
26
|
-
const hasBody = opts?.json != null && (method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE");
|
|
27
|
-
if (hasBody) {
|
|
28
|
-
headers["Content-Type"] = "application/json";
|
|
29
|
-
}
|
|
30
|
-
if (opts?.token) {
|
|
31
|
-
headers.Authorization = `Bearer ${opts.token.trim()}`;
|
|
32
|
-
}
|
|
33
|
-
const init = { method, headers };
|
|
34
|
-
if (hasBody && opts?.json) {
|
|
35
|
-
init.body = JSON.stringify(opts.json);
|
|
36
|
-
}
|
|
37
|
-
const resp = await fetch(url, init);
|
|
38
|
-
const text = await resp.text();
|
|
39
|
-
let parsed;
|
|
40
|
-
try {
|
|
41
|
-
parsed = text ? JSON.parse(text) : {};
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
parsed = { raw: text };
|
|
45
|
-
}
|
|
46
|
-
const body = typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
47
|
-
? parsed
|
|
48
|
-
: { value: parsed };
|
|
49
|
-
return {
|
|
50
|
-
status_code: resp.status,
|
|
51
|
-
ok: resp.ok,
|
|
52
|
-
...body,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Register MCP tools for routes/auth (email, OTP, password, JWT).
|
|
57
|
-
* Proxies to the same process as preman-local (PREMAN_BACKEND, e.g. http://127.0.0.1:8000).
|
|
58
|
-
*/
|
|
59
|
-
export function registerUserAuthFlowTools(server, backendUrl) {
|
|
60
|
-
const base = normalizeBase(backendUrl);
|
|
61
|
-
server.tool("user_auth_start_signup", "Start signup with email only. Sends an OTP; next call user_auth_set_password with email, OTP, and new password. Uses POST /auth/start-signup on PREMAN_BACKEND (no API key). If the backend does not support this endpoint yet, use user_auth_signup instead.", {
|
|
62
|
-
email: z.string().describe("User email"),
|
|
63
|
-
}, async (args) => {
|
|
64
|
-
try {
|
|
65
|
-
const r = await callAuthJson(base, "POST", "/auth/start-signup", {
|
|
66
|
-
json: { email: args.email },
|
|
67
|
-
});
|
|
68
|
-
if (!r.ok) {
|
|
69
|
-
if (r.status_code === 404) {
|
|
70
|
-
return toolError("This backend does not support email-only signup yet. Use user_auth_signup with email and password, then user_auth_verify_otp.", "backend_error", {
|
|
71
|
-
next_actions: ["Call user_auth_signup with email and password.", "Then call user_auth_verify_otp with the email code."],
|
|
72
|
-
related_tools: ["user_auth_signup", "user_auth_verify_otp"],
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
return toolError(String(r.detail ?? r.message ?? "start signup failed"), r.status_code === 400 ? "invalid_input" : "backend_error", {
|
|
76
|
-
next_actions: ["If email exists, use user_auth_login or user_auth_resend_otp."],
|
|
77
|
-
related_tools: ["user_auth_set_password", "user_auth_login"],
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
return jsonOk(r);
|
|
81
|
-
}
|
|
82
|
-
catch (e) {
|
|
83
|
-
const m = e instanceof Error ? e.message : String(e);
|
|
84
|
-
return toolError(m, "backend_error", {
|
|
85
|
-
next_actions: ["Ensure PREMAN_BACKEND is running and JWT_SECRET is set on the server."],
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
server.tool("user_auth_signup", "Register with email and password. Sends an OTP; next call user_auth_verify_otp. Uses POST /auth/signup on PREMAN_BACKEND (no API key).", {
|
|
90
|
-
email: z.string().describe("User email"),
|
|
91
|
-
password: z.string().describe("Password (min 6 characters on the server)"),
|
|
92
|
-
}, async (args) => {
|
|
93
|
-
try {
|
|
94
|
-
const r = await callAuthJson(base, "POST", "/auth/signup", {
|
|
95
|
-
json: { email: args.email, password: args.password },
|
|
96
|
-
});
|
|
97
|
-
if (!r.ok) {
|
|
98
|
-
return toolError(String(r.detail ?? r.message ?? "signup failed"), r.status_code === 400 ? "invalid_input" : "backend_error", {
|
|
99
|
-
next_actions: ["If email exists, use user_auth_login or user_auth_resend_otp."],
|
|
100
|
-
related_tools: ["user_auth_verify_otp", "user_auth_login"],
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
return jsonOk(r);
|
|
104
|
-
}
|
|
105
|
-
catch (e) {
|
|
106
|
-
const m = e instanceof Error ? e.message : String(e);
|
|
107
|
-
return toolError(m, "backend_error", {
|
|
108
|
-
next_actions: ["Ensure PREMAN_BACKEND is running and JWT_SECRET is set on the server."],
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
server.tool("user_auth_verify_otp", "Verify the email OTP and receive access_token (JWT). POST /auth/verify-otp.", { email: z.string(), otp: z.string().describe("6-digit code from email") }, async (args) => {
|
|
113
|
-
try {
|
|
114
|
-
const r = await callAuthJson(base, "POST", "/auth/verify-otp", {
|
|
115
|
-
json: { email: args.email, otp: args.otp },
|
|
116
|
-
});
|
|
117
|
-
if (!r.ok) {
|
|
118
|
-
return toolError(String(r.detail ?? "verify failed"), "auth_required", {
|
|
119
|
-
next_actions: ["Request a new code with user_auth_resend_otp if expired."],
|
|
120
|
-
related_tools: ["user_auth_resend_otp", "user_auth_signup"],
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
return jsonOk(r);
|
|
124
|
-
}
|
|
125
|
-
catch (e) {
|
|
126
|
-
return toolError(e instanceof Error ? e.message : String(e), "backend_error");
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
server.tool("user_auth_login", "Login with email and password. Returns access_token if email is verified. POST /auth/login.", { email: z.string(), password: z.string() }, async (args) => {
|
|
130
|
-
try {
|
|
131
|
-
const r = await callAuthJson(base, "POST", "/auth/login", {
|
|
132
|
-
json: { email: args.email, password: args.password },
|
|
133
|
-
});
|
|
134
|
-
if (!r.ok) {
|
|
135
|
-
const sc = r.status_code;
|
|
136
|
-
const code = sc === 403 || sc === 401 ? "auth_required" : "backend_error";
|
|
137
|
-
return toolError(String(r.detail ?? "login failed"), code, {
|
|
138
|
-
next_actions: [
|
|
139
|
-
"If 403 email not verified: use user_auth_verify_otp or user_auth_resend_otp.",
|
|
140
|
-
"If 403 migrated account: use user_auth_forgot_password or user_auth_set_password flow.",
|
|
141
|
-
],
|
|
142
|
-
related_tools: ["user_auth_verify_otp", "user_auth_needs_password", "user_auth_set_password"],
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
return jsonOk(r);
|
|
146
|
-
}
|
|
147
|
-
catch (e) {
|
|
148
|
-
return toolError(e instanceof Error ? e.message : String(e), "backend_error");
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
server.tool("user_auth_needs_password", "Check if an account must set a password (e.g. migrated user). GET /auth/needs-password?email=", { email: z.string().optional().describe("Email to check; omit to return false/false from server") }, async (args) => {
|
|
152
|
-
try {
|
|
153
|
-
const r = await callAuthJson(base, "GET", "/auth/needs-password", {
|
|
154
|
-
query: { email: args.email },
|
|
155
|
-
});
|
|
156
|
-
if (!r.ok) {
|
|
157
|
-
return toolError(String(r.detail ?? "request failed"), "backend_error");
|
|
158
|
-
}
|
|
159
|
-
return jsonOk(r);
|
|
160
|
-
}
|
|
161
|
-
catch (e) {
|
|
162
|
-
return toolError(e instanceof Error ? e.message : String(e), "backend_error");
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
server.tool("user_auth_resend_otp", "Resend verification OTP. POST /auth/resend-otp with { email }.", { email: z.string() }, async (args) => {
|
|
166
|
-
try {
|
|
167
|
-
const r = await callAuthJson(base, "POST", "/auth/resend-otp", {
|
|
168
|
-
json: { email: args.email },
|
|
169
|
-
});
|
|
170
|
-
if (!r.ok) {
|
|
171
|
-
return toolError(String(r.detail ?? "resend failed"), "invalid_input");
|
|
172
|
-
}
|
|
173
|
-
return jsonOk(r);
|
|
174
|
-
}
|
|
175
|
-
catch (e) {
|
|
176
|
-
return toolError(e instanceof Error ? e.message : String(e), "backend_error");
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
server.tool("user_auth_forgot_password", "Request password reset OTP. POST /auth/forgot-password with { email }.", { email: z.string() }, async (args) => {
|
|
180
|
-
try {
|
|
181
|
-
const r = await callAuthJson(base, "POST", "/auth/forgot-password", {
|
|
182
|
-
json: { email: args.email },
|
|
183
|
-
});
|
|
184
|
-
if (!r.ok) {
|
|
185
|
-
return toolError(String(r.detail ?? "forgot failed"), "backend_error");
|
|
186
|
-
}
|
|
187
|
-
return jsonOk(r);
|
|
188
|
-
}
|
|
189
|
-
catch (e) {
|
|
190
|
-
return toolError(e instanceof Error ? e.message : String(e), "backend_error");
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
server.tool("user_auth_set_password", "Set a new password using OTP (migrated / forgot flow). Returns access_token. POST /auth/set-password.", {
|
|
194
|
-
email: z.string(),
|
|
195
|
-
otp: z.string(),
|
|
196
|
-
new_password: z.string().min(6),
|
|
197
|
-
}, async (args) => {
|
|
198
|
-
try {
|
|
199
|
-
const r = await callAuthJson(base, "POST", "/auth/set-password", {
|
|
200
|
-
json: {
|
|
201
|
-
email: args.email,
|
|
202
|
-
otp: args.otp,
|
|
203
|
-
new_password: args.new_password,
|
|
204
|
-
},
|
|
205
|
-
});
|
|
206
|
-
if (!r.ok) {
|
|
207
|
-
return toolError(String(r.detail ?? "set password failed"), "auth_required", {
|
|
208
|
-
next_actions: ["Request a new OTP with user_auth_forgot_password or user_auth_resend_otp."],
|
|
209
|
-
related_tools: ["user_auth_forgot_password"],
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
return jsonOk(r);
|
|
213
|
-
}
|
|
214
|
-
catch (e) {
|
|
215
|
-
return toolError(e instanceof Error ? e.message : String(e), "backend_error");
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
server.tool("user_auth_me", "Current user profile (JWT). GET /auth/me with Authorization: Bearer access_token from login/verify.", {
|
|
219
|
-
access_token: z.string().describe("JWT from user_auth_login or user_auth_verify_otp"),
|
|
220
|
-
}, async (args) => {
|
|
221
|
-
try {
|
|
222
|
-
const r = await callAuthJson(base, "GET", "/auth/me", {
|
|
223
|
-
token: args.access_token,
|
|
224
|
-
});
|
|
225
|
-
if (!r.ok) {
|
|
226
|
-
return toolError(String(r.detail ?? "unauthorized"), "auth_required", {
|
|
227
|
-
next_actions: ["Call user_auth_login to obtain a fresh access_token."],
|
|
228
|
-
related_tools: ["user_auth_login"],
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
return jsonOk(r);
|
|
232
|
-
}
|
|
233
|
-
catch (e) {
|
|
234
|
-
return toolError(e instanceof Error ? e.message : String(e), "backend_error");
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
server.tool("user_auth_change_password", "Change password for the signed-in user. POST /auth/change-password with JWT.", {
|
|
238
|
-
access_token: z.string(),
|
|
239
|
-
current_password: z.string(),
|
|
240
|
-
new_password: z.string().min(6),
|
|
241
|
-
}, async (args) => {
|
|
242
|
-
try {
|
|
243
|
-
const r = await callAuthJson(base, "POST", "/auth/change-password", {
|
|
244
|
-
token: args.access_token,
|
|
245
|
-
json: {
|
|
246
|
-
current_password: args.current_password,
|
|
247
|
-
new_password: args.new_password,
|
|
248
|
-
},
|
|
249
|
-
});
|
|
250
|
-
if (!r.ok) {
|
|
251
|
-
return toolError(String(r.detail ?? "change password failed"), r.status_code === 401 ? "auth_required" : "invalid_input", {
|
|
252
|
-
related_tools: ["user_auth_login", "user_auth_me"],
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
return jsonOk(r);
|
|
256
|
-
}
|
|
257
|
-
catch (e) {
|
|
258
|
-
return toolError(e instanceof Error ? e.message : String(e), "backend_error");
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
server.tool("user_auth_delete_account", "Delete the current account. DELETE /auth/me with JWT. Irreversible.", {
|
|
262
|
-
access_token: z.string().describe("JWT from user_auth_login or user_auth_verify_otp"),
|
|
263
|
-
}, async (args) => {
|
|
264
|
-
try {
|
|
265
|
-
const r = await callAuthJson(base, "DELETE", "/auth/me", {
|
|
266
|
-
token: args.access_token,
|
|
267
|
-
});
|
|
268
|
-
if (!r.ok) {
|
|
269
|
-
return toolError(String(r.detail ?? "delete failed"), "auth_required", {
|
|
270
|
-
related_tools: ["user_auth_login"],
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
return jsonOk(r);
|
|
274
|
-
}
|
|
275
|
-
catch (e) {
|
|
276
|
-
return toolError(e instanceof Error ? e.message : String(e), "backend_error");
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
}
|