@jay-framework/aiditor 0.18.4 → 0.19.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/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import path, { extname } from "path";
5
5
  import { query } from "@anthropic-ai/claude-agent-sdk";
6
6
  import fs$1 from "fs";
7
7
  import crypto from "crypto";
8
+ import { load } from "js-yaml";
8
9
  import { getLogger } from "@jay-framework/logger";
9
10
  import { fileURLToPath } from "url";
10
11
  import { spawn } from "child_process";
@@ -271,6 +272,517 @@ function buildChatPrompt(config, pageRoute, renderedUrl, userMessage) {
271
272
  ];
272
273
  return lines.filter((l) => l !== null).join("\n");
273
274
  }
275
+ const REJECTED_ITEM_FIELDS = [
276
+ "kind",
277
+ "parameters",
278
+ "component",
279
+ "allowedScopes"
280
+ ];
281
+ function isRecord(value) {
282
+ return typeof value === "object" && value !== null && !Array.isArray(value);
283
+ }
284
+ function requiredString(obj, field, path2, errors) {
285
+ const value = obj[field];
286
+ if (typeof value !== "string" || value.trim().length === 0) {
287
+ errors.push({
288
+ path: `${path2}.${field}`,
289
+ message: "required non-empty string"
290
+ });
291
+ return null;
292
+ }
293
+ return value.trim();
294
+ }
295
+ function optionalString(obj, field) {
296
+ const value = obj[field];
297
+ if (value === void 0) return void 0;
298
+ if (typeof value !== "string") return void 0;
299
+ const trimmed = value.trim();
300
+ return trimmed.length > 0 ? trimmed : void 0;
301
+ }
302
+ function validateAddMenuItem(raw, path2) {
303
+ const errors = [];
304
+ if (!isRecord(raw)) {
305
+ return {
306
+ item: null,
307
+ errors: [{ path: path2, message: "item must be an object" }]
308
+ };
309
+ }
310
+ for (const field of REJECTED_ITEM_FIELDS) {
311
+ if (field in raw) {
312
+ errors.push({
313
+ path: `${path2}.${field}`,
314
+ message: `field "${field}" is not allowed in Add Menu catalog items`
315
+ });
316
+ }
317
+ }
318
+ const id = requiredString(raw, "id", path2, errors);
319
+ const title = requiredString(raw, "title", path2, errors);
320
+ const category = requiredString(raw, "category", path2, errors);
321
+ const prompt = requiredString(raw, "prompt", path2, errors);
322
+ if (errors.length > 0 || !id || !title || !category || !prompt) {
323
+ return { item: null, errors };
324
+ }
325
+ return {
326
+ item: {
327
+ id,
328
+ title,
329
+ category,
330
+ prompt,
331
+ pluginName: optionalString(raw, "pluginName"),
332
+ packageName: optionalString(raw, "packageName"),
333
+ subCategory: optionalString(raw, "subCategory"),
334
+ thumbnail: optionalString(raw, "thumbnail")
335
+ },
336
+ errors
337
+ };
338
+ }
339
+ function validateAddMenuCatalogFile(raw, sourcePath) {
340
+ const errors = [];
341
+ if (!isRecord(raw)) {
342
+ return {
343
+ file: null,
344
+ errors: [{ path: sourcePath, message: "catalog file must be an object" }]
345
+ };
346
+ }
347
+ if (!Array.isArray(raw.items)) {
348
+ return {
349
+ file: null,
350
+ errors: [
351
+ { path: `${sourcePath}.items`, message: "items must be an array" }
352
+ ]
353
+ };
354
+ }
355
+ const items = [];
356
+ raw.items.forEach((entry, index) => {
357
+ const result = validateAddMenuItem(entry, `${sourcePath}.items[${index}]`);
358
+ errors.push(...result.errors);
359
+ if (result.item) items.push(result.item);
360
+ });
361
+ if (items.length === 0 && errors.length > 0) {
362
+ return { file: null, errors };
363
+ }
364
+ return { file: { items }, errors };
365
+ }
366
+ function parsePluginsIndexYaml(yaml) {
367
+ const entries = [];
368
+ let currentPlugin = null;
369
+ let inContracts = false;
370
+ for (const line of yaml.split("\n")) {
371
+ const pluginMatch = line.match(/^ - name:\s*(.+)$/);
372
+ if (pluginMatch && !line.startsWith(" ")) {
373
+ currentPlugin = pluginMatch[1].trim();
374
+ inContracts = false;
375
+ continue;
376
+ }
377
+ if (line.trim() === "contracts:") {
378
+ inContracts = true;
379
+ continue;
380
+ }
381
+ if (inContracts && currentPlugin) {
382
+ const contractMatch = line.match(/^ - name:\s*(.+)$/);
383
+ if (contractMatch) {
384
+ entries.push({
385
+ pluginName: currentPlugin,
386
+ contractName: contractMatch[1].trim()
387
+ });
388
+ continue;
389
+ }
390
+ const descMatch = line.match(/^ description:\s*(.+)$/);
391
+ if (descMatch && entries.length > 0) {
392
+ const last = entries[entries.length - 1];
393
+ if (last.pluginName === currentPlugin) {
394
+ last.description = descMatch[1].trim();
395
+ }
396
+ continue;
397
+ }
398
+ const typeMatch = line.match(/^ type:\s*(.+)$/);
399
+ if (typeMatch && entries.length > 0) {
400
+ const last = entries[entries.length - 1];
401
+ if (last.pluginName === currentPlugin) {
402
+ last.isDynamic = typeMatch[1].trim() === "dynamic";
403
+ }
404
+ }
405
+ }
406
+ if (line.match(/^ actions:/) || line.match(/^ routes:/)) {
407
+ inContracts = false;
408
+ }
409
+ }
410
+ return entries;
411
+ }
412
+ function packageNameToPluginName(packageName) {
413
+ const prefix = "@jay-framework/";
414
+ if (packageName.startsWith(prefix)) {
415
+ return packageName.slice(prefix.length);
416
+ }
417
+ return packageName;
418
+ }
419
+ async function readPackageJsonDeps(projectRoot) {
420
+ const raw = await fs.readFile(
421
+ path.join(projectRoot, "package.json"),
422
+ "utf-8"
423
+ );
424
+ const pkg = JSON.parse(raw);
425
+ return { ...pkg.devDependencies, ...pkg.dependencies };
426
+ }
427
+ async function getInstalledPlugins(projectRoot) {
428
+ const deps = await readPackageJsonDeps(projectRoot);
429
+ const installed = [];
430
+ for (const [packageName, spec] of Object.entries(deps)) {
431
+ if (!packageName.startsWith("@jay-framework/")) continue;
432
+ installed.push({
433
+ pluginName: packageNameToPluginName(packageName),
434
+ packageName,
435
+ installSpec: spec
436
+ });
437
+ }
438
+ return installed;
439
+ }
440
+ async function isPluginInstalled(projectRoot, pluginName) {
441
+ const installed = await getInstalledPlugins(projectRoot);
442
+ return installed.some((p) => p.pluginName === pluginName);
443
+ }
444
+ async function parsePluginsIndexContracts(projectRoot) {
445
+ const indexPath = path.join(projectRoot, "agent-kit", "plugins-index.yaml");
446
+ try {
447
+ const yaml = await fs.readFile(indexPath, "utf-8");
448
+ return parsePluginsIndexYaml(yaml);
449
+ } catch (e) {
450
+ const code = e && typeof e === "object" && "code" in e ? e.code : void 0;
451
+ if (code === "ENOENT") return [];
452
+ throw e;
453
+ }
454
+ }
455
+ async function parsePluginYamlContracts(projectRoot, pluginName) {
456
+ const pluginYamlPath = path.join(
457
+ projectRoot,
458
+ "node_modules",
459
+ "@jay-framework",
460
+ pluginName,
461
+ "plugin.yaml"
462
+ );
463
+ try {
464
+ const yaml = await fs.readFile(pluginYamlPath, "utf-8");
465
+ const entries = [];
466
+ let inContracts = false;
467
+ for (const line of yaml.split("\n")) {
468
+ if (line.trim() === "contracts:") {
469
+ inContracts = true;
470
+ continue;
471
+ }
472
+ if (inContracts) {
473
+ const nameMatch = line.match(/^ - name:\s*(.+)$/);
474
+ if (nameMatch) {
475
+ entries.push({
476
+ pluginName,
477
+ contractName: nameMatch[1].trim()
478
+ });
479
+ }
480
+ if (line.match(/^actions:/) || line.match(/^routes:/)) {
481
+ break;
482
+ }
483
+ }
484
+ }
485
+ return entries;
486
+ } catch {
487
+ return [];
488
+ }
489
+ }
490
+ const ADD_MENU_DIR = path.join("agent-kit", "aiditor", "add-menu");
491
+ function packageNameFromItem(item) {
492
+ if (item.packageName) return item.packageName;
493
+ if (item.pluginName) return `@jay-framework/${item.pluginName}`;
494
+ const colon = item.id.indexOf(":");
495
+ if (colon > 0) {
496
+ return `@jay-framework/${item.id.slice(0, colon)}`;
497
+ }
498
+ return null;
499
+ }
500
+ function pluginNameFromItem(item) {
501
+ if (item.pluginName) return item.pluginName;
502
+ const colon = item.id.indexOf(":");
503
+ if (colon > 0) return item.id.slice(0, colon);
504
+ return null;
505
+ }
506
+ function groupAddMenuItems(items) {
507
+ const byCategory = /* @__PURE__ */ new Map();
508
+ for (const item of items) {
509
+ const sub = item.subCategory ?? null;
510
+ let subMap = byCategory.get(item.category);
511
+ if (!subMap) {
512
+ subMap = /* @__PURE__ */ new Map();
513
+ byCategory.set(item.category, subMap);
514
+ }
515
+ const list = subMap.get(sub) ?? [];
516
+ list.push(item);
517
+ subMap.set(sub, list);
518
+ }
519
+ return [...byCategory.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([category, subMap]) => ({
520
+ category,
521
+ subCategories: [...subMap.entries()].sort(([a], [b]) => {
522
+ const sa = a ?? "";
523
+ const sb = b ?? "";
524
+ return sa.localeCompare(sb);
525
+ }).map(([subCategory, subItems]) => ({
526
+ subCategory,
527
+ items: subItems.sort((a, b) => a.title.localeCompare(b.title))
528
+ }))
529
+ }));
530
+ }
531
+ async function listAddMenuItems(projectRoot) {
532
+ const addMenuDir = path.join(projectRoot, ADD_MENU_DIR);
533
+ const warnings = [];
534
+ let yamlFiles = [];
535
+ try {
536
+ const entries = await fs.readdir(addMenuDir);
537
+ yamlFiles = entries.filter((name) => name.endsWith(".yaml") || name.endsWith(".yml")).map((name) => path.join(addMenuDir, name)).sort((a, b) => a.localeCompare(b));
538
+ } catch (e) {
539
+ const code = e && typeof e === "object" && "code" in e ? e.code : void 0;
540
+ if (code === "ENOENT") {
541
+ return { items: [], categories: [], warnings: [] };
542
+ }
543
+ throw e;
544
+ }
545
+ const installed = await getInstalledPlugins(projectRoot);
546
+ const installedPackages = new Set(installed.map((p) => p.packageName));
547
+ const installedPlugins = new Set(installed.map((p) => p.pluginName));
548
+ const byId = /* @__PURE__ */ new Map();
549
+ for (const filePath of yamlFiles) {
550
+ const relPath = path.relative(projectRoot, filePath);
551
+ let rawContent;
552
+ try {
553
+ rawContent = await fs.readFile(filePath, "utf-8");
554
+ } catch {
555
+ warnings.push(`Could not read Add Menu catalog file: ${relPath}`);
556
+ continue;
557
+ }
558
+ let parsed;
559
+ try {
560
+ parsed = load(rawContent);
561
+ } catch (err) {
562
+ warnings.push(
563
+ `Invalid YAML in ${relPath}: ${err instanceof Error ? err.message : String(err)}`
564
+ );
565
+ continue;
566
+ }
567
+ const validated = validateAddMenuCatalogFile(parsed, relPath);
568
+ if (validated.errors.length > 0) {
569
+ for (const err of validated.errors) {
570
+ warnings.push(`${err.path}: ${err.message}`);
571
+ }
572
+ continue;
573
+ }
574
+ for (const item of validated.file.items) {
575
+ const prev = byId.get(item.id);
576
+ if (prev) {
577
+ warnings.push(
578
+ `Duplicate Add Menu item id "${item.id}": "${prev.sourcePath}" overwritten by "${relPath}"`
579
+ );
580
+ }
581
+ byId.set(item.id, { item, sourcePath: relPath });
582
+ }
583
+ }
584
+ const merged = [...byId.values()].map((entry) => entry.item);
585
+ const filtered = merged.filter((item) => {
586
+ const pkg = packageNameFromItem(item);
587
+ const plugin = pluginNameFromItem(item);
588
+ if (pkg && installedPackages.has(pkg)) return true;
589
+ if (plugin && installedPlugins.has(plugin)) return true;
590
+ return false;
591
+ });
592
+ return {
593
+ items: filtered.sort((a, b) => a.title.localeCompare(b.title)),
594
+ categories: groupAddMenuItems(filtered),
595
+ warnings
596
+ };
597
+ }
598
+ const CATALOG = [
599
+ {
600
+ pluginName: "wix-server-client",
601
+ packageName: "@jay-framework/wix-server-client",
602
+ description: "Wix API client and authentication",
603
+ kind: "service-only",
604
+ requires: [],
605
+ showInAddPagePicker: false,
606
+ configFiles: ["config/.wix.yaml"]
607
+ },
608
+ {
609
+ pluginName: "wix-cart",
610
+ packageName: "@jay-framework/wix-cart",
611
+ description: "Shopping cart headless components",
612
+ kind: "headless",
613
+ requires: ["wix-server-client"],
614
+ showInAddPagePicker: true
615
+ },
616
+ {
617
+ pluginName: "wix-stores",
618
+ packageName: "@jay-framework/wix-stores",
619
+ description: "Wix Stores product search, product page, categories",
620
+ kind: "headless",
621
+ requires: ["wix-server-client", "wix-cart"],
622
+ showInAddPagePicker: true,
623
+ configFiles: ["config/.wix-stores.yaml"]
624
+ },
625
+ {
626
+ pluginName: "wix-stores-v1",
627
+ packageName: "@jay-framework/wix-stores-v1",
628
+ description: "Wix Stores v1 catalog API",
629
+ kind: "headless",
630
+ requires: ["wix-server-client", "wix-cart"],
631
+ showInAddPagePicker: true
632
+ },
633
+ {
634
+ pluginName: "wix-data",
635
+ packageName: "@jay-framework/wix-data",
636
+ description: "Wix Data collections CMS",
637
+ kind: "headless",
638
+ requires: ["wix-server-client"],
639
+ showInAddPagePicker: true,
640
+ configFiles: ["config/.wix-data.yaml"]
641
+ },
642
+ {
643
+ pluginName: "wix-media",
644
+ packageName: "@jay-framework/wix-media",
645
+ description: "Wix media service (no page components)",
646
+ kind: "service-only",
647
+ requires: ["wix-server-client"],
648
+ showInAddPagePicker: true
649
+ },
650
+ {
651
+ pluginName: "ui-kit",
652
+ packageName: "@jay-framework/ui-kit",
653
+ description: "Jay UI kit headless components",
654
+ kind: "headless",
655
+ requires: [],
656
+ showInAddPagePicker: true
657
+ },
658
+ {
659
+ pluginName: "gemini-agent",
660
+ packageName: "@jay-framework/gemini-agent",
661
+ description: "Gemini AI agent plugin",
662
+ kind: "headless",
663
+ requires: [],
664
+ showInAddPagePicker: true,
665
+ configFiles: ["config/.gemini-agent.yaml"]
666
+ },
667
+ {
668
+ pluginName: "webmcp",
669
+ packageName: "@jay-framework/webmcp",
670
+ description: "Web MCP tooling",
671
+ kind: "tooling",
672
+ requires: [],
673
+ showInAddPagePicker: false
674
+ },
675
+ {
676
+ pluginName: "aiditor",
677
+ packageName: "@jay-framework/aiditor",
678
+ description: "AIditor self plugin",
679
+ kind: "tooling",
680
+ requires: [],
681
+ showInAddPagePicker: false
682
+ }
683
+ ];
684
+ function getJayPluginCatalog() {
685
+ return CATALOG.map((e) => ({ ...e, requires: [...e.requires] }));
686
+ }
687
+ function getCatalogEntry(pluginName) {
688
+ return CATALOG.find((e) => e.pluginName === pluginName);
689
+ }
690
+ const DEFAULT_COMPONENT_KEYS = {
691
+ "product-page": "p",
692
+ "product-search": "search",
693
+ "cart-indicator": "cart",
694
+ "cart-page": "cartPage",
695
+ "category-list": "categories"
696
+ };
697
+ function getDefaultComponentKey(contractName) {
698
+ if (contractName in DEFAULT_COMPONENT_KEYS) {
699
+ return DEFAULT_COMPONENT_KEYS[contractName];
700
+ }
701
+ return contractName.charAt(0) || "c";
702
+ }
703
+ function pagePluginSelectionsToAttachments(selectedPlugins) {
704
+ const attachments = [];
705
+ for (const plugin of selectedPlugins) {
706
+ for (const contract of plugin.contracts) {
707
+ attachments.push({
708
+ itemId: `${plugin.pluginName}:${contract.contractName}`
709
+ });
710
+ }
711
+ }
712
+ return attachments;
713
+ }
714
+ const ADD_MENU_Q3_RULE = `Add Menu usage (Q3 — follow strictly):
715
+ - Each attached Add Menu item describes a **resource in scope**, not a fixed number of DOM instances.
716
+ - Wire **only** attachments the user referenced in prose, \`@Title\` mentions, or marker instructions.
717
+ - Do **not** implement an attachment's prompt if the user did not reference it.
718
+ - One headless import per resource; multiple instances with different props when the brief requires.`;
719
+ function resolveAddMenuAttachments(input) {
720
+ if (input.addMenuAttachments?.length) return input.addMenuAttachments;
721
+ if (input.selectedPlugins?.length) {
722
+ return pagePluginSelectionsToAttachments(input.selectedPlugins);
723
+ }
724
+ return [];
725
+ }
726
+ function buildAddMenuAttachmentPromptSection(attachments, itemById, options) {
727
+ const scoped = options?.annotationId ? attachments.filter((a) => a.annotationId === options.annotationId) : attachments.filter((a) => !a.annotationId);
728
+ if (scoped.length === 0) return "";
729
+ const lines = ["", "## Add Menu attachments", "", ADD_MENU_Q3_RULE, ""];
730
+ for (const attachment of scoped) {
731
+ const item = itemById.get(attachment.itemId);
732
+ if (!item) {
733
+ lines.push(
734
+ `### ${attachment.itemId}`,
735
+ "",
736
+ "(catalog item not found)",
737
+ ""
738
+ );
739
+ continue;
740
+ }
741
+ const scope = attachment.annotationId ? `Marker #${attachment.annotationId}` : "Request-level";
742
+ lines.push(
743
+ `### ${item.title} (${item.id})`,
744
+ `Scope: ${scope}`,
745
+ "",
746
+ item.prompt.trim(),
747
+ ""
748
+ );
749
+ }
750
+ return lines.join("\n");
751
+ }
752
+ function buildLegacyPluginManifestPromptSection(selectedPlugins) {
753
+ if (!selectedPlugins.length) return "";
754
+ const lines = [
755
+ "",
756
+ "## Page plugin manifest (legacy — prefer Add Menu attachments)",
757
+ "",
758
+ "| Plugin | Contract | Key |",
759
+ "| --- | --- | --- |"
760
+ ];
761
+ for (const plugin of selectedPlugins) {
762
+ for (const contract of plugin.contracts) {
763
+ const key = contract.componentKey ?? getDefaultComponentKey(contract.contractName);
764
+ lines.push(
765
+ `| ${plugin.packageName} | ${contract.contractName} | ${key} |`
766
+ );
767
+ }
768
+ }
769
+ lines.push("");
770
+ return lines.join("\n");
771
+ }
772
+ function buildRouteMigrationPromptSection(routeMigration) {
773
+ if (!routeMigration) return "";
774
+ return [
775
+ "",
776
+ "## Planned route migration (apply only if user request requires it)",
777
+ "",
778
+ `- Current route: \`${routeMigration.currentRoute}\``,
779
+ `- Planned route: \`${routeMigration.plannedRoute}\``,
780
+ `- Triggering Add Menu items: ${routeMigration.triggeringItemIds.join(", ")}`,
781
+ "",
782
+ "Migrate page files to the planned route only when the user's instruction uses the attached route-param components.",
783
+ ""
784
+ ].join("\n");
785
+ }
274
786
  function buildNonVisualPrompt(config, notes, imagePath) {
275
787
  const lines = [
276
788
  `Project directory: ${config.projectDir}`,
@@ -290,6 +802,27 @@ function structuredAnnotationBindings(ann) {
290
802
  if (ann.pluginBinding) return [ann.pluginBinding];
291
803
  return [];
292
804
  }
805
+ function appendAddMenuLinesForAnnotation(body, ann, itemById, indent = "") {
806
+ const attachments = ann.addMenuAttachments ?? [];
807
+ if (attachments.length === 0) return;
808
+ const section = buildAddMenuAttachmentPromptSection(
809
+ attachments.map((a) => ({
810
+ itemId: a.itemId,
811
+ annotationId: a.annotationId ?? ann.id
812
+ })),
813
+ itemById,
814
+ { annotationId: ann.id }
815
+ );
816
+ if (!section.trim()) return;
817
+ for (const line of section.trim().split("\n")) {
818
+ body.push(`${indent}${line}`);
819
+ }
820
+ }
821
+ function appendLegacyBindingLines(body, ann, markerId, indent = "") {
822
+ const bindings = structuredAnnotationBindings(ann);
823
+ if (bindings.length === 0) return;
824
+ appendBindingPromptLines(body, bindings, markerId, indent);
825
+ }
293
826
  function appendBindingPromptLines(body, bindings, markerId, indent = "") {
294
827
  if (bindings.length === 0) return;
295
828
  body.push(`${indent}Headless bindings:`);
@@ -336,7 +869,9 @@ function parseNotesField(notes) {
336
869
  structured: null,
337
870
  structuredVideo: {
338
871
  annotations: j.annotations,
339
- previewNavLog
872
+ previewNavLog,
873
+ addMenuAttachments: j.addMenuAttachments,
874
+ routeMigration: j.routeMigration
340
875
  },
341
876
  plain: ""
342
877
  };
@@ -345,7 +880,11 @@ function parseNotesField(notes) {
345
880
  (a) => a && typeof a === "object" && typeof a.id === "string" && typeof a.instruction === "string" && typeof a.mode === "string"
346
881
  )) {
347
882
  return {
348
- structured: { annotations: j.annotations },
883
+ structured: {
884
+ annotations: j.annotations,
885
+ addMenuAttachments: j.addMenuAttachments,
886
+ routeMigration: j.routeMigration
887
+ },
349
888
  structuredVideo: null,
350
889
  plain: ""
351
890
  };
@@ -354,7 +893,7 @@ function parseNotesField(notes) {
354
893
  }
355
894
  return empty();
356
895
  }
357
- function buildVisualPromptDual(config, pageRoute, renderedUrl, dual, parsed, attachmentPathsByAnnotationId) {
896
+ function buildVisualPromptDual(config, pageRoute, renderedUrl, dual, parsed, attachmentPathsByAnnotationId, addMenu = { itemById: /* @__PURE__ */ new Map() }) {
358
897
  const preamble = [
359
898
  `Project directory: ${config.projectDir}`,
360
899
  "",
@@ -373,6 +912,18 @@ function buildVisualPromptDual(config, pageRoute, renderedUrl, dual, parsed, att
373
912
  ""
374
913
  ];
375
914
  const body = [];
915
+ const routeMigration = parsed.structured?.routeMigration ?? void 0;
916
+ const requestAddMenu = parsed.structured?.addMenuAttachments ?? [];
917
+ if (requestAddMenu.length > 0) {
918
+ const section = buildAddMenuAttachmentPromptSection(
919
+ requestAddMenu,
920
+ addMenu.itemById
921
+ );
922
+ if (section.trim()) body.push(section.trim(), "");
923
+ }
924
+ if (routeMigration) {
925
+ body.push(buildRouteMigrationPromptSection(routeMigration).trim(), "");
926
+ }
376
927
  if (parsed.structured) {
377
928
  body.push("Annotations (apply in order):", "");
378
929
  const sorted = [...parsed.structured.annotations].sort(
@@ -382,7 +933,10 @@ function buildVisualPromptDual(config, pageRoute, renderedUrl, dual, parsed, att
382
933
  const pin = ann.id;
383
934
  const paths = attachmentPathsByAnnotationId.get(pin) ?? [];
384
935
  body.push(`Annotation ${pin} (${ann.mode})`);
385
- appendBindingPromptLines(body, structuredAnnotationBindings(ann), ann.id);
936
+ appendAddMenuLinesForAnnotation(body, ann, addMenu.itemById);
937
+ if (!ann.addMenuAttachments?.length) {
938
+ appendLegacyBindingLines(body, ann, ann.id);
939
+ }
386
940
  body.push(`Instruction: ${ann.instruction.trim()}`);
387
941
  if (paths.length > 0) {
388
942
  body.push(
@@ -407,7 +961,7 @@ function pinOrder(annotations) {
407
961
  return a.id.localeCompare(b.id, void 0, { sensitivity: "base" });
408
962
  });
409
963
  }
410
- function buildVisualPromptVideo(config, pageRoute, renderedUrl, videoPath, frames, parsed, attachmentPathsByPin) {
964
+ function buildVisualPromptVideo(config, pageRoute, renderedUrl, videoPath, frames, parsed, attachmentPathsByPin, addMenu = { itemById: /* @__PURE__ */ new Map() }) {
411
965
  const sv = parsed.structuredVideo;
412
966
  const navLines = [];
413
967
  if (sv?.previewNavLog && sv.previewNavLog.length > 0) {
@@ -441,7 +995,20 @@ function buildVisualPromptVideo(config, pageRoute, renderedUrl, videoPath, frame
441
995
  for (const [i, a] of pinOrder(sv.annotations).entries()) {
442
996
  pinById.set(a.id, String(i + 1));
443
997
  }
444
- const body = ["Per-moment captures:", ""];
998
+ const body = [];
999
+ const routeMigration = sv.routeMigration;
1000
+ const requestAddMenu = sv.addMenuAttachments ?? [];
1001
+ if (requestAddMenu.length > 0) {
1002
+ const section = buildAddMenuAttachmentPromptSection(
1003
+ requestAddMenu,
1004
+ addMenu.itemById
1005
+ );
1006
+ if (section.trim()) body.push(section.trim(), "");
1007
+ }
1008
+ if (routeMigration) {
1009
+ body.push(buildRouteMigrationPromptSection(routeMigration).trim(), "");
1010
+ }
1011
+ body.push("Per-moment captures:", "");
445
1012
  for (const fr of frames) {
446
1013
  body.push(
447
1014
  `Moment ${fr.frameIndex + 1} — t=${fr.timeSec.toFixed(2)}s`,
@@ -455,12 +1022,10 @@ function buildVisualPromptVideo(config, pageRoute, renderedUrl, videoPath, frame
455
1022
  const pinNum = pinById.get(ann.id) ?? "?";
456
1023
  const paths = attachmentPathsByPin.get(pinNum) ?? [];
457
1024
  body.push(`- Pin ${pinNum} (id ${ann.id}) — ${ann.mode}`);
458
- appendBindingPromptLines(
459
- body,
460
- structuredAnnotationBindings(ann),
461
- ann.id,
462
- " "
463
- );
1025
+ appendAddMenuLinesForAnnotation(body, ann, addMenu.itemById, " ");
1026
+ if (!ann.addMenuAttachments?.length) {
1027
+ appendLegacyBindingLines(body, ann, ann.id, " ");
1028
+ }
464
1029
  body.push(` Instruction: ${ann.instruction.trim()}`);
465
1030
  if (ann.previewUrlAtTime) {
466
1031
  body.push(
@@ -500,6 +1065,10 @@ const submitTaskAction = makeJayStream("aiditor.submitTask").withFiles().withHan
500
1065
  const parsed = parseNotesField(input.notes);
501
1066
  const isVideo = input.visualTask === "5";
502
1067
  const isVisual = !!input.visualTask;
1068
+ const catalog = await listAddMenuItems(projectDir);
1069
+ const addMenu = {
1070
+ itemById: new Map(catalog.items.map((item) => [item.id, item]))
1071
+ };
503
1072
  let promptContent;
504
1073
  if (isVideo) {
505
1074
  let videoPath = "";
@@ -541,7 +1110,8 @@ const submitTaskAction = makeJayStream("aiditor.submitTask").withFiles().withHan
541
1110
  videoPath,
542
1111
  frames,
543
1112
  parsed,
544
- attachmentPathsByPin
1113
+ attachmentPathsByPin,
1114
+ addMenu
545
1115
  );
546
1116
  } else if (isVisual && input.screenshot_markers_only) {
547
1117
  const ssDir = path.join(taskDir, "screenshots");
@@ -570,7 +1140,8 @@ const submitTaskAction = makeJayStream("aiditor.submitTask").withFiles().withHan
570
1140
  input.renderedUrl ?? "",
571
1141
  dual,
572
1142
  parsed,
573
- attachmentPathsByAnnotationId
1143
+ attachmentPathsByAnnotationId,
1144
+ addMenu
574
1145
  );
575
1146
  } else if (input.messageKind === "chat") {
576
1147
  promptContent = buildChatPrompt(
@@ -606,164 +1177,59 @@ const submitTaskAction = makeJayStream("aiditor.submitTask").withFiles().withHan
606
1177
  systemPrompt
607
1178
  };
608
1179
  try {
609
- async function* streamAgentQuery(opts) {
610
- for await (const message of query({
611
- prompt: promptContent,
612
- options: {
613
- ...sharedOptions,
614
- ...opts
615
- }
616
- })) {
617
- for (const chunk of transformSDKMessage(message)) {
618
- yield chunk;
619
- }
620
- }
621
- }
622
- let didStaleResumeRetry = false;
623
- try {
624
- if (sessionPlan.useResume) {
625
- yield* streamAgentQuery({ resume: sessionPlan.sessionId });
626
- } else {
627
- yield* streamAgentQuery({
628
- sessionId: sessionPlan.sessionId,
629
- title: routeKey
630
- });
631
- }
632
- } catch (err) {
633
- if (sessionPlan.useResume && !didStaleResumeRetry) {
634
- didStaleResumeRetry = true;
635
- const fresh = await replaceRouteSessionAfterStaleResume(
636
- projectDir,
637
- routeKey
638
- );
639
- try {
640
- yield* streamAgentQuery({
641
- sessionId: fresh.sessionId,
642
- title: routeKey
643
- });
644
- } catch (err2) {
645
- yield {
646
- type: "error",
647
- message: err2 instanceof Error ? err2.message : String(err2)
648
- };
649
- }
650
- } else {
651
- yield {
652
- type: "error",
653
- message: err instanceof Error ? err.message : String(err)
654
- };
655
- }
656
- }
657
- } finally {
658
- endRouteQuery(routeKey);
659
- }
660
- yield { type: "done" };
661
- });
662
- const CATALOG = [
663
- {
664
- pluginName: "wix-server-client",
665
- packageName: "@jay-framework/wix-server-client",
666
- description: "Wix API client and authentication",
667
- kind: "service-only",
668
- requires: [],
669
- showInAddPagePicker: false,
670
- configFiles: ["config/.wix.yaml"]
671
- },
672
- {
673
- pluginName: "wix-cart",
674
- packageName: "@jay-framework/wix-cart",
675
- description: "Shopping cart headless components",
676
- kind: "headless",
677
- requires: ["wix-server-client"],
678
- showInAddPagePicker: true
679
- },
680
- {
681
- pluginName: "wix-stores",
682
- packageName: "@jay-framework/wix-stores",
683
- description: "Wix Stores product search, product page, categories",
684
- kind: "headless",
685
- requires: ["wix-server-client", "wix-cart"],
686
- showInAddPagePicker: true,
687
- configFiles: ["config/.wix-stores.yaml"]
688
- },
689
- {
690
- pluginName: "wix-stores-v1",
691
- packageName: "@jay-framework/wix-stores-v1",
692
- description: "Wix Stores v1 catalog API",
693
- kind: "headless",
694
- requires: ["wix-server-client", "wix-cart"],
695
- showInAddPagePicker: true
696
- },
697
- {
698
- pluginName: "wix-data",
699
- packageName: "@jay-framework/wix-data",
700
- description: "Wix Data collections CMS",
701
- kind: "headless",
702
- requires: ["wix-server-client"],
703
- showInAddPagePicker: true,
704
- configFiles: ["config/.wix-data.yaml"]
705
- },
706
- {
707
- pluginName: "wix-media",
708
- packageName: "@jay-framework/wix-media",
709
- description: "Wix media service (no page components)",
710
- kind: "service-only",
711
- requires: ["wix-server-client"],
712
- showInAddPagePicker: true
713
- },
714
- {
715
- pluginName: "ui-kit",
716
- packageName: "@jay-framework/ui-kit",
717
- description: "Jay UI kit headless components",
718
- kind: "headless",
719
- requires: [],
720
- showInAddPagePicker: true
721
- },
722
- {
723
- pluginName: "gemini-agent",
724
- packageName: "@jay-framework/gemini-agent",
725
- description: "Gemini AI agent plugin",
726
- kind: "headless",
727
- requires: [],
728
- showInAddPagePicker: true,
729
- configFiles: ["config/.gemini-agent.yaml"]
730
- },
731
- {
732
- pluginName: "webmcp",
733
- packageName: "@jay-framework/webmcp",
734
- description: "Web MCP tooling",
735
- kind: "tooling",
736
- requires: [],
737
- showInAddPagePicker: false
738
- },
739
- {
740
- pluginName: "aiditor",
741
- packageName: "@jay-framework/aiditor",
742
- description: "AIditor self plugin",
743
- kind: "tooling",
744
- requires: [],
745
- showInAddPagePicker: false
746
- }
747
- ];
748
- function getJayPluginCatalog() {
749
- return CATALOG.map((e) => ({ ...e, requires: [...e.requires] }));
750
- }
751
- function getCatalogEntry(pluginName) {
752
- return CATALOG.find((e) => e.pluginName === pluginName);
753
- }
754
- const DEFAULT_COMPONENT_KEYS = {
755
- "product-page": "p",
756
- "product-search": "search",
757
- "cart-indicator": "cart",
758
- "cart-page": "cartPage",
759
- "category-list": "categories"
760
- };
761
- function getDefaultComponentKey(contractName) {
762
- if (contractName in DEFAULT_COMPONENT_KEYS) {
763
- return DEFAULT_COMPONENT_KEYS[contractName];
1180
+ async function* streamAgentQuery(opts) {
1181
+ for await (const message of query({
1182
+ prompt: promptContent,
1183
+ options: {
1184
+ ...sharedOptions,
1185
+ ...opts
1186
+ }
1187
+ })) {
1188
+ for (const chunk of transformSDKMessage(message)) {
1189
+ yield chunk;
1190
+ }
1191
+ }
1192
+ }
1193
+ let didStaleResumeRetry = false;
1194
+ try {
1195
+ if (sessionPlan.useResume) {
1196
+ yield* streamAgentQuery({ resume: sessionPlan.sessionId });
1197
+ } else {
1198
+ yield* streamAgentQuery({
1199
+ sessionId: sessionPlan.sessionId,
1200
+ title: routeKey
1201
+ });
1202
+ }
1203
+ } catch (err) {
1204
+ if (sessionPlan.useResume && !didStaleResumeRetry) {
1205
+ didStaleResumeRetry = true;
1206
+ const fresh = await replaceRouteSessionAfterStaleResume(
1207
+ projectDir,
1208
+ routeKey
1209
+ );
1210
+ try {
1211
+ yield* streamAgentQuery({
1212
+ sessionId: fresh.sessionId,
1213
+ title: routeKey
1214
+ });
1215
+ } catch (err2) {
1216
+ yield {
1217
+ type: "error",
1218
+ message: err2 instanceof Error ? err2.message : String(err2)
1219
+ };
1220
+ }
1221
+ } else {
1222
+ yield {
1223
+ type: "error",
1224
+ message: err instanceof Error ? err.message : String(err)
1225
+ };
1226
+ }
1227
+ }
1228
+ } finally {
1229
+ endRouteQuery(routeKey);
764
1230
  }
765
- return contractName.charAt(0) || "c";
766
- }
1231
+ yield { type: "done" };
1232
+ });
767
1233
  class AddPagePromptLoadError extends Error {
768
1234
  constructor(message) {
769
1235
  super(message);
@@ -814,34 +1280,6 @@ async function listDirFiles(dir, prefix) {
814
1280
  return [];
815
1281
  }
816
1282
  }
817
- function buildPluginManifestPromptSection(selectedPlugins) {
818
- const lines = [
819
- "",
820
- "## Page plugin manifest (authoritative)",
821
- "",
822
- "The user selected these headless components for this page. You MUST:",
823
- '1. Add `<script type="application/jay-headless" plugin="..." contract="..." key="...">` for each entry',
824
- "2. Bind UI using contract ViewState and refs per agent-kit designer guides",
825
- "3. Run jay-stack validate",
826
- "",
827
- "| Plugin | Contract | Key | Notes |",
828
- "| --- | --- | --- | --- |"
829
- ];
830
- for (const plugin of selectedPlugins) {
831
- for (const contract of plugin.contracts) {
832
- const key = contract.componentKey ?? getDefaultComponentKey(contract.contractName);
833
- lines.push(
834
- `| ${plugin.packageName} | ${contract.contractName} | ${key} | |`
835
- );
836
- }
837
- }
838
- lines.push(
839
- "",
840
- "If Wix config is incomplete, implement layout and headless imports; live store data may be unavailable until config/.wix.yaml is filled.",
841
- ""
842
- );
843
- return lines.join("\n");
844
- }
845
1283
  async function buildAddPagePrompt(input) {
846
1284
  const { system, skill } = await loadAddPageAgentPrompts();
847
1285
  const contentPath = path.join(input.requestDir, "content.md");
@@ -870,7 +1308,13 @@ async function buildAddPagePrompt(input) {
870
1308
  ## Retry mode
871
1309
  The page at route \`${input.pageRoute}\` may already have partial files from a previous run. Read existing page files and **fix** validate errors rather than recreating from scratch.
872
1310
  ` : "";
873
- const manifestSection = input.selectedPlugins && input.selectedPlugins.length > 0 ? buildPluginManifestPromptSection(input.selectedPlugins) : "";
1311
+ const attachments = resolveAddMenuAttachments(input);
1312
+ const catalog = await listAddMenuItems(input.projectDir);
1313
+ const itemById = new Map(catalog.items.map((item) => [item.id, item]));
1314
+ const addMenuSection = attachments.length > 0 ? buildAddMenuAttachmentPromptSection(attachments, itemById) : input.selectedPlugins?.length ? buildLegacyPluginManifestPromptSection(input.selectedPlugins) : "";
1315
+ const routeMigrationSection = buildRouteMigrationPromptSection(
1316
+ input.routeMigration
1317
+ );
874
1318
  const userMessage = `# Add Page task
875
1319
 
876
1320
  Create **one** Jay Stack page at route: \`${input.pageRoute}\` (${input.routeKind}).
@@ -898,7 +1342,7 @@ Asset files live under the request folder. **Copy** cited assets from \`${input.
898
1342
  - Match user intent from content.md and design.md as closely as possible.
899
1343
  - Reuse existing site patterns (header, footer, styling) from other pages when design.md requests it.
900
1344
  - Summarize files created and how they cover the brief when done.
901
- ${manifestSection}
1345
+ ${addMenuSection}${routeMigrationSection}
902
1346
  ---
903
1347
 
904
1348
  ${skill}`;
@@ -2062,7 +2506,9 @@ const submitAddPageAction = makeJayStream("aiditor.submitAddPage").withServices(
2062
2506
  pageRoute: normalizedRoute,
2063
2507
  routeKind,
2064
2508
  isRetry: input.isRetry,
2065
- selectedPlugins: request.selectedPlugins
2509
+ selectedPlugins: request.selectedPlugins,
2510
+ addMenuAttachments: request.addMenuAttachments,
2511
+ routeMigration: request.routeMigration
2066
2512
  })) {
2067
2513
  yield chunk;
2068
2514
  if (chunk.type === "error") {
@@ -2090,227 +2536,103 @@ const submitAddPageAction = makeJayStream("aiditor.submitAddPage").withServices(
2090
2536
  message: "Validation failed.",
2091
2537
  text: validateResult.errors.join("\n"),
2092
2538
  validateErrors: validateResult.errors
2093
- };
2094
- yield { type: "done" };
2095
- return;
2096
- }
2097
- await updatePageRequestStatus(projectDir, input.requestId, "completed");
2098
- await devServer.refreshRoutes();
2099
- const jayHtmlPath = await resolveJayHtmlPathForPageRoute(
2100
- projectDir,
2101
- normalizedRoute,
2102
- devServer.listRoutes()
2103
- );
2104
- if (jayHtmlPath) {
2105
- try {
2106
- await savePageBriefFromRequest(projectDir, jayHtmlPath, {
2107
- requestId: input.requestId,
2108
- pageRoute: normalizedRoute,
2109
- contentMd: input.contentMd,
2110
- designMd: input.designMd
2111
- });
2112
- } catch (err) {
2113
- getLogger().warn(
2114
- `[Add Page] Failed to save page brief for ${normalizedRoute}: ${err instanceof Error ? err.message : String(err)}`
2115
- );
2116
- }
2117
- }
2118
- yield {
2119
- type: "result",
2120
- message: `Page created at ${normalizedRoute}.`,
2121
- text: normalizedRoute
2122
- };
2123
- yield { type: "done" };
2124
- });
2125
- const generateAddPageBriefFromImageAction = makeJayQuery(
2126
- "aiditor.generateAddPageBriefFromImage"
2127
- ).withMethod("POST").withFiles().withHandler(
2128
- async (input) => {
2129
- const imageFiles = extractImageFilesFromExtraFiles(input.extraFiles);
2130
- const validationError = validateBriefFillImages(imageFiles);
2131
- if (validationError) {
2132
- return { ok: false, error: validationError };
2133
- }
2134
- const projectDir = process.cwd();
2135
- if (input.requestId) {
2136
- const request = await readPageRequest(projectDir, input.requestId);
2137
- if (!request) {
2138
- return {
2139
- ok: false,
2140
- error: `Page request not found: ${input.requestId}`
2141
- };
2142
- }
2143
- }
2144
- try {
2145
- const images = imageFiles.map(readImageAsBase64);
2146
- const result = await generateBriefFromImages(
2147
- {
2148
- target: input.target,
2149
- images,
2150
- contextNotes: input.contextNotes,
2151
- pageRoute: input.pageRoute
2152
- },
2153
- { projectDir }
2154
- );
2155
- const referenceAttachments = [];
2156
- if (input.requestId) {
2157
- for (const file of imageFiles) {
2158
- const id = generatePageRequestId();
2159
- const filename = sanitizeAttachmentFilename(file.name);
2160
- const relPath = `references/${filename}`;
2161
- const destPath = path.join(
2162
- getPageRequestDir(projectDir, input.requestId),
2163
- relPath
2164
- );
2165
- persistUpload(file, destPath);
2166
- await addPageRequestAttachment(
2167
- projectDir,
2168
- input.requestId,
2169
- { id, path: relPath, label: filename },
2170
- "reference"
2171
- );
2172
- referenceAttachments.push({ id, path: relPath, filename });
2173
- }
2174
- }
2175
- return {
2176
- ok: true,
2177
- markdown: result.markdown,
2178
- suggestedRoute: result.suggestedRoute,
2179
- suggestedRouteKind: result.suggestedRouteKind,
2180
- referenceAttachments
2181
- };
2182
- } catch (err) {
2183
- return {
2184
- ok: false,
2185
- error: err instanceof Error ? err.message : String(err)
2186
- };
2187
- }
2188
- }
2189
- );
2190
- function parsePluginsIndexYaml(yaml) {
2191
- const entries = [];
2192
- let currentPlugin = null;
2193
- let inContracts = false;
2194
- for (const line of yaml.split("\n")) {
2195
- const pluginMatch = line.match(/^ - name:\s*(.+)$/);
2196
- if (pluginMatch && !line.startsWith(" ")) {
2197
- currentPlugin = pluginMatch[1].trim();
2198
- inContracts = false;
2199
- continue;
2200
- }
2201
- if (line.trim() === "contracts:") {
2202
- inContracts = true;
2203
- continue;
2204
- }
2205
- if (inContracts && currentPlugin) {
2206
- const contractMatch = line.match(/^ - name:\s*(.+)$/);
2207
- if (contractMatch) {
2208
- entries.push({
2209
- pluginName: currentPlugin,
2210
- contractName: contractMatch[1].trim()
2211
- });
2212
- continue;
2213
- }
2214
- const descMatch = line.match(/^ description:\s*(.+)$/);
2215
- if (descMatch && entries.length > 0) {
2216
- const last = entries[entries.length - 1];
2217
- if (last.pluginName === currentPlugin) {
2218
- last.description = descMatch[1].trim();
2219
- }
2220
- continue;
2221
- }
2222
- const typeMatch = line.match(/^ type:\s*(.+)$/);
2223
- if (typeMatch && entries.length > 0) {
2224
- const last = entries[entries.length - 1];
2225
- if (last.pluginName === currentPlugin) {
2226
- last.isDynamic = typeMatch[1].trim() === "dynamic";
2227
- }
2228
- }
2229
- }
2230
- if (line.match(/^ actions:/) || line.match(/^ routes:/)) {
2231
- inContracts = false;
2232
- }
2233
- }
2234
- return entries;
2235
- }
2236
- function packageNameToPluginName(packageName) {
2237
- const prefix = "@jay-framework/";
2238
- if (packageName.startsWith(prefix)) {
2239
- return packageName.slice(prefix.length);
2240
- }
2241
- return packageName;
2242
- }
2243
- async function readPackageJsonDeps(projectRoot) {
2244
- const raw = await fs.readFile(
2245
- path.join(projectRoot, "package.json"),
2246
- "utf-8"
2247
- );
2248
- const pkg = JSON.parse(raw);
2249
- return { ...pkg.devDependencies, ...pkg.dependencies };
2250
- }
2251
- async function getInstalledPlugins(projectRoot) {
2252
- const deps = await readPackageJsonDeps(projectRoot);
2253
- const installed = [];
2254
- for (const [packageName, spec] of Object.entries(deps)) {
2255
- if (!packageName.startsWith("@jay-framework/")) continue;
2256
- installed.push({
2257
- pluginName: packageNameToPluginName(packageName),
2258
- packageName,
2259
- installSpec: spec
2260
- });
2261
- }
2262
- return installed;
2263
- }
2264
- async function isPluginInstalled(projectRoot, pluginName) {
2265
- const installed = await getInstalledPlugins(projectRoot);
2266
- return installed.some((p) => p.pluginName === pluginName);
2267
- }
2268
- async function parsePluginsIndexContracts(projectRoot) {
2269
- const indexPath = path.join(projectRoot, "agent-kit", "plugins-index.yaml");
2270
- try {
2271
- const yaml = await fs.readFile(indexPath, "utf-8");
2272
- return parsePluginsIndexYaml(yaml);
2273
- } catch (e) {
2274
- const code = e && typeof e === "object" && "code" in e ? e.code : void 0;
2275
- if (code === "ENOENT") return [];
2276
- throw e;
2539
+ };
2540
+ yield { type: "done" };
2541
+ return;
2277
2542
  }
2278
- }
2279
- async function parsePluginYamlContracts(projectRoot, pluginName) {
2280
- const pluginYamlPath = path.join(
2281
- projectRoot,
2282
- "node_modules",
2283
- "@jay-framework",
2284
- pluginName,
2285
- "plugin.yaml"
2543
+ await updatePageRequestStatus(projectDir, input.requestId, "completed");
2544
+ await devServer.refreshRoutes();
2545
+ const jayHtmlPath = await resolveJayHtmlPathForPageRoute(
2546
+ projectDir,
2547
+ normalizedRoute,
2548
+ devServer.listRoutes()
2286
2549
  );
2287
- try {
2288
- const yaml = await fs.readFile(pluginYamlPath, "utf-8");
2289
- const entries = [];
2290
- let inContracts = false;
2291
- for (const line of yaml.split("\n")) {
2292
- if (line.trim() === "contracts:") {
2293
- inContracts = true;
2294
- continue;
2550
+ if (jayHtmlPath) {
2551
+ try {
2552
+ await savePageBriefFromRequest(projectDir, jayHtmlPath, {
2553
+ requestId: input.requestId,
2554
+ pageRoute: normalizedRoute,
2555
+ contentMd: input.contentMd,
2556
+ designMd: input.designMd
2557
+ });
2558
+ } catch (err) {
2559
+ getLogger().warn(
2560
+ `[Add Page] Failed to save page brief for ${normalizedRoute}: ${err instanceof Error ? err.message : String(err)}`
2561
+ );
2562
+ }
2563
+ }
2564
+ yield {
2565
+ type: "result",
2566
+ message: `Page created at ${normalizedRoute}.`,
2567
+ text: normalizedRoute
2568
+ };
2569
+ yield { type: "done" };
2570
+ });
2571
+ const generateAddPageBriefFromImageAction = makeJayQuery(
2572
+ "aiditor.generateAddPageBriefFromImage"
2573
+ ).withMethod("POST").withFiles().withHandler(
2574
+ async (input) => {
2575
+ const imageFiles = extractImageFilesFromExtraFiles(input.extraFiles);
2576
+ const validationError = validateBriefFillImages(imageFiles);
2577
+ if (validationError) {
2578
+ return { ok: false, error: validationError };
2579
+ }
2580
+ const projectDir = process.cwd();
2581
+ if (input.requestId) {
2582
+ const request = await readPageRequest(projectDir, input.requestId);
2583
+ if (!request) {
2584
+ return {
2585
+ ok: false,
2586
+ error: `Page request not found: ${input.requestId}`
2587
+ };
2295
2588
  }
2296
- if (inContracts) {
2297
- const nameMatch = line.match(/^ - name:\s*(.+)$/);
2298
- if (nameMatch) {
2299
- entries.push({
2300
- pluginName,
2301
- contractName: nameMatch[1].trim()
2302
- });
2303
- }
2304
- if (line.match(/^actions:/) || line.match(/^routes:/)) {
2305
- break;
2589
+ }
2590
+ try {
2591
+ const images = imageFiles.map(readImageAsBase64);
2592
+ const result = await generateBriefFromImages(
2593
+ {
2594
+ target: input.target,
2595
+ images,
2596
+ contextNotes: input.contextNotes,
2597
+ pageRoute: input.pageRoute
2598
+ },
2599
+ { projectDir }
2600
+ );
2601
+ const referenceAttachments = [];
2602
+ if (input.requestId) {
2603
+ for (const file of imageFiles) {
2604
+ const id = generatePageRequestId();
2605
+ const filename = sanitizeAttachmentFilename(file.name);
2606
+ const relPath = `references/${filename}`;
2607
+ const destPath = path.join(
2608
+ getPageRequestDir(projectDir, input.requestId),
2609
+ relPath
2610
+ );
2611
+ persistUpload(file, destPath);
2612
+ await addPageRequestAttachment(
2613
+ projectDir,
2614
+ input.requestId,
2615
+ { id, path: relPath, label: filename },
2616
+ "reference"
2617
+ );
2618
+ referenceAttachments.push({ id, path: relPath, filename });
2306
2619
  }
2307
2620
  }
2621
+ return {
2622
+ ok: true,
2623
+ markdown: result.markdown,
2624
+ suggestedRoute: result.suggestedRoute,
2625
+ suggestedRouteKind: result.suggestedRouteKind,
2626
+ referenceAttachments
2627
+ };
2628
+ } catch (err) {
2629
+ return {
2630
+ ok: false,
2631
+ error: err instanceof Error ? err.message : String(err)
2632
+ };
2308
2633
  }
2309
- return entries;
2310
- } catch {
2311
- return [];
2312
2634
  }
2313
- }
2635
+ );
2314
2636
  const LOCAL_SPEC_PREFIXES = ["portal:", "file:", "workspace:"];
2315
2637
  function isRegistryInstallSpec(installSpec) {
2316
2638
  if (!installSpec) return true;
@@ -2721,9 +3043,35 @@ async function checkPluginUpdate(projectRoot, pluginName, packageName, installSp
2721
3043
  message: updateAvailable ? `Update available: ${installedVersion} → ${latestVersion}` : installedVersion ? `Up to date (${installedVersion})` : void 0
2722
3044
  };
2723
3045
  }
3046
+ const writeQueues = /* @__PURE__ */ new Map();
2724
3047
  function getSetupCachePath(projectRoot) {
2725
3048
  return path.join(projectRoot, ".aiditor", "plugin-setup-status.json");
2726
3049
  }
3050
+ function getSetupCacheTempPath(projectRoot) {
3051
+ return `${getSetupCachePath(projectRoot)}.tmp`;
3052
+ }
3053
+ function enqueueCacheWrite(projectRoot, operation) {
3054
+ const previous = writeQueues.get(projectRoot) ?? Promise.resolve();
3055
+ const next = previous.catch(() => void 0).then(operation);
3056
+ writeQueues.set(
3057
+ projectRoot,
3058
+ next.then(
3059
+ () => void 0,
3060
+ () => void 0
3061
+ )
3062
+ );
3063
+ return next;
3064
+ }
3065
+ async function writeSetupCacheFile(projectRoot, cache2) {
3066
+ const dir = path.join(projectRoot, ".aiditor");
3067
+ await fs.mkdir(dir, { recursive: true });
3068
+ const filePath = getSetupCachePath(projectRoot);
3069
+ const tempPath = getSetupCacheTempPath(projectRoot);
3070
+ const body = `${JSON.stringify(cache2, null, 2)}
3071
+ `;
3072
+ await fs.writeFile(tempPath, body, "utf-8");
3073
+ await fs.rename(tempPath, filePath);
3074
+ }
2727
3075
  async function readSetupCache(projectRoot) {
2728
3076
  const filePath = getSetupCachePath(projectRoot);
2729
3077
  try {
@@ -2732,20 +3080,20 @@ async function readSetupCache(projectRoot) {
2732
3080
  } catch (e) {
2733
3081
  const code = e && typeof e === "object" && "code" in e ? e.code : void 0;
2734
3082
  if (code === "ENOENT") return {};
3083
+ if (e instanceof SyntaxError) {
3084
+ const backupPath = `${filePath}.corrupt-${Date.now()}`;
3085
+ await fs.rename(filePath, backupPath).catch(() => void 0);
3086
+ return {};
3087
+ }
2735
3088
  throw e;
2736
3089
  }
2737
3090
  }
2738
3091
  async function writeSetupCacheEntry(projectRoot, pluginName, entry) {
2739
- const cache2 = await readSetupCache(projectRoot);
2740
- cache2[pluginName] = entry;
2741
- const dir = path.join(projectRoot, ".aiditor");
2742
- await fs.mkdir(dir, { recursive: true });
2743
- await fs.writeFile(
2744
- getSetupCachePath(projectRoot),
2745
- `${JSON.stringify(cache2, null, 2)}
2746
- `,
2747
- "utf-8"
2748
- );
3092
+ return enqueueCacheWrite(projectRoot, async () => {
3093
+ const cache2 = await readSetupCache(projectRoot);
3094
+ cache2[pluginName] = entry;
3095
+ await writeSetupCacheFile(projectRoot, cache2);
3096
+ });
2749
3097
  }
2750
3098
  function getCachedSetupStatus(cache2, pluginName) {
2751
3099
  return cache2[pluginName];
@@ -2906,57 +3254,133 @@ async function* ensureProjectPlugin(projectRoot, pluginName, options) {
2906
3254
  message: parsed.message
2907
3255
  };
2908
3256
  }
2909
- const PLUGIN_MANIFEST_START = "<!-- aiditor:plugins:start -->";
2910
- const PLUGIN_MANIFEST_END = "<!-- aiditor:plugins:end -->";
2911
- function formatManifestLine(packageName, contractName, componentKey) {
2912
- const key = componentKey ?? getDefaultComponentKey(contractName);
2913
- return `- ${packageName} / ${contractName} (key: ${key})`;
2914
- }
2915
- function buildPluginManifestSection(selectedPlugins) {
2916
- const lines = [
2917
- PLUGIN_MANIFEST_START,
2918
- "",
2919
- "## Plugins & contracts (selected)",
2920
- ""
2921
- ];
2922
- for (const plugin of selectedPlugins) {
2923
- for (const contract of plugin.contracts) {
2924
- lines.push(
2925
- formatManifestLine(
2926
- plugin.packageName,
2927
- contract.contractName,
2928
- contract.componentKey
2929
- )
2930
- );
3257
+ function parseContractParamsFromYaml(contractYaml, contractName, pluginName) {
3258
+ const lines = contractYaml.split("\n");
3259
+ let inParams = false;
3260
+ const allParams = [];
3261
+ const requiredParams = [];
3262
+ for (const line of lines) {
3263
+ if (!inParams) {
3264
+ if (line.match(/^params:\s*$/)) {
3265
+ inParams = true;
3266
+ }
3267
+ continue;
3268
+ }
3269
+ if (line.match(/^[a-zA-Z]/) && !line.startsWith(" ")) {
3270
+ break;
3271
+ }
3272
+ const paramMatch = line.match(/^\s{2}([a-zA-Z0-9_]+):\s*(.+)$/);
3273
+ if (paramMatch) {
3274
+ const name = paramMatch[1];
3275
+ const typePart = paramMatch[2].trim();
3276
+ allParams.push(name);
3277
+ const isOptional = typePart.endsWith("?") || line.match(/^\s{2}[a-zA-Z0-9_]+\?:/) !== null;
3278
+ if (!isOptional) {
3279
+ requiredParams.push(name);
3280
+ }
2931
3281
  }
2932
3282
  }
2933
- lines.push(PLUGIN_MANIFEST_END);
2934
- return lines.join("\n");
3283
+ if (allParams.length === 0) return null;
3284
+ return { contractName, pluginName, requiredParams, allParams };
2935
3285
  }
2936
- function syncPluginManifestInContentMd(contentMd, selectedPlugins) {
2937
- const section = buildPluginManifestSection(selectedPlugins);
2938
- const startIx = contentMd.indexOf(PLUGIN_MANIFEST_START);
2939
- const endIx = contentMd.indexOf(PLUGIN_MANIFEST_END);
2940
- if (startIx >= 0 && endIx > startIx) {
2941
- const before = contentMd.slice(0, startIx).trimEnd();
2942
- const after = contentMd.slice(endIx + PLUGIN_MANIFEST_END.length).trimStart();
2943
- const parts = [before, section, after].filter((p) => p.length > 0);
2944
- return parts.length > 0 ? `${parts.join("\n\n")}
2945
- ` : `${section}
2946
- `;
3286
+ function contractNameFromItemId(itemId) {
3287
+ const colon = itemId.indexOf(":");
3288
+ if (colon < 0 || colon === itemId.length - 1) return null;
3289
+ return itemId.slice(colon + 1);
3290
+ }
3291
+ function pluginNameFromItemId(itemId) {
3292
+ const colon = itemId.indexOf(":");
3293
+ if (colon <= 0) return null;
3294
+ return itemId.slice(0, colon);
3295
+ }
3296
+ async function resolveContractParamsForItem(projectRoot, item) {
3297
+ const pluginName = item.pluginName ?? pluginNameFromItemId(item.id);
3298
+ const contractName = contractNameFromItemId(item.id);
3299
+ if (!pluginName || !contractName) return null;
3300
+ const contractPath = path.join(
3301
+ projectRoot,
3302
+ "agent-kit",
3303
+ "materialized-contracts",
3304
+ pluginName,
3305
+ `${contractName}.jay-contract`
3306
+ );
3307
+ let yaml;
3308
+ try {
3309
+ yaml = await fs.readFile(contractPath, "utf-8");
3310
+ } catch {
3311
+ return null;
2947
3312
  }
2948
- const hasContracts = selectedPlugins.some((p) => p.contracts.length > 0);
2949
- if (!hasContracts) return contentMd;
2950
- const trimmed = contentMd.trimEnd();
2951
- if (trimmed.length === 0) {
2952
- return `${section}
2953
- `;
3313
+ return parseContractParamsFromYaml(yaml, contractName, pluginName);
3314
+ }
3315
+ const listAddMenuItemsAction = makeJayQuery("aiditor.listAddMenuItems").withServices(DEV_SERVER_SERVICE).withHandler(async () => {
3316
+ const projectRoot = process.cwd();
3317
+ return listAddMenuItems(projectRoot);
3318
+ });
3319
+ const resolveAddMenuContractParamsAction = makeJayQuery(
3320
+ "aiditor.resolveAddMenuContractParams"
3321
+ ).withServices(DEV_SERVER_SERVICE).withHandler(async (input) => {
3322
+ const projectRoot = process.cwd();
3323
+ const resolved = await resolveContractParamsForItem(projectRoot, {
3324
+ id: input.itemId,
3325
+ pluginName: input.pluginName
3326
+ });
3327
+ if (!resolved) {
3328
+ return {
3329
+ contractName: null,
3330
+ pluginName: null,
3331
+ requiredParams: null,
3332
+ allParams: null
3333
+ };
2954
3334
  }
2955
- return `${trimmed}
2956
-
2957
- ${section}
2958
- `;
3335
+ return {
3336
+ contractName: resolved.contractName,
3337
+ pluginName: resolved.pluginName,
3338
+ requiredParams: resolved.requiredParams,
3339
+ allParams: resolved.allParams
3340
+ };
3341
+ });
3342
+ async function syncAddMenuAttachmentsCore(projectRoot, input) {
3343
+ const request = await readPageRequest(projectRoot, input.requestId);
3344
+ if (!request) {
3345
+ return { ok: false, contentMd: "", error: "Page request not found." };
3346
+ }
3347
+ const requestDir = path.join(
3348
+ projectRoot,
3349
+ ".aiditor",
3350
+ "page-requests",
3351
+ input.requestId
3352
+ );
3353
+ const contentPath = path.join(requestDir, "content.md");
3354
+ let contentMd = "";
3355
+ if (input.contentMd !== void 0) {
3356
+ contentMd = input.contentMd;
3357
+ await writePageRequestMarkdown(projectRoot, input.requestId, {
3358
+ contentMd,
3359
+ designMd: await fs.readFile(path.join(requestDir, "design.md"), "utf-8").catch(() => "")
3360
+ });
3361
+ } else {
3362
+ try {
3363
+ contentMd = await fs.readFile(contentPath, "utf-8");
3364
+ } catch {
3365
+ contentMd = "";
3366
+ }
3367
+ }
3368
+ await writePageRequest(projectRoot, {
3369
+ ...request,
3370
+ addMenuAttachments: input.addMenuAttachments,
3371
+ routeMigration: input.routeMigration,
3372
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3373
+ });
3374
+ return { ok: true, contentMd };
2959
3375
  }
3376
+ const syncAddMenuAttachmentsAction = makeJayQuery(
3377
+ "aiditor.syncAddMenuAttachments"
3378
+ ).withServices(DEV_SERVER_SERVICE).withHandler(
3379
+ async (input) => {
3380
+ const projectRoot = process.cwd();
3381
+ return syncAddMenuAttachmentsCore(projectRoot, input);
3382
+ }
3383
+ );
2960
3384
  async function contractsForPlugin(projectRoot, pluginName) {
2961
3385
  const fromIndex = await parsePluginsIndexContracts(projectRoot);
2962
3386
  const filtered = fromIndex.filter((c) => c.pluginName === pluginName);
@@ -3205,37 +3629,12 @@ const syncAddPagePluginManifestAction = makeJayQuery(
3205
3629
  ).withServices(DEV_SERVER_SERVICE).withHandler(
3206
3630
  async (input) => {
3207
3631
  const projectRoot = process.cwd();
3208
- const request = await readPageRequest(projectRoot, input.requestId);
3209
- if (!request) {
3210
- return { ok: false, contentMd: "", error: "Page request not found." };
3211
- }
3212
- const requestDir = path.join(
3213
- projectRoot,
3214
- ".aiditor",
3215
- "page-requests",
3216
- input.requestId
3217
- );
3218
- const contentPath = path.join(requestDir, "content.md");
3219
- let contentMd = "";
3220
- try {
3221
- contentMd = await fs.readFile(contentPath, "utf-8");
3222
- } catch {
3223
- contentMd = "";
3224
- }
3225
- const nextMd = syncPluginManifestInContentMd(
3226
- contentMd,
3227
- input.selectedPlugins
3228
- );
3229
- await writePageRequestMarkdown(projectRoot, input.requestId, {
3230
- contentMd: nextMd,
3231
- designMd: await fs.readFile(path.join(requestDir, "design.md"), "utf-8").catch(() => "")
3232
- });
3233
- await writePageRequest(projectRoot, {
3234
- ...request,
3235
- selectedPlugins: input.selectedPlugins,
3236
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3632
+ return syncAddMenuAttachmentsCore(projectRoot, {
3633
+ requestId: input.requestId,
3634
+ addMenuAttachments: pagePluginSelectionsToAttachments(
3635
+ input.selectedPlugins
3636
+ )
3237
3637
  });
3238
- return { ok: true, contentMd: nextMd };
3239
3638
  }
3240
3639
  );
3241
3640
  const writePluginSourceAction = makeJayQuery("aiditor.writePluginSource").withServices(DEV_SERVER_SERVICE).withHandler(async (input) => {
@@ -3243,8 +3642,81 @@ const writePluginSourceAction = makeJayQuery("aiditor.writePluginSource").withSe
3243
3642
  await writePluginSource(projectRoot, input);
3244
3643
  return { ok: true };
3245
3644
  });
3246
- setActionCallerOptions({ timeout: 12e4 });
3247
- const page = makeJayStackComponent().withProps();
3645
+ const AIDITOR_ADD_MENU_DOC = "aiditor-add-menu.md";
3646
+ const INSTRUCTIONS_LINK_LINE = "- [Add Menu integration](./aiditor-add-menu.md) — contribute catalog items for AIditor";
3647
+ async function resolveTemplateDir(fromModuleUrl) {
3648
+ const thisDir = path.dirname(fileURLToPath(fromModuleUrl));
3649
+ const candidates = [
3650
+ path.join(thisDir, "agent-kit-template", "plugin"),
3651
+ path.join(thisDir, "..", "agent-kit-template", "plugin"),
3652
+ path.join(thisDir, "..", "..", "agent-kit-template", "plugin")
3653
+ ];
3654
+ for (const dir of candidates) {
3655
+ const probe = path.join(dir, AIDITOR_ADD_MENU_DOC);
3656
+ try {
3657
+ await fs.access(probe);
3658
+ return dir;
3659
+ } catch {
3660
+ }
3661
+ }
3662
+ throw new Error(
3663
+ `aiditor add-menu template not found (searched from ${thisDir})`
3664
+ );
3665
+ }
3666
+ async function appendInstructionsLink(instructionsPath, force) {
3667
+ let content;
3668
+ try {
3669
+ content = await fs.readFile(instructionsPath, "utf-8");
3670
+ } catch {
3671
+ return false;
3672
+ }
3673
+ if (content.includes(AIDITOR_ADD_MENU_DOC) && !force) {
3674
+ return false;
3675
+ }
3676
+ if (content.includes(AIDITOR_ADD_MENU_DOC) && force) {
3677
+ return false;
3678
+ }
3679
+ const next = content.trimEnd() + `
3680
+
3681
+ ## Add Menu
3682
+
3683
+ ${INSTRUCTIONS_LINK_LINE}
3684
+ `;
3685
+ await fs.writeFile(instructionsPath, next, "utf-8");
3686
+ return true;
3687
+ }
3688
+ async function setupAiditor(ctx) {
3689
+ const templateDir = await resolveTemplateDir(import.meta.url);
3690
+ const srcDoc = path.join(templateDir, AIDITOR_ADD_MENU_DOC);
3691
+ const agentKitPluginDir = path.join(ctx.projectRoot, "agent-kit", "plugin");
3692
+ const destDoc = path.join(agentKitPluginDir, AIDITOR_ADD_MENU_DOC);
3693
+ await fs.mkdir(agentKitPluginDir, { recursive: true });
3694
+ const filesCreated = [];
3695
+ const filesUpdated = [];
3696
+ let exists = false;
3697
+ try {
3698
+ await fs.access(destDoc);
3699
+ exists = true;
3700
+ } catch {
3701
+ exists = false;
3702
+ }
3703
+ if (!exists || ctx.force) {
3704
+ await fs.copyFile(srcDoc, destDoc);
3705
+ if (exists) filesUpdated.push(`agent-kit/plugin/${AIDITOR_ADD_MENU_DOC}`);
3706
+ else filesCreated.push(`agent-kit/plugin/${AIDITOR_ADD_MENU_DOC}`);
3707
+ }
3708
+ const instructionsPath = path.join(agentKitPluginDir, "INSTRUCTIONS.md");
3709
+ const linked = await appendInstructionsLink(instructionsPath, ctx.force);
3710
+ if (linked) {
3711
+ filesUpdated.push("agent-kit/plugin/INSTRUCTIONS.md");
3712
+ }
3713
+ return {
3714
+ status: "configured",
3715
+ message: "AIditor Add Menu contributor guide installed.",
3716
+ ...filesCreated.length > 0 ? { filesCreated } : {},
3717
+ ...filesUpdated.length > 0 ? { filesUpdated } : {}
3718
+ };
3719
+ }
3248
3720
  const aiditorShell = makeJayStackComponent().withProps().withSlowlyRender(async () => {
3249
3721
  return phaseOutput({}, { headline: "AIditor" });
3250
3722
  }).withFastRender(async (_props, carryForward) => {
@@ -3256,6 +3728,8 @@ const aiditorShell = makeJayStackComponent().withProps().withSlowlyRender(async
3256
3728
  carryForward
3257
3729
  }));
3258
3730
  });
3731
+ setActionCallerOptions({ timeout: 12e4 });
3732
+ const page = makeJayStackComponent().withProps();
3259
3733
  export {
3260
3734
  page as aiditorPage,
3261
3735
  aiditorShell,
@@ -3268,6 +3742,7 @@ export {
3268
3742
  getPageParamsAction,
3269
3743
  getPluginSetupStatusAction,
3270
3744
  getProjectInfoAction,
3745
+ listAddMenuItemsAction,
3271
3746
  listFreezesAction,
3272
3747
  listJayPluginsAction,
3273
3748
  listPluginContractsAction,
@@ -3275,10 +3750,13 @@ export {
3275
3750
  readFileAction,
3276
3751
  reclassifyAddPageAssetAction,
3277
3752
  rerunPluginSetupAction,
3753
+ resolveAddMenuContractParamsAction,
3278
3754
  saveAddPageDraftAction,
3755
+ setupAiditor,
3279
3756
  startAddPageRequestAction,
3280
3757
  submitAddPageAction,
3281
3758
  submitTaskAction,
3759
+ syncAddMenuAttachmentsAction,
3282
3760
  syncAddPagePluginManifestAction,
3283
3761
  uploadAddPageAssetAction,
3284
3762
  writePluginSourceAction