@agent-native/core 0.52.0 → 0.54.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 (267) hide show
  1. package/README.md +41 -95
  2. package/blueprints/action/crud.md +98 -0
  3. package/blueprints/channel/discord.md +74 -0
  4. package/blueprints/provider/stripe.md +87 -0
  5. package/blueprints/sandbox/docker.md +78 -0
  6. package/dist/action.d.ts +64 -1
  7. package/dist/action.d.ts.map +1 -1
  8. package/dist/action.js +73 -2
  9. package/dist/action.js.map +1 -1
  10. package/dist/agent/index.d.ts +1 -0
  11. package/dist/agent/index.d.ts.map +1 -1
  12. package/dist/agent/index.js +1 -0
  13. package/dist/agent/index.js.map +1 -1
  14. package/dist/agent/observational-memory/compactor.d.ts +43 -0
  15. package/dist/agent/observational-memory/compactor.d.ts.map +1 -0
  16. package/dist/agent/observational-memory/compactor.js +50 -0
  17. package/dist/agent/observational-memory/compactor.js.map +1 -0
  18. package/dist/agent/observational-memory/config.d.ts +37 -0
  19. package/dist/agent/observational-memory/config.d.ts.map +1 -0
  20. package/dist/agent/observational-memory/config.js +48 -0
  21. package/dist/agent/observational-memory/config.js.map +1 -0
  22. package/dist/agent/observational-memory/index.d.ts +26 -0
  23. package/dist/agent/observational-memory/index.d.ts.map +1 -0
  24. package/dist/agent/observational-memory/index.js +25 -0
  25. package/dist/agent/observational-memory/index.js.map +1 -0
  26. package/dist/agent/observational-memory/internal-run.d.ts +37 -0
  27. package/dist/agent/observational-memory/internal-run.d.ts.map +1 -0
  28. package/dist/agent/observational-memory/internal-run.js +59 -0
  29. package/dist/agent/observational-memory/internal-run.js.map +1 -0
  30. package/dist/agent/observational-memory/message-text.d.ts +13 -0
  31. package/dist/agent/observational-memory/message-text.d.ts.map +1 -0
  32. package/dist/agent/observational-memory/message-text.js +46 -0
  33. package/dist/agent/observational-memory/message-text.js.map +1 -0
  34. package/dist/agent/observational-memory/migrations.d.ts +13 -0
  35. package/dist/agent/observational-memory/migrations.d.ts.map +1 -0
  36. package/dist/agent/observational-memory/migrations.js +43 -0
  37. package/dist/agent/observational-memory/migrations.js.map +1 -0
  38. package/dist/agent/observational-memory/observer.d.ts +37 -0
  39. package/dist/agent/observational-memory/observer.d.ts.map +1 -0
  40. package/dist/agent/observational-memory/observer.js +82 -0
  41. package/dist/agent/observational-memory/observer.js.map +1 -0
  42. package/dist/agent/observational-memory/plugin.d.ts +16 -0
  43. package/dist/agent/observational-memory/plugin.d.ts.map +1 -0
  44. package/dist/agent/observational-memory/plugin.js +26 -0
  45. package/dist/agent/observational-memory/plugin.js.map +1 -0
  46. package/dist/agent/observational-memory/prompts.d.ts +27 -0
  47. package/dist/agent/observational-memory/prompts.d.ts.map +1 -0
  48. package/dist/agent/observational-memory/prompts.js +42 -0
  49. package/dist/agent/observational-memory/prompts.js.map +1 -0
  50. package/dist/agent/observational-memory/read.d.ts +45 -0
  51. package/dist/agent/observational-memory/read.d.ts.map +1 -0
  52. package/dist/agent/observational-memory/read.js +97 -0
  53. package/dist/agent/observational-memory/read.js.map +1 -0
  54. package/dist/agent/observational-memory/reflector.d.ts +31 -0
  55. package/dist/agent/observational-memory/reflector.d.ts.map +1 -0
  56. package/dist/agent/observational-memory/reflector.js +76 -0
  57. package/dist/agent/observational-memory/reflector.js.map +1 -0
  58. package/dist/agent/observational-memory/schema.d.ts +267 -0
  59. package/dist/agent/observational-memory/schema.d.ts.map +1 -0
  60. package/dist/agent/observational-memory/schema.js +48 -0
  61. package/dist/agent/observational-memory/schema.js.map +1 -0
  62. package/dist/agent/observational-memory/store.d.ts +52 -0
  63. package/dist/agent/observational-memory/store.d.ts.map +1 -0
  64. package/dist/agent/observational-memory/store.js +197 -0
  65. package/dist/agent/observational-memory/store.js.map +1 -0
  66. package/dist/agent/observational-memory/types.d.ts +61 -0
  67. package/dist/agent/observational-memory/types.d.ts.map +1 -0
  68. package/dist/agent/observational-memory/types.js +9 -0
  69. package/dist/agent/observational-memory/types.js.map +1 -0
  70. package/dist/agent/processors.d.ts +146 -0
  71. package/dist/agent/processors.d.ts.map +1 -0
  72. package/dist/agent/processors.js +122 -0
  73. package/dist/agent/processors.js.map +1 -0
  74. package/dist/agent/production-agent.d.ts +25 -0
  75. package/dist/agent/production-agent.d.ts.map +1 -1
  76. package/dist/agent/production-agent.js +341 -1
  77. package/dist/agent/production-agent.js.map +1 -1
  78. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  79. package/dist/agent/run-loop-with-resume.js +48 -0
  80. package/dist/agent/run-loop-with-resume.js.map +1 -1
  81. package/dist/agent/run-store.d.ts +17 -0
  82. package/dist/agent/run-store.d.ts.map +1 -1
  83. package/dist/agent/run-store.js +55 -0
  84. package/dist/agent/run-store.js.map +1 -1
  85. package/dist/agent/runtime-context.d.ts +30 -0
  86. package/dist/agent/runtime-context.d.ts.map +1 -1
  87. package/dist/agent/runtime-context.js +54 -1
  88. package/dist/agent/runtime-context.js.map +1 -1
  89. package/dist/agent/tool-call-journal.d.ts +99 -0
  90. package/dist/agent/tool-call-journal.d.ts.map +1 -0
  91. package/dist/agent/tool-call-journal.js +212 -0
  92. package/dist/agent/tool-call-journal.js.map +1 -0
  93. package/dist/agent/types.d.ts +35 -0
  94. package/dist/agent/types.d.ts.map +1 -1
  95. package/dist/agent/types.js.map +1 -1
  96. package/dist/cli/add.d.ts +109 -0
  97. package/dist/cli/add.d.ts.map +1 -0
  98. package/dist/cli/add.js +352 -0
  99. package/dist/cli/add.js.map +1 -0
  100. package/dist/cli/connect.d.ts +2 -2
  101. package/dist/cli/connect.d.ts.map +1 -1
  102. package/dist/cli/connect.js +92 -24
  103. package/dist/cli/connect.js.map +1 -1
  104. package/dist/cli/eval.d.ts +17 -0
  105. package/dist/cli/eval.d.ts.map +1 -0
  106. package/dist/cli/eval.js +121 -0
  107. package/dist/cli/eval.js.map +1 -0
  108. package/dist/cli/index.js +44 -3
  109. package/dist/cli/index.js.map +1 -1
  110. package/dist/cli/mcp.d.ts.map +1 -1
  111. package/dist/cli/mcp.js +11 -5
  112. package/dist/cli/mcp.js.map +1 -1
  113. package/dist/cli/plan-local.d.ts +66 -5
  114. package/dist/cli/plan-local.d.ts.map +1 -1
  115. package/dist/cli/plan-local.js +622 -21
  116. package/dist/cli/plan-local.js.map +1 -1
  117. package/dist/cli/skills.d.ts +2 -2
  118. package/dist/cli/skills.d.ts.map +1 -1
  119. package/dist/cli/skills.js +108 -62
  120. package/dist/cli/skills.js.map +1 -1
  121. package/dist/client/AssistantChat.d.ts.map +1 -1
  122. package/dist/client/AssistantChat.js +118 -92
  123. package/dist/client/AssistantChat.js.map +1 -1
  124. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  125. package/dist/client/agent-chat-adapter.js +16 -0
  126. package/dist/client/agent-chat-adapter.js.map +1 -1
  127. package/dist/client/chat/tool-call-display.d.ts +20 -1
  128. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  129. package/dist/client/chat/tool-call-display.js +32 -7
  130. package/dist/client/chat/tool-call-display.js.map +1 -1
  131. package/dist/client/sse-event-processor.d.ts +13 -0
  132. package/dist/client/sse-event-processor.d.ts.map +1 -1
  133. package/dist/client/sse-event-processor.js +21 -0
  134. package/dist/client/sse-event-processor.js.map +1 -1
  135. package/dist/coding-tools/run-code.d.ts.map +1 -1
  136. package/dist/coding-tools/run-code.js +18 -2
  137. package/dist/coding-tools/run-code.js.map +1 -1
  138. package/dist/db/client.d.ts +4 -2
  139. package/dist/db/client.d.ts.map +1 -1
  140. package/dist/db/client.js +6 -4
  141. package/dist/db/client.js.map +1 -1
  142. package/dist/deploy/route-discovery.d.ts.map +1 -1
  143. package/dist/deploy/route-discovery.js +1 -0
  144. package/dist/deploy/route-discovery.js.map +1 -1
  145. package/dist/eval/agent-runner.d.ts +63 -0
  146. package/dist/eval/agent-runner.d.ts.map +1 -0
  147. package/dist/eval/agent-runner.js +142 -0
  148. package/dist/eval/agent-runner.js.map +1 -0
  149. package/dist/eval/define-eval.d.ts +29 -0
  150. package/dist/eval/define-eval.d.ts.map +1 -0
  151. package/dist/eval/define-eval.js +43 -0
  152. package/dist/eval/define-eval.js.map +1 -0
  153. package/dist/eval/index.d.ts +18 -0
  154. package/dist/eval/index.d.ts.map +1 -0
  155. package/dist/eval/index.js +17 -0
  156. package/dist/eval/index.js.map +1 -0
  157. package/dist/eval/report.d.ts +8 -0
  158. package/dist/eval/report.d.ts.map +1 -0
  159. package/dist/eval/report.js +44 -0
  160. package/dist/eval/report.js.map +1 -0
  161. package/dist/eval/runner.d.ts +67 -0
  162. package/dist/eval/runner.d.ts.map +1 -0
  163. package/dist/eval/runner.js +256 -0
  164. package/dist/eval/runner.js.map +1 -0
  165. package/dist/eval/scorer.d.ts +83 -0
  166. package/dist/eval/scorer.d.ts.map +1 -0
  167. package/dist/eval/scorer.js +195 -0
  168. package/dist/eval/scorer.js.map +1 -0
  169. package/dist/eval/types.d.ts +162 -0
  170. package/dist/eval/types.d.ts.map +1 -0
  171. package/dist/eval/types.js +20 -0
  172. package/dist/eval/types.js.map +1 -0
  173. package/dist/extensions/fetch-tool.d.ts.map +1 -1
  174. package/dist/extensions/fetch-tool.js +80 -15
  175. package/dist/extensions/fetch-tool.js.map +1 -1
  176. package/dist/extensions/web-content.d.ts +61 -0
  177. package/dist/extensions/web-content.d.ts.map +1 -0
  178. package/dist/extensions/web-content.js +468 -0
  179. package/dist/extensions/web-content.js.map +1 -0
  180. package/dist/extensions/web-search-tool.js +3 -3
  181. package/dist/extensions/web-search-tool.js.map +1 -1
  182. package/dist/mcp/build-server.d.ts.map +1 -1
  183. package/dist/mcp/build-server.js +4 -1
  184. package/dist/mcp/build-server.js.map +1 -1
  185. package/dist/observability/traces.d.ts.map +1 -1
  186. package/dist/observability/traces.js +100 -1
  187. package/dist/observability/traces.js.map +1 -1
  188. package/dist/observability/tracing.d.ts +73 -0
  189. package/dist/observability/tracing.d.ts.map +1 -0
  190. package/dist/observability/tracing.js +126 -0
  191. package/dist/observability/tracing.js.map +1 -0
  192. package/dist/onboarding/default-steps.d.ts.map +1 -1
  193. package/dist/onboarding/default-steps.js +4 -1
  194. package/dist/onboarding/default-steps.js.map +1 -1
  195. package/dist/provider-api/actions/query-staged-dataset.d.ts +1 -1
  196. package/dist/provider-api/corpus-jobs.d.ts +80 -0
  197. package/dist/provider-api/corpus-jobs.d.ts.map +1 -1
  198. package/dist/provider-api/corpus-jobs.js +219 -22
  199. package/dist/provider-api/corpus-jobs.js.map +1 -1
  200. package/dist/provider-api/index.d.ts +24 -32
  201. package/dist/provider-api/index.d.ts.map +1 -1
  202. package/dist/provider-api/index.js +28 -1
  203. package/dist/provider-api/index.js.map +1 -1
  204. package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
  205. package/dist/scripts/agent-engines/list-agent-engines.js +10 -3
  206. package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
  207. package/dist/server/action-discovery.d.ts.map +1 -1
  208. package/dist/server/action-discovery.js +4 -0
  209. package/dist/server/action-discovery.js.map +1 -1
  210. package/dist/server/agent-chat-plugin.d.ts +9 -0
  211. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  212. package/dist/server/agent-chat-plugin.js +119 -111
  213. package/dist/server/agent-chat-plugin.js.map +1 -1
  214. package/dist/server/agent-teams.d.ts +62 -0
  215. package/dist/server/agent-teams.d.ts.map +1 -1
  216. package/dist/server/agent-teams.js +99 -2
  217. package/dist/server/agent-teams.js.map +1 -1
  218. package/dist/server/better-auth-instance.d.ts +7 -0
  219. package/dist/server/better-auth-instance.d.ts.map +1 -1
  220. package/dist/server/better-auth-instance.js +90 -0
  221. package/dist/server/better-auth-instance.js.map +1 -1
  222. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  223. package/dist/server/core-routes-plugin.js +7 -4
  224. package/dist/server/core-routes-plugin.js.map +1 -1
  225. package/dist/server/credential-provider.d.ts.map +1 -1
  226. package/dist/server/credential-provider.js +2 -0
  227. package/dist/server/credential-provider.js.map +1 -1
  228. package/dist/server/deep-link.d.ts +7 -0
  229. package/dist/server/deep-link.d.ts.map +1 -1
  230. package/dist/server/deep-link.js +13 -2
  231. package/dist/server/deep-link.js.map +1 -1
  232. package/dist/server/framework-request-handler.d.ts.map +1 -1
  233. package/dist/server/framework-request-handler.js +33 -1
  234. package/dist/server/framework-request-handler.js.map +1 -1
  235. package/dist/server/index.d.ts +2 -1
  236. package/dist/server/index.d.ts.map +1 -1
  237. package/dist/server/index.js +2 -1
  238. package/dist/server/index.js.map +1 -1
  239. package/dist/templates/default/.agents/skills/actions/SKILL.md +52 -1
  240. package/dist/templates/default/.agents/skills/security/SKILL.md +22 -0
  241. package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +52 -1
  242. package/dist/templates/workspace-core/.agents/skills/external-agents/SKILL.md +16 -4
  243. package/dist/templates/workspace-core/.agents/skills/harness-agents/SKILL.md +20 -0
  244. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +31 -0
  245. package/dist/templates/workspace-core/.agents/skills/security/SKILL.md +22 -0
  246. package/docs/content/actions.md +50 -0
  247. package/docs/content/agent-teams.md +32 -0
  248. package/docs/content/blueprint-installer.md +73 -0
  249. package/docs/content/durable-resume.md +49 -0
  250. package/docs/content/evals.md +141 -0
  251. package/docs/content/external-agents.md +2 -2
  252. package/docs/content/human-approval.md +101 -0
  253. package/docs/content/observability.md +21 -0
  254. package/docs/content/observational-memory.md +63 -0
  255. package/docs/content/plan-plugin.md +5 -0
  256. package/docs/content/pr-visual-recap.md +9 -5
  257. package/docs/content/processors.md +99 -0
  258. package/docs/content/sandbox-adapters.md +134 -0
  259. package/docs/content/template-plan.md +97 -21
  260. package/package.json +10 -1
  261. package/src/templates/default/.agents/skills/actions/SKILL.md +52 -1
  262. package/src/templates/default/.agents/skills/security/SKILL.md +22 -0
  263. package/src/templates/workspace-core/.agents/skills/actions/SKILL.md +52 -1
  264. package/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +16 -4
  265. package/src/templates/workspace-core/.agents/skills/harness-agents/SKILL.md +20 -0
  266. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +31 -0
  267. package/src/templates/workspace-core/.agents/skills/security/SKILL.md +22 -0
@@ -2,17 +2,30 @@
2
2
  * Plan helper commands.
3
3
  *
4
4
  * The `plan local` commands are intentionally separate from the Plan app
5
- * actions. They do not call MCP, HTTP, SQLite, or the Plan template runtime;
6
- * they only read and write local files so privacy-focused users have an
7
- * auditable no-DB path. The top-level `plan blocks` command is a schema-only,
8
- * no-auth helper for fetching the public block catalog before authoring local
9
- * MDX; it never sends plan content.
5
+ * actions. They do not call MCP, hosted write actions, SQLite, or hosted
6
+ * storage; they only read local files or serve them from a localhost bridge so
7
+ * privacy-focused users have an auditable no-DB path. The top-level
8
+ * `plan blocks` command is a schema-only, no-auth helper for fetching the
9
+ * public block catalog before authoring local MDX; it never sends plan content.
10
10
  */
11
11
  import fs from "node:fs";
12
+ import crypto from "node:crypto";
13
+ import http from "node:http";
12
14
  import path from "node:path";
13
15
  import { spawnSync } from "node:child_process";
14
16
  import { pathToFileURL } from "node:url";
15
17
  import { DEFAULT_PLAN_APP_URL, defaultPlanBlocksOut, fetchPlanBlockCatalog, normalizePlanBlockFormat, } from "./plan-blocks.js";
18
+ const LOCAL_PLAN_ASSET_MAX_SINGLE_BYTES = 2 * 1024 * 1024;
19
+ const LOCAL_PLAN_ASSET_MAX_TOTAL_BYTES = 10 * 1024 * 1024;
20
+ const AGENT_NATIVE_MANIFEST_FILE = "agent-native.json";
21
+ const LOCAL_PLAN_ASSET_EXTENSIONS = new Set([
22
+ "png",
23
+ "jpg",
24
+ "jpeg",
25
+ "gif",
26
+ "webp",
27
+ "svg",
28
+ ]);
16
29
  function parseArgs(argv) {
17
30
  const out = {};
18
31
  for (let i = 0; i < argv.length; i += 1) {
@@ -62,19 +75,135 @@ function normalizeKind(value) {
62
75
  return value;
63
76
  throw new Error(`Invalid --kind "${value}" (expected plan or recap)`);
64
77
  }
78
+ function normalizeSlash(filePath) {
79
+ return filePath.replace(/\\/g, "/");
80
+ }
81
+ function normalizeRelativePath(filePath) {
82
+ if (!filePath.trim() || path.isAbsolute(filePath))
83
+ return null;
84
+ const normalized = path.posix
85
+ .normalize(normalizeSlash(filePath.trim()))
86
+ .replace(/\/+$/, "");
87
+ if (!normalized ||
88
+ normalized === "." ||
89
+ normalized === ".." ||
90
+ normalized.startsWith("../") ||
91
+ normalized.split("/").some((part) => !part || part === "." || part === "..")) {
92
+ return null;
93
+ }
94
+ return normalized;
95
+ }
96
+ function findUpward(startDir, filename) {
97
+ let current = path.resolve(startDir);
98
+ for (;;) {
99
+ const candidate = path.join(current, filename);
100
+ if (fs.existsSync(candidate))
101
+ return candidate;
102
+ const parent = path.dirname(current);
103
+ if (parent === current)
104
+ return null;
105
+ current = parent;
106
+ }
107
+ }
108
+ function isRecord(value) {
109
+ return !!value && typeof value === "object" && !Array.isArray(value);
110
+ }
111
+ function manifestRootPath(value) {
112
+ if (typeof value === "string")
113
+ return normalizeRelativePath(value);
114
+ if (isRecord(value) && typeof value.path === "string") {
115
+ return normalizeRelativePath(value.path);
116
+ }
117
+ return null;
118
+ }
119
+ function planManifestConfig() {
120
+ const configuredManifest = process.env.AGENT_NATIVE_MANIFEST?.trim() ||
121
+ process.env.AGENT_NATIVE_MANIFEST_PATH?.trim();
122
+ const manifestPath = configuredManifest
123
+ ? path.resolve(configuredManifest)
124
+ : findUpward(process.cwd(), AGENT_NATIVE_MANIFEST_FILE);
125
+ if (!manifestPath)
126
+ return null;
127
+ try {
128
+ const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
129
+ const apps = isRecord(parsed) && isRecord(parsed.apps) ? parsed.apps : null;
130
+ const planApp = apps && isRecord(apps.plan) ? apps.plan : null;
131
+ const roots = planApp && Array.isArray(planApp.roots) ? planApp.roots : [];
132
+ const plansPath = roots
133
+ .map(manifestRootPath)
134
+ .find((item) => Boolean(item));
135
+ return plansPath
136
+ ? { rootDir: path.dirname(manifestPath), plansPath }
137
+ : null;
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ }
143
+ function localPlanWorkspaceRoot(startDir = process.cwd()) {
144
+ const manifestPath = findUpward(startDir, AGENT_NATIVE_MANIFEST_FILE);
145
+ if (manifestPath)
146
+ return path.dirname(manifestPath);
147
+ const gitDir = findUpward(startDir, ".git");
148
+ if (gitDir)
149
+ return path.dirname(gitDir);
150
+ const configuredManifest = process.env.AGENT_NATIVE_MANIFEST?.trim() ||
151
+ process.env.AGENT_NATIVE_MANIFEST_PATH?.trim();
152
+ if (configuredManifest)
153
+ return path.dirname(path.resolve(configuredManifest));
154
+ return process.cwd();
155
+ }
65
156
  function defaultPlansDir() {
66
- return path.resolve(process.env.PLAN_LOCAL_DIR || "plans");
157
+ const configured = process.env.PLAN_LOCAL_DIR;
158
+ if (configured?.trim())
159
+ return path.resolve(configured.trim());
160
+ const manifest = planManifestConfig();
161
+ if (manifest)
162
+ return path.resolve(manifest.rootDir, manifest.plansPath);
163
+ return path.resolve("plans");
67
164
  }
68
165
  function defaultLocalPlanAppUrl() {
69
166
  return (process.env.PLAN_LOCAL_APP_URL ||
70
167
  process.env.PLAN_BASE_URL ||
71
168
  "http://localhost:8096");
72
169
  }
170
+ function defaultLocalPlanBridgeAppUrl() {
171
+ return (process.env.PLAN_LOCAL_BRIDGE_APP_URL ||
172
+ process.env.PLAN_BASE_URL ||
173
+ DEFAULT_PLAN_APP_URL);
174
+ }
73
175
  function normalizeAppUrl(value) {
74
176
  return (value || defaultLocalPlanAppUrl()).replace(/\/+$/, "");
75
177
  }
178
+ function normalizeBridgeAppUrl(value) {
179
+ return (value || defaultLocalPlanBridgeAppUrl()).replace(/\/+$/, "");
180
+ }
76
181
  function localPlanPreviewUrl(dir, appUrl) {
77
- return `${normalizeAppUrl(appUrl)}/local-plans/${encodeURIComponent(path.basename(path.resolve(dir)))}`;
182
+ const base = `${normalizeAppUrl(appUrl)}/local-plans/${encodeURIComponent(path.basename(path.resolve(dir)))}`;
183
+ const repoPath = repoRelativePlanPath(dir);
184
+ if (!repoPath)
185
+ return base;
186
+ return `${base}?${new URLSearchParams({ path: repoPath }).toString()}`;
187
+ }
188
+ function realpathIfExists(filePath) {
189
+ try {
190
+ return fs.realpathSync.native(filePath);
191
+ }
192
+ catch {
193
+ return path.resolve(filePath);
194
+ }
195
+ }
196
+ function repoRelativePlanPath(dir) {
197
+ const resolved = realpathIfExists(dir);
198
+ const root = realpathIfExists(localPlanWorkspaceRoot(resolved));
199
+ const relative = path.relative(root, resolved);
200
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
201
+ return null;
202
+ }
203
+ return normalizeSlash(relative);
204
+ }
205
+ function localPlanBridgePageUrl(input) {
206
+ return `${normalizeBridgeAppUrl(input.appUrl)}/local-plans/${encodeURIComponent(path.basename(path.resolve(input.dir)))}?bridge=${encodeURIComponent(input.bridgeUrl)}`;
78
207
  }
79
208
  function openLocalUrl(url) {
80
209
  const platform = process.platform;
@@ -241,6 +370,33 @@ function renderMarkdownish(source) {
241
370
  flushList();
242
371
  return html.join("\n");
243
372
  }
373
+ function readLocalPlanAssets(dir) {
374
+ const assetsDir = path.join(dir, "assets");
375
+ if (!fs.existsSync(assetsDir))
376
+ return undefined;
377
+ const assets = {};
378
+ let totalBytes = 0;
379
+ for (const entry of fs.readdirSync(assetsDir, { withFileTypes: true })) {
380
+ if (!entry.isFile())
381
+ continue;
382
+ const filename = path.basename(entry.name);
383
+ if (!filename || filename !== entry.name)
384
+ continue;
385
+ const ext = filename.split(".").pop()?.toLowerCase() ?? "";
386
+ if (!LOCAL_PLAN_ASSET_EXTENSIONS.has(ext))
387
+ continue;
388
+ const abs = path.join(assetsDir, filename);
389
+ const bytes = fs.readFileSync(abs);
390
+ if (bytes.byteLength > LOCAL_PLAN_ASSET_MAX_SINGLE_BYTES)
391
+ continue;
392
+ if (totalBytes + bytes.byteLength > LOCAL_PLAN_ASSET_MAX_TOTAL_BYTES) {
393
+ continue;
394
+ }
395
+ totalBytes += bytes.byteLength;
396
+ assets[filename] = bytes.toString("base64");
397
+ }
398
+ return Object.keys(assets).length > 0 ? assets : undefined;
399
+ }
244
400
  export function readLocalPlanFiles(dir) {
245
401
  const resolved = path.resolve(dir);
246
402
  const planPath = path.join(resolved, "plan.mdx");
@@ -257,10 +413,255 @@ export function readLocalPlanFiles(dir) {
257
413
  canvasMdx: readOptional("canvas.mdx"),
258
414
  prototypeMdx: readOptional("prototype.mdx"),
259
415
  stateJson: readOptional(".plan-state.json"),
416
+ assets: readLocalPlanAssets(resolved),
417
+ };
418
+ }
419
+ function localPlanMdxFolder(files) {
420
+ return {
421
+ "plan.mdx": files.planMdx,
422
+ ...(files.canvasMdx ? { "canvas.mdx": files.canvasMdx } : {}),
423
+ ...(files.prototypeMdx ? { "prototype.mdx": files.prototypeMdx } : {}),
424
+ ...(files.stateJson ? { ".plan-state.json": files.stateJson } : {}),
425
+ ...(files.assets ? { "assets/": files.assets } : {}),
260
426
  };
261
427
  }
428
+ function localPlanFileList(files) {
429
+ return [
430
+ "plan.mdx",
431
+ ...(files.canvasMdx ? ["canvas.mdx"] : []),
432
+ ...(files.prototypeMdx ? ["prototype.mdx"] : []),
433
+ ...(files.stateJson ? [".plan-state.json"] : []),
434
+ ...Object.keys(files.assets ?? {}).map((filename) => `assets/${filename}`),
435
+ ];
436
+ }
437
+ function localPlanSourceEntries(files) {
438
+ return [
439
+ { file: "plan.mdx", source: files.planMdx },
440
+ ...(files.canvasMdx
441
+ ? [{ file: "canvas.mdx", source: files.canvasMdx }]
442
+ : []),
443
+ ...(files.prototypeMdx
444
+ ? [{ file: "prototype.mdx", source: files.prototypeMdx }]
445
+ : []),
446
+ ];
447
+ }
448
+ function lineNumberAt(source, index) {
449
+ let line = 1;
450
+ for (let i = 0; i < index; i += 1) {
451
+ if (source.charCodeAt(i) === 10)
452
+ line += 1;
453
+ }
454
+ return line;
455
+ }
456
+ function maskFencedCode(source) {
457
+ return source.replace(/(^|\n)(```|~~~)[\s\S]*?(\n\2[^\n]*(?=\n|$))/g, (match) => match.replace(/[^\n]/g, " "));
458
+ }
459
+ function findJsxOpeningTagEnd(source, start) {
460
+ let quote = null;
461
+ let braceDepth = 0;
462
+ for (let i = start; i < source.length; i += 1) {
463
+ const char = source[i];
464
+ if (quote) {
465
+ if (char === "\\" && i + 1 < source.length) {
466
+ i += 1;
467
+ continue;
468
+ }
469
+ if (char === quote)
470
+ quote = null;
471
+ continue;
472
+ }
473
+ if (char === '"' || char === "'" || char === "`") {
474
+ quote = char;
475
+ continue;
476
+ }
477
+ if (char === "{") {
478
+ braceDepth += 1;
479
+ continue;
480
+ }
481
+ if (char === "}") {
482
+ braceDepth = Math.max(0, braceDepth - 1);
483
+ continue;
484
+ }
485
+ if (char === ">" && braceDepth === 0)
486
+ return i;
487
+ }
488
+ return -1;
489
+ }
490
+ function addValidationIssue(issues, file, source, index, message) {
491
+ issues.push({ file, line: lineNumberAt(source, index), message });
492
+ }
493
+ const ENTITY_RE = /&(?:[a-z][a-z0-9]+|#[0-9]+|#x[0-9a-f]+);/gi;
494
+ const HTML_TEXT_ATTR_RE = /\b(?:aria-label|alt|placeholder|title|value)=\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`)/gi;
495
+ const WIREFRAME_TEXT_ATTR_RE = /\b(?:text|value|label|placeholder|title|note|due)=\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`)/gi;
496
+ function normalizeVisibleText(value) {
497
+ return value
498
+ .replace(/&nbsp;|&#160;|&#x0*a0;/gi, " ")
499
+ .replace(ENTITY_RE, "x")
500
+ .replace(/\s+/g, " ")
501
+ .trim();
502
+ }
503
+ function meaningfulTextLength(value) {
504
+ return normalizeVisibleText(value ?? "").length;
505
+ }
506
+ function htmlMeaningfulTextLength(html) {
507
+ let length = 0;
508
+ for (const match of html.matchAll(HTML_TEXT_ATTR_RE)) {
509
+ length += meaningfulTextLength(match[1] ?? match[2] ?? match[3]);
510
+ }
511
+ const visibleText = html
512
+ .replace(/<!--[\s\S]*?-->/g, " ")
513
+ .replace(/<script\b[\s\S]*?<\/script>/gi, " ")
514
+ .replace(/<style\b[\s\S]*?<\/style>/gi, " ")
515
+ .replace(/<[^>]+>/g, " ");
516
+ length += meaningfulTextLength(visibleText);
517
+ return length;
518
+ }
519
+ function hasSkeletonGeometry(html) {
520
+ return (/<(?:div|span|section|main|article|ul|li)\b/i.test(html) &&
521
+ /\b(?:height|width|background|border|padding|wf-card|wf-box|wf-pill|wf-chip)\b/i.test(html));
522
+ }
523
+ function stringAttributeValues(source, name) {
524
+ const values = [];
525
+ const re = new RegExp(`\\b${name}\\s*=\\s*(?:\\{\\s*\`([\\s\\S]*?)\`\\s*\\}|\\{\\s*"([^"]*)"\\s*\\}|\\{\\s*'([^']*)'\\s*\\}|"([^"]*)"|'([^']*)')`, "g");
526
+ for (const match of source.matchAll(re)) {
527
+ const value = match[1] ?? match[2] ?? match[3] ?? match[4] ?? match[5];
528
+ if (value !== undefined)
529
+ values.push(value);
530
+ }
531
+ return values;
532
+ }
533
+ function hasUnparsedAttributeExpression(source, name) {
534
+ return new RegExp(`\\b${name}\\s*=\\s*\\{`).test(source);
535
+ }
536
+ function hasMeaningfulWireframeHtml(screenOpening) {
537
+ const htmlValues = stringAttributeValues(screenOpening, "html");
538
+ if (htmlValues.length === 0) {
539
+ return hasUnparsedAttributeExpression(screenOpening, "html") ? null : false;
540
+ }
541
+ return htmlValues.some((html) => htmlMeaningfulTextLength(html) >= 2 || hasSkeletonGeometry(html));
542
+ }
543
+ function hasMeaningfulKitScreen(screenSource) {
544
+ for (const match of screenSource.matchAll(WIREFRAME_TEXT_ATTR_RE)) {
545
+ if (meaningfulTextLength(match[1] ?? match[2] ?? match[3]) >= 2) {
546
+ return true;
547
+ }
548
+ }
549
+ if (/\bitems\s*=\s*\{[\s\S]*?\blabel\s*:/i.test(screenSource))
550
+ return true;
551
+ if (/\brows\s*=\s*\{[\s\S]*?\b[klv]\s*:/i.test(screenSource))
552
+ return true;
553
+ return false;
554
+ }
555
+ function hasMeaningfulWireframeScreen(blockSource) {
556
+ const screenMatch = /<Screen\b/.exec(blockSource);
557
+ if (!screenMatch)
558
+ return false;
559
+ const screenStart = screenMatch.index;
560
+ const screenOpeningEnd = findJsxOpeningTagEnd(blockSource, screenStart);
561
+ if (screenOpeningEnd < 0)
562
+ return false;
563
+ const screenOpening = blockSource.slice(screenStart, screenOpeningEnd + 1);
564
+ const htmlMeaningful = hasMeaningfulWireframeHtml(screenOpening);
565
+ if (htmlMeaningful === true)
566
+ return true;
567
+ const selfClosing = /\/\s*>$/.test(screenOpening);
568
+ const closeIndex = selfClosing
569
+ ? -1
570
+ : blockSource.indexOf("</Screen>", screenOpeningEnd + 1);
571
+ const screenSource = closeIndex >= 0
572
+ ? blockSource.slice(screenStart, closeIndex + "</Screen>".length)
573
+ : screenOpening;
574
+ if (hasMeaningfulKitScreen(screenSource))
575
+ return true;
576
+ return htmlMeaningful === null ? null : false;
577
+ }
578
+ function lintWireframeBlocks(file, source, issues) {
579
+ const scanSource = maskFencedCode(source);
580
+ const re = /<WireframeBlock\b/g;
581
+ let match;
582
+ while ((match = re.exec(scanSource))) {
583
+ const start = match.index;
584
+ const openingEnd = findJsxOpeningTagEnd(scanSource, start);
585
+ if (openingEnd < 0) {
586
+ addValidationIssue(issues, file, source, start, "WireframeBlock opening tag is not closed.");
587
+ continue;
588
+ }
589
+ const opening = scanSource.slice(start, openingEnd + 1);
590
+ const unsupportedAttr = opening.match(/\b(data|screens|screen|elements)\s*=/);
591
+ if (unsupportedAttr) {
592
+ addValidationIssue(issues, file, source, start, `WireframeBlock uses unsupported "${unsupportedAttr[1]}" prop. Put content inside a <Screen> child instead.`);
593
+ }
594
+ const selfClosing = /\/\s*>$/.test(opening);
595
+ const closeTag = "</WireframeBlock>";
596
+ const closeIndex = selfClosing
597
+ ? -1
598
+ : scanSource.indexOf(closeTag, openingEnd + 1);
599
+ const blockSource = selfClosing
600
+ ? opening
601
+ : closeIndex >= 0
602
+ ? scanSource.slice(start, closeIndex + closeTag.length)
603
+ : scanSource.slice(start, openingEnd + 1);
604
+ if (!selfClosing && closeIndex < 0) {
605
+ addValidationIssue(issues, file, source, start, "WireframeBlock must have a closing </WireframeBlock> tag.");
606
+ }
607
+ if (selfClosing || !/<Screen\b/.test(blockSource)) {
608
+ addValidationIssue(issues, file, source, start, 'WireframeBlock must wrap a <Screen> child; self-closing wireframes render empty. Use <WireframeBlock><Screen surface="browser">...</Screen></WireframeBlock>.');
609
+ continue;
610
+ }
611
+ const meaningfulScreen = hasMeaningfulWireframeScreen(blockSource);
612
+ if (meaningfulScreen === false) {
613
+ addValidationIssue(issues, file, source, start, 'WireframeBlock contains an empty <Screen>; local previews render blank wireframes. Add visible html text/controls or kit nodes such as <Title text="Checkout" /> and <Btn label="Pay" />.');
614
+ }
615
+ }
616
+ }
617
+ function lintColumnsBlocks(file, source, issues) {
618
+ const scanSource = maskFencedCode(source);
619
+ const re = /<Columns\b/g;
620
+ let match;
621
+ while ((match = re.exec(scanSource))) {
622
+ const start = match.index;
623
+ const openingEnd = findJsxOpeningTagEnd(scanSource, start);
624
+ if (openingEnd < 0)
625
+ continue;
626
+ const opening = scanSource.slice(start, openingEnd + 1);
627
+ if (/\bcolumns\s*=/.test(opening)) {
628
+ addValidationIssue(issues, file, source, start, 'Columns must use <Column> children, not a columns= prop. Use <Columns><Column label="Before">...</Column><Column label="After">...</Column></Columns>.');
629
+ }
630
+ }
631
+ }
632
+ // Blank out fenced code blocks and inline code spans (preserving newlines and
633
+ // length) so block-tag linters don't trip on documentation examples written in
634
+ // prose — e.g. an inline `<WireframeBlock><Screen>...</Screen></WireframeBlock>`
635
+ // example is not a real block to validate. Real blocks (outside code) are left
636
+ // intact, so their offsets/line numbers stay correct. Without this the default
637
+ // `plan local init` scaffold fails its own `plan local check`/`serve` lint.
638
+ function maskCodeRegions(source) {
639
+ const blank = (s) => s.replace(/[^\n]/g, " ");
640
+ return source.replace(/```[\s\S]*?```/g, blank).replace(/`[^`\n]*`/g, blank);
641
+ }
642
+ export function validateLocalPlanFiles(files) {
643
+ const issues = [];
644
+ for (const entry of localPlanSourceEntries(files)) {
645
+ const source = maskCodeRegions(entry.source);
646
+ lintWireframeBlocks(entry.file, source, issues);
647
+ lintColumnsBlocks(entry.file, source, issues);
648
+ }
649
+ return issues;
650
+ }
651
+ export function assertLocalPlanFilesValid(files) {
652
+ const issues = validateLocalPlanFiles(files);
653
+ if (issues.length === 0)
654
+ return;
655
+ const details = issues
656
+ .slice(0, 8)
657
+ .map((issue) => `${issue.file}:${issue.line} ${issue.message}`)
658
+ .join("\n");
659
+ const overflow = issues.length > 8 ? `\n...plus ${issues.length - 8} more issues` : "";
660
+ throw new Error(`Local plan source validation failed:\n${details}${overflow}\nRun \`npx @agent-native/core@latest plan blocks --out plan-blocks.md\` and update the MDX to the documented block shapes.`);
661
+ }
262
662
  export function buildLocalPlanPreviewHtml(input) {
263
663
  const files = readLocalPlanFiles(input.dir);
664
+ assertLocalPlanFilesValid(files);
264
665
  const parsed = stripFrontmatter(files.planMdx);
265
666
  const title = input.title ||
266
667
  parsed.frontmatter.title ||
@@ -358,7 +759,9 @@ export function buildLocalPlanPreviewHtml(input) {
358
759
  }
359
760
  export function writeLocalPlanPreview(input) {
360
761
  const dir = path.resolve(input.dir);
361
- const parsed = stripFrontmatter(readLocalPlanFiles(dir).planMdx);
762
+ const files = readLocalPlanFiles(dir);
763
+ assertLocalPlanFilesValid(files);
764
+ const parsed = stripFrontmatter(files.planMdx);
362
765
  const kind = input.kind || normalizeKind(parsed.frontmatter.kind);
363
766
  const title = input.title ||
364
767
  parsed.frontmatter.title ||
@@ -369,12 +772,6 @@ export function writeLocalPlanPreview(input) {
369
772
  fs.mkdirSync(path.dirname(out), { recursive: true });
370
773
  fs.writeFileSync(out, buildLocalPlanPreviewHtml({ ...input, dir, kind }));
371
774
  }
372
- const files = [
373
- "plan.mdx",
374
- "canvas.mdx",
375
- "prototype.mdx",
376
- ".plan-state.json",
377
- ].filter((file) => fs.existsSync(path.join(dir, file)));
378
775
  const result = {
379
776
  ok: true,
380
777
  dir,
@@ -382,7 +779,7 @@ export function writeLocalPlanPreview(input) {
382
779
  url: out ? pathToFileURL(out).href : localPlanPreviewUrl(dir, input.appUrl),
383
780
  title,
384
781
  kind,
385
- files,
782
+ files: localPlanFileList(files),
386
783
  };
387
784
  if (!input.open)
388
785
  return result;
@@ -394,6 +791,162 @@ export function writeLocalPlanPreview(input) {
394
791
  ...(openResult.error ? { openError: openResult.error } : {}),
395
792
  };
396
793
  }
794
+ function buildLocalPlanBridgePayload(input) {
795
+ const dir = path.resolve(input.dir);
796
+ const files = readLocalPlanFiles(dir);
797
+ assertLocalPlanFilesValid(files);
798
+ const parsed = stripFrontmatter(files.planMdx);
799
+ const kind = input.kind || normalizeKind(parsed.frontmatter.kind);
800
+ const title = input.title ||
801
+ parsed.frontmatter.title ||
802
+ firstHeading(parsed.body) ||
803
+ path.basename(dir);
804
+ const brief = input.brief || parsed.frontmatter.brief || "";
805
+ return {
806
+ ok: true,
807
+ version: 1,
808
+ source: "agent-native-local-bridge",
809
+ localOnly: true,
810
+ slug: path.basename(dir),
811
+ dir,
812
+ title,
813
+ brief,
814
+ kind,
815
+ updatedAt: latestLocalPlanMtime(dir, files),
816
+ files: localPlanFileList(files),
817
+ mdx: localPlanMdxFolder(files),
818
+ };
819
+ }
820
+ function latestLocalPlanMtime(dir, files) {
821
+ const candidates = [
822
+ path.join(dir, "plan.mdx"),
823
+ ...(files.canvasMdx ? [path.join(dir, "canvas.mdx")] : []),
824
+ ...(files.prototypeMdx ? [path.join(dir, "prototype.mdx")] : []),
825
+ ...(files.stateJson ? [path.join(dir, ".plan-state.json")] : []),
826
+ ...Object.keys(files.assets ?? {}).map((filename) => path.join(dir, "assets", filename)),
827
+ ];
828
+ let latest = 0;
829
+ for (const file of candidates) {
830
+ try {
831
+ latest = Math.max(latest, fs.statSync(file).mtimeMs);
832
+ }
833
+ catch {
834
+ // Ignore files deleted between the read and stat passes.
835
+ }
836
+ }
837
+ return new Date(latest || Date.now()).toISOString();
838
+ }
839
+ function sendBridgeJson(res, status, payload) {
840
+ res.writeHead(status, {
841
+ "access-control-allow-origin": "*",
842
+ "access-control-allow-methods": "GET, OPTIONS",
843
+ "access-control-allow-headers": "content-type",
844
+ // Required when the hosted HTTPS Plan UI fetches this localhost bridge.
845
+ "access-control-allow-private-network": "true",
846
+ "cache-control": "no-store",
847
+ "content-type": "application/json; charset=utf-8",
848
+ "x-agent-native-local-bridge": "1",
849
+ });
850
+ res.end(`${JSON.stringify(payload)}\n`);
851
+ }
852
+ function bridgeRequestUrl(req) {
853
+ return new URL(req.url || "/", "http://127.0.0.1");
854
+ }
855
+ function bridgeHostForUrl(host) {
856
+ if (host === "0.0.0.0" || host === "::")
857
+ return "127.0.0.1";
858
+ return host;
859
+ }
860
+ export async function startLocalPlanBridge(input) {
861
+ const dir = path.resolve(input.dir);
862
+ const initialPayload = buildLocalPlanBridgePayload({
863
+ dir,
864
+ kind: input.kind,
865
+ title: input.title,
866
+ brief: input.brief,
867
+ });
868
+ const token = input.token || crypto.randomBytes(24).toString("base64url");
869
+ const host = input.host || "127.0.0.1";
870
+ const appUrl = normalizeBridgeAppUrl(input.appUrl);
871
+ const server = http.createServer((req, res) => {
872
+ if (req.method === "OPTIONS") {
873
+ sendBridgeJson(res, 204, "");
874
+ return;
875
+ }
876
+ if (req.method !== "GET") {
877
+ sendBridgeJson(res, 405, { ok: false, error: "Method not allowed." });
878
+ return;
879
+ }
880
+ const url = bridgeRequestUrl(req);
881
+ if (url.pathname !== "/local-plan.json") {
882
+ sendBridgeJson(res, 404, { ok: false, error: "Not found." });
883
+ return;
884
+ }
885
+ if (url.searchParams.get("token") !== token) {
886
+ sendBridgeJson(res, 403, { ok: false, error: "Invalid bridge token." });
887
+ return;
888
+ }
889
+ try {
890
+ sendBridgeJson(res, 200, buildLocalPlanBridgePayload({
891
+ dir,
892
+ kind: input.kind,
893
+ title: input.title,
894
+ brief: input.brief,
895
+ }));
896
+ }
897
+ catch (error) {
898
+ sendBridgeJson(res, 500, {
899
+ ok: false,
900
+ error: error instanceof Error ? error.message : String(error),
901
+ });
902
+ }
903
+ });
904
+ await new Promise((resolve, reject) => {
905
+ const onError = (error) => {
906
+ server.off("listening", onListening);
907
+ reject(error);
908
+ };
909
+ const onListening = () => {
910
+ server.off("error", onError);
911
+ resolve();
912
+ };
913
+ server.once("error", onError);
914
+ server.once("listening", onListening);
915
+ server.listen(input.port ?? 0, host);
916
+ });
917
+ const address = server.address();
918
+ if (!address || typeof address === "string") {
919
+ server.close();
920
+ throw new Error("Local plan bridge did not bind to a TCP port.");
921
+ }
922
+ const bridgeUrl = `http://${bridgeHostForUrl(host)}:${address.port}/local-plan.json?token=${encodeURIComponent(token)}`;
923
+ const url = localPlanBridgePageUrl({ dir, bridgeUrl, appUrl });
924
+ const openResult = input.open
925
+ ? (input.openUrl || openLocalUrl)(url)
926
+ : undefined;
927
+ return {
928
+ server,
929
+ result: {
930
+ ok: true,
931
+ dir,
932
+ url,
933
+ bridgeUrl,
934
+ appUrl,
935
+ title: initialPayload.title,
936
+ kind: initialPayload.kind,
937
+ files: initialPayload.files,
938
+ host,
939
+ port: address.port,
940
+ ...(openResult
941
+ ? {
942
+ opened: openResult.ok,
943
+ openCommand: openResult.command,
944
+ ...(openResult.error ? { openError: openResult.error } : {}),
945
+ }
946
+ : {}),
947
+ },
948
+ };
949
+ }
397
950
  function writeLocalPlanSkeleton(input) {
398
951
  const dir = path.resolve(input.dir || path.join(defaultPlansDir(), localPlanFolderName(input.title)));
399
952
  const planPath = path.join(dir, "plan.mdx");
@@ -421,9 +974,9 @@ function writeLocalPlanSkeleton(input) {
421
974
  "## Review Surface",
422
975
  "",
423
976
  "Author the structured plan or recap here. You can add Agent-Native Plan MDX",
424
- "blocks such as `<WireframeBlock />`, `<Diagram />`, `<TabsBlock />`,",
425
- "`<FileTree />`, or `<Diff />`; the local preview will show the source",
426
- "without publishing it to the Plan app.",
977
+ 'blocks such as `<WireframeBlock><Screen surface="browser">...</Screen></WireframeBlock>`,',
978
+ "`<Diagram />`, `<TabsBlock />`, `<FileTree />`, or `<Diff />`; the local",
979
+ "preview will show the source without publishing it to the Plan app.",
427
980
  "",
428
981
  ].join("\n");
429
982
  fs.writeFileSync(planPath, mdx, "utf-8");
@@ -448,10 +1001,12 @@ function runInit(args) {
448
1001
  function runCheck(args) {
449
1002
  const dir = stringArg(args, "dir");
450
1003
  const files = readLocalPlanFiles(dir);
1004
+ assertLocalPlanFilesValid(files);
451
1005
  const parsed = stripFrontmatter(files.planMdx);
452
1006
  const result = {
453
1007
  ok: true,
454
1008
  noDb: true,
1009
+ validation: "passed",
455
1010
  dir: files.dir,
456
1011
  title: parsed.frontmatter.title || firstHeading(parsed.body),
457
1012
  kind: normalizeKind(parsed.frontmatter.kind),
@@ -466,6 +1021,12 @@ function runCheck(args) {
466
1021
  ...(files.stateJson
467
1022
  ? { ".plan-state.json": Buffer.byteLength(files.stateJson) }
468
1023
  : {}),
1024
+ ...(files.assets
1025
+ ? Object.fromEntries(Object.entries(files.assets).map(([filename, base64]) => [
1026
+ `assets/${filename}`,
1027
+ Buffer.byteLength(base64, "base64"),
1028
+ ]))
1029
+ : {}),
469
1030
  },
470
1031
  };
471
1032
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
@@ -484,6 +1045,34 @@ function runPreview(args) {
484
1045
  });
485
1046
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
486
1047
  }
1048
+ async function runServe(args) {
1049
+ const portValue = optionalArg(args, "port");
1050
+ const port = portValue ? Number(portValue) : undefined;
1051
+ if (portValue && (!Number.isInteger(port) || port < 0 || port > 65535)) {
1052
+ throw new Error("--port must be an integer between 0 and 65535.");
1053
+ }
1054
+ const bridge = await startLocalPlanBridge({
1055
+ dir: stringArg(args, "dir"),
1056
+ appUrl: optionalArg(args, "app-url"),
1057
+ title: optionalArg(args, "title"),
1058
+ brief: optionalArg(args, "brief"),
1059
+ host: optionalArg(args, "host"),
1060
+ port,
1061
+ open: boolArg(args, "open"),
1062
+ kind: optionalArg(args, "kind")
1063
+ ? normalizeKind(optionalArg(args, "kind"))
1064
+ : undefined,
1065
+ });
1066
+ 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`);
1068
+ await new Promise((resolve) => {
1069
+ const stop = () => {
1070
+ bridge.server.close(() => resolve());
1071
+ };
1072
+ process.once("SIGINT", stop);
1073
+ process.once("SIGTERM", stop);
1074
+ });
1075
+ }
487
1076
  async function runBlocks(args) {
488
1077
  const format = normalizePlanBlockFormat(optionalArg(args, "format"));
489
1078
  const appUrl = optionalArg(args, "app-url") ||
@@ -536,6 +1125,7 @@ Usage:
536
1125
  agent-native plan blocks [--format reference|schema] [--app-url <url>] [--out <file>] [--json]
537
1126
  agent-native plan local init --title <title> [--brief <text>] [--kind plan|recap] [--dir <folder>] [--force]
538
1127
  agent-native plan local check --dir <folder>
1128
+ agent-native plan local serve --dir <folder> [--app-url <url>] [--kind plan|recap] [--open] [--port <port>]
539
1129
  agent-native plan local preview --dir <folder> [--app-url <url>] [--kind plan|recap] [--open] [--out preview.html]
540
1130
 
541
1131
  The blocks command fetches the no-auth, read-only get-plan-blocks catalog from
@@ -552,10 +1142,12 @@ write actions, hosted storage, or SQLite.
552
1142
  Common flow:
553
1143
  agent-native plan blocks --out plan-blocks.md
554
1144
  agent-native plan local init --title "Checkout review" --kind plan
555
- agent-native plan local preview --dir plans/checkout-review --open
1145
+ agent-native plan local serve --dir plans/checkout-review --open
556
1146
 
557
- \`plan local preview\` opens the local Plan app route by default. Pass
558
- \`--app-url\` when your local Plan app is on a non-default port. \`--out\` is a
1147
+ \`plan local serve\` starts a tiny localhost bridge and opens the hosted Plan UI
1148
+ 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
559
1151
  legacy/debug escape hatch that writes a standalone static HTML file.
560
1152
  `;
561
1153
  export async function runPlan(argv) {
@@ -582,6 +1174,12 @@ export async function runPlan(argv) {
582
1174
  process.exit(1);
583
1175
  }
584
1176
  const args = parseArgs(rest);
1177
+ // `plan local <sub> --help` / `-h` shows help instead of running the
1178
+ // subcommand (e.g. `plan local init --help` must not scaffold a folder).
1179
+ if (args.help === true || args.h === true) {
1180
+ process.stdout.write(HELP);
1181
+ return;
1182
+ }
585
1183
  switch (sub) {
586
1184
  case "init":
587
1185
  runInit(args);
@@ -592,6 +1190,9 @@ export async function runPlan(argv) {
592
1190
  case "preview":
593
1191
  runPreview(args);
594
1192
  return;
1193
+ case "serve":
1194
+ await runServe(args);
1195
+ return;
595
1196
  case "help":
596
1197
  case "--help":
597
1198
  case "-h":