@agent-native/core 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (394) hide show
  1. package/README.md +31 -0
  2. package/dist/adapters/convex/adapter.d.ts +24 -0
  3. package/dist/adapters/convex/adapter.d.ts.map +1 -0
  4. package/dist/adapters/convex/adapter.js +125 -0
  5. package/dist/adapters/convex/adapter.js.map +1 -0
  6. package/dist/adapters/convex/index.d.ts +4 -0
  7. package/dist/adapters/convex/index.d.ts.map +1 -0
  8. package/dist/adapters/convex/index.js +3 -0
  9. package/dist/adapters/convex/index.js.map +1 -0
  10. package/dist/adapters/drizzle/adapter.d.ts +36 -0
  11. package/dist/adapters/drizzle/adapter.d.ts.map +1 -0
  12. package/dist/adapters/drizzle/adapter.js +210 -0
  13. package/dist/adapters/drizzle/adapter.js.map +1 -0
  14. package/dist/adapters/drizzle/index.d.ts +3 -0
  15. package/dist/adapters/drizzle/index.d.ts.map +1 -0
  16. package/dist/adapters/drizzle/index.js +3 -0
  17. package/dist/adapters/drizzle/index.js.map +1 -0
  18. package/dist/adapters/drizzle/schema.d.ts +146 -0
  19. package/dist/adapters/drizzle/schema.d.ts.map +1 -0
  20. package/dist/adapters/drizzle/schema.js +20 -0
  21. package/dist/adapters/drizzle/schema.js.map +1 -0
  22. package/dist/adapters/firestore/adapter.d.ts +3 -2
  23. package/dist/adapters/firestore/adapter.d.ts.map +1 -1
  24. package/dist/adapters/firestore/adapter.js +23 -6
  25. package/dist/adapters/firestore/adapter.js.map +1 -1
  26. package/dist/adapters/supabase/adapter.d.ts +2 -1
  27. package/dist/adapters/supabase/adapter.d.ts.map +1 -1
  28. package/dist/adapters/supabase/adapter.js +4 -1
  29. package/dist/adapters/supabase/adapter.js.map +1 -1
  30. package/dist/adapters/sync/config.d.ts +22 -2
  31. package/dist/adapters/sync/config.d.ts.map +1 -1
  32. package/dist/adapters/sync/config.js +175 -16
  33. package/dist/adapters/sync/config.js.map +1 -1
  34. package/dist/adapters/sync/create-file-sync.d.ts +32 -0
  35. package/dist/adapters/sync/create-file-sync.d.ts.map +1 -0
  36. package/dist/adapters/sync/create-file-sync.js +218 -0
  37. package/dist/adapters/sync/create-file-sync.js.map +1 -0
  38. package/dist/adapters/sync/file-sync.d.ts +40 -6
  39. package/dist/adapters/sync/file-sync.d.ts.map +1 -1
  40. package/dist/adapters/sync/file-sync.js +442 -97
  41. package/dist/adapters/sync/file-sync.js.map +1 -1
  42. package/dist/adapters/sync/index.d.ts +3 -2
  43. package/dist/adapters/sync/index.d.ts.map +1 -1
  44. package/dist/adapters/sync/index.js +3 -1
  45. package/dist/adapters/sync/index.js.map +1 -1
  46. package/dist/adapters/sync/merge.js +3 -2
  47. package/dist/adapters/sync/merge.js.map +1 -1
  48. package/dist/adapters/sync/types.d.ts +36 -2
  49. package/dist/adapters/sync/types.d.ts.map +1 -1
  50. package/dist/adapters/sync/types.js +22 -1
  51. package/dist/adapters/sync/types.js.map +1 -1
  52. package/dist/agent/index.d.ts +3 -0
  53. package/dist/agent/index.d.ts.map +1 -0
  54. package/dist/agent/index.js +2 -0
  55. package/dist/agent/index.js.map +1 -0
  56. package/dist/agent/production-agent.d.ts +16 -0
  57. package/dist/agent/production-agent.d.ts.map +1 -0
  58. package/dist/agent/production-agent.js +158 -0
  59. package/dist/agent/production-agent.js.map +1 -0
  60. package/dist/agent/types.d.ts +40 -0
  61. package/dist/agent/types.d.ts.map +1 -0
  62. package/dist/agent/types.js +2 -0
  63. package/dist/agent/types.js.map +1 -0
  64. package/dist/application-state/emitter.d.ts +10 -0
  65. package/dist/application-state/emitter.d.ts.map +1 -0
  66. package/dist/application-state/emitter.js +18 -0
  67. package/dist/application-state/emitter.js.map +1 -0
  68. package/dist/application-state/handlers.d.ts +20 -0
  69. package/dist/application-state/handlers.d.ts.map +1 -0
  70. package/dist/application-state/handlers.js +94 -0
  71. package/dist/application-state/handlers.js.map +1 -0
  72. package/dist/application-state/index.d.ts +5 -0
  73. package/dist/application-state/index.d.ts.map +1 -0
  74. package/dist/application-state/index.js +9 -0
  75. package/dist/application-state/index.js.map +1 -0
  76. package/dist/application-state/script-helpers.d.ts +17 -0
  77. package/dist/application-state/script-helpers.d.ts.map +1 -0
  78. package/dist/application-state/script-helpers.js +28 -0
  79. package/dist/application-state/script-helpers.js.map +1 -0
  80. package/dist/application-state/store.d.ts +9 -0
  81. package/dist/application-state/store.d.ts.map +1 -0
  82. package/dist/application-state/store.js +93 -0
  83. package/dist/application-state/store.js.map +1 -0
  84. package/dist/cli/create.d.ts.map +1 -1
  85. package/dist/cli/create.js +40 -24
  86. package/dist/cli/create.js.map +1 -1
  87. package/dist/cli/index.js +37 -11
  88. package/dist/cli/index.js.map +1 -1
  89. package/dist/client/AgentPanel.d.ts +53 -0
  90. package/dist/client/AgentPanel.d.ts.map +1 -0
  91. package/dist/client/AgentPanel.js +70 -0
  92. package/dist/client/AgentPanel.js.map +1 -0
  93. package/dist/client/AssistantChat.d.ts +27 -0
  94. package/dist/client/AssistantChat.d.ts.map +1 -0
  95. package/dist/client/AssistantChat.js +165 -0
  96. package/dist/client/AssistantChat.js.map +1 -0
  97. package/dist/client/ErrorBoundary.d.ts +4 -0
  98. package/dist/client/ErrorBoundary.d.ts.map +1 -0
  99. package/dist/client/ErrorBoundary.js +22 -0
  100. package/dist/client/ErrorBoundary.js.map +1 -0
  101. package/dist/client/MultiTabAssistantChat.d.ts +4 -0
  102. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -0
  103. package/dist/client/MultiTabAssistantChat.js +125 -0
  104. package/dist/client/MultiTabAssistantChat.js.map +1 -0
  105. package/dist/client/PoweredByBadge.d.ts +14 -0
  106. package/dist/client/PoweredByBadge.d.ts.map +1 -0
  107. package/dist/client/PoweredByBadge.js +60 -0
  108. package/dist/client/PoweredByBadge.js.map +1 -0
  109. package/dist/client/ProductionAgentPanel.d.ts +6 -0
  110. package/dist/client/ProductionAgentPanel.d.ts.map +1 -0
  111. package/dist/client/ProductionAgentPanel.js +6 -0
  112. package/dist/client/ProductionAgentPanel.js.map +1 -0
  113. package/dist/client/Turnstile.d.ts +35 -0
  114. package/dist/client/Turnstile.d.ts.map +1 -0
  115. package/dist/client/Turnstile.js +77 -0
  116. package/dist/client/Turnstile.js.map +1 -0
  117. package/dist/client/agent-chat-adapter.d.ts +10 -0
  118. package/dist/client/agent-chat-adapter.d.ts.map +1 -0
  119. package/dist/client/agent-chat-adapter.js +235 -0
  120. package/dist/client/agent-chat-adapter.js.map +1 -0
  121. package/dist/client/agent-chat.d.ts +9 -1
  122. package/dist/client/agent-chat.d.ts.map +1 -1
  123. package/dist/client/agent-chat.js +11 -1
  124. package/dist/client/agent-chat.js.map +1 -1
  125. package/dist/client/index.d.ts +13 -1
  126. package/dist/client/index.d.ts.map +1 -1
  127. package/dist/client/index.js +14 -1
  128. package/dist/client/index.js.map +1 -1
  129. package/dist/client/terminal/AgentTerminal.d.ts +35 -0
  130. package/dist/client/terminal/AgentTerminal.d.ts.map +1 -0
  131. package/dist/client/terminal/AgentTerminal.js +346 -0
  132. package/dist/client/terminal/AgentTerminal.js.map +1 -0
  133. package/dist/client/terminal/index.d.ts +7 -0
  134. package/dist/client/terminal/index.d.ts.map +1 -0
  135. package/dist/client/terminal/index.js +7 -0
  136. package/dist/client/terminal/index.js.map +1 -0
  137. package/dist/client/use-agent-chat.d.ts +1 -1
  138. package/dist/client/use-agent-chat.d.ts.map +1 -1
  139. package/dist/client/use-agent-chat.js +1 -1
  140. package/dist/client/use-agent-chat.js.map +1 -1
  141. package/dist/client/use-file-sync-status.d.ts +21 -0
  142. package/dist/client/use-file-sync-status.d.ts.map +1 -0
  143. package/dist/client/use-file-sync-status.js +65 -0
  144. package/dist/client/use-file-sync-status.js.map +1 -0
  145. package/dist/client/use-session.d.ts +16 -0
  146. package/dist/client/use-session.d.ts.map +1 -0
  147. package/dist/client/use-session.js +49 -0
  148. package/dist/client/use-session.js.map +1 -0
  149. package/dist/client/useProductionAgent.d.ts +23 -0
  150. package/dist/client/useProductionAgent.d.ts.map +1 -0
  151. package/dist/client/useProductionAgent.js +137 -0
  152. package/dist/client/useProductionAgent.js.map +1 -0
  153. package/dist/db/create-get-db.d.ts +3 -0
  154. package/dist/db/create-get-db.d.ts.map +1 -0
  155. package/dist/db/create-get-db.js +18 -0
  156. package/dist/db/create-get-db.js.map +1 -0
  157. package/dist/db/index.d.ts +23 -0
  158. package/dist/db/index.d.ts.map +1 -0
  159. package/dist/db/index.js +19 -0
  160. package/dist/db/index.js.map +1 -0
  161. package/dist/db/migrations.d.ts +7 -0
  162. package/dist/db/migrations.d.ts.map +1 -0
  163. package/dist/db/migrations.js +35 -0
  164. package/dist/db/migrations.js.map +1 -0
  165. package/dist/index.browser.d.ts +1 -1
  166. package/dist/index.browser.d.ts.map +1 -1
  167. package/dist/index.browser.js +1 -1
  168. package/dist/index.browser.js.map +1 -1
  169. package/dist/index.d.ts +5 -2
  170. package/dist/index.d.ts.map +1 -1
  171. package/dist/index.js +6 -2
  172. package/dist/index.js.map +1 -1
  173. package/dist/oauth-tokens/index.d.ts +2 -0
  174. package/dist/oauth-tokens/index.d.ts.map +1 -0
  175. package/dist/oauth-tokens/index.js +2 -0
  176. package/dist/oauth-tokens/index.js.map +1 -0
  177. package/dist/oauth-tokens/store.d.ts +9 -0
  178. package/dist/oauth-tokens/store.d.ts.map +1 -0
  179. package/dist/oauth-tokens/store.js +90 -0
  180. package/dist/oauth-tokens/store.js.map +1 -0
  181. package/dist/router/index.d.ts +3 -0
  182. package/dist/router/index.d.ts.map +1 -0
  183. package/dist/router/index.js +5 -0
  184. package/dist/router/index.js.map +1 -0
  185. package/dist/scripts/core-scripts.d.ts +10 -0
  186. package/dist/scripts/core-scripts.d.ts.map +1 -0
  187. package/dist/scripts/core-scripts.js +15 -0
  188. package/dist/scripts/core-scripts.js.map +1 -0
  189. package/dist/scripts/db/exec.d.ts +11 -0
  190. package/dist/scripts/db/exec.d.ts.map +1 -0
  191. package/dist/scripts/db/exec.js +101 -0
  192. package/dist/scripts/db/exec.js.map +1 -0
  193. package/dist/scripts/db/index.d.ts +2 -0
  194. package/dist/scripts/db/index.d.ts.map +1 -0
  195. package/dist/scripts/db/index.js +6 -0
  196. package/dist/scripts/db/index.js.map +1 -0
  197. package/dist/scripts/db/query.d.ts +10 -0
  198. package/dist/scripts/db/query.d.ts.map +1 -0
  199. package/dist/scripts/db/query.js +112 -0
  200. package/dist/scripts/db/query.js.map +1 -0
  201. package/dist/scripts/db/schema.d.ts +12 -0
  202. package/dist/scripts/db/schema.d.ts.map +1 -0
  203. package/dist/scripts/db/schema.js +140 -0
  204. package/dist/scripts/db/schema.js.map +1 -0
  205. package/dist/scripts/dev/index.d.ts +15 -0
  206. package/dist/scripts/dev/index.d.ts.map +1 -0
  207. package/dist/scripts/dev/index.js +118 -0
  208. package/dist/scripts/dev/index.js.map +1 -0
  209. package/dist/scripts/dev/list-files.d.ts +5 -0
  210. package/dist/scripts/dev/list-files.d.ts.map +1 -0
  211. package/dist/scripts/dev/list-files.js +102 -0
  212. package/dist/scripts/dev/list-files.js.map +1 -0
  213. package/dist/scripts/dev/read-file.d.ts +5 -0
  214. package/dist/scripts/dev/read-file.d.ts.map +1 -0
  215. package/dist/scripts/dev/read-file.js +68 -0
  216. package/dist/scripts/dev/read-file.js.map +1 -0
  217. package/dist/scripts/dev/search-files.d.ts +5 -0
  218. package/dist/scripts/dev/search-files.d.ts.map +1 -0
  219. package/dist/scripts/dev/search-files.js +133 -0
  220. package/dist/scripts/dev/search-files.js.map +1 -0
  221. package/dist/scripts/dev/shell.d.ts +5 -0
  222. package/dist/scripts/dev/shell.d.ts.map +1 -0
  223. package/dist/scripts/dev/shell.js +65 -0
  224. package/dist/scripts/dev/shell.js.map +1 -0
  225. package/dist/scripts/dev/write-file.d.ts +5 -0
  226. package/dist/scripts/dev/write-file.d.ts.map +1 -0
  227. package/dist/scripts/dev/write-file.js +50 -0
  228. package/dist/scripts/dev/write-file.js.map +1 -0
  229. package/dist/scripts/index.d.ts +5 -0
  230. package/dist/scripts/index.d.ts.map +1 -1
  231. package/dist/scripts/index.js +5 -0
  232. package/dist/scripts/index.js.map +1 -1
  233. package/dist/scripts/runner.d.ts +3 -0
  234. package/dist/scripts/runner.d.ts.map +1 -1
  235. package/dist/scripts/runner.js +53 -14
  236. package/dist/scripts/runner.js.map +1 -1
  237. package/dist/server/agent-chat-plugin.d.ts +43 -0
  238. package/dist/server/agent-chat-plugin.d.ts.map +1 -0
  239. package/dist/server/agent-chat-plugin.js +92 -0
  240. package/dist/server/agent-chat-plugin.js.map +1 -0
  241. package/dist/server/auth-plugin.d.ts +6 -0
  242. package/dist/server/auth-plugin.d.ts.map +1 -0
  243. package/dist/server/auth-plugin.js +8 -0
  244. package/dist/server/auth-plugin.js.map +1 -0
  245. package/dist/server/auth.d.ts +75 -0
  246. package/dist/server/auth.d.ts.map +1 -0
  247. package/dist/server/auth.js +499 -0
  248. package/dist/server/auth.js.map +1 -0
  249. package/dist/server/captcha.d.ts +12 -0
  250. package/dist/server/captcha.d.ts.map +1 -0
  251. package/dist/server/captcha.js +43 -0
  252. package/dist/server/captcha.js.map +1 -0
  253. package/dist/server/create-server.d.ts +20 -10
  254. package/dist/server/create-server.d.ts.map +1 -1
  255. package/dist/server/create-server.js +48 -28
  256. package/dist/server/create-server.js.map +1 -1
  257. package/dist/server/default-watcher.d.ts +17 -0
  258. package/dist/server/default-watcher.d.ts.map +1 -0
  259. package/dist/server/default-watcher.js +37 -0
  260. package/dist/server/default-watcher.js.map +1 -0
  261. package/dist/server/file-sync-plugin.d.ts +7 -0
  262. package/dist/server/file-sync-plugin.d.ts.map +1 -0
  263. package/dist/server/file-sync-plugin.js +38 -0
  264. package/dist/server/file-sync-plugin.js.map +1 -0
  265. package/dist/server/google-auth-plugin.d.ts +22 -0
  266. package/dist/server/google-auth-plugin.d.ts.map +1 -0
  267. package/dist/server/google-auth-plugin.js +122 -0
  268. package/dist/server/google-auth-plugin.js.map +1 -0
  269. package/dist/server/index.d.ts +13 -2
  270. package/dist/server/index.d.ts.map +1 -1
  271. package/dist/server/index.js +14 -2
  272. package/dist/server/index.js.map +1 -1
  273. package/dist/server/missing-key.d.ts +9 -5
  274. package/dist/server/missing-key.d.ts.map +1 -1
  275. package/dist/server/missing-key.js +12 -7
  276. package/dist/server/missing-key.js.map +1 -1
  277. package/dist/server/sse.d.ts +18 -8
  278. package/dist/server/sse.d.ts.map +1 -1
  279. package/dist/server/sse.js +90 -19
  280. package/dist/server/sse.js.map +1 -1
  281. package/dist/settings/handlers.d.ts +9 -0
  282. package/dist/settings/handlers.d.ts.map +1 -0
  283. package/dist/settings/handlers.js +29 -0
  284. package/dist/settings/handlers.js.map +1 -0
  285. package/dist/settings/index.d.ts +5 -0
  286. package/dist/settings/index.d.ts.map +1 -0
  287. package/dist/settings/index.js +9 -0
  288. package/dist/settings/index.js.map +1 -0
  289. package/dist/settings/script-helpers.d.ts +9 -0
  290. package/dist/settings/script-helpers.d.ts.map +1 -0
  291. package/dist/settings/script-helpers.js +16 -0
  292. package/dist/settings/script-helpers.js.map +1 -0
  293. package/dist/settings/store.d.ts +7 -0
  294. package/dist/settings/store.d.ts.map +1 -0
  295. package/dist/settings/store.js +88 -0
  296. package/dist/settings/store.js.map +1 -0
  297. package/dist/settings/user-settings.d.ts +21 -0
  298. package/dist/settings/user-settings.d.ts.map +1 -0
  299. package/dist/settings/user-settings.js +35 -0
  300. package/dist/settings/user-settings.js.map +1 -0
  301. package/dist/tailwind.preset.js +1 -1
  302. package/dist/tailwind.preset.js.map +1 -1
  303. package/dist/terminal/cli-registry.d.ts +16 -0
  304. package/dist/terminal/cli-registry.d.ts.map +1 -0
  305. package/dist/terminal/cli-registry.js +42 -0
  306. package/dist/terminal/cli-registry.js.map +1 -0
  307. package/dist/terminal/index.d.ts +9 -0
  308. package/dist/terminal/index.d.ts.map +1 -0
  309. package/dist/terminal/index.js +9 -0
  310. package/dist/terminal/index.js.map +1 -0
  311. package/dist/terminal/pty-server.d.ts +31 -0
  312. package/dist/terminal/pty-server.d.ts.map +1 -0
  313. package/dist/terminal/pty-server.js +252 -0
  314. package/dist/terminal/pty-server.js.map +1 -0
  315. package/dist/terminal/terminal-plugin.d.ts +23 -0
  316. package/dist/terminal/terminal-plugin.d.ts.map +1 -0
  317. package/dist/terminal/terminal-plugin.js +90 -0
  318. package/dist/terminal/terminal-plugin.js.map +1 -0
  319. package/dist/vite/client.d.ts +28 -5
  320. package/dist/vite/client.d.ts.map +1 -1
  321. package/dist/vite/client.js +55 -16
  322. package/dist/vite/client.js.map +1 -1
  323. package/dist/vite/dev-api-server.d.ts +10 -0
  324. package/dist/vite/dev-api-server.d.ts.map +1 -0
  325. package/dist/vite/dev-api-server.js +160 -0
  326. package/dist/vite/dev-api-server.js.map +1 -0
  327. package/dist/vite/index.d.ts +2 -3
  328. package/dist/vite/index.d.ts.map +1 -1
  329. package/dist/vite/index.js +2 -3
  330. package/dist/vite/index.js.map +1 -1
  331. package/package.json +74 -19
  332. package/src/templates/default/.agents/skills/create-skill/SKILL.md +1 -1
  333. package/src/templates/default/.agents/skills/delegate-to-agent/SKILL.md +3 -3
  334. package/src/templates/default/.agents/skills/files-as-database/SKILL.md +82 -63
  335. package/src/templates/default/.agents/skills/frontend-design/SKILL.md +2 -2
  336. package/src/templates/default/.agents/skills/scripts/SKILL.md +20 -17
  337. package/src/templates/default/.agents/skills/self-modifying-code/SKILL.md +2 -2
  338. package/src/templates/default/.agents/skills/sse-file-watcher/SKILL.md +49 -64
  339. package/src/templates/default/.ignore +0 -0
  340. package/src/templates/default/AGENTS.md +129 -28
  341. package/src/templates/default/_gitignore +9 -5
  342. package/src/templates/default/app/entry.client.tsx +4 -0
  343. package/src/templates/default/app/entry.server.tsx +55 -0
  344. package/src/templates/default/app/global.css +95 -0
  345. package/src/templates/default/app/root.tsx +95 -0
  346. package/src/templates/default/app/routes/_index.tsx +62 -0
  347. package/src/templates/default/app/routes.ts +4 -0
  348. package/src/templates/default/app/vite-env.d.ts +6 -0
  349. package/src/templates/default/application-state/.gitkeep +0 -0
  350. package/src/templates/default/components.json +1 -1
  351. package/src/templates/default/data/sync-config.json +1 -0
  352. package/src/templates/default/package.json +7 -8
  353. package/src/templates/default/react-router.config.ts +6 -0
  354. package/src/templates/default/scripts/run.ts +1 -8
  355. package/src/templates/default/server/plugins/agent-chat.ts +1 -0
  356. package/src/templates/default/server/plugins/auth.ts +1 -0
  357. package/src/templates/default/server/plugins/file-sync.ts +1 -0
  358. package/src/templates/default/server/plugins/terminal.ts +1 -0
  359. package/src/templates/default/server/routes/[...page].get.ts +12 -0
  360. package/src/templates/default/server/routes/api/events.get.ts +3 -0
  361. package/src/templates/default/server/routes/api/file-sync/status.get.ts +4 -0
  362. package/src/templates/default/server/routes/api/hello.get.ts +5 -0
  363. package/src/templates/default/tailwind.config.ts +1 -1
  364. package/src/templates/default/tsconfig.json +9 -1
  365. package/src/templates/default/vite.config.ts +4 -1
  366. package/tsconfig.base.json +5 -3
  367. package/dist/adapters/neon/adapter.d.ts +0 -28
  368. package/dist/adapters/neon/adapter.d.ts.map +0 -1
  369. package/dist/adapters/neon/adapter.js +0 -135
  370. package/dist/adapters/neon/adapter.js.map +0 -1
  371. package/dist/adapters/neon/index.d.ts +0 -3
  372. package/dist/adapters/neon/index.d.ts.map +0 -1
  373. package/dist/adapters/neon/index.js +0 -3
  374. package/dist/adapters/neon/index.js.map +0 -1
  375. package/dist/server/production.d.ts +0 -18
  376. package/dist/server/production.d.ts.map +0 -1
  377. package/dist/server/production.js +0 -37
  378. package/dist/server/production.js.map +0 -1
  379. package/dist/vite/express-plugin.d.ts +0 -14
  380. package/dist/vite/express-plugin.d.ts.map +0 -1
  381. package/dist/vite/express-plugin.js +0 -53
  382. package/dist/vite/express-plugin.js.map +0 -1
  383. package/dist/vite/server.d.ts +0 -21
  384. package/dist/vite/server.d.ts.map +0 -1
  385. package/dist/vite/server.js +0 -68
  386. package/dist/vite/server.js.map +0 -1
  387. package/src/templates/default/client/App.tsx +0 -56
  388. package/src/templates/default/client/global.css +0 -75
  389. package/src/templates/default/client/vite-env.d.ts +0 -1
  390. package/src/templates/default/index.html +0 -14
  391. package/src/templates/default/server/index.ts +0 -22
  392. package/src/templates/default/server/node-build.ts +0 -4
  393. package/src/templates/default/vite.config.server.ts +0 -3
  394. /package/src/templates/default/{client → app}/lib/utils.ts +0 -0
@@ -1,24 +1,51 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { EventEmitter } from "events";
4
3
  import { watch } from "chokidar";
5
- import { shouldSyncFile, getDocId, loadSyncConfig } from "./config.js";
4
+ import pLimit from "p-limit";
5
+ import { shouldSyncFile, getDocId, loadSyncConfig, hashContent, assertSafePath, assertNotSymlink, validateIdentifier, } from "./config.js";
6
6
  import { threeWayMerge } from "./merge.js";
7
+ import { TypedEventEmitter, } from "./types.js";
7
8
  // ---------------------------------------------------------------------------
8
9
  // Core sync implementation
9
10
  // ---------------------------------------------------------------------------
10
- const TTL_MS = 3000;
11
+ const TTL_MS = 5000;
12
+ const MAX_RETRY_QUEUE = 100;
13
+ const MAX_MERGE_BASES = 50;
14
+ const MERGE_BASE_SIZE_LIMIT = 50 * 1024; // 50 KB
11
15
  export class FileSync {
12
16
  options;
13
- recentlyPulled = new Map();
17
+ // -- State tracking --------------------------------------------------------
18
+ lastSyncedHash = new Map();
19
+ mergeBaseCache = new Map();
14
20
  recentlyPushed = new Map();
15
- lastSyncedContent = new Map();
21
+ expectedWrites = new Set();
22
+ pushInFlight = new Map();
16
23
  sharedSyncInitialized = false;
17
24
  privateSyncInitialized = false;
25
+ // -- Retry queue -----------------------------------------------------------
26
+ retryQueue = new Map();
27
+ retryTimer = null;
28
+ flushing = false;
29
+ // -- Lifecycle -------------------------------------------------------------
30
+ abortController = new AbortController();
31
+ stopped = false;
32
+ watchers = [];
33
+ unsubscribeRemote = [];
18
34
  purgeTimer = null;
19
- syncEvents = new EventEmitter();
35
+ // -- Sync status -----------------------------------------------------------
36
+ hasError = false;
37
+ lastSyncTimestamp = null;
38
+ conflictPaths = new Set();
39
+ // -- Public ----------------------------------------------------------------
40
+ syncEvents = new TypedEventEmitter();
41
+ get conflictCount() {
42
+ return this.conflictPaths.size;
43
+ }
20
44
  constructor(options) {
21
45
  this.options = options;
46
+ // Validate identifiers at construction time
47
+ validateIdentifier("appId", options.appId);
48
+ validateIdentifier("ownerId", options.ownerId);
22
49
  }
23
50
  // -- Public API -----------------------------------------------------------
24
51
  /**
@@ -28,7 +55,7 @@ export class FileSync {
28
55
  async initFileSync() {
29
56
  if (this.sharedSyncInitialized)
30
57
  return;
31
- this.sharedSyncInitialized = true;
58
+ // Do NOT set flag here — only on success (1e)
32
59
  const config = loadSyncConfig(this.options.syncConfigPath);
33
60
  const patterns = config.syncFilePatterns;
34
61
  if (patterns.length === 0) {
@@ -36,10 +63,38 @@ export class FileSync {
36
63
  return;
37
64
  }
38
65
  console.log(`[file-sync:shared] Initializing with ${patterns.length} pattern(s)`);
39
- this.startPurgeTimer();
40
- await this.initStartupSync(patterns, this.options.ownerId, "shared");
41
- this.startRemoteListener(patterns, this.options.ownerId, "shared");
42
- this.startFileWatcher(patterns, this.options.ownerId, "shared");
66
+ try {
67
+ this.startPurgeTimer();
68
+ await this.initStartupSync(patterns, this.options.ownerId, "shared");
69
+ if (this.stopped)
70
+ return;
71
+ this.startRemoteListener(patterns, this.options.ownerId, "shared");
72
+ this.startFileWatcher(patterns, this.options.ownerId, "shared");
73
+ this.sharedSyncInitialized = true; // only on success (1e)
74
+ this.writeSyncStatus();
75
+ }
76
+ catch (err) {
77
+ const msg = err instanceof Error ? err.message : String(err);
78
+ if (msg.includes("relation") && msg.includes("does not exist")) {
79
+ console.error(`[file-sync] Supabase table not found. Create it by running this SQL in your Supabase dashboard:\n\n` +
80
+ ` CREATE TABLE files (\n` +
81
+ ` id TEXT PRIMARY KEY,\n` +
82
+ ` path TEXT NOT NULL,\n` +
83
+ ` content TEXT NOT NULL,\n` +
84
+ ` app TEXT NOT NULL,\n` +
85
+ ` owner_id TEXT NOT NULL,\n` +
86
+ ` last_updated BIGINT NOT NULL,\n` +
87
+ ` created_at BIGINT\n` +
88
+ ` );\n` +
89
+ ` CREATE INDEX idx_files_app_owner ON files(app, owner_id);\n`);
90
+ }
91
+ else {
92
+ console.error("[file-sync] Init failed, will allow retry:", err);
93
+ }
94
+ this.hasError = true;
95
+ this.writeSyncStatus();
96
+ // flag stays false — next call retries
97
+ }
43
98
  }
44
99
  /**
45
100
  * Initialize the private sync channel using a per-user UID.
@@ -47,7 +102,9 @@ export class FileSync {
47
102
  async initPrivateSync(userUid) {
48
103
  if (this.privateSyncInitialized)
49
104
  return;
50
- this.privateSyncInitialized = true;
105
+ // Do NOT set flag here — only on success (1e)
106
+ // Validate userUid (1c — missed in pass 1)
107
+ validateIdentifier("userUid", userUid);
51
108
  const config = loadSyncConfig(this.options.syncConfigPath);
52
109
  const patterns = config.privateSyncFilePatterns;
53
110
  if (patterns.length === 0) {
@@ -55,13 +112,88 @@ export class FileSync {
55
112
  return;
56
113
  }
57
114
  console.log(`[file-sync:private] Initializing private sync for user ${userUid.slice(0, 8)}...`);
58
- await this.initStartupSync(patterns, userUid, "private");
59
- this.startRemoteListener(patterns, userUid, "private");
60
- this.startFileWatcher(patterns, userUid, "private");
115
+ try {
116
+ await this.initStartupSync(patterns, userUid, "private");
117
+ if (this.stopped)
118
+ return;
119
+ this.startRemoteListener(patterns, userUid, "private");
120
+ this.startFileWatcher(patterns, userUid, "private");
121
+ this.privateSyncInitialized = true; // only on success (1e)
122
+ }
123
+ catch (err) {
124
+ console.error("[file-sync:private] Init failed, will allow retry:", err);
125
+ // flag stays false — next call retries
126
+ }
127
+ }
128
+ /**
129
+ * Graceful shutdown. Cancels in-flight operations, drains retry queue,
130
+ * closes watchers, unsubscribes listeners, and disposes the adapter.
131
+ */
132
+ async stop() {
133
+ this.stopped = true;
134
+ // Clear timers
135
+ if (this.retryTimer) {
136
+ clearInterval(this.retryTimer);
137
+ this.retryTimer = null;
138
+ }
139
+ if (this.purgeTimer) {
140
+ clearInterval(this.purgeTimer);
141
+ this.purgeTimer = null;
142
+ }
143
+ // Final flush attempt with timeout BEFORE aborting (abort would skip the flush)
144
+ await Promise.race([
145
+ this.flushRetryQueue(),
146
+ new Promise((resolve) => setTimeout(resolve, 5000)),
147
+ ]);
148
+ if (this.retryQueue.size > 0) {
149
+ console.warn(`[file-sync] ${this.retryQueue.size} unsynced changes lost on shutdown`);
150
+ this.writeDeadLetterLog("shutdown");
151
+ }
152
+ // Drain in-flight pushes
153
+ const inFlightPromises = [...this.pushInFlight.values()];
154
+ if (inFlightPromises.length > 0) {
155
+ await Promise.race([
156
+ Promise.allSettled(inFlightPromises),
157
+ new Promise((resolve) => setTimeout(resolve, 5000)),
158
+ ]);
159
+ }
160
+ // Now abort — after graceful drain is complete
161
+ this.abortController.abort();
162
+ // Close watchers (chokidar v4: close() returns Promise)
163
+ for (const watcher of this.watchers) {
164
+ await watcher.close();
165
+ }
166
+ this.watchers = [];
167
+ // Unsubscribe remote listeners
168
+ for (const unsub of this.unsubscribeRemote) {
169
+ unsub();
170
+ }
171
+ this.unsubscribeRemote = [];
172
+ // Dispose adapter (release gRPC channels, WebSocket connections)
173
+ await this.options.adapter.dispose();
174
+ this.sharedSyncInitialized = false;
175
+ this.privateSyncInitialized = false;
176
+ }
177
+ /**
178
+ * Check if a file was recently written by the sync engine (echo suppression).
179
+ * Consumes the entry — can only return true once per write.
180
+ */
181
+ wasSyncPulled(relPath) {
182
+ if (this.expectedWrites.has(relPath)) {
183
+ this.expectedWrites.delete(relPath);
184
+ return true;
185
+ }
186
+ return false;
187
+ }
188
+ /**
189
+ * Get paths of currently unresolved conflicts.
190
+ */
191
+ getConflictPaths() {
192
+ return [...this.conflictPaths];
61
193
  }
62
194
  // -- Private helpers ------------------------------------------------------
63
195
  emitSyncEvent(event) {
64
- this.syncEvents.emit("sync", event);
196
+ this.syncEvents.emit("sync", { source: "sync", ...event });
65
197
  }
66
198
  markRecent(map, filePath) {
67
199
  map.set(filePath, Date.now());
@@ -77,12 +209,10 @@ export class FileSync {
77
209
  return true;
78
210
  }
79
211
  startPurgeTimer() {
212
+ if (this.purgeTimer)
213
+ return;
80
214
  this.purgeTimer = setInterval(() => {
81
215
  const now = Date.now();
82
- for (const [k, v] of this.recentlyPulled) {
83
- if (now - v > TTL_MS)
84
- this.recentlyPulled.delete(k);
85
- }
86
216
  for (const [k, v] of this.recentlyPushed) {
87
217
  if (now - v > TTL_MS)
88
218
  this.recentlyPushed.delete(k);
@@ -98,17 +228,123 @@ export class FileSync {
98
228
  }
99
229
  }
100
230
  writeSyncedFile(filePath, absPath, content) {
101
- this.markRecent(this.recentlyPulled, filePath);
231
+ const projectRoot = path.resolve(this.options.contentRoot, "..");
232
+ assertSafePath(projectRoot, filePath);
233
+ assertNotSymlink(absPath);
234
+ this.expectedWrites.add(filePath);
102
235
  fs.mkdirSync(path.dirname(absPath), { recursive: true });
103
236
  fs.writeFileSync(absPath, content, "utf-8");
104
- this.lastSyncedContent.set(filePath, content);
237
+ const hash = hashContent(content);
238
+ this.lastSyncedHash.set(filePath, hash);
239
+ this.updateMergeBase(filePath, content);
240
+ this.lastSyncTimestamp = Date.now();
105
241
  }
106
242
  docId(filePath) {
107
243
  return getDocId(this.options.appId, filePath);
108
244
  }
245
+ // -- Merge base cache (1i) ------------------------------------------------
246
+ updateMergeBase(relPath, content) {
247
+ // Skip caching for large files
248
+ if (content.length > MERGE_BASE_SIZE_LIMIT)
249
+ return;
250
+ // Simple LRU: evict oldest if at capacity
251
+ if (this.mergeBaseCache.size >= MAX_MERGE_BASES) {
252
+ const oldest = this.mergeBaseCache.keys().next().value;
253
+ if (oldest)
254
+ this.mergeBaseCache.delete(oldest);
255
+ }
256
+ this.mergeBaseCache.set(relPath, content);
257
+ }
258
+ // -- Retry queue (1l) -----------------------------------------------------
259
+ enqueueRetry(relPath, docId, payload) {
260
+ if (this.retryQueue.size >= MAX_RETRY_QUEUE) {
261
+ const oldest = this.retryQueue.keys().next().value;
262
+ if (oldest) {
263
+ this.retryQueue.delete(oldest);
264
+ this.appendDeadLetter(oldest, "evicted");
265
+ }
266
+ }
267
+ this.retryQueue.set(relPath, { docId, payload });
268
+ if (!this.retryTimer) {
269
+ const jitter = Math.random() * 5000;
270
+ this.retryTimer = setInterval(() => this.flushRetryQueue(), 30_000 + jitter);
271
+ }
272
+ this.writeSyncStatus();
273
+ }
274
+ async flushRetryQueue() {
275
+ if (this.flushing)
276
+ return;
277
+ this.flushing = true;
278
+ try {
279
+ const snapshot = [...this.retryQueue.entries()];
280
+ for (const [relPath, { docId, payload }] of snapshot) {
281
+ if (this.abortController.signal.aborted)
282
+ break;
283
+ try {
284
+ await this.options.adapter.set(docId, payload);
285
+ this.retryQueue.delete(relPath);
286
+ if (payload.content) {
287
+ this.lastSyncedHash.set(relPath, hashContent(payload.content));
288
+ this.markRecent(this.recentlyPushed, relPath);
289
+ }
290
+ }
291
+ catch {
292
+ break; // stop on first failure, retry next cycle
293
+ }
294
+ }
295
+ }
296
+ finally {
297
+ this.flushing = false;
298
+ if (this.retryQueue.size === 0 && this.retryTimer) {
299
+ clearInterval(this.retryTimer);
300
+ this.retryTimer = null;
301
+ }
302
+ }
303
+ }
304
+ // -- Dead letter log (1q) -------------------------------------------------
305
+ appendDeadLetter(relPath, reason) {
306
+ const entry = { path: relPath, reason, timestamp: Date.now() };
307
+ const logPath = path.resolve(this.options.contentRoot, ".sync-failures.json");
308
+ try {
309
+ const existing = fs.existsSync(logPath)
310
+ ? JSON.parse(fs.readFileSync(logPath, "utf-8"))
311
+ : [];
312
+ existing.push(entry);
313
+ const trimmed = existing.slice(-200);
314
+ fs.writeFileSync(logPath, JSON.stringify(trimmed, null, 2));
315
+ }
316
+ catch {
317
+ /* best-effort */
318
+ }
319
+ }
320
+ writeDeadLetterLog(reason) {
321
+ for (const relPath of this.retryQueue.keys()) {
322
+ this.appendDeadLetter(relPath, reason);
323
+ }
324
+ }
325
+ // -- Sync status file (1r) ------------------------------------------------
326
+ writeSyncStatus() {
327
+ const status = {
328
+ enabled: true,
329
+ connected: !this.hasError,
330
+ conflicts: this.getConflictPaths(),
331
+ lastSyncedAt: this.lastSyncTimestamp,
332
+ retryQueueSize: this.retryQueue.size,
333
+ failedPaths: [...this.retryQueue.keys()],
334
+ };
335
+ const statusPath = path.resolve(this.options.contentRoot, ".sync-status.json");
336
+ try {
337
+ fs.mkdirSync(path.dirname(statusPath), { recursive: true });
338
+ fs.writeFileSync(statusPath, JSON.stringify(status, null, 2));
339
+ }
340
+ catch {
341
+ /* best-effort */
342
+ }
343
+ }
109
344
  // -- Conflict resolution --------------------------------------------------
110
345
  resolveConflict(filePath, absPath, localContent, remoteContent, ownerId) {
111
- const base = this.lastSyncedContent.get(filePath);
346
+ // Use merge base cache instead of full content (1i)
347
+ const base = this.mergeBaseCache.get(filePath);
112
348
  if (base !== undefined) {
113
349
  const result = threeWayMerge(base, localContent, remoteContent);
114
350
  if (result.success && result.merged !== null) {
@@ -122,21 +358,39 @@ export class FileSync {
122
358
  ownerId,
123
359
  lastUpdated: now,
124
360
  })
125
- .then(() => this.markRecent(this.recentlyPushed, filePath))
126
- .catch((err) => console.error(`[file-sync] Failed to push merged ${filePath}:`, err));
361
+ .then(() => {
362
+ if (this.abortController.signal.aborted)
363
+ return;
364
+ this.markRecent(this.recentlyPushed, filePath);
365
+ this.retryQueue.delete(filePath);
366
+ })
367
+ .catch((err) => {
368
+ console.error(`[file-sync] Failed to push merged ${filePath}:`, err);
369
+ this.enqueueRetry(filePath, this.docId(filePath), {
370
+ path: filePath,
371
+ content: result.merged,
372
+ app: this.options.appId,
373
+ ownerId,
374
+ lastUpdated: now,
375
+ });
376
+ });
127
377
  this.emitSyncEvent({
128
378
  type: "conflict-resolved",
129
379
  path: filePath,
130
380
  strategy: "auto-merge",
131
381
  });
382
+ this.conflictPaths.delete(filePath);
132
383
  console.log(`[file-sync] auto-merged ${filePath}`);
133
384
  return;
134
385
  }
135
386
  }
136
387
  // Auto-merge failed or no base -- write .conflict sidecar
137
- const conflictPath = absPath + ".conflict";
388
+ const projectRoot = path.resolve(this.options.contentRoot, "..");
389
+ const conflictPath = assertSafePath(projectRoot, filePath + ".conflict");
390
+ assertNotSymlink(conflictPath);
138
391
  fs.writeFileSync(conflictPath, remoteContent, "utf-8");
139
392
  console.log(`[file-sync] conflict in ${filePath} - wrote ${filePath}.conflict`);
393
+ this.conflictPaths.add(filePath);
140
394
  this.emitSyncEvent({
141
395
  type: "conflict-saved",
142
396
  path: filePath,
@@ -148,74 +402,115 @@ export class FileSync {
148
402
  localSnippet: localContent.slice(0, 500),
149
403
  remoteSnippet: remoteContent.slice(0, 500),
150
404
  });
405
+ this.writeSyncStatus();
151
406
  }
152
407
  // -- Startup sync ---------------------------------------------------------
153
408
  async initStartupSync(patterns, ownerId, label) {
154
409
  if (patterns.length === 0)
155
410
  return;
156
411
  console.log(`[file-sync:${label}] Running full startup sync...`);
157
- const rows = await this.options.adapter.query(this.options.appId, ownerId);
158
- const docsByPath = new Map();
159
- const orphanedDocIds = [];
160
- for (const row of rows) {
161
- const filePath = row.data.path;
162
- const canonicalId = this.docId(filePath);
163
- if (row.id !== canonicalId) {
164
- orphanedDocIds.push(row.id);
165
- continue;
412
+ // Emit burst start for SSE batching
413
+ this.syncEvents.emit("sync-burst-start");
414
+ try {
415
+ const rows = await this.options.adapter.query(this.options.appId, ownerId);
416
+ const docsByPath = new Map();
417
+ const orphanedDocIds = [];
418
+ // Check for legacy doc ID format
419
+ const legacyDocs = rows.filter((r) => r.id.includes("__"));
420
+ if (legacyDocs.length > 0) {
421
+ console.warn(`[file-sync] Found ${legacyDocs.length} document(s) with legacy '__' separator. ` +
422
+ `These will be treated as orphans. See: https://agent-native.dev/docs/file-sync#migration`);
166
423
  }
167
- docsByPath.set(filePath, row);
168
- }
169
- if (orphanedDocIds.length > 0) {
170
- console.log(`[file-sync:${label}] Cleaning up ${orphanedDocIds.length} orphaned doc(s)...`);
171
- for (const id of orphanedDocIds) {
172
- await this.options.adapter.delete(id).catch(() => { });
424
+ for (const row of rows) {
425
+ const filePath = row.data.path;
426
+ const canonicalId = this.docId(filePath);
427
+ if (row.id !== canonicalId) {
428
+ orphanedDocIds.push(row.id);
429
+ continue;
430
+ }
431
+ docsByPath.set(filePath, row);
173
432
  }
174
- }
175
- const projectRoot = path.resolve(this.options.contentRoot, "..");
176
- let syncedCount = 0;
177
- for (const [filePath, row] of docsByPath) {
178
- const data = row.data;
179
- if (!shouldSyncFile(filePath, patterns))
180
- continue;
181
- const absPath = path.resolve(projectRoot, filePath);
182
- const remoteContent = data.content ?? "";
183
- const localContent = this.readLocalFile(absPath);
184
- if (localContent === null) {
185
- this.writeSyncedFile(filePath, absPath, remoteContent);
186
- syncedCount++;
433
+ // Parallelize orphan cleanup with p-limit (1g)
434
+ const limit = pLimit(this.options.startupConcurrency ?? 10);
435
+ if (orphanedDocIds.length > 0) {
436
+ console.log(`[file-sync:${label}] Cleaning up ${orphanedDocIds.length} orphaned doc(s)...`);
437
+ await Promise.all(orphanedDocIds.map((id) => limit(() => this.options.adapter.delete(id).catch((err) => {
438
+ console.warn(`[file-sync] Failed to delete orphan ${id}:`, err);
439
+ }))));
187
440
  }
188
- else if (localContent !== remoteContent) {
189
- const remoteMs = data.lastUpdated ?? 0;
190
- let localMs = 0;
191
- try {
192
- localMs = fs.statSync(absPath).mtimeMs;
193
- }
194
- catch { }
195
- if (remoteMs > localMs) {
441
+ const projectRoot = path.resolve(this.options.contentRoot, "..");
442
+ let syncedCount = 0;
443
+ // Collect push operations for parallel execution
444
+ const pushOps = [];
445
+ for (const [filePath, row] of docsByPath) {
446
+ if (this.stopped)
447
+ return;
448
+ const data = row.data;
449
+ if (!shouldSyncFile(filePath, patterns))
450
+ continue;
451
+ const absPath = assertSafePath(projectRoot, filePath);
452
+ const remoteContent = data.content ?? "";
453
+ const localContent = this.readLocalFile(absPath);
454
+ if (localContent === null) {
196
455
  this.writeSyncedFile(filePath, absPath, remoteContent);
197
456
  syncedCount++;
198
457
  }
458
+ else if (localContent !== remoteContent) {
459
+ const remoteMs = data.lastUpdated ?? 0;
460
+ let localMs = 0;
461
+ try {
462
+ localMs = fs.statSync(absPath).mtimeMs;
463
+ }
464
+ catch {
465
+ /* file may have been deleted */
466
+ }
467
+ if (remoteMs > localMs) {
468
+ this.writeSyncedFile(filePath, absPath, remoteContent);
469
+ syncedCount++;
470
+ }
471
+ else {
472
+ // Queue push for parallel execution
473
+ const capturedFilePath = filePath;
474
+ const capturedLocalContent = localContent;
475
+ const capturedCreatedAt = data.createdAt;
476
+ pushOps.push(async () => {
477
+ if (this.abortController.signal.aborted)
478
+ return;
479
+ const now = Date.now();
480
+ await this.options.adapter.set(this.docId(capturedFilePath), {
481
+ path: capturedFilePath,
482
+ content: capturedLocalContent,
483
+ app: this.options.appId,
484
+ ownerId,
485
+ lastUpdated: now,
486
+ createdAt: capturedCreatedAt ?? now,
487
+ });
488
+ if (this.abortController.signal.aborted)
489
+ return;
490
+ this.lastSyncedHash.set(capturedFilePath, hashContent(capturedLocalContent));
491
+ this.updateMergeBase(capturedFilePath, capturedLocalContent);
492
+ this.markRecent(this.recentlyPushed, capturedFilePath);
493
+ });
494
+ syncedCount++;
495
+ }
496
+ }
199
497
  else {
200
- const now = Date.now();
201
- await this.options.adapter.set(this.docId(filePath), {
202
- path: filePath,
203
- content: localContent,
204
- app: this.options.appId,
205
- ownerId,
206
- lastUpdated: now,
207
- createdAt: data.createdAt ?? now,
208
- });
209
- this.lastSyncedContent.set(filePath, localContent);
210
- this.markRecent(this.recentlyPushed, filePath);
211
- syncedCount++;
498
+ this.lastSyncedHash.set(filePath, hashContent(localContent));
499
+ this.updateMergeBase(filePath, localContent);
212
500
  }
213
501
  }
214
- else {
215
- this.lastSyncedContent.set(filePath, localContent);
502
+ // Execute pushes in parallel with concurrency limit (1g)
503
+ if (pushOps.length > 0) {
504
+ await Promise.all(pushOps.map((fn) => limit(fn)));
216
505
  }
506
+ this.lastSyncTimestamp = Date.now();
507
+ this.writeSyncStatus();
508
+ console.log(`[file-sync:${label}] Startup sync complete - ${syncedCount} file(s) synced`);
509
+ }
510
+ finally {
511
+ // Always emit burst end — even if startup sync failed
512
+ this.syncEvents.emit("sync-burst-end");
217
513
  }
218
- console.log(`[file-sync:${label}] Startup sync complete - ${syncedCount} file(s) synced`);
219
514
  }
220
515
  // -- Remote -> disk listener ----------------------------------------------
221
516
  startRemoteListener(patterns, ownerId, label) {
@@ -223,7 +518,7 @@ export class FileSync {
223
518
  return;
224
519
  console.log(`[file-sync:${label}] Listening for remote changes...`);
225
520
  const projectRoot = path.resolve(this.options.contentRoot, "..");
226
- this.options.adapter.subscribe(this.options.appId, ownerId, (changes) => {
521
+ const unsub = this.options.adapter.subscribe(this.options.appId, ownerId, (changes) => {
227
522
  for (const change of changes) {
228
523
  const data = change.data;
229
524
  const filePath = data.path;
@@ -232,38 +527,67 @@ export class FileSync {
232
527
  if (change.type === "added" || change.type === "modified") {
233
528
  if (change.id !== this.docId(filePath))
234
529
  continue;
235
- if (this.wasRecent(this.recentlyPushed, filePath))
530
+ // Content-hash dedup instead of TTL-only (1m)
531
+ if (this.wasRecent(this.recentlyPushed, filePath)) {
532
+ const pushedHash = this.lastSyncedHash.get(filePath);
533
+ const incomingHash = hashContent(data.content ?? "");
534
+ if (pushedHash === incomingHash)
535
+ continue; // genuine echo
536
+ // Different content — real remote change, proceed
537
+ }
538
+ let absPath;
539
+ try {
540
+ absPath = assertSafePath(projectRoot, filePath);
541
+ }
542
+ catch (err) {
543
+ console.error(`[file-sync:${label}] Rejected remote path:`, err);
236
544
  continue;
237
- const absPath = path.resolve(projectRoot, filePath);
545
+ }
238
546
  const incoming = data.content ?? "";
239
547
  const local = this.readLocalFile(absPath);
240
548
  if (local === incoming) {
241
- this.lastSyncedContent.set(filePath, incoming);
549
+ this.lastSyncedHash.set(filePath, hashContent(incoming));
550
+ this.updateMergeBase(filePath, incoming);
242
551
  continue;
243
552
  }
244
553
  if (local === null) {
245
554
  this.writeSyncedFile(filePath, absPath, incoming);
246
555
  continue;
247
556
  }
248
- const lastSynced = this.lastSyncedContent.get(filePath);
249
- if (lastSynced === undefined || local === lastSynced) {
557
+ const lastHash = this.lastSyncedHash.get(filePath);
558
+ const localHash = hashContent(local);
559
+ if (lastHash === undefined || localHash === lastHash) {
560
+ // No local changes since last sync — safe to overwrite
250
561
  this.writeSyncedFile(filePath, absPath, incoming);
251
562
  }
252
563
  else {
253
564
  this.resolveConflict(filePath, absPath, local, incoming, ownerId);
254
565
  }
566
+ this.lastSyncTimestamp = Date.now();
567
+ this.writeSyncStatus();
255
568
  }
256
569
  if (change.type === "removed") {
257
- const absPath = path.resolve(projectRoot, filePath);
258
- if (fs.existsSync(absPath)) {
259
- fs.unlinkSync(absPath);
260
- this.lastSyncedContent.delete(filePath);
570
+ let absPath;
571
+ try {
572
+ absPath = assertSafePath(projectRoot, filePath);
261
573
  }
574
+ catch (err) {
575
+ console.error(`[file-sync:${label}] Rejected remote delete path:`, err);
576
+ continue;
577
+ }
578
+ // Use fs.rm with force to eliminate TOCTOU race
579
+ fs.rm(absPath, { force: true }, () => { });
580
+ this.lastSyncedHash.delete(filePath);
581
+ this.mergeBaseCache.delete(filePath);
582
+ this.retryQueue.delete(filePath);
262
583
  }
263
584
  }
264
585
  }, (err) => {
265
586
  console.error(`[file-sync:${label}] Remote listener error:`, err);
587
+ this.hasError = true;
588
+ this.writeSyncStatus();
266
589
  });
590
+ this.unsubscribeRemote.push(unsub);
267
591
  }
268
592
  // -- Disk -> remote watcher -----------------------------------------------
269
593
  startFileWatcher(patterns, ownerId, label) {
@@ -274,17 +598,26 @@ export class FileSync {
274
598
  const watcher = watch(this.options.contentRoot, {
275
599
  ignoreInitial: true,
276
600
  });
601
+ this.watchers.push(watcher);
277
602
  const handleChange = async (absPath) => {
278
603
  const relPath = path.relative(projectRoot, absPath);
279
604
  if (!shouldSyncFile(relPath, patterns))
280
605
  return;
281
- if (this.wasRecent(this.recentlyPulled, relPath))
606
+ // Use expectedWrites Set for echo suppression (1o)
607
+ if (this.wasSyncPulled(relPath))
608
+ return;
609
+ // Per-file push serialization (1h) — wait for in-flight push
610
+ const prior = this.pushInFlight.get(relPath);
611
+ if (prior)
612
+ await prior;
613
+ if (this.abortController.signal.aborted)
282
614
  return;
283
615
  const content = this.readLocalFile(absPath);
284
616
  if (content === null)
285
617
  return;
286
- const existing = await this.options.adapter.get(this.docId(relPath));
287
- if (existing && existing.data?.content === content)
618
+ // Content hash comparison instead of adapter.get() (1h)
619
+ const hash = hashContent(content);
620
+ if (this.lastSyncedHash.get(relPath) === hash)
288
621
  return;
289
622
  const now = Date.now();
290
623
  const payload = {
@@ -294,17 +627,27 @@ export class FileSync {
294
627
  ownerId,
295
628
  lastUpdated: now,
296
629
  };
297
- if (!existing) {
298
- payload.createdAt = now;
299
- }
300
- this.options.adapter
630
+ const pushPromise = this.options.adapter
301
631
  .set(this.docId(relPath), payload)
302
632
  .then(() => {
303
- this.lastSyncedContent.set(relPath, content);
633
+ if (this.abortController.signal.aborted)
634
+ return;
635
+ this.lastSyncedHash.set(relPath, hash);
636
+ this.updateMergeBase(relPath, content);
304
637
  this.markRecent(this.recentlyPushed, relPath);
638
+ this.retryQueue.delete(relPath);
639
+ this.lastSyncTimestamp = Date.now();
305
640
  console.log(`[file-sync:${label}] -> pushed ${relPath}`);
306
641
  })
307
- .catch((err) => console.error(`[file-sync:${label}] Failed to push ${relPath}:`, err));
642
+ .catch(() => {
643
+ this.enqueueRetry(relPath, this.docId(relPath), payload);
644
+ })
645
+ .finally(() => {
646
+ if (this.pushInFlight.get(relPath) === pushPromise) {
647
+ this.pushInFlight.delete(relPath);
648
+ }
649
+ });
650
+ this.pushInFlight.set(relPath, pushPromise);
308
651
  };
309
652
  const handleDelete = (absPath) => {
310
653
  const relPath = path.relative(projectRoot, absPath);
@@ -313,7 +656,9 @@ export class FileSync {
313
656
  this.options.adapter
314
657
  .delete(this.docId(relPath))
315
658
  .then(() => {
316
- this.lastSyncedContent.delete(relPath);
659
+ this.lastSyncedHash.delete(relPath);
660
+ this.mergeBaseCache.delete(relPath);
661
+ this.retryQueue.delete(relPath);
317
662
  console.log(`[file-sync:${label}] -> deleted ${relPath}`);
318
663
  })
319
664
  .catch((err) => console.error(`[file-sync:${label}] Failed to delete ${relPath}:`, err));