comfyui-mcp 0.52.2 → 0.52.4

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.
Files changed (44) hide show
  1. package/README.md +13 -0
  2. package/dist/comfyui/client.js +35 -8
  3. package/dist/comfyui/client.js.map +1 -1
  4. package/dist/comfyui/fetch.js +44 -0
  5. package/dist/comfyui/fetch.js.map +1 -1
  6. package/dist/comfyui/json-guard.js +47 -3
  7. package/dist/comfyui/json-guard.js.map +1 -1
  8. package/dist/config.js +7 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/orchestrator/panel-tools.js +1511 -332
  11. package/dist/orchestrator/panel-tools.js.map +1 -1
  12. package/dist/orchestrator/promoted-widget.js +101 -0
  13. package/dist/orchestrator/promoted-widget.js.map +1 -0
  14. package/dist/services/download-cache.js +10 -0
  15. package/dist/services/download-cache.js.map +1 -1
  16. package/dist/services/manager-node-search.js +190 -0
  17. package/dist/services/manager-node-search.js.map +1 -0
  18. package/dist/services/manifest.js +27 -60
  19. package/dist/services/manifest.js.map +1 -1
  20. package/dist/services/mid-command-remedy.js +31 -4
  21. package/dist/services/mid-command-remedy.js.map +1 -1
  22. package/dist/services/model-resolver.js +217 -49
  23. package/dist/services/model-resolver.js.map +1 -1
  24. package/dist/services/node-authoring.js +25 -12
  25. package/dist/services/node-authoring.js.map +1 -1
  26. package/dist/services/process-control.js +230 -3
  27. package/dist/services/process-control.js.map +1 -1
  28. package/dist/services/queue-monitor.js +51 -2
  29. package/dist/services/queue-monitor.js.map +1 -1
  30. package/dist/services/trainer-bootstrap.js +51 -5
  31. package/dist/services/trainer-bootstrap.js.map +1 -1
  32. package/dist/services/ui-bridge.js +89 -17
  33. package/dist/services/ui-bridge.js.map +1 -1
  34. package/dist/services/workspace-env.js +40 -0
  35. package/dist/services/workspace-env.js.map +1 -1
  36. package/dist/tools/node-pack.js +20 -4
  37. package/dist/tools/node-pack.js.map +1 -1
  38. package/dist/tools/process-control.js +1 -1
  39. package/dist/tools/process-control.js.map +1 -1
  40. package/dist/tools/vocabulary.js +350 -0
  41. package/dist/tools/vocabulary.js.map +1 -1
  42. package/dist/utils/origin.js +12 -2
  43. package/dist/utils/origin.js.map +1 -1
  44. package/package.json +1 -1
@@ -1,8 +1,9 @@
1
- import { execSync, spawn, } from "node:child_process";
1
+ import { exec, execSync, spawn, } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { existsSync, mkdirSync, openSync, readdirSync, readFileSync, readlinkSync, statSync, unlinkSync, writeSync, } from "node:fs";
4
4
  import { homedir, platform } from "node:os";
5
5
  import { isAbsolute, join } from "node:path";
6
+ import { promisify } from "node:util";
6
7
  import { getSystemStats, resetClient, resetObjectInfoCache } from "../comfyui/client.js";
7
8
  import { config, getComfyUIBaseUrl, getComfyuiTargetGeneration, isRemoteMode, targetIsOnThisMachine, } from "../config.js";
8
9
  import { comfyuiFetch, describeTargetDrift } from "../comfyui/fetch.js";
@@ -15,6 +16,7 @@ import { desktopSavedLaunchArgs, describeSavedLaunchArgDrift, } from "./desktop-
15
16
  import { findComfyuiPython } from "./env-capabilities.js";
16
17
  import { readLiveProcessEnv, resolveLaunchEnvironment, } from "./launcher-env.js";
17
18
  import { classifyDesktopSupervision, classifyListenerOwnership, launchedChildStillRunning, unclassifiedOwnership, unclassifiedSupervision, } from "./listener-ownership.js";
19
+ import { isLoopbackServerUrl } from "./local-vram.js";
18
20
  import { resetManagerApiCache } from "./manager-api-cache.js";
19
21
  import { parseListenerPidFromNetstat, findPidByPort, probePortOwner, } from "./port-owner.js";
20
22
  import { recordLaunchedInterpreter, clearLaunchedInterpreter, readProcessIdentity, argv0FromCommandLine, commandLineMatchesArgv, } from "./live-interpreter.js";
@@ -3259,6 +3261,54 @@ async function restartViaManagerRebootDispatch(context, anchoredBase, witness) {
3259
3261
  ? describeSavedLaunchArgDrift(desktopSavedLaunchArgs(config.comfyuiPath ?? undefined), afterArgv)
3260
3262
  : "")
3261
3263
  : "";
3264
+ // #1642: the readiness poll alone can never see a cycle — it certifies that
3265
+ // SOMETHING healthy answers now, which is why every Manager reboot used to
3266
+ // land on `started:false, startup:"unconfirmed"` no matter how cleanly the
3267
+ // server came back (the issue's exact report). But since #871 this path holds
3268
+ // the instance WITNESS across the dispatch, and on a LOOPBACK endpoint that
3269
+ // witness supplies the down→up the poller cannot: a socket on this machine
3270
+ // has no tunnel to hiccup and no proxy to idle out (instance-witness.ts's
3271
+ // reasons a close is inconclusive are all off-loopback), so its close AFTER
3272
+ // the dispatch is the instance that accepted the reboot going away, and the
3273
+ // healthy probe is the successor answering. That is precisely the positive
3274
+ // evidence the rounds-7/8 ruling below says `started` requires, so this
3275
+ // branch returns the confirmed verdict the fields below must withhold.
3276
+ //
3277
+ // Scoped to loopback on purpose: past the first hop a close still proves
3278
+ // nothing, so a remote endpoint keeps the unconfirmed verdict even with a
3279
+ // dropped witness. A loopback REVERSE PROXY could in principle drop the
3280
+ // client connection while its origin never cycled — the substitution edge
3281
+ // the witness module already declines to cover, not a reason to deny every
3282
+ // plain local install the confirmation its reboot earned. The strict `>`
3283
+ // boundary is the same one the argv fence uses: a same-millisecond stamp is
3284
+ // ambiguous (event-delivery slack), and ambiguous declines.
3285
+ const witnessSawTheCycle = witness !== undefined &&
3286
+ witnessClosedAt !== undefined &&
3287
+ witnessClosedAt > dispatchedAt &&
3288
+ isLoopbackServerUrl(anchoredBase);
3289
+ if (witnessSawTheCycle) {
3290
+ return {
3291
+ stopped: true,
3292
+ started: true,
3293
+ ready: true,
3294
+ startup: "confirmed",
3295
+ readiness,
3296
+ message: (reboot.acked
3297
+ ? "The ComfyUI-Manager reboot request was acknowledged."
3298
+ : `A ComfyUI-Manager reboot was dispatched but not acknowledged (${reboot.note ?? "no reply"}).`) +
3299
+ ` ComfyUI is healthy now (${readiness.waited_ms}ms) — ${context.label}/supervised ` +
3300
+ "restart, CONFIRMED: the instance that accepted the reboot dropped the " +
3301
+ "connection this call was holding open across the dispatch (the down), " +
3302
+ "and the same endpoint answers healthy again (the up)." +
3303
+ argvNote +
3304
+ // #1647: same disclosure as every other branch — an inferred
3305
+ // supervision is stated whatever the probes saw.
3306
+ (context.supervisionNote ? ` ${context.supervisionNote}` : ""),
3307
+ // This path still launched nothing of ours — a supervisor cycled the
3308
+ // process — so the listener is never ours to claim, confirmed or not.
3309
+ listener_ownership: unclassifiedOwnership(),
3310
+ };
3311
+ }
3262
3312
  return {
3263
3313
  stopped: true,
3264
3314
  // THE FIELDS MUST AGREE WITH THE SENTENCE (codex gate rounds 7 and 8). A message
@@ -3272,7 +3322,9 @@ async function restartViaManagerRebootDispatch(context, anchoredBase, witness) {
3272
3322
  // demonstrably existed and only its attribution was uncertain. HERE this call
3273
3323
  // spawned nothing at all, and an acknowledged Manager request can be a no-op —
3274
3324
  // so there is no positive evidence that this call started anything, and
3275
- // `started` is exactly the claim that it did.
3325
+ // `started` is exactly the claim that it did. (#1642: when the loopback witness
3326
+ // DID supply that evidence — the observed down→up — the confirmed branch above
3327
+ // has already returned; this return is the unobserved case only.)
3276
3328
  //
3277
3329
  // The fear that drove the earlier choice — that `false` reads as a failed
3278
3330
  // restart — is answered by the fields beside it rather than by overstating this
@@ -3320,12 +3372,183 @@ async function restartViaManagerRebootDispatch(context, anchoredBase, witness) {
3320
3372
  listener_ownership: unclassifiedOwnership(),
3321
3373
  };
3322
3374
  }
3375
+ let execAsyncImpl = null;
3376
+ function execAsync(command, options) {
3377
+ execAsyncImpl ??= promisify(exec);
3378
+ return execAsyncImpl(command, options);
3379
+ }
3380
+ /**
3381
+ * Hard cap on the configured restart command's own runtime (panel#1262). A
3382
+ * `docker restart`/`systemctl restart` answers in seconds; this bounds the
3383
+ * pathological case (a command that hangs) so the tool call still returns.
3384
+ */
3385
+ const CONFIGURED_RESTART_COMMAND_TIMEOUT_MS = 120_000;
3386
+ /**
3387
+ * Restart via the user-configured COMFYUI_RESTART_COMMAND (panel#1262) — the
3388
+ * recovery path for an EXTERNALLY-MANAGED ComfyUI (a local container, a systemd
3389
+ * unit, a launcher) whose relaunch can never be proven from here: its argv[0]
3390
+ * is a bare `main.py` anchored only inside its own mount/namespace, so the
3391
+ * refuse-safe preflight (correctly) refuses a kill+relaunch, and a wedged
3392
+ * server answers no Manager reboot either. The configured command is the user
3393
+ * telling us exactly what cycles the instance, so it IS the proven relaunch.
3394
+ *
3395
+ * The honesty rules are the Manager reboot path's own: we launched nothing, so
3396
+ * `listener_ownership` is never ours to claim; `started` is claimed only when
3397
+ * the loopback witness OBSERVED the down→up (#1642); a healthy answer with no
3398
+ * observed cycle is "unconfirmed", and an expired readiness budget is NOT a
3399
+ * failure verdict (#367).
3400
+ */
3401
+ async function restartViaConfiguredCommand(command) {
3402
+ logger.info("Restarting ComfyUI via the configured COMFYUI_RESTART_COMMAND...");
3403
+ // Pin the base and open the instance WITNESS before anything else, exactly as
3404
+ // the Manager reboot path does: the command runs after awaits, and the
3405
+ // configured target is mutable across every one of them.
3406
+ const anchoredBase = getComfyUIBaseUrl();
3407
+ const probeUrl = `${anchoredBase}/system_stats`;
3408
+ const witness = await acquireInstanceWitness(anchoredBase);
3409
+ try {
3410
+ // FENCE BEFORE THE IRREVERSIBLE ACT (same rule as the Manager path): a
3411
+ // retarget during the witness handshake would send the restart at one
3412
+ // server and report the health of another. Nothing has run yet — refuse.
3413
+ if (getComfyUIBaseUrl() !== anchoredBase) {
3414
+ return {
3415
+ stopped: false,
3416
+ started: false,
3417
+ startup: "not-attempted",
3418
+ message: "The configured ComfyUI target changed while this restart was preparing, " +
3419
+ "so the configured restart command was NOT run — it would have acted on a " +
3420
+ "different server than the one this call read. Nothing was restarted. " +
3421
+ "Re-run the restart to act on the current target.",
3422
+ listener_ownership: unclassifiedOwnership(),
3423
+ };
3424
+ }
3425
+ const dispatchedAt = Date.now();
3426
+ try {
3427
+ await execAsync(command, {
3428
+ timeout: CONFIGURED_RESTART_COMMAND_TIMEOUT_MS,
3429
+ windowsHide: true,
3430
+ });
3431
+ }
3432
+ catch (err) {
3433
+ // The command itself reports failure (non-zero exit / timeout / signal).
3434
+ // That is OBSERVED — but only about the command, not about the server:
3435
+ // say what the command did, then probe ONCE and say what answers now.
3436
+ const probe = await waitForApiReady({
3437
+ intervalMs: 250,
3438
+ maxTries: 1,
3439
+ probeUrl,
3440
+ });
3441
+ return {
3442
+ stopped: false,
3443
+ started: false,
3444
+ ready: probe.ready,
3445
+ startup: "failed",
3446
+ readiness: probe,
3447
+ message: `The configured restart command (COMFYUI_RESTART_COMMAND) failed: ${errorText(err)}. ` +
3448
+ (probe.ready
3449
+ ? `Something IS answering on ${probeUrl} — the restart very likely did not ` +
3450
+ "happen and the old instance is still serving. Fix the command and retry."
3451
+ : `and nothing is answering on ${probeUrl} right now. Whether the command ` +
3452
+ "stopped something before failing is not knowable from here — check the " +
3453
+ "instance's own manager (docker ps / systemctl status / its launcher) " +
3454
+ "before assuming either way."),
3455
+ listener_ownership: unclassifiedOwnership(),
3456
+ };
3457
+ }
3458
+ // The command claims to have cycled the instance. Record the dispatch so a
3459
+ // later decline-path DOWN report may name causation (process-wide slot only —
3460
+ // a panel caller stamps its own session token from this result).
3461
+ recordRestartDispatch(anchoredBase, PROCESS_WIDE_RESTART_DISPATCH_TOKEN);
3462
+ const timing = getRemoteRebootTiming();
3463
+ if (timing.settleMs > 0)
3464
+ await sleep(timing.settleMs);
3465
+ const intervalMs = Math.max(250, timing.intervalMs);
3466
+ const maxTries = Math.max(1, Math.ceil(timing.budgetMs / intervalMs));
3467
+ const readiness = await waitForApiReady({ intervalMs, maxTries, probeUrl });
3468
+ if (!readiness.ready) {
3469
+ return {
3470
+ stopped: true,
3471
+ started: false,
3472
+ ready: false,
3473
+ startup: "unconfirmed",
3474
+ readiness,
3475
+ message: `The configured restart command (COMFYUI_RESTART_COMMAND) ran, but no ` +
3476
+ `readiness probe got a healthy response from ${readiness.probe_url} within ` +
3477
+ `${seconds(readiness.waited_ms)}s (${readiness.attempts}/${readiness.max_tries} ` +
3478
+ `probes over a COMFYUI_REMOTE_REBOOT_BUDGET_S=${seconds(timing.budgetMs)}s ` +
3479
+ "budget). That budget expiring means the restart is NOT CONFIRMED YET — it " +
3480
+ "does NOT mean it failed; a cold start can take longer than this. Re-check " +
3481
+ 'with get_system_stats (action:"health") in ' +
3482
+ `another ${RECHECK_HINT_S}s before intervening. If it is still down then, ` +
3483
+ "start ComfyUI from whatever manages it, or raise that budget to wait " +
3484
+ "longer next time.",
3485
+ listener_ownership: unclassifiedOwnership(),
3486
+ };
3487
+ }
3488
+ // Back and ready — the caches memoized against the old instance are stale.
3489
+ resetClient();
3490
+ resetObjectInfoCache();
3491
+ resetManagerApiCache("comfyui restarted via configured command");
3492
+ // Answering again, so this dispatch can no longer be the cause of a LATER
3493
+ // down report (the Manager path clears on the same rule).
3494
+ clearRestartDispatch(PROCESS_WIDE_RESTART_DISPATCH_TOKEN);
3495
+ // #1642's rule applies unchanged: on a LOOPBACK endpoint the witness's close
3496
+ // AFTER the command ran is the observed down→up the readiness poll alone can
3497
+ // never supply; anything else (no witness, still-open witness, non-loopback)
3498
+ // reports honestly that the cycle itself was not observed.
3499
+ const witnessClosedAt = witness?.closedAt();
3500
+ const witnessSawTheCycle = witness !== undefined &&
3501
+ witnessClosedAt !== undefined &&
3502
+ witnessClosedAt > dispatchedAt &&
3503
+ isLoopbackServerUrl(anchoredBase);
3504
+ if (witnessSawTheCycle) {
3505
+ return {
3506
+ stopped: true,
3507
+ started: true,
3508
+ ready: true,
3509
+ startup: "confirmed",
3510
+ readiness,
3511
+ message: `The configured restart command (COMFYUI_RESTART_COMMAND) ran. ComfyUI is ` +
3512
+ `healthy now (${readiness.waited_ms}ms) — CONFIRMED restart: the instance ` +
3513
+ "dropped the connection this call was holding open across the command " +
3514
+ "(the down), and the same endpoint answers healthy again (the up).",
3515
+ listener_ownership: unclassifiedOwnership(),
3516
+ };
3517
+ }
3518
+ return {
3519
+ stopped: true,
3520
+ started: false,
3521
+ ready: true,
3522
+ startup: "unconfirmed",
3523
+ readiness,
3524
+ message: `The configured restart command (COMFYUI_RESTART_COMMAND) ran. ComfyUI is ` +
3525
+ `healthy now (${readiness.waited_ms}ms). The cycle itself was not directly ` +
3526
+ "observed from here, so verify with " +
3527
+ 'get_system_stats (action:"health") if you need certainty that it actually ' +
3528
+ "restarted.",
3529
+ listener_ownership: unclassifiedOwnership(),
3530
+ };
3531
+ }
3532
+ finally {
3533
+ witness?.close();
3534
+ }
3535
+ }
3323
3536
  export async function restartComfyUI() {
3324
3537
  if (isRemoteMode()) {
3325
3538
  // Remote target: can't process-control it, but a Manager HTTP reboot brings
3326
3539
  // back a self-supervised ComfyUI (e.g. the tunnelled Desktop app).
3327
3540
  return restartViaManagerReboot({ label: "remote" });
3328
3541
  }
3542
+ // panel#1262: an EXTERNALLY-MANAGED local instance (container, systemd unit,
3543
+ // launcher) offers nothing the kill+relaunch path can validate — its bare
3544
+ // `main.py` argv anchors only inside its own namespace — so without this the
3545
+ // restart refused and a wedged server had no recovery. The configured command
3546
+ // is the user's explicit statement of what cycles the instance; it outranks
3547
+ // every inferred path below (including Desktop's), and it needs no live
3548
+ // process info, so it also works when the server is too wedged to answer.
3549
+ if (config.comfyuiRestartCommand) {
3550
+ return restartViaConfiguredCommand(config.comfyuiRestartCommand);
3551
+ }
3329
3552
  logger.info("Restarting ComfyUI...");
3330
3553
  // #848: the target generation as of BEFORE the instance is resolved, so the argv
3331
3554
  // that resolution observes can be fenced to the instance it was actually read
@@ -3434,7 +3657,11 @@ export async function restartComfyUI() {
3434
3657
  message: `Refusing to restart: ${relaunch.reason} ComfyUI was left running (not stopped) ` +
3435
3658
  "so you don't lose the server. " +
3436
3659
  (relaunch.advice ??
3437
- "Fix the launch path (e.g. COMFYUI_PATH) and try again.") +
3660
+ "Fix the launch path (e.g. COMFYUI_PATH) and try again. If this ComfyUI " +
3661
+ "is externally managed (a container, a systemd unit, a launcher), set " +
3662
+ "COMFYUI_RESTART_COMMAND to the exact command that restarts it (e.g. " +
3663
+ "`docker restart <container>`) — the restart then runs that command " +
3664
+ "instead of needing the launch path resolvable from here.") +
3438
3665
  describeRecovery(recoveryHint(info)),
3439
3666
  // Refused before touching anything: the still-running server is the one that
3440
3667
  // was already there, which this call did not start.