@happyvertical/smrt-cli 0.55.16 → 0.55.17

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 (40) hide show
  1. package/agents/app-commands.md +10 -0
  2. package/agents/kitchen.md +24 -4
  3. package/dist/{AuditLogCollection-WDHL-_AF-C1ZiQ7V9.js → AuditLogCollection-WDHL-_AF-02D9mDot.js} +1 -1
  4. package/dist/{OidcIdentity-BUblgQLs-D3Aab-GC.js → OidcIdentity-BUblgQLs-hzB0lrxA.js} +2 -2
  5. package/dist/{OidcIdentityCollection-ClOyaHeF-DFU0wHa8.js → OidcIdentityCollection-ClOyaHeF-D7aAvcYW.js} +1 -1
  6. package/dist/{ProfileAssetCollection-CRMcjLJ--Dn-umPHt.js → ProfileAssetCollection-CRMcjLJ--CRQsOHTZ.js} +3 -3
  7. package/dist/{ProfileCollection-C4INKh7Y-BkYbrF-h.js → ProfileCollection-C4INKh7Y-DUcvwlG4.js} +17 -17
  8. package/dist/{ProfileLinkCollection-DFfZRsgv-Bofzf-Yw.js → ProfileLinkCollection-DFfZRsgv-4ulMzaJO.js} +2 -2
  9. package/dist/{ProfileMetadataCollection-BIYKy4Y3-CXjlWaGs.js → ProfileMetadataCollection-BIYKy4Y3-BGVdmb-k.js} +1 -1
  10. package/dist/{ProfileMetafieldCollection-Buz7gzag-CiGCszRX.js → ProfileMetafieldCollection-Buz7gzag-Cuuzb-Yt.js} +1 -1
  11. package/dist/{ProfileRelationshipCollection-C4g_QLI4-Bqb15ye8.js → ProfileRelationshipCollection-C4g_QLI4-DHnLSOex.js} +1 -1
  12. package/dist/{ProfileTypeCollection-B7kwi1mU-Bvx-bDdd.js → ProfileTypeCollection-B7kwi1mU-tffgStmE.js} +2 -2
  13. package/dist/app/cli.d.ts.map +1 -1
  14. package/dist/app/operations.d.ts.map +1 -1
  15. package/dist/app.js +2 -2
  16. package/dist/{cli-OZsH5cdL.js → cli-FDpY0gCt.js} +51 -5
  17. package/dist/commands/kitchen/mcp/bridge.d.ts +3 -0
  18. package/dist/commands/kitchen/mcp/bridge.d.ts.map +1 -0
  19. package/dist/commands/kitchen/mcp/preview.d.ts +8 -0
  20. package/dist/commands/kitchen/mcp/preview.d.ts.map +1 -0
  21. package/dist/commands/kitchen/mcp/server.d.ts.map +1 -1
  22. package/dist/commands/kitchen/mcp/tools.d.ts +21 -2
  23. package/dist/commands/kitchen/mcp/tools.d.ts.map +1 -1
  24. package/dist/commands/kitchen/mcp/ui.d.ts +40 -0
  25. package/dist/commands/kitchen/mcp/ui.d.ts.map +1 -0
  26. package/dist/commands/kitchen.d.ts.map +1 -1
  27. package/dist/{commands-BnN3-s2f.js → commands-Cve0qfxc.js} +30 -4
  28. package/dist/{dist-AM3Ihu2D.js → dist-BQ5ZrH9U.js} +16 -16
  29. package/dist/{dist-BAcq9rFh.js → dist-BQjejZ0-.js} +3 -3
  30. package/dist/{dist-Bp_HahKu.js → dist-BtlfVNPV.js} +4 -4
  31. package/dist/{dist-E6ZsbcFn.js → dist-C_pNF2sM.js} +2 -2
  32. package/dist/dist-DHRli5Sc.js +21 -0
  33. package/dist/index.js +12 -12
  34. package/dist/{oidcProvisioningPrimitives-DNP9EZlU-D6tRfyyK.js → oidcProvisioningPrimitives-DNP9EZlU-B5DmitVf.js} +3 -3
  35. package/dist/{portability-CES1YC2m.js → portability-BbltyqQ-.js} +1 -1
  36. package/dist/{resolveIdentity-CkyqvctR-CXzDyaMs.js → resolveIdentity-CkyqvctR-BDafA2yA.js} +14 -14
  37. package/dist/{server-Dp096tZj.js → server-C69K3w-t.js} +682 -39
  38. package/package.json +10 -10
  39. package/scripts/verify-package-types-exports.js +6 -1
  40. package/dist/dist-C0OyGC_L.js +0 -21
@@ -1,12 +1,107 @@
1
1
  import { c as SMRT_SCOPE, l as loadRegistryManifest, r as cliVersion, s as validateCookbookText, t as createCookbookApplier, u as resolveRecipeIndex } from "./apply-BNDIdixP.js";
2
- import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { dirname, isAbsolute, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
6
  import { validateCookbook } from "@happyvertical/smrt-core/cookbook";
7
+ import { createServer } from "node:http";
7
8
  import { Server } from "@modelcontextprotocol/server";
8
9
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
9
10
  import { buildBatchTool, checkSchema, createCookbookEngine } from "@happyvertical/smrt-core/cookbook/engine";
11
+ //#region src/commands/kitchen/mcp/preview.ts
12
+ /** Loopback-only bridge for the live SvelteKit preview during local development. */
13
+ async function startKitchenPreview(session, port, origin) {
14
+ if (!/^http:\/\/127\.0\.0\.1:\d+$/.test(origin)) throw new Error("Preview origin must be a 127.0.0.1 HTTP origin.");
15
+ const server = createServer(async (request, response) => {
16
+ response.setHeader("Cache-Control", "no-store");
17
+ response.setHeader("Vary", "Origin");
18
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
19
+ if (request.headers.origin !== origin) {
20
+ response.writeHead(403).end(JSON.stringify({
21
+ ok: false,
22
+ error: "Origin refused."
23
+ }));
24
+ return;
25
+ }
26
+ response.setHeader("Access-Control-Allow-Origin", origin);
27
+ response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
28
+ response.setHeader("Access-Control-Allow-Headers", "Content-Type");
29
+ if (request.method === "OPTIONS") {
30
+ response.writeHead(204).end();
31
+ return;
32
+ }
33
+ const path = new URL(request.url ?? "/", "http://127.0.0.1").pathname;
34
+ const match = /^\/plans\/(plan-[a-f0-9-]{8,36})(\/commands)?$/.exec(path);
35
+ if (!match || !session.previewPlans.has(match[1]) || !(request.method === "GET" && !match[2] || request.method === "POST" && match[2])) {
36
+ response.writeHead(404).end(JSON.stringify({
37
+ ok: false,
38
+ error: "Not found."
39
+ }));
40
+ return;
41
+ }
42
+ try {
43
+ let result;
44
+ if (request.method === "GET") result = await session.call("get_plan", { planId: match[1] });
45
+ else {
46
+ if (!/^application\/json(?:\s*;|$)/i.test(request.headers["content-type"] ?? "")) {
47
+ response.writeHead(415).end(JSON.stringify({
48
+ ok: false,
49
+ error: "JSON required."
50
+ }));
51
+ return;
52
+ }
53
+ const chunks = [];
54
+ let bytes = 0;
55
+ for await (const chunk of request) {
56
+ bytes += chunk.length;
57
+ if (bytes > 2097152) {
58
+ response.writeHead(413).end(JSON.stringify({
59
+ ok: false,
60
+ error: "Request too large."
61
+ }));
62
+ return;
63
+ }
64
+ chunks.push(chunk);
65
+ }
66
+ const input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
67
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
68
+ response.writeHead(400).end(JSON.stringify({
69
+ ok: false,
70
+ error: {
71
+ code: "invalid_input",
72
+ message: "A JSON object is required."
73
+ }
74
+ }));
75
+ return;
76
+ }
77
+ result = await session.call("run_commands", {
78
+ ...input,
79
+ planId: match[1]
80
+ });
81
+ }
82
+ response.writeHead(200).end(JSON.stringify(result.data));
83
+ } catch {
84
+ response.writeHead(400).end(JSON.stringify({
85
+ ok: false,
86
+ error: {
87
+ code: "invalid_input",
88
+ message: "Invalid JSON request."
89
+ }
90
+ }));
91
+ }
92
+ });
93
+ await new Promise((resolve, reject) => {
94
+ server.once("error", reject);
95
+ server.listen(port, "127.0.0.1", resolve);
96
+ });
97
+ const address = server.address();
98
+ if (!address || typeof address === "string") throw new Error("Preview listener has no port.");
99
+ return {
100
+ server,
101
+ url: `http://127.0.0.1:${address.port}`
102
+ };
103
+ }
104
+ //#endregion
10
105
  //#region src/commands/kitchen/mcp/catalog.ts
11
106
  /**
12
107
  * The engine catalog (#3752) built from real recipe manifests: the recipes
@@ -199,6 +294,19 @@ var snapshotSchema = {
199
294
  required: ["revision"],
200
295
  additionalProperties: true
201
296
  };
297
+ /** A document and snapshot captured together from the current engine state. */
298
+ var stateSchema = {
299
+ type: "object",
300
+ description: "Current authoritative state; paired atomically, including on replay.",
301
+ properties: {
302
+ snapshot: snapshotSchema,
303
+ cookbook: {
304
+ type: "object",
305
+ additionalProperties: true
306
+ }
307
+ },
308
+ required: ["snapshot", "cookbook"]
309
+ };
202
310
  /** An output schema: `ok` always, `error` when it is false, plus the extras. */
203
311
  var output = (properties, required = []) => ({
204
312
  $schema: SCHEMA,
@@ -315,7 +423,7 @@ var KITCHEN_TOOLS = [
315
423
  {
316
424
  name: "start_plan",
317
425
  title: "Start a plan",
318
- description: "Start a new plan for an app, empty or from a ready-made cookbook (see list_cookbooks). Returns the plan id every other tool takes. The plan lives in this server process until apply_plan writes it to a project.",
426
+ description: "Start a new plan for an app, empty or from a ready-made cookbook (see list_cookbooks). Returns the plan id every other tool takes.",
319
427
  inputSchema: input({
320
428
  cookbook: {
321
429
  type: "string",
@@ -332,7 +440,8 @@ var KITCHEN_TOOLS = [
332
440
  }),
333
441
  outputSchema: output({
334
442
  planId: { type: "string" },
335
- snapshot: snapshotSchema
443
+ snapshot: snapshotSchema,
444
+ state: stateSchema
336
445
  }, []),
337
446
  annotations: annotations("Start a plan", {})
338
447
  },
@@ -360,7 +469,8 @@ var KITCHEN_TOOLS = [
360
469
  type: "array",
361
470
  items: { type: "object" }
362
471
  },
363
- snapshot: snapshotSchema
472
+ snapshot: snapshotSchema,
473
+ state: stateSchema
364
474
  }),
365
475
  annotations: annotations("Change the plan", { idempotentHint: true })
366
476
  },
@@ -371,10 +481,22 @@ var KITCHEN_TOOLS = [
371
481
  inputSchema: input({ planId }, ["planId"]),
372
482
  outputSchema: output({
373
483
  planId: { type: "string" },
374
- snapshot: snapshotSchema
484
+ snapshot: snapshotSchema,
485
+ state: stateSchema
375
486
  }),
376
487
  annotations: annotations("Get the plan", READ_ONLY)
377
488
  },
489
+ {
490
+ name: "open_preview",
491
+ title: "Open the live app preview",
492
+ description: "Return a local browser URL for this plan when the Kitchen preview bridge is enabled. The app shell and chat then use the same plan.",
493
+ inputSchema: input({ planId }, ["planId"]),
494
+ outputSchema: output({
495
+ planId: { type: "string" },
496
+ url: { type: "string" }
497
+ }),
498
+ annotations: annotations("Open the live app preview", READ_ONLY)
499
+ },
378
500
  {
379
501
  name: "validate_plan",
380
502
  title: "Validate the plan",
@@ -526,6 +648,10 @@ var KitchenSession = class KitchenSession {
526
648
  options;
527
649
  loaded;
528
650
  plans = /* @__PURE__ */ new Map();
651
+ updates = /* @__PURE__ */ new Map();
652
+ previewUrl;
653
+ /** Plans `open_preview` handed to the browser; the preview bridge serves only these. */
654
+ previewPlans = /* @__PURE__ */ new Set();
529
655
  constructor(options) {
530
656
  this.options = options;
531
657
  }
@@ -540,8 +666,93 @@ var KitchenSession = class KitchenSession {
540
666
  }
541
667
  return this.loaded;
542
668
  }
543
- plan(id) {
544
- return (typeof id === "string" ? this.plans.get(id) : void 0) ?? fail("not_found", `No plan ${JSON.stringify(id)} in this server; call start_plan (a restart forgets plans).`, { path: "planId" });
669
+ planPath(id) {
670
+ if (!/^plan-[a-f0-9-]{8,36}$/.test(id)) throw new Error("Invalid plan id.");
671
+ if (!this.options.plansDir) throw new Error("Plans directory is not configured.");
672
+ return join(this.options.plansDir, `${id}.json`);
673
+ }
674
+ savePlan(id, engine) {
675
+ if (!this.options.plansDir) return;
676
+ mkdirSync(this.options.plansDir, {
677
+ recursive: true,
678
+ mode: 448
679
+ });
680
+ const target = this.planPath(id);
681
+ const temporary = `${target}.${randomUUID()}.tmp`;
682
+ const serialized = `${JSON.stringify(engine.state())}\n`;
683
+ try {
684
+ writeFileSync(temporary, serialized, { mode: 384 });
685
+ renameSync(temporary, target);
686
+ this.plans.set(id, {
687
+ engine,
688
+ serialized
689
+ });
690
+ } finally {
691
+ if (existsSync(temporary)) rmSync(temporary);
692
+ }
693
+ }
694
+ /** Serialize the read/check/write transaction across Kitchen processes. */
695
+ async lockPlan(id) {
696
+ if (!this.options.plansDir) return () => {};
697
+ mkdirSync(this.options.plansDir, {
698
+ recursive: true,
699
+ mode: 448
700
+ });
701
+ const path = `${this.planPath(id)}.lock`;
702
+ const token = randomUUID();
703
+ const deadline = Date.now() + 3e4;
704
+ for (;;) {
705
+ try {
706
+ const fd = openSync(path, "wx", 384);
707
+ try {
708
+ writeFileSync(fd, JSON.stringify({
709
+ pid: process.pid,
710
+ token
711
+ }));
712
+ } catch {
713
+ rmSync(path, { force: true });
714
+ return fail("unavailable", "The plan cannot be locked on this device.");
715
+ } finally {
716
+ closeSync(fd);
717
+ }
718
+ return () => {
719
+ try {
720
+ if (JSON.parse(readFileSync(path, "utf8")).token === token) unlinkSync(path);
721
+ } catch {}
722
+ };
723
+ } catch (cause) {
724
+ if (cause.code !== "EEXIST") return fail("unavailable", "The plan cannot be locked on this device.");
725
+ }
726
+ if (Date.now() >= deadline) return fail("conflict", "This plan is busy in another Kitchen session. Retry the same command; if it stays busy, stop Kitchen and remove its stale plan lock.");
727
+ await new Promise((resolve) => setTimeout(resolve, 40));
728
+ }
729
+ }
730
+ async plan(id) {
731
+ if (typeof id === "string" && this.options.plansDir) {
732
+ if (!/^plan-[a-f0-9-]{8,36}$/.test(id)) return fail("not_found", `No plan ${JSON.stringify(id)}.`, { path: "planId" });
733
+ let serialized;
734
+ try {
735
+ serialized = readFileSync(this.planPath(id), "utf8");
736
+ } catch (error) {
737
+ if (error.code === "ENOENT") return fail("not_found", `No plan ${JSON.stringify(id)}.`, { path: "planId" });
738
+ throw error;
739
+ }
740
+ const cached = this.plans.get(id);
741
+ if (cached?.serialized === serialized) return cached;
742
+ const saved = JSON.parse(serialized);
743
+ const { catalog } = await this.catalog();
744
+ const plan = {
745
+ engine: createCookbookEngine({
746
+ catalog,
747
+ state: saved,
748
+ undoPrefix: `undo-${randomUUID()}`
749
+ }),
750
+ serialized
751
+ };
752
+ this.plans.set(id, plan);
753
+ return plan;
754
+ }
755
+ return (typeof id === "string" ? this.plans.get(id) : void 0) ?? fail("not_found", `No plan ${JSON.stringify(id)} in this server; call start_plan.`, { path: "planId" });
545
756
  }
546
757
  /** Validate arguments against a tool's published schema. */
547
758
  static check(tool, args) {
@@ -576,18 +787,38 @@ var KitchenSession = class KitchenSession {
576
787
  case "start_plan": return this.startPlan(args);
577
788
  case "run_commands": return this.runCommands(args);
578
789
  case "get_plan": {
579
- const plan = this.plan(args.planId);
790
+ const plan = await this.plan(args.planId);
580
791
  if (!("engine" in plan)) return plan;
581
792
  const snapshot = plan.engine.snapshot();
582
793
  return {
583
794
  data: {
584
795
  ok: true,
585
796
  planId: args.planId,
586
- snapshot
797
+ snapshot,
798
+ state: {
799
+ snapshot,
800
+ cookbook: plan.engine.cookbook()
801
+ }
587
802
  },
588
803
  text: describeSnapshot(snapshot)
589
804
  };
590
805
  }
806
+ case "open_preview": {
807
+ const plan = await this.plan(args.planId);
808
+ if (!("engine" in plan)) return plan;
809
+ if (!this.previewUrl || !this.options.previewOrigin) return fail("unavailable", "The local preview bridge is not enabled.");
810
+ const port = new URL(this.previewUrl).port;
811
+ this.previewPlans.add(args.planId);
812
+ const url = `${this.options.previewOrigin}/?kitchenPlan=${encodeURIComponent(args.planId)}&kitchenPort=${port}`;
813
+ return {
814
+ data: {
815
+ ok: true,
816
+ planId: args.planId,
817
+ url
818
+ },
819
+ text: `Open the live app shell: ${url}`
820
+ };
821
+ }
591
822
  case "validate_plan": return this.validatePlan(args);
592
823
  case "export_plan": return this.exportPlan(args);
593
824
  case "apply_plan": return this.applyPlan(args);
@@ -636,7 +867,10 @@ var KitchenSession = class KitchenSession {
636
867
  }
637
868
  async startPlan(args) {
638
869
  const { catalog } = await this.catalog();
639
- const engine = createCookbookEngine({ catalog });
870
+ const engine = createCookbookEngine({
871
+ catalog,
872
+ undoPrefix: `undo-${randomUUID()}`
873
+ });
640
874
  const steps = [];
641
875
  if (typeof args.cookbook === "string") steps.push({
642
876
  name: "apply_cookbook",
@@ -650,27 +884,62 @@ var KitchenSession = class KitchenSession {
650
884
  const result = engine.run(step);
651
885
  if (!result.ok) return failFrom(result.error);
652
886
  }
653
- const id = `plan-${randomUUID().slice(0, 8)}`;
654
- this.plans.set(id, { engine });
655
- while (this.plans.size > 20) {
656
- const oldest = this.plans.keys().next().value;
657
- if (oldest === void 0) break;
658
- this.plans.delete(oldest);
887
+ const id = `plan-${randomUUID()}`;
888
+ if (this.options.plansDir) try {
889
+ this.savePlan(id, engine);
890
+ } catch {
891
+ return fail("unavailable", "The plan could not be saved on this device.");
892
+ }
893
+ else {
894
+ this.plans.set(id, { engine });
895
+ while (this.plans.size > 20) {
896
+ const oldest = this.plans.keys().next().value;
897
+ if (oldest === void 0) break;
898
+ this.plans.delete(oldest);
899
+ }
659
900
  }
660
901
  const snapshot = engine.snapshot();
661
902
  return {
662
903
  data: {
663
904
  ok: true,
664
905
  planId: id,
665
- snapshot
906
+ snapshot,
907
+ state: {
908
+ snapshot,
909
+ cookbook: engine.cookbook()
910
+ }
666
911
  },
667
912
  text: `Started ${id}.\n${describeSnapshot(snapshot)}`
668
913
  };
669
914
  }
670
- runCommands(args) {
671
- const plan = this.plan(args.planId);
915
+ async runCommands(args) {
916
+ const id = args.planId;
917
+ const previous = this.updates.get(id) ?? Promise.resolve();
918
+ let release;
919
+ const gate = new Promise((resolve) => {
920
+ release = resolve;
921
+ });
922
+ const current = previous.then(() => gate);
923
+ this.updates.set(id, current);
924
+ await previous;
925
+ try {
926
+ const lock = await this.lockPlan(id);
927
+ if (typeof lock !== "function") return lock;
928
+ try {
929
+ return await this.runCommandsUnlocked(args);
930
+ } finally {
931
+ lock();
932
+ }
933
+ } finally {
934
+ release();
935
+ if (this.updates.get(id) === current) this.updates.delete(id);
936
+ }
937
+ }
938
+ async runCommandsUnlocked(args) {
939
+ const plan = await this.plan(args.planId);
672
940
  if (!("engine" in plan)) return plan;
673
941
  const { planId: _planId, ...batch } = args;
942
+ if (!Array.isArray(batch.commands)) return fail("invalid_input", "commands must be an array.", { path: "commands" });
674
943
  const commands = batch.commands;
675
944
  const lone = commands.length === 1 && commands[0]?.name === "undo" ? plan.engine.run({
676
945
  ...commands[0],
@@ -678,7 +947,26 @@ var KitchenSession = class KitchenSession {
678
947
  ...batch.expectedRevision !== void 0 ? { expectedRevision: batch.expectedRevision } : {}
679
948
  }) : null;
680
949
  if (lone) {
681
- if (!lone.ok) return failFrom(lone.error);
950
+ if (!lone.ok) {
951
+ const failed = failFrom(lone.error);
952
+ return {
953
+ ...failed,
954
+ data: {
955
+ ...failed.data,
956
+ planId: args.planId,
957
+ state: {
958
+ snapshot: plan.engine.snapshot(),
959
+ cookbook: plan.engine.cookbook()
960
+ }
961
+ }
962
+ };
963
+ }
964
+ if (!lone.replayed) try {
965
+ this.savePlan(args.planId, plan.engine);
966
+ } catch {
967
+ this.plans.delete(args.planId);
968
+ return fail("unavailable", "The plan could not be saved on this device.");
969
+ }
682
970
  return {
683
971
  data: {
684
972
  ok: true,
@@ -686,7 +974,11 @@ var KitchenSession = class KitchenSession {
686
974
  ...lone.replayed ? { replayed: true } : {},
687
975
  receipt: lone.receipt,
688
976
  results: [lone.receipt],
689
- snapshot: lone.snapshot
977
+ snapshot: lone.snapshot,
978
+ state: {
979
+ snapshot: plan.engine.snapshot(),
980
+ cookbook: plan.engine.cookbook()
981
+ }
690
982
  },
691
983
  text: `${lone.receipt.summary}\n${describeSnapshot(lone.snapshot)}`
692
984
  };
@@ -699,11 +991,21 @@ var KitchenSession = class KitchenSession {
699
991
  ok: false,
700
992
  planId: args.planId,
701
993
  error: result.error,
702
- snapshot: current
994
+ snapshot: current,
995
+ state: {
996
+ snapshot: current,
997
+ cookbook: plan.engine.cookbook()
998
+ }
703
999
  },
704
1000
  text: `Error ${result.error.code}: ${result.error.message}\nNothing was changed (revision ${current.revision}).`
705
1001
  };
706
1002
  }
1003
+ if (!result.replayed) try {
1004
+ this.savePlan(args.planId, plan.engine);
1005
+ } catch {
1006
+ this.plans.delete(args.planId);
1007
+ return fail("unavailable", "The plan could not be saved on this device.");
1008
+ }
707
1009
  return {
708
1010
  data: {
709
1011
  ok: true,
@@ -711,13 +1013,17 @@ var KitchenSession = class KitchenSession {
711
1013
  ...result.replayed ? { replayed: true } : {},
712
1014
  receipt: result.receipt,
713
1015
  results: result.results.map((r) => r.receipt),
714
- snapshot: result.snapshot
1016
+ snapshot: result.snapshot,
1017
+ state: {
1018
+ snapshot: plan.engine.snapshot(),
1019
+ cookbook: plan.engine.cookbook()
1020
+ }
715
1021
  },
716
1022
  text: `${result.replayed ? "Already applied (same id): " : ""}${result.results.map((r) => r.receipt.summary).join("\n")}\n${describeSnapshot(result.snapshot)}`
717
1023
  };
718
1024
  }
719
1025
  async validatePlan(args) {
720
- const plan = this.plan(args.planId);
1026
+ const plan = await this.plan(args.planId);
721
1027
  if (!("engine" in plan)) return plan;
722
1028
  const { index } = await this.catalog();
723
1029
  const checked = plan.engine.run({ name: "validate" });
@@ -741,8 +1047,8 @@ var KitchenSession = class KitchenSession {
741
1047
  text: valid ? `Valid. Packages needed: ${report.packages.join(", ") || "none"}.` : `Not valid:\n${firstLines(errors.length ? errors : diagnostics.map((d) => `${d.path}: ${d.message}`), 12)}`
742
1048
  };
743
1049
  }
744
- exportPlan(args) {
745
- const plan = this.plan(args.planId);
1050
+ async exportPlan(args) {
1051
+ const plan = await this.plan(args.planId);
746
1052
  if (!("engine" in plan)) return plan;
747
1053
  const result = plan.engine.run({
748
1054
  name: "export_cookbook",
@@ -762,7 +1068,7 @@ var KitchenSession = class KitchenSession {
762
1068
  };
763
1069
  }
764
1070
  async applyPlan(args) {
765
- const plan = this.plan(args.planId);
1071
+ const plan = await this.plan(args.planId);
766
1072
  if (!("engine" in plan)) return plan;
767
1073
  const dir = args.dir;
768
1074
  if (!isAbsolute(dir)) return fail("invalid_input", "dir must be an absolute path.", { path: "dir" });
@@ -835,6 +1141,217 @@ var KitchenSession = class KitchenSession {
835
1141
  }
836
1142
  };
837
1143
  //#endregion
1144
+ //#region src/commands/kitchen/mcp/bridge.ts
1145
+ /** Minimal MCP Apps bridge, injected before the self-contained Planner bundle. */
1146
+ var KITCHEN_BRIDGE = `<script>
1147
+ (() => {
1148
+ const pending = new Map();
1149
+ const listeners = new Set();
1150
+ let sequence = 0;
1151
+ let latest;
1152
+ let hostOrigin = '*';
1153
+ let initialized = false;
1154
+ function send(message) { window.parent.postMessage({ jsonrpc: '2.0', ...message }, hostOrigin); }
1155
+ function request(method, params) {
1156
+ return new Promise((resolve, reject) => {
1157
+ const id = 'kitchen-' + ++sequence;
1158
+ const timer = setTimeout(() => {
1159
+ pending.delete(id);
1160
+ reject(new Error('The host did not answer. Refresh the plan before retrying.'));
1161
+ }, 30000);
1162
+ pending.set(id, { resolve, reject, timer });
1163
+ send({ id, method, params });
1164
+ });
1165
+ }
1166
+ // The Planner is an app shell that scrolls inside itself, so it asks for a
1167
+ // stable frame instead of its content height: the host's fixed height wins,
1168
+ // else its maxHeight, else a default. Fullscreen belongs to the host.
1169
+ const DEFAULT_HEIGHT = 720;
1170
+ let dimensions = {};
1171
+ let displayMode = 'inline';
1172
+ let reported;
1173
+ function appHeight() {
1174
+ if (displayMode === 'fullscreen' || typeof dimensions.height === 'number') return undefined;
1175
+ const max = dimensions.maxHeight;
1176
+ return typeof max === 'number' && max > 0 ? Math.round(max) : DEFAULT_HEIGHT;
1177
+ }
1178
+ function reportSize() {
1179
+ const height = appHeight();
1180
+ if (height === undefined || height === reported) return;
1181
+ reported = height;
1182
+ send({ method: 'ui/notifications/size-changed', params: { height } });
1183
+ }
1184
+ function applyHostContext(context) {
1185
+ if (!context || typeof context !== 'object') return;
1186
+ const next = context.containerDimensions;
1187
+ if (next && typeof next === 'object') dimensions = next;
1188
+ if (typeof context.displayMode === 'string') displayMode = context.displayMode;
1189
+ }
1190
+ function publish(data) {
1191
+ if (!data || typeof data.planId !== 'string' || !data.state) return;
1192
+ latest = data;
1193
+ for (const listener of listeners) listener(data);
1194
+ }
1195
+ window.addEventListener('message', (event) => {
1196
+ if (event.source !== window.parent) return;
1197
+ if (hostOrigin !== '*' && event.origin !== hostOrigin) return;
1198
+ const message = event.data;
1199
+ if (!message || message.jsonrpc !== '2.0') return;
1200
+ if (message.id !== undefined && pending.has(message.id)) {
1201
+ if (event.origin && event.origin !== 'null') hostOrigin = event.origin;
1202
+ const item = pending.get(message.id);
1203
+ clearTimeout(item.timer);
1204
+ pending.delete(message.id);
1205
+ if (message.error) item.reject(new Error(message.error.message || 'Host request failed'));
1206
+ else item.resolve(message.result);
1207
+ } else if (message.method === 'ui/notifications/host-context-changed') {
1208
+ applyHostContext(message.params);
1209
+ if (initialized) reportSize();
1210
+ } else if (message.method === 'ui/notifications/tool-result') {
1211
+ publish(message.params?.structuredContent);
1212
+ } else if (message.method === 'ping' && message.id !== undefined) {
1213
+ send({ id: message.id, result: {} });
1214
+ }
1215
+ });
1216
+ const ready = request('ui/initialize', {
1217
+ appInfo: { name: 'smrt-kitchen-planner', version: '1.0.0' },
1218
+ appCapabilities: { availableDisplayModes: ['inline', 'fullscreen'] },
1219
+ protocolVersion: '2026-01-26',
1220
+ }).then((result) => {
1221
+ if (result?.protocolVersion !== '2026-01-26') throw new Error('Unsupported MCP Apps protocol');
1222
+ send({ method: 'ui/notifications/initialized', params: {} });
1223
+ applyHostContext(result.hostContext);
1224
+ initialized = true;
1225
+ reportSize();
1226
+ return result;
1227
+ });
1228
+ ready.catch(() => {});
1229
+ window.smrtKitchen = {
1230
+ async callTool(name, args) {
1231
+ if (!['get_plan', 'run_commands', 'export_plan', 'validate_plan', 'planner_ui_update'].includes(name)) {
1232
+ throw new Error('This tool is not available from the Planner UI');
1233
+ }
1234
+ await ready;
1235
+ const result = await request('tools/call', { name, arguments: args });
1236
+ if (!result?.structuredContent) throw new Error('The host returned no structured data');
1237
+ return result.structuredContent;
1238
+ },
1239
+ subscribe(listener) {
1240
+ listeners.add(listener);
1241
+ if (latest) listener(latest);
1242
+ return () => listeners.delete(listener);
1243
+ },
1244
+ };
1245
+ ready.then(async () => {
1246
+ const resumePlanId = window.__smrtKitchenReloadPlanId;
1247
+ if (resumePlanId) {
1248
+ try {
1249
+ const result = await request('tools/call', {
1250
+ name: 'get_plan', arguments: { planId: resumePlanId },
1251
+ });
1252
+ publish(result?.structuredContent);
1253
+ } catch { /* A later render result can still supply the plan. */ }
1254
+ }
1255
+ let version = window.__smrtKitchenUiVersion;
1256
+ if (!version) return;
1257
+ let checking = false;
1258
+ let stopped = false;
1259
+ let reloadNotice;
1260
+ function showReloadNotice() {
1261
+ if (reloadNotice) return;
1262
+ reloadNotice = document.createElement('p');
1263
+ reloadNotice.setAttribute('role', 'alert');
1264
+ reloadNotice.textContent = 'The new Planner UI could not reload. Reopen this app tab to use it.';
1265
+ document.body.prepend(reloadNotice);
1266
+ }
1267
+ const check = async () => {
1268
+ if (checking || stopped || document.visibilityState === 'hidden') return;
1269
+ checking = true;
1270
+ let attemptedVersion = false;
1271
+ try {
1272
+ const result = await request('tools/call', {
1273
+ name: 'planner_ui_update',
1274
+ arguments: { version, planId: latest?.planId || resumePlanId || '' },
1275
+ });
1276
+ const update = result?.structuredContent;
1277
+ if (update?.changed && typeof update.html === 'string' && typeof update.version === 'string') {
1278
+ // A rejected artifact must not be resent every poll. A later build
1279
+ // has a different digest and can still replace it.
1280
+ version = update.version;
1281
+ attemptedVersion = true;
1282
+ const next = new DOMParser().parseFromString(update.html, 'text/html');
1283
+ const nextStyle = next.getElementById('smrt-planner-styles');
1284
+ const nextEntry = next.getElementById('smrt-planner-entry');
1285
+ const currentStyle = document.getElementById('smrt-planner-styles');
1286
+ const currentEntry = document.getElementById('smrt-planner-entry');
1287
+ if (!nextStyle || !nextEntry || !currentStyle || !currentEntry || !window.smrtKitchenUnmount) {
1288
+ throw new Error('The Planner bundle cannot be reloaded in this tab');
1289
+ }
1290
+ await window.smrtKitchenUnmount();
1291
+ const style = document.createElement('style');
1292
+ style.id = 'smrt-planner-styles';
1293
+ style.textContent = nextStyle.textContent;
1294
+ currentStyle.replaceWith(style);
1295
+ const entry = document.createElement('script');
1296
+ entry.id = 'smrt-planner-entry';
1297
+ entry.textContent = nextEntry.textContent;
1298
+ currentEntry.replaceWith(entry);
1299
+ reloadNotice?.remove();
1300
+ reloadNotice = undefined;
1301
+ }
1302
+ } catch {
1303
+ if (attemptedVersion) showReloadNotice();
1304
+ // A build may be in progress; check again for a newer digest.
1305
+ }
1306
+ checking = false;
1307
+ };
1308
+ const timer = setInterval(check, 1500);
1309
+ window.addEventListener('pagehide', () => {
1310
+ stopped = true;
1311
+ clearInterval(timer);
1312
+ }, { once: true });
1313
+ }).catch(() => {});
1314
+ })();
1315
+ <\/script>`;
1316
+ //#endregion
1317
+ //#region src/commands/kitchen/mcp/ui.ts
1318
+ /** Opt-in desktop capability probe. This is not the Planner UI. */
1319
+ var KITCHEN_UI_URI = "ui://smrt-kitchen/capability-v1.html";
1320
+ var KITCHEN_UI_CONTENT = {
1321
+ uri: KITCHEN_UI_URI,
1322
+ name: "SMRT Kitchen desktop capability check",
1323
+ mimeType: "text/html;profile=mcp-app",
1324
+ text: `<!doctype html>
1325
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
1326
+ ${KITCHEN_BRIDGE}
1327
+ <title>SMRT Kitchen capability check</title></head>
1328
+ <body><main><h1>SMRT Kitchen</h1>
1329
+ <p>Local MCP App resource rendered.</p>
1330
+ <p>This capability check does not edit your plan. Continue using the Kitchen tools in chat.</p>
1331
+ </main></body></html>`,
1332
+ _meta: { ui: {
1333
+ prefersBorder: true,
1334
+ csp: {
1335
+ connectDomains: [],
1336
+ resourceDomains: []
1337
+ }
1338
+ } }
1339
+ };
1340
+ /** Load only an operator-selected local artifact; tool arguments never choose files. */
1341
+ function loadPlannerUi(file, resumePlanId) {
1342
+ const html = readFileSync(file, "utf8");
1343
+ if (!/<head(?:\s[^>]*)?>/i.test(html)) throw new Error("Planner UI must be a complete self-contained HTML document");
1344
+ const digest = createHash("sha256").update(html).digest("hex").slice(0, 16);
1345
+ const bootstrap = `<script>window.__smrtKitchenUiVersion=${JSON.stringify(digest)};window.__smrtKitchenReloadPlanId=${JSON.stringify(resumePlanId ?? "").replace(/</g, "\\u003c")};<\/script>`;
1346
+ const text = html.replace(/<head(?:\s[^>]*)?>/i, (head) => head + bootstrap + KITCHEN_BRIDGE);
1347
+ return {
1348
+ ...KITCHEN_UI_CONTENT,
1349
+ uri: `ui://smrt-kitchen/planner-${digest}.html`,
1350
+ name: "SMRT Planner",
1351
+ text
1352
+ };
1353
+ }
1354
+ //#endregion
838
1355
  //#region src/commands/kitchen/mcp/server.ts
839
1356
  /**
840
1357
  * `smrt kitchen mcp` (#3752): the Kitchen tools served over stdio. stdout
@@ -849,24 +1366,143 @@ var CATALOG_CACHE_HINT = {
849
1366
  /** Build the MCP server; one {@link KitchenSession} (plans, catalog) per server. */
850
1367
  function createKitchenMcpServer(options) {
851
1368
  const session = new KitchenSession(options);
1369
+ let plannerUi;
1370
+ if (options.plannerUi) try {
1371
+ plannerUi = loadPlannerUi(options.plannerUi);
1372
+ } catch {
1373
+ console.error(`[${SERVER_NAME}] Planner UI is unavailable; planning tools remain active.`);
1374
+ }
1375
+ const resources = [...options.uiSmoke ? [KITCHEN_UI_CONTENT] : [], ...plannerUi ? [plannerUi] : []];
1376
+ const getPlanTool = KITCHEN_TOOLS.find((tool) => tool.name === "get_plan");
1377
+ if (!getPlanTool) throw new Error("Kitchen get_plan tool is missing");
852
1378
  const server = new Server({
853
1379
  name: SERVER_NAME,
854
1380
  version: cliVersion()
855
1381
  }, {
856
- capabilities: { tools: {} },
1382
+ capabilities: {
1383
+ tools: {},
1384
+ ...resources.length ? { resources: {} } : {}
1385
+ },
857
1386
  cacheHints: { "tools/list": CATALOG_CACHE_HINT }
858
1387
  });
859
- server.setRequestHandler("tools/list", async () => ({ tools: KITCHEN_TOOLS.map((tool) => ({
860
- name: tool.name,
861
- title: tool.title,
862
- description: tool.description,
863
- inputSchema: tool.inputSchema,
864
- outputSchema: tool.outputSchema,
865
- annotations: tool.annotations
866
- })) }));
1388
+ server.setRequestHandler("tools/list", async () => ({ tools: [
1389
+ ...KITCHEN_TOOLS.map((tool) => ({
1390
+ name: tool.name,
1391
+ title: tool.title,
1392
+ description: tool.description,
1393
+ inputSchema: tool.inputSchema,
1394
+ outputSchema: tool.outputSchema,
1395
+ annotations: tool.annotations
1396
+ })),
1397
+ ...plannerUi ? [{
1398
+ name: "planner_ui_update",
1399
+ title: "Check Planner UI build",
1400
+ description: "Check whether the local Planner UI artifact has changed.",
1401
+ inputSchema: {
1402
+ type: "object",
1403
+ properties: {
1404
+ version: { type: "string" },
1405
+ planId: { type: "string" }
1406
+ },
1407
+ required: ["version"],
1408
+ additionalProperties: false
1409
+ },
1410
+ annotations: {
1411
+ readOnlyHint: true,
1412
+ destructiveHint: false,
1413
+ idempotentHint: true,
1414
+ openWorldHint: false
1415
+ },
1416
+ _meta: { ui: { visibility: ["app"] } }
1417
+ }, {
1418
+ ...getPlanTool,
1419
+ inputSchema: getPlanTool.inputSchema,
1420
+ outputSchema: getPlanTool.outputSchema,
1421
+ name: "render_plan",
1422
+ title: "Open the Planner",
1423
+ description: "Open the interactive Planner for an existing plan. Call start_plan first, then pass its planId. Data tools keep working without the UI.",
1424
+ _meta: {
1425
+ ui: { resourceUri: plannerUi.uri },
1426
+ "openai/ui": { entrypoints: [{ type: "thread" }] }
1427
+ }
1428
+ }] : [],
1429
+ ...options.uiSmoke ? [{
1430
+ name: "kitchen_ui_smoke",
1431
+ title: "Check local Kitchen UI",
1432
+ description: "Render a static local MCP Apps capability check. Does not edit or display the Planner.",
1433
+ inputSchema: {
1434
+ type: "object",
1435
+ properties: {},
1436
+ additionalProperties: false
1437
+ },
1438
+ annotations: {
1439
+ readOnlyHint: true,
1440
+ destructiveHint: false,
1441
+ idempotentHint: true,
1442
+ openWorldHint: false
1443
+ },
1444
+ _meta: { ui: { resourceUri: KITCHEN_UI_URI } }
1445
+ }] : []
1446
+ ] }));
1447
+ if (resources.length) {
1448
+ server.setRequestHandler("resources/list", async () => ({ resources: resources.map(({ uri, name, mimeType }) => ({
1449
+ uri,
1450
+ name,
1451
+ mimeType
1452
+ })) }));
1453
+ server.setRequestHandler("resources/read", async (request) => {
1454
+ const resource = resources.find((item) => item.uri === request.params.uri);
1455
+ if (!resource) throw new Error(`Unknown Kitchen resource: ${request.params.uri}`);
1456
+ return { contents: [resource] };
1457
+ });
1458
+ }
867
1459
  server.setRequestHandler("tools/call", async (request) => {
868
1460
  const { name, arguments: args } = request.params;
869
- const result = await session.call(name, args ?? {});
1461
+ if (plannerUi && options.plannerUi && name === "planner_ui_update") {
1462
+ const version = typeof args?.version === "string" ? args.version : "";
1463
+ const planId = typeof args?.planId === "string" ? args.planId : void 0;
1464
+ let current;
1465
+ try {
1466
+ current = loadPlannerUi(options.plannerUi, planId);
1467
+ } catch {
1468
+ return {
1469
+ content: [{
1470
+ type: "text",
1471
+ text: "Planner UI build is not ready."
1472
+ }],
1473
+ structuredContent: {
1474
+ changed: false,
1475
+ version,
1476
+ error: {
1477
+ code: "unavailable",
1478
+ message: "Planner UI build is not ready."
1479
+ }
1480
+ }
1481
+ };
1482
+ }
1483
+ const currentVersion = current.uri.slice(26, -5);
1484
+ const changed = currentVersion !== version;
1485
+ const data = changed ? {
1486
+ changed,
1487
+ version: currentVersion,
1488
+ html: current.text
1489
+ } : {
1490
+ changed,
1491
+ version: currentVersion
1492
+ };
1493
+ return {
1494
+ content: [{
1495
+ type: "text",
1496
+ text: changed ? "Planner UI updated." : "Planner UI unchanged."
1497
+ }],
1498
+ structuredContent: data
1499
+ };
1500
+ }
1501
+ if (options.uiSmoke && name === "kitchen_ui_smoke") return { content: [{
1502
+ type: "text",
1503
+ text: "Local MCP Apps capability check. Confirm whether the host displays the SMRT Kitchen card."
1504
+ }] };
1505
+ const result = await session.call(plannerUi && name === "render_plan" ? "get_plan" : name, args ?? {});
870
1506
  return {
871
1507
  content: [{
872
1508
  type: "text",
@@ -883,10 +1519,17 @@ function createKitchenMcpServer(options) {
883
1519
  }
884
1520
  /** Serve on this process's stdio until the host closes it or the process is stopped. */
885
1521
  async function runKitchenMcp(options) {
886
- const handle = serveStdio(() => createKitchenMcpServer(options).server, { onerror: (error) => console.error(`[${SERVER_NAME}] ${error.message}`) });
1522
+ const { server, session } = createKitchenMcpServer(options);
1523
+ const preview = options.previewPort !== void 0 ? await startKitchenPreview(session, options.previewPort, options.previewOrigin ?? "") : void 0;
1524
+ session.previewUrl = preview?.url;
1525
+ if (preview) console.error(`[${SERVER_NAME}] Live preview bridge: ${preview.url}`);
1526
+ const handle = serveStdio(() => server, { onerror: (error) => console.error(`[${SERVER_NAME}] ${error.message}`) });
887
1527
  await new Promise((resolve) => {
888
1528
  const done = () => {
889
- handle.close().finally(resolve);
1529
+ handle.close().finally(() => {
1530
+ preview?.server.close();
1531
+ resolve();
1532
+ });
890
1533
  };
891
1534
  process.once("SIGINT", done);
892
1535
  process.once("SIGTERM", done);