synthos 0.10.1 → 0.11.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 (311) hide show
  1. package/README.md +5 -5
  2. package/default-pages/elevenlabs_effects_studio/chat-history.json +1 -0
  3. package/default-pages/elevenlabs_effects_studio/page.html +1345 -1363
  4. package/default-pages/elevenlabs_effects_studio/page.json +13 -11
  5. package/default-pages/elevenlabs_voice_studio/chat-history.json +1 -0
  6. package/default-pages/elevenlabs_voice_studio/page.html +782 -801
  7. package/default-pages/elevenlabs_voice_studio/page.json +13 -11
  8. package/default-pages/json_tools/chat-history.json +1 -0
  9. package/default-pages/json_tools/page.html +70 -90
  10. package/default-pages/json_tools/page.json +12 -10
  11. package/default-pages/my_notes/chat-history.json +1 -0
  12. package/default-pages/my_notes/page.html +115 -131
  13. package/default-pages/my_notes/page.json +14 -12
  14. package/default-pages/neon_asteroids/chat-history.json +1 -0
  15. package/default-pages/neon_asteroids/page.html +1777 -1803
  16. package/default-pages/neon_asteroids/page.json +14 -12
  17. package/default-pages/oregon_trail/chat-history.json +1 -0
  18. package/default-pages/oregon_trail/page.html +290 -307
  19. package/default-pages/oregon_trail/page.json +14 -12
  20. package/default-pages/solar_explorer/chat-history.json +1 -0
  21. package/default-pages/solar_explorer/page.html +1929 -1951
  22. package/default-pages/solar_explorer/page.json +14 -12
  23. package/default-pages/solar_tutorial/chat-history.json +1 -0
  24. package/default-pages/solar_tutorial/page.html +464 -478
  25. package/default-pages/solar_tutorial/page.json +12 -10
  26. package/default-pages/us_map/chat-history.json +1 -0
  27. package/default-pages/us_map/page.html +170 -193
  28. package/default-pages/us_map/page.json +14 -12
  29. package/default-pages/us_map/page.light.png +0 -0
  30. package/default-pages/us_map_1850/chat-history.json +1 -0
  31. package/default-pages/us_map_1850/page.html +302 -326
  32. package/default-pages/us_map_1850/page.json +14 -12
  33. package/default-pages/western_cities_1850/chat-history.json +1 -0
  34. package/default-pages/western_cities_1850/page.html +503 -527
  35. package/default-pages/western_cities_1850/page.json +14 -12
  36. package/default-themes/aurora-dawn.v3.css +15 -14
  37. package/default-themes/aurora-dusk.v3.css +26 -26
  38. package/default-themes/cosmos-dawn.v3.css +15 -14
  39. package/default-themes/cosmos-dusk.v3.css +26 -26
  40. package/default-themes/elemental-dawn.v3.css +200 -0
  41. package/default-themes/nebula-dawn.v3.css +15 -14
  42. package/default-themes/nebula-dusk.v3.css +24 -24
  43. package/default-themes/solar-flare-dawn.v3.css +15 -14
  44. package/default-themes/solar-flare-dusk.v3.css +26 -26
  45. package/dist/builders/anthropic.d.ts +26 -2
  46. package/dist/builders/anthropic.d.ts.map +1 -1
  47. package/dist/builders/anthropic.js +132 -31
  48. package/dist/builders/anthropic.js.map +1 -1
  49. package/dist/builders/claudecode.d.ts +13 -0
  50. package/dist/builders/claudecode.d.ts.map +1 -0
  51. package/dist/builders/claudecode.js +253 -0
  52. package/dist/builders/claudecode.js.map +1 -0
  53. package/dist/builders/index.d.ts +2 -1
  54. package/dist/builders/index.d.ts.map +1 -1
  55. package/dist/builders/index.js +8 -1
  56. package/dist/builders/index.js.map +1 -1
  57. package/dist/builders/openai.js +2 -1
  58. package/dist/builders/openai.js.map +1 -1
  59. package/dist/builders/types.d.ts +31 -7
  60. package/dist/builders/types.d.ts.map +1 -1
  61. package/dist/builders/types.js +60 -28
  62. package/dist/builders/types.js.map +1 -1
  63. package/dist/connectors/types.d.ts +8 -0
  64. package/dist/connectors/types.d.ts.map +1 -1
  65. package/dist/init.d.ts.map +1 -1
  66. package/dist/init.js +13 -6
  67. package/dist/init.js.map +1 -1
  68. package/dist/migrations.d.ts.map +1 -1
  69. package/dist/migrations.js +161 -14
  70. package/dist/migrations.js.map +1 -1
  71. package/dist/models/anthropic.d.ts +1 -0
  72. package/dist/models/anthropic.d.ts.map +1 -1
  73. package/dist/models/anthropic.js +129 -29
  74. package/dist/models/anthropic.js.map +1 -1
  75. package/dist/models/chainOfThought.d.ts.map +1 -1
  76. package/dist/models/chainOfThought.js +32 -19
  77. package/dist/models/chainOfThought.js.map +1 -1
  78. package/dist/models/index.d.ts +2 -2
  79. package/dist/models/index.d.ts.map +1 -1
  80. package/dist/models/index.js +2 -1
  81. package/dist/models/index.js.map +1 -1
  82. package/dist/models/providers.d.ts +1 -0
  83. package/dist/models/providers.d.ts.map +1 -1
  84. package/dist/models/providers.js +12 -4
  85. package/dist/models/providers.js.map +1 -1
  86. package/dist/models/types.d.ts +15 -1
  87. package/dist/models/types.d.ts.map +1 -1
  88. package/dist/models/types.js.map +1 -1
  89. package/dist/pages.d.ts +57 -8
  90. package/dist/pages.d.ts.map +1 -1
  91. package/dist/pages.js +258 -45
  92. package/dist/pages.js.map +1 -1
  93. package/dist/service/createCompletePrompt.d.ts.map +1 -1
  94. package/dist/service/createCompletePrompt.js +5 -0
  95. package/dist/service/createCompletePrompt.js.map +1 -1
  96. package/dist/service/mediaCache.d.ts +36 -0
  97. package/dist/service/mediaCache.d.ts.map +1 -0
  98. package/dist/service/mediaCache.js +182 -0
  99. package/dist/service/mediaCache.js.map +1 -0
  100. package/dist/service/pageValidator.d.ts +25 -0
  101. package/dist/service/pageValidator.d.ts.map +1 -0
  102. package/dist/service/pageValidator.js +315 -0
  103. package/dist/service/pageValidator.js.map +1 -0
  104. package/dist/service/server.d.ts.map +1 -1
  105. package/dist/service/server.js +4 -0
  106. package/dist/service/server.js.map +1 -1
  107. package/dist/service/sharedTableSchema.d.ts +73 -0
  108. package/dist/service/sharedTableSchema.d.ts.map +1 -0
  109. package/dist/service/sharedTableSchema.js +206 -0
  110. package/dist/service/sharedTableSchema.js.map +1 -0
  111. package/dist/service/transformPage.d.ts +49 -11
  112. package/dist/service/transformPage.d.ts.map +1 -1
  113. package/dist/service/transformPage.js +354 -241
  114. package/dist/service/transformPage.js.map +1 -1
  115. package/dist/service/useApiRoutes.d.ts.map +1 -1
  116. package/dist/service/useApiRoutes.js +288 -34
  117. package/dist/service/useApiRoutes.js.map +1 -1
  118. package/dist/service/useConnectorRoutes.d.ts.map +1 -1
  119. package/dist/service/useConnectorRoutes.js +170 -32
  120. package/dist/service/useConnectorRoutes.js.map +1 -1
  121. package/dist/service/useDataRoutes.d.ts.map +1 -1
  122. package/dist/service/useDataRoutes.js +59 -2
  123. package/dist/service/useDataRoutes.js.map +1 -1
  124. package/dist/service/useExtractRoutes.d.ts +4 -0
  125. package/dist/service/useExtractRoutes.d.ts.map +1 -0
  126. package/dist/service/useExtractRoutes.js +304 -0
  127. package/dist/service/useExtractRoutes.js.map +1 -0
  128. package/dist/service/usePageRoutes.d.ts +17 -0
  129. package/dist/service/usePageRoutes.d.ts.map +1 -1
  130. package/dist/service/usePageRoutes.js +1385 -483
  131. package/dist/service/usePageRoutes.js.map +1 -1
  132. package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
  133. package/dist/service/useSharedDataRoutes.js +54 -2
  134. package/dist/service/useSharedDataRoutes.js.map +1 -1
  135. package/dist/settings.d.ts +27 -0
  136. package/dist/settings.d.ts.map +1 -1
  137. package/dist/settings.js +40 -1
  138. package/dist/settings.js.map +1 -1
  139. package/dist/themes.d.ts +0 -5
  140. package/dist/themes.d.ts.map +1 -1
  141. package/dist/themes.js +3 -95
  142. package/dist/themes.js.map +1 -1
  143. package/migration-rules/v2-to-v3.md +277 -119
  144. package/package.json +5 -1
  145. package/{default-pages/application → required-pages/_shell}/page.html +56 -42
  146. package/required-pages/_shell/page.json +14 -0
  147. package/required-pages/_starters/page.html +534 -0
  148. package/required-pages/_starters/page.json +12 -0
  149. package/required-pages/builder/page.html +353 -43
  150. package/required-pages/builder/page.json +12 -10
  151. package/required-pages/pages/page.html +697 -924
  152. package/required-pages/pages/page.json +12 -10
  153. package/required-pages/settings/page.html +1879 -1753
  154. package/required-pages/settings/page.json +12 -10
  155. package/required-pages/synthos_apis/page.html +834 -845
  156. package/required-pages/synthos_apis/page.json +12 -10
  157. package/required-pages/synthos_scripts/page.html +74 -88
  158. package/required-pages/synthos_scripts/page.json +12 -10
  159. package/scripts/append-instructions.py +90 -0
  160. package/scripts/audit-instructions.py +76 -0
  161. package/scripts/cleanup-shell-markup.mjs +112 -0
  162. package/service-connectors/buffer/connector.json +46 -0
  163. package/service-connectors/canva/connector.json +67 -0
  164. package/service-connectors/elevenlabs/connector.json +1 -1
  165. package/src/builders/anthropic.ts +150 -25
  166. package/src/builders/claudecode.ts +310 -0
  167. package/src/builders/index.ts +7 -1
  168. package/src/builders/openai.ts +2 -1
  169. package/src/builders/types.ts +93 -32
  170. package/src/connectors/types.ts +8 -0
  171. package/src/init.ts +13 -7
  172. package/src/migrations.ts +187 -16
  173. package/src/models/anthropic.ts +140 -30
  174. package/src/models/chainOfThought.ts +33 -18
  175. package/src/models/index.ts +2 -2
  176. package/src/models/providers.ts +10 -1
  177. package/src/models/types.ts +21 -1
  178. package/src/pages.ts +271 -35
  179. package/src/service/createCompletePrompt.ts +6 -0
  180. package/src/service/mediaCache.ts +206 -0
  181. package/src/service/pageValidator.ts +337 -0
  182. package/src/service/server.ts +4 -0
  183. package/src/service/sharedTableSchema.ts +236 -0
  184. package/src/service/transformPage.ts +370 -260
  185. package/src/service/useApiRoutes.ts +282 -32
  186. package/src/service/useConnectorRoutes.ts +189 -34
  187. package/src/service/useDataRoutes.ts +198 -116
  188. package/src/service/useExtractRoutes.ts +331 -0
  189. package/src/service/usePageRoutes.ts +1411 -394
  190. package/src/service/useSharedDataRoutes.ts +184 -109
  191. package/src/settings.ts +65 -0
  192. package/src/themes.ts +78 -180
  193. package/starters/blank_starter/chat-history.json +1 -0
  194. package/starters/blank_starter/page.dark.png +0 -0
  195. package/starters/blank_starter/page.html +47 -0
  196. package/starters/blank_starter/page.json +13 -0
  197. package/starters/blank_starter/page.light.png +0 -0
  198. package/starters/calculator_starter/chat-history.json +1 -0
  199. package/starters/calculator_starter/page.dark.png +0 -0
  200. package/starters/calculator_starter/page.html +232 -0
  201. package/starters/calculator_starter/page.json +13 -0
  202. package/starters/calculator_starter/page.light.png +0 -0
  203. package/starters/calendar_starter/chat-history.json +1 -0
  204. package/starters/calendar_starter/page.dark.png +0 -0
  205. package/starters/calendar_starter/page.html +495 -0
  206. package/starters/calendar_starter/page.json +13 -0
  207. package/starters/calendar_starter/page.light.png +0 -0
  208. package/starters/chat_starter/chat-history.json +1 -0
  209. package/starters/chat_starter/page.dark.png +0 -0
  210. package/starters/chat_starter/page.html +351 -0
  211. package/starters/chat_starter/page.json +13 -0
  212. package/starters/chat_starter/page.light.png +0 -0
  213. package/starters/checklist_starter/chat-history.json +1 -0
  214. package/starters/checklist_starter/page.dark.png +0 -0
  215. package/starters/checklist_starter/page.html +437 -0
  216. package/starters/checklist_starter/page.json +13 -0
  217. package/starters/checklist_starter/page.light.png +0 -0
  218. package/starters/dashboard_starter/chat-history.json +1 -0
  219. package/starters/dashboard_starter/page.dark.png +0 -0
  220. package/starters/dashboard_starter/page.html +195 -0
  221. package/starters/dashboard_starter/page.json +13 -0
  222. package/starters/dashboard_starter/page.light.png +0 -0
  223. package/starters/form_starter/chat-history.json +1 -0
  224. package/starters/form_starter/page.dark.png +0 -0
  225. package/starters/form_starter/page.html +313 -0
  226. package/starters/form_starter/page.json +13 -0
  227. package/starters/form_starter/page.light.png +0 -0
  228. package/starters/gallery_starter/chat-history.json +1 -0
  229. package/starters/gallery_starter/page.dark.png +0 -0
  230. package/starters/gallery_starter/page.html +418 -0
  231. package/starters/gallery_starter/page.json +13 -0
  232. package/starters/gallery_starter/page.light.png +0 -0
  233. package/starters/generator_starter/chat-history.json +1 -0
  234. package/starters/generator_starter/page.dark.png +0 -0
  235. package/starters/generator_starter/page.html +261 -0
  236. package/starters/generator_starter/page.json +13 -0
  237. package/starters/generator_starter/page.light.png +0 -0
  238. package/starters/index.html +538 -0
  239. package/starters/kanban_starter/chat-history.json +1 -0
  240. package/starters/kanban_starter/page.dark.png +0 -0
  241. package/starters/kanban_starter/page.html +432 -0
  242. package/starters/kanban_starter/page.json +13 -0
  243. package/starters/kanban_starter/page.light.png +0 -0
  244. package/starters/presentation_builder/chat-history.json +1 -0
  245. package/starters/presentation_builder/page.dark.png +0 -0
  246. package/starters/presentation_builder/page.html +970 -0
  247. package/starters/presentation_builder/page.json +15 -0
  248. package/starters/presentation_builder/page.light.png +0 -0
  249. package/starters/presentation_builder/presentation_voice/voice_config.json +9 -0
  250. package/starters/pulse_starter/chat-history.json +1 -0
  251. package/starters/pulse_starter/page.dark.png +0 -0
  252. package/starters/pulse_starter/page.html +698 -0
  253. package/starters/pulse_starter/page.json +13 -0
  254. package/starters/pulse_starter/page.light.png +0 -0
  255. package/starters/quiz_starter/chat-history.json +1 -0
  256. package/starters/quiz_starter/page.dark.png +0 -0
  257. package/starters/quiz_starter/page.html +292 -0
  258. package/starters/quiz_starter/page.json +13 -0
  259. package/starters/quiz_starter/page.light.png +0 -0
  260. package/starters/reference_starter/chat-history.json +1 -0
  261. package/starters/reference_starter/page.dark.png +0 -0
  262. package/starters/reference_starter/page.html +250 -0
  263. package/starters/reference_starter/page.json +13 -0
  264. package/starters/reference_starter/page.light.png +0 -0
  265. package/starters/retro_game_starter/chat-history.json +1 -0
  266. package/starters/retro_game_starter/page.dark.png +0 -0
  267. package/{default-pages → starters}/retro_game_starter/page.html +1281 -1308
  268. package/starters/retro_game_starter/page.json +15 -0
  269. package/starters/retro_game_starter/page.light.png +0 -0
  270. package/starters/roster_starter/chat-history.json +1 -0
  271. package/starters/roster_starter/page.dark.png +0 -0
  272. package/starters/roster_starter/page.html +600 -0
  273. package/starters/roster_starter/page.json +13 -0
  274. package/starters/roster_starter/page.light.png +0 -0
  275. package/starters/server.js +182 -0
  276. package/starters/start.cmd +1 -0
  277. package/starters/timeline_starter/chat-history.json +1 -0
  278. package/starters/timeline_starter/page.dark.png +0 -0
  279. package/starters/timeline_starter/page.html +446 -0
  280. package/starters/timeline_starter/page.json +13 -0
  281. package/starters/timeline_starter/page.light.png +0 -0
  282. package/starters/tutorial_starter/chat-history.json +1 -0
  283. package/starters/tutorial_starter/page.dark.png +0 -0
  284. package/starters/tutorial_starter/page.html +283 -0
  285. package/starters/tutorial_starter/page.json +13 -0
  286. package/starters/tutorial_starter/page.light.png +0 -0
  287. package/static-files/agent.v3.js +122 -0
  288. package/static-files/connector.v3.js +48 -0
  289. package/static-files/extract.v3.js +188 -0
  290. package/static-files/helpers.v3.js +50 -6
  291. package/static-files/page-bridge.js +114 -0
  292. package/static-files/page.v3.js +1292 -1290
  293. package/static-files/script.v3.js +32 -0
  294. package/static-files/server.v3.js +89 -0
  295. package/static-files/shell-bridge.v3.js +174 -0
  296. package/static-files/shell-modals.v3.js +521 -0
  297. package/static-files/{shell.css → shell.v3.css} +271 -22
  298. package/static-files/shell.v3.js +1865 -0
  299. package/static-files/storage.v3.js +176 -0
  300. package/tests/anthropic.spec.ts +42 -7
  301. package/tests/builders.spec.ts +70 -2
  302. package/tests/pageValidator.spec.ts +548 -0
  303. package/tests/profiles.spec.ts +122 -0
  304. package/tests/sharedTableSchema.spec.ts +242 -0
  305. package/tests/transformPage.spec.ts +62 -81
  306. package/default-pages/application/page.json +0 -10
  307. package/default-pages/retro_game_starter/page.json +0 -12
  308. package/default-pages/sidebar_page/page.html +0 -51
  309. package/default-pages/sidebar_page/page.json +0 -10
  310. package/default-pages/two-panel_page/page.html +0 -68
  311. package/default-pages/two-panel_page/page.json +0 -10
@@ -0,0 +1,310 @@
1
+ import { spawn } from 'child_process';
2
+ import { ChangeOp, getTransformInstr } from '../service/transformPage';
3
+ import { parseBuilderResponse } from './anthropic';
4
+ import { Builder, BuilderResult, CHANGE_OPS_FORMAT_INSTRUCTION } from './types';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Options
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export interface ClaudeCodeBuilderOptions {
11
+ /** Model to pass as --model to the CLI. */
12
+ model?: string;
13
+ /** Override path to the claude binary (defaults to 'claude' on PATH). */
14
+ commandPath?: string;
15
+ /** Request timeout in milliseconds (default 120 000). */
16
+ timeout?: number;
17
+ /** Additional CLI flags passed to every invocation. */
18
+ extraFlags?: string[];
19
+ }
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Helpers
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /** Strip ANSI escape codes from CLI output. */
26
+ function stripAnsi(text: string): string {
27
+ // eslint-disable-next-line no-control-regex
28
+ return text.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
29
+ }
30
+
31
+ /**
32
+ * Spawn the Claude CLI with a prompt on stdin and capture stdout.
33
+ * Returns the raw text output (ANSI-stripped).
34
+ */
35
+ function runClaude(
36
+ prompt: string,
37
+ options: ClaudeCodeBuilderOptions
38
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
39
+ const cmd = options.commandPath ?? 'claude';
40
+ const args: string[] = ['-p', '--output-format', 'text'];
41
+ if (options.model) {
42
+ args.push('--model', options.model);
43
+ }
44
+ if (options.extraFlags) {
45
+ args.push(...options.extraFlags);
46
+ }
47
+
48
+ const timeout = options.timeout ?? 120_000;
49
+
50
+ return new Promise((resolve, reject) => {
51
+ const child = spawn(cmd, args, {
52
+ stdio: ['pipe', 'pipe', 'pipe'],
53
+ env: {
54
+ ...process.env,
55
+ FORCE_COLOR: '0', // suppress ANSI in captured output
56
+ CLAUDECODE: undefined, // prevent recursive detection
57
+ },
58
+ });
59
+
60
+ let stdout = '';
61
+ let stderr = '';
62
+
63
+ child.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString(); });
64
+ child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString(); });
65
+
66
+ // EOF protection — swallow EPIPE if the child closes stdin early
67
+ child.stdin.on('error', (err: NodeJS.ErrnoException) => {
68
+ if (err.code !== 'EPIPE' && err.code !== 'EOF') throw err;
69
+ });
70
+
71
+ // Timeout handling: SIGTERM → 5 s → SIGKILL
72
+ const timer = setTimeout(() => {
73
+ child.kill('SIGTERM');
74
+ setTimeout(() => {
75
+ if (!child.killed) child.kill('SIGKILL');
76
+ }, 5_000);
77
+ }, timeout);
78
+
79
+ child.on('close', (code) => {
80
+ clearTimeout(timer);
81
+ resolve({
82
+ stdout: stripAnsi(stdout),
83
+ stderr: stripAnsi(stderr),
84
+ exitCode: code ?? 1,
85
+ });
86
+ });
87
+
88
+ child.on('error', (err) => {
89
+ clearTimeout(timer);
90
+ reject(err);
91
+ });
92
+
93
+ // Pipe the prompt via stdin
94
+ child.stdin.write(prompt, () => {
95
+ child.stdin.end();
96
+ });
97
+ });
98
+ }
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Spawn + retry wrapper
102
+ // ---------------------------------------------------------------------------
103
+
104
+ /**
105
+ * Execute the Claude CLI and return trimmed output.
106
+ * Retries once on empty output with a simplified prompt prefix.
107
+ */
108
+ async function executeWithRetry(
109
+ prompt: string,
110
+ options: ClaudeCodeBuilderOptions,
111
+ ): Promise<string> {
112
+ const attempt = async (p: string): Promise<string> => {
113
+ const result = await runClaude(p, options);
114
+
115
+ // Handle process-level failures
116
+ if (result.exitCode !== 0 && !result.stdout.trim()) {
117
+ if (result.stderr.includes('ENOENT') || result.stderr.includes('not found') || result.stderr.includes('not recognized')) {
118
+ throw new Error(
119
+ 'Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code'
120
+ );
121
+ }
122
+ throw new Error(result.stderr || `Claude CLI exited with code ${result.exitCode}`);
123
+ }
124
+
125
+ return result.stdout.trim();
126
+ };
127
+
128
+ // First attempt
129
+ const output = await attempt(prompt);
130
+ if (output) return output;
131
+
132
+ // Retry once with a nudge prepended to the prompt
133
+ console.warn('[ClaudeCode] Empty response — retrying once');
134
+ const retryOutput = await attempt(
135
+ 'IMPORTANT: You must produce output. Do not return an empty response.\n\n' + prompt
136
+ );
137
+ if (retryOutput) return retryOutput;
138
+
139
+ throw new Error('Claude CLI returned empty output after retry');
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Change-op validation
144
+ // ---------------------------------------------------------------------------
145
+
146
+ const VALID_OPS = new Set(['update', 'replace', 'delete', 'insert', 'style-element', 'search-replace', 'search-insert']);
147
+
148
+ /**
149
+ * Validate a single change operation against the expected schema.
150
+ * Returns true if the op is structurally valid, false otherwise.
151
+ */
152
+ function isValidChangeOp(op: unknown): op is ChangeOp {
153
+ if (!op || typeof op !== 'object') return false;
154
+ const o = op as Record<string, unknown>;
155
+ if (typeof o.op !== 'string' || !VALID_OPS.has(o.op)) return false;
156
+
157
+ switch (o.op) {
158
+ case 'update':
159
+ case 'replace':
160
+ return typeof o.nodeId === 'string' && typeof o.html === 'string';
161
+ case 'delete':
162
+ return typeof o.nodeId === 'string';
163
+ case 'insert':
164
+ return typeof o.parentId === 'string'
165
+ && typeof o.position === 'string'
166
+ && typeof o.html === 'string';
167
+ case 'style-element':
168
+ return typeof o.nodeId === 'string' && typeof o.style === 'string';
169
+ case 'search-replace':
170
+ return typeof o.nodeId === 'string'
171
+ && typeof o.search === 'string'
172
+ && typeof o.replace === 'string';
173
+ case 'search-insert':
174
+ return typeof o.nodeId === 'string'
175
+ && typeof o.after === 'string'
176
+ && typeof o.content === 'string';
177
+ default:
178
+ return false;
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Filter a list of change ops, keeping only valid ones.
184
+ * Logs warnings for any skipped ops.
185
+ */
186
+ function filterValidOps(ops: unknown[]): ChangeOp[] {
187
+ const valid: ChangeOp[] = [];
188
+ for (let i = 0; i < ops.length; i++) {
189
+ const op = ops[i];
190
+ if (isValidChangeOp(op)) {
191
+ valid.push(op);
192
+ } else {
193
+ console.warn(`[ClaudeCode] Skipping invalid change op at index ${i}:`, JSON.stringify(ops[i]));
194
+ }
195
+ }
196
+ return valid;
197
+ }
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // Chat mode heuristic
201
+ // ---------------------------------------------------------------------------
202
+
203
+ /**
204
+ * Simple heuristic to detect chat/question messages.
205
+ * Per spec: message ends with `?` and is under 100 characters.
206
+ */
207
+ function isChatQuestion(message: string): boolean {
208
+ const trimmed = message.trim();
209
+ return trimmed.length < 100 && trimmed.endsWith('?');
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Builder factory
214
+ // ---------------------------------------------------------------------------
215
+
216
+ export function createClaudeCodeBuilder(
217
+ userInstructions?: string,
218
+ productName?: string,
219
+ options?: ClaudeCodeBuilderOptions,
220
+ ): Builder {
221
+ const name = productName ?? 'SynthOS';
222
+ const opts: ClaudeCodeBuilderOptions = options ?? {};
223
+
224
+ return {
225
+ async run(currentPage, additionalSections, userMessage, newBuild, _attachments?): Promise<BuilderResult> {
226
+ try {
227
+ const chatMode = !newBuild && isChatQuestion(userMessage);
228
+
229
+ let prompt: string;
230
+
231
+ if (chatMode) {
232
+ // -- Chat prompt: no change-ops, no node annotations --
233
+ const contextParts: string[] = [
234
+ `<CURRENT_PAGE>\n${currentPage.content}\n</CURRENT_PAGE>`,
235
+ ];
236
+ for (const section of additionalSections) {
237
+ if (section.content) {
238
+ contextParts.push(`${section.title}\n${section.content}`);
239
+ }
240
+ }
241
+
242
+ prompt = `You are ${name}, a helpful assistant. Answer the user's question about their page.\n\n`
243
+ + contextParts.join('\n\n')
244
+ + `\n\n<USER_MESSAGE>\n${userMessage}\n</USER_MESSAGE>`;
245
+ } else {
246
+ // -- Build/change prompt (same structure as before) --
247
+ const systemParts: string[] = [
248
+ `${currentPage.title}\n${currentPage.content}`,
249
+ ];
250
+ for (const section of additionalSections) {
251
+ if (section.content) {
252
+ systemParts.push(`${section.title}\n${section.content}`);
253
+ }
254
+ }
255
+ const systemContent = systemParts.join('\n\n');
256
+
257
+ const instructionParts: string[] = [];
258
+ if (userInstructions?.trim()) {
259
+ instructionParts.push(userInstructions);
260
+ }
261
+ for (const section of additionalSections) {
262
+ if (section.instructions?.trim()) {
263
+ instructionParts.push(section.instructions);
264
+ }
265
+ }
266
+ instructionParts.push(getTransformInstr(name));
267
+ instructionParts.push(CHANGE_OPS_FORMAT_INSTRUCTION);
268
+
269
+ const instructions = instructionParts.filter(s => s.trim() !== '').join('\n');
270
+
271
+ prompt = `${systemContent}\n\n<USER_MESSAGE>\n${userMessage}\n\n<INSTRUCTIONS>\n${instructions}`;
272
+ }
273
+
274
+ // -- Spawn claude CLI (with retry on empty output) --
275
+ const output = await executeWithRetry(prompt, opts);
276
+
277
+ // Chat mode always returns a reply — skip change-ops parsing
278
+ if (chatMode) {
279
+ return { kind: 'reply', text: output };
280
+ }
281
+
282
+ // Try to parse as builder response (JSON change ops or reply)
283
+ const parsed = parseBuilderResponse(output);
284
+
285
+ // Validate individual change ops — skip invalid ones
286
+ if (parsed.kind === 'transforms') {
287
+ const validated = filterValidOps(parsed.changes);
288
+ if (validated.length === 0) {
289
+ // All ops were invalid — treat the raw output as a text reply
290
+ return { kind: 'reply', text: output };
291
+ }
292
+ return { kind: 'transforms', changes: validated };
293
+ }
294
+
295
+ return parsed;
296
+ } catch (err: unknown) {
297
+ // Spawn failure (e.g. ENOENT when claude is not on PATH)
298
+ if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT') {
299
+ return {
300
+ kind: 'error',
301
+ error: new Error(
302
+ 'Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code'
303
+ ),
304
+ };
305
+ }
306
+ return { kind: 'error', error: err instanceof Error ? err : new Error(String(err)) };
307
+ }
308
+ }
309
+ };
310
+ }
@@ -2,12 +2,14 @@ import { ProviderName, completePrompt } from '../models';
2
2
  import { createAnthropicBuilder, AnthropicBuilderOptions } from './anthropic';
3
3
  import { createOpenAIBuilder } from './openai';
4
4
  import { createFireworksAIBuilder } from './fireworksai';
5
+ import { createClaudeCodeBuilder } from './claudecode';
5
6
  import { Builder } from './types';
6
7
 
7
- export { ContextSection, BuilderResult, Builder, Attachment } from './types';
8
+ export { ContextSection, SectionMode, BuilderResult, Builder, Attachment } from './types';
8
9
  export { createAnthropicBuilder, AnthropicBuilderOptions } from './anthropic';
9
10
  export { createOpenAIBuilder } from './openai';
10
11
  export { createFireworksAIBuilder } from './fireworksai';
12
+ export { createClaudeCodeBuilder, ClaudeCodeBuilderOptions } from './claudecode';
11
13
  export { parseBuilderResponse } from './anthropic';
12
14
 
13
15
  /**
@@ -27,6 +29,10 @@ export function createBuilder(
27
29
  return createOpenAIBuilder(complete, userInstructions, productName);
28
30
  case 'FireworksAI':
29
31
  return createFireworksAIBuilder(complete, userInstructions, productName);
32
+ case 'ClaudeCode':
33
+ return createClaudeCodeBuilder(userInstructions, productName, {
34
+ model: options?.model,
35
+ });
30
36
  default:
31
37
  throw new Error(`Unknown provider: ${provider}`);
32
38
  }
@@ -80,7 +80,8 @@ function parseOpenAIResponse(raw: string): BuilderResult {
80
80
  try {
81
81
  const parsed = JSON.parse(raw);
82
82
  if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && Array.isArray(parsed.changes)) {
83
- return { kind: 'transforms', changes: parsed.changes };
83
+ const message = typeof parsed.message === 'string' ? parsed.message : undefined;
84
+ return { kind: 'transforms', changes: parsed.changes, message };
84
85
  }
85
86
  } catch {
86
87
  // fall through to shared parser
@@ -1,4 +1,5 @@
1
1
  import { ChangeList } from '../service/transformPage';
2
+ import { ToolDefinition, ToolHandler } from '../models/types';
2
3
 
3
4
  // ---------------------------------------------------------------------------
4
5
  // Change operations output format — text instruction for non-structured builders
@@ -8,28 +9,31 @@ import { ChangeList } from '../service/transformPage';
8
9
  * Text instruction that tells the model to return a JSON array of change operations.
9
10
  * Append this to <INSTRUCTIONS> for builders that don't support structured outputs.
10
11
  */
11
- export const CHANGE_OPS_FORMAT_INSTRUCTION = `Return a JSON array of change operations to apply to the page. Do NOT return the full HTML page.
12
+ export const CHANGE_OPS_FORMAT_INSTRUCTION = `Return a JSON object with two fields: "message" and "changes".
12
13
 
13
- Each operation must be one of:
14
- { "op": "update", "nodeId": "<data-node-id>", "html": "<new innerHTML>" }
14
+ "message" is a brief (1 sentence) message to the user explaining what you changed. It is shown in the chat feed as the assistant's reply — write it directly to the user, not a description of the JSON.
15
+ "changes" is an array of change operations to apply to the page. Do NOT return the full HTML page.
16
+
17
+ Each change operation must be one of:
18
+ { "op": "update", "nodeId": "<data-nid>", "html": "<new innerHTML>" }
15
19
  — replaces the innerHTML of the target element
16
20
 
17
- { "op": "replace", "nodeId": "<data-node-id>", "html": "<new outerHTML>" }
21
+ { "op": "replace", "nodeId": "<data-nid>", "html": "<new outerHTML>" }
18
22
  — replaces the entire element (outerHTML) with new markup
19
23
 
20
- { "op": "delete", "nodeId": "<data-node-id>" }
24
+ { "op": "delete", "nodeId": "<data-nid>" }
21
25
  — removes the element from the page
22
26
 
23
- { "op": "insert", "parentId": "<data-node-id>", "position": "prepend"|"append"|"before"|"after", "html": "<new element HTML>" }
27
+ { "op": "insert", "parentId": "<data-nid>", "position": "prepend"|"append"|"before"|"after", "html": "<new element HTML>" }
24
28
  — inserts new HTML relative to the parent element
25
29
 
26
- { "op": "style-element", "nodeId": "<data-node-id>", "style": "<css style string>" }
30
+ { "op": "style-element", "nodeId": "<data-nid>", "style": "<css style string>" }
27
31
  — sets the style attribute of the target element (must be unlocked)
28
32
 
29
- { "op": "search-replace", "nodeId": "<data-node-id>", "search": "<exact text to find>", "replace": "<replacement text>" }
33
+ { "op": "search-replace", "nodeId": "<data-nid>", "search": "<exact text to find>", "replace": "<replacement text>" }
30
34
  — finds exact text within a script/style block and replaces it. Use empty string for replace to delete.
31
35
 
32
- { "op": "search-insert", "nodeId": "<data-node-id>", "after": "<exact text to find>", "content": "<new lines to insert>" }
36
+ { "op": "search-insert", "nodeId": "<data-nid>", "after": "<exact text to find>", "content": "<new lines to insert>" }
33
37
  — inserts new content immediately after the matched text in a script/style block.
34
38
 
35
39
  For partial edits to large scripts/styles, use search-replace or search-insert instead of
@@ -38,21 +42,26 @@ Copy the search/after text exactly as it appears in the source.
38
42
  When making multiple edits to the same block, ensure each search string targets distinct text.
39
43
  To delete code, use search-replace with an empty replace string.
40
44
 
41
- Return ONLY the JSON array. Example:
42
- [
43
- { "op": "update", "nodeId": "5", "html": "<p>Hello world</p>" },
44
- { "op": "insert", "parentId": "3", "position": "append", "html": "<div class=\\"msg\\">New message</div>" }
45
- ]`;
45
+ Return ONLY the JSON object. Example:
46
+ {
47
+ "message": "Changed the heading and added a new notice.",
48
+ "changes": [
49
+ { "op": "update", "nodeId": "5", "html": "<p>Hello world</p>" },
50
+ { "op": "insert", "parentId": "3", "position": "append", "html": "<div class=\\"msg\\">New message</div>" }
51
+ ]
52
+ }`;
46
53
 
47
54
  // ---------------------------------------------------------------------------
48
55
  // Change operations JSON schema — for structured output (constrained decoding)
49
56
  // ---------------------------------------------------------------------------
50
57
 
51
- /**
52
- * JSON schema matching the ChangeOp union type for Anthropic structured outputs.
53
- * The top-level schema is an array of change operations.
54
- */
55
- export const CHANGE_OPS_SCHEMA: Record<string, unknown> = {
58
+ const CHANGE_OP_MESSAGE_SCHEMA = {
59
+ type: 'string',
60
+ description: 'Brief (1 sentence) message to the user explaining the change. Written directly to the user; shown in the chat feed as the assistant reply.',
61
+ };
62
+
63
+ /** Array schema for the full change-op union (ranged writes allowed). */
64
+ const CHANGE_OPS_ARRAY_SCHEMA: Record<string, unknown> = {
56
65
  type: 'array',
57
66
  items: {
58
67
  anyOf: [
@@ -132,12 +141,8 @@ export const CHANGE_OPS_SCHEMA: Record<string, unknown> = {
132
141
  },
133
142
  };
134
143
 
135
- /**
136
- * Variant of CHANGE_OPS_SCHEMA that omits ranged write operations (search-replace, search-insert).
137
- * Used for medium/hard/re-write changes where the model should replace full blocks instead of
138
- * attempting partial edits.
139
- */
140
- export const CHANGE_OPS_SCHEMA_NO_RANGED: Record<string, unknown> = {
144
+ /** Array schema with ranged write ops omitted. */
145
+ const CHANGE_OPS_ARRAY_SCHEMA_NO_RANGED: Record<string, unknown> = {
141
146
  type: 'array',
142
147
  items: {
143
148
  anyOf: [
@@ -195,6 +200,36 @@ export const CHANGE_OPS_SCHEMA_NO_RANGED: Record<string, unknown> = {
195
200
  },
196
201
  };
197
202
 
203
+ /**
204
+ * JSON schema for Anthropic structured outputs.
205
+ * Wraps the change-ops array in `{ message, changes }` so the model always
206
+ * emits a user-facing message alongside its page changes.
207
+ */
208
+ export const CHANGE_OPS_SCHEMA: Record<string, unknown> = {
209
+ type: 'object',
210
+ properties: {
211
+ message: CHANGE_OP_MESSAGE_SCHEMA,
212
+ changes: CHANGE_OPS_ARRAY_SCHEMA,
213
+ },
214
+ required: ['message', 'changes'],
215
+ additionalProperties: false,
216
+ };
217
+
218
+ /**
219
+ * Variant of CHANGE_OPS_SCHEMA that omits ranged write operations (search-replace, search-insert).
220
+ * Used for medium/hard/re-write changes where the model should replace full blocks instead of
221
+ * attempting partial edits.
222
+ */
223
+ export const CHANGE_OPS_SCHEMA_NO_RANGED: Record<string, unknown> = {
224
+ type: 'object',
225
+ properties: {
226
+ message: CHANGE_OP_MESSAGE_SCHEMA,
227
+ changes: CHANGE_OPS_ARRAY_SCHEMA_NO_RANGED,
228
+ },
229
+ required: ['message', 'changes'],
230
+ additionalProperties: false,
231
+ };
232
+
198
233
  /**
199
234
  * Text instruction variant for no-ranged-writes mode.
200
235
  * Tells the model to use `update` for full innerHTML replacement instead of partial edits.
@@ -203,14 +238,15 @@ export const CHANGE_OPS_FORMAT_INSTRUCTION_NO_RANGED = `For script and style blo
203
238
 
204
239
  /**
205
240
  * OpenAI structured outputs require a root-level object.
206
- * This wraps CHANGE_OPS_SCHEMA in { changes: [...] }.
241
+ * Matches the Anthropic shape: { message, changes }.
207
242
  */
208
243
  export const OPENAI_CHANGE_OPS_SCHEMA: Record<string, unknown> = {
209
244
  type: 'object',
210
245
  properties: {
211
- changes: CHANGE_OPS_SCHEMA,
246
+ message: CHANGE_OP_MESSAGE_SCHEMA,
247
+ changes: CHANGE_OPS_ARRAY_SCHEMA,
212
248
  },
213
- required: ['changes'],
249
+ required: ['message', 'changes'],
214
250
  additionalProperties: false,
215
251
  };
216
252
 
@@ -218,11 +254,33 @@ export const OPENAI_CHANGE_OPS_SCHEMA: Record<string, unknown> = {
218
254
  // Context sections — structured blocks passed to the builder
219
255
  // ---------------------------------------------------------------------------
220
256
 
257
+ export type SectionMode =
258
+ /** Always rendered in full. Never sketched. Used for small/universally relevant sections. */
259
+ | 'always-full'
260
+ /** Sketched by default; rendered in full when the classifier expands it. */
261
+ | 'classifier-decides'
262
+ /** Skipped entirely when sketch === null; otherwise sketched/expanded like classifier-decides. */
263
+ | 'always-omit-when-empty';
264
+
221
265
  export interface ContextSection {
222
- /** Section title, e.g. "<CURRENT_PAGE>", "<SERVER_SCRIPTS>" */
266
+ /** Section title, e.g. "<CURRENT_PAGE>", "<SERVER_SCRIPTS>". Used as the classifier expand-set key. */
223
267
  title: string;
224
- /** The text body of this section */
268
+ /** Full render what the model sees when this section is expanded. */
225
269
  content: string;
270
+ /**
271
+ * Compact render — what the classifier sees, and what the model sees when this
272
+ * section is NOT expanded. `null` means "omit entirely from the prompt when not
273
+ * expanded" (used by always-omit-when-empty sections that have nothing to say,
274
+ * or by always-full sections where sketch is irrelevant).
275
+ */
276
+ sketch: string | null;
277
+ /** Default behavior for this section. Producer-declared. */
278
+ mode: SectionMode;
279
+ /**
280
+ * If true, this section is always rendered in full on the initial page build,
281
+ * regardless of classifier output. Defaults to false.
282
+ */
283
+ forceFullOnInitial?: boolean;
226
284
  /** How the model should work with this section (appended to instructions) */
227
285
  instructions: string;
228
286
  }
@@ -232,7 +290,7 @@ export interface ContextSection {
232
290
  // ---------------------------------------------------------------------------
233
291
 
234
292
  export type BuilderResult =
235
- | { kind: 'transforms'; changes: ChangeList }
293
+ | { kind: 'transforms'; changes: ChangeList; message?: string }
236
294
  | { kind: 'reply'; text: string }
237
295
  | { kind: 'error'; error: Error };
238
296
 
@@ -256,6 +314,9 @@ export interface Builder {
256
314
  additionalSections: ContextSection[],
257
315
  userMessage: string,
258
316
  newBuild: boolean,
259
- attachments?: Attachment[]
317
+ attachments?: Attachment[],
318
+ tools?: ToolDefinition[],
319
+ toolHandlers?: Record<string, ToolHandler>,
320
+ onToolCall?: (names: string[]) => void,
260
321
  ): Promise<BuilderResult>;
261
322
  }
@@ -30,6 +30,10 @@ export interface ConnectorDefinition {
30
30
  tokenUrl?: string;
31
31
  /** OAuth2: Requested scopes. */
32
32
  scopes?: string[];
33
+ /** OAuth2: Use PKCE (RFC 7636) on the authorization-code flow. Required by Buffer, Canva, etc. */
34
+ usePkce?: boolean;
35
+ /** OAuth2: Separator used to join scopes in the authorize request. Defaults to comma; RFC 6749 mandates space. */
36
+ scopeSeparator?: ' ' | ',';
33
37
  }
34
38
 
35
39
  export interface ConnectorJson {
@@ -46,6 +50,8 @@ export interface ConnectorJson {
46
50
  authorizationUrl?: string;
47
51
  tokenUrl?: string;
48
52
  scopes?: string[];
53
+ usePkce?: boolean;
54
+ scopeSeparator?: ' ' | ',';
49
55
  }
50
56
 
51
57
  export interface ConnectorConfig {
@@ -61,6 +67,8 @@ export interface ConnectorOAuthConfig extends ConnectorConfig {
61
67
  accountName?: string;
62
68
  clientId?: string;
63
69
  clientSecret?: string;
70
+ /** Stored between /authorize and /callback for PKCE-enabled connectors. Cleared after callback. */
71
+ codeVerifier?: string;
64
72
  }
65
73
 
66
74
  export type ConnectorsConfig = Record<string, ConnectorConfig | ConnectorOAuthConfig>;
package/src/init.ts CHANGED
@@ -38,6 +38,11 @@ export async function createConfig(
38
38
  customizer?.requiredPagesFolders ?? ['default'],
39
39
  path.join(__dirname, '../required-pages')
40
40
  );
41
+ // Starter pages (category "_Starters") ship in the package's starters/
42
+ // folder and act as a read-only fallback for the runtime: they are
43
+ // discoverable by listPages, loadPageMetadata, and loadPageWithFallback
44
+ // without being copied into .synthos/pages/.
45
+ requiredPagesFolders.push(path.join(__dirname, '../starters'));
41
46
  const requiredPages = await getRequiredPages(requiredPagesFolders);
42
47
  return {
43
48
  localFolder: pagesFolder,
@@ -107,10 +112,6 @@ export async function init(config: SynthOSConfig, includeDefaultPages: boolean =
107
112
 
108
113
  await sp.saveFile(path.join(scriptsFolder, 'example.sh'), `#!/bin/bash\n\n# This is an example script\n\n# You can run this script using the following command:\n# sh ${config.localFolder}/scripts/example.sh\n\n# This script will print "Hello, World!" to the console\n\necho "Hello, World!"\n`);
109
114
 
110
- // Create empty themes folder — default themes are served directly from
111
- // defaultThemesFolders; users can add custom themes here.
112
- await sp.ensureFolderExists(path.join(config.pagesFolder, 'themes'));
113
-
114
115
  // Copy pages
115
116
  if (includeDefaultPages) {
116
117
  console.log(`Copying default pages to ${config.localFolder} folder...`);
@@ -141,9 +142,14 @@ async function repairMissingFolders(config: SynthOSConfig): Promise<void> {
141
142
  await sp.saveFile(path.join(scriptsFolder, 'example.sh'), `#!/bin/bash\n\n# This is an example script\n\n# You can run this script using the following command:\n# sh ${config.localFolder}/scripts/example.sh\n\n# This script will print "Hello, World!" to the console\n\necho "Hello, World!"\n`);
142
143
  }
143
144
 
144
- // Ensure themes folder existsdefault themes are served directly from
145
- // defaultThemesFolders; this folder is for user-added custom themes only.
146
- await sp.ensureFolderExists(path.join(config.pagesFolder, 'themes'));
145
+ // User-local themes are no longer supported all themes are served from
146
+ // defaultThemesFolders. Remove any stale .synthos/themes/ folder left over
147
+ // from previous versions so it can't shadow default themes.
148
+ const staleThemesFolder = path.join(config.pagesFolder, 'themes');
149
+ if (await sp.checkIfExists(staleThemesFolder)) {
150
+ console.log(`Removing stale ${config.localFolder}/themes folder (themes now come from default-themes)...`);
151
+ await sp.deleteFolder(staleThemesFolder);
152
+ }
147
153
 
148
154
  // Ensure pages/ subfolder exists
149
155
  const pagesSubdir = path.join(config.pagesFolder, 'pages');