@agent-native/core 0.54.1 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +16 -1
  2. package/dist/agent/production-agent.d.ts.map +1 -1
  3. package/dist/agent/production-agent.js +4 -0
  4. package/dist/agent/production-agent.js.map +1 -1
  5. package/dist/cli/connect.d.ts +3 -1
  6. package/dist/cli/connect.d.ts.map +1 -1
  7. package/dist/cli/connect.js +7 -1
  8. package/dist/cli/connect.js.map +1 -1
  9. package/dist/cli/index.js +1 -1
  10. package/dist/cli/index.js.map +1 -1
  11. package/dist/cli/plan-local.d.ts +40 -0
  12. package/dist/cli/plan-local.d.ts.map +1 -1
  13. package/dist/cli/plan-local.js +495 -9
  14. package/dist/cli/plan-local.js.map +1 -1
  15. package/dist/cli/skills.d.ts +17 -2
  16. package/dist/cli/skills.d.ts.map +1 -1
  17. package/dist/cli/skills.js +135 -36
  18. package/dist/cli/skills.js.map +1 -1
  19. package/dist/client/AgentChatHome.d.ts +23 -0
  20. package/dist/client/AgentChatHome.d.ts.map +1 -0
  21. package/dist/client/AgentChatHome.js +13 -0
  22. package/dist/client/AgentChatHome.js.map +1 -0
  23. package/dist/client/AgentPanel.d.ts +29 -2
  24. package/dist/client/AgentPanel.d.ts.map +1 -1
  25. package/dist/client/AgentPanel.js +116 -68
  26. package/dist/client/AgentPanel.js.map +1 -1
  27. package/dist/client/AssistantChat.d.ts.map +1 -1
  28. package/dist/client/AssistantChat.js +140 -18
  29. package/dist/client/AssistantChat.js.map +1 -1
  30. package/dist/client/FeedbackButton.d.ts +7 -1
  31. package/dist/client/FeedbackButton.d.ts.map +1 -1
  32. package/dist/client/FeedbackButton.js +13 -3
  33. package/dist/client/FeedbackButton.js.map +1 -1
  34. package/dist/client/MultiTabAssistantChat.d.ts +1 -2
  35. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  36. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  37. package/dist/client/agent-sidebar-state.d.ts +2 -0
  38. package/dist/client/agent-sidebar-state.d.ts.map +1 -1
  39. package/dist/client/agent-sidebar-state.js +15 -4
  40. package/dist/client/agent-sidebar-state.js.map +1 -1
  41. package/dist/client/chat/index.d.ts +5 -0
  42. package/dist/client/chat/index.d.ts.map +1 -1
  43. package/dist/client/chat/index.js +5 -0
  44. package/dist/client/chat/index.js.map +1 -1
  45. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  46. package/dist/client/chat/run-recovery.js +4 -6
  47. package/dist/client/chat/run-recovery.js.map +1 -1
  48. package/dist/client/chat/tool-call-display.d.ts +1 -0
  49. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  50. package/dist/client/chat/tool-call-display.js +16 -0
  51. package/dist/client/chat/tool-call-display.js.map +1 -1
  52. package/dist/client/chat/tool-render-registry.d.ts +24 -0
  53. package/dist/client/chat/tool-render-registry.d.ts.map +1 -0
  54. package/dist/client/chat/tool-render-registry.js +37 -0
  55. package/dist/client/chat/tool-render-registry.js.map +1 -0
  56. package/dist/client/chat/widgets/DataChartRenderer.d.ts +5 -0
  57. package/dist/client/chat/widgets/DataChartRenderer.d.ts.map +1 -0
  58. package/dist/client/chat/widgets/DataChartRenderer.js +33 -0
  59. package/dist/client/chat/widgets/DataChartRenderer.js.map +1 -0
  60. package/dist/client/chat/widgets/DataChartWidget.d.ts +5 -0
  61. package/dist/client/chat/widgets/DataChartWidget.d.ts.map +1 -0
  62. package/dist/client/chat/widgets/DataChartWidget.js +15 -0
  63. package/dist/client/chat/widgets/DataChartWidget.js.map +1 -0
  64. package/dist/client/chat/widgets/DataInsightsWidget.d.ts +5 -0
  65. package/dist/client/chat/widgets/DataInsightsWidget.d.ts.map +1 -0
  66. package/dist/client/chat/widgets/DataInsightsWidget.js +18 -0
  67. package/dist/client/chat/widgets/DataInsightsWidget.js.map +1 -0
  68. package/dist/client/chat/widgets/DataTableWidget.d.ts +9 -0
  69. package/dist/client/chat/widgets/DataTableWidget.d.ts.map +1 -0
  70. package/dist/client/chat/widgets/DataTableWidget.js +95 -0
  71. package/dist/client/chat/widgets/DataTableWidget.js.map +1 -0
  72. package/dist/client/chat/widgets/builtin-tool-renderers.d.ts +2 -0
  73. package/dist/client/chat/widgets/builtin-tool-renderers.d.ts.map +1 -0
  74. package/dist/client/chat/widgets/builtin-tool-renderers.js +27 -0
  75. package/dist/client/chat/widgets/builtin-tool-renderers.js.map +1 -0
  76. package/dist/client/chat/widgets/data-widget-types.d.ts +52 -0
  77. package/dist/client/chat/widgets/data-widget-types.d.ts.map +1 -0
  78. package/dist/client/chat/widgets/data-widget-types.js +93 -0
  79. package/dist/client/chat/widgets/data-widget-types.js.map +1 -0
  80. package/dist/client/chat-view-transition.d.ts +23 -0
  81. package/dist/client/chat-view-transition.d.ts.map +1 -0
  82. package/dist/client/chat-view-transition.js +50 -0
  83. package/dist/client/chat-view-transition.js.map +1 -0
  84. package/dist/client/composer/PromptComposer.d.ts +1 -1
  85. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  86. package/dist/client/composer/PromptComposer.js +2 -2
  87. package/dist/client/composer/PromptComposer.js.map +1 -1
  88. package/dist/client/composer/TiptapComposer.d.ts +2 -1
  89. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  90. package/dist/client/composer/TiptapComposer.js +2 -1
  91. package/dist/client/composer/TiptapComposer.js.map +1 -1
  92. package/dist/client/index.d.ts +5 -1
  93. package/dist/client/index.d.ts.map +1 -1
  94. package/dist/client/index.js +5 -1
  95. package/dist/client/index.js.map +1 -1
  96. package/dist/client/route-state.d.ts +6 -0
  97. package/dist/client/route-state.d.ts.map +1 -1
  98. package/dist/client/route-state.js +29 -1
  99. package/dist/client/route-state.js.map +1 -1
  100. package/dist/client/use-chat-threads.d.ts.map +1 -1
  101. package/dist/client/use-chat-threads.js +19 -4
  102. package/dist/client/use-chat-threads.js.map +1 -1
  103. package/dist/scripts/dev/index.d.ts +1 -0
  104. package/dist/scripts/dev/index.d.ts.map +1 -1
  105. package/dist/scripts/dev/index.js +129 -127
  106. package/dist/scripts/dev/index.js.map +1 -1
  107. package/dist/server/agent-chat-plugin.d.ts +8 -0
  108. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  109. package/dist/server/agent-chat-plugin.js +29 -21
  110. package/dist/server/agent-chat-plugin.js.map +1 -1
  111. package/dist/server/prompts/framework-core-compact.d.ts +4 -1
  112. package/dist/server/prompts/framework-core-compact.d.ts.map +1 -1
  113. package/dist/server/prompts/framework-core-compact.js +11 -4
  114. package/dist/server/prompts/framework-core-compact.js.map +1 -1
  115. package/dist/server/prompts/framework-core.d.ts +4 -1
  116. package/dist/server/prompts/framework-core.d.ts.map +1 -1
  117. package/dist/server/prompts/framework-core.js +15 -5
  118. package/dist/server/prompts/framework-core.js.map +1 -1
  119. package/dist/server/prompts/shared-rules.d.ts +4 -1
  120. package/dist/server/prompts/shared-rules.d.ts.map +1 -1
  121. package/dist/server/prompts/shared-rules.js +4 -1
  122. package/dist/server/prompts/shared-rules.js.map +1 -1
  123. package/dist/styles/agent-native.css +55 -0
  124. package/dist/vite/client.d.ts.map +1 -1
  125. package/dist/vite/client.js +4 -0
  126. package/dist/vite/client.js.map +1 -1
  127. package/docs/content/external-agents.md +14 -4
  128. package/docs/content/getting-started.md +1 -0
  129. package/docs/content/key-concepts.md +34 -15
  130. package/docs/content/mcp-apps.md +2 -0
  131. package/docs/content/mcp-protocol.md +2 -2
  132. package/docs/content/template-plan.md +16 -1
  133. package/docs/content/what-is-agent-native.md +10 -2
  134. package/package.json +1 -1
@@ -205,10 +205,12 @@ function repoRelativePlanPath(dir) {
205
205
  function localPlanBridgePageUrl(input) {
206
206
  return `${normalizeBridgeAppUrl(input.appUrl)}/local-plans/${encodeURIComponent(path.basename(path.resolve(input.dir)))}?bridge=${encodeURIComponent(input.bridgeUrl)}`;
207
207
  }
208
- function openLocalUrl(url) {
209
- const platform = process.platform;
210
- const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
211
- const args = platform === "win32" ? ["/c", "start", "", url] : [url];
208
+ function writeLocalPlanUrlFile(dir, url, urlFile) {
209
+ const file = path.resolve(urlFile || path.join(dir, ".plan-url"));
210
+ fs.writeFileSync(file, `${url}\n`, { encoding: "utf-8", mode: 0o600 });
211
+ return file;
212
+ }
213
+ function runOpenCommand(command, args) {
212
214
  const result = spawnSync(command, args, {
213
215
  stdio: "ignore",
214
216
  windowsHide: true,
@@ -230,6 +232,26 @@ function openLocalUrl(url) {
230
232
  }
231
233
  return { ok: true, command: commandDisplay };
232
234
  }
235
+ function openLocalUrl(url) {
236
+ const platform = process.platform;
237
+ if (platform === "darwin") {
238
+ for (const appName of [
239
+ "Google Chrome",
240
+ "Chromium",
241
+ "Microsoft Edge",
242
+ "Brave Browser",
243
+ ]) {
244
+ const result = runOpenCommand("open", ["-a", appName, url]);
245
+ if (result.ok)
246
+ return result;
247
+ }
248
+ return runOpenCommand("open", [url]);
249
+ }
250
+ if (platform === "win32") {
251
+ return runOpenCommand("cmd", ["/c", "start", "", url]);
252
+ }
253
+ return runOpenCommand("xdg-open", [url]);
254
+ }
233
255
  function escapeHtml(value) {
234
256
  return value
235
257
  .replace(/&/g, "&")
@@ -629,6 +651,333 @@ function lintColumnsBlocks(file, source, issues) {
629
651
  }
630
652
  }
631
653
  }
654
+ function findBalancedEnd(source, start, open, close) {
655
+ let depth = 0;
656
+ let quote = null;
657
+ for (let i = start; i < source.length; i += 1) {
658
+ const char = source[i];
659
+ if (quote) {
660
+ if (char === "\\" && i + 1 < source.length) {
661
+ i += 1;
662
+ continue;
663
+ }
664
+ if (char === quote)
665
+ quote = null;
666
+ continue;
667
+ }
668
+ if (char === '"' || char === "'" || char === "`") {
669
+ quote = char;
670
+ continue;
671
+ }
672
+ if (char === open) {
673
+ depth += 1;
674
+ continue;
675
+ }
676
+ if (char === close) {
677
+ depth -= 1;
678
+ if (depth === 0)
679
+ return i;
680
+ }
681
+ }
682
+ return -1;
683
+ }
684
+ function readJsxAttribute(opening, name) {
685
+ let i = 0;
686
+ while (i < opening.length) {
687
+ if (!/[A-Za-z_:]/.test(opening[i] ?? "")) {
688
+ i += 1;
689
+ continue;
690
+ }
691
+ const nameStart = i;
692
+ i += 1;
693
+ while (/[\w:.-]/.test(opening[i] ?? ""))
694
+ i += 1;
695
+ const attrName = opening.slice(nameStart, i);
696
+ while (/\s/.test(opening[i] ?? ""))
697
+ i += 1;
698
+ if (opening[i] !== "=") {
699
+ if (attrName === name) {
700
+ return { name, kind: "boolean", value: "true", start: nameStart };
701
+ }
702
+ continue;
703
+ }
704
+ i += 1;
705
+ while (/\s/.test(opening[i] ?? ""))
706
+ i += 1;
707
+ const valueStart = i;
708
+ const quote = opening[i];
709
+ if (quote === '"' || quote === "'" || quote === "`") {
710
+ i += 1;
711
+ while (i < opening.length) {
712
+ if (opening[i] === "\\" && i + 1 < opening.length) {
713
+ i += 2;
714
+ continue;
715
+ }
716
+ if (opening[i] === quote)
717
+ break;
718
+ i += 1;
719
+ }
720
+ const value = opening.slice(valueStart + 1, i);
721
+ i += 1;
722
+ if (attrName === name) {
723
+ return { name, kind: "string", value, start: valueStart + 1 };
724
+ }
725
+ continue;
726
+ }
727
+ if (quote === "{") {
728
+ const end = findBalancedEnd(opening, i, "{", "}");
729
+ if (end < 0) {
730
+ if (attrName === name) {
731
+ return {
732
+ name,
733
+ kind: "expression",
734
+ value: opening.slice(valueStart + 1),
735
+ start: valueStart + 1,
736
+ };
737
+ }
738
+ break;
739
+ }
740
+ const value = opening.slice(valueStart + 1, end);
741
+ i = end + 1;
742
+ if (attrName === name) {
743
+ return { name, kind: "expression", value, start: valueStart + 1 };
744
+ }
745
+ continue;
746
+ }
747
+ while (i < opening.length && !/[\s>]/.test(opening[i] ?? ""))
748
+ i += 1;
749
+ if (attrName === name) {
750
+ return {
751
+ name,
752
+ kind: "bare",
753
+ value: opening.slice(valueStart, i),
754
+ start: valueStart,
755
+ };
756
+ }
757
+ }
758
+ return null;
759
+ }
760
+ function expressionOffset(expression) {
761
+ return expression.search(/\S/);
762
+ }
763
+ function extractTopLevelObjectLiterals(expression) {
764
+ const leading = expressionOffset(expression);
765
+ if (leading < 0 || expression[leading] !== "[")
766
+ return null;
767
+ const arrayEnd = findBalancedEnd(expression, leading, "[", "]");
768
+ if (arrayEnd < 0)
769
+ return null;
770
+ const objects = [];
771
+ let i = leading + 1;
772
+ while (i < arrayEnd) {
773
+ const char = expression[i];
774
+ if (/\s|,/.test(char ?? "")) {
775
+ i += 1;
776
+ continue;
777
+ }
778
+ if (char !== "{") {
779
+ return null;
780
+ }
781
+ const objectEnd = findBalancedEnd(expression, i, "{", "}");
782
+ if (objectEnd < 0 || objectEnd > arrayEnd)
783
+ return null;
784
+ objects.push({
785
+ source: expression.slice(i, objectEnd + 1),
786
+ start: i,
787
+ });
788
+ i = objectEnd + 1;
789
+ }
790
+ return objects;
791
+ }
792
+ function findValueEnd(source, start) {
793
+ let i = start;
794
+ let quote = null;
795
+ let depth = 0;
796
+ while (i < source.length) {
797
+ const char = source[i];
798
+ if (quote) {
799
+ if (char === "\\" && i + 1 < source.length) {
800
+ i += 2;
801
+ continue;
802
+ }
803
+ if (char === quote)
804
+ quote = null;
805
+ i += 1;
806
+ continue;
807
+ }
808
+ if (char === '"' || char === "'" || char === "`") {
809
+ quote = char;
810
+ i += 1;
811
+ continue;
812
+ }
813
+ if (char === "{" || char === "[" || char === "(") {
814
+ depth += 1;
815
+ i += 1;
816
+ continue;
817
+ }
818
+ if (char === "}" || char === "]" || char === ")") {
819
+ if (depth === 0)
820
+ return i;
821
+ depth -= 1;
822
+ i += 1;
823
+ continue;
824
+ }
825
+ if (char === "," && depth === 0)
826
+ return i;
827
+ i += 1;
828
+ }
829
+ return source.length;
830
+ }
831
+ function readObjectKey(source, start) {
832
+ let i = start;
833
+ while (/\s|,/.test(source[i] ?? ""))
834
+ i += 1;
835
+ const keyStart = i;
836
+ const quote = source[i];
837
+ let key = "";
838
+ if (quote === '"' || quote === "'") {
839
+ i += 1;
840
+ const valueStart = i;
841
+ while (i < source.length) {
842
+ if (source[i] === "\\" && i + 1 < source.length) {
843
+ i += 2;
844
+ continue;
845
+ }
846
+ if (source[i] === quote)
847
+ break;
848
+ i += 1;
849
+ }
850
+ key = source.slice(valueStart, i);
851
+ i += 1;
852
+ }
853
+ else if (/[A-Za-z_$]/.test(quote ?? "")) {
854
+ i += 1;
855
+ while (/[\w$]/.test(source[i] ?? ""))
856
+ i += 1;
857
+ key = source.slice(keyStart, i);
858
+ }
859
+ else {
860
+ return null;
861
+ }
862
+ while (/\s/.test(source[i] ?? ""))
863
+ i += 1;
864
+ if (source[i] !== ":")
865
+ return null;
866
+ return { key, keyStart, colon: i };
867
+ }
868
+ function readTopLevelObjectProperty(objectSource, name) {
869
+ const body = objectSource.trim().startsWith("{")
870
+ ? objectSource.slice(1, objectSource.lastIndexOf("}"))
871
+ : objectSource;
872
+ let i = 0;
873
+ while (i < body.length) {
874
+ const key = readObjectKey(body, i);
875
+ if (!key) {
876
+ i += 1;
877
+ continue;
878
+ }
879
+ const valueStart = key.colon + 1;
880
+ const valueEnd = findValueEnd(body, valueStart);
881
+ if (key.key === name) {
882
+ return { value: body.slice(valueStart, valueEnd), valueStart };
883
+ }
884
+ i = valueEnd + 1;
885
+ }
886
+ return null;
887
+ }
888
+ function isStaticNonEmptyStringLiteral(value) {
889
+ const trimmed = value.trim();
890
+ const quote = trimmed[0];
891
+ if (quote !== '"' && quote !== "'" && quote !== "`")
892
+ return false;
893
+ if (!trimmed.endsWith(quote))
894
+ return false;
895
+ if (quote === "`" && /\$\{/.test(trimmed))
896
+ return false;
897
+ return trimmed.slice(1, -1).trim().length > 0;
898
+ }
899
+ function hasRequiredStaticId(objectSource) {
900
+ const prop = readTopLevelObjectProperty(objectSource, "id");
901
+ return prop ? isStaticNonEmptyStringLiteral(prop.value) : false;
902
+ }
903
+ function lintChecklistShape(file, source, issues) {
904
+ const scanSource = maskCodeRegions(source);
905
+ const re = /<Checklist\b/g;
906
+ let match;
907
+ while ((match = re.exec(scanSource))) {
908
+ const start = match.index;
909
+ const openingEnd = findJsxOpeningTagEnd(scanSource, start);
910
+ if (openingEnd < 0)
911
+ continue;
912
+ const opening = source.slice(start, openingEnd + 1);
913
+ const items = readJsxAttribute(opening, "items");
914
+ if (!items)
915
+ continue;
916
+ if (items.kind !== "expression") {
917
+ addValidationIssue(issues, file, source, start + items.start, "Checklist items must be an inline array expression with stable item ids.");
918
+ continue;
919
+ }
920
+ const objects = extractTopLevelObjectLiterals(items.value);
921
+ if (!objects) {
922
+ addValidationIssue(issues, file, source, start + items.start, "Checklist items must be an inline array of object literals so local check can validate the renderer schema.");
923
+ continue;
924
+ }
925
+ objects.forEach((item, index) => {
926
+ if (hasRequiredStaticId(item.source))
927
+ return;
928
+ addValidationIssue(issues, file, source, start + items.start + item.start, `Checklist items[${index}].id is required by the Plan renderer schema; add a stable string id.`);
929
+ });
930
+ }
931
+ }
932
+ function lintQuestionFormShape(file, source, issues) {
933
+ const scanSource = maskCodeRegions(source);
934
+ const re = /<(QuestionForm|VisualQuestions)\b/g;
935
+ let match;
936
+ while ((match = re.exec(scanSource))) {
937
+ const start = match.index;
938
+ const tag = match[1] ?? "QuestionForm";
939
+ const openingEnd = findJsxOpeningTagEnd(scanSource, start);
940
+ if (openingEnd < 0)
941
+ continue;
942
+ const opening = source.slice(start, openingEnd + 1);
943
+ const questions = readJsxAttribute(opening, "questions");
944
+ if (!questions) {
945
+ addValidationIssue(issues, file, source, start, `${tag} requires a questions array with at least one question object.`);
946
+ continue;
947
+ }
948
+ if (questions.kind !== "expression") {
949
+ addValidationIssue(issues, file, source, start + questions.start, `${tag} questions must be an inline array expression with stable question ids.`);
950
+ continue;
951
+ }
952
+ const questionObjects = extractTopLevelObjectLiterals(questions.value);
953
+ if (!questionObjects || questionObjects.length === 0) {
954
+ addValidationIssue(issues, file, source, start + questions.start, `${tag} questions must be a non-empty inline array of object literals.`);
955
+ continue;
956
+ }
957
+ questionObjects.forEach((question, questionIndex) => {
958
+ if (!hasRequiredStaticId(question.source)) {
959
+ addValidationIssue(issues, file, source, start + questions.start + question.start, `${tag} questions[${questionIndex}].id is required by the Plan renderer schema; add a stable string id.`);
960
+ }
961
+ const options = readTopLevelObjectProperty(question.source, "options");
962
+ if (!options)
963
+ return;
964
+ const optionObjects = extractTopLevelObjectLiterals(options.value);
965
+ if (!optionObjects) {
966
+ addValidationIssue(issues, file, source, start + questions.start + question.start + options.valueStart, `${tag} questions[${questionIndex}].options must be an inline array of object literals.`);
967
+ return;
968
+ }
969
+ optionObjects.forEach((option, optionIndex) => {
970
+ if (hasRequiredStaticId(option.source))
971
+ return;
972
+ addValidationIssue(issues, file, source, start +
973
+ questions.start +
974
+ question.start +
975
+ options.valueStart +
976
+ option.start, `${tag} questions[${questionIndex}].options[${optionIndex}].id is required by the Plan renderer schema; add a stable string id.`);
977
+ });
978
+ });
979
+ }
980
+ }
632
981
  // Blank out fenced code blocks and inline code spans (preserving newlines and
633
982
  // length) so block-tag linters don't trip on documentation examples written in
634
983
  // prose — e.g. an inline `<WireframeBlock><Screen>...</Screen></WireframeBlock>`
@@ -645,6 +994,8 @@ export function validateLocalPlanFiles(files) {
645
994
  const source = maskCodeRegions(entry.source);
646
995
  lintWireframeBlocks(entry.file, source, issues);
647
996
  lintColumnsBlocks(entry.file, source, issues);
997
+ lintChecklistShape(entry.file, entry.source, issues);
998
+ lintQuestionFormShape(entry.file, entry.source, issues);
648
999
  }
649
1000
  return issues;
650
1001
  }
@@ -921,6 +1272,9 @@ export async function startLocalPlanBridge(input) {
921
1272
  }
922
1273
  const bridgeUrl = `http://${bridgeHostForUrl(host)}:${address.port}/local-plan.json?token=${encodeURIComponent(token)}`;
923
1274
  const url = localPlanBridgePageUrl({ dir, bridgeUrl, appUrl });
1275
+ const urlFile = input.urlFile === false
1276
+ ? undefined
1277
+ : writeLocalPlanUrlFile(dir, url, input.urlFile);
924
1278
  const openResult = input.open
925
1279
  ? (input.openUrl || openLocalUrl)(url)
926
1280
  : undefined;
@@ -930,6 +1284,7 @@ export async function startLocalPlanBridge(input) {
930
1284
  ok: true,
931
1285
  dir,
932
1286
  url,
1287
+ ...(urlFile ? { urlFile } : {}),
933
1288
  bridgeUrl,
934
1289
  appUrl,
935
1290
  title: initialPayload.title,
@@ -947,6 +1302,94 @@ export async function startLocalPlanBridge(input) {
947
1302
  },
948
1303
  };
949
1304
  }
1305
+ function localPlanBridgeWarnings(input) {
1306
+ const warnings = [];
1307
+ try {
1308
+ const appUrl = new URL(input.appUrl);
1309
+ const bridgeUrl = new URL(input.bridgeUrl);
1310
+ if (appUrl.protocol === "https:" && bridgeUrl.protocol === "http:") {
1311
+ warnings.push("Safari may block the hosted HTTPS Plan UI from reading the HTTP localhost bridge. Use Chrome/Chromium/Edge, or pass --app-url http://localhost:8096 when running a local Plan app.");
1312
+ }
1313
+ }
1314
+ catch {
1315
+ // The URLs were normalized earlier; ignore defensive parse failures.
1316
+ }
1317
+ return warnings;
1318
+ }
1319
+ export async function verifyLocalPlanBridge(input) {
1320
+ const fetchFn = input.fetchFn ?? fetch;
1321
+ const bridge = await startLocalPlanBridge({
1322
+ dir: input.dir,
1323
+ kind: input.kind,
1324
+ title: input.title,
1325
+ brief: input.brief,
1326
+ appUrl: input.appUrl,
1327
+ host: input.host,
1328
+ port: input.port,
1329
+ token: input.token,
1330
+ urlFile: input.urlFile,
1331
+ });
1332
+ try {
1333
+ const preflight = await fetchFn(bridge.result.bridgeUrl, {
1334
+ method: "OPTIONS",
1335
+ headers: {
1336
+ origin: bridge.result.appUrl,
1337
+ "access-control-request-method": "GET",
1338
+ "access-control-request-private-network": "true",
1339
+ },
1340
+ });
1341
+ const response = await fetchFn(bridge.result.bridgeUrl, {
1342
+ method: "GET",
1343
+ headers: { accept: "application/json" },
1344
+ });
1345
+ const payload = (await response.json().catch(() => null));
1346
+ const mdxFiles = payload?.mdx
1347
+ ? Object.keys(payload.mdx).filter((file) => file !== "assets/")
1348
+ : undefined;
1349
+ const bridgeOk = response.ok &&
1350
+ payload?.ok === true &&
1351
+ payload.source === "agent-native-local-bridge" &&
1352
+ payload.localOnly === true &&
1353
+ Boolean(payload.mdx?.["plan.mdx"]);
1354
+ const preflightOk = preflight.status === 204 &&
1355
+ preflight.headers.get("access-control-allow-origin") === "*" &&
1356
+ preflight.headers.get("access-control-allow-private-network") === "true";
1357
+ return {
1358
+ ok: bridgeOk && preflightOk,
1359
+ dir: bridge.result.dir,
1360
+ url: bridge.result.url,
1361
+ ...(bridge.result.urlFile ? { urlFile: bridge.result.urlFile } : {}),
1362
+ bridgeUrl: bridge.result.bridgeUrl,
1363
+ appUrl: bridge.result.appUrl,
1364
+ title: bridge.result.title,
1365
+ kind: bridge.result.kind,
1366
+ files: bridge.result.files,
1367
+ preflight: {
1368
+ status: preflight.status,
1369
+ allowOrigin: preflight.headers.get("access-control-allow-origin"),
1370
+ allowPrivateNetwork: preflight.headers.get("access-control-allow-private-network"),
1371
+ },
1372
+ bridge: {
1373
+ status: response.status,
1374
+ ok: bridgeOk,
1375
+ ...(payload?.source ? { source: payload.source } : {}),
1376
+ ...(typeof payload?.localOnly === "boolean"
1377
+ ? { localOnly: payload.localOnly }
1378
+ : {}),
1379
+ ...(Array.isArray(payload?.files) ? { files: payload.files } : {}),
1380
+ ...(mdxFiles ? { mdxFiles } : {}),
1381
+ ...(payload?.error ? { error: payload.error } : {}),
1382
+ },
1383
+ warnings: localPlanBridgeWarnings({
1384
+ appUrl: bridge.result.appUrl,
1385
+ bridgeUrl: bridge.result.bridgeUrl,
1386
+ }),
1387
+ };
1388
+ }
1389
+ finally {
1390
+ await new Promise((resolve) => bridge.server.close(() => resolve()));
1391
+ }
1392
+ }
950
1393
  function writeLocalPlanSkeleton(input) {
951
1394
  const dir = path.resolve(input.dir || path.join(defaultPlansDir(), localPlanFolderName(input.title)));
952
1395
  const planPath = path.join(dir, "plan.mdx");
@@ -1059,12 +1502,25 @@ async function runServe(args) {
1059
1502
  host: optionalArg(args, "host"),
1060
1503
  port,
1061
1504
  open: boolArg(args, "open"),
1505
+ urlFile: optionalArg(args, "url-file") || optionalArg(args, "out"),
1062
1506
  kind: optionalArg(args, "kind")
1063
1507
  ? normalizeKind(optionalArg(args, "kind"))
1064
1508
  : undefined,
1065
1509
  });
1066
1510
  process.stdout.write(`${JSON.stringify(bridge.result, null, 2)}\n`);
1067
- process.stderr.write(`Local Plan bridge running at ${bridge.result.bridgeUrl}\nPress Ctrl+C to stop.\n`);
1511
+ process.stderr.write([
1512
+ `Local Plan bridge running at ${bridge.result.bridgeUrl}`,
1513
+ bridge.result.urlFile
1514
+ ? `Open URL written to ${bridge.result.urlFile}`
1515
+ : "",
1516
+ ...localPlanBridgeWarnings({
1517
+ appUrl: bridge.result.appUrl,
1518
+ bridgeUrl: bridge.result.bridgeUrl,
1519
+ }),
1520
+ "Press Ctrl+C to stop.",
1521
+ ]
1522
+ .filter(Boolean)
1523
+ .join("\n") + "\n");
1068
1524
  await new Promise((resolve) => {
1069
1525
  const stop = () => {
1070
1526
  bridge.server.close(() => resolve());
@@ -1073,6 +1529,28 @@ async function runServe(args) {
1073
1529
  process.once("SIGTERM", stop);
1074
1530
  });
1075
1531
  }
1532
+ async function runVerify(args) {
1533
+ const portValue = optionalArg(args, "port");
1534
+ const port = portValue ? Number(portValue) : undefined;
1535
+ if (portValue && (!Number.isInteger(port) || port < 0 || port > 65535)) {
1536
+ throw new Error("--port must be an integer between 0 and 65535.");
1537
+ }
1538
+ const result = await verifyLocalPlanBridge({
1539
+ dir: stringArg(args, "dir"),
1540
+ appUrl: optionalArg(args, "app-url"),
1541
+ title: optionalArg(args, "title"),
1542
+ brief: optionalArg(args, "brief"),
1543
+ host: optionalArg(args, "host"),
1544
+ port,
1545
+ urlFile: optionalArg(args, "url-file") || optionalArg(args, "out") || false,
1546
+ kind: optionalArg(args, "kind")
1547
+ ? normalizeKind(optionalArg(args, "kind"))
1548
+ : undefined,
1549
+ });
1550
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
1551
+ if (!result.ok)
1552
+ process.exit(1);
1553
+ }
1076
1554
  async function runBlocks(args) {
1077
1555
  const format = normalizePlanBlockFormat(optionalArg(args, "format"));
1078
1556
  const appUrl = optionalArg(args, "app-url") ||
@@ -1125,7 +1603,8 @@ Usage:
1125
1603
  agent-native plan blocks [--format reference|schema] [--app-url <url>] [--out <file>] [--json]
1126
1604
  agent-native plan local init --title <title> [--brief <text>] [--kind plan|recap] [--dir <folder>] [--force]
1127
1605
  agent-native plan local check --dir <folder>
1128
- agent-native plan local serve --dir <folder> [--app-url <url>] [--kind plan|recap] [--open] [--port <port>]
1606
+ agent-native plan local serve --dir <folder> [--app-url <url>] [--kind plan|recap] [--open] [--port <port>] [--url-file <file>]
1607
+ agent-native plan local verify --dir <folder> [--app-url <url>] [--kind plan|recap] [--port <port>]
1129
1608
  agent-native plan local preview --dir <folder> [--app-url <url>] [--kind plan|recap] [--open] [--out preview.html]
1130
1609
 
1131
1610
  The blocks command fetches the no-auth, read-only get-plan-blocks catalog from
@@ -1146,9 +1625,13 @@ Common flow:
1146
1625
 
1147
1626
  \`plan local serve\` starts a tiny localhost bridge and opens the hosted Plan UI
1148
1627
  against that local-only source. The hosted app fetches the MDX from localhost in
1149
- the browser; it does not write plan content to the hosted database. Use
1150
- \`plan local preview\` for a local Plan dev server route. \`preview --out\` is a
1151
- legacy/debug escape hatch that writes a standalone static HTML file.
1628
+ the browser; it does not write plan content to the hosted database. The served
1629
+ URL is written to \`.plan-url\` by default; pass \`--url-file\` to choose a
1630
+ different local-only file. On macOS, \`--open\` prefers Chromium browsers because
1631
+ Safari may block the hosted HTTPS page from reading the HTTP localhost bridge.
1632
+ Use \`plan local verify\` for headless bridge/CORS diagnostics that exit cleanly.
1633
+ Use \`plan local preview\` for a local Plan dev server route. \`preview --out\` is
1634
+ a legacy/debug escape hatch that writes a standalone static HTML file.
1152
1635
  `;
1153
1636
  export async function runPlan(argv) {
1154
1637
  const [area, sub, ...rest] = argv;
@@ -1193,6 +1676,9 @@ export async function runPlan(argv) {
1193
1676
  case "serve":
1194
1677
  await runServe(args);
1195
1678
  return;
1679
+ case "verify":
1680
+ await runVerify(args);
1681
+ return;
1196
1682
  case "help":
1197
1683
  case "--help":
1198
1684
  case "-h":